关键词:queryLoop / StreamingToolExecutor / Permission System / Hooks / Agent Loop / 安全闸门
真正让 Agent 跑起来的是循环,让它不出事的是权限系统
到这里,Claude Code 的主骨架已经很清楚了,但还差两块最关键的硬件:
- 它为什么能自己把任务一轮一轮推进下去;
- 它为什么拥有写代码、跑命令的能力,却不至于一路失控。
这正是第五章和第六章要解决的问题。
一、queryLoop() 的真正价值:它不是多轮问答,而是状态机
Claude Code 的核心逻辑都收敛在 queryLoop() 里。
它本质上是一个持续更新状态的 while (true):
while (true) {
let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]
const { compactionResult } = await deps.autocompact(...)
for await (const message of deps.callModel({...})) {
yield message
if (message.type === 'assistant' && 含有 tool_use) {
streamingToolExecutor.addTool(toolBlock, message)
}
}
for await (const update of toolUpdates) {
yield update.message
}
if (!needsFollowUp) return { reason: 'completed' }
state = { messages: [..., ...toolResults], ... }
}
关键点不在语法,而在顺序:
- 准备消息;
- 预处理压缩;
- 调模型;
- 发现工具调用;
- 执行工具;
- 把结果回写;
- 再进下一轮。
这就是 Agent 和聊天产品的本质分界线。
二、Claude Code 不是“先生成完,再执行工具”,而是流式并发
很多实现会这么做:
- 等模型整段回复结束;
- 解析出工具调用;
- 再开始执行工具。
Claude Code 不是。
它在流式输出过程中,一旦看到 tool_use 块,就立刻把工具加入执行队列:
for await (const message of deps.callModel({...})) {
yield message
if (message.type === 'assistant') {
const toolUseBlocks = message.message.content.filter(c => c.type === 'tool_use')
if (toolUseBlocks.length > 0) {
needsFollowUp = true
for (const toolBlock of toolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}
}
}
这意味着:工具执行和模型输出是并行的。
sequenceDiagram
participant Q as queryLoop
participant M as Claude API
participant S as StreamingToolExecutor
participant T as Tool Layer
Q->>M: callModel()
M-->>Q: text_delta
M-->>Q: tool_use
Q->>S: addTool()
S->>T: 启动工具
M-->>Q: 继续输出 text_delta
T-->>S: 返回结果
Q-->>Q: 收集 tool_result
这不是体验优化,而是架构选择。
只要工具启动足够早,整轮任务延迟就会显著收缩。
三、StreamingToolExecutor 最核心的事,是并发安全分层
Claude Code 在工具调度上很克制。
它不会因为工具多就一股脑并发,而是先问每个工具:
isConcurrencySafe(input): boolean
调度规则很简单:
- 如果当前没有正在执行的工具,可以启动新工具;
- 如果当前执行中的工具全是并发安全的,而且新工具也是并发安全的,也能启动;
- 只要涉及非并发安全工具,必须串行。
对应的判断大概是这样:
private canExecuteTool(isConcurrencySafe: boolean): boolean {
const executingTools = this.tools.filter(t => t.status === 'executing')
return (
executingTools.length === 0 ||
(isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
)
}
这背后的逻辑很明确:
- 搜索、读取、列目录,这类工具适合并发;
- 写文件、跑副作用命令、改仓库,这类工具必须保守。
所以 Claude Code 的并发不是“追求越多越好”,而是“在可证明安全的范围内并发”。
四、终止条件不是一个 break,而是一组明确的 Terminal reason
成熟系统不会只关心“成功结束”,还要区分“为什么结束”。
Claude Code 的 queryLoop() 至少有这些终止原因:
completedaborted_streamingprompt_too_longmodel_errorblocking_limitstop_hook_prevented
这非常重要,因为它让上层能知道:
- 是模型自然结束;
- 是用户中断;
- 是上下文太长;
- 还是被 Hook 主动拦下来了。
没有这层终止语义,系统就很难做可靠恢复。
五、权限系统真正的第一原则:不是“能不能”,而是 allow / deny / ask
Claude Code 的权限系统没有直接把工具分成允许和禁止,而是设计成三态:
export type PermissionBehavior = 'allow' | 'deny' | 'ask'
这一步非常关键,因为:
allow代表系统自动放行;deny代表系统自动拒绝;ask代表系统把决策权交给更高一层。
只要有了 ask,Agent 才能在自动执行和人工确认之间切换。
这正是生产环境里最需要的弹性。
六、权限模式不是开关,而是五档速度
Claude Code 有多种权限模式:
| 模式 | 含义 |
|---|---|
default | 常规模式 |
acceptEdits | 自动接受文件编辑 |
plan | 只读规划模式 |
bypassPermissions | 绕过权限检查 |
dontAsk | 不弹确认,ask 直接转 deny |
这个设计非常实际。
同一个 Agent 在不同环境里,本来就应该有不同速度:
- 本地探索可以快一点;
- 关键仓库应该慢一点;
- 非交互环境不能依赖弹窗。
所以权限系统不是 yes/no,而是速度控制器。
七、useCanUseTool 把权限判断收口成了一条总闸门
Claude Code 每次工具调用都会经过 useCanUseTool:
export type CanUseToolFn = (
tool: ToolType,
input: Input,
toolUseContext: ToolUseContext,
assistantMessage: AssistantMessage,
toolUseID: string,
forceDecision?: PermissionDecision,
) => Promise<PermissionDecision>
它做的事情很像总闸门:
- 已中止就直接短路;
- 有
forceDecision就直接采用; - 否则进入正式权限检查;
- 对结果分成
allow / deny / ask。
flowchart TD
A["工具请求"] --> B{"已中止?"}
B --> |"是"| X["直接返回"]
B --> |"否"| C{"forceDecision?"}
C --> |"是"| Y["直接采用"]
C --> |"否"| D["hasPermissionsToUseTool()"]
D --> E{"allow / deny / ask"}
E --> |"allow"| F["执行"]
E --> |"deny"| G["拒绝"]
E --> |"ask"| H["进入确认流程"]
所有工具都走同一条总闸门,意味着权限不会分散在各个工具里各写各的逻辑。
八、真正复杂的是 ask 分支:谁来做最终授权?
ask 并不等于“弹一个确认框”。
Claude Code 的做法是把多个授权通道同时发起,然后谁先给答案谁赢:
- 本地 UI;
- PermissionRequest Hook;
- Bash AI 分类器;
- Bridge 远程响应;
- 其他渠道。
flowchart TD
ASK["ask"] --> R["createResolveOnce()"]
R --> UI["本地 UI"]
R --> HOOK["PermissionRequest Hook"]
R --> AI["AI 分类器"]
R --> BRIDGE["远程响应"]
UI --> F["首个结果生效"]
HOOK --> F
AI --> F
BRIDGE --> F
这说明 Claude Code 的授权模型不是“围绕终端 UI 建的”,而是“围绕决策链建的”。
只要结果能合法返回,它不在乎是谁给的答案。
九、真正的规则引擎在 hasPermissionsToUseTool()
权限判断的优先级大致是:
PermissionRequest Hooks- 工具自己的
checkPermissions - 路径安全检查
- 全局权限模式过滤
dontAsk把ask转成deny
这是一条很像中间件链的结构。
它把权限拆成三层:
- 外层策略;
- 工具本地规则;
- 环境总规则。
所以 Claude Code 的安全,不是靠某个弹窗撑起来的,而是靠整条决策链层层收口。
十、Hooks 不只是给权限用,它们是系统的可编程神经系统
Claude Code 支持大量 Hook 事件:
PreToolUsePostToolUsePermissionRequestSessionStartSessionEndPreCompactPostCompactSubagentStartTaskCompleted
这意味着:
- 工具调用前你能插逻辑;
- 工具调用后你能补上下文;
- 会话开始时能做环境准备;
- 压缩前后也能被感知。
换句话说,Hooks 让 Claude Code 不只是一个封闭程序,而是一个可编排系统。
十一、第五章和第六章合起来,其实就是“行动力”和“刹车系统”
第五章讲的是:Agent 为什么真的能自己跑起来。
第六章讲的是:它为什么在有行动力之后还不至于出事故。
这两章必须一起看。因为:
- 没有循环,Agent 只是聊天;
- 没有权限,Agent 只是炸弹。
最后
Claude Code 在这两章最值得学的,不是“写了一个 while(true)”,也不是“加了几层确认框”。
真正值钱的是它把这两件事都做成了可维护、可恢复、可扩展的系统结构:
- 循环负责推进任务;
- 并发规则保证执行有序;
- Terminal reason 让系统知道为什么停;
- 三态权限让自动化和人工确认能共存;
- Hooks 让整套系统具备可编排性。
一个能写代码的 Agent 到这里,才算真正像样。