源码版本:claude-code 2.1.88

本文基于对 Claude Code 源码的深度阅读,分析其 Agent 架构中最值得借鉴的设计理念。


一、Agent 的心脏:异步生成器循环

它是什么

Claude Code 的核心是一个 async function* query() —— 一个 异步生成器 函数。整个 Agent 的工作方式,就像一条永不停止的传送带:

用户输入 → 模型思考 → 输出文本/工具调用 → 执行工具 → 结果回传 → 继续思考 → …

这条传送带用 while(true) 驱动,每轮迭代就是一次"模型响应 + 工具执行"的完整周期。外部通过 for await (const message of query(...)) 消费产出,来一条处理一条

为什么巧妙

想象你在餐厅点了一份套餐。传统做法是:前菜、主菜、甜点全部做好了一起上(Promise 模式)。而异步生成器更像是一道菜做好了就端上来——前菜上桌的同时主菜已经在做了。

这意味着:

  • 渐进式渲染:用户能立即看到模型的第一个字,不用等整轮结束
  • 自然背压:如果消费者(UI)处理不过来,生产者(模型流)自动减速
  • 组合性:子流程通过 yield* 委托,像俄罗斯套娃一样嵌套

源码位置:query.ts:219


二、流式工具执行:不等齐就开跑

它是什么

大多数 Agent 系统的工作方式是:

等模型输出完所有 tool_use → 一次性执行所有工具 → 全部结果回传

Claude Code 的 StreamingToolExecutor 打破了这个等待:

模型输出 tool_use A → 立即执行 A
模型输出 tool_use B → 立即执行 B(如果安全,跟 A 并行)
模型输出 tool_use C → 等前面执行完(如果是写操作)

为什么巧妙

想象你在超市结账。传统模式是:等所有商品扫完码,再一起付钱、装袋。而 Claude Code 更像是边扫码边装袋——扫一件装一件,有冲突的商品(比如冷冻食品和热食)才需要等一等。

具体实现上:

  • 只读工具(Read、Grep、Glob)可以安全并行——它们不会改任何东西
  • 写操作(Edit、Write、Bash)必须串行——避免冲突
  • 工具有一个 isConcurrencySafe() 方法来声明自己的安全性

这个区分看似简单,实则关键。在代码分析场景中,Agent 经常一次发起 5-10 个搜索请求,并行执行可以将延迟从 10x 单次延迟 压到 1x 单次延迟

源码位置:StreamingToolExecutor.ts


三、Fork 子 Agent:字节级 Prompt Cache 共享

它是什么

Claude Code 的子 Agent(如后台记忆提取、Session Memory)启动时,不是从零开始的对话,而是完整继承父 Agent 的对话历史和系统提示词

为什么巧妙

这涉及到大模型 API 的一个底层优化:Prompt Cache(提示词缓存)。API 端会缓存已处理过的 prompt 前缀,下次请求如果前缀相同,就能跳过重复计算,直接命中缓存。

问题是:缓存是字节级精确匹配的。哪怕差一个空格,整段缓存就失效了。

所以 Claude Code 做了这些事:

  1. CacheSafeParams 类型显式声明了哪些参数必须一致(system prompt、tools、model、messages、thinking config)
  2. FORK_PLACEHOLDER_RESULT —— 所有 fork 子进程的工具结果占位符必须完全相同,确保字节对齐
  3. 子进程的系统提示词不是重新生成,而是直接引用父进程的同一份

想象你在写一份合同模板给律师审阅。如果 10 个子公司都要用同一份模板,聪明的做法是让律师审一次(缓存),后面 9 个只看修改的部分(增量)。但如果每个子公司都改了一个字,律师就要从头看 10 遍(缓存全部失效)。Claude Code 的 fork 设计就是确保前 N 页一个字都不改

源码位置:forkSubagent.ts:60-72, forkedAgent.ts:57-68


四、YOLO 分类器:用 AI 审 AI

它是什么

在 Auto 模式下,Claude Code 不会对每个工具调用都弹权限确认框。它用了一个"YOLO 分类器"来判断命令是否安全——安全就自动放行,危险才问用户。

为什么巧妙

这个分类器本身就是一个 Claude API 调用,不是一个独立训练的小模型。它通过巧妙的 prompt 设计,让 Claude 自己充当安全审核员:

两阶段审核流程:

Stage 1(快审):max_tokens=64,要求模型立即输出 <block>allow</block> 或 <block>deny</block>
  ↓ 如果 allow → 直接放行(快路径,延迟极低)
  ↓ 如果 deny
Stage 2(深思):放宽 token 限制,允许 chain-of-thought 推理,减少误杀

这就像机场安检的快速通道

  • 大多数旅客(安全命令)走快速通道,扫一眼就过——对应 Stage 1
  • 可疑行李触发开箱检查——对应 Stage 2
  • 两个阶段共享同一套安检标准(system prompt),且共享 prompt cache

