前面讲了 Deep Research Agent 的数据构造方法,SailorFog-QA 用知识图谱生成复杂问题,WebFrontier 迭代升级问题复杂度。但有了问题之后,还差最关键的一步:生成模型能学习的"搜索轨迹"。

问题是静态的文本,轨迹是动态的行为序列,模型面对这个问题时,第一步搜了什么,看到结果后第二步又搜了什么,什么时候决定停下来给答案。这套完整的决策过程,才是 SFT 阶段模型真正要学的东西。

轨迹从哪来?让一个更强的 Teacher 模型(比如 DeepSeek V3 或 GPT-4)替我们"示范"一遍,记录下它的完整搜索过程,就得到了一条轨迹数据。

但问题在于:Teacher 也会犯错。

上周有个学员去鹅厂面,简历上写了"构建了 1200 条高质量轨迹数据用于 SFT 训练"。面试官看完直接问:

“1200 条轨迹,怎么生成的?”

他说:“用 DeepSeek V3 作为 Teacher 模型,给它种子问题,让它调工具搜索,记录完整轨迹。”

面试官皱眉:“那 Teacher 模型搜错了怎么办?比如它第一步搜了个不相关的关键词,后面全偏了,这种轨迹你也拿来训练?”

他停了一下:“……应该过滤掉了吧。”

面试官追问:“怎么过滤的?你的过滤标准是什么?只看最终答案对不对,还是每一步搜索行为都检查?”

他答不上来。

面试官说了一句:“轨迹数据和普通的 QA 数据不一样。QA 数据只需要答案对就行,但轨迹数据的每一步都在教模型’遇到这种情况应该怎么做’。一步搜错了,模型就学会了这个错误的搜索策略。你的过滤体系决定了 SFT 的上限。”

今天把轨迹采样的完整流程拆开讲:Teacher 模型怎么调用,生成的轨迹怎么过滤,最终怎么标准化成可用的训练数据。

Teacher 模型调用:不是简单地"让它搜一搜"

选 Teacher 模型有两个基本要求:第一,它的搜索能力必须显著强于你要训练的 Student 模型(否则学不到东西)。第二,它必须支持你定义的工具格式(或者你愿意做格式转换)。

我们用的是 DeepSeek V3 作为 Teacher。给它一个 System Prompt,定义清楚工具列表和输出格式,然后传入种子问题让它自主搜索。

TEACHER_SYSTEM_PROMPT = """你是一个深度研究助手,能够通过多轮搜索回答复杂问题。 可用工具: 1. search(query: str) - 搜索引擎查询 2. visit(url: str) - 访问网页获取正文 3. scholar(query: str) - 学术论文搜索 4. finish(answer: str) - 输出最终答案 规则: - 每次行动前先用 <think> 标签分析当前状态和下一步计划 - 工具调用用 <tool_call> 标签包裹,内容为合法 JSON - 搜索策略:先广后深,先确定大方向再细化 - 遇到矛盾信息时,交叉验证后取更可靠来源 - 最终答案必须标注引用来源 [1][2]... - 如果问题可以直接回答(常识题),直接 finish,不需要搜索 """

这里有个关键设计:System Prompt 里要明确告诉 Teacher “简单问题可以不搜索”。如果不加这条,Teacher 会对所有问题都走完整的搜索流程——包括"水的化学式是什么"这种。生成的轨迹数据里搜索类占比过高,Student 学完之后对任何问题都条件反射式搜索,简单题效率极低。

我们第一批数据就犯了这个错。200 条轨迹里 95% 都有搜索动作,Student 训完之后遇到"1+1=?"也要先 search 一下。排查了半天才意识到是 Teacher Prompt 的问题。

轨迹生成脚本

