恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
通义WebWeaver双Agent框架拆解:像人类一样做研究的ReAct实现路径
首页
资讯中心
/
通义WebWeaver双Agent框架拆解:像人类一样做研究的ReAct实现路径
通义WebWeaver双Agent框架拆解:像人类一样做研究的ReAct实现路径
发布时间:2026/10/3 22:02:59
1. 从「搜完就写」到「边搜边改」WebWeaver 双 Agent 框架到底解决了什么如果你让一个普通 LLM 智能体去写「帕金森病不同阶段的预警信号及术后护理」这种题目大概率会看到两种翻车现场。第一种是它先疯狂搜索十几轮把一堆网页片段塞进上下文然后一次性硬生成两万字结果中间章节开始胡编引用对不上号。第二种是它一开始就定死大纲后面搜到再好的证据也不改结构最后报告像八股文深度全靠堆字数。通义实验室的 WebWeaver 就是冲着这两个毛病去的。它把开放式深度研究OEDR拆成规划Planning和写作Writing两个阶段分别交给 Planner 和 Writer 两个 Agent。Planner 负责在 ReAct 循环里反复「搜索—读证据—改大纲」Writer 负责拿着带引用 ID 的大纲逐节从记忆库里精准取证据、内部推理、再落笔。核心检索词就是 WebWeaver 双 Agent 框架、ReAct 推理机制、动态大纲优化。它适合谁如果你正在做 LLM Agent 开发、RAG 长报告生成、或者想复现一个能跟做的多 Agent 研究流程这篇就是给你写的。我下面会给出可复现的角色配置、工具调用链路、ReAct 循环伪代码以及在本地环境跑通双 Agent 协作的具体步骤。论文里最关键的三个设计是动态大纲协同进化、记忆库Memory Bank做上下文管理、Writer 的分层检索与上下文清理。这三件事决定了它为什么能在 DeepResearch Bench 上把引用准确率做到 93% 以上。先说清楚一个常见误解WebWeaver 不是「两个模型互相聊天」。Planner 和 Writer 共享同一个记忆库但职责边界非常硬。Planner 只输出结构化大纲和引用 ID不写正文Writer 只消费大纲和证据不改结构。这种硬边界是它能稳定跑长任务的前提也是你在本地复现时最该先固定下来的部分。2. 前置准备TaoToken 接入与本地环境依赖在本地验证双 Agent 协作之前你需要一个能稳定调用 Claude 或 Qwen 系列模型的入口。我实测下来用 TaoToken 的 API 做基座调用比较省事Base URL 固定为https://taotoken.net/apiKey 在控制台生成。如果你还没建 Key可以直接去 API Keys 页面拿https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 里面有各语言 SDK 的调用示例。本地环境我建议用 Python 3.10依赖三件套openai走兼容接口、requests、pydantic。搜索引擎部分论文用的是网页检索本地复现你可以先用一个 mock 检索器返回固定片段把 Agent 循环跑通再换成真实搜索 API。这样排障成本最低。模型选择上Planner 建议用推理能力强的模型Writer 可以用同款或稍小的模型。论文里 SFT 实验用的是 Qwen3-30B微调后引用准确率从 25% 提到 85.9%说明工具调用格式的稳定性比模型大小更关键。你本地先用 Claude Sonnet 系列跑通流程再考虑换小模型。环境变量这样配export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api然后验证一下连通性from openai import OpenAI import os client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL], ) resp client.chat.completions.create( modelclaude-sonnet-4-20250514, messages[{role: user, content: 只回复 ok}], ) print(resp.choices[0].message.content)如果这里报 401先检查 Key 有没有复制完整如果报 model not found去模型对话页面确认当前账号可用的模型 IDhttps://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 。这一步跑通再往下否则后面 Agent 循环里的报错你分不清是逻辑问题还是鉴权问题。3. 可复制配置Planner 与 Writer 的角色定义与工具 Schema这一节是全文最核心的可复制部分。我按论文的行动空间把两个 Agent 的 system prompt、工具 JSON Schema、以及记忆库结构都写成可直接落地的片段。你新建一个webweaver_config.json把下面内容贴进去。Planner 的行动空间是search、write_outline、terminate。Writer 的行动空间是retrieve、write、terminate。工具定义用 OpenAI function calling 格式{ planner_tools: [ { type: function, function: { name: search, description: 根据当前知识缺口发起网页搜索返回候选 URL 与片段, parameters: { type: object, properties: { query: {type: string, description: 搜索查询词}, top_k: {type: integer, default: 5} }, required: [query] } } }, { type: function, function: { name: write_outline, description: 新增或优化大纲每个小节必须带 citations 引用 ID 列表, parameters: { type: object, properties: { outline: { type: array, items: { type: object, properties: { section_id: {type: string}, title: {type: string}, goal: {type: string}, citations: {type: array, items: {type: string}} }, required: [section_id, title, citations] } } }, required: [outline] } } }, { type: function, function: { name: terminate, description: 当大纲覆盖全面且证据充分时终止规划, parameters: {type: object, properties: {}} } } ], writer_tools: [ { type: function, function: { name: retrieve, description: 按引用 ID 从记忆库取回原始证据, parameters: { type: object, properties: { citation_ids: {type: array, items: {type: string}} }, required: [citation_ids] } } }, { type: function, function: { name: write, description: 输出当前小节的正文用 write 标签包裹, parameters: { type: object, properties: { section_id: {type: string}, content: {type: string} }, required: [section_id, content] } } }, { type: function, function: { name: terminate, description: 所有小节写完时终止, parameters: {type: object, properties: {}} } } ] }记忆库结构用 Pydantic 定义每条证据必须有唯一 ID、来源 URL、摘要、原文片段from pydantic import BaseModel from typing import List class Evidence(BaseModel): evidence_id: str source_url: str summary: str raw_snippet: str class MemoryBank: def __init__(self): self.store {} def add(self, ev: Evidence): self.store[ev.evidence_id] ev def retrieve(self, ids: List[str]) - List[Evidence]: return [self.store[i] for i in ids if i in self.store]Planner 的 system prompt 关键约束我写成这样你可以直接抄你是研究规划者。每轮先思考当前大纲缺什么证据再决定调用 search 还是 write_outline。search 返回的片段只用于决策原始证据由系统写入记忆库。write_outline 必须为每个小节标注 citations引用 ID 来自记忆库。当大纲覆盖主题且每个小节都有至少 2 条证据时调用 terminate。Writer 的 system prompt你是报告写作者。按大纲顺序逐节处理。每节先调用 retrieve 取回该节 citations 对应的证据在内部推理中合成关键见解再调用 write 输出正文。写完一节后该节证据从上下文清除只保留占位符。全部小节完成后调用 terminate。这里有个容易踩的坑Planner 的write_outline每次返回的是完整大纲还是增量论文里是「回顾并优化」我建议实现成完整大纲覆盖这样状态机简单不会出现增量合并冲突。代价是 token 多一点但排障容易得多。4. ReAct 循环伪代码与本地验证跑通双 Agent 协作配置就绪后核心是一个双层循环。外层是 Planner 的 ReAct 循环内层是 Writer 的逐节写作循环。伪代码如下我把它写成接近可运行的 Pythondef planner_react_loop(client, memory, max_rounds8): outline [] for step in range(max_rounds): # Reason: 让模型基于当前大纲和记忆库摘要决策 messages build_planner_messages(outline, memory) action call_llm_with_tools(client, messages, planner_tools) if action.name search: results web_search(action.args[query]) for r in results: summary summarize(r, action.args[query]) ev Evidence( evidence_idfev_{hash(r.url)}, source_urlr.url, summarysummary, raw_snippetr.snippet, ) memory.add(ev) # Observe: 把摘要回填给 Planner messages.append({role: tool, content: summary}) elif action.name write_outline: outline action.args[outline] elif action.name terminate: break return outline def writer_loop(client, outline, memory): report [] for section in outline: # Retrieve: 精准取回该节证据 evidences memory.retrieve(section[citations]) # Think: 内部推理合成见解 think internal_reasoning(client, section, evidences) # Write: 输出正文 content call_llm_with_tools( client, build_writer_messages(section, think), writer_tools, ) report.append(content) # Clear: 清理该节证据只留占位符 clear_context(section[section_id]) return \n\n.join(report)本地验证时先用 mock 搜索器返回 3 条固定片段跑一轮 Planner看它能不能生成带 citations 的大纲。成功标志是大纲里每个小节都有非空 citations且这些 ID 都能在记忆库里查到。然后跑 Writer检查每节正文是否引用了对应证据。我试过把max_rounds设成 3 和 8 对比轮次越多大纲越细但超过 8 轮后边际收益明显下降token 成本却线性涨。论文图 5 也显示大纲优化轮次与质量单调上升但实际工程里你要在成本和深度之间取平衡。验证成功的输出长这样{ outline: [ { section_id: s1, title: 帕金森病早期预警信号, goal: 列举运动与非运动早期信号, citations: [ev_a1, ev_b2] }, { section_id: s2, title: 术后护理要点, goal: 分阶段说明护理措施, citations: [ev_c3, ev_d4] } ] }如果你想让 Writer 用更小的模型跑建议先把工具调用格式固定成 few-shot 示例塞进 system prompt否则小模型很容易把retrieve的参数写成自然语言。论文的 SFT 实验本质上就是在教模型稳定输出这种结构化调用。5. 常见报错排查401、local proxy failed、reading choices、OAuth本地跑双 Agent 时报错基本集中在四类。我按真实遇到的顺序列出来你对照着查。第一类401 Unauthorized。这个最常见九成是 Key 没带对。检查TAOTOKEN_API_KEY有没有多余空格Base URL 是不是写成了带路径的https://taotoken.net/api/v1。正确写法就是https://taotoken.net/apiSDK 会自动拼/v1/chat/completions。如果还报 401去控制台重新生成一个 Key 再试。第二类local proxy failed或连接超时。这通常是你本地网络环境或代理配置干扰了请求。先确认没有设置HTTP_PROXY、HTTPS_PROXY环境变量再确认防火墙没拦 443。如果你在公司内网可能需要找网管放行taotoken.net。这类报错跟 Agent 逻辑无关先单独用 curl 测通再跑循环。第三类reading choices或KeyError: choices。这是响应体结构不符合预期常见于模型返回了错误 JSON 或者你用了不存在的 model ID。打印完整resp看error字段。如果是model not found去模型对话页面确认可用模型列表。如果是工具调用返回格式问题检查你的tools参数有没有传对有些模型要求tool_choiceauto显式声明。第四类OAuth相关报错。如果你用的是 Claude Code 或 Codex 这类带 OAuth 的客户端报错往往出在 token 刷新环节。这时候不要混用 OAuth 和 API Key 两套鉴权。用 API Key 就统一走base_urlapi_key别让客户端去读本地 OAuth 缓存。Claude Code 接入时Base URL、Key、Model ID 三件套必须同时配对缺一个都会在 OAuth 回调和 API 调用之间打架。还有一个隐蔽的坑Writer 的上下文清理如果没做干净第二轮 retrieve 会把上一节的证据也带进来导致章节间信息串味。排查方法是打印每轮 Writer 的 messages 长度正常应该随小节推进保持稳定而不是单调增长。如果一直涨说明clear_context没生效。6. 从跑通到跑好把双 Agent 研究流程用起来跑通最小闭环之后你可以按三个方向加深。第一把 mock 搜索器换成真实检索注意论文里的两阶段过滤先用标题和片段筛 URL再解析正文提取证据。这一步决定了引用准确率的上限。第二给 Planner 加一个「知识缺口检测」步骤让它每轮先输出当前缺什么再决定搜什么这样搜索查询会更聚焦。第三Writer 的内部推理步骤可以显式输出方便你调试它到底合成了什么见解而不是黑盒生成。如果你打算长期跑这类研究型 AgentCoding Plan 的额度模型比按次调用更适合高频循环https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan 。接入文档里也有 function calling 的完整参数说明遇到工具 schema 报错可以直接对照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。最后留一个我踩过的坑别一上来就追求两万字长报告。先用一个 3 小节的小题目把 Planner 和 Writer 的边界跑稳确认引用 ID 能对上、上下文能清干净再放大到 10 节以上。WebWeaver 的威力在长任务上才体现但长任务的排障成本也高循序渐进比一步到位靠谱得多。