Article Share

系列 AI Agent 工程实战

第 3 / 3 篇
  1. 01 AI Agent 编年史:四年五代的演进规律
  2. 02 Harness 工程:在不确定世界中探寻确定路径
  3. 03 从泄露源码看 Agent 设计:一个本地 Runtime 的工程切片

从泄露源码看 Agent 设计:一个本地 Runtime 的工程切片

Claude Code 的全部复杂度,都在回答两个问题——怎么安全可靠,怎么压榨有限上下文

12 分钟阅读
目录
阅读模式

来源:从泄露源码看 Agent 设计(技术分享视频字幕)

一句话总结:Claude Code 的全部工程复杂度,本质都在解决两个问题——让模型的自主行为安全可靠,以及在有限上下文约束下把长任务跑完


开场

Claude Code 的源码不久前被 Anthropic 意外泄露。这是第一次,可以从代码层面去看一个真实生产级 Agent 是怎么搭出来的。

大多数人盯着 prompt 模板、tool 封装、workflow 编排在打转,但代码里展示的工程模式很反直觉:模型不是总指挥,Runtime 才是;Tool 不是函数,而是协议对象;上下文不是”装得越多越好”,而是要主动压缩、按需召回;多 Agent 不是为了角色扮演,而是为了隔离上下文

那么这些设计背后,是不是有一条统一的主线?

答案是一句话:所有设计都在回答两个问题——安全可靠 + 有限上下文。下面分四层展开。


金字塔总览

mindmap
  root((Agent 设计<br/>安全 × 压缩))
    一·Runtime 内核
      CLI 只是壳
      统一 query 循环
      七级错误恢复
      流式调度抢时间
    二·工具与执行
      Tool 是协议对象
      并发性自我声明
      每层都有治理点
      默认走慢路径
    三·上下文与状态
      Prompt 分层缓存
      五级压缩梯子
      Memory 索引化召回
      Transcript append-only
    四·安全与扩展
      Sandbox + Permission 互补
      MCP/Skills 统一治理
      多 Agent 三层架构
      Sub-agent = 上下文隔离

一、Runtime 内核:CLI 只是入口,所有形态共用一条执行链

1.1 外壳与内核:一切都汇聚到 query 循环

Claude Code 的 CLI 只是入口,真正干活的是后面的本地 Agent Runtime。

  • 启动链:cli.tsxmain.tsx 主流程调度 → init/setup 安全初始化(确认目录可信、加载用户配置)→ launch repl → 进入 query 函数
  • 启动参数分流:--print 走 headless 后台、--remote 走远程模式、默认走完整路径(装配工具池、MCP、内建 skills、Agent 定义)
  • 关键事实:REPL / headless / SDK 嵌入 / sub-agent,走的都是同一段 query 执行循环,不存在两套实现

金句:CLI 只是壳,内核是一整套本地 Agent Runtime;主 Agent 派出的子 Agent 跑的也是同一个内核,只是上下文做了隔离。

1.2 query 循环:本质上是一个 while

剥掉所有工程外壳,Agent 的核心就是一个反复调用模型的 while:

flowchart LR
    A[user message] --> B[API call]
    B --> C{stop_reason?}
    C -->|tool_use| D[execute tool]
    D --> E[append result]
    E --> B
    C -->|end_turn| F[exit loop]

每一轮,模型决定继续用工具还是停下来。Claude Code 在工程上把每一轮拆成了六个阶段:

阶段干什么
pre_api_phase装配 attachment / skill / memory
api_request_phase流式调用 API,带重试
process_response把模型输出解析成 transition 状态字
execute_tools按并发性分批跑,加权限、加 hooks
stop_hooks轮次结束后的钩子,可否决退出
next_turn判断收工,否则 turn_count++

transition 是状态机的语义灵魂:continue / tool_use / end_turn / stop_sequence,每个值对应一种”接下来干嘛”的决策。

1.3 流式调度:跑起来丝滑的底层原因