import json import asyncio from pathlib import Path async def generate_trajectory(     question: str,     teacher_model: str = "deepseek-v3",     max_steps: int = 15,     timeout: int = 120 ) -> dict:     """     调用 Teacher 模型生成一条完整轨迹。     为什么要设 max_steps=15?     Teacher 模型偶尔也会陷入循环搜索。     没有步数限制的话,我们见过 Teacher 搜了 30+ 步     还在转圈——这种轨迹不但没用,token 费用还爆了。     """     messages = [         {"role": "system", "content": TEACHER_SYSTEM_PROMPT},         {"role": "user", "content": question}     ]     trajectory_steps = []     step_count = 0     while step_count < max_steps:         # 调用 Teacher 模型         response = await call_llm(teacher_model, messages, temperature=0.3)         assistant_msg = response["content"]         trajectory_steps.append({"role": "assistant", "content": assistant_msg})         # 检查是否调用了 finish         if "<tool_call>" in assistant_msg:             tool_call = extract_tool_call(assistant_msg)             if tool_call["tool"] == "finish":                 break             # 执行工具调用,获取真实结果             observation = await execute_tool(tool_call)             trajectory_steps.append({"role": "tool", "content": observation})             messages.append({"role": "assistant", "content": assistant_msg})             messages.append({"role": "tool", "content": observation})         else:             # 没有工具调用也没有 finish,异常退出             break         step_count += 1     return {         "question": question,         "trajectory": trajectory_steps,         "steps": step_count,         "status": "completed" if step_count < max_steps else "truncated"     }

注意 temperature=0.3。Teacher 生成轨迹时我们用低温度,因为这里需要的是高质量的"标准示范",不需要多样性。多样性通过多个种子问题来保证,不通过单个问题的随机采样来保证。

三阶段漏斗式过滤:从粗到细筛选轨迹

生成 2000 条轨迹,最终能用的可能只有 1200 条。过滤率大约 40%。这不是浪费,用 60% 的 token 成本换取数据质量的保证,在 SFT 阶段的收益是数倍的。

我们的过滤分三个阶段,每个阶段筛掉不同类型的问题。

轨迹数据三阶段漏斗过滤

阶段一:格式验证(全自动,成本几乎为零)

这是最基础的一层,检查轨迹的结构是否合法。

def stage1_format_check(trajectory: dict) -> tuple[bool, str]:     """     阶段一:格式合规性检查。     通过率约 90%,筛掉的都是 Teacher 模型的"手滑"。     """     steps = trajectory["trajectory"]     for i, step in enumerate(steps):         if step["role"] == "assistant":             content = step["content"]             # 检查 think 标签配对             think_open = content.count("<think>")             think_close = content.count("</think>")             if think_open != think_close:                 return False, f"Step {i}: think 标签不配对"             # 检查工具调用 JSON 合法性             if "<tool_call>" in content:                 tc_content = extract_between(content, "<tool_call>", "</tool_call>")                 try:                     tc_json = json.loads(tc_content)                     # 检查工具名是否在允许列表中                     if tc_json.get("tool") not in ["search", "visit", "scholar", "finish"]:                         return False, f"Step {i}: 未知工具名 {tc_json.get('tool')}"                 except json.JSONDecodeError:                     return False, f"Step {i}: JSON 解析失败"     # 检查轨迹是否以 finish 结束     last_assistant = [s for s in steps if s["role"] == "assistant"][-1]     if "finish" not in last_assistant["content"]:         return False, "轨迹未以 finish 结束"     return True, "格式检查通过"

阶段二:答案正确性验证(自动化,需要 Ground Truth)

格式对了不代表内容对。Teacher 可能格式完美但答案完全错误。这一步对比 Teacher 的最终答案和标准答案。

def stage2_answer_check(trajectory: dict, ground_truth: str,                         question_type: str) -> tuple[bool, float, str]:     """     阶段二:答案正确性验证。     通过率约 85%(在格式合规的基础上)。     为什么不只看 exact match?     因为 Teacher 的回答往往是长文本,     和标准答案不可能逐字一致。     需要判断语义是否一致。     """     answer = extract_final_answer(trajectory)     if question_type == "factual":         # 事实题:F1 匹配,阈值 0.6         f1 = compute_f1(answer, ground_truth)         if f1 < 0.6:             return False, f1, f"F1={f1:.2f} 低于阈值 0.6"         return True, f1, "答案正确"     elif question_type == "research":         # 研究题:LLM 判断语义一致性         score = llm_judge_consistency(answer, ground_truth)         if score < 0.7:             return False, score, f"语义一致性={score:.2f} 低于 0.7"         return True, score, "答案正确"     # 引用一致性额外检查     citations_valid = check_citation_consistency(trajectory)     if not citations_valid:         return False, 0.0, "引用与搜索结果不一致"     return True, 1.0, "答案正确"

