3.3 与自研运行时对照:内置省了什么、锁定了什么
3.1 和 3.2 都在回答「怎么用」。这一节回答本卷每一节都必须回答的那个问题:这套框架替我做了什么,我又让渡了什么? 对照的基准是卷一从零手写的 minagent——它有明确的契约(run() 返回 RunResult、Message / ToolCall / Step 数据类、FakeModel.calls 二元组),我们逐项看 Claude Agent SDK 在哪一块省了力,又在哪一块上了锁。
本节把「框架选型」推进到可决策的粒度:把 SDK 的 subagent / hook / 权限 / session 逐条映射回 minagent 的自研实现,量化它省下的代码量与让渡的控制权,给出可观测性对照,并给出「何时用 SDK、何时自研、何时混合」的清单。
3.3.1 先立对照的基准
卷一的 minagent 是一条极简但完整的链路:Agent 持有 model / registry / system / max_steps 与 history,run() 里是 for i in range(max_steps) 的循环,模型返回 tool_calls 就分发、为空就收敛,过程通过 on_step / on_tool_call 回调外泄。它的核心契约三卷共用:
# minagent 的引擎契约(卷一,三卷共用)
@dataclass
class Message:
role: Role; content: str = ""
tool_calls: list[ToolCall] = field(default_factory=list)
tool_call_id: str | None = None
@dataclass
class RunResult:
messages: list[Message]; steps: list[Step]; final: str
def run(model, registry, user_input, system="", max_steps=8) -> RunResult: ...
它跑在你自己的进程里,每一步都可见、可改、可断点。FakeModel.calls 是个二元组列表 list[tuple[list[Message], list[dict]]],让确定性测试成为可能——这是卷一第 16 章整章的基础。
Claude Agent SDK 的模型完全不同:循环跑在 claude CLI 子进程里,你的 Python 只通过 async for 消费它吐出的消息。这个根本差异决定了后面所有对照项——你的控制点从「循环内部」退到了「进程边界」。3.1 节实测的 query() 签名已经透露了这一点:它返回的是 AsyncIterator[AssistantMessage | ResultMessage | ...],而不是一个 RunResult。
把两者的能力逐项对齐:
| 能力 | minagent | Claude Agent SDK |
|---|---|---|
| Agent 循环 | 你写的 for 循环 | claude CLI 内部 |
| 工具注册 | ToolRegistry + schema 生成 | CLI 内置工具 + MCP |
| subagent | 自己写调度 | agents=AgentDefinition |
| 钩子 | 事后回调 | PreToolUse 等前置 hook |
| 权限 | 自己写包装 | permission_mode + can_use_tool |
| 会话持久化 | 自己写 checkpoint | resume / continue |
| 成本核算 | 自己埋点 | ResultMessage.total_cost_usd |
| 模型 | 任意(假模型可测) | Anthropic 家族 |
3.3.2 内置省了什么
省的第一块:subagent 调度。 卷一第 13 章整章都在解决「多个 Agent 怎么分工」——supervisor 怎么派活、worker 怎么回话、消息总线怎么设计、拓扑选哪种。SDK 里这些缩成一行声明:
agents = {"researcher": AgentDefinition(description="检索资料", prompt="...")}
主 Agent 读各 subagent 的 description 自行委派,你不用写调度器、不用设计消息格式、不用管拓扑。这是省得最彻底的一块——一整章的设计被一个字段替代。
省的第二块:hook 的拦截能力。 minagent 的 on_step / on_tool_call 只能事后被通知(卷一 3.3 节),想「执行前拦截」得自己在 registry.call 前后加钩子:
# minagent 想拦截,只能自己包一层
def call(self, tc):
if tc.name == "Bash" and "rm" in tc.arguments.get("command", ""):
return '{"error": "blocked"}'
return self._dispatch(tc)
SDK 的 PreToolUse hook 直接给到「执行前 + 可改写结果」的位置,permissionDecision 还能返回 deny / allow / ask / defer。省下的是一套「拦截点」设计。
省的第三块:权限基础设施。 卷二 4.3 节我们手写了 permissions.py:工具分只读/写入/危险三档、危险档挂起等审批、超时处理。SDK 里这些是 permission_mode + allowed_tools + can_use_tool 三个字段(3.2 节已展开)。省下的是一整层安全脚手架。
省的第四块:session 持久化。 卷一 7.3 节手写 save_checkpoint / restore,把消息、步骤、模型游标序列化成 JSON:
# minagent 的检查点(卷一 7.3)
def save_checkpoint(path, session, model, steps):
data = {"session": session.snapshot(), "model_cursor": model.cursor,
"steps": [{"index": s.index, "kind": s.kind, "detail": s.detail} for s in steps]}
json.dump(data, open(path, "w", encoding="utf-8"))
SDK 里对应的是 resume / continue_conversation / fork_session——CLI 自己管会话存储。省下的是序列化、恢复、以及「哪些已完成不该重跑」的全部判断逻辑。
还有一个附赠项:成本核算。minagent 要在卷一 15.3 节自己埋点算 token 与成本,SDK 的 ResultMessage 白送 num_turns / total_cost_usd / usage。
3.3.3 又锁定了什么
省力的另一面是让渡。四项锁定必须写清楚,否则选型时会踩坑。
| 锁定 | 具体表现 | 影响面 |
|---|---|---|
| 依赖 CLI | 必须装 claude + 登录 + 有额度 | 部署 / CI / 离线 |
| 模型绑定 | 只能 Claude 模型 | 可替换性 |
| 可观测边界 | 只有进程边界事件 | 延迟归因 / 成本分摊 |
| 消息契约 | 用 SDK 类型,无法注入假模型 | 确定性测试 |
下面逐项展开。
锁定一:依赖 claude CLI。 这是最硬的约束。你的生产环境必须装 claude(本机是 2.1.285)、必须能登录、必须有额度。容器镜像要带上它,CI 要配好它,离线环境直接不可用。更要命的是版本漂移:CLI 升级可能改变工具行为、权限匹配、会话格式,而你的 Python 代码看不出任何变化——你锁定的不只是「一个依赖」,而是「一个持续演进的产品」的行为。
锁定二:模型绑定 Anthropic 家族。 ClaudeAgentOptions.model 只接受 Claude 模型名。想换成别的厂商,SDK 这条路直接堵死。而 minagent 的 Model 是一个 Protocol——卷一第 2 章换 provider 只改适配器,上层一行不动。SDK 用「便利」换掉了「可替换」,这是本卷 1.3 节选型打分里「锁定度」这一维度的典型扣分项。
锁定三:可观测与扩展的边界。 循环跑在子进程里,你能观测到的只有它愿意吐出来的消息(AssistantMessage / SystemMessage / ResultMessage)。想在「第 3 步到第 4 步之间」插一段自己的逻辑?做不到——那里不是你的代码。卷二的 OTel 埋点(13.1 节)在 SDK 上只能挂在进程边界,拿不到 CLI 内部的 span。扩展点被压缩成了「hooks + MCP 工具 + 回调」三处。
锁定四:消息契约不属于你。 minagent 三卷共用 Message / RunResult 契约,测试靠 FakeModel。SDK 的消息是 AssistantMessage 等它定义的类型,你没法注入假模型跑确定性测试——测试要么打桩 CLI(工作量大且脆弱),要么真连(消耗额度)。这把卷一的「假模型确定性测试」这条路径直接掐断了。对一个要上 CI 的项目,这是很重的一刀。
3.3.4 让渡清单
把「省了什么」与「让渡了什么」并列,对照才完整:
| 维度 | 省了什么 | 让渡了什么 |
|---|---|---|
| subagent | 调度、拓扑、消息总线全免 | 委派策略不可定制 |
| hook | 前置拦截现成 | 只有固定几个事件点 |
| 权限 | 白黑名单 + 审批现成 | 匹配语义由 CLI 定义 |
| session | 持久化现成 | 存储格式与位置不可控 |
| 循环 | 不用写、不用维护 | 循环内部不可介入 |
| 模型 | 与 Claude 深度调优 | 不能换厂商 |
| 测试 | — | 无法注入假模型 |
| 部署 | — | 必须带 CLI + 登录 |
3.3.5 同一需求,两条实现路径
把对照落到一个具体需求上:「一个研究助理,两个 subagent(检索 + 摘要),危险工具需审批,会话可续跑」。两条路径各自要做什么:
minagent 路径(卷一 + 卷二):
- 定义
Message/ToolCall/RunResult契约(卷一 1.2) - 实现
ToolRegistry与 schema 生成(卷一 4.1) - 写
Agent循环与停止条件(卷一 3.1–3.3) - 写 supervisor/worker 调度与消息总线(卷一 13.1–13.2)
- 写
permissions.py分档与审批挂起(卷二 4.3) - 写 checkpoint 序列化与 restore(卷一 7.3)
- 写成本埋点与 token 统计(卷一 15.3)
Claude Agent SDK 路径:
- 声明两个
AgentDefinition放进agents= - 设
allowed_tools/disallowed_tools - 写一个
can_use_tool回调做审批 - 设
permission_mode - 传
resume=<session_id>续跑 - 读
ResultMessage.total_cost_usd拿成本
对照结论很直白:minagent 是「七步各写一遍」,SDK 是「六项各填一个字段」。省力程度一目了然。但硬币的另一面同样直白——minagent 那七步每一处你都能改、能测、能换实现;SDK 这六项每一处都由 claude CLI 定义,你只能填值,不能改行为。这就是「省了什么」与「锁定了什么」在同一需求上的正面碰撞。
还有一个容易被忽略的差别在上手成本曲线上:minagent 的七步是「陡坡起步、平缓收尾」——前几章难,但每一步都在你掌控中;SDK 的六项是「平缓起步、陡坡收尾」——demo 十分钟能跑,但真要做延迟归因、确定性测试、换模型时,才发现每一样都得绕开 CLI 的限制。选型时不要只看第一天的体验,要看到第三个月的需求。
3.3.6 可观测性:一个具体对照
可观测性是「锁定」里最容易被低估的一项,单独对照一次。minagent 因为循环在自己手里,可以在任意粒度打点:
# minagent:循环内每一处都能埋点
for i in range(self.max_steps):
t0 = time.perf_counter()
resp = self.model.complete(messages, self.registry.schemas())
log.info("model_call", step=i, latency=time.perf_counter() - t0,
tokens=resp.usage)
for tc in resp.message.tool_calls:
log.info("tool_call", name=tc.name, args=tc.arguments)
SDK 侧,你能拿到的只有 CLI 吐出来的事件:
async for msg in query(prompt=..., options=...):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if getattr(block, "type", "") == "tool_use":
log.info("tool_use", name=block.name)
elif isinstance(msg, ResultMessage):
log.info("done", turns=msg.num_turns, cost=msg.total_cost_usd)
差别在哪?minagent 能测「模型调用的延迟」「每一步的 token 增量」;SDK 只能测「整个会话的总耗时与总成本」。要精细到步级延迟,SDK 层面拿不到——那些 span 在 CLI 进程内部。这不是 SDK 的 bug,而是「进程边界」的物理限制:跨进程只传得回它约定要传的东西。
对 ResearchCopilot 这种要做延迟归因与成本分摊的服务,这个限制意味着:要么接受粗粒度,要么在 CLI 外自己重放一遍时间线。卷二的 14.1 节「延迟剖析」在 SDK 上需要重新设计采集点。
3.3.7 混合策略:用 Protocol 包一层
既想借 SDK 的运行时,又想保住自己的契约,卷一卷二的做法给了答案:用第 1.2 节的统一 Protocol 把 SDK 包成一个适配器,对外仍暴露 run() -> RunResult。这样上层代码不感知底下是 minagent 还是 Claude Agent SDK,替换成本被压到一层。
# sdk_adapter.py —— 结构示意(仅构造 options,不触发模型调用)
from claude_agent_sdk import ClaudeAgentOptions, query, AssistantMessage, ResultMessage
class ClaudeSDKModel:
"""把 Claude Agent SDK 适配成 minagent 的 Model 契约。"""
def __init__(self, options: ClaudeAgentOptions) -> None:
self.options = options
async def complete(self, messages, tools):
# 真实实现里:把 minagent 的 messages 转成 prompt,
# 消费 query() 的流,再把结果拼回 ModelResponse。
# 这一步需要活的 Claude 会话,本节未执行。
raise NotImplementedError("需活的 Claude 会话")
这层适配的代价是「双重翻译」:minagent 的 Message → SDK 的 prompt,SDK 的 AssistantMessage → minagent 的 ModelResponse。但收益是上层的评测、日志、UI 全不用改,而且模型可替换这条后路保住了——同一个 Protocol 下,Claude Agent SDK 只是又一个实现。是否值得,取决于你要不要保留这条后路。
3.3.8 何时用 SDK、何时自研
把决策收敛成一张清单:
| 你的处境 | 建议 |
|---|---|
| 就是要一个「会查资料、管权限、记会话」的 Agent | 用 SDK,别重造 |
| 需要换多家模型 / 本地模型 | 自研(Protocol 适配) |
| 需要注入假模型做确定性测试 | 自研(SDK 掐断这条路径) |
| 要在循环内部插自定义逻辑 | 自研或混合 |
| 团队已用 Claude Code,想要一致体验 | 用 SDK |
| 生产环境无法装 CLI / 不能登录 | 自研 |
| 想要现成的成本核算与审计 | 用 SDK |
| 需要精细的 OTel 内部 span | 自研(SDK 只有边界) |
一句话概括:当「Agent 的行为」正是你要的,SDK 最短;当「Agent 的内部」是你要改的,SDK 最挡路。 这张清单里没有「绝对更好」的选项,只有「和你的约束更匹配」的选项。
3.3.9 这套框架替你做了什么、你又让渡了什么
回到本卷的主线问题。Claude Agent SDK 替你把「一个成熟 Agent 产品运行时的四大件」——subagent 调度、hook 拦截、权限闸门、会话持久化——打包成几个字段加回调,省掉的代码量以「章」计(卷一第 13 章、第 7.3 节、卷二第 4.3 节)。
但它同时把控制权锁在了 claude CLI 进程里:模型不可换、循环不可入、测试不可假、内部不可观测。这不是缺陷,而是「运行时即产品」的必然代价——你买的是一台整机,不是一堆零件。
所以真正的选型问题不是「SDK 好不好」,而是**「你要的是一台整机,还是要一堆能自己拼的零件」**。对 ResearchCopilot 这种「行为导向」的研究助理,整机往往更划算;对「要嵌进自己状态机、要可替换模型」的场景,零件才是对的。
一个务实的建议:先用自研的 Protocol 抽象把「你要的 Agent 行为」定义清楚,再决定底层是 minagent 还是 Claude Agent SDK。这样即便今天选了整机,明天要换回零件,也只改一层适配器——3.3.7 节那段 ClaudeSDKModel 就是为此存在的。
下一章 4.1 OpenAI Agents SDK:handoff/guardrails/tracing 实现委派 会看另一种「整机」怎么设计委派与护栏;想回看权限闸门的细节,见 3.2 工具与权限:allow/deny 与审批回调 。
阅读导航:上一节:3.2 工具与权限:allow/deny 与审批回调 · 下一节:4.1 OpenAI Agents SDK:handoff/guardrails/tracing 实现委派 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。