Agent 速成笔记 · 第 14 章 自动化深度研究智能体

Agent 速成笔记 · 第 14 章 自动化深度研究智能体

源:Datawhale《Hello-Agents》第 14 章 | 定位:实战案例(全栈多智能体系统) | 一句话:把"研究一个开放主题"这件发散的事,收敛成「规划 3-5 个 TODO → 逐条搜索总结 → 整合成带引用的长报告」的确定性流水线。


0. 一章速览(30 秒)

  • 一句话:深度研究助手 = TODO 驱动的三阶段流水线(规划 / 执行 / 报告),由三个专职 Agent 顺序接力,两个工具(SearchTool、NoteTool)负责取数与落盘,SSE 把全过程实时推给前端。
  • 本章解决什么问题:搜索引擎只能回答"单个问题",而真实研究需要回答"一系列相关问题"。本章给出一种可工程化的范式,把开放主题变成有结构、可追溯、可复现的报告。
  • 必须记住的 5 个点:
    • TODO 驱动是范式内核:复杂主题 → 3-5 个子任务(title / intent / query)→ 逐个执行 → 整合。太少覆盖不全,太多冗余。
    • 三 Agent 顺序协作:TODO Planner、Task Summarizer、Report Writer,各配一套专用 Prompt,无并发。
    • ToolAwareSimpleAgent 在 SimpleAgent 上加了 tool_call_listener 回调,让"工具调用"变成可观测事件(调试 / 日志 / 进度)。
    • 长报告的关键是"分而治之的上下文压缩":每个子任务的搜索结果先被压成一段带引用的 Markdown 总结,报告 Agent 只读总结、不读原始网页。
    • 质量控制靠三层:来源引用标记 [1][2]、URL 去重与重复信息合并、搜索失败降级返回空列表而不中断流程。

1. 项目概述与架构设计(14.1)

1.1 为什么需要深度研究助手

传统研究的三个痛点:信息过载(搜索引擎返回成千上万结果,逐个点开)、缺少结构(找到的信息碎片化)、重复劳动(每次研究都重走"搜索→阅读→总结→整理")。

深度研究助手的核心价值有四点:

价值 含义
节省时间 把 1-2 小时的研究压缩到 5-10 分钟
提高质量 系统化流程,避免遗漏重要信息
可追溯 记录所有搜索结果与来源,便于验证引用
可扩展 可新增搜索引擎、数据源、分析工具

与第 13 章旅行助手相比,深度研究的难点在于信息不断发散、事实快速更新、用户对引用来源要求高。因此智能体必须具备三个核心能力:

  1. 问题剖析:把开放主题拆解成可检索的查询语句。
  2. 多轮信息采集:结合不同搜索 API 持续挖掘,并去重整合。
  3. 反思与总结:依据阶段结果识别知识空白,判断是否继续检索,并生成结构化总结。

1.2 四层架构与一次请求的完整流转

系统采用前后端分离的四层架构:

层次 技术栈 职责
前端层 Vue3 + TypeScript 全屏模态对话框 UI、Markdown 结果可视化
后端层 FastAPI API 路由(/research/stream)
智能体层 HelloAgents 三个 Agent + 两个工具
外部服务层 搜索引擎 + LLM 提供商 提供检索与生成能力

智能体层里的固定角色:三个专门 Agent(TODO Planner、Task Summarizer、Report Writer)+ 两个核心工具(SearchTool、NoteTool)。

一次研究请求在系统中的流转是八个步骤,这一段就是全章骨架,值得背下来:

  1. 用户输入:在前端输入研究主题。
  2. 前端发送:通过 SSE 连接到 /research/stream。
  3. 后端接收:FastAPI 接收请求,创建研究状态。
  4. 规划阶段:调用研究规划 Agent,分解为 3 个子任务。
  5. 执行阶段:逐个执行子任务——SearchTool 搜索 → 任务总结 Agent 总结 → NoteTool 记录结果。
  6. 报告阶段:调用报告生成 Agent,整合所有总结。
  7. 流式返回:通过 SSE 推送进度与结果到前端。
  8. 前端展示:实时更新任务状态、进度条、日志、报告。

