第十四章 自动化深度研究智能体.md 27 KB

第十四章 自动化深度研究智能体

在第十三章的旅行助手项目中,我们体验了如何将HelloAgents应用于一个复杂但闭环的多智能体产品。本章我们继续向前,聚焦「知识密集型应用」: 构建一个能够自动化执行深度研究任务的智能体体系。

相比旅行规划,深度研究的难点在于信息的不断发散、事实的快速更新以及用户对引用来源的高要求。为了交付可信的研究报告,我们需要让智能体具备三个核心能力:

(1)问题剖析:将用户的开放主题拆解为可检索的查询语句。

(2)多轮信息采集:结合不同搜索API持续挖掘资料,并去重整合。

(3)反思与总结:依据阶段结果识别知识空白,决定是否继续检索,并生成结构化总结。

本章的目标是带你深入理解 helloagents-deepresearch 项目,掌握其架构、运行方式与扩展技巧,最终能够独立运营一套本地可控的研究助手。

14.1 项目概述与场景定位

14.1.1 为什么需要自动化深度研究智能体

当话题复杂且信息分散时,单纯依赖搜索引擎难以获得结构化洞见。常见的痛点包括:

  • 信息碎片化:结果散落在不同站点,需要手动筛选、比对可靠性。
  • 维度不平衡:容易沉迷于同类观点,忽略实现细节、对比数据或最新进展。
  • 时间成本高:人工整理引用、撰写总结耗时,且难以复盘全过程。

自动化深度研究智能体的优势在于它能够:

  • 以智能体循环执行「生成查询→检索→总结→反思」流程,保证主题覆盖度。
  • 在本地运行(Ollama、LMStudio),保护敏感主题,同时避免外部服务波动。
  • 输出带引用的Markdown报告,便于复盘、分享与二次编辑。

14.1.2 整体能力与用户价值

helloagents-deepresearch 项目将后端HelloAgents智能体、可配置搜索适配器与前端可视化界面结合,形成「输入主题→实时观察→获取总结」的闭环体验。整体亮点如下:

  • 多提供者模型接入:支持 Ollama、LMStudio 或自定义 OpenAI 兼容服务,自主选择推理能力与成本。
  • 多搜索源融合:内置 DuckDuckGo、Tavily、Perplexity、SearXNG 适配器,灵活切换,甚至可以按轮次混合使用。
  • 流式反馈:后端通过 Server-Sent Events 推送各阶段结果,前端即时展示时间线、最新来源和逐字更新的总结。
  • 配置优先级清晰:环境变量、代码默认值分层管理,方便调试与部署。 系统采用经典的前后端分离架构,分为四个层次,如图14.1所示

    graph LR
    U((用户))
    subgraph FE[前端(Vue 3 + TypeScript)]
    UI[App.vue 研究界面]
    FEAPI[services/api.ts\nrunResearchStream]
    end
    subgraph BE[后端(FastAPI DeepResearcher)]
    API[/POST /research + /research/stream/]
    Config[Configuration.from_env]
    subgraph Workflow[DeepResearchAgent 工作流]
      Planner{{PlanningService\n任务规划}}
      Search{{dispatch_search\nprepare_research_context}}
      Summarizer{{SummarizationService\n任务总结}}
      Reporter{{ReportingService\n报告整合}}
      Tracker[[ToolCallTracker\n工具事件]]
    end
    end
    subgraph LLMTools[LLM + 工具层]
    LLM[(HelloAgentsLLM)]
    NoteTool[(NoteTool\nToolRegistry)]
    end
    subgraph External[外部依赖]
    SearchAPI[(Tavily / Perplexity /\nDuckDuckGo / SearxNG / Advanced)]
    Notes[(本地笔记\nnotes_workspace)]
    end
    
    U -->|交互| UI
    UI -->|提交 topic/search_api| FEAPI
    FEAPI -->|POST 请求| API
    API -->|构建配置| Config
    Config -->|实例化| Workflow
    Workflow -->|加载模型| LLM
    Workflow -->|注册工具| NoteTool
    LLM --> Planner
    LLM --> Summarizer
    LLM --> Reporter
    Planner -->|TODO 列表| Workflow
    Workflow -->|SSE todo_list| API
    Workflow --> Search
    Search -->|调用| SearchAPI
    SearchAPI --> Search
    Search --> Summarizer
    Summarizer -->|摘要片段| Workflow
    Planner --> Tracker
    Summarizer --> Tracker
    Reporter --> Tracker
    NoteTool --> Tracker
    NoteTool -->|读写| Notes
    Notes --> NoteTool
    Workflow --> Reporter
    Reporter -->|最终报告| Workflow
    Workflow -->|final_report/done SSE| API
    Tracker -->|tool_call SSE| API
    API -->|Server-Sent Events| FEAPI
    FEAPI -->|更新状态/日志| UI
    UI -->|结果呈现| U
    