整条循环是流式生成——模型还在生成,界面已经在显示,下游已经开始消化、开始执行工具,不用等整段回答完。这是 Claude Code “感觉很快”的真正原因,不是模型快,是调度在帮模型抢时间

1.4 七级错误恢复:长任务不死的真正秘密

Agent 跑长任务会失败:网络断、上下文塞满、话说一半被截、token 预算耗尽……Claude Code 把这些按严重程度分了 L1 ~ L7 七级,每级一条恢复策略:

级别场景策略
L1流式连接中断切非流式模式重试
L2上下文快塞满预防式折叠旧工具输出(context clipse)
L3上下文已爆完整压缩历史成摘要
L4输出预算用完提升单轮输出配额
L5半句被截下一轮明确告诉它”接着说”
L6stop_hook 拦截外部检查没过,强制再跑一轮
L7token 预算耗尽写好状态,给 resume 接口,不要直接抛

金句:七级恢复不是处理一两种异常,是把”长任务可能死的所有方式”都铺成了梯子——这才是 Claude Code 能跑半小时长任务的前提。


二、工具与执行:Tool 不是函数,而是协议对象

2.1 Tool 接口:把治理写进协议

很多框架里 Tool 就是”一个函数 + 一段 description,模型说调就调”。Claude Code 把 Tool 做成了协议对象,每个 Tool 必须显式声明:

  • name / description / inputSchema / call
  • 是否只读
  • 是否并发安全
  • 权限检查回调

这些属性直接决定了:能不能并发跑、需不需要走权限弹窗、UI 上如何呈现、能不能修改文件。

默认值是悲观的:未声明 = 非并发 + 非只读 + 必走权限。换句话说,新工具默认走慢路径——串行执行、加权限检查、绕不开治理。只有显式声明”我是只读、并发安全”,才升级到快路径。

金句:安全和并发不是外包给一层管理层去管,而是写进协议里,变成每一个工具作者必须面对的设计责任。

2.2 执行管线:每一层都有治理点

模型输出一段 assistant_message,里面带一个或多个 tool_use block。Query 收到后不是直接调函数,而是走一整条治理链:

flowchart LR
    A[tool_use block] --> B[tool_orchestration<br/>按并发性分批]
    B --> C[validate_input_schema]
    C --> D[tool_hooks<br/>可拦截]
    D --> E[permission<br/>可询问/拒绝]
    E --> F[tool_core<br/>真正执行]
    F --> G[tool_result message]
    G --> H[下一轮 API]

关键不在层多,而在每一层都有显式的治理点——可拒绝、可改写、可补充上下文。模型不是想调就调,每一步都能被喊停。

2.3 并发分批:按声明拆批起跑

举例:模型一次输出七个 tool_use(A~G):

工具并发性调度
read_a, b, grep_c只读,安全并发执行
file_edit_d写操作,不安全独占串行
glob_e, web_fetch_f只读,安全并发执行
bash_g不安全独占串行

安全的并发起跑,不安全的退化成单条。底层还有一个 streaming tool executor,用 queued / executing / completed / yielded 四状态追踪——边接收 tool_use 边启动,不等模型整段输出完。


三、上下文与状态:在永远有限的窗口里活下来

这是整个 Runtime 的”灵魂层”。所有压缩、记忆、恢复机制,都在回答同一个问题:上下文永远是有限的,怎么用得最值

3.1 三段式 Prompt:稳定的放前面,会变的放后面

输入侧分成三类来源:

类型内容变化频率
system_prompt身份、规则、工具使用方式、输出风格几乎不变
user_system_contextCLAUDE.md、当前日期、git 状态每次会变
task_specific_promptcompact / session memory 等后台任务专用任务相关

System prompt 内部又切成「静态主干 + 动态边界」两段——前者放前面,后者放后面。为什么这么排? 因为服务器端的 prompt cache 按前缀匹配,前缀越稳定越长,重复请求命中率越高、token 越便宜。排序本身就是工程优化

