跳转到内容

为什么选 Claude Agent SDK 作 runtime

如果你把所有 “让 LLM 在循环里调工具” 的工程任务摊开,你会得到一个无聊但很长的清单:

  • 把消息发给模型,处理 streaming 响应
  • 解析 tool call、调对应的工具、把结果塞回 conversation history
  • 处理 tool 调用的 hook(pre / post)、错误、超时、cancel
  • 维护 turn 状态、context 窗口压缩、多轮对话
  • 跟 MCP server 集成
  • 暴露模型/工具/usage 给 observability 层
  • 抽象不同的 model provider

这就是 “agent runtime” 在做的事。每一家做 agent 产品的公司都要解决这套问题。问题是,你应不应该自己解决

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 在内部用,他们把它发布出来只是时间问题。

那天的 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 不再是 “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_boardopenAppshowVizlocalBashsubmit_jobcompact_session
  • SDK 自己也内置 Read/Write/Edit/Bash/Glob/Grep——我们直接用
  • PreToolUse hook 在每次工具调用前 emit 一个 tool_use 事件,PostToolUse hook emit 结果

3. 输出侧:流式 + 嘴型同步 + 学习面板

  • text_delta 事件按句切片→ 转给 TTS pipeline 拼语音 segment
  • write_to_board 工具调用写到学习者的右侧黑板(学习面板,跟语音同步)
  • 每个 turn 一个 agent_turn_events row 在 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 装在一个人脑子里。

好的部分

  • 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 -p CLI 进程而不是 SDK 内部 spawn——两层接口割裂。
  • 本地模型 fallback。 当前 ANTHROPIC_BASE_URL 单选——MiniMax 或 Anthropic,没有运行时 fallback。容灾完全靠 deploy 时选 URL。

每次 SDK 升级,我们都重新问一个问题:这次能不能再从 agent-engine 里删点代码?

如果不能,说明 SDK 没在长大。 如果能,说明 SDK 把以前需要我们兜底的东西收回去了——这是我们想要的。

理想终态:agent-engine 退化成 50 行 glue。整个 “agent runtime” 概念彻底消失在 SDK 后面。harness 越薄越好。

我们离那一天还远。但每删一行 OpenClaw 时代的遗物,都是朝那个方向走。


相关文章