Claude Code 值得分析的地方,不在于它接了多少工具,也不在于它的命令行体验有多顺,而在于它把 AI Agent(人工智能代理)真正跑起来之后会遇到的复杂度,都放进了明确的运行时结构里。
很多 Agent Demo 在早期都很顺:用户输入一句话,模型决定调用工具,工具返回结果,模型继续生成回答。可一旦系统开始支持多种启动方式、权限确认、后台任务、多工具并发、长上下文压缩、远程执行和插件扩展,原来那个简单主循环很快会变成一团特判。
Claude Code 的设计更像一套 Agent Runtime(代理运行时):模型只是其中一个节点,围绕模型还有启动装配、控制面、状态机、工具协议、权限链、任务生命周期和扩展收敛层。
可以先用一张总图建立整体印象:
flowchart TB
A[启动层<br/>入口分流 / 进程初始化 / 会话准备] --> B{宿主选择}
B --> C[REPL 控制面<br/>交互式终端]
B --> D[Headless Engine<br/>无界面运行 / SDK / Remote]
C --> E[Query Loop<br/>连续运行状态机]
D --> E
E --> F[Tool Runtime<br/>工具解析 / 校验 / 执行 / 回灌]
F --> G[Permission System<br/>规则 / 自动判定 / 用户确认]
G --> H[Sandbox<br/>文件 / 网络 / 命令边界]
H --> I[真实外部世界<br/>文件系统 / Shell / 网络 / 服务]
E <--> J[Task Runtime<br/>后台任务 / Subagent / Remote Agent]
J --> E
K[扩展层<br/>MCP / Skills / Plugins] --> L[统一内部对象<br/>Tool / Command / Skill / Resource]
L --> C
L --> E
L --> F
这套架构可以理解成三条主链:
| 主链 | 解决的问题 | 关键模块 |
|---|---|---|
| 控制链 | 这一轮对话在什么上下文、能力和边界下运行 | 启动层、REPL、Query Loop |
| 执行链 | 模型决定行动后,系统怎样安全、稳定地落到真实世界 | Tool Runtime、Permission System、Sandbox |
| 任务链 | 长时任务、后台任务、多 Agent 如何被管理和回流 | Task Runtime、Subagent、Remote Agent |
扩展层不是一条孤立链路。它负责把 MCP、Skills、Plugins 等外部能力翻译成内部统一对象,再注入到控制链、执行链和任务链里。
1. 启动层:先定边界,再进入会话
成熟 Agent 通常不只有一种运行方式。Claude Code 至少要面对这些启动场景:
- 本地交互式终端;
- 无界面运行;
- SDK(Software Development Kit,软件开发工具包)调用;
- Remote 远程会话;
- 后台 Session 恢复;
- 某些不需要完整运行时的快速路径。
如果启动入口只是一个越来越大的 main(),所有模式都会在主循环里偷偷增加自己的语义。时间久了,交互式运行、远程运行、后台运行会变成几套相似但不兼容的系统。
Claude Code 的启动链路更克制,它大致分成三段:
flowchart LR
A[入口分流<br/>判断启动模式] --> B[进程级初始化<br/>配置 / 遥测 / 清理回调 / 全局设施]
B --> C[会话级准备<br/>cwd / sessionId / 工具面 / 权限 / 扩展 / 恢复方式]
C --> D{选择宿主}
D --> E[REPL 交互宿主]
D --> F[Headless / SDK / Remote 宿主]
这三段分别回答不同问题:
| 阶段 | 关心的问题 | 不该做的事 |
|---|---|---|
| 入口分流 | 这次到底是什么启动模式 | 不急着加载完整运行时 |
| 进程级初始化 | 当前进程能不能安全运行 | 不决定单轮 Agent 语义 |
| 会话级准备 | 当前会话拥有哪些能力、权限和上下文 | 不把宿主差异混进核心 Runtime |
这里有一个很关键的分层:进程状态和交互状态被分开管理。
像 cwd、projectRoot、sessionId、遥测信息、token 与 cost 计数,更接近基础设施状态,适合放在更底层的 bootstrap/state 中;而任务状态、MCP 客户端、插件状态、权限上下文、界面选择状态,更接近交互控制面,才进入 AppState。
这样做有两个好处。
一是宿主可以共享核心运行语义。交互式终端、无界面调用、远程会话不需要各写一套权限、工具和上下文逻辑。
二是执行边界能在第一轮请求前定型。哪些工具可用,当前权限模式是什么,扩展能力是否加载,会话是否从历史恢复,这些都会影响模型后续行为,不能等模型跑起来之后再临时猜。
启动层看起来“重”,但它的重量换来的是后续系统不分裂。对于 Agent 平台来说,越早支持多宿主、多模式、多会话,越需要把启动模式、会话制度和宿主承载拆开。
一个可操作的启动清单可以这样列:
启动前必须定型的边界:
- 当前运行模式:interactive / headless / sdk / remote / background
- 当前工作目录和项目根目录
- 当前 sessionId 与恢复策略
- 当前可用工具集合
- 当前权限模式
- 当前扩展来源与加载结果
- 当前系统约束和环境信息
- 当前宿主:REPL / API / Remote / Background
只要这些信息会影响执行边界,就应该在进入 Query Loop 前准备好。
2. REPL 控制面:UI 不是消息展示器
REPL(Read-Eval-Print Loop,读入-求值-输出循环)在很多命令行工具里只是输入框加输出区域。但对 Claude Code 这样的 Agent 来说,REPL 更像运行时控制台。
原因很简单:用户面对的不是一个只会回答问题的模型,而是一个会调用工具、申请权限、跑后台任务、接入外部能力的系统。UI(User Interface,用户界面)如果只显示文本,用户就无法理解系统当前在做什么,也无法在关键节点介入。
一次用户输入进入 REPL 后,并不会立刻丢给模型,而是会被放进当前运行制度里处理:
flowchart TB
A[用户输入] --> B{是否本地命令 / 快捷指令}
B -->|是| C[本地处理<br/>不进入 Query Loop]
B -->|否| D[组装本轮上下文]
D --> E[汇总能力面<br/>本地工具 / MCP 工具 / 插件命令 / Skills]
E --> F[合并运行状态<br/>任务 / 权限队列 / 远程会话 / MCP 连接]
F --> G[准备系统约束和用户环境]
G --> H[进入 Query Loop]
REPL 在这里承担两类职责。
一类是汇总当前能力面。它要知道本地有哪些工具,MCP(Model Context Protocol,模型上下文协议)连上了哪些外部工具,插件提供了哪些命令,Skills 又声明了哪些能力。用户提交输入的那一刻,REPL 要生成一个 turn-scoped context,也就是“这一轮请求可见的完整执行上下文”。
另一类是归并运行时事件流。Claude Code 消费的不是纯文本,而是一串带语义的事件:
assistant message
tool progress
pending permission
compact boundary
task notification
api error
remote session update
这些事件最终要被 REPL 重新组织成用户能理解的会话视图。用户不只看到“模型说了什么”,还应该看到:
- 当前正在执行哪个工具;
- 为什么系统暂停等待确认;
- 后台任务是否还在跑;
- 某个 MCP 服务是否断开;
- API 调用失败后系统是否在恢复;
- 当前上下文是否发生压缩。
这就是为什么复杂 Agent 的 UI 不能只是 View Component。它必须承担控制面职责。
可以用表格区分两种 UI 心智:
| UI 心智 | 典型实现 | 适合场景 | 问题 |
|---|---|---|---|
| 消息展示器 | 输入框 + 消息列表 | 单轮问答、简单聊天 | 工具、权限、后台任务都会变成黑箱 |
| 运行时控制台 | 输入、能力面、事件流、任务、权限统一编排 | 编程 Agent、自动化 Agent、多工具系统 | REPL 会变厚,需要明确状态边界 |
REPL 变大并不一定是坏事。只要它负责的是控制面编排,而不是把业务逻辑随意塞进界面层,这种集中反而能减少状态散落。
一个更健康的拆法是:REPL 负责汇总和呈现运行时状态,Query Loop 负责连续推理,Tool Runtime 负责动作执行,Permission System 负责授权决策。REPL 不应该直接执行工具,也不应该直接改权限规则,但它必须知道这些状态的变化,并把它们组织成用户可操作的界面。
3. Query Loop:把一次请求升级成连续运行状态机
普通聊天系统的核心逻辑通常很简单:
const response = await model.chat(messages)
return response.text
这种写法适合问答,不适合 Agent。Agent 一旦开始连续运行,就会遇到几个无法回避的问题:
- 上下文越来越长,必须压缩、裁剪或折叠;
- 模型可能输出工具调用,而不是最终答案;
- 工具结果要重新喂给模型;
- 模型输出可能被截断,需要恢复;
- prompt too long 需要兜底;
- 某些信息可以提前预取,减少等待;
- 多轮运行需要预算控制,不能无限循环。
Claude Code 的 Query Loop 更像一个状态机,而不是模型 API(Application Programming Interface,应用程序编程接口)的薄封装。
它维护的不是一次性请求参数,而是一组跨迭代状态:
type QueryState = {
messages: Message[]
toolUseContext: ToolUseContext
maxOutputTokensOverride?: number
maxOutputTokensRecoveryCount: number
autoCompactTracking: AutoCompactTracking
hasAttemptedReactiveCompact: boolean
turnCount: number
pendingToolUseSummary?: ToolUseSummary
transition?: QueryTransition
}
这些字段说明了一个关键事实:一次 Agent Turn 不是线性 API 调用,而是一段会被工具、上下文压缩、失败恢复、预算和中断反复改写的运行过程。
Query Loop 的主链路可以压缩成这样:
flowchart TB
A[进入本轮 Query] --> B[预取 Memory / Skills]
B --> C[应用上下文预算]
C --> D[Snip / Microcompact / Collapse / Autocompact]
D --> E[流式调用模型]
E --> F{模型是否发出 tool_use}
F -->|否| G[完成本轮输出]
F -->|是| H[Tool Runtime 执行工具]
H --> I[工具结果标准化]
I --> J[写回消息流]
J --> C
这条链路里,模型不是终点,只是运行图中的一个节点。工具调用也不是旁路,而是 Query Loop 的主路径之一。
更接近代码的骨架如下:
while (true) {
await prefetchMemoryAndSkills(state)
let messagesForQuery = applyContextBudget(state.messages)
messagesForQuery = compactIfNeeded(messagesForQuery, state.autoCompactTracking)
const assistantMessages = await streamModel(messagesForQuery, {
maxOutputTokens: state.maxOutputTokensOverride,
})
const toolUses = collectToolUses(assistantMessages)
if (toolUses.length === 0) {
return finishTurn(assistantMessages)
}
const toolResults = await runTools(toolUses, state.toolUseContext)
state.messages = writeBackToolResults({
previousMessages: state.messages,
assistantMessages,
toolResults,
})
state.turnCount += 1
}
这里最重要的动作是 writeBackToolResults。工具结果不会只写进日志,也不会简单打印到终端,而是会被标准化成模型下一轮能理解的消息,再回到主消息流。
一个常见的工具回灌链路如下:
const messagesForQuery = getMessagesAfterCompactBoundary(messages)
const assistantMessages = await streamModel(normalize(messagesForQuery))
const toolUseBlocks = collectToolUses(assistantMessages)
const toolUpdates = await runTools({
toolUseBlocks,
assistantMessages,
canUseTool,
toolUseContext,
})
const toolResults = normalizeToolResults(toolUpdates)
state.messages = [
...messages,
...assistantMessages,
...toolResults,
]
这意味着工具执行链和会话链没有分裂。工具结果会以协议化形式进入上下文,模型在下一轮推理时能看到“刚才发生了什么”。
Query Loop 还要处理坏路径。真实系统中,坏路径并不是边角情况,而是高频情况:
| 问题 | 如果没有 Runtime 机制 | 更稳的处理方式 |
|---|---|---|
| 上下文过长 | 模型直接报错 | snip、compact、collapse、autocompact |
| 输出被截断 | 用户只看到半截答案 | max output recovery |
| prompt too long | 整轮失败 | reactive compact 后重试 |
| 模型不可用 | 请求失败 | fallback model |
| 工具失败 | 主循环中断 | 工具错误标准化后回灌 |
| 长任务超预算 | 无限跑或直接断 | turn budget / task budget |
Claude Code 的 Query Loop 成熟之处在于:上下文治理、失败恢复、工具回灌都被提升成 Runtime 机制,而不是散落在业务代码和 prompt 技巧里。
当 Agent 还只是单轮问答时,模型调用封装足够;当 Agent 开始连续执行任务时,Query Loop 就必须单独成为一层系统设计。
4. Tool Runtime:把工具调用变成受控系统调用
很多 Agent 项目的工具抽象是这样的:
type Tool = (input: unknown) => Promise<string>
这只能说明模型可以调用一个函数,却无法说明这个函数是否安全、是否能并发、是否需要权限、进度如何上报、失败如何表达、结果如何回灌。
Claude Code 的 Tool 更接近一个受控执行单元:
interface Tool {
name: string
description: string
inputSchema: Schema
canRunInParallel: boolean
validate?(input: unknown): ValidationResult
execute(
input: unknown,
context: ToolUseContext
): AsyncIterable<ToolEvent>
toModelResult(output: ToolOutput): StructuredToolResult
}
这里的 Tool 已经不是普通函数,而是带有完整运行时语义的对象。它至少包含这些制度:
- schema 输入校验;
- 权限关联;
- 并发安全声明;
- 中断语义;
- 流式进度;
- 错误归一化;
- 结果回填规则。
工具执行链也不是“找到函数然后调用”,而是分成多个阶段:
flowchart LR
A[tool_use] --> B[解析真实工具<br/>名称 / alias / fallback]
B --> C[Schema 校验]
C --> D[工具自定义校验]
D --> E[PreToolUse Hooks]
E --> F[Permission Decision]
F --> G{allow?}
G -->|deny / ask failed| H[包装成标准 tool_result]
G -->|allow| I[执行工具]
I --> J[进度事件 / 取消处理]
J --> K[结果标准化]
K --> L[写回 Query Loop]
H --> L
更贴近实现的伪代码如下:
async function* runToolUse(
toolUse: ToolUseBlock,
assistantMessage: AssistantMessage,
canUseTool: PermissionChecker,
ctx: ToolUseContext
) {
const tool =
findToolByName(ctx.options.tools, toolUse.name) ??
findToolAlias(toolUse.name)
if (!tool) {
yield toolResultError(toolUse.id, "No such tool available")
return
}
if (ctx.abortController.signal.aborted) {
yield cancelledToolResult(toolUse.id)
return
}
const parsed = tool.inputSchema.safeParse(toolUse.input)
if (!parsed.success) {
yield validationErrorToolResult(toolUse.id, parsed.error)
return
}
yield* streamedCheckPermissionsAndCallTool({
tool,
toolUseId: toolUse.id,
input: parsed.data,
context: ctx,
canUseTool,
assistantMessage,
})
}
工具上下文也不是简单传参:
type ToolUseContext = {
options: {
tools: Tool[]
commands: Command[]
mcpClients: MCPClient[]
refreshTools?: () => Promise<void>
}
abortController: AbortController
messages: Message[]
setAppState: (patch: Partial<AppState>) => void
setInProgressToolUseIDs: (ids: string[]) => void
setResponseLength: (length: number) => void
}
这说明 Tool Runtime 需要和 UI 状态、取消信号、消息历史、MCP 客户端、工具刷新机制协作。它不是函数调用器,而是行动协议层。
Claude Code 在工具层还有两个重要判断。
并发策略不由模型决定,而由工具语义决定。只读工具可以并发,写文件、执行命令、修改环境的工具必须更谨慎。如果调度层简单 Promise.all(),模型一次输出多个副作用工具调用时,很容易造成顺序错乱和状态污染。
流式工具执行也被正式建模。模型还在流式输出时,某些工具可以提前启动,但 Runtime 必须跟踪工具状态、缓存结果、处理取消,并保证最终结果按正确语义回到主循环。否则“边生成边执行”会变成竞态条件制造机。
工具层一旦制度化,新工具接入就不需要重新发明这些横切问题:
| 横切问题 | 放在每个工具里 | 放进 Tool Runtime |
|---|---|---|
| 参数校验 | 风格不统一 | schema 和 validate 统一 |
| 权限检查 | 容易遗漏 | 调用前统一决策 |
| 并发控制 | 依赖工具开发者自觉 | Runtime 按工具语义调度 |
| 错误表达 | 字符串随意返回 | 标准 tool_result |
| 进度上报 | 每个工具各写各的 | 统一 ToolEvent |
| 结果回灌 | 会话链和工具链分裂 | 标准化写回消息流 |
只要工具开始碰文件、命令、网络和副作用,Tool Runtime 就不是过度设计,而是 Agent 能稳定行动的基础设施。
5. Permission System:权限不是一个确认框
Agent 权限系统最容易被做成一个弹窗:允许还是拒绝。这个模型太薄,因为真实权限包含四个问题:
- 逻辑上是否应该允许;
- 能否自动判断,不打断用户;
- 什么时候必须让用户确认;
- 即使允许了,进程层面到底能触达什么。
Claude Code 把权限拆成一条执行链:
flowchart TB
A[工具请求] --> B[规则层匹配<br/>allow / deny / ask]
B --> C[保留来源和理由]
C --> D[运行时判定<br/>classifier / hooks / coordinator]
D --> E{是否能自动决定}
E -->|能| F[返回 allow / deny]
E -->|不能| G[交互确认]
G --> H[用户决策]
F --> I[执行隔离映射]
H --> I
I --> J[Sandbox 边界<br/>文件 / 网络 / 命令]
J --> K[工具执行或拒绝结果]
权限决策对象不是布尔值,而是一种可解释结构:
type PermissionDecision =
| {
behavior: "allow"
updatedInput?: unknown
decisionReason?: string
}
| {
behavior: "ask"
message: string
suggestions?: PermissionUpdate[]
blockedPath?: string
pendingClassifierCheck?: PendingClassifierCheck
}
| {
behavior: "deny"
message: string
decisionReason: string
}
这个结构很重要。它不仅回答“过不过”,还回答:
- 为什么允许;
- 为什么拒绝;
- 卡在哪个路径或资源;
- 是否有建议的权限更新;
- 是否还有自动判定在等待。
这让权限可以被 UI 展示、被日志记录、被任务系统感知、被 Query Loop 继续消费。
更关键的是,Claude Code 区分了逻辑授权和执行隔离。
逻辑授权回答“应不应该让模型做这件事”。执行隔离回答“即使允许了,它在操作系统里究竟能做到什么”。如果只做逻辑授权,权限系统就只是提示框;如果只做沙箱,用户又很难理解为什么系统被拦住。两者打通之后,权限才同时具备可解释性和真实约束力。
权限调用链可以简化成这样:
const parsedInput = tool.inputSchema.safeParse(input)
const validatedInput = tool.validateInput?.(parsedInput.data)
const hookResult = await runPreToolUseHooks(tool, validatedInput)
const permissionDecision = await resolvePermissionDecision({
hookResult,
tool,
input: validatedInput,
canUseTool,
})
if (permissionDecision.behavior !== "allow") {
return createToolResult({
toolUseId,
status: permissionDecision.behavior,
message: permissionDecision.message,
reason: permissionDecision.decisionReason,
})
}
const result = await tool.call(validatedInput, toolUseContext)
return mapToolOutputToToolResult(toolUseId, result)
注意这里的关键点:权限拒绝也会被包装成标准工具结果返回给 Query Loop。主循环不需要因为一次拒绝而崩掉,模型可以根据“权限被拒绝”的结构化结果继续调整计划。
权限系统还有一个成熟的产品判断:自动模式不等于无边界放行。自动模式应该在收紧危险能力后尽量自动,而不是为了少打断用户就放宽 Bash、PowerShell、通配 agent 等高风险能力。
权限系统真正成熟的标志,不是弹窗更多,而是决策、理由、来源和执行边界都能被系统化管理。
6. Task Runtime:多 Agent 的核心是任务生命周期
多 Agent 经常被讲成 prompt 分工:一个 manager agent 负责拆任务,多个 worker agent 负责执行。这个说法只覆盖了表层。真正困难的是:多个执行体同时存在时,状态怎么管、进度怎么看、失败怎么恢复、结果怎么回流、上下文怎么隔离。
Claude Code 的思路是先统一任务抽象,再把多种执行形态映射进去:
- 主会话后台化;
- 本地 Subagent;
- in-process teammate;
- Remote Agent;
- 任务通知;
- 状态恢复;
- 输出回流。
也就是说,Subagent 先是一个 Task,再是一个 Agent。
一个本地子 Agent 的状态不会只是 Promise<string>,而更像这样:
type LocalAgentTaskState = {
agentId: string
prompt: string
progress?: AgentProgress
error?: string
result?: AgentToolResult
messages?: Message[]
isBackgrounded: boolean
pendingMessages: string[]
retain: boolean
diskLoaded: boolean
evictAfter?: number
}
这些字段说明了多 Agent 的真实复杂度:
| 字段 | 代表的系统语义 |
|---|---|
pendingMessages | 子 Agent 不是一次性函数,而是有“邮箱”的执行体 |
isBackgrounded | 前台和后台切换是正式状态 |
messages | 子 Agent 有自己的会话轨迹 |
retain | UI 是否继续持有这个任务 |
diskLoaded | 历史记录是否已从磁盘恢复 |
evictAfter | 长期任务需要回收策略 |
一个更通用的任务抽象可以写成这样:
interface Task {
id: string
status:
| "pending"
| "running"
| "blocked"
| "done"
| "failed"
| "cancelled"
progress: ProgressState
output: StructuredOutput[]
notifications: Notification[]
cancel(): void
resume(): void
send(message: string): void
}
这个抽象比 spawnAgent(prompt): Promise<string> 厚得多,但它解决的是长期运行系统真正会遇到的问题。
Task Runtime 的关键职责可以用一张图表示:
flowchart TB
A[主会话] --> B[创建任务]
B --> C{任务类型}
C --> D[后台主会话]
C --> E[本地 Subagent]
C --> F[进程内协作者]
C --> G[Remote Agent]
D --> H[统一任务状态]
E --> H
F --> H
G --> H
H --> I[进度通知]
H --> J[消息记录]
H --> K[失败恢复]
H --> L[结果产物]
I --> M[REPL 可见]
J --> M
K --> M
L --> N[回流主 Query Loop]
这里还有一个容易被低估的点:上下文隔离。多个 Agent 并发运行时,如果身份、权限、工具上下文、通知目标发生串线,系统会出现很难排查的问题。Claude Code 对进程内协作者使用异步上下文隔离,目的就是保证每个执行体有独立上下文,而不是共享一堆容易污染的全局变量。
多 Agent 要从 Demo 走向可用系统,关键不是让 prompt 分工看起来更聪明,而是把生命周期、通知、恢复和回流做好。前台和后台的差别应该是调度与可见性差异,不应该变成两套 Agent 世界观。
7. MCP、Skills、Plugins:外部可以复杂,内部必须收敛
Agent 平台越往后做,扩展来源一定越多:
- MCP 服务器;
- 用户自定义命令;
- 项目级能力;
- 插件市场;
- 内建能力包;
- 团队私有工具;
- 远程资源。
危险不在于外部来源多,而在于每接一种来源,主系统就多一种内部模型。这样发展下去,UI、权限、Query Loop、Tool Runtime 都要为不同扩展源写特殊逻辑。
Claude Code 的关键策略是:动态能力面,稳定内部对象。
MCP prompt 不直接以 MCP 原生对象进入系统,而是翻译成 Command;MCP tool 会进入 Tool Runtime;MCP resource 会进入资源或资源工具体系;必要时还会注入认证工具。也就是说,MCP 接入重点不是“连上服务器”,而是“把外部能力翻译成本地运行时能理解的对象”。
Skills 也不是简单 prompt snippet,而是一种能力声明:
type SkillDescriptor = {
description: string
allowedTools: string[]
whenToUse?: string
model?: Model
effort?: Effort
hooks?: Hooks
executionContext?: "fork"
agent?: string
}
这份声明可以约束:
- 什么时候适合使用;
- 允许调用哪些工具;
- 偏好的模型;
- 推理力度;
- hooks;
- 执行上下文;
- 是否关联某个 agent 身份。
当 Skill 被建模成声明对象,它就可以进入统一能力面,而不是散落在 prompt 文本里。
Plugin 则更像能力组合包。一个插件可以带来命令、Skills、hooks、MCP 接入、语言服务、代理定义、输出风格和设置项。插件加载层要处理来源、校验、缓存、版本、启停和策略,这已经是小型平台分发问题。
扩展收敛关系可以画成这样:
flowchart LR
A[MCP Server] --> B[Adapter]
C[Skills] --> B
D[Plugins] --> B
E[Built-in Capabilities] --> B
F[Project / User Custom] --> B
B --> G[Tool]
B --> H[Command]
B --> I[Skill]
B --> J[Resource]
B --> K[Hook]
G --> L[统一能力面]
H --> L
I --> L
J --> L
K --> L
L --> M[REPL]
L --> N[Query Loop]
L --> O[Tool Runtime]
L --> P[Permission System]
平台扩展的核心原则可以写成一句话:外部格式可以很多,内部对象必须少。
| 外部来源 | 不推荐做法 | 更稳的做法 |
|---|---|---|
| MCP Tool | 直接把 MCP 对象塞进主循环 | 翻译成内部 Tool |
| MCP Prompt | 单独做一套 Prompt 调用逻辑 | 翻译成 Command |
| Skill | 当作 prompt 字符串拼接 | 建模成能力声明 |
| Plugin | 每个插件自己决定 UI、权限、执行方式 | 插件只贡献内部对象和配置 |
| 内建能力 | 写死在主流程里 | 复用 Tool / Command / Skill 等对象 |
这样做前期接入成本更高,因为每种外部能力都要认真映射到内部模型。但长期收益很明显:UI 不需要理解所有外部协议,权限系统不需要给每个扩展源开后门,Query Loop 也不需要为每种能力写分支。
平台能不能长大,取决于内部语言是否稳定。扩展层的价值不是让系统认识更多外部格式,而是让系统在面对更多外部世界时,内部仍然只说同一种语言。
8. 把复杂度放到正确层级
Claude Code 的架构不是“层数越多越好”,而是每类复杂度都有明确承接位置。
| 复杂度类型 | 应该放到哪里 | 如果放错位置会怎样 |
|---|---|---|
| 启动模式、宿主差异、会话边界 | 启动层 | 主循环充满模式判断 |
| 用户输入、权限状态、任务状态、事件流 | REPL 控制面 | UI 变成黑箱消息列表 |
| 长上下文、工具回灌、失败恢复、续跑预算 | Query Loop | 模型调用层堆满特判 |
| 参数校验、进度、错误、并发、结果格式 | Tool Runtime | 每个工具都有一套风格 |
| 允许、拒绝、确认、隔离 | Permission System | 权限变成弹窗或硬沙箱 |
| 后台任务、Subagent、Remote Agent | Task Runtime | 多 Agent 变成一堆黑盒 |
| MCP、Skills、Plugins 等外部能力 | 扩展收敛层 | 主系统被外部格式污染 |
一次真实请求穿过这些层,大致会经历这样的路径:
sequenceDiagram
participant User as 用户
participant REPL as REPL 控制面
participant Query as Query Loop
participant Tool as Tool Runtime
participant Perm as Permission System
participant Sand as Sandbox
participant Task as Task Runtime
participant Ext as 扩展层
Ext-->>REPL: 注入 Tool / Command / Skill / Resource
User->>REPL: 提交输入
REPL->>REPL: 汇总能力面、权限状态、任务状态
REPL->>Query: 创建本轮执行上下文
Query->>Query: 预算控制、上下文压缩、预取能力
Query->>Query: 流式调用模型
Query->>Tool: 模型发出 tool_use
Tool->>Perm: 请求权限决策
Perm->>Sand: 映射执行边界
Sand-->>Tool: 允许执行或限制
Tool-->>Query: 标准化 tool_result
Query->>Task: 需要长时执行时创建任务
Task-->>REPL: 通知进度和状态
Query-->>REPL: 返回结构化事件流
REPL-->>User: 展示消息、进度、权限和任务状态
这个路径体现了 Claude Code 的几个核心判断:
- 执行边界要在第一轮推理前确定。
- UI 要成为运行时控制面,而不是文本展示器。
- Query Loop 要管理连续运行状态,而不是只封装模型 API。
- 工具调用要被制度化,尤其在存在副作用时。
- 权限要同时具备可解释决策和真实隔离。
- 多 Agent 要从任务生命周期入手,而不是从 prompt 分工入手。
- 扩展层要把外部复杂度翻译成内部少数稳定对象。
成熟 Agent 系统不是“一个大模型拖着一堆函数”,而是“控制面 + 受约束执行链 + 长期任务系统 + 能力收敛层”。模型负责推理,Runtime 负责让推理过程可控、可恢复、可观测、可扩展。
很多 Agent 项目真正的问题不是模型不够强,而是系统没有准备好承接模型开始行动后的复杂度。Claude Code 给出的架构启发很直接:不要把所有问题塞进一个主循环;让边界问题、连续运行问题、行动问题、风险问题、并发问题和扩展问题分别在合适的层里解决。