细节上还有个讲究:关掉 thinkingtemperature: 0thinking: false)。因为 extended thinking 产出的 token 会被解析器忽略,白白浪费。只有内部特殊模型(强制 thinking)才保留,并额外加 2048 token 余量防止 thinking 吃光预算。

还有一个防死循环设计:拒绝追踪自动降级。如果 auto 模式连续被拒 3 次,自动降回"每次都问用户"的 prompt 模式。这防止 Agent 在无人值守时陷入"申请→被拒→再申请→再被拒"的死循环。

源码位置:yoloClassifier.ts, denialTracking.ts


五、任务完成的判定:不是你想停就能停

它是什么

直觉上,判断 Agent 任务是否完成很简单——模型没有调用工具,就说明它认为任务做完了。Claude Code 的实现确实以此为基础,但在上面叠加了多层"拦截器"。

为什么巧妙

想象一个法官判案。表面上是"法槌一敲就结案",但实际流程是:

法槌落下(模型没有输出 tool_use)
  → 这是程序性错误吗?(413/Media/MaxTokens → 不算,重新审理)
  → 这是 API 故障吗?(错误消息 → 特殊处理,直接结束)
  → 陪审团有异议吗?(Stop Hooks → 可能把案子打回重审)
  → 预算用完了吗?(Token Budget → 可能强制续写)
  → 全部通过 → 真正结案 ✅

其中最有趣的是 Stop Hook。它是一个用户可配置的外部脚本,在 Agent "认为"自己完成之后运行。Stop Hook 可以:

  • 打回重做(blockingError):注入一条消息告诉模型"你漏了什么",然后 continue 回主循环
  • 强制终止(preventContinuation):不等模型同意,直接结束
  • 静默通过:确认任务确实完成了

这是一个**“默认结束,但多方可否决”**的设计。比起简单的"有 tool_use 就继续,没有就停",它给了用户、Hook 系统、预算系统各一个否决权。

另一个细节:stop_reason === 'tool_use' 在 API 中不可靠,不总是被正确设置。所以 Claude Code 不依赖它,而是自己在流式接收时维护一个 needsFollowUp 标志位。这体现了工程实践中一个重要原则:不信任外部系统的隐式状态,自己维护自己需要的真相

源码位置:query.ts:554-558(needsFollowUp 定义), query.ts:1062-1357(完成分支), stopHooks.ts(Stop Hook 处理)


六、上下文管理:投影而非快照

它是什么

Agent 在长对话中会逐渐耗尽上下文窗口。常见的做法是:上下文太长时,做一次摘要替换。Claude Code 用了一种更精细的方式——上下文坍缩(Context Collapse)

为什么巧妙

想象你在读一本 1000 页的小说。传统摘要方式是:读到第 500 页时,让人帮你写个 50 页的摘要,扔掉前 500 页原文,然后带着摘要继续读。问题是你丢掉了所有细节。

Claude Code 的做法更像折叠地图

  • 原始对话历史不删除,完整保留
  • 维护一个"坍缩日志"——记录哪些部分被折叠了
  • 发送给 API 时,动态投影出折叠后的视图
  • 需要细节时可以"展开"某个特定部分

系统注释写道:

“Nothing is yielded — the collapsed view is a read-time projection over the REPL’s full history.”

翻译过来就是:什么都不丢弃,只是在"读取时"投影出一个更短的版本

此外还有多种互补策略:

  • Auto-compaction:自动在 token 使用超标时触发摘要
  • Micro-compaction:更细粒度,通过 cache editing 保留工具结构
  • Snip compaction:只裁剪冗长的工具输出,保留关键消息
  • Reactive compaction:在 prompt-too-long 错误发生后的紧急抢救

这五套策略层层递进,从"预防"到"急救"形成完整的上下文管理防线。

源码位置:query.ts:440-447(context collapse), autoCompact.ts(自动摘要)


七、工具结果持久化:避免无限递归

它是什么

每个工具有一个 maxResultSizeChars 属性。当工具输出超过这个阈值时,结果会被持久化到磁盘,消息中只保留一个文件路径引用。

为什么巧妙

考虑这个场景:Read 工具读取了一个大文件,输出超过阈值,被持久化到了 /tmp/result.txt。模型看到消息里说"结果已保存到 /tmp/result.txt",于是发起第二次 Read 去读那个文件。那个文件的内容又超过阈值,又被持久化……无限递归

Claude Code 的解决方案极其简洁:Read 工具的 maxResultSizeChars 设为 Infinity

这意味着 Read 的输出永远不会被持久化,无论多大都留在消息中。从根源上消除了递归的可能。

这体现了安全设计中的一个重要原则:不要用复杂的逻辑来防止循环,而是从结构上让循环不可能发生

源码位置:Tool.ts:466-468


八、错误恢复:五层防线

它是什么

