
1. 從零到一為什么我們需要一個純 TypeScript 的 Agent 調度系統最近在折騰 AI Agent 項目特別是想把一些想法落地成可用的服務時遇到了一個挺典型的問題市面上現成的 Agent 框架要么太重像 LangChain為了通用性封裝了太多層調試起來像在走迷宮要么就是生態綁定太深比如一些框架強依賴特定的云服務或運行時想在自己的服務器上跑或者想深度定制一下調度邏輯感覺處處掣肘。更別提那些用 Python 寫的框架雖然生態好但想在 Node.js 或瀏覽器端復用核心邏輯基本就得重寫一遍。這時候一個叫 OpenClaw 的項目進入了我的視野。它最吸引我的點就是標題里寫的用純 TypeScript 造了一套 Agent 調度系統。這聽起來像是個“輪子”但在 AI 應用開發尤其是 Agent 領域這個“輪子”可能恰恰是很多團隊缺的那一個。TypeScript 意味著什么首先它是 JavaScript 的超集能編譯成純凈的 JS這帶來了無與倫比的運行時靈活性。你的調度核心可以跑在 Node.js 服務器上可以跑在瀏覽器里甚至可以打包進 Electron 桌面應用或者 React Native 移動端。一次編寫多處部署這對于需要跨端能力的 AI 應用來說價值巨大。其次TypeScript 的靜態類型系統在構建像“調度系統”這樣復雜的、狀態機式的邏輯時簡直是救命稻草。Agent 的執行流往往涉及多個步驟、條件分支、異步操作和共享狀態。用純 JS 寫很容易就變成“面條代碼”調試時一個變量的類型搞錯可能就得花半天時間。TypeScript 能在編碼階段就幫你卡住很多低級錯誤并且它的接口Interface和類型別名Type Alias非常適合用來定義 Agent 的“技能”Skill、工具Tool、以及執行上下文Context的數據結構讓整個系統的設計從一開始就清晰可控。OpenClaw 選擇純 TypeScript在我看來不是炫技而是針對 Agent 開發中“快速迭代”和“可靠部署”這兩個核心痛點的務實選擇。它試圖提供一套輕量、類型安全、不綁定特定后端的調度內核讓開發者能更專注于 Agent 本身的行為邏輯而不是陷在框架的復雜性里。接下來我們就深入這套系統的內部看看它是如何被“造”出來的。2. 調度系統的核心架構事件驅動與工作流引擎OpenClaw 的調度系統其核心思想可以概括為“事件驅動的工作流”。它沒有采用一些傳統后臺服務那種復雜的隊列和消費者模型而是設計了一個更貼合 Agent 交互場景的輕量級中樞。整個架構圍繞幾個關鍵概念展開理解了它們就理解了調度的脈絡。2.1 調度中樞SVR Operator 與事件總線在 OpenClaw 的代碼中你經常會看到一個核心類比如叫SvrOperator。這個 Operator 不是 Kubernetes 里那個 Operator在這里你可以把它理解為“服務操作員”或“調度員”。它是整個調度系統的入口和總控。它的核心職責是接收外部的請求比如來自 HTTP API、WebSocket 消息、命令行指令這個請求通常包含了要執行哪個 Agent、以及初始的輸入參數。SvrOperator會將這些請求轉化成一個標準的內部事件比如AgentExecutionRequestEvent然后拋給系統內部的事件總線Event Bus。這里的事件總線是一個典型的發布-訂閱模式實現。為什么用事件驅動因為 Agent 的執行過程本質上是異步的、離散的。一個 Agent 執行一個技能Skill可能需要調用語言模型LLM、查詢數據庫、執行一段代碼每一步都可能成功或失敗都可能產生需要后續步驟處理的數據。用事件來串聯這些狀態變化比用一個大而全的同步函數調用鏈要清晰和靈活得多。每個模塊如技能執行器、工具調用器、狀態管理器只監聽自己關心的事件完成自己的工作后再發出新的事件從而驅動流程向下進行。這種松耦合的設計也使得擴展新的技能或工具變得非常容易你只需要編寫一個新的監聽器Listener并注冊到總線即可。一個常見的錯誤提示比如openclaw llamap svr operator(): got exception: { error: { code: 400, me...往往就發生在SvrOperator處理請求的初始階段。這可能是請求格式不符合預期、必要的參數缺失、或者請求的 Agent 或 Skill 不存在。好的調度系統會在這一層就做好完備的請求驗證和錯誤格式化把問題盡可能早地暴露出來而不是讓錯誤滲透到后續復雜的執行鏈路中。2.2. 工作流定義用 TypeScript 類型描述執行藍圖Agent 不是一個黑盒函數它通常有一個預設的執行流程也就是工作流Workflow。OpenClaw 如何定義這個流程它充分利用了 TypeScript 的類型能力。通常我們會用一個 TypeScript 接口Interface或類型別名Type來定義一個工作流。這個類型可能長這樣interface AgentWorkflow { id: string; entrySkill: string; // 入口技能如 “analyze_user_query” skills: { [skillName: string]: { execute: (context: WorkflowContext) PromiseSkillResult; next?: string | ((result: SkillResult) string); // 下一個技能名或根據結果決定的函數 onError?: string; // 出錯時跳轉到哪個技能如 “handle_error” }; }; }這里WorkflowContext是一個貫穿整個工作流執行過程的上下文對象它用 TypeScript 嚴格定義了在每個階段可以存取的數據結構。比如interface WorkflowContext { sessionId: string; userInput: string; llmResponse?: string; extractedData?: Recordstring, any; error?: Error; // ... 其他自定義字段 }通過類型定義我們在編碼時就能清晰地知道在執行“分析用戶查詢”這個技能后context.llmResponse字段會被填充在執行“數據提取”技能時我們可以安全地讀取llmResponse并期望它是字符串類型。這種編譯時的安全保障是純 JavaScript 項目難以企及的。工作流引擎的職責就是根據這個藍圖監聽技能執行完成的事件然后查找next規則決定下一個要執行的技能并再次派發事件。它可能還需要處理循環、條件分支比如根據結果決定走 A 路徑還是 B 路徑、以及并行執行等復雜邏輯。OpenClaw 的實現通常會有一個WorkflowExecutor類它內部維護著當前執行到了哪個技能、上下文狀態是什么并作為事件總線的一個主要監聽者和驅動者。2.3. 技能Skill與工具Tool的注冊與發現技能是 Agent 能力的原子單位。一個“總結文檔”的技能內部可能調用了“調用 LLM API”和“解析 Markdown”兩個工具。OpenClaw 需要一套機制來管理這些技能和工具。通常會有一個全局的注冊中心Registry。在系統初始化時所有定義好的技能和工具模塊會向這個注冊中心“報到”登記自己的名字、描述、輸入輸出參數類型等信息。這個注冊過程同樣可以借助 TypeScript 的裝飾器Decorator來實現讓代碼看起來非常清晰Skill({ name: summarize_document, description: 總結一篇長文檔的核心內容 }) export class SummarizeDocumentSkill implements ISkill { async execute(context: WorkflowContext): PromiseSkillResult { // 1. 從 context 中獲取文檔內容 const doc context.documentContent; // 2. 調用 LLM 工具 const llmResult await ToolRegistry.getTool(call_llm).invoke({ model: gpt-4, prompt: 請總結以下文檔\n${doc} }); // 3. 將結果存入 context context.summary llmResult.content; return { success: true, output: context.summary }; } }工具Tool的注冊也類似它們更像是底層的、可復用的功能函數比如 HTTP 請求、數據庫查詢、代碼執行等。調度系統在需要調用工具時不會硬編碼而是通過注冊中心按名查找這實現了徹底的解耦。當你需要新增一個工具時只需要編寫實現類并注冊所有技能都能立即使用它無需修改調度核心代碼。這種基于注冊的模式也使得 OpenClaw 能夠實現類似“技能市場”或動態加載的功能。理論上你可以從遠程加載一個符合接口規范的技能模塊在運行時注冊進去Agent 就立刻獲得了新能力。3. 狀態管理、持久化與容錯機制一個健壯的調度系統不能是“一錘子買賣”。Agent 與用戶的對話可能是多輪的一個復雜任務可能被中斷后需要恢復。因此OpenClaw 必須考慮狀態管理和持久化。3.1. 會話狀態與上下文持久化每一次用戶與 Agent 的交互通常會被關聯到一個唯一的會話 IDSession ID。WorkflowContext對象就是這個會話在內存中的實時狀態。但是內存狀態是脆弱的服務重啟就沒了。所以調度系統需要將關鍵的上下文狀態持久化到外部存儲比如 Redis、數據庫或文件系統。OpenClaw 的做法通常是在工作流引擎的某些關鍵節點例如一個技能執行完成后、或等待外部輸入時觸發持久化操作。它不會每次都全量保存而是可能采用快照Snapshot機制。定義一個ContextPersistenceService其接口可能是interface IContextPersistenceService { save(sessionId: string, contextSnapshot: PartialWorkflowContext): Promisevoid; load(sessionId: string): PromiseWorkflowContext | null; }在持久化時一個重要的細節是序列化。WorkflowContext里可能包含復雜的對象、甚至函數雖然不推薦。純 TypeScript/JavaScript 環境里直接用JSON.stringify可能會丟失信息如 Date 對象變成字符串undefined 字段被忽略。因此OpenClaw 可能需要引入一個序列化庫或者自定義一套序列化規則確保上下文恢復后類型和結構依然正確。3.2. 錯誤處理與重試邏輯在熱詞里我們看到openclaw llamap svr operator(): got exception錯誤處理是調度系統必須精心設計的部分。錯誤可能發生在各個層面技能執行錯誤比如調用 LLM API 超時、返回格式異常。工具調用錯誤比如數據庫連接失敗、第三方服務不可用。工作流邏輯錯誤比如next指向了一個不存在的技能。OpenClaw 的調度系統需要有一個統一的錯誤捕獲和分發機制。事件總線在這里再次發揮作用。任何一個技能或工具在執行中拋出的異常不應該直接導致整個進程崩潰而應該被包裝成一個AgentExecutionErrorEvent事件。工作流引擎監聽到這個錯誤事件后會根據當前技能定義中的onError字段決定錯誤處理路徑。例如可以跳轉到一個專門的“錯誤處理”技能這個技能可能會嘗試重試原操作對于網絡波動錯誤、或者向用戶發送友好的錯誤信息、亦或是將任務標記為失敗并通知管理員。對于可重試的錯誤如網絡超時調度系統可以實現一個簡單的重試機制。但這需要謹慎對于非冪等的操作如創建訂單盲目重試會導致嚴重問題。因此重試邏輯最好與具體技能/工具綁定由開發者根據業務語義來決定調度系統只提供重試的基礎設施比如一個Retry(maxAttempts: 3)的裝飾器。3.3. 超時控制與資源隔離Agent 任務可能陷入死循環或者某個外部調用永遠不返回。調度系統必須有能力強制終止長時間運行的任務。這可以通過為每個工作流的執行設置一個全局超時或者為每個技能設置單獨的超時來實現。在實現上可以利用 JavaScript 的Promise.race或AbortController。當啟動一個技能執行時同時啟動一個定時器。如果技能在超時前完成則取消定時器如果定時器先觸發則向技能執行發送中止信號并拋出超時錯誤事件。async executeSkillWithTimeout(skill: ISkill, context: WorkflowContext, timeoutMs: number): PromiseSkillResult { const abortController new AbortController(); const timeoutId setTimeout(() abortController.abort(), timeoutMs); try { // 將 abortController.signal 傳遞給技能技能內部需要支持中止 const result await skill.execute(context, abortController.signal); clearTimeout(timeoutId); return result; } catch (error) { clearTimeout(timeoutId); if (error.name AbortError) { throw new AgentExecutionError(Skill execution timeout, TIMEOUT); } throw error; } }資源隔離則更為復雜。在 Node.js 環境下多個 Agent 會話共享同一個進程內存。如果一個技能有內存泄漏或者某個任務消耗了巨量 CPU可能會影響其他任務。OpenClaw 作為輕量級調度系統可能不會實現完整的沙箱隔離但可以通過一些模式來緩解比如限制單個技能的執行時間超時控制、監控進程內存使用并在超過閾值時報警或重啟、以及最重要的——在技能開發規范中強調資源清理如關閉數據庫連接、清理臨時文件。4. 實戰從零配置一個 OpenClaw Agent 并集成大模型理論說了這么多我們動手配置一個最簡單的 OpenClaw Agent并讓它接入一個大模型比如通過 Ollama 本地運行的 Llama 3來直觀感受一下這套調度系統是如何運作的。這里假設你已經按照一些教程如“ubuntu極速部署openclaw完全指南”完成了基礎環境的搭建。4.1. 項目初始化與核心依賴安裝首先創建一個新的 TypeScript 項目并安裝 OpenClaw 的核心包這里假設包名為openclaw/core具體名稱需查閱官方文檔。mkdir my-openclaw-agent cd my-openclaw-agent npm init -y npm install typescript ts-node types/node --save-dev npm install openclaw/core --save接著初始化 TypeScript 配置。注意熱詞中的警告選項“baseurl”已棄用,并將停止在 typescript 7.0 中運行。指定 compileroption。這是 TypeScript 配置的更新。在你的tsconfig.json中避免使用已棄用的baseUrl而是使用compilerOptions下的paths等新方式進行路徑映射。{ compilerOptions: { target: ES2020, module: commonjs, lib: [ES2020], outDir: ./dist, rootDir: ./src, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, // 使用 paths 替代已棄用的 baseUrl paths: { /*: [./src/*] } }, include: [src/**/*], exclude: [node_modules] }4.2. 定義第一個技能與工具調用本地 LLM我們創建一個簡單的“問答”技能它調用本地的 Ollama 服務。首先定義一個調用 LLM 的工具。在src/tools/llm-tool.ts中import { Tool, ITool, ToolContext } from openclaw/core; export interface LLMCallParams { model: string; prompt: string; systemPrompt?: string; } export interface LLMCallResult { content: string; model: string; usage?: { prompt_tokens: number; completion_tokens: number }; } // 使用裝飾器注冊工具 Tool({ name: call_ollama_llm, description: 調用本地 Ollama 服務的 LLM 模型, inputSchema: { /* 可以用 JSON Schema 定義參數結構 */ } }) export class OllamaLLMTool implements IToolLLMCallParams, LLMCallResult { private ollamaBaseUrl: string; constructor(baseUrl: string http://localhost:11434) { this.ollamaBaseUrl baseUrl; } async invoke(params: LLMCallParams, context?: ToolContext): PromiseLLMCallResult { const response await fetch(${this.ollamaBaseUrl}/api/generate, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ model: params.model, prompt: params.prompt, system: params.systemPrompt, stream: false // 簡單起見關閉流式 }) }); if (!response.ok) { const errorBody await response.text(); throw new Error(Ollama API call failed: ${response.status} ${errorBody}); } const data await response.json(); return { content: data.response, model: data.model, usage: { prompt_tokens: 0, completion_tokens: 0 } // Ollama 可能不返回這里示意 }; } }接著在src/skills/qa-skill.ts中定義技能import { Skill, ISkill, SkillResult, WorkflowContext } from openclaw/core; import { OllamaLLMTool, LLMCallParams } from ../tools/llm-tool; Skill({ name: answer_question, description: 根據用戶問題調用 LLM 生成回答 }) export class QASkill implements ISkill { private llmTool: OllamaLLMTool; constructor() { this.llmTool new OllamaLLMTool(); } async execute(context: WorkflowContext): PromiseSkillResult { // 從上下文中取出用戶問題 const userQuestion context.userInput; if (!userQuestion) { return { success: false, error: 用戶輸入為空 }; } try { const llmParams: LLMCallParams { model: llama3, // 假設本地已拉取 llama3 模型 prompt: 請回答以下問題${userQuestion}, systemPrompt: 你是一個樂于助人的AI助手。 }; const result await this.llmTool.invoke(llmParams); // 將回答存入上下文供后續技能或輸出使用 context.llmAnswer result.content; return { success: true, output: result.content }; } catch (error) { // 錯誤處理記錄日志并返回失敗結果 console.error(QASkill 執行失敗:, error); context.lastError error.message; return { success: false, error: 獲取答案失敗: ${error.message} }; } } }4.3. 組裝 Agent 與啟動調度服務現在我們需要創建一個 Agent將技能組裝起來并啟動調度服務。在src/agent/simple-qa-agent.ts中import { Agent, IAgent, Workflow, SvrOperator } from openclaw/core; import { QASkill } from ../skills/qa-skill; // 1. 定義工作流 const qaWorkflow: Workflow { id: simple_qa_workflow, entrySkill: answer_question, skills: { answer_question: { execute: async (context) { const skill new QASkill(); return await skill.execute(context); }, // 執行完就結束沒有下一個技能 next: null } } }; // 2. 創建 Agent export class SimpleQAAgent implements IAgent { name Simple QA Agent; workflow qaWorkflow; async onStartup() { console.log(Agent ${this.name} 已初始化。); } async onShutdown() { console.log(Agent ${this.name} 已關閉。); } }最后在src/index.ts中創建服務入口import { SvrOperator } from openclaw/core; import { SimpleQAAgent } from ./agent/simple-qa-agent; async function main() { // 1. 初始化調度操作員 const operator new SvrOperator(); // 2. 創建并注冊我們的 Agent const myAgent new SimpleQAAgent(); operator.registerAgent(myAgent); // 3. 啟動 HTTP 服務器或其它傳輸層等待請求 // 這里以簡單的 HTTP 服務器為例 const http require(http); const server http.createServer(async (req, res) { if (req.method POST req.url /ask) { let body ; req.on(data, chunk body chunk); req.on(end, async () { try { const { question, sessionId sess_${Date.now()} } JSON.parse(body); // 構造執行上下文 const initialContext { userInput: question, sessionId }; // 通過調度操作員執行 Agent const result await operator.executeAgent(Simple QA Agent, initialContext); res.writeHead(200, { Content-Type: application/json }); res.end(JSON.stringify({ answer: result.output, success: result.success })); } catch (error) { console.error(請求處理錯誤:, error); res.writeHead(500, { Content-Type: application/json }); res.end(JSON.stringify({ error: Internal Server Error, details: error.message })); } }); } else { res.writeHead(404).end(); } }); server.listen(3000, () { console.log(OpenClaw Agent 調度服務已啟動監聽端口 3000); console.log(嘗試發送 POST 請求到 http://localhost:3000/ask 并攜帶 JSON 體: {question: 你的問題}); }); } main().catch(console.error);運行npx ts-node src/index.ts你的純 TypeScript Agent 調度服務就跑起來了。你可以用 curl 或 Postman 測試它。這個簡單的例子串聯了從 HTTP 請求進入SvrOperator到觸發工作流執行技能調用工具最后返回結果的完整調度鏈條。4.4. 配置多模型與技能擴展熱詞中提到“本地openclaw如何添加多個大模型”。在我們的架構里這非常直觀。你不需要修改調度核心只需擴展工具層。創建新的 LLM 工具類比如OpenAITool、AzureOpenAITool實現相同的ITool接口但內部調用不同的 API。在技能中動態選擇模型可以通過上下文中的某個配置字段來決定使用哪個工具。例如修改QASkill的execute方法async execute(context: WorkflowContext): PromiseSkillResult { const modelProvider context.modelProvider || ollama; // 默認為 ollama let llmTool: ITool; if (modelProvider openai) { llmTool new OpenAITool(process.env.OPENAI_API_KEY); } else { llmTool new OllamaLLMTool(); } // ... 后續調用邏輯不變 }通過注冊中心更優雅地管理更高級的做法是將所有 LLM 工具都注冊到全局工具注冊中心技能只需要根據名稱來獲取工具。這樣新增模型提供商時只需要編寫并注冊新工具所有技能自動獲得支持。技能擴展同理。如果你想增加一個“聯網搜索”后再回答的技能只需定義一個新的WebSearchSkill然后在工作流定義中將answer_question技能的next指向它或者在answer_question之前插入它。工作流引擎會自動按照新的藍圖來調度執行順序。通過這個實戰流程你可以看到 OpenClaw 這類純 TypeScript 調度系統的靈活性。它的核心價值不在于提供了多少預置的 AI 能力而在于提供了一套類型安全、松耦合、可擴展的框架讓你能像搭積木一樣快速構建和迭代屬于自己的 AI Agent 應用。從簡單的問答到復雜的多步驟工作流如分析需求 - 搜索信息 - 生成報告 - 發送郵件這套調度系統都能提供清晰、可靠的控制骨架。