工程要点:一次完整研究约需 1-3 分钟(取决于主题复杂度与搜索响应速度)。这个量级决定了产品形态——必须做流式进度,否则用户会以为页面卡死。

1.3 后端目录结构中的分层意图

后端 backend/src/ 的分层本身就是考点:

agent.py      # 核心协调器 DeepResearchAgent
main.py       # FastAPI 入口
models.py     # 数据模型(TodoItem、SummaryState)
prompts.py    # Prompt 模板集中管理
config.py     # 配置管理
services/     # planner.py / summarizer.py / reporter.py / search.py
workspace/    # 研究笔记

为什么要拆 services? 每个服务只做一件事:构建 Prompt、调用对应 Agent、处理返回值。Agent 与业务逻辑解耦后,换 Prompt 不影响调度,换模型不影响业务。

前端侧只需关注两个文件:components/ResearchModal.vue(全屏模态 UI)与 composables/useResearch.ts(SSE 与状态管理)。


2. TODO 驱动的研究范式(14.2)

2.1 从"搜一次"到"规划→执行→整合"

传统搜索方式的问题在于:每个链接只覆盖主题的一个方面、缺乏系统性结构、需要手工整理。TODO 驱动把"研究"这个复杂任务转化为规划 → 执行 → 整合的固定流程。

以"Datawhale 是一个什么样的组织?"为例,系统先规划出四个方向(基本信息 / 主要项目 / 社区文化 / 影响力),再对每个 TODO 执行"搜索 → 总结 → 记录来源",最后整合为带参考文献的四段式报告。

它的优势:子问题清晰、每条子任务的搜索结果与总结都被记录便于追溯、流程化避免遗漏、可随时增删子任务或调整顺序。

三个核心要素(对应三个 Agent 的雏形):

要素 职责 关键约束
智能规划器 把主题分解为子任务 3-5 个;为每个子任务设计合适的搜索查询
任务执行器 执行子任务 搜索取资料、提取关键信息去冗余、保存来源引用
报告生成器 整合子任务结果 按逻辑顺序组织、合并重复信息、每个观点带引用

整个流程是线性的,每个阶段有明确输入输出——这带来的是可调试性:任何一环出问题都能单独复现。

2.2 三阶段流程的输入输出契约

阶段 1:规划(Planning)

  • 输入:研究主题 + 当前日期(用于获取最新信息)。
  • 输出:JSON 格式子任务列表,每条含 title(任务标题)、intent(研究意图)、query(搜索查询)。
  • 分解策略:通常从基础概念入手,再到技术现状、实际应用、发展趋势,必要时做对比分析。
  • 好的规划四条标准:覆盖全面、逻辑清晰、查询精准、数量适中。
  • 细节:Prompt 中提示 query 可用英文以获得更好的搜索结果,并明确"只返回 JSON,不要包含其他文本"。

阶段 2:执行(Execution)

  • 输入:子任务列表 + 搜索引擎配置。
  • 输出:每个子任务的 Markdown 总结 + 来源引用列表。
  • 单任务四步循环:
    1. 搜索资料:调用 SearchTool,参数 {"input": task.query, "backend": "tavily", "mode": "structured", "max_results": 5}。
    2. 获取结果:提取每条结果的标题、URL、摘要(title / url / snippet)。
    3. 调用总结 Agent:把任务信息与检索结果一起交给 Task Summarizer。
    4. 记录总结与来源:写入 NoteTool,tags=["research","summary"]。
  • 执行期间持续向前端推送状态事件,类型形如 status("正在搜索:…"、"正在总结搜索结果…")与 task(带 id、title、status: completed)。

阶段 3:报告(Reporting)

  • 输入:所有子任务总结 + 研究主题。
  • 输出:Markdown 最终报告,固定五个部分:标题 / 概述 / 各子任务详细分析 / 总结 / 参考文献。
  • 报告 Agent 做四件事:按子任务逻辑顺序组织内容、在开头补一段简要概述、合并重复信息、把所有来源引用统一整理到参考文献(按子任务分组)。