Agent 在运行中会碰到各种错误:模型挂了、输出截断了、上下文太长了、工具执行失败了……Claude Code 为每种情况都准备了恢复策略,形成五层防线:

层级 错误类型 恢复策略 源码位置
1 模型故障 切换到备用模型,清理残留状态重试 query.ts:893-951
2 输出截断 注入续写指令,让模型从断点继续(最多 3 次) query.ts:1222-1257
3 上下文过长 先尝试释放已缓存的坍缩,再做紧急摘要 query.ts:1085-1183
4 工具结果缺失 为未匹配的 tool_use 生成合成错误消息 query.ts:123-149
5 流式中途故障 发射"墓碑消息"清理无效的 thinking block query.ts:713-728

为什么巧妙

想象一个经验丰富的登山向导:

  • 路有点滑 → 换条路走(模型 fallback)
  • 天色晚了 → 找个地方过夜,明天继续(输出截断续写)
  • 雪崩封路 → 先清理积雪(collapse drain),清理不了就改道(reactive compact)
  • 装备掉了 → 用替代品补上(合成错误消息)
  • 队友走散了 → 留个标记等他们跟上(墓碑消息)

关键是每一层恢复都不是简单的重试,而是针对特定错误类型的专门策略。而且它们之间可以串联——第 3 层尝试失败后,可能退到第 5 层做最后的清理。


九、动态工具加载:按需取用

它是什么

Claude Code 支持大量的工具——内置的 Read/Write/Edit/Bash/Grep/Glob,加上用户通过 MCP 接入的外部工具。如果把所有工具的 JSON Schema 都放在初始 prompt 里,会消耗大量 token。

解决方案:工具可以标记为 defer_loading: true,不在初始 prompt 中发送。模型通过一个特殊的 ToolSearchTool 来搜索和加载需要的工具。

为什么巧妙

想象你去图书馆借书。一种做法是把图书馆所有书都搬回家(全部工具 schema 都放 prompt),显然不现实。另一种是先带一个目录索引(工具名称和一句话描述),需要哪本再去书架上取。

这就是 ToolSearchTool 的作用。它像一个图书管理员

  • 模型说"我需要一个操作文件的工具"
  • ToolSearch 返回相关工具的完整 schema
  • 模型获得足够信息后正常调用

这对 MCP 生态尤其重要——用户可能接入了 20 个 MCP 服务,每个服务有 5-10 个工具,总共 100+ 个工具。全部放 prompt 里根本放不下。

源码位置:Tool.ts:439-450


十、Hook 系统:可编程的生命周期

它是什么

Claude Code 在几乎所有关键生命周期节点都暴露了 Hook 接口。用户可以注册 Shell 脚本,在特定事件触发时执行,并且可以修改行为

支持的 Hook 事件包括:

类别 事件
工具执行 PreToolUse, PostToolUse, PostToolUseFailure
会话生命周期 SessionStart, SessionEnd, Stop, StopFailure
Agent 生命周期 SubagentStart, SubagentStop
上下文管理 PreCompact, PostCompact
用户交互 UserPromptSubmit, PermissionRequest
任务管理 TaskCreated, TaskCompleted
状态变更 FileChanged, CwdChanged, ConfigChange

为什么巧妙

这就像编程语言中的面向切面编程(AOP)。你不需要修改 Agent 的核心逻辑,就能在任意切面插入自定义行为。

而且 Hook 不仅是"监听器",它有修改能力

  • PreToolUse Hook 可以修改工具的输入参数
  • Stop Hook 可以阻止 Agent 停下来(让它继续干活)
  • Permission Hook 可以覆盖权限决策(但有一个安全不变量:Hook 的 allow 不能覆盖用户的显式 deny)

最后一个点特别重要。这体现了安全设计的最小权限原则——即使 Hook 被恶意配置,也无法绕过用户明确设定的安全规则。

源码位置:hooksConfigManager.ts, toolHooks.ts


总结:设计的核心张力

纵观 Claude Code 的架构,几乎所有巧妙的设计都在解决同一对矛盾:

低延迟 vs 安全控制

  • 流式工具执行 → 降低延迟
  • YOLO 分类器 → 自动审批降低延迟,同时保持安全
  • Prompt Cache 共享 → 降低子 Agent 启动延迟
  • 动态工具加载 → 减少初始 prompt 大小
  • 上下文坍缩 → 延长可用对话轮次

而在安全这一侧:

  • 分层权限模型(规则 > Hook > 分类器)
  • Hook 的 allow 不能覆盖用户 deny
  • Read 的 Infinity 预算从结构上防递归
  • 拒绝追踪自动降级防死循环

这种**“默认快速,层层安全兜底”**的设计哲学,使得 Claude Code 在保持响应速度的同时,不牺牲用户对 Agent 行为的控制权。这对任何构建 AI Agent 系统的人来说,都是值得借鉴的思路。


本文基于 claude-code 2.1.88 源码分析,源码目录结构可能随版本迭代发生变化。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