建一個(gè) Agent(TaoToken 配置骨架版))
1. 從 OpenClaw 源碼看 Agent 到底由什么組成OpenClaw 是一個(gè)跑在生產(chǎn)環(huán)境里的 AI Agent 框架代碼量不小但核心就四個(gè)模塊——執(zhí)行循環(huán)、工具系統(tǒng)、記憶系統(tǒng)、插件系統(tǒng)。這篇文章把每個(gè)模塊拆開看看里面怎么寫的最后整理一份自己做 Agent 時(shí)可以參考的清單并且給出一份可以直接復(fù)制的 TaoToken 配置骨架讓你在本地把 Agent 跑起來。如果你熟悉 TypeScript 和 LLM API 的基本概念讀起來會很順。如果你還不了解 Function Calling建議先補(bǔ)一下工具調(diào)用Tool Use的基礎(chǔ)概念再回來看源碼結(jié)構(gòu)會清晰很多。一句話概括 OpenClaw 的架構(gòu)Gateway 接收消息 → Agent 循環(huán)調(diào)用 LLM 工具 → 記憶系統(tǒng)提供上下文 → 插件擴(kuò)展一切。四個(gè)模塊各管各的耦合度不高。先掃一眼目錄結(jié)構(gòu)目錄一句話說明src/agents/Agent 執(zhí)行循環(huán)、工具注冊、模型管理src/memory/記憶索引、嵌入向量、混合檢索src/gateway/WebSocket 網(wǎng)關(guān)、認(rèn)證、RPCsrc/plugin-sdk/插件 SDK、Hook 系統(tǒng)src/channels/通道抽象層狀態(tài)機(jī)、路由、線程綁定extensions/73 個(gè)插件通道 / LLM Provider / 工具擴(kuò)展這篇的重點(diǎn)不是把每個(gè)文件都念一遍而是把「構(gòu)建一個(gè) Agent 需要哪些零件」講清楚然后給你一份能直接跑的配置骨架。LLM 調(diào)用通道這塊我用 TaoToken 做統(tǒng)一入口一個(gè) Key 就能覆蓋多種模型省得在多個(gè) Provider 之間來回切。2. TaoToken 前置統(tǒng)一 Key 與 API 通道在動(dòng)手寫 Agent 之前先把 LLM 調(diào)用通道準(zhǔn)備好。OpenClaw 的模型管理模塊src/agents/ 下的 Provider 相關(guān)代碼本質(zhì)上就是維護(hù)一組「Provider Auth Profile 模型名」的映射然后按優(yōu)先級做 Failover。你自己做 Agent 時(shí)如果每個(gè) Provider 都單獨(dú)配 Key、單獨(dú)處理限流和重試代碼會迅速膨脹。TaoToken 在這里扮演的角色是統(tǒng)一入口一個(gè) API Key一個(gè) Base URL就能調(diào)用多種模型。對 Agent 來說這意味著你的 LLM 調(diào)用層只需要維護(hù)一套認(rèn)證邏輯模型切換只是改一個(gè)字符串。你需要準(zhǔn)備的東西一個(gè) TaoToken 賬號登錄后在控制臺創(chuàng)建 API Key記下 API Base URLhttps://taotoken.net/api選一個(gè)默認(rèn)模型名比如claude-sonnet-4-20250514或gpt-4o具體以控制臺模型列表為準(zhǔn)控制臺入口在這里https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Key 管理頁面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite拿到 Key 之后先別急著寫 Agent用一條 curl 驗(yàn)證通道是否通curl https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: 只回復(fù)兩個(gè)字通了}], max_tokens: 32 }如果返回里能看到choices[0].message.content說明通道沒問題。這一步很重要因?yàn)楹竺?Agent 報(bào)錯(cuò)時(shí)你需要快速判斷是「通道問題」還是「Agent 邏輯問題」。把通道單獨(dú)驗(yàn)證過排障范圍就縮小了一半。注意API Key 不要硬編碼進(jìn)源碼。用環(huán)境變量TAOTOKEN_API_KEY或者放進(jìn).env文件并加進(jìn).gitignore。3. 可復(fù)制配置settings.json 與 config.toml 骨架OpenClaw 的配置入口分散在幾個(gè)地方但核心就兩類一類是「運(yùn)行時(shí)配置」模型、通道、記憶一類是「Agent 行為配置」System Prompt、工具開關(guān)、循環(huán)上限。下面給兩份骨架你可以直接復(fù)制到自己的項(xiàng)目里改。3.1 settings.jsonAgent 運(yùn)行時(shí)配置這份配置對應(yīng) OpenClaw 里src/agents/和src/memory/的初始化參數(shù)。我把它整理成一份扁平結(jié)構(gòu)方便你對照源碼理解每個(gè)字段的作用{ agent: { id: my-first-agent, name: 本地 Agent 雛形, maxToolRounds: 12, stream: true, systemPromptFile: ./prompts/system.md }, llm: { provider: taotoken, baseUrl: https://taotoken.net/api, apiKeyEnv: TAOTOKEN_API_KEY, model: claude-sonnet-4-20250514, fallbackModels: [gpt-4o, claude-haiku-4-20250514], timeoutMs: 60000, maxRetries: 3 }, tools: { enabled: [read_file, write_file, exec, web_search], requireConfirm: [write_file, exec], loopDetection: { windowSize: 30, warnAt: 10, stopAt: 20, abortAt: 30 } }, memory: { enabled: true, dbPath: ./data/memory.sqlite, embeddingProvider: taotoken, embeddingModel: text-embedding-3-small, hybridWeights: { vector: 0.7, bm25: 0.3 }, temporalDecayHalfLifeDays: 30 } }幾個(gè)字段值得單獨(dú)說maxToolRounds對應(yīng) OpenClaw 里工具循環(huán)的上限。設(shè)太小復(fù)雜任務(wù)跑不完設(shè)太大一旦邏輯出錯(cuò)會燒很多 token。12 是個(gè)比較穩(wěn)的起步值。fallbackModels就是 Failover 的簡化版。主模型限流或報(bào)錯(cuò)時(shí)按順序切下一個(gè)。OpenClaw 的run.ts里做得更細(xì)會冷卻出問題的 Auth Profile但核心思路一致。loopDetection直接抄了 OpenClaw 的三級熔斷10 次警告、20 次強(qiáng)制提示、30 次終止?;瑒?dòng)窗口大小 30 是它的默認(rèn)值。3.2 config.toml通道與網(wǎng)關(guān)配置如果你要接消息通道Discord、Slack、Web UI這部分對應(yīng)src/gateway/和src/channels/[gateway] host 127.0.0.1 port 8787 auth_token_env GATEWAY_TOKEN [channels.web] enabled true path /chat [channels.discord] enabled false bot_token_env DISCORD_BOT_TOKEN [state_machine] idle_timeout_sec 300 max_concurrent_sessions 4 [logging] level info file ./logs/agent.logstate_machine這段對應(yīng) OpenClaw 的src/channels/run-state-machine.ts。每個(gè)會話有獨(dú)立狀態(tài)idle → running → drafting → completed保證同一會話不會被并發(fā)請求搞亂。你自己做的時(shí)候哪怕先不做完整狀態(tài)機(jī)至少也要給每個(gè)會話加一把鎖。3.3 System Prompt 骨架OpenClaw 的system-prompt.ts是動(dòng)態(tài)拼裝的——運(yùn)行時(shí)信息、工具列表、通道能力、用戶指令按需注入。你可以先從一個(gè)靜態(tài)文件開始你是運(yùn)行在本地環(huán)境中的 AI Agent。 ## 運(yùn)行環(huán)境 - 操作系統(tǒng){{os}} - 當(dāng)前時(shí)間{{now}} - 工作目錄{{cwd}} ## 可用工具 {{tool_list}} ## 行為準(zhǔn)則 1. 需要讀取文件時(shí)先調(diào)用 read_file不要憑記憶猜測內(nèi)容。 2. 執(zhí)行有副作用的操作寫文件、跑命令前先說明你要做什么。 3. 如果連續(xù)兩次工具調(diào)用沒有進(jìn)展停下來向用戶確認(rèn)。{{tool_list}}由代碼在啟動(dòng)時(shí)注入格式就是工具名 描述。LLM 靠這段描述判斷什么時(shí)候該調(diào)哪個(gè)工具所以描述要寫清楚「做什么」和「什么時(shí)候用」。4. 驗(yàn)證請求本地啟動(dòng)并跑通一次 Agent 響應(yīng)配置寫好了接下來把它跑起來。下面給一個(gè)最小可運(yùn)行的 TypeScript 入口對應(yīng) OpenClaw 的src/agents/pi-embedded-runner/run/attempt.ts——單次 LLM 調(diào)用加工具循環(huán)。4.1 安裝依賴npm init -y npm install openai dotenv npm install -D typescript tsx types/node這里用openai這個(gè) SDK 就行因?yàn)?TaoToken 的 API 兼容 OpenAI 的請求格式改一下baseURL就能用。4.2 核心循環(huán)代碼// src/agent.ts import OpenAI from openai; import dotenv/config; const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY!, baseURL: https://taotoken.net/api/v1, }); const tools [ { type: function as const, function: { name: read_file, description: 讀取指定路徑的文件內(nèi)容, parameters: { type: object, properties: { path: { type: string } }, required: [path], }, }, }, ]; async function runAgent(userInput: string) { const messages: OpenAI.Chat.ChatCompletionMessageParam[] [ { role: system, content: 你是一個(gè)本地 Agent需要讀文件時(shí)調(diào)用 read_file。 }, { role: user, content: userInput }, ]; for (let round 0; round 12; round) { const res await client.chat.completions.create({ model: claude-sonnet-4-20250514, messages, tools, stream: false, }); const msg res.choices[0].message; messages.push(msg); if (!msg.tool_calls || msg.tool_calls.length 0) { console.log(最終回復(fù), msg.content); return msg.content; } for (const call of msg.tool_calls) { const args JSON.parse(call.function.arguments); let result ; if (call.function.name read_file) { try { result await import(fs/promises).then((fs) fs.readFile(args.path, utf-8)); } catch (e) { result 讀取失敗${(e as Error).message}; } } messages.push({ role: tool, tool_call_id: call.id, content: result.slice(0, 4000), }); } } throw new Error(工具循環(huán)超過上限已終止); } runAgent(讀一下 package.json告訴我項(xiàng)目名和依賴數(shù)量);4.3 運(yùn)行與預(yù)期結(jié)果npx tsx src/agent.ts正常的話你會看到 Agent 先發(fā)起一次read_file工具調(diào)用拿到文件內(nèi)容后再生成一段自然語言回復(fù)類似最終回復(fù)項(xiàng)目名是 my-agent-demodependencies 里有 2 個(gè)依賴openai 和 dotenv。這個(gè)過程就是 Agent 的最小骨架LLM 決定調(diào)工具 → 代碼執(zhí)行工具 → 結(jié)果喂回 LLM → LLM 生成最終回復(fù)。OpenClaw 的attempt.ts做的也是這件事只是外面包了流式處理、容錯(cuò)、上下文壓縮。4.4 加上流式輸出把stream: false改成true然后處理text_delta事件用戶就能邊生成邊看到字。OpenClaw 的pi-embedded-subscribe.ts就是干這個(gè)的它還會在語義邊界處切分文本塊避免把半個(gè)句子推給用戶。const stream await client.chat.completions.create({ model: claude-sonnet-4-20250514, messages, tools, stream: true, }); for await (const chunk of stream) { const delta chunk.choices[0]?.delta; if (delta?.content) process.stdout.write(delta.content); }流式模式下工具調(diào)用的參數(shù)是分片到達(dá)的需要自己拼接tool_calls的arguments字符串等finish_reason變成tool_calls再解析。這是新手最容易踩的坑之一。5. 本篇常見錯(cuò)排查5.1 401 / 403Key 沒讀到或格式不對最常見的原因是環(huán)境變量沒加載。dotenv/config要在最頂部導(dǎo)入且.env文件里寫的是TAOTOKEN_API_KEYsk-xxx不要加引號。如果用的是 shell 導(dǎo)出確認(rèn)echo $TAOTOKEN_API_KEY有輸出。另一個(gè)原因是 Base URL 寫錯(cuò)。注意區(qū)分https://taotoken.net/api是根路徑SDK 里通常要寫https://taotoken.net/api/v1。如果你用的是原生 fetch 拼/v1/chat/completions那就用根路徑。5.2 模型名報(bào)錯(cuò) model_not_found模型名要以控制臺模型列表為準(zhǔn)不要憑記憶寫。不同 Provider 的命名風(fēng)格不一樣有的帶日期后綴有的不帶。建議把模型名放進(jìn)配置文件別散落在代碼里。5.3 工具調(diào)用死循環(huán)表現(xiàn)是 Agent 反復(fù)調(diào)同一個(gè)工具token 嘩嘩燒。原因通常是工具返回的結(jié)果 LLM 看不懂或者 System Prompt 沒告訴它「拿到結(jié)果后該干什么」。排查方法把每輪的工具調(diào)用名和參數(shù)打出來看是不是同一個(gè)調(diào)用重復(fù)出現(xiàn)。修復(fù)方法有兩個(gè)一是把工具返回內(nèi)容結(jié)構(gòu)化別返回一坨亂碼二是在循環(huán)里加計(jì)數(shù)超過閾值就強(qiáng)制讓 LLM 生成最終回復(fù)。const callCount new Mapstring, number(); // 在每次工具調(diào)用前 const key ${call.function.name}:${call.function.arguments}; const n (callCount.get(key) ?? 0) 1; callCount.set(key, n); if (n 3) { messages.push({ role: user, content: 同一工具已重復(fù)調(diào)用多次請基于現(xiàn)有信息直接回答。 }); }5.4 上下文超長導(dǎo)致請求失敗長對話跑到后面messages 數(shù)組會超過模型窗口。OpenClaw 的做法是用 Context Engine 對歷史做摘要壓縮compact.ts而不是簡單砍掉前面的消息。你自己實(shí)現(xiàn)時(shí)可以先做一個(gè)簡化版保留 System Prompt 和最近 N 輪對話把更早的內(nèi)容用一次 LLM 調(diào)用總結(jié)成一段話。5.5 流式模式下工具調(diào)用解析失敗前面提過流式返回的tool_calls是分片的。delta.tool_calls[0].function.arguments每次只給你一小段 JSON 字符串需要按index累積拼接等流結(jié)束后再JSON.parse。直接對每個(gè) chunk 解析會報(bào)Unexpected end of JSON input。5.6 記憶檢索返回空結(jié)果如果你接了記憶系統(tǒng)搜索時(shí)返回空先檢查三件事嵌入模型是否配置正確、SQLite 里chunks表是否有數(shù)據(jù)、查詢向量維度是否和存儲時(shí)一致。維度不一致是最隱蔽的坑比如存儲用 1536 維查詢用了 1024 維的模型相似度計(jì)算會直接失效。6. 把 Agent 跑起來之后下一步做什么到這里你已經(jīng)有了一個(gè)能跑的最小 Agent統(tǒng)一 Key 通道、可復(fù)制配置、工具循環(huán)、流式輸出、基礎(chǔ)排障。接下來按優(yōu)先級補(bǔ)三件事。第一是容錯(cuò)。裸循環(huán)跑 demo 沒問題上線必須加 Auth Failover 和上下文壓縮。前者解決限流和 Key 過期后者解決長對話崩潰。這兩塊 OpenClaw 的run.ts和compact.ts都有現(xiàn)成思路可以抄。第二是記憶。讓 Agent 從「一次性對話」變成「持續(xù)助手」最小實(shí)現(xiàn)就是 SQLite 加向量檢索加 FTS5 全文兜底。嵌入模型選text-embedding-3-small就夠用便宜且穩(wěn)定?;旌蠙z索權(quán)重先用 0.7 向量加 0.3 BM25跑一段時(shí)間再調(diào)。第三是擴(kuò)展。等你要接第二個(gè)通道、加第三個(gè)工具的時(shí)候再考慮插件化和 Hook 系統(tǒng)。不用一開始就做 25 個(gè) Hook先從before_prompt_build和before_tool_call這兩個(gè)高頻的做起。如果你在接入過程中遇到通道或 Key 的問題可以直接去 API Keys 頁面重新生成一個(gè)驗(yàn)證https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite想先驗(yàn)證模型響應(yīng)是否符合預(yù)期可以用模型對話頁面快速試一條https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite如果你打算長期跑編碼類 Agent 或做多輪工具調(diào)用Coding Plan 會更省心額度和模型調(diào)度都幫你管好了https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite接入文檔在這里配置字段和錯(cuò)誤碼都有說明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite我自己的習(xí)慣是每加一個(gè)新工具先用模型對話頁面手動(dòng)構(gòu)造一次工具調(diào)用請求確認(rèn)返回格式對了再寫進(jìn) Agent 代碼。這樣能把「模型問題」和「代碼問題」分開排障快很多。