注意 case 编号的转换:引用编号是局部的——每条子任务内部的 [1][2] 只对应它自己那份来源列表;报告阶段按子任务分组重排参考文献,而不是全局重新编号。这是实现引用可追溯时最容易出错的地方。


3. 智能体系统设计(14.3)

3.1 为什么不用单个 SimpleAgent

第 7 章的 SimpleAgent 设计理念是简单直接:每次 run() 时分析问题、决定是否调用工具、返回结果。这在简单任务上足够,但面对深度研究这种"多阶段 + 长上下文 + 需要引用"的任务就不够了——它没有阶段划分,也没有产出物的结构约束。

因此本章继续采用多智能体协作:把流程切成三段,每段一个专职 Agent。好处是每个 Agent 都很简单、Prompt 可定制、互不影响、易于维护。

三个 Agent 的职责对照:

Agent 职责 核心动作
TODO Planner 研究规划专家 主题 → 3-5 个子任务(title/intent/query)
Task Summarizer 任务总结专家 检索结果 → 带引用的 Markdown 总结
Report Writer 报告撰写专家 多份总结 → 结构化最终报告

3.2 三套 Prompt 的关键设计点

TODO Planner 的 Prompt 必须包含:current_date、research_topic、三条子任务要求(覆盖重要方面 / 有明确研究目标 / 能被搜索引擎找到)、JSON 字段定义、示例输出,以及四条约束(数量 3-5、子任务间有逻辑关系如"从基础到应用、从现状到趋势"、查询能准确找到资料、只返回 JSON)。

Task Summarizer 的 Prompt 必须包含:task_title、task_intent、task_query、格式化后的 search_results。输出要求三条:核心观点、关键数据(数字、日期、名称)、来源引用(用 [1]、[2] 标记)。附加要求:简洁避免冗余、保留重要细节、每个观点都要带引用、使用 Markdown。

Report Writer 的 Prompt 必须包含:research_topic 与拼接好的 task_summaries。输出结构五项(标题 / 概述 2-3 段 / 各子任务详细分析用二级标题 / 总结 1-2 段 / 参考文献按子任务分组)、四条要求(结构清晰逻辑连贯、消除重复信息、保留所有来源引用、Markdown 格式)。

三套 Prompt 的共同套路:字段占位符 + 结构化输出要求 + 示例输出 + 否定式约束("不要包含其他文本")。示例输出是让模型稳定产出结构化结果最便宜的手段。

3.3 ToolAwareSimpleAgent:把工具调用变成可观测事件

是什么:SimpleAgent 的扩展类,多了一个 tool_call_listener 回调参数,每次工具调用时触发。

怎么运作:它继承 SimpleAgent 并重写 _execute_tool_call——先解析参数,再调 super() 真正执行工具,最后把四元组回调出去。

def _execute_tool_call(self, tool_name: str, parameters: str) -> str:
    parsed_parameters = self._parse_parameters(parameters)
    result = super()._execute_tool_call(tool_name, parameters)
    if self._tool_call_listener:
        self._tool_call_listener({
            "agent_name": self.name,
            "tool_name": tool_name,
            "parsed_parameters": parsed_parameters,
            "result": result,
        })
    return result

这段代码在做的事:在工具执行的"前后夹一层",把 (谁、调了什么工具、传了什么参数、得到什么结果) 结构化地广播出去。回调内容与返回值解耦,因此监听器可以随时增删,不影响 Agent 行为。

四个用途:调试(看调了哪些工具、传了什么参数)、日志(记录研究全过程)、分析(观察 Agent 行为模式)、进度展示(实时显示 Agent 正在做什么)。

在 DeepResearchAgent 中,一个监听器被三个 Service 共用,回调里统一转成事件推给前端:

def tool_listener(call_info):
    self._emit_event({
        "type": "tool_call",
        "agent": call_info["agent_name"],
        "tool": call_info["tool_name"],
        "parameters": call_info["parsed_parameters"],
    })