图14.1 整体架构图

下一节我们会详细拆解技术架构与数据流转,理解智能体在每一轮循环中的决策与协作方式。

14.2 技术架构与流程

14.2.1 智能体循环的核心机制

helloagents-deepresearch 的核心仍然是 DeepResearchAgent,但它不再让单个智能体“单线程”执行所有步骤,而是复用前几章学到的能力组合成一个协作团队:

  • 研究规划专家PlanningService 驱动的 ToolAwareSimpleAgent)——由第七章扩展过的 SimpleAgent 模板派生,专门负责把主题拆解成结构化任务。
  • 任务总结专家SummarizationService)——在每个任务的上下文就绪后撰写总结,并通过第九章的 NoteTool 进行笔记读写。
  • 报告撰写专家ReportingService)——整合所有任务笔记,产出最终报告并落盘。

三位专家共享同一套工具注册表,其中既包括我们在第七章实现的多源搜索工具,也包括第九章整合的笔记管理能力。

新版 DeepResearchAgent 的执行顺序如下:

  1. PlanningService 启动「研究规划专家」,由模型一次性列出 3~5 条任务,每条任务都会绑定唯一的 task_id、笔记标签与 stream_token
  2. run_stream 为每条任务创建后台线程,线程内部依次完成搜索、总结、笔记写入,并通过队列把 SSE 事件推回主线程。
  3. ReportingService 等待所有任务完成后,再触发「报告撰写专家」,生成最终 Markdown 并固化到结论笔记中。

以下代码片段(backend/src/deep_researcher/app/agents/deep_research_agent.py:126 起)展示了并行执行的核心结构:

channel_map = {task.id: {"step": index, "token": f"task_{task.id}"}
               for index, task in enumerate(state.todo_items, start=1)}

def worker(task: TodoItem, step: int) -> None:
    try:
        enqueue({
            "type": "task_status",
            "task_id": task.id,
            "status": "in_progress",
            "title": task.title,
        })
        for event in self._execute_task(state, task, emit_stream=True, step=step):
            enqueue(event)
    finally:
        enqueue({"type": "__task_done__", "task_id": task.id})

threads = [Thread(target=worker, args=(task, channel_map[task.id]["step"]))
           for task in state.todo_items]
for thread in threads:
    thread.start()

enqueue 会自动补全 stepstream_token 等字段,保证前端可以按任务维度渲染事件。得益于这种结构,多个任务可以同时检索、总结,再通过主线程串行输出,既不会丢事件,也避免了线程安全问题。

下图给出了各智能体协同完成研究流程的职责分层与数据流向(图14.2):

