首页/Agent 核心/实战:Deep Research Agent
Agent 核心 · 第 09 章

实战:Deep Research Agent

第 8 章我们造出了自主执行的引擎:把工具调用装进循环,让模型自己决定下一步。这一章是第三章「怎么让它干活」的收官——把前面的装备全部综合运用起来:系统提示词(第 5 章)、会话管理(第 6 章)、工具设计(第 7 章)…

约 11 分钟4,370 字5 个代码示例

第 8 章我们造出了自主执行的引擎:把工具调用装进循环,让模型自己决定下一步。这一章是第三章「怎么让它干活」的收官——把前面的装备全部综合运用起来:系统提示词(第 5 章)、会话管理(第 6 章)、工具设计(第 7 章)、主循环(第 8 章),造一个能联网查证的搜索助手。它体现 Agent 相对普通 LLM 的核心价值:回答的事实来自工具结果而非参数记忆,且每条结论可溯源。

1. 目标定义

先说清楚要造什么,以及“好”的标准:

能力:回答时效性/事实性问题,先搜索、再阅读、后作答,并给出来源标注。

验收标准(能写成测试的才算标准):

  1. 问“今天有什么新闻”,不允许直接用模型记忆作答(第 4 章:知识有截止日期)
  2. 每条事实性结论带 [n] 引用,对应文末来源列表
  3. 搜不到的信息明说“未找到”,不许编(第 4 章:幻觉)
  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 与渐进式披露 —— 当工具说明书比工具本身还大时,把文档从上下文里挪出去,用到再加载:渐进式披露的设计哲学。