最近审阅:2026-10
“这家图书馆几点关门?”可能一次查找就够。“这几家图书馆今年的开放时间改过几次,周末和节假日分别适用哪条规则?”就需要拆问题、核对时间、处理冲突,再决定还有什么没查到。写得长不是区别,根据新证据调整下一步才是。
下面是一套原创的教学设计,不是某家产品的完整复刻。公开研究系统可以参考 Anthropic 的工程复盘;其中的模型、成本和效果属于当时配置,不能直接当作我们的性能。这里的搜索循环靠代码和 prompt 组织;把多轮搜索本身当成 RL 任务来训练,见 Agent RL。
1. 先定义这次要回答什么
把用户问题拆成能核对的事项,而不是立刻开十个搜索:
| 子问题 | 需要的证据 | 完成条件 |
|---|---|---|
| 当前常规时间 | 生效中的官方通知 | 地点、日期和营业时间匹配 |
| 周末是否不同 | 适用条款 | 周末规则明确,或明确未找到 |
| 节假日例外 | 当次临时公告 | 不是去年同节日的通知 |
| 规则何时改变 | 前后两个版本 | 区分发布时间和生效时间(effective time) |
“没找到例外”不是“没有例外”。必要信息还缺时,可以给部分结论,不必为了凑齐表格编答案。
2. 五个部件就能先跑起来
flowchart LR
Q["问题与范围"] --> P["待查队列"]
P --> T["搜索 / 读取工具"]
T --> E["证据记录"]
E --> C{"仍有缺口或冲突?"}
C -->|有,预算允许| P
C -->|没有或无法继续| W["整理结论与未解决项"]
W --> V["引用与事实复核"]
- Planner 提出待查问题及依赖,不替证据下结论。
- Executor 只运行获准工具,检查参数、超时和重试。
- Evidence store 保存来源版本、原文位置、时间和支持的具体结论。
- Controller 管预算、重复搜索与停止,不让模型自己无限循环。
- Writer / verifier 把结论连接到证据,检查遗漏与不支持的扩写。
第一版可以是一个模型加普通代码。只有能独立搜索的子问题才值得并行;多 agent 会增加上下文、重复工作、协调与成本,不是研究质量的免费升级。
3. 证据不能只存一段摘要
至少保留 source_id, URL, fetched_at, published_at, effective_at, content_hash, span, claim_id。时间字段不清楚就留空,不从搜索结果猜。
自拟过程:旧通知说 17:00 关闭,新通知说 18:00。先确认新通知是否针对同一地点、是否已生效,再考虑能否替代旧通知。如果一份讲平日、一份讲节日,它们可能都对;如果访问不到原文,就把“只有搜索摘要”留在记录里。
内容相同的转载可以归为同一证据来源。5 个网页转述同一条消息,不是 5 次独立确认。网页中的指令也不是系统权限;读取来源不能顺便授权发邮件、执行代码或上传资料。
4. 用代码固定边界,不靠一句“请谨慎”
下面的最小实现检查研究状态与引用位置。证据是否真正支持结论,仍需单独判断;supported 必须来自已完成的核对,不能由“搜到一个 URL”直接设置。
def research_status(questions, calls_left):
if type(calls_left) is not int or calls_left < 0 or not questions:
raise ValueError("Expected questions and a nonnegative call budget")
allowed = {"unseen", "supported", "conflict", "unavailable"}
unresolved = []
for question_id, record in questions.items():
status = record["status"]
if status not in allowed:
raise ValueError("Unknown evidence status")
if status != "supported" or not record.get("evidence_ids"):
unresolved.append(question_id)
if not unresolved:
return "ready_for_review", []
return ("budget_exhausted" if calls_left == 0 else "needs_research"), unresolved
def check_citation_spans(citations, sources):
errors = []
for citation in citations:
source = sources.get(citation["source_id"])
start, end = citation["span"]
if source is None or not source.get("read"):
errors.append("missing_or_unread_source")
elif type(start) is not int or type(end) is not int or not 0 <= start < end <= len(source["text"]):
errors.append("invalid_span")
elif source["text"][start:end] != citation["quote"]:
errors.append("quote_mismatch")
return errors
questions = {
"hours": {"status": "supported", "evidence_ids": ["notice-v2"]},
"holiday_exception": {"status": "conflict", "evidence_ids": ["notice-v1", "notice-v2"]},
}
assert research_status(questions, 0) == ("budget_exhausted", ["holiday_exception"])
sources = {"notice-v2": {"read": True, "text": "Open until 18:00."}}
citations = [{"source_id": "notice-v2", "span": (0, 17), "quote": "Open until 18:00."}]
assert check_citation_spans(citations, sources) == []
第一个例子在预算耗尽时仍报告节假日冲突,不会把“停止”写成“完成”。第二个只证明引用文字确实出现在已读来源中,不证明语义蕴含:原文“18:00 关门”不能支持“周日一定开放”。
真实实现还要将状态持久化。保存问题队列、已读内容 hash、工具结果、消耗预算与版本;重启后恢复未完成项,而不是重新搜索并丢掉旧冲突。给工具调用分配唯一 ID,避免失败重试被当成新证据。
5. 怎样判断值得增加一轮搜索?
没有通用的“查满几篇”。可以用这些停止条件:
| 状态 | 合理动作 |
|---|---|
| 关键结论有来源,已完成冲突核查 | 进入最终复核 |
| 明确还缺用户日期或地点 | 问用户,不盲搜 |
| 新搜索反复返回相同信息 | 改问题或停止,记录边界 |
| 来源不可访问 / 无权限 | 标明缺失,不绕过访问限制 |
| 达到时间、工具或 token 预算 | 给部分结果和未解决项 |
“证据足够”可以是停止建议,但高风险任务不能只靠模型的自信决定。更长的搜索链还可能增加误读和陈旧信息,不保证单调变好。
6. 怎么测,而不是怎么演示?
先用本地自拟文档测试:一份旧公告、一份新公告、一份转载、一份冲突公告。注入超时、来源消失、错误引用和“忽略原规则”的文本。固定夹具能定位程序错误;真实网页测试则检查外部变化,两者都需要。
| 指标 | 看什么 | 容易误判什么 |
|---|---|---|
| 事实正确率 | 结论与独立标注是否一致 | 文风流畅不等于事实正确 |
| 引用准确 / 覆盖 | 引用支持结论,重要结论都有依据 | 有链接不代表支持 |
| 冲突与拒答 | 能识别缺证据,而不是一律猜 | 全拒答也不算完成 |
| 成本与延迟 | 调用、token、墙钟时间 | 并行更快但可能更贵 |
| 重复运行稳定性 | 同一任务多次运行 | 偶然成功不等于可靠 |
按 Agent 评估指南的思路,检查最终状态和重要约束,不强求每次执行同一条路径。把少量人审作为锚点,再校准(calibration) judge;不要让生成答案的同一个流程不加核对地给自己满分。
这段代码没有发起真实搜索、调用模型或测出提升。它是一套可测试的边界;接入模型后,再用固定协议比较单次 RAG、单 agent 和多 agent,且报告各自预算。
本页术语 2
- 生效时间 effective time
- 某一版本的数据或属性从什么时候开始对外有效
- 校准 calibration
- 模型给出的置信度是否等于真实正确率