graph TD
    TOPIC["研究主题输入"] --> DRA["DeepResearchAgent<br/>协调器"]
    subgraph Shared["共享资源"]
        CFG["Configuration"]
        LLM["LLM 实例"]
        NOTE["NoteTool / ToolRegistry"]
        TRACKER["ToolCallTracker"]
    end
    CFG --> DRA
    LLM --> DRA
    NOTE -.注册.- DRA
    TRACKER --> DRA
    DRA --> STATE["SummaryState / TodoItem"]

    subgraph Planning["规划阶段"]
        PLAN_AGENT["研究规划专家<br/>ToolAwareSimpleAgent"]
        PLAN_SERVICE["PlanningService"]
    end
    DRA --> PLAN_AGENT
    PLAN_AGENT --> PLAN_SERVICE
    PLAN_SERVICE -->|Todo 列表| STATE

    subgraph Execution["任务执行"]
        STATE --> TASK["TodoItem"]
        TASK --> SEARCH["dispatch_search"]
        SEARCH --> BACKENDS["搜索 API<br/>Tavily / Perplexity / DuckDuckGo / SearXNG / Advanced"]
        SEARCH --> CONTEXT["prepare_research_context"]
        CONTEXT --> SUMM_SERVICE["SummarizationService"]
        DRA --> SUMM_AGENT["任务总结专家<br/>ToolAwareSimpleAgent"]
        SUMM_AGENT --> SUMM_SERVICE
        SUMM_SERVICE --> STATE
        SUMM_SERVICE --> TRACKER
    end

    subgraph Reporting["报告阶段"]
        REPORT_AGENT["报告撰写专家<br/>ToolAwareSimpleAgent"]
        REPORT_SERVICE["ReportingService"]
    end
    DRA --> REPORT_AGENT
    REPORT_AGENT --> REPORT_SERVICE
    STATE --> REPORT_SERVICE
    REPORT_SERVICE --> REPORT["最终报告 Markdown"]
    REPORT --> STATE
    REPORT --> NOTE
    TRACKER --> STATE
    REPORT -.SSE.-> OUTPUT["final_report / done"]
    TRACKER -.SSE.-> OUTPUT

图14.2 Agents架构图

14.2.2 后端组件与配置流

深度研究智能体通过 FastAPI 暴露 /research/research/stream 接口,主要模块如下(文件均位于 backend/src/deep_researcher/app/ 下):

  • agents/deep_research_agent.py:顶层协调者,负责任务规划、并行执行与报告沉淀。
  • services/planner_service.pysummarization_service.pyreporting_service.py:分别封装计划、总结、报告逻辑,内部都复用了 ToolAwareSimpleAgent
  • services/tool_events.py:跟踪 note 工具调用,把事件转换成 SSE,可见第九章 NoteTool 的集成成果。
  • services/search_service.py:统一封装多源搜索结果,与第七章的自定义搜索工具同样遵循“结果字典”约定。
  • configuration.pyapi.py:负责配置加载、HelloAgentsLLM 初始化与 HTTP 层的流式推送。

数据流转顺序为:

  1. 前端提交主题、迭代次数、搜索 API(可选)。
  2. FastAPI 解析请求,合并配置并实例化 DeepResearchAgent
  3. 智能体并行执行所有任务,并持续通过 SSE 推送 tool_calltask_summary_chunksourcesreport_note 等事件。
  4. 客户端结束时收到携带 note_idfinal_reportdone 事件,可直接展示、跳转或二次编辑。

    class Configuration(BaseModel):
    max_web_research_loops: int = Field(default=3)
    local_llm: str = Field(default="llama3.2")
    llm_provider: str = Field(default="ollama")
    search_api: SearchAPI = Field(default=SearchAPI.DUCKDUCKGO)
    enable_notes: bool = Field(default=True)
    notes_workspace: str = Field(default="./notes/deep_research")
    fetch_full_page: bool = Field(default=True)
    ollama_base_url: str = Field(default="http://localhost:11434")
    lmstudio_base_url: str = Field(default="http://localhost:1234/v1")
    strip_thinking_tokens: bool = Field(default=True)
    use_tool_calling: bool = Field(default=False)
    llm_api_key: Optional[str] = None
    llm_base_url: Optional[str] = None
    llm_model_id: Optional[str] = None
    
    @classmethod
    def from_env(cls, overrides: Optional[dict[str, Any]] = None) -> "Configuration":
        raw_values: dict[str, Any] = {}
    
        for field_name in cls.model_fields.keys():
            env_key = field_name.upper()
            if env_key in os.environ:
                raw_values[field_name] = os.environ[env_key]
    
        env_aliases = {
            "local_llm": os.getenv("LOCAL_LLM"),
            "llm_provider": os.getenv("LLM_PROVIDER"),
            "llm_api_key": os.getenv("LLM_API_KEY"),
            "llm_model_id": os.getenv("LLM_MODEL_ID"),
            "llm_base_url": os.getenv("LLM_BASE_URL"),
            "lmstudio_base_url": os.getenv("LMSTUDIO_BASE_URL"),
            "ollama_base_url": os.getenv("OLLAMA_BASE_URL"),
            "max_web_research_loops": os.getenv("MAX_WEB_RESEARCH_LOOPS"),
            "fetch_full_page": os.getenv("FETCH_FULL_PAGE"),
            "strip_thinking_tokens": os.getenv("STRIP_THINKING_TOKENS"),
            "use_tool_calling": os.getenv("USE_TOOL_CALLING"),
            "search_api": os.getenv("SEARCH_API"),
            "enable_notes": os.getenv("ENABLE_NOTES"),
            "notes_workspace": os.getenv("NOTES_WORKSPACE"),
        }
    
        for key, value in env_aliases.items():
            if value is not None:
                raw_values.setdefault(key, value)
    
        if overrides:
            for key, value in overrides.items():
                if value is not None:
                    raw_values[key] = value
    
        return cls(**raw_values)
    