引用一致性检查是容易被忽略的一点。Teacher 生成的答案里写了"根据[3]",但实际轨迹里第 3 次搜索的结果跟答案里引用的内容对不上,这种"幻觉引用"在 Teacher 输出里大约占 8-12%。如果不过滤掉,Student 会学会"随便编一个引用编号"的坏习惯。

阶段三:行为质量评估(半自动,最关键的一步)

答案对了,不代表搜索过程是高效的。Teacher 可能搜了 10 步才找到答案,但其中 6 步是重复搜索或无效探索。这种轨迹虽然"最终结果正确",但搜索策略很差,Student 如果学了这套策略,效率会很低。

def stage3_behavior_quality(trajectory: dict) -> tuple[bool, float, str]:     """     阶段三:搜索行为质量评估。     通过率约 75%(在答案正确的基础上)。     这是最严格的一层,也是数据质量的核心保障。     """     steps = extract_tool_calls_from_trajectory(trajectory)     search_queries = [s for s in steps if s["tool"] == "search"]     issues = []     score = 1.0     # 1. 重复搜索检测:相邻搜索的 query 相似度 > 0.85 视为重复     for i in range(1, len(search_queries)):         sim = compute_similarity(search_queries[i-1]["query"],                                 search_queries[i]["query"])         if sim > 0.85:             score -= 0.2             issues.append(f"Step {i}: 与上一步搜索高度重复 (sim={sim:.2f})")     # 2. 步骤效率:总步数与问题复杂度是否匹配     expected_steps = estimate_optimal_steps(trajectory["question"])     actual_steps = len(steps)     if actual_steps > expected_steps * 2:         score -= 0.3         issues.append(f"步骤过多: 期望{expected_steps}步, 实际{actual_steps}步")     # 3. 信息增量:每步搜索是否带来新信息     observations = [s["observation"] for s in steps if "observation" in s]     for i in range(1, len(observations)):         info_gain = compute_info_gain(observations[i], observations[:i])         if info_gain < 0.1:             score -= 0.1             issues.append(f"Step {i}: 信息增量极低 ({info_gain:.2f})")     # 4. 搜索策略合理性:是否先广后深     # 好的策略:先用宽泛关键词确定方向,再用精确关键词深入     strategy_score = evaluate_search_strategy(search_queries)     score += strategy_score * 0.2 - 0.1  # 奖励好策略,惩罚差策略     passed = score >= 0.5     reason = "行为质量合格" if passed else f"行为质量不足: {'; '.join(issues)}"     return passed, score, reason

阶段三是"半自动"的。自动化规则能筛掉大部分明显的低质量轨迹,但有些边界 case 需要人工确认,比如 Teacher 搜了一个看起来不相关的关键词,但实际上是一种"迂回搜索"策略,最终搜到了直接搜搜不到的结果。这种轨迹自动化规则可能误杀,我们会抽样 10% 做人工复核。

格式标准化:对齐 Student 模型的原生格式

通过三阶段过滤之后,还有最后一步:把 Teacher 模型的输出格式转换成 Student 模型能直接训练的格式。

如果 Teacher 是 DeepSeek V3,Student 是 Qwen3,两者的特殊 token 和对话模板不同。必须做格式对齐。

