《AI Agent 高级编程》3.3 与自研运行时对照:内置省了什么、锁定了什么

前两节讲了 Claude Agent SDK 怎么用,这一节算总账。把它的每项能力逐条映射回卷一 minagent 的自研实现,看清它内置省下的四块重活(subagent 调度、hook、权限、session 持久化),也看清它锁定的边界:依赖 claude CLI、绑定 Anthropic 模型、可观测与扩展受限、假模型测试被掐断。文末给对照表与选型清单。

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。

把两者的能力逐项对齐:

能力minagentClaude Agent SDK
Agent 循环你写的 for 循环claude CLI 内部
工具注册ToolRegistry + schema 生成CLI 内置工具 + MCP
subagent自己写调度agents=AgentDefinition
钩子事后回调PreToolUse 等前置 hook
权限自己写包装permission_mode + can_use_tool
会话持久化自己写 checkpointresume / 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 路径(卷一 + 卷二):

  1. 定义 Message / ToolCall / RunResult 契约(卷一 1.2)
  2. 实现 ToolRegistry 与 schema 生成(卷一 4.1)
  3. 写 Agent 循环与停止条件(卷一 3.1–3.3)
  4. 写 supervisor/worker 调度与消息总线(卷一 13.1–13.2)
  5. 写 permissions.py 分档与审批挂起(卷二 4.3)
  6. 写 checkpoint 序列化与 restore(卷一 7.3)
  7. 写成本埋点与 token 统计(卷一 15.3)

Claude Agent SDK 路径:

  1. 声明两个 AgentDefinition 放进 agents=
  2. 设 allowed_tools / disallowed_tools
  3. 写一个 can_use_tool 回调做审批
  4. 设 permission_mode
  5. 传 resume=<session_id> 续跑
  6. 读 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 实现委派 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「agent」更多文章

  1. 《AI Agent 高级编程》目录
  2. 《AI Agent 高级编程》11.3 全景回顾:从零到框架的完整路径与展望
  3. 《AI Agent 高级编程》11.2 真实 API 冒烟:假模型 vs 真模型量化差距