配置解析逻辑(backend/src/deep_researcher/configuration.py:18)先读取所有大写环境变量,再应用显式别名,最后才合并 API 请求的覆盖值。实际运行时意味着:

  • .env 或系统环境变量拥有最高优先级,便于本地调试和生产部署。
  • LangGraph/UI 提交的临时参数通过 overrides 注入,不会修改全局环境。
  • 字段均由 Pydantic 校验,错误值会在启动阶段被捕获,避免无效配置导致的运行时故障。

14.2.3 自定义多源搜索工具

为了适应不同团队的检索与合规要求,helloagents-deepresearch 的搜索层设计成可插拔结构。除了内置的 DuckDuckGo、Tavily、Perplexity、SearXNG,我们还可以像第七章那样编写自己的多源工具,再通过配置切换到 advanced 模式使用。建议按如下步骤操作:

  1. 复用示例代码:参考 code/chapter7/my_advanced_search.py 中的 MyAdvancedSearchTool 类和 create_advanced_search_registry() 工厂函数。该示例演示了如何根据环境变量自动检测 Tavily、SerpApi 等后端,并在执行时做降级提示。
  2. 本地验证:运行 code/chapter7/test_advanced_search.py 可以快速检查自定义工具是否按预期返回结果;若未配置任何 API key,它会输出友好的诊断信息,便于调试。
  3. 接入深度研究项目:在 backend/src/deep_researcher/utils.py 中新增一个适配函数(示例实现见函数 advanced_search),内部直接调用你自定义的工具,再将返回的文本整理为统一的 {"results": [...], "notices": [...]} 结构,方便后续的去重与摘要逻辑复用。
  4. 启用新工具:将后端配置中的 SEARCH_API 设置为 advanced,或在前端表单里选择该选项;深度研究 Agent 会自动透传工具的降级提示与直接答案,并在前端时间线中显示,确保用户知晓检索来源和失败原因。

通过这种方式,你可以把任意内部/垂直领域的检索能力接入到深度研究工作流里,同时保持与核心流程的高度解耦。

提示:配置项 ENABLE_NOTES 默认为 true。当启用时,后端会为每个任务自动同步一份 Markdown 笔记(目录由 NOTES_WORKSPACE 指定),并把 note 工具挂载到所有 Agent,方便它们在需要时对笔记做增删改查。流式事件会附带 note_id,前端可据此展示或跳转对应笔记。

14.2.4 前端交互与流式反馈

前端基于 Vue 3 + Vite,核心页面是 App.vue,其职责包括:

  • 通过 runResearchStream 调用 /research/stream,逐条解析 SSE 数据,并依据 task_id / stream_token 把事件路由到对应任务卡片。
  • 监听 tool_callreport_note 事件,实时展示笔记更新与最终报告位置。
  • 支持用户在研究进行时主动取消请求(AbortController),确保界面响应及时。
  • 提供表单化配置入口(迭代次数、搜索引擎选择),与后端的覆盖策略一一对应。

