現(xiàn)路徑)
1. 從「搜完就寫」到「邊搜邊改」WebWeaver 雙 Agent 框架到底解決了什么如果你讓一個普通 LLM 智能體去寫「帕金森病不同階段的預(yù)警信號及術(shù)后護(hù)理」這種題目大概率會看到兩種翻車現(xiàn)場。第一種是它先瘋狂搜索十幾輪把一堆網(wǎng)頁片段塞進(jìn)上下文然后一次性硬生成兩萬字結(jié)果中間章節(jié)開始胡編引用對不上號。第二種是它一開始就定死大綱后面搜到再好的證據(jù)也不改結(jié)構(gòu)最后報告像八股文深度全靠堆字?jǐn)?shù)。通義實(shí)驗室的 WebWeaver 就是沖著這兩個毛病去的。它把開放式深度研究OEDR拆成規(guī)劃Planning和寫作Writing兩個階段分別交給 Planner 和 Writer 兩個 Agent。Planner 負(fù)責(zé)在 ReAct 循環(huán)里反復(fù)「搜索—讀證據(jù)—改大綱」Writer 負(fù)責(zé)拿著帶引用 ID 的大綱逐節(jié)從記憶庫里精準(zhǔn)取證據(jù)、內(nèi)部推理、再落筆。核心檢索詞就是 WebWeaver 雙 Agent 框架、ReAct 推理機(jī)制、動態(tài)大綱優(yōu)化。它適合誰如果你正在做 LLM Agent 開發(fā)、RAG 長報告生成、或者想復(fù)現(xiàn)一個能跟做的多 Agent 研究流程這篇就是給你寫的。我下面會給出可復(fù)現(xiàn)的角色配置、工具調(diào)用鏈路、ReAct 循環(huán)偽代碼以及在本地環(huán)境跑通雙 Agent 協(xié)作的具體步驟。論文里最關(guān)鍵的三個設(shè)計是動態(tài)大綱協(xié)同進(jìn)化、記憶庫Memory Bank做上下文管理、Writer 的分層檢索與上下文清理。這三件事決定了它為什么能在 DeepResearch Bench 上把引用準(zhǔn)確率做到 93% 以上。先說清楚一個常見誤解WebWeaver 不是「兩個模型互相聊天」。Planner 和 Writer 共享同一個記憶庫但職責(zé)邊界非常硬。Planner 只輸出結(jié)構(gòu)化大綱和引用 ID不寫正文Writer 只消費(fèi)大綱和證據(jù)不改結(jié)構(gòu)。這種硬邊界是它能穩(wěn)定跑長任務(wù)的前提也是你在本地復(fù)現(xiàn)時最該先固定下來的部分。2. 前置準(zhǔn)備TaoToken 接入與本地環(huán)境依賴在本地驗證雙 Agent 協(xié)作之前你需要一個能穩(wěn)定調(diào)用 Claude 或 Qwen 系列模型的入口。我實(shí)測下來用 TaoToken 的 API 做基座調(diào)用比較省事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 的調(diào)用示例。本地環(huán)境我建議用 Python 3.10依賴三件套o(hù)penai走兼容接口、requests、pydantic。搜索引擎部分論文用的是網(wǎng)頁檢索本地復(fù)現(xiàn)你可以先用一個 mock 檢索器返回固定片段把 Agent 循環(huán)跑通再換成真實(shí)搜索 API。這樣排障成本最低。模型選擇上Planner 建議用推理能力強(qiáng)的模型Writer 可以用同款或稍小的模型。論文里 SFT 實(shí)驗用的是 Qwen3-30B微調(diào)后引用準(zhǔn)確率從 25% 提到 85.9%說明工具調(diào)用格式的穩(wěn)定性比模型大小更關(guān)鍵。你本地先用 Claude Sonnet 系列跑通流程再考慮換小模型。環(huán)境變量這樣配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: 只回復(fù) ok}], ) print(resp.choices[0].message.content)如果這里報 401先檢查 Key 有沒有復(fù)制完整如果報 model not found去模型對話頁面確認(rèn)當(dāng)前賬號可用的模型 IDhttps://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat 。這一步跑通再往下否則后面 Agent 循環(huán)里的報錯你分不清是邏輯問題還是鑒權(quán)問題。3. 可復(fù)制配置Planner 與 Writer 的角色定義與工具 Schema這一節(jié)是全文最核心的可復(fù)制部分。我按論文的行動空間把兩個 Agent 的 system prompt、工具 JSON Schema、以及記憶庫結(jié)構(gòu)都寫成可直接落地的片段。你新建一個webweaver_config.json把下面內(nèi)容貼進(jìn)去。Planner 的行動空間是search、write_outline、terminate。Writer 的行動空間是retrieve、write、terminate。工具定義用 OpenAI function calling 格式{ planner_tools: [ { type: function, function: { name: search, description: 根據(jù)當(dāng)前知識缺口發(fā)起網(wǎng)頁搜索返回候選 URL 與片段, parameters: { type: object, properties: { query: {type: string, description: 搜索查詢詞}, top_k: {type: integer, default: 5} }, required: [query] } } }, { type: function, function: { name: write_outline, description: 新增或優(yōu)化大綱每個小節(jié)必須帶 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: 當(dāng)大綱覆蓋全面且證據(jù)充分時終止規(guī)劃, parameters: {type: object, properties: {}} } } ], writer_tools: [ { type: function, function: { name: retrieve, description: 按引用 ID 從記憶庫取回原始證據(jù), parameters: { type: object, properties: { citation_ids: {type: array, items: {type: string}} }, required: [citation_ids] } } }, { type: function, function: { name: write, description: 輸出當(dāng)前小節(jié)的正文用 write 標(biāo)簽包裹, parameters: { type: object, properties: { section_id: {type: string}, content: {type: string} }, required: [section_id, content] } } }, { type: function, function: { name: terminate, description: 所有小節(jié)寫完時終止, parameters: {type: object, properties: {}} } } ] }記憶庫結(jié)構(gòu)用 Pydantic 定義每條證據(jù)必須有唯一 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 關(guān)鍵約束我寫成這樣你可以直接抄你是研究規(guī)劃者。每輪先思考當(dāng)前大綱缺什么證據(jù)再決定調(diào)用 search 還是 write_outline。search 返回的片段只用于決策原始證據(jù)由系統(tǒng)寫入記憶庫。write_outline 必須為每個小節(jié)標(biāo)注 citations引用 ID 來自記憶庫。當(dāng)大綱覆蓋主題且每個小節(jié)都有至少 2 條證據(jù)時調(diào)用 terminate。Writer 的 system prompt你是報告寫作者。按大綱順序逐節(jié)處理。每節(jié)先調(diào)用 retrieve 取回該節(jié) citations 對應(yīng)的證據(jù)在內(nèi)部推理中合成關(guān)鍵見解再調(diào)用 write 輸出正文。寫完一節(jié)后該節(jié)證據(jù)從上下文清除只保留占位符。全部小節(jié)完成后調(diào)用 terminate。這里有個容易踩的坑Planner 的write_outline每次返回的是完整大綱還是增量論文里是「回顧并優(yōu)化」我建議實(shí)現(xiàn)成完整大綱覆蓋這樣狀態(tài)機(jī)簡單不會出現(xiàn)增量合并沖突。代價是 token 多一點(diǎn)但排障容易得多。4. ReAct 循環(huán)偽代碼與本地驗證跑通雙 Agent 協(xié)作配置就緒后核心是一個雙層循環(huán)。外層是 Planner 的 ReAct 循環(huán)內(nèi)層是 Writer 的逐節(jié)寫作循環(huán)。偽代碼如下我把它寫成接近可運(yùn)行的 Pythondef planner_react_loop(client, memory, max_rounds8): outline [] for step in range(max_rounds): # Reason: 讓模型基于當(dāng)前大綱和記憶庫摘要決策 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: 精準(zhǔn)取回該節(jié)證據(jù) evidences memory.retrieve(section[citations]) # Think: 內(nèi)部推理合成見解 think internal_reasoning(client, section, evidences) # Write: 輸出正文 content call_llm_with_tools( client, build_writer_messages(section, think), writer_tools, ) report.append(content) # Clear: 清理該節(jié)證據(jù)只留占位符 clear_context(section[section_id]) return \n\n.join(report)本地驗證時先用 mock 搜索器返回 3 條固定片段跑一輪 Planner看它能不能生成帶 citations 的大綱。成功標(biāo)志是大綱里每個小節(jié)都有非空 citations且這些 ID 都能在記憶庫里查到。然后跑 Writer檢查每節(jié)正文是否引用了對應(yīng)證據(jù)。我試過把max_rounds設(shè)成 3 和 8 對比輪次越多大綱越細(xì)但超過 8 輪后邊際收益明顯下降token 成本卻線性漲。論文圖 5 也顯示大綱優(yōu)化輪次與質(zhì)量單調(diào)上升但實(shí)際工程里你要在成本和深度之間取平衡。驗證成功的輸出長這樣{ outline: [ { section_id: s1, title: 帕金森病早期預(yù)警信號, goal: 列舉運(yùn)動與非運(yùn)動早期信號, citations: [ev_a1, ev_b2] }, { section_id: s2, title: 術(shù)后護(hù)理要點(diǎn), goal: 分階段說明護(hù)理措施, citations: [ev_c3, ev_d4] } ] }如果你想讓 Writer 用更小的模型跑建議先把工具調(diào)用格式固定成 few-shot 示例塞進(jìn) system prompt否則小模型很容易把retrieve的參數(shù)寫成自然語言。論文的 SFT 實(shí)驗本質(zhì)上就是在教模型穩(wěn)定輸出這種結(jié)構(gòu)化調(diào)用。5. 常見報錯排查401、local proxy failed、reading choices、OAuth本地跑雙 Agent 時報錯基本集中在四類。我按真實(shí)遇到的順序列出來你對照著查。第一類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或連接超時。這通常是你本地網(wǎng)絡(luò)環(huán)境或代理配置干擾了請求。先確認(rèn)沒有設(shè)置HTTP_PROXY、HTTPS_PROXY環(huán)境變量再確認(rèn)防火墻沒攔 443。如果你在公司內(nèi)網(wǎng)可能需要找網(wǎng)管放行taotoken.net。這類報錯跟 Agent 邏輯無關(guān)先單獨(dú)用 curl 測通再跑循環(huán)。第三類reading choices或KeyError: choices。這是響應(yīng)體結(jié)構(gòu)不符合預(yù)期常見于模型返回了錯誤 JSON 或者你用了不存在的 model ID。打印完整resp看error字段。如果是model not found去模型對話頁面確認(rèn)可用模型列表。如果是工具調(diào)用返回格式問題檢查你的tools參數(shù)有沒有傳對有些模型要求tool_choiceauto顯式聲明。第四類OAuth相關(guān)報錯。如果你用的是 Claude Code 或 Codex 這類帶 OAuth 的客戶端報錯往往出在 token 刷新環(huán)節(jié)。這時候不要混用 OAuth 和 API Key 兩套鑒權(quán)。用 API Key 就統(tǒng)一走base_urlapi_key別讓客戶端去讀本地 OAuth 緩存。Claude Code 接入時Base URL、Key、Model ID 三件套必須同時配對缺一個都會在 OAuth 回調(diào)和 API 調(diào)用之間打架。還有一個隱蔽的坑Writer 的上下文清理如果沒做干凈第二輪 retrieve 會把上一節(jié)的證據(jù)也帶進(jìn)來導(dǎo)致章節(jié)間信息串味。排查方法是打印每輪 Writer 的 messages 長度正常應(yīng)該隨小節(jié)推進(jìn)保持穩(wěn)定而不是單調(diào)增長。如果一直漲說明clear_context沒生效。6. 從跑通到跑好把雙 Agent 研究流程用起來跑通最小閉環(huán)之后你可以按三個方向加深。第一把 mock 搜索器換成真實(shí)檢索注意論文里的兩階段過濾先用標(biāo)題和片段篩 URL再解析正文提取證據(jù)。這一步?jīng)Q定了引用準(zhǔn)確率的上限。第二給 Planner 加一個「知識缺口檢測」步驟讓它每輪先輸出當(dāng)前缺什么再決定搜什么這樣搜索查詢會更聚焦。第三Writer 的內(nèi)部推理步驟可以顯式輸出方便你調(diào)試它到底合成了什么見解而不是黑盒生成。如果你打算長期跑這類研究型 AgentCoding Plan 的額度模型比按次調(diào)用更適合高頻循環(huán)https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan 。接入文檔里也有 function calling 的完整參數(shù)說明遇到工具 schema 報錯可以直接對照https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。最后留一個我踩過的坑別一上來就追求兩萬字長報告。先用一個 3 小節(jié)的小題目把 Planner 和 Writer 的邊界跑穩(wěn)確認(rèn)引用 ID 能對上、上下文能清干凈再放大到 10 節(jié)以上。WebWeaver 的威力在長任務(wù)上才體現(xiàn)但長任務(wù)的排障成本也高循序漸進(jìn)比一步到位靠譜得多。