self.planner = PlanningService(self.llm, tool_listener)
self.summarizer = SummarizationService(self.llm, tool_listener)
self.reporter = ReportingService(self.llm, tool_listener)

这段代码在做的事:一处监听、全局可见。所有 Agent 的工具调用都被记录并通过 SSE 推送,用户能看到实时进展。

3.4 顺序协作与核心协调器

三个 Agent 之间是顺序协作,特征三条:线性流程(固定顺序执行)、明确的输入输出(每个 Agent 的输入来自上一个的输出)、无并发(同一时刻只有一个 Agent 在工作)。

DeepResearchAgent 是全系统的核心协调器,run() 就是三阶段流水线本身:

def run(self, research_topic: str) -> str:
    # 1. 规划阶段
    self._emit_event({"type": "status", "message": "正在规划研究任务..."})
    todo_list = self.planner.plan_todo_list(research_topic)
    self._emit_event({"type": "tasks", "tasks": todo_list})

    # 2. 执行阶段
    task_summaries = []
    for task in todo_list:
        self._emit_event({"type": "status", "message": f"正在研究:{task.title}"})
        search_results = self.search_service.search(task.query)
        summary = self.summarizer.summarize_task(task, search_results)
        task_summaries.append((task, summary))
        self._emit_event({"type": "task_completed", "task_id": task.id})

    # 3. 报告阶段
    self._emit_event({"type": "status", "message": "正在生成报告..."})
    report = self.reporter.generate_report(research_topic, task_summaries)
    self._emit_event({"type": "report", "content": report})
    return report

这段代码在做的事:协调器不写业务逻辑,只做调度与事件广播。三个阶段的中间产物(todo_list、task_summaries)在内存里逐级传递,事件在阶段边界与每个任务边界发出——前端进度条的粒度天然就等于"子任务粒度"。


4. 工具系统集成(14.4)

4.1 SearchTool 的多引擎扩展

第 7 章的 SearchTool 只集成了 Tavily 与 SerpApi;本章新增 DuckDuckGo、Perplexity、SearXNG,并增加 Advanced 模式(组合多个搜索引擎)。所有引擎共享统一调用接口,换引擎不改调用代码。

引擎通过配置选择,而非硬编码:

class SearchAPI(str, Enum):
    TAVILY = "tavily"
    DUCKDUCKGO = "duckduckgo"
    PERPLEXITY = "perplexity"
    SEARXNG = "searxng"
    ADVANCED = "advanced"

默认值是 SearchAPI.DUCKDUCKGO(免密钥,开箱可用);.env 里一行 SEARCH_API=tavily 即可切换。

返回值契约(这是接服务层时的关键约定):

字段 含义
results 结果列表,每条含标题、URL、摘要
backend 实际使用的搜索引擎
answer AI 生成的答案(仅 Perplexity)
notices 通知信息(API 限制、错误等)

搜索结果有两类必须先处理的问题:

  • URL 重复 → deduplicate_sources() 用 set 记录 seen_urls,保留首次出现的条目。
  • 单条文本过长 → limit_source_tokens() 截断:按"1 Token ≈ 4 字符"估算,max_tokens=2000 即 max_chars=8000,超出就截断并加 "..."。

工程要点:limit_source_tokens 是一种极便宜的上下文控制手段——不用真的分词器计数,用字符数近似即可,误差可接受但能挡住"一条摘要吃掉半个上下文"的情况。

4.2 NoteTool:把研究过程落盘

NoteTool(第 9 章集成的内置工具)负责持久化研究进度,支持创建、读取、更新、删除笔记。笔记以 Markdown 文件存在工作空间目录下,文件名就是任务 ID。

落盘后形成的产物结构:

workspace/
├── notes/
│   ├── 1.md   # 任务1的笔记
│   ├── 2.md
│   └── 3.md
└── reports/
    └── final_report.md

NotesService.save_task_summary() 写入的内容分三块:任务信息(意图、查询)、搜索结果(逐条 [idx] 标题 / URL / 摘要)、总结。