UI 设计强调“实时性”与“可追溯性”:时间线用于说明状态变化,来源列表/历史面板展示引用详情,摘要与报告区域则会在收到 task_summary_chunkreport_note 等事件时滚动高亮,提示用户关注最新内容。

export async function runResearchStream(
  payload: ResearchRequest,
  onEvent: (event: ResearchStreamEvent) => void,
  options: StreamOptions = {}
): Promise<void> {
  const response = await fetch(`${baseURL}/research/stream`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Accept: "text/event-stream"
    },
    body: JSON.stringify(payload),
    signal: options.signal
  });

  if (!response.ok) {
    const errorText = await response.text().catch(() => "");
    throw new Error(errorText || `研究请求失败,状态码:${response.status}`);
  }

  const body = response.body;
  if (!body) {
    throw new Error("浏览器不支持流式响应,无法获取研究进度");
  }

  const reader = body.getReader();
  const decoder = new TextDecoder("utf-8");
  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();
    buffer += decoder.decode(value || new Uint8Array(), { stream: !done });

    let boundary = buffer.indexOf("\n\n");
    while (boundary !== -1) {
      const rawEvent = buffer.slice(0, boundary).trim();
      buffer = buffer.slice(boundary + 2);

      if (rawEvent.startsWith("data:")) {
        const dataPayload = rawEvent.slice(5).trim();
        if (dataPayload) {
          const event = JSON.parse(dataPayload) as ResearchStreamEvent;
          onEvent(event);

          if (event.type === "error" || event.type === "done") {
            return;
          }
        }
      }

      boundary = buffer.indexOf("\n\n");
    }

    if (done) {
      if (buffer.trim()) {
        const rawEvent = buffer.trim();
        if (rawEvent.startsWith("data:")) {
          const event = JSON.parse(rawEvent.slice(5).trim()) as ResearchStreamEvent;
          onEvent(event);
        }
      }
      break;
    }
  }
}

这段 TypeScript 实现(frontend/src/services/api.ts:1)通过原生 ReadableStream 持续解析 SSE:

  • 逐块累积缓冲区并按 \n\n 分割事件,兼容浏览器对流式分片的差异化处理。
  • 一旦收到 errordone 事件便主动收尾,防止前端挂起。
  • 支持传入 AbortController,与界面上的“取消研究”按钮形成闭环,用户体验更加可控。

14.3 UI交互图 图14.3 UI交互图

14.3 快速上手指南

为了在本地体验深度研究智能体,我们建议分三步完成:准备运行环境、配置模型与搜索服务、分别启动后端与前端。以下流程基于 code/chapter14/helloagents-deepresearch/ 目录。

14.3.1 环境要求与依赖安装

  • Python ≥ 3.11(后端使用 pyproject.toml 配合 uvpip 安装依赖)
  • Node.js ≥ 18.0(建议 20+)与 npm ≥ 9.0
  • 至少一种本地大模型服务:Ollama 或 LMStudio或API供应商

推荐使用虚拟环境隔离依赖:

cd code/chapter14/helloagents-deepresearch/backend
python -m venv .venv
source .venv/bin/activate  

随后安装 Python 依赖:

uv sync  # 若未安装 uv,可先使用pip install uv 来进行安装

前端部分进入 frontend 目录执行 npm install 即可。

14.3.2 配置本地模型与搜索服务

后端默认读取同目录下的 .env 文件。参考步骤:

cp .env.example .env

根据自身环境设置以下核心变量:

  • 模型提供者:LLM_PROVIDER=ollamaLLM_PROVIDER=lmstudioLLM_PROVIDER=custom
  • 模型名称:LOCAL_LLM=deepseek-r1:8b(Ollama)/LOCAL_LLM=qwen_qwq-32b(LMStudio)或云端场景下的 LLM_MODEL_ID
  • 服务地址:OLLAMA_BASE_URLLMSTUDIO_BASE_URL 或通用 LLM_BASE_URL
  • 是否开启工具调用:USE_TOOL_CALLING=true
  • 搜索服务:SEARCH_API=duckduckgotavilyperplexitysearxng

