实战:Deep Research Agent
第 8 章我们造出了自主执行的引擎:把工具调用装进循环,让模型自己决定下一步。这一章是第三章「怎么让它干活」的收官——把前面的装备全部综合运用起来:系统提示词(第 5 章)、会话管理(第 6 章)、工具设计(第 7 章)…
第 8 章我们造出了自主执行的引擎:把工具调用装进循环,让模型自己决定下一步。这一章是第三章「怎么让它干活」的收官——把前面的装备全部综合运用起来:系统提示词(第 5 章)、会话管理(第 6 章)、工具设计(第 7 章)、主循环(第 8 章),造一个能联网查证的搜索助手。它体现 Agent 相对普通 LLM 的核心价值:回答的事实来自工具结果而非参数记忆,且每条结论可溯源。
1. 目标定义
先说清楚要造什么,以及“好”的标准:
能力:回答时效性/事实性问题,先搜索、再阅读、后作答,并给出来源标注。
验收标准(能写成测试的才算标准):
- 问“今天有什么新闻”,不允许直接用模型记忆作答(第 4 章:知识有截止日期)
- 每条事实性结论带
[n]引用,对应文末来源列表 - 搜不到的信息明说“未找到”,不许编(第 4 章:幻觉)
- 达到轮次上限时优雅收场(第 8 章:闸门)
2. 工具层
搜索工具选型:DuckDuckGo 搜索免 key 免费,适合本地实验(生产可换 Tavily/Bing,质量更高):
# uv add ddgs httpx
# 注:ddgs 的旧包名是 duckduckgo-search,若安装失败可回退旧名
from ddgs import DDGS
import httpx
import re
from pathlib import Path
from pydantic import BaseModel, Field
# ---------- web_search ----------
class WebSearchArgs(BaseModel):
query: str = Field(description="搜索关键词。精炼具体,一次只搜一个主题")
max_results: int = Field(default=5, description="返回结果数,默认 5")
def web_search(query: str, max_results: int = 5) -> str:
try:
results = DDGS().text(query, max_results=max_results)
except Exception as e:
return f"搜索失败:{e}"
if not results:
return "没有搜索到任何结果,建议更换关键词"
lines = []
for i, r in enumerate(results, 1):
lines.append(f"[{i}] {r['title']}\n URL: {r['href']}\n 摘要: {r['body']}")
return "\n".join(lines)
# ---------- read_webpage ----------
class ReadWebpageArgs(BaseModel):
url: str = Field(description="要阅读的网页 URL(来自搜索结果的 URL 字段)")
max_chars: int = Field(default=3000, description="最多返回的字符数")
def read_webpage(url: str, max_chars: int = 3000) -> str:
try:
r = httpx.get(url, timeout=15, follow_redirects=True,
headers={"User-Agent": "Mozilla/5.0"})
html = r.text
html = re.sub(r"<(script|style)[^>]*>.*?</\1>", "", html,
flags=re.S | re.I) # 去掉脚本和样式
text = re.sub(r"<[^>]+>", " ", html) # 去标签
text = re.sub(r"\s+", " ", text).strip() # 压缩空白
if len(text) > max_chars:
return text[:max_chars] + f"...(已截断,全文 {len(text)} 字符)"
return text or "(页面无可提取文本)"
except Exception as e:
return f"读取失败:{e}"
两个设计决策,都来自第 7 章的原则:
- search 和 read 分成两个工具——而不是一个“搜索并总结”的大工具。小工具让模型能自由组合策略(先搜索、挑着读、再补搜),这是第 8 章 ReAct 的自由度所在
- read_webpage 的说明写明“来自搜索结果的 URL 字段”——用 description 引导参数来源,减少模型幻觉 URL
注册进第 7 章的 Registry:
registry = Registry()
registry.register(Tool(web_search, WebSearchArgs,
"联网搜索最新信息。当问题涉及时事、数据、你不确定的事实时必须先调用。"
"返回带编号的结果列表。"))
registry.register(Tool(read_webpage, ReadWebpageArgs,
"读取网页正文内容。需要深入阅读某个搜索结果的原文时使用。"))
3. 系统提示词:研究者的行为准则
按第 5 章的五要素框架写:
SYSTEM = """你是一个严谨的研究助手,只依据搜索到的信息回答问题。
## 行为规范
1. 回答事实性问题前,必须先调用 web_search;信息不足时用 read_webpage 阅读原文
2. 搜索结果矛盾时,优先采信原文,并说明分歧
3. 确实查不到时,明确说"未找到可靠来源",禁止编造
## 输出格式
- 每条事实性结论后标注来源编号,如:某产品 2025 年销量为 X 万台[1]
- 回答末尾列出"参考来源",格式:[n] 标题 — URL
- 回答保持精炼,与问题无关的信息不写"""
4. 组装与运行
第 8 章的 Agent 类原样复用:
agent = Agent(system=SYSTEM, registry=registry, max_rounds=6)
def logger(event, data):
if event == Ev.TOOL_CALL:
print(f" ⚙ {data['call'][0]}({data['call'][1][:70]})")
elif event == Ev.TOOL_RESULT:
print(f" ↳ {data['result'][:60]}")
elif event == Ev.FINISH:
print(f" ✔ {data['round']} 轮完成")
agent.on(logger)
answer = agent.run("Qwen3 发布于什么时候?和 DeepSeek-V3 比主要特点是什么?")
print(answer)
一次典型的运行轨迹:
⚙ web_search({"query": "Qwen3 发布时间 特点"})
↳ [1] Qwen3 发布:混合推理模式... URL: https://... 摘要: ...
⚙ web_search({"query": "DeepSeek-V3 特点"})
↳ [1] DeepSeek-V3 技术报告... 摘要: ...
⚙ read_webpage({"url": "https://...(第 1 条结果的 URL)"})
↳ Qwen3 于 2025 年 4 月发布,提供 0.6B~235B 八种规模...
✔ 4 轮完成
最终回答(节选示意):
Qwen3 于 2025 年 4 月底发布,提供 0.6B 到 235B 共八种规模,主打混合推理模式(可开关思考)[1]。与 DeepSeek-V3 相比,二者都是 MoE 架构的开放权重模型……[2]
参考来源: [1] Qwen3 发布 — https://... [2] DeepSeek-V3 技术报告 — https://...
注意模型自主选择的策略:并行两轮搜索(第 7 章的多调用)→ 挑原文精读 → 对比作答。没有一行代码编排这个流程——策略来自模型,骨架来自第 8 章的循环,纪律来自系统提示词。
5. 检验与调优
按验收标准逐条检查,顺带示范 Agent 的调试方法——事件轨迹是最好的调参依据:
| 症状(看轨迹) | 诊断 | 处方 |
|---|---|---|
| 不搜索直接回答 | 系统提示词约束弱 | 强化“必须先搜索”;或首轮 tool_choice="required" |
| 搜一次就作答,细节全靠脑补 | 模型偷懒 | 提示词加“结果不足以支撑结论时继续搜索/阅读” |
| 连续 3 轮搜索不同关键词仍不满 | 搜索词质量差 | description 教它“精炼关键词”;或加一个“改写查询”工具 |
| read 了很多无关页面 | 选页策略差 | 搜索结果的摘要描述更完整(第 7 章:可读返回) |
| 回答无引用 | 格式纪律松 | 系统提示词的输出格式加粗;加第 6 章式的校验兜底 |
调试心法:先看轨迹定位问题层级,再动对应的层——工具层(第 7 章)改工具质量和 description,行为层(第 5 章)改系统提示词,引擎层(第 8 章)改闸门参数。跨层乱改是 Agent 调优的大忌。
6. 局限与下一步
诚实评估这个 Agent 的边界:
- 搜索质量受限:免费搜索源噪声大;生产换 Tavily/Bing API,并考虑加“站内搜索”“新闻搜索”等专用工具
- 没有长期记忆:每次对话从零开始,不会记住昨天查过什么(第 15 章)
- 没有权限与防护:read_webpage 可以访问任意 URL,存在 SSRF 风险(第 15 章)
- 单线程串行:两个搜索本可并行(第 7 章的并发执行 / 第 12 章的子代理)
- 上下文随轮次膨胀:6 轮之后 messages 已经相当可观(第 13 章)
但核心价值已经成立:一个合计两三百行、无任何 Agent 框架依赖的完整 Agent。你亲眼看到它如何从“模型 + 工具 + 循环”中涌现出自主行为——这就是 00 章使用指南里说的“从理解到能造”。
本章小结
- 搜索 Agent 的本质价值:事实来自工具结果而非参数记忆,结论可溯源
- 小工具组合(search + read)优于大而全的工具,策略自由度是 ReAct 的精髓
- 系统提示词承担“研究纪律”:先查证、带引用、查不到明说
- 调试方法论:看事件轨迹定位层级(工具层/行为层/引擎层),对症下药
- 局限清单即路线图:记忆(第 15 章)、安全(第 15 章)、并行(第 12 章)、上下文(第 13 章)
第三章「怎么让它干活」到此完结。你已经能从零造 Agent 了。接下来的问题是生态级的:当工具越来越多、越来越杂,怎么组织?第四章「怎么扩展它」讲三件武器——Skill、MCP、子代理。
下一篇:10 · Skill 与渐进式披露 —— 当工具说明书比工具本身还大时,把文档从上下文里挪出去,用到再加载:渐进式披露的设计哲学。