为什么必须落盘(而不是只放内存):一是研究中断后能从上次进度继续;二是便于复盘全部操作、分析研究的质量与效率;三是审计需要——报告里的每个引用都能回溯到原始 URL。

易错点:落盘的笔记已经包含"搜索结果快照",它同时是引用溯源依据和调试样本。如果把原始结果丢掉只存总结,后续就没法验证总结是否失真。

4.3 ToolRegistry 与工具调用六步流程

ToolRegistry 是框架的工具注册表,统一管理注册与调用。使用顺序是先建工具 → 再建注册表 → 注册 → 最后创建 Agent:

search_tool = SearchTool(backend="hybrid")
note_tool = NoteTool(workspace="./workspace/notes")

registry = ToolRegistry()
registry.register_tool(search_tool)
registry.register_tool(note_tool)

agent = ToolAwareSimpleAgent(
    name="研究助手", system_prompt="你是一个研究助手",
    llm=llm, tool_registry=registry
)

工具调用的六步流程(全章最值得背的机制之一):

  1. Agent 生成指令:如 [TOOL_CALL:search_tool:{"input": "Datawhale组织", "backend": "tavily"}]。
  2. 解析指令:ToolRegistry 提取工具名称与参数。
  3. 查找工具:按名称查找对应工具。
  4. 调用工具:调用工具的 run 方法并传入参数。
  5. 返回结果:工具返回执行结果。
  6. 格式化结果:把结果格式化为字符串,回填给 Agent。

[TOOL_CALL:工具名:JSON参数] 这个约定值得单独记:工具名与参数都是纯文本,所以任何兼容 OpenAI 接口的模型都能用,不依赖特定厂商的 function calling 字段。


5. 服务层实现(14.5)

服务层是连接 Agent 与工具的桥梁,负责具体业务逻辑(构 Prompt、调 Agent、洗数据)。四个服务各司其职:

服务 输入 输出
PlanningService 研究状态(含主题) List[TodoItem]
SummarizationService 任务 + 检索结果 总结文本 + 来源 URL 列表
ReportingService 主题 + 各任务总结 最终 Markdown 报告
SearchService 查询语句 去重限流后的结果列表

5.1 PlanningService:结构化输出的鲁棒解析

四个职责:构建规划 Prompt(含当前日期)、调用 TODO Planner、从响应中提取 JSON、校验必需字段。

解析之所以要写成"鲁棒"的,是因为 LLM 返回 JSON 有三类常见问题:包含额外文本(JSON 前后夹着说明)、格式错误(缺引号、缺逗号)、字段缺失。

对应的三层方案:

# 方法1:正则提取 JSON 数组
json_match = re.search(r'\[.*\]', response, re.DOTALL)
if json_match:
    return json.loads(json_match.group(0))

# 方法2:没有数组就直接整体解析
try:
    return json.loads(response)
except json.JSONDecodeError:
    raise ValueError("无法从响应中提取JSON")

# 字段验证:每个任务必须含 title / intent / query
if not all(key in item for key in ["title", "intent", "query"]):
    raise ValueError(f"任务{idx}缺少必需字段")

这段代码在做的事:用最省事的兜底换稳定性。提示词里已经写了"只返回 JSON",但仍按"可能不听话"来设计——re.DOTALL 让 . 能跨行匹配,[...] 锚定 JSON 数组边界,最后给每个 TodoItem 分配 id=idx(从 1 开始,这个 id 就是后续 NoteTool 的文件名)。

5.2 规划质量评估:一个可落地的自检打分

规划是全流程最关键的一步,因此本章给出一个显式的评分函数 evaluate_plan(),基准 100 分:

  • 子任务 少于 3 个:扣 20 分,提示"可能遗漏重要信息"。
  • 子任务 多于 5 个:扣 10 分,提示"可能存在冗余"。
  • 某个 task 的 query 按空格切分少于 2 个词:扣 10 分,提示"查询过于简单"。

返回 {"score": ..., "suggestions": [...]}。

