docs: 文档更新
This commit is contained in:
parent
7d5271e63e
commit
8b63e54e94
@ -1,70 +1,194 @@
|
||||
---
|
||||
title: "子 Agent 机制 - AI 分身术与任务委派"
|
||||
description: "深入解析 Claude Code 子 Agent 机制:主 Agent 如何通过 AgentTool 委派子任务,子 Agent 的生命周期管理、工具继承和结果回传。"
|
||||
keywords: ["子 Agent", "Agent 分身", "任务委派", "AgentTool", "多 Agent"]
|
||||
title: "子 Agent 机制 - AgentTool 的执行链路与隔离架构"
|
||||
description: "从源码角度解析 Claude Code 子 Agent:AgentTool.call() 的完整执行链路、Fork 子进程的 Prompt Cache 共享、Worktree 隔离、工具池独立组装、以及结果回传的数据格式。"
|
||||
keywords: ["子 Agent", "AgentTool", "任务委派", "forkSubagent", "子进程隔离"]
|
||||
---
|
||||
|
||||
{/* 本章目标:解释子 Agent 机制的设计和应用场景 */}
|
||||
{/* 本章目标:从源码角度揭示子 Agent 的完整执行链路、工具隔离、通信协议和生命周期管理 */}
|
||||
|
||||
## 为什么需要子 Agent
|
||||
## 执行链路总览
|
||||
|
||||
有些任务太大,一个 AI 实例忙不过来:
|
||||
一条 `Agent(prompt="修复 bug")` 调用的完整路径:
|
||||
|
||||
- "在 5 个不同的文件中分别找到并修复同类 bug"
|
||||
- "一边重构后端 API,一边更新前端调用"
|
||||
- "研究这个库的用法,同时修改我们的代码"
|
||||
```
|
||||
AI 生成 tool_use: { prompt: "修复 bug", subagent_type: "Explore" }
|
||||
↓
|
||||
AgentTool.call() ← 入口(AgentTool.tsx:239)
|
||||
├── 解析 effectiveType(fork vs 命名 agent)
|
||||
├── filterDeniedAgents() ← 权限过滤
|
||||
├── 检查 requiredMcpServers ← MCP 依赖验证(最长等 30s)
|
||||
├── assembleToolPool(workerPermissionContext) ← 独立组装工具池
|
||||
├── createAgentWorktree() ← 可选 worktree 隔离
|
||||
↓
|
||||
runAgent() ← 核心执行(runAgent.ts:248)
|
||||
├── getAgentSystemPrompt() ← 构建 agent 专属 system prompt
|
||||
├── initializeAgentMcpServers() ← agent 级 MCP 服务器
|
||||
├── executeSubagentStartHooks() ← Hook 注入
|
||||
├── query() ← 进入标准 agentic loop
|
||||
│ ├── 消息流逐条 yield
|
||||
│ └── recordSidechainTranscript() ← JSONL 持久化
|
||||
↓
|
||||
finalizeAgentTool() ← 结果汇总
|
||||
├── 提取文本内容 + usage 统计
|
||||
└── mapToolResultToToolResultBlockParam() ← 格式化为 tool_result
|
||||
```
|
||||
|
||||
## 分身术的运作方式
|
||||
## 两种子 Agent 路径:命名 Agent vs Fork
|
||||
|
||||
Claude Code 中的 Agent 工具让 AI 能够**启动另一个 AI 实例**来处理子任务:
|
||||
`AgentTool.call()` 根据是否提供 `subagent_type` 走两条完全不同的路径(`AgentTool.tsx:322-356`):
|
||||
|
||||
<Steps>
|
||||
<Step title="主 Agent 分析任务">
|
||||
主 Agent 判断任务可以被拆解为独立的子任务
|
||||
</Step>
|
||||
<Step title="启动子 Agent">
|
||||
通过 Agent 工具创建一个或多个子 Agent,每个子 Agent 收到一个清晰的子任务描述
|
||||
</Step>
|
||||
<Step title="并行执行">
|
||||
多个子 Agent 可以同时工作,互不干扰
|
||||
</Step>
|
||||
<Step title="结果汇总">
|
||||
子 Agent 完成后,结果返回给主 Agent,主 Agent 汇总并呈现给用户
|
||||
</Step>
|
||||
</Steps>
|
||||
| 维度 | 命名 Agent(`subagent_type` 指定) | Fork 子进程(`subagent_type` 省略) |
|
||||
|------|-------------------------------------|--------------------------------------|
|
||||
| **触发条件** | `subagent_type` 有值 | `isForkSubagentEnabled()` && 未指定类型 |
|
||||
| **System Prompt** | Agent 自身的 `getSystemPrompt()` | 继承父 Agent 的完整 System Prompt |
|
||||
| **工具池** | `assembleToolPool()` 独立组装 | 父 Agent 的原始工具池(`useExactTools: true`) |
|
||||
| **上下文** | 仅任务描述 | 父 Agent 的完整对话历史(`forkContextMessages`) |
|
||||
| **模型** | 可独立指定 | 继承父模型(`model: 'inherit'`) |
|
||||
| **权限模式** | Agent 定义的 `permissionMode` | `'bubble'`(上浮到父终端) |
|
||||
| **目的** | 专业任务委派 | Prompt Cache 命中率优化 |
|
||||
|
||||
## 子 Agent 的边界
|
||||
Fork 路径的设计核心是 **Prompt Cache 共享**:所有 fork 子进程共享父 Agent 的完整 `assistant` 消息(所有 `tool_use` 块),用相同的占位符 `tool_result` 填充,只有最后一个 `text` 块包含各自的指令。这使得 API 请求前缀字节完全一致,最大化缓存命中。
|
||||
|
||||
子 Agent 不是和主 Agent 完全一样的——它有明确的能力边界:
|
||||
```typescript
|
||||
// forkSubagent.ts:142 — 所有 fork 子进程的占位结果
|
||||
const FORK_PLACEHOLDER_RESULT = 'Fork started — processing in background'
|
||||
|
||||
| 特性 | 主 Agent | 子 Agent |
|
||||
|------|---------|---------|
|
||||
| 可用工具 | 全部工具 | 受限子集(不能再启动子 Agent 等) |
|
||||
| 上下文 | 完整的会话历史 | 只有主 Agent 给的任务描述 |
|
||||
| 权限 | 用户设定 | 继承主 Agent 的权限,或更严格 |
|
||||
| 状态 | 可修改全局状态 | 隔离的状态空间 |
|
||||
// buildForkedMessages() 构建:
|
||||
// [assistant(全量 tool_use), user(placeholder_results..., 子进程指令)]
|
||||
```
|
||||
|
||||
## 通信方式
|
||||
### Fork 递归防护
|
||||
|
||||
主 Agent 和子 Agent 之间通过**消息邮箱**通信:
|
||||
Fork 子进程保留 Agent 工具(为了 cache-identical tool defs),但通过两道防线防止递归 fork(`AgentTool.tsx:332`):
|
||||
|
||||
- 主 Agent 通过 `Agent` 工具启动子 Agent
|
||||
- 子 Agent 通过 `SendMessage` 工具向主 Agent 报告进度
|
||||
- 这种松耦合的通信方式让 Agent 可以异步协作
|
||||
1. **`querySource` 检查**(压缩安全):`context.options.querySource === 'agent:builtin:fork'`
|
||||
2. **消息扫描**(降级兜底):检测 `<fork-boilerplate>` 标签
|
||||
|
||||
## 工具池的独立组装
|
||||
|
||||
子 Agent 不继承父 Agent 的工具限制——它的工具池完全独立组装(`AgentTool.tsx:573-577`):
|
||||
|
||||
```typescript
|
||||
const workerPermissionContext = {
|
||||
...appState.toolPermissionContext,
|
||||
mode: selectedAgent.permissionMode ?? 'acceptEdits'
|
||||
}
|
||||
const workerTools = assembleToolPool(workerPermissionContext, appState.mcp.tools)
|
||||
```
|
||||
|
||||
关键设计决策:
|
||||
- **权限模式独立**:子 Agent 使用 `selectedAgent.permissionMode`(默认 `acceptEdits`),不受父 Agent 当前模式的限制
|
||||
- **MCP 工具继承**:`appState.mcp.tools` 包含所有已连接的 MCP 工具,子 Agent 自动获得
|
||||
- **Agent 级 MCP 服务器**:`runAgent()` 中的 `initializeAgentMcpServers()` 可以为特定 Agent 额外连接专属 MCP 服务器
|
||||
|
||||
### 工具过滤的 resolveAgentTools
|
||||
|
||||
`runAgent.ts:500-502` 在工具组装后进一步过滤:
|
||||
|
||||
```typescript
|
||||
const resolvedTools = useExactTools
|
||||
? availableTools // Fork: 直接使用父工具
|
||||
: resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools
|
||||
```
|
||||
|
||||
`resolveAgentTools()` 会根据 Agent 定义中的 `tools` 字段过滤可用工具,将 `['*']` 映射为全量工具。
|
||||
|
||||
## Worktree 隔离机制
|
||||
|
||||
`isolation: "worktree"` 参数让子 Agent 在独立的 git worktree 中工作(`AgentTool.tsx:590-593`):
|
||||
|
||||
```typescript
|
||||
const slug = `agent-${earlyAgentId.slice(0, 8)}`
|
||||
worktreeInfo = await createAgentWorktree(slug)
|
||||
```
|
||||
|
||||
Worktree 生命周期:
|
||||
1. **创建**:在 `.git/worktrees/` 下创建独立工作副本
|
||||
2. **CWD 覆盖**:`runWithCwdOverride(worktreePath, fn)` 让所有文件操作在 worktree 中执行
|
||||
3. **路径翻译**:Fork + worktree 时注入路径翻译通知(`buildWorktreeNotice`)
|
||||
4. **清理**(`cleanupWorktreeIfNeeded`):
|
||||
- Hook-based worktree → 始终保留
|
||||
- 有变更 → 保留,返回 `worktreePath`
|
||||
- 无变更 → 自动删除
|
||||
|
||||
## 生命周期管理:同步 vs 异步
|
||||
|
||||
### 异步 Agent(后台运行)
|
||||
|
||||
当 `run_in_background=true` 或 `selectedAgent.background=true` 时,Agent 立即返回 `async_launched` 状态(`AgentTool.tsx:686-764`):
|
||||
|
||||
```
|
||||
registerAsyncAgent(agentId, ...) ← 注册到 AppState.tasks
|
||||
↓ (void — 火后不管)
|
||||
runAsyncAgentLifecycle() ← 后台执行
|
||||
├── runAgent().onCacheSafeParams ← 进度摘要初始化
|
||||
├── 消息流迭代
|
||||
├── completeAsyncAgent() ← 标记完成
|
||||
├── classifyHandoffIfNeeded() ← 安全检查
|
||||
└── enqueueAgentNotification() ← 通知主 Agent
|
||||
```
|
||||
|
||||
异步 Agent 获得独立的 `AbortController`,不与父 Agent 共享——用户按 ESC 取消主线程不会杀掉后台 Agent。
|
||||
|
||||
### 同步 Agent(前台运行)
|
||||
|
||||
同步 Agent 的关键特性是 **可后台化**(`AgentTool.tsx:818-833`):
|
||||
|
||||
```typescript
|
||||
const registration = registerAgentForeground({
|
||||
autoBackgroundMs: getAutoBackgroundMs() || undefined // 默认 120s
|
||||
})
|
||||
backgroundPromise = registration.backgroundSignal.then(...)
|
||||
```
|
||||
|
||||
在 agentic loop 的每次迭代中,系统用 `Promise.race` 竞争下一条消息和后台化信号:
|
||||
|
||||
```typescript
|
||||
const raceResult = await Promise.race([
|
||||
nextMessagePromise.then(r => ({ type: 'message', result: r })),
|
||||
backgroundPromise // 超过 autoBackgroundMs 触发
|
||||
])
|
||||
```
|
||||
|
||||
后台化后,前台迭代器被终止(`agentIterator.return()`),新的 `runAgent()` 以 `isAsync: true` 重新启动,当前台的输出文件继续写入。
|
||||
|
||||
## 结果回传格式
|
||||
|
||||
`mapToolResultToToolResultBlockParam()` 根据状态返回不同格式(`AgentTool.tsx:1298-1375`):
|
||||
|
||||
| 状态 | 返回内容 |
|
||||
|------|---------|
|
||||
| `completed` | 内容 + `<usage>` 块(token/tool_calls/duration) |
|
||||
| `async_launched` | agentId + outputFile 路径 + 操作指引 |
|
||||
| `teammate_spawned` | agent_id + name + team_name |
|
||||
| `remote_launched` | taskId + sessionUrl + outputFile |
|
||||
|
||||
对于一次性内置 Agent(Explore、Plan),`<usage>` 块被省略——每周节省约 1-2 Gtok 的上下文窗口。
|
||||
|
||||
## MCP 依赖的等待机制
|
||||
|
||||
如果 Agent 声明了 `requiredMcpServers`,`call()` 会等待这些服务器连接完成(`AgentTool.tsx:371-410`):
|
||||
|
||||
```typescript
|
||||
const MAX_WAIT_MS = 30_000 // 最长等 30 秒
|
||||
const POLL_INTERVAL_MS = 500 // 每 500ms 轮询
|
||||
```
|
||||
|
||||
早期退出条件:任何必需服务器进入 `failed` 状态时立即停止等待。工具可用性通过 `mcp__` 前缀工具名解析(`mcp__serverName__toolName`)判断。
|
||||
|
||||
## 适用场景
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="并行研究" icon="magnifying-glass">
|
||||
多个子 Agent 同时搜索不同方向的信息
|
||||
多个 fork 子进程并行搜索不同方向,共享 Prompt Cache 前缀,只有指令不同
|
||||
</Card>
|
||||
<Card title="分治修改" icon="code-branch">
|
||||
把大规模修改拆分到多个子 Agent 并行执行
|
||||
</Card>
|
||||
<Card title="前后台配合" icon="layer-group">
|
||||
一个子 Agent 在后台运行测试,主 Agent 继续写代码
|
||||
<Card title="专业委派" icon="code-branch">
|
||||
使用命名 Agent(Explore/Plan/verification)执行专业任务,受限工具集 + 独立权限
|
||||
</Card>
|
||||
<Card title="隔离实验" icon="flask">
|
||||
在 worktree 中启动子 Agent 尝试一个方案,不影响主分支
|
||||
`isolation: "worktree"` 在独立工作副本中尝试方案,不影响主分支
|
||||
</Card>
|
||||
<Card title="后台构建" icon="layer-group">
|
||||
`run_in_background: true` 启动长时间构建/测试任务,主 Agent 继续工作
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
@ -1,73 +1,239 @@
|
||||
---
|
||||
title: "Hooks 生命周期钩子 - 自定义行为注入"
|
||||
description: "解析 Claude Code Hooks 系统:在 AI 工具调用的关键节点(PreToolUse、PostToolUse、Notification 等)插入自定义 shell 命令,实现行为定制。"
|
||||
keywords: ["Hooks", "生命周期钩子", "自定义 Hook", "行为注入", "PreToolUse"]
|
||||
title: "Hooks 生命周期钩子 - 执行引擎与拦截协议"
|
||||
description: "从源码角度解析 Claude Code Hooks 系统:22 种 Hook 事件、6 种 Hook 类型、同步/异步执行协议、JSON 输出 schema、if 条件匹配、以及 Hook 如何注入上下文和拦截工具调用。"
|
||||
keywords: ["Hooks", "生命周期钩子", "拦截器", "PreToolUse", "Hook 协议"]
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 Hooks 系统的设计和应用场景 */}
|
||||
{/* 本章目标:从源码角度揭示 Hook 的执行引擎、匹配机制、返回值协议和生命周期管理 */}
|
||||
|
||||
## 什么是 Hooks
|
||||
## 22 种 Hook 事件
|
||||
|
||||
Hooks 是用户定义的 shell 命令,在 Claude Code 生命周期的特定时刻自动执行。
|
||||
Claude Code 定义了 22 种 Hook 事件(`coreTypes.ts:25-53`),覆盖完整的 Agent 生命周期:
|
||||
|
||||
类比:React 的 `useEffect` 让你在组件渲染后执行自定义逻辑。Claude Code 的 Hooks 让你在 AI 的关键行为前后执行自定义脚本。
|
||||
| 阶段 | 事件 | 触发时机 | 匹配字段 |
|
||||
|------|------|---------|---------|
|
||||
| **会话** | `SessionStart` | 会话启动 | `source` |
|
||||
| | `SessionEnd` | 会话结束 | `reason` |
|
||||
| | `Setup` | 初始化完成 | `trigger` |
|
||||
| **用户交互** | `UserPromptSubmit` | 用户提交消息 | — |
|
||||
| | `Stop` | Agent 停止响应 | — |
|
||||
| | `StopFailure` | Agent 停止失败 | `error` |
|
||||
| **工具执行** | `PreToolUse` | 工具调用前 | `tool_name` |
|
||||
| | `PostToolUse` | 工具调用后(成功) | `tool_name` |
|
||||
| | `PostToolUseFailure` | 工具调用后(失败) | `tool_name` |
|
||||
| **权限** | `PermissionRequest` | 权限请求 | `tool_name` |
|
||||
| | `PermissionDenied` | 权限被拒 | `tool_name` |
|
||||
| **子 Agent** | `SubagentStart` | 子 Agent 启动 | `agent_type` |
|
||||
| | `SubagentStop` | 子 Agent 停止 | `agent_type` |
|
||||
| **压缩** | `PreCompact` | 上下文压缩前 | `trigger` |
|
||||
| | `PostCompact` | 上下文压缩后 | `trigger` |
|
||||
| **协作** | `TeammateIdle` | Teammate 空闲 | — |
|
||||
| | `TaskCreated` | 任务创建 | — |
|
||||
| | `TaskCompleted` | 任务完成 | — |
|
||||
| **MCP** | `Elicitation` | MCP 服务器请求用户输入 | `mcp_server_name` |
|
||||
| | `ElicitationResult` | Elicitation 结果返回 | `mcp_server_name` |
|
||||
| **环境** | `ConfigChange` | 配置变更 | `source` |
|
||||
| | `CwdChanged` | 工作目录变更 | — |
|
||||
| | `FileChanged` | 文件变更 | `file_path` |
|
||||
| | `InstructionsLoaded` | 指令加载 | `load_reason` |
|
||||
| | `WorktreeCreate` / `WorktreeRemove` | Worktree 操作 | — |
|
||||
|
||||
## 可用的 Hook 事件
|
||||
## 6 种 Hook 类型
|
||||
|
||||
| 事件 | 触发时机 | 典型用途 |
|
||||
Hooks 配置支持 6 种执行方式(`src/types/hooks.ts`):
|
||||
|
||||
| 类型 | 执行方式 | 适用场景 |
|
||||
|------|---------|---------|
|
||||
| **PreToolUse** | 工具调用前 | 拦截危险操作、自定义审批逻辑 |
|
||||
| **PostToolUse** | 工具调用后 | 记录日志、触发通知、自动格式化 |
|
||||
| **PreCompact** | 上下文压缩前 | 标记不可丢失的信息 |
|
||||
| **PostCompact** | 上下文压缩后 | 验证关键信息是否保留 |
|
||||
| **Notification** | AI 发出通知时 | 自定义通知渠道(Slack、邮件等) |
|
||||
| **StopFailure** | AI 循环异常停止时 | 自定义错误处理 |
|
||||
| `command` | Shell 命令(bash/PowerShell) | 通用脚本、CI 检查 |
|
||||
| `prompt` | 注入到 AI 上下文 | 代码规范提醒 |
|
||||
| `agent` | 启动子 Agent 执行 | 复杂分析任务 |
|
||||
| `http` | HTTP 请求 | 远程服务、Webhook |
|
||||
| `callback` | 内部 JS 函数 | 系统内置 Hook |
|
||||
| `function` | 运行时注册的函数 Hook | Agent/Skill 内部使用 |
|
||||
|
||||
## Hook 的能力
|
||||
## 执行引擎:execCommandHook
|
||||
|
||||
Hook 脚本不仅能"观察",还能"干预":
|
||||
`execCommandHook()`(`src/utils/hooks.ts:829-1417`)是命令型 Hook 的执行核心:
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="拦截操作" icon="hand">
|
||||
返回特定信号可以阻止工具调用执行
|
||||
</Card>
|
||||
<Card title="修改行为" icon="pen">
|
||||
返回结构化的 JSON 输出,影响 Claude Code 的后续行为
|
||||
</Card>
|
||||
<Card title="注入上下文" icon="syringe">
|
||||
向 AI 的对话中注入额外信息
|
||||
</Card>
|
||||
<Card title="触发外部流程" icon="bolt">
|
||||
调用 CI/CD、发送通知、更新 Issue tracker
|
||||
</Card>
|
||||
</CardGroup>
|
||||
```
|
||||
execCommandHook(hook, hookEvent, hookName, jsonInput, signal)
|
||||
├── Shell 选择: hook.shell ?? DEFAULT_HOOK_SHELL
|
||||
│ ├── bash: spawn(cmd, [], { shell: gitBashPath | true })
|
||||
│ └── powershell: spawn(pwsh, ['-NoProfile', '-NonInteractive', '-Command', cmd])
|
||||
├── 变量替换
|
||||
│ ├── ${CLAUDE_PLUGIN_ROOT} → pluginRoot 路径
|
||||
│ ├── ${CLAUDE_PLUGIN_DATA} → plugin 数据目录
|
||||
│ └── ${user_config.X} → 用户配置值
|
||||
├── 环境变量注入
|
||||
│ ├── CLAUDE_PROJECT_DIR
|
||||
│ ├── CLAUDE_ENV_FILE(SessionStart/Setup/CwdChanged/FileChanged)
|
||||
│ └── CLAUDE_PLUGIN_OPTION_*(plugin options)
|
||||
├── stdin 写入: jsonInput + '\n'
|
||||
├── 超时: hook.timeout * 1000 ?? 600000ms(10分钟)
|
||||
└── 异步检测: 检查 stdout 首行是否为 {"async":true}
|
||||
```
|
||||
|
||||
## 配置方式
|
||||
### 异步 Hook 的检测协议
|
||||
|
||||
Hooks 在 `settings.json` 中配置:
|
||||
Hook 进程的 stdout 第一行如果是 `{"async":true}`,系统将其转为后台任务(`hooks.ts:1199-1246`):
|
||||
|
||||
```typescript
|
||||
const firstLine = firstLineOf(stdout).trim()
|
||||
if (isAsyncHookJSONOutput(parsed)) {
|
||||
executeInBackground({
|
||||
processId: `async_hook_${child.pid}`,
|
||||
asyncResponse: parsed,
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
后台 Hook 通过 `registerPendingAsyncHook()` 注册到 `AsyncHookRegistry`,完成后通过 `enqueuePendingNotification()` 通知主线程。
|
||||
|
||||
### asyncRewake:Hook 唤醒模型
|
||||
|
||||
`asyncRewake` 模式的 Hook 绕过 `AsyncHookRegistry`。当 Hook 退出码为 2 时,通过 `enqueuePendingNotification()` 以 `task-notification` 模式注入消息,唤醒空闲的模型(通过 `useQueueProcessor`)或在忙碌时注入 `queued_command` 附件。
|
||||
|
||||
## Hook 输出的 JSON Schema
|
||||
|
||||
同步 Hook 的输出遵循严格的 Zod schema(`src/types/hooks.ts:49-567`):
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": { "tool_name": "Write" },
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "npx prettier --write $CLAUDE_FILE_PATH"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"continue": false, // 是否继续执行
|
||||
"suppressOutput": true, // 隐藏 stdout
|
||||
"stopReason": "安全检查失败", // continue=false 时的原因
|
||||
"decision": "approve" | "block", // 全局决策
|
||||
"reason": "原因说明", // 决策原因
|
||||
"systemMessage": "警告内容", // 注入到上下文的系统消息
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "allow" | "deny" | "ask",
|
||||
"permissionDecisionReason": "匹配了安全规则",
|
||||
"updatedInput": { ... }, // 修改后的工具输入
|
||||
"additionalContext": "额外上下文" // 注入到对话
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这个例子:每当 AI 写入一个文件后,自动用 Prettier 格式化。
|
||||
### 各事件的 hookSpecificOutput
|
||||
|
||||
## 安全控制
|
||||
| 事件 | 专有字段 | 作用 |
|
||||
|------|---------|------|
|
||||
| `PreToolUse` | `permissionDecision`, `updatedInput`, `additionalContext` | 拦截/修改工具输入 |
|
||||
| `UserPromptSubmit` | `additionalContext` | 注入额外上下文 |
|
||||
| `PostToolUse` | `additionalContext`, `updatedMCPToolOutput` | 修改 MCP 工具输出 |
|
||||
| `SessionStart` | `initialUserMessage`, `watchPaths` | 设置初始消息和文件监控 |
|
||||
| `PermissionDenied` | `retry` | 指示是否重试 |
|
||||
| `Elicitation` | `action`, `content` | 控制用户输入对话框 |
|
||||
|
||||
- 托管设置(企业管理员)的 Hooks 优先级最高,用户不能覆盖
|
||||
- Hook 执行有超时限制
|
||||
- Hook 的输出会被解析和验证,防止注入攻击
|
||||
## Hook 匹配机制:getMatchingHooks
|
||||
|
||||
`getMatchingHooks()`(`hooks.ts:1685-1956`)负责从所有来源中查找匹配的 Hook:
|
||||
|
||||
### 多来源合并
|
||||
|
||||
```
|
||||
getHooksConfig()
|
||||
├── getHooksConfigFromSnapshot() ← settings.json 中的 Hook(user/project/local)
|
||||
├── getRegisteredHooks() ← SDK 注册的 callback Hook
|
||||
├── getSessionHooks() ← Agent/Skill 前置注册的 session Hook
|
||||
└── getSessionFunctionHooks() ← 运行时 function Hook
|
||||
```
|
||||
|
||||
### 匹配规则
|
||||
|
||||
`matcher` 字段支持三种模式(`matchesPattern()`, `hooks.ts:1428-1463`):
|
||||
|
||||
```
|
||||
"Write" → 精确匹配
|
||||
"Write|Edit" → 管道分隔的多值匹配
|
||||
"^Bash(git.*)" → 正则匹配
|
||||
"*" 或 "" → 通配(匹配所有)
|
||||
```
|
||||
|
||||
### if 条件过滤
|
||||
|
||||
Hook 可以指定 `if` 条件,只在特定输入时触发。`prepareIfConditionMatcher()`(`hooks.ts:1472-1503`)预编译匹配器:
|
||||
|
||||
```json
|
||||
{
|
||||
"hooks": [{
|
||||
"command": "check-git-branch.sh",
|
||||
"if": "Bash(git push*)"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`if` 条件使用 `permissionRuleValueFromString` 解析,支持与权限规则相同的语法(工具名 + 参数模式)。Bash 工具还会使用 tree-sitter 进行 AST 级别的命令解析。
|
||||
|
||||
### Hook 去重
|
||||
|
||||
同一个 Hook 命令在不同配置层级(user/project/local)可能重复。系统按 `pluginRoot\0command` 做 Map 去重,保留**最后合并的层级**。
|
||||
|
||||
## 工作区信任检查
|
||||
|
||||
**所有 Hook 都要求工作区信任**(`shouldSkipHookDueToTrust()`, `hooks.ts:286-296`)。这是纵深防御措施——防止恶意仓库的 `.claude/settings.json` 在未信任的情况下执行任意命令。
|
||||
|
||||
```typescript
|
||||
// 交互模式下,所有 Hook 要求信任
|
||||
const hasTrust = checkHasTrustDialogAccepted()
|
||||
return !hasTrust
|
||||
```
|
||||
|
||||
SDK 非交互模式下信任是隐式的(`getIsNonInteractiveSession()` 为 true 时跳过检查)。
|
||||
|
||||
## 四种 Hook 能力的源码映射
|
||||
|
||||
### 1. 拦截操作(PreToolUse)
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "deny"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`processHookJSONOutput()` 将 `permissionDecision` 映射为 `result.permissionBehavior = 'deny'`,并设置 `blockingError`,阻止工具执行。
|
||||
|
||||
### 2. 修改行为(updatedInput / updatedMCPToolOutput)
|
||||
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"updatedInput": { "command": "npm test -- --bail" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`updatedInput` 替换原始工具输入;`updatedMCPToolOutput`(PostToolUse 事件)替换 MCP 工具的返回值——可用于过滤敏感数据。
|
||||
|
||||
### 3. 注入上下文(additionalContext / systemMessage)
|
||||
|
||||
- `additionalContext` → 通过 `createAttachmentMessage({ type: 'hook_additional_context' })` 注入为用户消息
|
||||
- `systemMessage` → 注入为系统警告,直接显示给用户
|
||||
|
||||
### 4. 控制流程(continue / stopReason)
|
||||
|
||||
```json
|
||||
{ "continue": false, "stopReason": "构建失败,停止执行" }
|
||||
```
|
||||
|
||||
`continue: false` 设置 `preventContinuation = true`,阻止 Agent 继续执行后续操作。
|
||||
|
||||
## Session Hook 的生命周期
|
||||
|
||||
Agent 和 Skill 的前置 Hook 通过 `registerFrontmatterHooks()` 注册(`runAgent.ts:567-575`),绑定到 agent 的 session ID。Agent 结束时通过 `clearSessionHooks()` 清理。
|
||||
|
||||
```typescript
|
||||
// runAgent.ts:567 — 注册 agent 的前置 Hook
|
||||
registerFrontmatterHooks(rootSetAppState, agentId, agentDefinition.hooks, ...)
|
||||
|
||||
// runAgent.ts:820 — finally 块清理
|
||||
clearSessionHooks(rootSetAppState, agentId)
|
||||
```
|
||||
|
||||
这确保 Agent A 的 Hook 不会泄漏到 Agent B 的执行中。
|
||||
|
||||
@ -1,68 +1,175 @@
|
||||
---
|
||||
title: "MCP 协议 - 开放的工具生态扩展"
|
||||
description: "深入解析 Claude Code 的 MCP(Model Context Protocol)集成:通过标准协议对接数据库、API 和自定义服务,突破内置工具的能力边界。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "外部集成", "API 对接"]
|
||||
title: "MCP 协议 - 连接管理、工具发现与执行链路"
|
||||
description: "从源码角度解析 Claude Code 的 MCP 集成:7 种传输层实现、connectToServer 的 memoize 缓存、工具发现的 LRU 策略、认证状态机、以及 MCP 工具如何进入权限检查链路。"
|
||||
keywords: ["MCP", "Model Context Protocol", "工具扩展", "MCP 客户端", "工具发现"]
|
||||
---
|
||||
|
||||
{/* 本章目标:解释 MCP 协议如何扩展 AI 的能力边界 */}
|
||||
{/* 本章目标:从源码角度揭示 MCP 客户端的连接管理、工具发现协议和执行链路 */}
|
||||
|
||||
## 内置工具的局限
|
||||
## 架构总览:从配置到可用工具
|
||||
|
||||
Claude Code 内置了 50+ 工具,覆盖了通用的软件开发需求。但每个团队都有特殊需求:
|
||||
```
|
||||
settings.json: { mcpServers: { "my-db": { command: "npx", args: [...] } } }
|
||||
↓
|
||||
getAllMcpConfigs() ← 合并 user/project/local 三级配置
|
||||
↓
|
||||
useManageMCPConnections() ← React Hook 管理连接生命周期
|
||||
↓
|
||||
connectToServer(name, config) ← memoize 缓存(lodash memoize)
|
||||
├── 创建 Transport(stdio/sse/http/...)
|
||||
├── new Client() ← @modelcontextprotocol/sdk
|
||||
├── client.connect(transport) ← 超时控制(MCP_TIMEOUT, 默认 30s)
|
||||
└── 返回 MCPServerConnection ← { connected | failed | needs-auth | pending }
|
||||
↓
|
||||
fetchToolsForClient(client) ← LRU(20) 缓存
|
||||
├── client.request({ method: 'tools/list' })
|
||||
└── 每个工具包装为 MCPTool ← 统一 Tool 接口
|
||||
↓
|
||||
assembleToolPool() ← 合并内置工具 + MCP 工具
|
||||
↓
|
||||
工具名格式: mcp__<serverName>__<toolName> ← buildMcpToolName()
|
||||
```
|
||||
|
||||
- 连接内部数据库查询数据
|
||||
- 调用公司内部 API
|
||||
- 操作特定的 DevOps 工具
|
||||
- 访问私有的知识库
|
||||
## 7 种传输层实现
|
||||
|
||||
不可能把所有人的需求都内置进去。
|
||||
`connectToServer()`(`client.ts:596-1643`)根据 `config.type` 分发到不同的 Transport 实现:
|
||||
|
||||
## MCP:一个标准的"插头"
|
||||
| 传输类型 | Transport 类 | 适用场景 | 认证方式 |
|
||||
|----------|-------------|---------|---------|
|
||||
| `stdio`(默认) | `StdioClientTransport` | 本地子进程 | 无 |
|
||||
| `sse` | `SSEClientTransport` | 远程 SSE 服务 | `ClaudeAuthProvider` + OAuth |
|
||||
| `http` | `StreamableHTTPClientTransport` | HTTP 流 | `ClaudeAuthProvider` + OAuth |
|
||||
| `sse-ide` | `SSEClientTransport` | IDE 集成 | lockfile token |
|
||||
| `ws-ide` | `WebSocketTransport` | IDE WebSocket | `X-Claude-Code-Ide-Authorization` |
|
||||
| `ws` | `WebSocketTransport` | WebSocket 服务 | session ingress token |
|
||||
| `claudeai-proxy` | `StreamableHTTPClientTransport` | claude.ai 代理 | OAuth bearer + 401 重试 |
|
||||
|
||||
**Model Context Protocol**(模型上下文协议)是 Anthropic 提出的开放标准,定义了 AI 与外部工具之间的通信方式。
|
||||
### stdio 传输的进程管理
|
||||
|
||||
<Frame caption="MCP 连接架构——AI 的 USB 接口">
|
||||
<img src="/docs/images/mcp-architecture.png" alt="MCP 连接架构图" />
|
||||
</Frame>
|
||||
stdio 类型的 MCP 服务器作为子进程运行,cleanup 时采用 **信号升级策略**(`client.ts:1431-1564`):
|
||||
|
||||
类比:USB 是电脑连接外设的标准接口。MCP 是 AI 连接外部能力的标准接口。
|
||||
```
|
||||
SIGINT (100ms) → SIGTERM (400ms) → SIGKILL
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
总清理时间上限 600ms,防止 MCP 服务器关闭阻塞 CLI 退出。
|
||||
|
||||
<Steps>
|
||||
<Step title="启动 MCP Server">
|
||||
开发者编写一个 MCP Server,暴露自定义工具(比如"查询数据库"、"发送 Slack 消息")
|
||||
</Step>
|
||||
<Step title="Claude Code 连接">
|
||||
在配置文件中声明要连接的 MCP Server
|
||||
</Step>
|
||||
<Step title="工具自动发现">
|
||||
连接后,MCP Server 提供的工具自动出现在 AI 的可用工具列表中
|
||||
</Step>
|
||||
<Step title="透明调用">
|
||||
AI 像使用内置工具一样使用 MCP 工具——无需知道底层实现
|
||||
</Step>
|
||||
</Steps>
|
||||
### 远程传输的认证状态机
|
||||
|
||||
## 三种连接方式
|
||||
SSE/HTTP 类型使用 `ClaudeAuthProvider` 实现 OAuth 认证流程。认证失败时进入 `needs-auth` 状态,并写入 15 分钟 TTL 的缓存文件(`mcp-needs-auth-cache.json`),避免重复弹出认证提示。
|
||||
|
||||
| 方式 | 适用场景 |
|
||||
|------|---------|
|
||||
| **stdio** | MCP Server 作为子进程运行,通过标准输入/输出通信。最简单 |
|
||||
| **SSE** | 通过 HTTP Server-Sent Events 通信。适合远程服务 |
|
||||
| **StreamableHTTP** | 基于 HTTP 流的双向通信。适合复杂的交互场景 |
|
||||
```
|
||||
连接尝试 → 401 Unauthorized
|
||||
↓
|
||||
handleRemoteAuthFailure()
|
||||
├── logEvent('tengu_mcp_server_needs_auth')
|
||||
├── setMcpAuthCacheEntry(name) ← 写入 15min TTL 缓存
|
||||
└── return { type: 'needs-auth' } ← UI 显示认证提示
|
||||
```
|
||||
|
||||
## 权限一视同仁
|
||||
## 连接缓存与重连机制
|
||||
|
||||
MCP 提供的工具和内置工具一样受权限系统管控:
|
||||
`connectToServer` 使用 lodash `memoize` 缓存连接对象,缓存 key 为 `${name}-${JSON.stringify(config)}`。
|
||||
|
||||
- 需要用户确认才能调用
|
||||
- 可以设置 allow/deny 规则
|
||||
- 支持沙箱限制
|
||||
### 缓存失效触发
|
||||
|
||||
这确保了第三方工具不会绕过安全边界。
|
||||
当连接关闭时(`client.onclose`),清除所有相关缓存(`client.ts:1376-1404`):
|
||||
|
||||
## 实际例子
|
||||
```typescript
|
||||
client.onclose = () => {
|
||||
const key = getServerCacheKey(name, serverRef)
|
||||
fetchToolsForClient.cache.delete(name) // 工具缓存
|
||||
fetchResourcesForClient.cache.delete(name) // 资源缓存
|
||||
fetchCommandsForClient.cache.delete(name) // 命令缓存
|
||||
connectToServer.cache.delete(key) // 连接缓存
|
||||
}
|
||||
```
|
||||
|
||||
### 连接降级检测
|
||||
|
||||
远程传输有 **连续错误计数器**(`client.ts:1229`):
|
||||
|
||||
```typescript
|
||||
let consecutiveConnectionErrors = 0
|
||||
const MAX_ERRORS_BEFORE_RECONNECT = 3
|
||||
```
|
||||
|
||||
遇到终端错误(ECONNRESET、ETIMEDOUT、EPIPE 等)连续 3 次后,主动关闭 transport 触发重连。对于 HTTP 传输,还检测 session 过期(404 + JSON-RPC code -32001)。
|
||||
|
||||
### 请求级超时保护
|
||||
|
||||
每个 HTTP 请求使用独立的 `setTimeout` 超时(`wrapFetchWithTimeout`,`client.ts:493`),而非共享 `AbortSignal.timeout()`。原因是 Bun 对 AbortSignal.timeout 的 GC 是惰性的——每个请求约 2.4KB 原生内存,即使请求毫秒级完成也要等 60s 才回收。
|
||||
|
||||
```typescript
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(c => c.abort(...), MCP_REQUEST_TIMEOUT_MS, controller)
|
||||
timer.unref?.() // 不阻止进程退出
|
||||
```
|
||||
|
||||
## 工具发现:从 MCP 到 Tool 接口
|
||||
|
||||
`fetchToolsForClient()`(`client.ts:1745-2000`)使用 `memoizeWithLRU` 缓存(上限 20),将 MCP 工具转换为 Claude Code 的统一 Tool 接口:
|
||||
|
||||
```typescript
|
||||
const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
|
||||
// 结果: "mcp__my-db__query"
|
||||
```
|
||||
|
||||
### 工具描述截断
|
||||
|
||||
MCP 工具描述上限 2048 字符(`MAX_MCP_DESCRIPTION_LENGTH`)。OpenAPI 生成的 MCP 服务器曾观察到 15-60KB 的描述文档。
|
||||
|
||||
### 工具能力标注
|
||||
|
||||
每个 MCP 工具根据 `tool.annotations` 自动标注:
|
||||
|
||||
| 注解 | 映射到 | 含义 |
|
||||
|------|--------|------|
|
||||
| `readOnlyHint` | `isReadOnly()` + `isConcurrencySafe()` | 只读,可并行 |
|
||||
| `destructiveHint` | `isDestructive()` | 破坏性操作 |
|
||||
| `openWorldHint` | `isOpenWorld()` | 开放世界(不可枚举) |
|
||||
| `title` | `userFacingName()` | 显示名称 |
|
||||
|
||||
### MCP 工具的权限检查
|
||||
|
||||
MCP 工具默认返回 `{ behavior: 'passthrough' }`(`client.ts:1816-1834`),意味着它们始终进入权限确认流程。工具名使用 `mcp__` 前缀精确匹配权限规则。
|
||||
|
||||
## MCP 工具的执行链路
|
||||
|
||||
```
|
||||
AI 生成 tool_use: { name: "mcp__my-db__query", input: { sql: "..." } }
|
||||
↓
|
||||
MCPTool.call() ← client.ts:1835
|
||||
├── ensureConnectedClient() ← 确保连接有效(重连)
|
||||
├── callMCPToolWithUrlElicitationRetry() ← 带 Elicitation 重试
|
||||
│ ├── client.request({ method: 'tools/call' })
|
||||
│ ├── 处理图片结果(resize + persist)
|
||||
│ └── 内容截断(mcpContentNeedsTruncation)
|
||||
├── McpSessionExpiredError → 重试一次
|
||||
└── 返回 { data: content, mcpMeta }
|
||||
```
|
||||
|
||||
### Session 过期自动重试
|
||||
|
||||
HTTP 传输的 MCP session 可能过期。检测到 `McpSessionExpiredError` 后自动重试一次(`client.ts:1862`),因为 `ensureConnectedClient()` 已经清除了缓存并建立了新连接。
|
||||
|
||||
### 内容截断与持久化
|
||||
|
||||
大型 MCP 工具输出通过 `truncateMcpContentIfNeeded` 截断,二进制内容(图片)通过 `persistBinaryContent` 写入文件并返回文件路径。图片自动 resize(`maybeResizeAndDownsampleImageBuffer`)。
|
||||
|
||||
## MCP 连接的并发控制
|
||||
|
||||
```typescript
|
||||
// 本地服务器并发连接数
|
||||
getMcpServerConnectionBatchSize() // 默认 3
|
||||
|
||||
// 远程服务器并发连接数
|
||||
getRemoteMcpServerConnectionBatchSize() // 默认 20
|
||||
```
|
||||
|
||||
本地 MCP 服务器(stdio)是重量级的子进程,默认限制 3 个并发连接。远程服务器是轻量级 HTTP 请求,允许 20 个并发。
|
||||
|
||||
## 实际配置示例
|
||||
|
||||
```json
|
||||
// settings.json 中的 MCP 配置
|
||||
@ -72,9 +179,13 @@ MCP 提供的工具和内置工具一样受权限系统管控:
|
||||
"command": "npx",
|
||||
"args": ["@my-org/db-mcp-server"],
|
||||
"env": { "DB_URL": "postgres://..." }
|
||||
},
|
||||
"remote-api": {
|
||||
"type": "http",
|
||||
"url": "https://api.example.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置后,AI 就多了"查询数据库"这个能力——用自然语言描述需求,AI 自动生成查询并执行。
|
||||
配置后,AI 的工具列表中会出现 `mcp__my-database__query` 和 `mcp__remote-api__*` 工具——与内置工具使用相同的权限检查链路和 UI 渲染。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user