从零手写 Agent(五):调研报告、引用与落盘
第四篇之后,Agent 已经能 search → fetch → 多步回答。用户要的往往是另一件事:
写成 Markdown,带上参考资料,保存到
reports/xxx.md。
终端里一段口语摘要不算交付。第五篇在 同一个 _agent_loop 上补 落盘 和 可核对引用,核心新增:
save_markdown
sources(state + system 注入)
Loop:save nudge / 必要时强制 save_markdown
(仓库里还扩展了 search_hn、fetch_rss,与 search_web 一样会把 URL 收进 sources。)
1. 第四篇之后,还差哪一层?
Web Research 原型:
问题 → search → fetch → answer
调研 交付 还需要:
| 层次 | 目标 |
|---|---|
| 结构化正文 | 摘要 / 发现 / 局限 / 参考文献 |
| 可核对引用 | URL 来自本轮 Tool 结果,而不是模型「回忆」 |
| 落盘 |
cat reports/foo.md 能看到文件 |
最小链路:
search / fetch / hn / rss
↓
组织 Markdown
↓
save_markdown
↓
reports/*.md
注意: Loop 仍不需要为「报告」单独写一套;变的是 Tool 列表、State 和 prompt。
2. 第一个坑:说了「保存」,磁盘却没有文件
实测里很常见:
用户:保存到 reports/python-313.md
Agent:(长报告)…… 结束
ls reports/ # 空
原因分两层:
(1)没有 save_markdown Tool — 模型只能在对话里「假装保存」,程序写不了盘。
(2)有 Tool,但模型选择不调用 — Tool 注册成功 ≠ 一定会 tool_calls。Loop 在 没有 tool_calls 时直接返回 assistant 正文并退出;不会因为用户说了「保存」就自动写文件。
所以交付要同时做:
- 提供
save_markdown(真实 IO) - 在 prompt 里写清:用户指定路径时必须 save
- 必要时 Loop save nudge 或
tool_choice强制save_markdown一轮
这和「模型够不够聪明」有关,更是 完成条件写在代码里 的问题。
3. save_markdown:沙箱与 Tool 契约
实现要点(细节见仓库 agent/tools/save_markdown.py):
- 只允许写入
reports/下相对路径,禁止..逃逸 - 参数:
filename、content(整份 Markdown 由模型生成) - 成功时 Tool 返回里带
Saved:路径,便于 trace 验收
注意: 大段正文走 一次 tool 参数,7B 可能截断或偷懒;复杂报告可拆「先 fetch 凑 sources → 再 save」,或接受「摘要版报告 + 引用列表」。
4. sources:引用从哪来
模型会改写标题、补假 URL。更稳妥的规则:
参考文献里的 URL,应优先来自本轮 Agent 实际见过的搜索/抓取结果。
做法:
-
AgentState.sources— 每次 Tool 返回后ingest_tool_sources(name, args, result),从search_web/search_hn/fetch_rss/fetch_url等解析 URL,去重 -
sync_system_message()— 每轮调用 LLM 前,把 【本轮已收集来源】 写进 system(条数可配置上限) - prompt — 要求正文引用与列表一致,无来源时不得编造链接
商用产品往往还有成稿 URL 白名单校验;个人版先到 sources 注入 + trace 肉眼核对,第六篇会写边界。
5. Prompt 与 Loop:save nudge
agent/prompt.py 里典型约束:
- 用户要求保存 → 必须调用
save_markdown,不要只在聊天里贴全文 - 报告结构:标题、摘要、发现、局限、参考文献
- HN/RSS 类任务:先
search_hn或fetch_rss,再整理
agent/loop.py 在检测到「用户要保存但本轮未 save」时,可 追加一轮 nudge;仍不 save 时 force_tool_name="save_markdown" 强制调用一次(见 agent/llm.py 的 tool_choice)。
注意: 强制 save 只解决「不调用 Tool」;content 质量仍取决于模型与前面 fetch 是否充分。
6. 与第四篇实测的衔接
第四篇 §4.2 已说明:Tool 成功 ≠ 任务成功(宽 query、门户首页、只给链接)。第五篇在此基础上:
-
窄 query + 多源(如
search_hn、具体站点search_web)再 save,通过率更高 - 「列 HN 头条」 不等于自动落盘 — 除非用户要 save 或 prompt/nudge 触发
- trace 里同时看:
tool_calls、【本轮已收集来源】、Saved:
7. 本机验收
ollama pull qwen2.5:7b
pip install -r requirements.txt
# 专题 + 落盘(示例)
python main.py -v --trace '调研 Python 3.13 相对 3.12 的主要新特性,写 Markdown,参考文献用本轮来源,保存到 reports/python-313.md'
ls reports/
cat reports/python-313.md
交互调试:python main.py -i --trace。
8. 收束
| 概念 | 要点 |
|---|---|
| 交付 |
save_markdown + reports/
|
| 引用 |
sources 注入 system,减少假链 |
| Loop | 不变;完成条件靠 nudge / 强制 Tool |
| 模型 | 7B 可能跳过 save 或简写 — 用 trace 验收 |
下一篇
《从零手写 Agent(六):云模型 API Key 与网页调研台——Loop 仍不变》——LLM_* 换云端推理,FastAPI + SSE 把调研链可视化。
系列目录:GitHub docs(https://github.com/zealot2002/myagent/blob/main/docs/00-系列导读.md)