若选择 Tavily、Perplexity 或自建 SearXNG,请额外填写对应的 TAVILY_API_KEYPERPLEXITY_API_KEYSEARXNG_URL

# backend/.env 示例片段
SEARCH_API='duckduckgo'
LLM_PROVIDER=custom
LLM_MODEL_ID=your-model-name
LLM_API_KEY=your-api-key-here
LLM_BASE_URL=your-api-base-url
USE_TOOL_CALLING=true
MAX_WEB_RESEARCH_LOOPS=3
FETCH_FULL_PAGE=True
# 切换至本地模型时,只需启用以下注释行:
# LLM_PROVIDER=ollama
# LOCAL_LLM=llama3.2
# OLLAMA_BASE_URL=http://localhost:11434

.env.example 所示(backend/.env.example),云端、自建、本地三种配置都以注释形式保留。保持模板不删,有助于在不同部署模式间快速切换。 以下是调用 SiliconFlow API 作为大模型服务的示例环境配置: 14.5 SiliconFlow 环境配置

14.3.3 启动后端服务

后端以 FastAPI 形式提供 HTTP API 与 SSE 流接口。启动方式:

cd code/chapter14/helloagents-deepresearch/backend
uv run uvicorn deep_researcher.api:app --reload

成功后可访问 http://localhost:8000/docs 查看自动生成的 OpenAPI 文档,验证 /research/research/stream 接口是否可用。

14.4 FastApi页面

14.3.4 启动前端与联调

前端基于 Vite,默认通过 .env 或命令行指定后端地址:

cd code/chapter14/helloagents-deepresearch/frontend
echo "VITE_API_BASE_URL=http://localhost:8000" > .env.local  # 可选
npm run dev

浏览器访问 http://localhost:5173,输入研究主题即可观察到流式时间线、来源列表和逐渐成型的总结。若需要部署或静态打包,可使用 npm run build 并将输出托管至任意静态服务器,注意同步配置 VITE_API_BASE_URL

完成以上步骤后,一个全流程的深度研究助手便可以在本地运行。接下来我们将深入拆解关键模块的实现细节,理解为何它能稳定输出结构化的研究结果。

14.4 关键模块拆解

从代码角度理解核心模块,有助于在后续扩展或排查问题时做到心中有数。本节按流程拆解查询生成、搜索适配、总结与反思逻辑,以及流式通信协议。

14.4.1 任务规划:SimpleAgent 的再利用

PlanningService 把第七章的 SimpleAgent 模板直接升级为多任务规划器。通过 ToolAwareSimpleAgentrun 方法,我们既继承了原有的 Prompt 能力,也让模型得以调用 note 工具记录任务框架。

def plan_todo_list(self, state: SummaryState) -> List[TodoItem]:
    prompt = todo_planner_instructions.format(
        current_date=get_current_date(),
        research_topic=state.research_topic,
    )

    response = self._agent.run(prompt)
    self._agent.clear_history()

    tasks_payload = self._extract_tasks(response)
    todo_items: List[TodoItem] = []

    for idx, item in enumerate(tasks_payload, start=1):
        task = TodoItem(
            id=idx,
            title=str(item.get("title") or f"任务{idx}").strip(),
            intent=str(item.get("intent") or "聚焦主题的关键问题").strip(),
            query=str(item.get("query") or state.research_topic).strip(),
        )
        todo_items.append(task)

    state.todo_items = todo_items
    return todo_items

backend/src/deep_researcher/app/services/planner_service.py:26

_extract_tasks 优先解析 JSON,再兼容 [TOOL_CALL:note:{...}] 形式,这与我们在第九章使用 NoteTool 的方式完全一致:模型既可以一次输出结构化数组,也可以显式调用工具创建任务笔记。规划阶段只负责生成骨架,笔记的后续更新会在每个任务线程中完成。