工程要点:这是"启发式规则打分"而非模型评估——成本为零、结果稳定,适合做上线前的兜底断言(分数过低就不执行,直接提示用户改主题)。逻辑关系的检查留了口子但未实现,因为"逻辑是否连贯"很难用规则描述。

5.3 总结与报告服务

SummarizationService 四步:格式化搜索结果(序号 + 标题 + URL + 摘要)、用 task_title/task_intent/task_query/search_results 填 Prompt、调用 Task Summarizer、返回 (summary, source_urls)。source_urls 是从原始结果里直接取 URL 拼成的列表,不依赖模型输出——这样引用列表永远真实存在,不会因为模型"忘了写"而丢失。

ReportingService 四步:格式化各任务总结(任务序号 + 标题 + 意图 + 总结内容 + 来源 URL)、填 Prompt、调用 Report Writer、返回 Markdown。

for idx, (task, summary, source_urls) in enumerate(task_summaries, start=1):
    formatted.append(
        f"## 任务{idx}:{task.title}\n\n"
        f"**意图**:{task.intent}\n\n"
        f"{summary}\n\n"
        f"**来源**:\n"
    )
    for url in source_urls:
        formatted.append(f"- {url}\n")

这段代码在做的事:把"来源"和"总结"绑在一起喂给报告 Agent。报告 Agent 因此不需要(也无法)访问原始网页,它只做"编排与去重",这正是长报告不爆上下文的原因。

上下文与压缩策略小结(本章最核心的工程思想):

层级 上下文里装什么 谁负责压缩
搜索层 单条结果按 2000 Token 截断 SearchService
任务层 5 条结果 → 一段带引用总结 Task Summarizer
报告层 3-5 段总结 → 一份报告 Report Writer
持久层 全量原始结果写磁盘 NoteTool

关键点:上下文是被逐层收窄的。原始网页摘要只在"执行单个子任务"这一刻进上下文,出了这个循环就只剩总结,磁盘上另有一份全量快照兜底。这就是为什么既能做 3-5 段的长报告,又不会把上下文撑爆。

5.4 SearchService:调度、降级与缓存

SearchService 的定位很特别——它没有让 SimpleAgent 直接调工具,而是加了一层中间层把结果转交给 Agent。原文的理由是:这样能让 Agent 更专注于处理得到的信息,而不是分心在"怎么调搜索、调哪个引擎、结果怎么洗"上。

四个职责:按配置调度引擎、调用 SearchTool、处理结果(去重 / 限 Token / 格式化)、错误处理。

搜索是外部依赖,必然失败,所以写成"降级返回空列表":

try:
    raw_response = self.search_tool.run({
        "input": query,
        "backend": self.config.search_api.value,
        "mode": "structured",
        "max_results": max_results,
    })
    results = raw_response.get("results", [])
    results = self._deduplicate_sources(results)
    results = self._limit_source_tokens(results)
    return results
except Exception as e:
    logger.error(f"搜索失败:{query},错误:{e}")
    return []

这段代码在做的事:把"某条子任务查不到"降级成局部失败。返回 [] 而不是抛异常,流水线得以继续——总结 Agent 会基于空结果产出一段"信息不足"的总结,其余子任务与最终报告不受影响。这是长流程系统必须有的容错姿态。

缓存:为提效降本,可用磁盘缓存搜索结果。缓存键是 md5(f"{query}_{max_results}_{search_api}"),文件存放在 ./cache/search/,命中就直接读 JSON 返回,未命中才真搜并回写。注意缓存键包含引擎名——换引擎必须换缓存,否则会拿到别的引擎的结果。

5.5 服务层小结

四个服务串起来就是一条完整流水线:PlanningService 定任务 → SearchService 取数据 → SummarizationService 出中间产物 → ReportingService 出交付物。它们各司其职、接口清晰,而且每个都能单独测试与替换。


6. 前端与实时交互(14.6)

6.1 全屏模态对话框

选择全屏模态而非普通弹窗,理由是四条:沉浸式(避免干扰)、层次清晰(主页面与研究页分离)、易关闭(点关闭按钮或按 ESC 返回)、响应式(适配不同屏幕)。