def standardize_trajectory(raw_trajectory: dict, target_model: str = "qwen3") -> dict:     """     将 Teacher (DeepSeek V3) 的轨迹格式转换为 Student (Qwen3) 的格式。     转换规则:     - DeepSeek 的 <think> 映射到 Qwen3 的 <think>(恰好相同)     - 工具调用格式统一为 <tool_call>{"tool": ..., "args": ...}</tool_call>     - observation 统一用 role="tool" 表示     - 最终答案统一用 <answer>...</answer> 包裹     """     standardized_messages = []     for step in raw_trajectory["trajectory"]:         if step["role"] == "assistant":             content = step["content"]             # 确保 think 标签规范             content = normalize_think_tags(content)             # 确保 tool_call 格式统一             content = normalize_tool_call_format(content)             standardized_messages.append({                 "role": "assistant",                 "content": content             })         elif step["role"] == "tool":             # 截断过长的 observation(防止训练时 OOM)             obs = step["content"]             if len(obs) > 2000:                 obs = obs[:2000] + "\n[内容已截断]"             standardized_messages.append({                 "role": "tool",                 "content": obs             })     return {         "messages": [             {"role": "system", "content": STUDENT_SYSTEM_PROMPT},             {"role": "user", "content": raw_trajectory["question"]},             *standardized_messages         ],         "metadata": {             "source": "teacher_deepseek_v3",             "steps": raw_trajectory["steps"],             "quality_score": raw_trajectory.get("quality_score", 0.0)         }     }
```![](http://cdn.zhipoai.cn/21ce6ce2.jpg)

轨迹数据生产流水线

### Observation 截断:一个容易被忽视的细节

Teacher 搜索时,某些网页返回的正文可能有上万字。如果不截断,一条轨迹的总 token 量可能超过 20K,训练时要么 OOM 要么被 `max_length` 截断。

但截断也不能乱截。我们的策略是:

```plaintext
def truncate_observation(obs: str, max_chars: int = 2000,                          question: str = "") -> str:     """     智能截断 observation。     不是简单地取前 2000 字,而是保留与问题最相关的段落。     为什么要保留相关段落而不是开头?     因为网页开头往往是导航栏和广告,正文在中间。     如果截取开头,可能连有用信息都没截到。     """     if len(obs) <= max_chars:         return obs     # 按段落切分     paragraphs = obs.split("\n\n")     # 对每个段落计算与问题的相关性     if question:         scored_paras = [             (p, compute_relevance(p, question))             for p in paragraphs if len(p.strip()) > 20         ]         # 按相关性排序,取前 N 个段落(总长度不超限)         scored_paras.sort(key=lambda x: x[1], reverse=True)     else:         scored_paras = [(p, 0) for p in paragraphs]     result = []     current_len = 0     for para, _ in scored_paras:         if current_len + len(para) > max_chars:             break         result.append(para)         current_len += len(para)     return "\n\n".join(result) + "\n[内容已截断,保留与问题最相关的段落]"

数据增强:从 200 条种子到 1200 条轨迹

前面提到种子问题只有 200 条。要生成 1200 条轨迹,需要做数据增强。我们用三种策略:

问题改写(×2 扩展): 用 LLM 生成同一问题的不同表述方式。

REWRITE_PROMPT = """ 对以下问题生成 2 种不同的问法,保持含义不变但措辞完全不同: 原问题:{question} 要求: 1. 第一种改写:换一种语气(比如从"请问"变成"我想了解") 2. 第二种改写:换一个切入角度(比如从"是什么"变成"为什么"或"怎么做") """

参数变化(×2 扩展): 修改问题中的具体参数,时间、地点、金额、公司名等。

组合扩展(×1.5 扩展): 将两个简单问题组合成一个复合问题,强制 Agent 需要更多搜索步骤。

三种策略叠加:200 × 2 × 2 × 1.5 = 1200 条种子问题。每条种子问题生成一条轨迹,经过三阶段过滤后,实际可用约 1200 条(因为原始生成约 2000 条,过滤率约 40%)。

这套轨迹数据生产流水线的完整工程实现,是我们训练营 Deep Research Agent 项目里从"空白模型"到"能调工具"的关键桥梁。学员不只是学数据格式,而是真的跑过 Teacher 生成、三阶段过滤、格式标准化的完整 pipeline——包括"Teacher 生成了一条全是重复搜索的轨迹"这种 badcase 怎么被过滤掉的。

轨迹质量对比:高质量 vs 低质量

数据分布控制:别让模型"偏科"

最后一个常被忽略的问题:1200 条轨迹的分布是否均匀。如果数据里 80% 都是"搜索→回答"的简单模式,模型就学不会"搜索→反思→换方向→再搜索"的复杂策略。

我们按三个维度控制分布:

维度 分布要求 为什么
搜索步数 1-3步占30%、4-7步占40%、8+步占30% 让模型既学会快速回答,也学会深入研究
工具类型 search占50%、visit占25%、scholar占15%、finish_only占10% 防止只学search,不会用其他工具
问题类型 事实题30%、研究题40%、计算题15%、直接回答题15% 让模型学会判断何时搜索何时直接答