身份槽(system_prompt)还有一条优先级链:override > coordinator > agent > custom > default。这条链解释了一件事——sub-agent 不是模型变了,是模型”看到自己是谁”变了

3.2 五级压缩梯子:每轮都做上下文治理

上下文会被工具输出挤爆。Claude Code 的压缩从轻到重分五级:

级别名字做什么
最轻snip把旧的、价值低的工具输出直接删掉,不做摘要(因为做摘要本身要花 token)
micro_compact清理旧 tool_result,保住前缀稳定不破坏 cache
context_clipse把多轮相似操作折叠成一段结构化摘要
auto_compact阈值触发的完整压缩,单独请求模型总结历史
最重session_memory_compact复用后台已抽取的 session memory,避免再调一次总结模型

两条关键设计:

  1. 轻量优先:每轮 API 请求前都按梯子顺序尝试,前面的策略已经把占用降到阈值以下,就不进入完整 compact——因为 auto_compact 自己也会失败、成本更高
  2. 完整压缩 ≠ 删历史:是 summary + post_compact_attachment 重建工作台(重新塞回工具声明、文件上下文、计划状态),否则模型会忘了自己刚干到哪一步

3.3 Memory:入口索引,不是正文仓库

memory.md 不是正文仓库,而是入口索引——每条记忆只记一个链接 + 一行描述,整个文件限制在 200 行 / 25 KB 以内。

为什么?因为如果让 memory 直接进 prompt,几次会话下来 prompt 就被长期状态彻底挤满了。所以走的是另一条思路——不是全量注入,而是按需召回

flowchart LR
    A[请求前扫描<br/>memory 文件头元数据] --> B[生成清单]
    B --> C[轻量模型<br/>从中选最多 5 个]
    C --> D[只把这 5 个的正文<br/>代入本轮请求]

这个流程叫 relevant_recall。Memory 还分三类:auto_memory(用户偏好,后台抽取)/ session_memory(当前会话滚动摘要)/ agent_memory(特定 Agent 长期经验,声明了 memory 字段才有)。

3.4 Transcript:append-only 事件流,不是聊天数组

Claude Code 关掉再打开还能接着跑,靠的是 transcript。它不是聊天记录数组,而是 append-only 事件流

  • 每个事件有唯一 uuid,并指向前一条 parent_uuid,靠这两个字段连成一条链
  • 主链有边界:只有四类事件能进——用户输入、模型输出、附加内容、系统消息
  • 写盘是 append-only + 批量 flush(内存队列 → 后台刷盘)
  • 文件尾部周期性重挂元数据(标题、模式、worktree),因为 resume 列表页用的是轻量读取器,只扫尾部一小段窗口

Resume 是一条四步重建管线:

  1. 加载日志:读整个 JSONL,按事件类型分桶
  2. 重建主链:从叶子节点沿 parent_uuid 一路回溯
  3. 修复断点:处理被 snip 删掉的中间节点造成的空洞,往前找还活着的祖先重新挂上
  4. 恢复运行态:plan、文件读取历史、context clipse 状态、Agent 模式,全部挂回内存,再把控制权交还给 REPL

四、安全与扩展:硬边界 + 统一治理

4.1 Sandbox 与 Permission:互补,不是替代

很多人会问:既然有 sandbox,为什么还要 permission?答案是两层各干各的事

  • Permission:回答”这条命令能不能执行”
  • Sandbox:处理”已经允许的命令怎么被进一步限制

每条 bash 命令到达宿主机之前要走四步:

  1. 逐条路由:判断要不要进沙箱,不符合就走普通路径
  2. 配置翻译:把用户配置(allowlist、读写目录)翻译成隔离环境真实可执行的限制
  3. 隔离运行:shell 层把命令包装到隔离环境里跑
  4. 收尾清理:清掉临时文件、可能影响后续的残留状态

金句:Sandbox 不是软规则,是硬边界。配置不是拿来对照检查,而是被翻译成系统级限制。