UI 四区:顶部栏(研究主题 + 关闭按钮)、进度区域(当前阶段:规划 / 执行 / 报告)、内容区域(Markdown 结果)、底部栏(状态文案)。响应式靠媒体查询:max-width: 768px 收敛为 95vw/95vh,max-width: 480px 全屏且去圆角。

6.2 SSE:为什么不用 WebSocket

系统用 SSE(Server-Sent Events,服务器推送技术) 做实时进度。流程五步:客户端 POST 发起请求 → 服务器返回 text/event-stream 建立连接 → 服务器按阶段周期性推送进度 → 客户端监听事件更新 UI → 推送最终报告后关闭连接。

后端用 FastAPI 的 StreamingResponse,响应头固定两个:Cache-Control: no-cache 与 Connection: keep-alive。每条消息格式为 data: {JSON}\n\n。

进度百分比的分配值得记:规划阶段发 10%,执行阶段按 10 + (idx / n) * 70 平滑推进,报告阶段 90%,完成 100%。

事件类型共七种,前端按 type 分支处理:

type 含义 前端动作
progress 阶段进度 更新百分比与文案
plan 规划结果 展示子任务列表
task_summary 单任务总结 追加到 Markdown
report 最终报告 覆盖 Markdown 内容
error 异常 提示并关闭连接
completed 完成 关闭连接、结束 loading
tool_call 工具调用 显示日志

为什么选 SSE 而不是 WebSocket:这是单向推送(服务器 → 客户端),客户端只需要一次 POST 表达意图,之后不要求双向通信。SSE 基于 HTTP、实现更简单、天然支持断线重连语义,对"进度播报"这类场景刚好够用。

6.3 结果可视化

用 marked 把 Markdown 转 HTML,配置两项:breaks: true(支持换行)、gfm: true(支持 GitHub Flavored Markdown)。报告中引用密集,参考文献按子任务分组、每条用 [标题](URL) 形式呈现,便于点击核验。

易错点:Markdown 内容用了 v-html 渲染,这要求输入可信。本系统的报告来自自家 LLM 与检索结果,若把用户输入直接拼进 Markdown 再渲染,就需要考虑注入风险。


7. 工程实践要点(从零复现的清单)

按依赖顺序复现,关键决策与踩坑都在这里:

  1. 先定数据模型再写逻辑。TodoItem(id, title, intent, query) 与 SummaryState(research_topic, ...) 是全系统的数据契约,尤其 TodoItem.id 同时是 NoteTool 的文件名,必须先定。
  2. 把三套 Prompt 抽到 prompts.py 单独管理。三套 Prompt 都用到 .format() 占位符,集中存放才不会漏字段;同时占位符里的字面花括号要写成 {{ }} 转义(示例 JSON 里尤其要注意)。
  3. 规划阶段务必注入当前日期。研究主题常有"最新""现状"这类时间敏感词,缺日期会导致检索到过期信息。
  4. 执行阶段按任务串行、任务内并行可选。本章是串行(无并发),好处是日志顺序清晰、易调试;要提高吞吐可对多个 TODO 并发,但要接受事件顺序错乱与限流风险。
  5. 搜索失败必须降级。让 SearchService.search() 捕获异常返回 [],而非上抛。否则一个引擎抖动就会整轮研究失败。
  6. 上下文要在三个层级设闸:单条结果 2000 Token 截断、任务层压缩成一段总结、报告层只吃总结。缺任何一层都可能在中途撞上下文上限。
  7. 引用编号是局部的,报告阶段不要重新编号,改为按子任务分组列参考文献,避免"编号对不上来源"这类不可追溯问题。
  8. 中间产物一律落盘(workspace/notes/{id}.md + reports/final_report.md),既为断点续跑,也为事后审计。
  9. 把工具调用做成事件流。用 ToolAwareSimpleAgent 的监听器统一收口,前端进度、日志、调试三件事一次解决。
  10. 给规划加自检门槛。用 evaluate_plan() 的分数判断是否直接执行,低成本挡住"3 个以上同义子任务"这类坏规划。
  11. 搜索结果缓存键要带引擎名,否则切换 SEARCH_API 后会命中错误的缓存。
  12. 进度必须真实分段。按"规划 → 执行(第 i/n) → 报告"上报,比"转圈圈"更能安抚用户——1-3 分钟的空等是不可接受的体验。