"直接回答题"占比 15% 是个重要细节。我们前面提到第一版数据里没有这类样本,导致模型对所有问题都搜索。加入 15% 的"常识题直接 finish"样本之后,模型学会了判断哪些问题不需要搜索。

def validate_distribution(trajectories: list[dict]) -> dict:     """     验证轨迹数据分布是否合理。     如果某个维度偏差过大,需要补充生成或剔除过多的类型。     """     # 统计步数分布     step_dist = {"short": 0, "medium": 0, "long": 0}     for t in trajectories:         steps = t["steps"]         if steps <= 3:             step_dist["short"] += 1         elif steps <= 7:             step_dist["medium"] += 1         else:             step_dist["long"] += 1     total = len(trajectories)     report = {         "total": total,         "step_distribution": {k: v/total for k, v in step_dist.items()},         "issues": []     }     # 检查偏差     if step_dist["short"] / total > 0.5:         report["issues"].append("短轨迹占比过高,需要补充复杂问题")     if step_dist["long"] / total < 0.15:         report["issues"].append("长轨迹不足,需要增加研究类问题")     return report

面试怎么答轨迹数据采样?

先说流程概览(30秒):

“轨迹数据的生产是一个四步流水线:种子问题构造、数据增强扩量、Teacher 模型生成轨迹、三阶段质量过滤。最终从 200 条种子问题得到约 1200 条高质量轨迹用于 SFT。”

再说过滤体系(1分钟):

“三阶段过滤是核心。第一阶段格式验证,自动化检查 JSON 合法性和标签配对,通过率约 90%。第二阶段答案正确性,对比 Ground Truth 用 F1 或 LLM Judge 评分,通过率约 85%。第三阶段行为质量,检查搜索是否有重复、步骤是否冗余、信息增量是否充足,通过率约 75%。三层叠加后总通过率约 60%。”

最后说关键教训(30秒):

“踩过两个坑:一是 Teacher Prompt 里没写’简单问题可以不搜索’,导致数据里 search 动作占比过高,Student 对任何问题都搜索。二是 observation 没做截断,训练时超长轨迹被 max_length 截断,模型学不到完整的 finish 动作。加了智能截断后,保留与问题最相关的段落,问题解决。”

今天这道题,只是大模型面试中 Agent 训练数据工程的一个切面。

真正的面试官不会只问这一问。他们会顺着你的回答追下去,追到你答不上来为止,判断的就是你到底做没做过这个系统。

背答案的人和真正做过的人,说话方式完全不一样。前者说"用 Teacher 模型生成训练数据",后者说"我们第一批数据里 95% 都带搜索动作,Student 训完之后对’1+1=?‘也要先 search 一下,后来在 Teacher Prompt 里加了’常识题可以直接 finish’,并且数据里强制加入 15% 的直接回答样本才解决"。

学AI大模型的正确顺序,千万不要搞错了

🤔2026年AI风口已来!各行各业的AI渗透肉眼可见,超多公司要么转型做AI相关产品,要么高薪挖AI技术人才,机遇直接摆在眼前!

有往AI方向发展,或者本身有后端编程基础的朋友,直接冲AI大模型应用开发转岗超合适!

就算暂时不打算转岗,了解大模型、RAG、Prompt、Agent这些热门概念,能上手做简单项目,也绝对是求职加分王🔋

在这里插入图片描述

📝给大家整理了超全最新的AI大模型应用开发学习清单和资料,手把手帮你快速入门!👇👇

学习路线:

✅大模型基础认知—大模型核心原理、发展历程、主流模型(GPT、文心一言等)特点解析
✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑
✅开发基础能力—Python进阶、API接口调用、大模型开发框架(LangChain等)实操
✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用
✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代
✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经

以上6大模块,看似清晰好上手,实则每个部分都有扎实的核心内容需要吃透!

我把大模型的学习全流程已经整理📚好了!抓住AI时代风口,轻松解锁职业新可能,希望大家都能把握机遇,实现薪资/职业跃迁~

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

在这里插入图片描述

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