深度研究助手是检验 Agent 工程能力的好项目:它需要规划、检索、并行、证据管理、写作和引用校验,几乎覆盖了前面所有章节。
目标与边界
目标:给定一个开放问题,产出 800–1500 字、带可核对引用的研究报告。
不做:
- 不提供投资/医疗建议;
- 不访问付费墙或需要登录的站点;
- 不编造数据;资料不足时明确说明。
验收标准:
- 每个事实性陈述有引用;
- 引用能定位到原文段落;
- 关键数字与来源一致;
- 资料不足时有「不确定」说明;
- P95 在 90 秒内,单次成本可控。
为什么难
- 问题开放:不知道要查什么,需要先规划;
- 来源多:网页、PDF、新闻、报告,质量参差;
- 证据冲突:不同来源口径不同;
- 上下文爆炸:10 篇文档就能塞满窗口;
- 引用幻觉:模型会编造来源或曲解原文;
- 长尾延迟:串行检索 10 次,用户等 2 分钟。
架构
图 1 · 深度研究助手
Planner澄清问题
生成子问题
生成子问题
→
Retriever并行搜索
去重、抓正文
去重、抓正文
→
Extractor抽取带来源的事实
→
Synthesizer组织论证
处理冲突
处理冲突
→
Verifier核对引用与数字
前两步可并行;Extractor 用子 Agent 隔离长文档;Verifier 不通过则回到 Synthesizer。
状态设计
不要让所有内容都堆在消息里。用结构化状态:
python
from dataclasses import dataclass, field
@dataclass
class Evidence:
id: str # E1, E2...
claim: str # 这条证据支持的结论
quote: str # 原文摘录(短)
source_url: str
source_title: str
published_at: str | None
retrieved_at: str
confidence: float
@dataclass
class ResearchState:
question: str
sub_questions: list[str] = field(default_factory=list)
evidence: list[Evidence] = field(default_factory=list)
conflicts: list[dict] = field(default_factory=list)
outline: list[str] = field(default_factory=list)
draft: str = ""
citations_ok: bool = False
budget: dict = field(default_factory=dict)关键点:
- 证据是原子的(一条 claim + 一段 quote),不是整篇文章;
- 每条证据有 URL、标题、时间,便于引用与时效判断;
- 冲突单独记录,不要偷偷选一个;
- 草稿与证据分离,写作时只引用证据 ID。
第一步:规划
text
你是研究规划器。把问题拆成 3–6 个可检索的子问题。
要求:
1. 每个子问题能独立搜索,避免相互依赖。
2. 覆盖:定义/背景、关键数据、不同立场、时间线。
3. 标注每个子问题需要的来源类型(论文/官方/新闻/数据库)。
4. 如果问题本身含糊,先列出需要澄清的点,不要硬编。
问题:{question}
输出 JSON:{"clarifications": [], "sub_questions": [{"q": "...", "source_type": "..."}]}规划质量决定后面所有步骤。子问题太宽,检索回来一堆噪声;太窄,覆盖不全。
第二步:并行检索
python
import asyncio
async def retrieve_all(sub_questions, per_query=6):
sem = asyncio.Semaphore(4) # 控制并发
async def one(sq):
async with sem:
results = await search(sq["q"], limit=per_query)
pages = await asyncio.gather(*[fetch_text(r.url) for r in results[:3]])
return list(zip(results[:3], pages))
batches = await asyncio.gather(*[one(sq) for sq in sub_questions])
return dedupe_by_url([item for batch in batches for item in batch])要点:
- 并发上限,避免触发搜索 API 限流;
- 只抓前 3 条正文,不要全抓;
- 按 URL/标题去重;
- 记录抓取时间与失败原因;
- 正文抽取后截断到 8k–12k 字符,再交给 Extractor。
第三步:抽取证据
把每篇文档交给一个子 Agent,只回传结构化证据,避免主上下文被原文淹没:
text
从下面的文档中抽取与问题相关的事实。
规则:
1. 每条事实必须附一段原文摘录(≤ 50 字),不得改写。
2. 不要推断文档没有的内容。
3. 数字、日期、机构名必须原样保留。
4. 如果文档与问题无关,返回空数组。
5. 标注这条事实支持哪个子问题。
文档:{text}
子问题:{sub_question}
输出:{"evidence": [{"claim": "...", "quote": "...", "confidence": 0.0-1.0}]}代码:
python
async def extract_all(pages, sub_questions):
tasks = [extract_one(text, sq) for (meta, text), sq in pair(pages, sub_questions)]
results = await asyncio.gather(*tasks, return_exceptions=True)
evidence = []
for meta, result in zip(pages, results):
if isinstance(result, Exception):
continue
for i, e in enumerate(result["evidence"]):
if e["confidence"] < 0.5:
continue
evidence.append(Evidence(
id=f"E{len(evidence)+1}",
claim=e["claim"],
quote=e["quote"],
source_url=meta.url,
source_title=meta.title,
published_at=meta.published_at,
retrieved_at=now_iso(),
confidence=e["confidence"],
))
return evidence第四步:综合与写作
先把证据按子问题分组,找出冲突:
text
证据表:
[SQ1] E1: 2025 营收 12.4 亿(来源 A)
E2: 2025 营收 12.1 亿(来源 B)
→ 冲突:口径不同(含/不含汇率影响),在报告中说明写作提示:
text
根据证据表写一份研究报告。
结构:
1. 结论(3 条以内)
2. 关键发现(按子问题组织)
3. 风险与不确定性
4. 来源列表
规则:
- 每个事实性句子后标注 [E1] 这样的证据 ID。
- 只使用证据表中的内容;不足时写「资料不足」。
- 遇到冲突,说明两种说法与可能原因,不要擅自选一个。
- 不要写投资建议。
- 800–1500 字。第五步:引用校验
这是最容易被省略、却最影响可信度的一步。
python
def verify_citations(draft, evidence_by_id):
problems = []
for sent, ids in extract_sentences_with_citations(draft):
if not ids:
if is_factual(sent):
problems.append(("missing_citation", sent))
continue
for eid in ids:
ev = evidence_by_id.get(eid)
if ev is None:
problems.append(("unknown_citation", eid))
continue
if not supports(ev.quote, sent):
problems.append(("quote_mismatch", eid, sent))
return problemssupports 可以用小模型判断,也可以先用规则:
- 数字是否出现在 quote 中;
- 实体名是否一致;
- 时间是否一致;
- 是否明显超出 quote 的范围。
校验失败时,只让模型修正有问题的句子,不要重写全文。
成本与延迟预算
| 阶段 | 调用 | 延迟 | 备注 |
|---|---|---|---|
| 规划 | 1 次 | 3–8s | 可用通用模型 |
| 检索 | 3–6 路并行 | 5–15s | 受搜索 API 限制 |
| 抽取 | 每篇 1 次,并行 | 10–30s | 小模型即可 |
| 写作 | 1 次 | 10–30s | 强模型 |
| 校验 | 1 次 + 规则 | 5–15s | 小模型 |
| 合计 | 10–20 次 | 40–90s | 取决于并发 |
控制手段:
- 限制子问题数量(≤ 6);
- 每路子问题只抓 3 篇;
- 抽取用便宜模型;
- 证据表去重、截断;
- 缓存搜索结果。
评估
构造 20–30 个研究问题,人工标注:
- 必须覆盖的关键事实;
- 可接受的来源;
- 不该出现的错误(过时数据、错误实体)。
指标:
- 事实覆盖率;
- 引用准确率(引用是否支持结论);
- 冲突识别率;
- 拒答/不确定性表达是否恰当;
- 平均耗时与成本。
一个很有效的测试:故意给一个资料不足的问题,看它是否会编。
失败模式
搜索到低质量内容
SEO 农场、洗稿站。用域名白名单/权威度排序,抽取时要求标注来源类型。
引用张冠李戴
模型把 E3 的结论标成 E5。写作后必须逐句校验。
冲突被抹平
两个来源数字不同,报告只写一个。应明确说明口径差异。
上下文爆炸
10 篇长文直接塞进写作提示。用子 Agent 抽取,只保留证据表。
过度自信
资料只有一条,却写成「业界普遍认为」。要求区分「单一来源」与「多方一致」。
可以继续扩展的方向
- 加入图表生成(从证据表生成);
- 支持多语言来源;
- 加入时间线视图;
- 人工在中间补充资料;
- 把报告发布为可分享页面。
小结
- 深度研究 = 规划 + 并行检索 + 证据抽取 + 综合 + 引用校验。
- 用结构化证据表代替长文上下文。
- 子 Agent 隔离长文档,主 Agent 只处理结论。
- 引用校验是可信度的关键,不能省。
- 控制子问题数量、抓取数量和模型层级,才能守住延迟与成本。
下一篇做一个更贴近业务的场景:客服工单 Agent。