另一个重要细节:设置文件 / .claude 目录 / skill 目录列入禁止写入范围——防止一个被攻陷的命令通过修改 Agent 自己的配置来扩大影响。

4.2 MCP 与 Skills:统一治理,不开旁路

外部能力接入有两条路径,但都不开旁路

入口是什么怎么治理
MCP外部工具入池标准化成内建 Tool 对象,继续走 schema / permission / tool_result 整套链
Skills任务协议注入markdown + frontmatter + 可选脚本,按需激活而非全量常驻

Skills 的激活有三条入口:用户主动调用 / 模型自主选择 / path 字段条件触发(比如改 .py 文件时自动激活 Python 相关 skill)。目标只有一个——避免一堆备用技能把上下文撑爆

Tool search 还做了延迟加载:工具数量多时(一堆 MCP),不会一次性把所有接口描述塞进 prompt,而是先暴露一个轻量发现入口,按任务需要再展开具体工具边界。这直接降低 prompt 体积、提高 cache 命中。

4.3 多 Agent:三层架构,统一内核

形态解决什么
Sub-agent单入子链,复用 Runtime,隔离 transcript防止主链被工具产物撑爆
Coordinator不是多开 sub-agent,而是把主 Agent 的 system_prompt 换掉,让它变成派工人任务编排
Swarmmailbox + task_board + team_file团队协作、空闲自找活

Sub-agent 的本质很容易被误解。看起来像”不同角色干不同事”,实际上它几乎只是为了解决有限上下文

  • Sub-agent 调的还是同一个 API,不是别的模型
  • 它的 prompt 被大幅精简,中间产生的 tool_use 累积上下文对主 Agent 完全屏蔽
  • 主 Agent 只拿到 sub-agent 返回的一条摘要(典型动画:3 条原始消息 → 1 条摘要)

所有这些多 Agent 入口虽然分了层,但底下还是同一个 Runtime、同一条 query 执行链


五、收口:所有设计只为两件事

把整套架构压成一句话:模型意图 → 受治理的本地行动 → 可恢复的长期任务

从下到上三层:

flowchart BT
    A[状态底座<br/>Memory · Transcript · Resume] --> B[运行治理<br/>Query Loop · Tool · Permission · Sandbox]
    B --> C[能力入口<br/>Tools · MCP · Skills · Agent]

再往上抽,所有设计都在回答两个问题:

5.1 安全可靠

让大模型的自主行为是相对安全的——不会随便删你的重要文件、不会搞乱系统。Permission 默认放行但走串行慢路径、Sandbox 把配置翻译成硬边界、控制平面禁止写入、MCP/Skills 不开旁路——这些设计累加起来,才让一个能执行 shell 的 Agent 敢交给普通用户用

5.2 有限上下文

上下文永远是有限的,而且这个约束不会消失——就算未来上下文真的无限了,模型也很难在那么长的输入里找到重点(密集注意力计算撑不住,也没有重心)。所以:

  • Prompt 分静态/动态两段是为了 cache 命中
  • 五级压缩梯子是为了在不丢工作台的前提下腾空间
  • Memory 做成索引 + 召回是为了不挤占 prompt
  • Transcript append-only 是为了任务可恢复
  • Sub-agent 几乎只是为了上下文隔离——这点最容易被想成”多角色协作”

一页速览

层级内容一句话核心
核心结论安全可靠 + 有限上下文Claude Code 全部复杂度都在回答这两个问题
Runtime 内核CLI 只是壳所有形态共用 query 执行循环 + 七级错误恢复
工具与执行Tool 是协议对象治理写进协议,默认走慢路径
上下文与状态Prompt / 压缩 / Memory / Transcript永远有限的窗口里要活下来
安全与扩展Sandbox + Permission 互补MCP/Skills 不开旁路,多 Agent 统一内核

一句话收束

模型不是总指挥,Runtime 才是。所有看起来复杂的 Agent 工程,本质都是在用最少的 token 把最多的事情做安全。

相关文章