项目二:Research Agent(调研 Agent)
难度:⭐⭐☆ 前置:项目一 + 04、05 章 目标:一个能"自己查资料、综合信息、给出带出处的调研结论"的 Agent。
项目一练的是 tool loop;项目二练的是让 Agent 围绕证据工作。调研 Agent 最重要的产物不是"答案",而是"证据"——如果结论追不回来源,那它和会编故事的聊天机器人没有本质区别。
一、从一个问题开始
拿"对比 A、B 两款产品的续航"做全文主线。它是个好样本:要查两个产品;A 可能在知识库里、B 可能要联网;答案必须带出处;还可能查不到(比如 B 的最新价格),甚至两个来源打架。规划、工具选择、上下文控制、证据、冲突和拒答,全在这一题里。
先看终点长什么样——一条调研轨迹加一份带出处的报告片段:
Plan: ① 查 A 续航 ② 查 B 续航 ③ 对比汇总
Action: search_kb("A 产品 续航 参数") → 命中规格文档
Action: search_kb("B 产品 续航 参数") → 库里没有
Thought: 知识库查不到 B,改联网
Action: web_search("B 产品 续航 官方") → 拿到官网数据
Answer: A 续航 12h(来源:S1);B 续航 10h(来源:S2),A 更持久。## 结论摘要
A 标准模式续航约 12 小时,长于 B 的 10 小时(来源:S1、S2)。
## 证据与来源
- S1:A 产品规格说明 / docs/a-product.md / 2026-08-01 / "标准模式约 12 小时"
- S2:B 官网 / https://b.example.com / 2026-09-01 / "续航 10 小时"
## 未确认项
- B 最新价格:未找到可信来源整篇要回答五个问题,它们也恰好是调研 Agent 的五个关节:
- Agent 怎么决定查什么(规划)?
- 怎么决定什么时候查、查几次(按需检索)?
- 怎么不让上下文随检索越撑越大?
- 查不到或来源打架时,怎么保持诚实?
- 怎么把过程变成可追溯的报告?
为了先把最小闭环跑通,第二节先简化成只用知识库、单产品"总结 A 产品的核心限制",讲清几个动作;再回到双产品对比讲工具切换;最后看冲突、拒答和报告。
二、最小闭环:一次按需检索的几个动作
复用项目一的 tool loop,但工具箱从 get_weather 换成检索类,循环外面再套一层"规划"。最小闭环只用 search_kb,跑通"提问 → 规划 → 按需检索 → 带出处作答"。记住一条总纲:把"查资料"从固定流程变成 Agent 的一个决策——这是 把 RAG 做成 Agent 的工具 的核心。
动作一:规划,把问题拆成子问题清单
复杂调研一次查不全,要么先拆、要么靠模型边查边想。这里有个取舍:
- 无显式规划,模型在 loop 里自主决定下一步 — 灵活、少代码:长任务容易跑偏、重复查、漏子问题 / 代价是轨迹不可预期。
- 先生成结构化子问题清单再逐个查 — 可控、可追踪、好评估:计划写空话或子问题过多会增成本 / 代价是要校验计划可执行。
- 两段式(先粗规划,执行中允许追加子问题) — 兼顾:实现复杂 / 代价是状态管理。
选先生成短清单(2-5 个子问题)再逐个查,并设 stop 条件——每个子问题至少有一个可信来源、或明确未找到就停。理由是调研任务要能复盘"查了哪些子问题、每个有没有来源";纯自主轨迹对学习项目太难评估。规划的完整机制见 任务规划与分解。
动作二:决定是否检索,而不是逢问必查
用户问"你好"或"帮我润色一句话"时,不该检索。但一个朴素实现是:每次用户一开口就先 RAG 检索再回答。这里有个取舍:
- 固定前置 RAG(每次都检索再回答) — 实现简单、不漏检:闲聊和改写也检索,引入无关噪声 / 代价是上下文变脏、成本上升。
- 工具化 RAG(把
search_kb/web_search做成工具,模型按需调) — 只在需要时检索、能多查能换路:依赖模型判断准不准 / 代价是触发逻辑要调。 - 混合(规则先判断是否检索,再工具化) — 更稳:多一层逻辑 / 代价是复杂度。
选工具化。本项目的核心练习就是"把查资料变成 Agent 的一个决策"而不是固定流水线——这也是为什么 把 RAG 做成 Agent 的工具 在 05 章单独成篇。
动作三:检索工具返回结构化证据,不是网页全文
search_kb 返回的不是一大段网页/文档全文,而是结构化证据片段。每条证据长这样:
{
"id": "kb-003",
"source_type": "kb",
"title": "A 产品规格说明",
"url": "docs/a-product.md",
"snippet": "A 产品在标准模式下续航约 12 小时。",
"updated_at": "2026-08-01",
"score": 0.82
}这里有个取舍:
- 把网页/文档全文塞给模型 — 信息全:上下文爆、模型抓不住重点、引用无法追溯 / 代价是成本和质量双输。
- 结构化证据对象(id / source_type / title / url / snippet / updated_at / score) — 可引用、可去重、可追溯:要写提取逻辑 / 代价是实现量。
选结构化证据。报告里用 S1、S2 引用编号,每个事实能追到具体来源对象——这是"带出处回答"的最小前提。证据筛选和压缩见上下文选择与压缩,核验方法见证据约束、引用与拒答。
动作四:每轮只放最相关片段进上下文
一次搜索几千 token,连续十次上下文就爆。所以工具只返回短片段,但还要在 messages 层控制总量。这里有个取舍:
- 不压缩,全量塞 — 实现 0:几轮后超窗口、成本飙升 / 代价是任务失败或被截断丢信息。
- 工具层只返回短片段 + 去重 + 每子问题限条数 — 可控:丢部分细节 / 代价是要设计证据结构。
- 摘要压缩历史(把旧检索结果摘要后替换) — 省 token:摘要会丢来源 / 代价是引用断裂。
选工具层就只返回结构化短片段 + 去重 + 每子问题限 3-5 条,报告只引用证据编号、不回放检索过程。不在模型层做激进摘要,避免丢掉出处。这是 上下文工程 的核心命题:每轮放进"恰好够用"的信息。
动作五:带出处作答,查不到就拒答
模型拿到证据后,要么继续查别的子问题,要么给出带出处的结论。这里有个最容易毁掉可信度的取舍:
- 查不到就用常识补 — 回答流畅:不可追溯、可能错、破坏"带出处"承诺 / 代价是可信度崩塌。
- 直接拒答"未找到"并列已查范围 — 诚实:体验偏生硬 / 代价是显得能力不足。
- 拒答 + 说明已查范围 + 给出可补充查询 — 诚实且可行动:多几行 / 代价是模板化。
选拒答并标注未确认项,区分"未找到"和"找到但冲突"。调研 Agent 的价值在于"知道什么不知道";用常识补洞,会让所有带出处的结论都失去可信度。拒答和未确认项怎么评估,见 RAG 评估。
最小闭环的骨架
把几个动作拼起来,其实就是项目一的循环加了检索工具和证据收集(完整设计见 把 RAG 做成 Agent 的工具):
plan = planner(user_question) # 动作一:拆子问题
evidence = [] # 收集证据,供报告引用
for turn in range(max_turns):
response = call_model(messages, tools=[search_kb, web_search])
messages.append(response.message)
if not response.message.tool_calls:
return answer_with_citations(response.message.content, evidence) # 动作五
for call in response.message.tool_calls:
result = safe_run_tool(call) # 动作三:返回证据片段,不是全文
evidence.extend(result["evidence"]) # 收集,给报告引用
messages.append(tool_msg(call.id, result)) # 动作四:只放短片段进上下文读这段骨架时注意:检索工具返回的是结构化证据,不是网页原文;证据收集起来,最后报告只引用编号——这就是为什么前面坚持不让全文进上下文。
三、回到主线:多方查证与工具切换
现在把问题换回"对比 A、B 两款产品的续航"。规划先把它拆成三个子问题:查 A 续航、查 B 续航、对比汇总。这一题的看点不在"会不会调工具",而在一条路走不通时会不会换路。
轨迹长这样:
search_kb("A 产品 续航") → 命中规格文档,拿到 S1(12h)
search_kb("B 产品 续航") → 库里没有
→ 改用 web_search("B 产品 续航 官方") → 拿到官网,S2(10h)
→ 两个子问题都有来源,停止,汇总这里隐含一个路由策略:什么时候查知识库,什么时候联网。可以给 Agent 一个简单起点:
| 场景 | 优先工具 |
|---|---|
| 内部政策、产品说明、课程笔记 | search_kb |
| 最新新闻、官网参数、外部资料 | web_search |
| 知识库无结果或低置信度 | 再尝试 web_search |
| 用户只是闲聊或改写文字 | 不检索 |
但策略不是死的——B 在知识库查不到时,模型自己决定换 web_search,这比写死"先 KB 后联网"更接近一个真正的调研员。关键是要在 trace 里能看出它为什么换路:是因为知识库无结果,还是因为置信度低。换路和重试的判断,本质还是 把 RAG 做成 Agent 的工具 里说的"按需检索"。
到这一步,主线问题已经能完整跑通,且每条结论都带出处。但调研 Agent 真正的考验,在证据打架或根本查不到的时候。
四、当证据打架或不够
用两种情况当第四轮轨迹。它们不是边角料,而是决定调研 Agent 可不可信的关键。
来源冲突:A 的续航,内部规格文档写 12 小时,但官网页面写 10 小时。怎么处理直接决定报告可信度。这里有个取舍:
- 让模型"平均一下"或选一个 — 报告简洁:丢掉不确定性,读者无法判断该信谁 / 代价是可信度受损。
- 强行合并成一个值 — 看着干净:掩盖冲突,可能给错结论 / 代价是误导。
- 标出冲突(双方值 / 来源 / 时间 / 采用依据) — 诚实可复核:报告变长 / 代价是写冲突逻辑。
选标出冲突,保留不确定性:
内部文档显示 A 续航为 12 小时,但官网页面写的是 10 小时。
两者更新时间不同(文档 2026-08-01,官网 2026-09-01),
建议以官网为准或人工确认。调研 Agent 的诚实比流畅更重要;冲突要写清双方来源和时间,让读者或人工裁决。这是证据约束、引用与拒答里证据核验的核心。
查不到:问"B 产品的最新价格",知识库和网页都没有可信来源。这时模型最容易犯的错,是用预训练常识编一个数字。正确的做法是老实说"当前资料未找到",并列出已查询范围,把它放进报告的"未确认项"。区分"拒答正确"和"回答得不完整"——不能把任何短回答都算通过,见 RAG 评估。
如果还想提升检索质量:向量检索的 top-k 里,相关文档不一定排最前。要不要加 rerank,是个取舍:
- 不 rerank,直接用向量 top-k — 快、便宜:相关结果可能排后被截断丢弃 / 代价是检索质量上限低。
- 加 cross-encoder rerank — 排序质量提升:加一次模型调用成本和延迟 / 代价是要评估是否值得。
建议先不加 rerank 跑通基线,再用同一批 query 对比 rerank 前后的正确证据排名、引用覆盖率和成本。只有当排序提升确实改善了最终任务结果(而不只是 top-1 看着更像),才保留。具体方法见Reranker:候选重排。
五、把调研变成可复盘的报告
一个能跑的调研 Agent,如果报告只是一段流畅文字,那它还不可信——读者没法复核。要变成可复盘的工程,报告至少要有四块:
- 结论摘要:3-5 句话回答用户问题,重要判断带来源编号。
- 关键信息:每个事实点后面跟
S1、S2等来源标识。 - 证据与来源:每个来源列标题 / 路径或链接 / 更新时间 / 相关片段。
- 未确认项:哪些没找到、哪些来源冲突、哪些只是基于当前证据的有限推断。
如果结论里没有任何不确定性,通常反而不真实。
和项目一一样,调研过程也要留结构化日志,每轮至少记 query、工具、命中证据 ID、token、延迟。这里同样有个取舍:日志要不要一上来就接 tracing 平台——选先输出结构化 JSON 行,字段对齐 08 章成本口径(input / cached / output / total 分开),等想清楚要统计什么再决定接哪个平台,而不是一开始就被绑住。可观测性的完整设计见 可观测性。
有了结构化报告和日志,就能回答三个复盘问题:哪一次检索是浪费的、哪条结论缺来源、上下文是不是随轮次线性膨胀。能回答这三个,这个调研 Agent 就从"能答"变成了"能解释、能优化、能拒答"——而它复用的正是项目一的 tool loop 底座,下一站会被项目三当成"调研工人"挂进多 Agent 流水线。