为什么选 Claude Agent SDK 作 runtime
一个 runtime 应该做什么
Section titled “一个 runtime 应该做什么”如果你把所有 “让 LLM 在循环里调工具” 的工程任务摊开,你会得到一个无聊但很长的清单:
- 把消息发给模型,处理 streaming 响应
- 解析 tool call、调对应的工具、把结果塞回 conversation history
- 处理 tool 调用的 hook(pre / post)、错误、超时、cancel
- 维护 turn 状态、context 窗口压缩、多轮对话
- 跟 MCP server 集成
- 暴露模型/工具/usage 给 observability 层
- 抽象不同的 model provider
这就是 “agent runtime” 在做的事。每一家做 agent 产品的公司都要解决这套问题。问题是,你应不应该自己解决。
我们曾经有过自己的:OpenClaw
Section titled “我们曾经有过自己的:OpenClaw”TeachClaw 的第一版 runtime 叫 OpenClaw。一个自写的 gateway——容器里跑 Node,HTTP 接收消息,调 Anthropic API,自己处理 tool call 循环、streaming、错误重试。
它工作。但每周都有维护成本:
- Anthropic 的 streaming protocol 改了一次(partial messages → typed events),我们改一周
- tool call 的 input schema 从 JSON Schema 变成 Pydantic-like,我们改一周
- 加 vision input、加 thinking blocks、加 prompt cache marker——每次都得追
更深的问题:我们在维护一个跟 Anthropic 内部 SDK 重复的东西。Anthropic 自己一定有完整的 client library 在内部用,他们把它发布出来只是时间问题。
2026-03-31:把它删了
Section titled “2026-03-31:把它删了”那天的 commit 086a7e91:
refactor(infra): remove OpenClaw, use Claude Agent SDK directly
净删除 359 行 OpenClaw gateway 代码。换成 @anthropic-ai/claude-agent-sdk,直接在 agent-engine 进程里 import { query }。
决定的依据很简单:Claude Agent SDK 此时已经稳定到值得用了——streaming、tool hooks(PreToolUse / PostToolUse)、MCP 集成、usage stats 都是官方维护的。任何一项功能他们改 protocol,我们升级 SDK 就行,不再是我们的 bug。
agent-engine:SDK 上面那层薄包装
Section titled “agent-engine:SDK 上面那层薄包装”替换之后,agent-engine 不再是 “runtime”,而是 “runtime 之上的薄包装”。整个包大约 430 行 TypeScript,主入口是一个轮询循环:
// packages/agent-engine/src/index.ts (简化)async function processMessages() { const messages = getNewMessages(1); // SQLite 里取一条 if (messages.length === 0) return;
const msg = messages[0]; const composed = await composeTurn({ userMessage: msg.content, userImId: recipientId || undefined, runtime: { mode, triggerReason, lastUserMessageAgeMs, lastUserSnippet }, });
const agentResult = await Promise.race([ runAgent(chatId, promptText, { cwd: CWD, mode, speakable, channelContext: { chatId, recipientId, groupId, sessionType, turnId, traceId }, systemPromptAppend: composed.staticSystemPrompt, onToolEvent: (event) => { broadcastToolEvent(event); }, }), new Promise((_, reject) => setTimeout( () => reject(new Error(`Agent timeout after ${AGENT_TIMEOUT_MS / 1000}s`)), AGENT_TIMEOUT_MS, ), ), ]);}runAgent 内部是调 SDK 的 query():
const q = query({ prompt: promptText, options: { cwd, resume: sessionId, systemPrompt: { type: "preset", preset: "claude_code", excludeDynamicSections: true, // 见下文 append: systemPromptAppend, }, permissionMode: "bypassPermissions", tools: [...BUILTIN_TOOLS], model: process.env.AGENT_MODEL || undefined, mcpServers, includePartialMessages: true, hooks: { /* PreToolUse, PostToolUse */ }, },});agent-engine 在 SDK 之上加的东西,按职责分四类:
1. 输入侧:轮询 + 上下文构造
- 一个 2 秒轮询 SQLite 的循环,FIFO 取消息
composeTurn()把 substrate(静态前缀)+ runtime context(动态部分)+ 用户消息拼起来excludeDynamicSections: true是关键——SDK 的preset: "claude_code"会自动塞当前工作目录 / auto-memory / git status 这种 dynamic 段,每个 turn 都变,在我们 substrate append 之前就破了 cache 前缀;设为 true 让 preset 也保持静态,prefix cache 才能命中
2. 工具侧:MCP server
- 6 个工具通过自写的 MCP server 暴露给 SDK:
write_to_board、openApp、showViz、localBash、submit_job、compact_session - SDK 自己也内置 Read/Write/Edit/Bash/Glob/Grep——我们直接用
- PreToolUse hook 在每次工具调用前 emit 一个
tool_use事件,PostToolUse hook emit 结果
3. 输出侧:流式 + 嘴型同步 + 学习面板
text_delta事件按句切片→ 转给 TTS pipeline 拼语音 segmentwrite_to_board工具调用写到学习者的右侧黑板(学习面板,跟语音同步)- 每个 turn 一个
agent_turn_eventsrow 在 Postgres,供 admin replay 用
4. 可观测:Langfuse 手搓 instrumentation
- 不用
@arizeai/openinference-instrumentation-claude-agent-sdk,OTel v1/v2 不兼容(III-2 详述) - 三类 observation:
agent-turn(span)/claude-agent-llm(generation)/tool/<name>(tool) - PreToolUse →
startToolObservation,PostToolUse →end()
这就是全部。没有 LangChain,没有 LangGraph,没有 agent framework。整个 runtime 装在一个人脑子里。
当前的选择有哪些 trade-off
Section titled “当前的选择有哪些 trade-off”好的部分:
- Streaming 协议、tool schema、provider abstraction 都是 Anthropic 在维护——他们改我们升级,零工程
- in-process import,没有 HTTP / IPC / gRPC 中转层
- 一个普通工程师一天能读完 agent-engine 全部源码
坏的部分:
- 我们绑定在 Anthropic API shape 上。我们用 MiniMax-M2.7 作为主力模型(详见 III-2),靠 MiniMax 提供的
https://api.minimaxi.com/anthropic兼容 shim 工作。这层 shim 是单点风险——MiniMax 改协议,或者 Anthropic 改 SDK 不兼容,我们都会断 - SDK 不开源,我们改不了它。遇到 bug 只能 workaround 或者等修
- 性能 ceiling 跟 SDK 一起涨——他们没把 streaming 做到 50ms 之前,我们也做不到
仍未解决的:
- 真正长的工具调用怎么处理。 SDK 的 tool 执行是 cooperative(PreToolUse → 调工具 → 拿结果 → PostToolUse),>1 分钟的任务会卡 turn。我们用 bg-worker 异步剥离绕开,但 SDK 本身的协议层应该支持 fire-and-forget tool。
- 多 agent 协作。 SDK 当前只支持单 agent。如果我们要在 turn 中临时分裂出 subagent 跑 skill(详见 I-3),现在是另起
claude -pCLI 进程而不是 SDK 内部 spawn——两层接口割裂。 - 本地模型 fallback。 当前
ANTHROPIC_BASE_URL单选——MiniMax 或 Anthropic,没有运行时 fallback。容灾完全靠 deploy 时选 URL。
下一代我们想问什么
Section titled “下一代我们想问什么”每次 SDK 升级,我们都重新问一个问题:这次能不能再从 agent-engine 里删点代码?
如果不能,说明 SDK 没在长大。 如果能,说明 SDK 把以前需要我们兜底的东西收回去了——这是我们想要的。
理想终态:agent-engine 退化成 50 行 glue。整个 “agent runtime” 概念彻底消失在 SDK 后面。harness 越薄越好。
我们离那一天还远。但每删一行 OpenClaw 时代的遗物,都是朝那个方向走。
相关文章:
- 容器外 vs 容器内:runtime 跑在哪?看 I-2
- 工具调用 > 1 分钟怎么办?看 I-3 bg-worker
- prompt cache 怎么不破?看 III-2 Langfuse trace