这套架构的可迁移经验:任何"输入开放、需要多轮检索、要求可追溯产出"的任务都适用——把长流程切成固定阶段、每个阶段一个专职 Agent 配一套 Prompt、阶段之间只传压缩后的结构化中间产物、全量原始数据落盘、全流程事件化上报。它与具体的"研究"领域无关,换掉 Prompt 与工具就能变成别的分析型 Agent。


8. 高频考点 & 易错点速查

  1. TODO 驱动的三阶段是规划 / 执行 / 报告,三个 Agent 依次是 TODO Planner、Task Summarizer、Report Writer;三个核心能力是问题剖析、多轮信息采集、反思与总结。
  2. 子任务数量 3-5 个:少于 3 覆盖不全,多于 5 冗余;每条子任务三个字段 title / intent / query。
  3. 三 Agent 是顺序协作:线性、输入输出明确、无并发(不是并行、不是辩论、不是层级制)。
  4. ToolAwareSimpleAgent 的机制:继承 SimpleAgent、重写 _execute_tool_call、通过 tool_call_listener 回调四元组信息;用途是调试 / 日志 / 分析 / 进度展示。
  5. 工具调用六步:生成指令 → 解析 → 查找 → 调用 → 返回 → 格式化;指令格式 [TOOL_CALL:工具名:JSON参数]。
  6. SearchTool 返回四字段:results / backend / answer(仅 Perplexity)/ notices。
  7. Token 估算约定:1 Token ≈ 4 字符;单条来源上限 2000 Token(约 8000 字符)。
  8. 易混点:SearchService 不让 Agent 直接调 SearchTool,而是走中间层——目的是让 Agent 专注处理信息,别把调度与清洗混进推理。
  9. 易混点:引用编号是"子任务内局部编号",报告阶段按子任务分组列参考文献,不做全局重排。
  10. 易错点:搜索异常吞掉返回 [] 是设计而非缺陷,这是保住长流水线不中断的降级策略。
  11. 易错点:缓存键必须含引擎名,只按 query 做键会在切换引擎后返回脏数据。
  12. 易错点:JSON 解析要按"模型不听话"设计——正则提取数组 + 整体解析兜底 + 必需字段校验,三层缺一不可。
  13. 易错点:报告 Agent 拿不到原始网页,它只吃总结与来源 URL;把原始结果也塞进去是最常见的上下文爆炸原因。
  14. 章末习题的考点映射:①简述 TODO 驱动三阶段(考流程)②为什么拆三个 Agent 而非一个(考职责划分)③ToolAwareSimpleAgent 解决了什么问题(考可观测性)④SearchTool 去重与限 Token 的必要性(考上下文控制)⑤NoteTool 落盘的价值(考状态管理)⑥SSE 为何优于轮询(考实时交互选型)⑦规划质量评估的规则设计(考质量控制)⑧如何扩展到新搜索引擎或新数据源(考可扩展性)。

9. 与前后章节的衔接

本章是"实战案例"链条上的一环:输入来自第 7 章的 SimpleAgent 与 SearchTool、第 9 章的 NoteTool,以及第 13 章多智能体产品化(旅行助手)的组织经验——本章把 SimpleAgent 扩展为 ToolAwareSimpleAgent,并把工具集从"两个搜索引擎"扩到"多引擎 + Advanced 模式"。输出则是一套可复用的长流程架构:分层服务、结构化中间产物、事件流上报、断点落盘。下一章把视角转向"与游戏引擎结合的多 Agent 系统——赛博小镇",重点变成 Agent 之间的复杂交互与协作模式。


10. 课后练习

在线试卷:https://md-quiz-online.app.workbuddy.host/

posted @ 2026-09-28 17:11  测试小罡  阅读(3)  评论(0)    收藏  举报