14.4.2 搜索执行与上下文整合

search_service.dispatch_search 把第七章的高级搜索工具纳入统一接口。无论是内置的 DuckDuckGo/Tavily,还是我们自定义的 advanced 工具(复用 code/chapter7/my_advanced_search.py),最终都会被整理成 {"results": [...], "notices": [...]} 的结构。随后 prepare_research_context 会调用 deduplicate_and_format_sources,去除重复 URL 并裁剪正文长度,确保总结阶段的上下文既全面又精简。

search_result, notices, answer_text, backend = dispatch_search(
    task.query,
    self.config,
    state.research_loop_count,
)

sources_summary, context = prepare_research_context(
    search_result,
    answer_text,
    self.config,
)

backend/src/deep_researcher/app/agents/deep_research_agent.py:261

正因为我们在第七章就把搜索工具封装成可复用的注册表,现在只需切换 SEARCH_API=advanceddispatch_search 就会自动调用自定义工具,并把降级信息通过 notices 推送到前端,让用户随时了解当前轮次是否触发了兜底策略。

14.4.3 并行总结与流式输出

总结阶段同样依赖 ToolAwareSimpleAgent,但我们重写了 stream_run 以支持在流式输出中执行工具:

for chunk in self.llm.stream_invoke(messages, **kwargs):
    residual += chunk
    for segment in process_residual():
        if not segment:
            continue
        segments_this_round.append(segment)
        final_segments.append(segment)
        yield segment

for call_text in tool_call_texts:
    tool_calls.extend(self._parse_tool_calls(call_text))

if tool_calls:
    messages.append({"role": "assistant", "content": clean_response})
    tool_results_text = "\n\n".join(self._execute_tool_call(...))
    messages.append({"role": "user", "content": "工具执行结果:\n..."})

backend/src/deep_researcher/app/agents/tool_aware_agent.py:243

process_residual 会截断尚未完整输出的 [TOOL_CALL:...] 片段,确保前端永远不会看到残缺的工具指令;一旦工具执行完成,新的上下文会立即写回消息栈,继续下一轮对话。配合 SummarizationService.stream_task_summary,我们可以在前端实时看到任务总结的增量内容,并且在总结结束后自动拿到最终文本。

14.4.4 笔记同步与报告沉淀

ToolCallTracker 记录所有笔记相关事件,并在任务线程之间同步 note_id。当所有任务完成后,_persist_final_report 会把最终 Markdown 写入结论笔记:若已有同名笔记则更新,否则创建新笔记并返回 report_note 事件。

payload = {
    "type": "report_note",
    "note_id": note_id,
    "title": note_title,
    "content": content,
    "note_path": str(note_path),
}

yield {
    "type": "final_report",
    "report": report,
    "note_id": state.report_note_id,
    "note_path": state.report_note_path,
}

backend/src/deep_researcher/app/agents/deep_research_agent.py:248

这意味着前端可以在报告区域直接显示笔记路径,甚至提供“一键打开 Markdown” 的快捷入口;而后端的数据结构中也会记录 report_note_id,便于二次加工或测试校验。

14.4.5 SSE 事件协议升级

随着并行化与笔记沉淀的引入,SSE 事件类型也进行了扩展:

  • todo_list:包含任务清单以及每个任务的 stream_token
  • task_status / task_summary_chunk / sources:逐任务输出状态、总结增量与来源摘要。
  • tool_call:复用第九章的事件格式,记录模型执行 note 等工具的详情。
  • report_note:最终报告落盘后的通知,附带 note_idnote_path 与正文内容。
  • final_report / done:宣告流程结束,并把最终 Markdown 与笔记信息一起返回。

前端 runResearchStream 只需在事件分发器中新增对 report_note 的处理,即可在 UI 中同步展示最新笔记。由于每条事件都包含 task_idstream_token,即使多个任务并行输出,也能稳定位于正确的面板。

掌握以上模块,对于定制 Prompt、扩展企业内部检索源或构建新的可视化界面都有直接帮助。下一节我们将讨论可能的扩展方向与实践建议。