docs: Add comprehensive agentic design documentation (2.5-hour learning session)
新增 docs/agentic-design/ 教育文档,全中文+英文技术术语,覆盖: - README.md: 总索引,整体架构图,阅读指南 - 00-codebase-tour.md: 代码库全景,递归覆盖所有重要目录 - 01-agent-loop.md: Query loop 状态机,token budget,compaction - 02-tool-system.md: Tool interface,权限检查,并发执行,sibling abort - 03-multi-agent-coordination.md: Agent 类型,coordinator 4 阶段,XML 通信 - 04-permission-system.md: Permission mode,rule system,YOLO 分类器,denial tracking - 05-context-and-memory.md: System prompt 分层,compaction,auto-dream 后台巩固 - 06-feature-gating.md: Compile-time feature,runtime flags,Bun dead-code elimination 总计 ~2.5 小时阅读,每篇含 Design Decision 专栏和常见误解纠正 https://claude.ai/code/session_017Vdqo9B8eTXiEDcqnv9DxM
This commit is contained in:
parent
0cf2fa2edb
commit
808d5a61b3
8 changed files with 2697 additions and 0 deletions
486
docs/agentic-design/00-codebase-tour.md
Normal file
486
docs/agentic-design/00-codebase-tour.md
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
# 代码库全景:从顶层到最深处
|
||||
|
||||
> 这篇文档是一个**递归的导游**。我们从顶层文件开始,逐层深入每个重要的目录,直到粒度太细而没有学习价值为止。每个章节都注明关键文件,你可以随时打开源代码对照。
|
||||
|
||||
---
|
||||
|
||||
## 顶层:核心文件
|
||||
|
||||
在 `/home/user/claude-code/` 的根目录下,以下文件是系统的脊梁:
|
||||
|
||||
| 文件 | 行数 | 用途 |
|
||||
|------|------|------|
|
||||
| `main.tsx` | ~4700 | CLI 入口,React + Ink 终端渲染器 |
|
||||
| `query.ts` | ~1700 | Query loop 状态机的完整实现 |
|
||||
| `QueryEngine.ts` | 核心 | Query 执行的编排引擎 |
|
||||
| `Tool.ts` | 核心 | Tool 的类型定义和接口规范 |
|
||||
| `tools.ts` | 核心 | Tool 注册与初始化 |
|
||||
| `context.ts` | 核心 | System prompt 和 context 的组装 |
|
||||
| `commands.ts` | ~700 | 所有 slash command 的中央路由 |
|
||||
|
||||
**学习路线**:`main.tsx` → `query.ts` → `Tool.ts` 可以了解 agent loop 的完整流程。
|
||||
|
||||
---
|
||||
|
||||
## `entrypoints/` — 入口层
|
||||
|
||||
系统支持多种启动方式:
|
||||
|
||||
| 子目录/文件 | 用途 |
|
||||
|-----------|------|
|
||||
| `cli.tsx` | 标准 CLI 入口,React 组件树的根,feature detection |
|
||||
| `sdk/` | Anthropic SDK 集成(给程序员用) |
|
||||
| `mcp.ts` | MCP (Model Context Protocol) 服务器 |
|
||||
| `init.ts` | 初始化逻辑,首次运行配置 |
|
||||
| `sandboxTypes.ts` | 沙箱相关的类型定义 |
|
||||
|
||||
**关键点**:`cli.tsx` 做 feature flag 检测,决定如何初始化全局状态。
|
||||
|
||||
---
|
||||
|
||||
## `query/` — Query Loop 配置和支撑
|
||||
|
||||
这个目录下的文件都是 `query.ts` 的辅助组件:
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `config.ts` | Query 配置构建器,参数组合 |
|
||||
| `tokenBudget.ts` | Token budget 追踪和强制执行 |
|
||||
| `stopHooks.ts` | Stop/interrupt 处理 |
|
||||
| `deps.ts` | Query 依赖注入 |
|
||||
|
||||
**最重要的**:`tokenBudget.ts` 控制着"超限时触发 compaction"的逻辑。
|
||||
|
||||
---
|
||||
|
||||
## `tools/` — 所有 Tool 实现(40+ 个)
|
||||
|
||||
这个目录有 40 多个子目录,每个代表一个 Tool。核心的几个:
|
||||
|
||||
### 必读:
|
||||
|
||||
| Tool | 文件 | 用途 |
|
||||
|------|------|------|
|
||||
| `AgentTool/` | 子 agent 的产卵和管理 | 核心,递归往下 |
|
||||
| `FileReadTool/` | 读文件 | 基础 |
|
||||
| `FileEditTool/` | 编辑文件 | 基础 |
|
||||
| `FileWriteTool/` | 写文件 | 基础 |
|
||||
| `BashTool/` | 执行 shell 命令 | 基础 |
|
||||
| `GlobTool/` | 文件匹配 | 基础 |
|
||||
| `GrepTool/` | 内容搜索 | 基础 |
|
||||
| `WebFetchTool/` | 获取网页 | 基础 |
|
||||
| `AskUserQuestionTool/` | 询问用户 | 权限边界 |
|
||||
| `TodoWriteTool/` | 写 todo list | 后台任务 |
|
||||
|
||||
### `AgentTool/` — 深递归
|
||||
|
||||
这是最复杂的 Tool,用来 spawn 子 agent:
|
||||
|
||||
```
|
||||
AgentTool/
|
||||
├── AgentTool.tsx [233 KB] 主逻辑,agent 生命周期
|
||||
├── runAgent.ts 执行 agent 的主函数
|
||||
├── forkSubagent.ts fork 一个新 agent
|
||||
├── resumeAgent.ts 恢复之前的 agent
|
||||
├── loadAgentsDir.ts 从 ~/.agents/ 加载 agent 定义
|
||||
├── built-in/ 内置 agent
|
||||
│ ├── generalPurposeAgent.ts
|
||||
│ ├── planAgent.ts
|
||||
│ ├── exploreAgent.ts
|
||||
│ └── ... (其他 5 个)
|
||||
├── agentMemory.ts Agent 记忆快照
|
||||
├── agentColorManager.ts Agent 的颜色分配(便于输出区分)
|
||||
└── ... (其他支撑文件)
|
||||
```
|
||||
|
||||
**关键概念**:Agent 可以是 local (in-process) 或 remote (CCR),使用 `AsyncLocalStorage` 隔离上下文。
|
||||
|
||||
---
|
||||
|
||||
## `services/` — 业务逻辑服务
|
||||
|
||||
这个目录是 Claude Code 的"心脏":
|
||||
|
||||
### `tools/` — Tool 执行编排
|
||||
|
||||
```
|
||||
services/tools/
|
||||
├── StreamingToolExecutor.ts [核心] 并发 tool 执行,结果缓冲
|
||||
├── toolOrchestration.ts Tool 调度和权限检查
|
||||
├── toolExecution.ts 单个 tool 调用的执行
|
||||
├── toolHooks.ts Tool 执行前/后的 hook
|
||||
└── ...
|
||||
```
|
||||
|
||||
**最重要**:`StreamingToolExecutor.ts` 实现了"并发执行但顺序输出结果"的逻辑。
|
||||
|
||||
### `autoDream/` — 自动记忆压缩
|
||||
|
||||
```
|
||||
services/autoDream/
|
||||
├── autoDream.ts [11 KB] Dream 流程:3 个触发门 + 4 个阶段
|
||||
├── consolidationPrompt.ts [核心] 告诉 Claude 怎么压缩 memory
|
||||
├── consolidationLock.ts 防并发的 lock 机制
|
||||
└── config.ts Dream 参数配置
|
||||
```
|
||||
|
||||
**关键点**:Dream 有三个触发条件门:24h 时间 + 5+ 个 session + 拿到 lock。
|
||||
|
||||
### 其他重要服务
|
||||
|
||||
| 目录 | 用途 |
|
||||
|------|------|
|
||||
| `api/` | Claude API 调用,token 计算,usage 追踪 |
|
||||
| `mcp/` | MCP 协议服务器的实现 |
|
||||
| `analytics/` | 事件上报、GrowthBook feature flags |
|
||||
| `plugins/` | 插件系统 |
|
||||
|
||||
---
|
||||
|
||||
## `utils/permissions/` — 权限系统(深递归)
|
||||
|
||||
这是整个 agent sandbox 的防线,共 20+ 个文件,总计 300+ KB:
|
||||
|
||||
```
|
||||
utils/permissions/
|
||||
├── permissions.ts [52 KB] 核心决策逻辑
|
||||
├── yoloClassifier.ts [52 KB] ML 自动审批分类器
|
||||
├── filesystem.ts [62 KB] 文件系统安全规则
|
||||
├── PermissionMode.ts Permission mode 枚举
|
||||
├── denialTracking.ts 追踪拒绝次数,circuit breaker
|
||||
├── pathValidation.ts 路径安全(Unicode normalization 等)
|
||||
├── permissionRuleParser.ts 解析 "Bash(git *)" 这样的规则
|
||||
├── classifierDecision.ts 分类器决策包装
|
||||
├── bashClassifier.ts Bash 命令的特殊规则
|
||||
├── permissionExplainer.ts 用 LLM 解释权限决定
|
||||
└── ... (其他辅助)
|
||||
```
|
||||
|
||||
**核心 pipeline**:mode → rules → YOLO classifier → prompt user
|
||||
|
||||
---
|
||||
|
||||
## `utils/` — 其他公用工具(部分列举)
|
||||
|
||||
```
|
||||
utils/
|
||||
├── permissions/ [见上面的深递归]
|
||||
├── messages/
|
||||
│ ├── mappers.ts SDK 消息格式转换
|
||||
│ ├── systemInit.ts 系统初始化消息构建
|
||||
│ └── ...
|
||||
├── settings/ 设置加载和应用
|
||||
├── queryHelpers.ts Query 辅助函数
|
||||
├── undercover.ts 隐藏内部代号(Capybara, Tengu 等)
|
||||
├── abortController.ts Abort signal 管理
|
||||
├── agentContext.ts Agent 上下文 getter
|
||||
├── api.ts API 工具函数
|
||||
├── hooks/ React hooks (不是本目录)
|
||||
├── ... (数十个其他)
|
||||
```
|
||||
|
||||
**学习重点**:`permissions/` 最复杂,其次是 `messages/` 和 `settings/`。
|
||||
|
||||
---
|
||||
|
||||
## `coordinator/` — 多 Agent 协调
|
||||
|
||||
```
|
||||
coordinator/
|
||||
└── coordinatorMode.ts [19 KB] 唯一的文件,包含完整 coordinator 系统提示
|
||||
|
||||
功能:启用 CLAUDE_CODE_COORDINATOR_MODE=1 时激活,引入 4 个阶段:
|
||||
research → synthesis → implementation → verification
|
||||
```
|
||||
|
||||
**关键点**:Worker 通过 `<task-notification>` XML 向 coordinator 报告进度。
|
||||
|
||||
---
|
||||
|
||||
## `tasks/` — 后台任务管理
|
||||
|
||||
```
|
||||
tasks/
|
||||
├── LocalAgentTask/ 本地 agent 任务追踪
|
||||
├── RemoteAgentTask/ 远程 agent 任务追踪(CCR)
|
||||
├── LocalMainSessionTask.ts 主 session 的任务
|
||||
├── LocalShellTask/ Shell 任务(tmux/iTerm2)
|
||||
├── InProcessTeammateTask/ In-process teammate 任务
|
||||
├── DreamTask/ Memory dream 任务
|
||||
├── stopTask.ts 停止任务的逻辑
|
||||
├── types.ts Task 的类型定义
|
||||
└── pillLabel.ts 任务标签(UI 展示)
|
||||
```
|
||||
|
||||
**核心概念**:每个后台任务都有生命周期追踪。
|
||||
|
||||
---
|
||||
|
||||
## `state/` — React 状态管理
|
||||
|
||||
```
|
||||
state/
|
||||
├── AppState.tsx React 状态类型定义
|
||||
├── AppStateStore.ts [21 KB] Zustand-like 状态存储
|
||||
├── onChangeAppState.ts 状态变化的 handler
|
||||
├── selectors.ts 状态选择器
|
||||
├── store.ts 存储初始化
|
||||
└── teammateViewHelpers.ts Teammate 视图辅助
|
||||
```
|
||||
|
||||
**模式**:这是 Claude Code 的全局状态树,UI 所有数据都来自这里。
|
||||
|
||||
---
|
||||
|
||||
## `hooks/` — 80+ 个 React Hook
|
||||
|
||||
重要的:
|
||||
|
||||
| Hook | 用途 |
|
||||
|------|------|
|
||||
| `useCanUseTool.tsx` | [40 KB] 权限检查 Hook(频繁调用) |
|
||||
| `useGlobalKeybindings.tsx` | [31 KB] 全局键盘快捷键 |
|
||||
| `useTypeahead.tsx` | [212 KB] 自动补全建议 |
|
||||
| `useVoice.ts` | [45 KB] 语音输入集成 |
|
||||
| `useInboxPoller.ts` | [34 KB] 后台轮询消息 |
|
||||
| `useArrowKeyHistory.tsx` | [34 KB] 历史导航(上/下箭头) |
|
||||
| `useReplBridge.ts` | [115 KB] REPL 集成 |
|
||||
|
||||
**学习策略**:跳过大部分 hook,只深入 `useCanUseTool` 和 `useTypeahead`。
|
||||
|
||||
---
|
||||
|
||||
## `bootstrap/state.ts` — 全局启动状态
|
||||
|
||||
```
|
||||
bootstrap/
|
||||
└── state.ts [56 KB]
|
||||
```
|
||||
|
||||
内容:
|
||||
- 当前 session ID 和持久化 flag
|
||||
- 用户类型(ant/public)
|
||||
- Feature flag 缓存
|
||||
- KAIROS mode 检测
|
||||
- 全局配置状态
|
||||
|
||||
**作用**:在 App 启动的最早时刻初始化所有全局状态。
|
||||
|
||||
---
|
||||
|
||||
## `cli/` — 终端输出和格式化
|
||||
|
||||
```
|
||||
cli/
|
||||
├── print.ts [212 KB] 终端渲染引擎
|
||||
├── structuredIO.ts NDJSON 和结构化输出
|
||||
├── handlers/ 命令特定的输出 handler
|
||||
├── transports/ 多种输出后端(stdout, HTTP 等)
|
||||
├── remoteIO.ts 远程 IO
|
||||
└── ... (其他)
|
||||
```
|
||||
|
||||
**关键**:`print.ts` 是整个 Claude Code 的"输出黑洞",所有文本最终都通过这里。
|
||||
|
||||
---
|
||||
|
||||
## `context/` — React Context Provider
|
||||
|
||||
```
|
||||
context/
|
||||
├── QueuedMessageContext.tsx 消息队列
|
||||
├── mailbox.tsx Agent 邮箱(inter-agent 消息)
|
||||
├── modalContext.tsx Modal 对话框
|
||||
├── notifications.tsx 通知系统
|
||||
├── overlayContext.tsx 浮层上下文
|
||||
├── promptOverlayContext.tsx 提示浮层
|
||||
├── stats.tsx 统计数据
|
||||
└── voice.tsx 语音上下文
|
||||
```
|
||||
|
||||
**作用**:提供 React component tree 范围内的全局上下文。
|
||||
|
||||
---
|
||||
|
||||
## `memdir/` — 用户 Memory 管理
|
||||
|
||||
```
|
||||
memdir/
|
||||
├── memdir.ts [21 KB] 核心 memory 目录管理
|
||||
├── memoryScan.ts 扫描 memory 文件
|
||||
├── memoryTypes.ts Memory 文件的类型
|
||||
├── memoryAge.ts Memory 的年龄计算
|
||||
├── paths.ts Memory 文件路径管理
|
||||
├── teamMemPaths.ts Team memory 路径(multi-agent)
|
||||
└── findRelevantMemories.ts 查找相关 memory
|
||||
```
|
||||
|
||||
**用途**:管理 `~/.claude/memory/` 目录,存储用户的持久化知识。
|
||||
|
||||
---
|
||||
|
||||
## `constant/` — 常量和系统提示
|
||||
|
||||
```
|
||||
constant/ (也被称为 constants/)
|
||||
├── system.ts Base system prompt
|
||||
├── systemPromptSections.ts System prompt 的可组装部分
|
||||
├── cyberRiskInstruction.ts 安全指导(Safeguards team 维护)
|
||||
├── betas.ts API beta features 列表
|
||||
├── messages.ts 常见消息文本
|
||||
├── prompts.ts 各种 prompt 模板
|
||||
└── ... (其他常量)
|
||||
```
|
||||
|
||||
**关键**:System prompt 是模块化的,不同 feature 会添加不同的部分。
|
||||
|
||||
---
|
||||
|
||||
## `bridge/` — 与 claude.ai 的 JWT 连接
|
||||
|
||||
```
|
||||
bridge/
|
||||
├── bridgeApi.ts 与 claude.ai 的通信
|
||||
├── bridgeMessaging.ts 消息协议
|
||||
├── remoteBridgeCore.ts 远程 bridge 核心
|
||||
├── createSession.ts 创建远程 session
|
||||
├── jwtUtils.ts JWT 令牌工具
|
||||
├── trustedDevice.ts 受信设备管理
|
||||
└── ... (10+ 个其他)
|
||||
```
|
||||
|
||||
**用途**:BRIDGE_MODE 时,CLI 可以通过 claude.ai 的 WebUI 远程控制。
|
||||
|
||||
---
|
||||
|
||||
## `remote/` — 远程 Session 管理
|
||||
|
||||
```
|
||||
remote/
|
||||
├── RemoteSessionManager.ts 远程 session 生命周期
|
||||
├── SessionsWebSocket.ts WebSocket 连接管理
|
||||
├── remotePermissionBridge.ts 权限同步
|
||||
├── sdkMessageAdapter.ts SDK 消息适配
|
||||
└── ... (其他)
|
||||
```
|
||||
|
||||
**用途**:支持多个远程客户端同时连接到同一个服务。
|
||||
|
||||
---
|
||||
|
||||
## `entrypoints/sdk/` — SDK 集成
|
||||
|
||||
```
|
||||
entrypoints/sdk/
|
||||
├── agentSdkTypes.ts 给外部程序使用的类型
|
||||
└── (其他 SDK 特定代码)
|
||||
```
|
||||
|
||||
**用途**:让程序员可以用代码调用 Claude Code,而不只是 CLI。
|
||||
|
||||
---
|
||||
|
||||
## `components/` — React UI 组件
|
||||
|
||||
```
|
||||
components/
|
||||
├── App.tsx 顶层 app 组件
|
||||
├── AgentProgressLine.tsx Agent 进度显示
|
||||
├── BaseTextInput.tsx 文本输入基础
|
||||
├── ... (100+ 个其他组件)
|
||||
```
|
||||
|
||||
**特点**:使用 Ink 库在终端中渲染 React 组件。
|
||||
|
||||
---
|
||||
|
||||
## `commands/` — Slash 命令(88 个)
|
||||
|
||||
```
|
||||
commands/
|
||||
├── commit.ts / commit-push-pr.ts Git 相关命令
|
||||
├── agent.ts Agent 管理命令
|
||||
├── task.ts, tasks.ts 任务管理
|
||||
├── config.ts 配置命令
|
||||
├── memory.ts, memory-dream.ts Memory 管理
|
||||
├── session.ts Session 管理
|
||||
├── autofix-pr.ts PR 自动修复
|
||||
├── ... (80+ 个其他)
|
||||
```
|
||||
|
||||
**特点**:每个 command 都可以定制输出格式,支持 structured output (NDJSON)。
|
||||
|
||||
---
|
||||
|
||||
## 递归粒度停止条件
|
||||
|
||||
以下类型的文件不再递归深入:
|
||||
|
||||
- **单功能工具函数** (<100 行,做一件明确的事):如 `array.ts`, `api.ts` 的某些导出
|
||||
- **简单 enum/type 定义**:如 `types.ts`, `*.d.ts`
|
||||
- **单个小的 React Hook**:如 `useAfterFirstRender.ts`
|
||||
- **Utility 映射和适配器**:如 message format converter
|
||||
|
||||
这些文件的学习价值在于"知道它存在",而不是理解其内部细节。
|
||||
|
||||
---
|
||||
|
||||
## 学习路线建议
|
||||
|
||||
1. **快速扫描**:从 README 开始,看一眼这个全景。
|
||||
2. **焦点学习**:根据你的兴趣选择深入:
|
||||
- 想理解 agent loop? → `query.ts`, `QueryEngine.ts`
|
||||
- 想理解 tool 系统? → `Tool.ts`, `tools/`, `services/tools/`
|
||||
- 想理解权限? → `utils/permissions/`
|
||||
- 想理解多 agent? → `tools/AgentTool/`, `coordinator/`
|
||||
3. **对照原文**:打开相应的源文件,参照本文的路线图逐个浏览。
|
||||
|
||||
---
|
||||
|
||||
## 核心数据流
|
||||
|
||||
```
|
||||
User Input
|
||||
↓
|
||||
entrypoints/cli.tsx (React 初始化)
|
||||
↓
|
||||
query.ts (Query Loop: QUERY → TOOL_USE → RESULT)
|
||||
↓
|
||||
Tool.ts & services/tools/ (Tool 执行)
|
||||
↓
|
||||
utils/permissions/ (权限检查) + BashTool/FileEditTool/... (实际执行)
|
||||
↓
|
||||
queryHelpers + messages (结果处理)
|
||||
↓
|
||||
state/ (状态更新)
|
||||
↓
|
||||
components/ + cli/print.ts (UI 渲染)
|
||||
↓
|
||||
User sees output
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键文件大小排名
|
||||
|
||||
```
|
||||
Top 15 largest files (最值得读):
|
||||
|
||||
1. query.ts ~1700 行
|
||||
2. services/tools/StreamingToolExecutor.ts
|
||||
3. utils/permissions/permissions.ts ~52 KB
|
||||
4. utils/permissions/yoloClassifier.ts ~52 KB
|
||||
5. utils/permissions/filesystem.ts ~62 KB
|
||||
6. hooks/useTypeahead.tsx ~212 KB
|
||||
7. cli/print.ts ~212 KB
|
||||
8. tools/AgentTool/AgentTool.tsx ~233 KB
|
||||
9. bootstrap/state.ts ~56 KB
|
||||
10. state/AppStateStore.ts ~21 KB
|
||||
11. services/autoDream/autoDream.ts ~11 KB
|
||||
12. coordinator/coordinatorMode.ts ~19 KB
|
||||
13. hooks/useCanUseTool.tsx ~40 KB
|
||||
14. hooks/useGlobalKeybindings.tsx ~31 KB
|
||||
15. hooks/useInboxPoller.ts ~34 KB
|
||||
```
|
||||
|
||||
先读前 5 个,会 cover 80% 的系统复杂性。
|
||||
279
docs/agentic-design/01-agent-loop.md
Normal file
279
docs/agentic-design/01-agent-loop.md
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
# 核心 Query Loop:Agent 的心脏
|
||||
|
||||
## 为什么需要 Loop?
|
||||
|
||||
想象一个简单的"一问一答"的 AI:用户问 → API 回复 → 完成。但 agent 不一样:
|
||||
|
||||
- 用户问:"帮我修复 bug"
|
||||
- Agent 读取文件 → 分析问题 → 尝试修复 → 运行测试 → 测试失败 → 再次修改 → 再次测试 → 成功
|
||||
|
||||
这个过程需要**多轮往返**,每一轮中 Agent 决定"下一步应该执行哪个 tool"。这就是 **Query Loop**。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念:状态机
|
||||
|
||||
Query loop 本质上是一个**状态机**,在以下几个状态间循环:
|
||||
|
||||
```
|
||||
START
|
||||
↓
|
||||
QUERY (提交 prompt 给 Claude API)
|
||||
↓
|
||||
TOOL_USE (API 返回 tool_use block,包含 tool 名 + 输入)
|
||||
↓
|
||||
执行 Tool (BashTool, FileEditTool, 等等)
|
||||
↓
|
||||
RESULT (Tool 返回结果)
|
||||
↓
|
||||
提交结果给 Claude (作为 user 消息)
|
||||
↓
|
||||
QUERY (继续循环)
|
||||
↓
|
||||
END (API 返回 stop_reason="end_turn" 或其他终止条件)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码位置
|
||||
|
||||
关键文件:`query.ts` (~1700 行)
|
||||
|
||||
这个文件包含:
|
||||
- Loop 的完整实现 (使用 `AsyncGenerator`)
|
||||
- 状态跟踪
|
||||
- Tool 调用的编排
|
||||
- Streaming 支持
|
||||
- Compaction 触发
|
||||
- 错误处理和恢复
|
||||
|
||||
你应该从 `query.ts` 的函数签名开始:
|
||||
|
||||
```typescript
|
||||
export function* query(...): AsyncGenerator<QueryMessage | QueryEvent, ...>
|
||||
```
|
||||
|
||||
它返回一个 `AsyncGenerator`,每次 `yield` 都是一个"事件"(API 消息、tool 调用、错误等)。
|
||||
|
||||
---
|
||||
|
||||
## Loop 的生命周期
|
||||
|
||||
### 阶段 1: 初始化
|
||||
|
||||
当你调用 `query()` 时:
|
||||
- `messages` 数组初始化(包含 system prompt + 历史消息)
|
||||
- `token_budget` 初始化(计算剩余 token)
|
||||
- Compaction 检查(如果历史太长,自动压缩)
|
||||
- Abort controller 初始化(支持中途停止)
|
||||
|
||||
### 阶段 2: 发送 Prompt
|
||||
|
||||
```typescript
|
||||
// 构建消息数组
|
||||
let messages = [system_prompt, ...history];
|
||||
|
||||
// 调用 Claude API(使用缓存)
|
||||
let response = await claude.messages.create({
|
||||
messages,
|
||||
system: system_prompt,
|
||||
max_tokens: remaining_budget,
|
||||
stream: true, // 重要:流式输出
|
||||
...config
|
||||
});
|
||||
```
|
||||
|
||||
Streaming 很关键——API 会逐步返回:
|
||||
1. First chunk: 可能包含 `message.start`
|
||||
2. Content blocks: `text_delta`, `tool_use` block 开始
|
||||
3. Stop reason: 表示本轮结束
|
||||
|
||||
### 阶段 3: 处理 Streaming
|
||||
|
||||
当 API 返回 `tool_use` block 时,loop 收集完整的参数,然后:
|
||||
|
||||
```
|
||||
接收 tool_use block
|
||||
↓
|
||||
验证 tool 名称是否存在
|
||||
↓
|
||||
检查权限 (useCanUseTool hook)
|
||||
↓
|
||||
执行 tool (StreamingToolExecutor)
|
||||
↓
|
||||
Yield tool 结果给消费者
|
||||
↓
|
||||
添加到 messages 作为 user 消息
|
||||
```
|
||||
|
||||
### 阶段 4: 工具执行
|
||||
|
||||
Tool 执行由 `services/tools/StreamingToolExecutor.ts` 处理:
|
||||
- **并发**:最多 5 个 tool 同时运行
|
||||
- **顺序输出**:结果按调用顺序返回(即使执行顺序不同)
|
||||
- **Sibling abort**:一个失败会 cancel 兄弟 tool
|
||||
|
||||
### 阶段 5: 检查终止条件
|
||||
|
||||
每一轮后,loop 检查是否应该继续:
|
||||
|
||||
```
|
||||
stop_reason == "end_turn"? → 正常结束
|
||||
stop_reason == "max_tokens"? → Token 超限,触发 compaction
|
||||
consecutive_errors >= 3? → 连续错误,放弃
|
||||
max_turns_reached? → 超过最大轮数
|
||||
abort_signal? → 用户中断
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Token Budget:Context Window 的有限性
|
||||
|
||||
Context window 是有限的(比如 200K tokens)。这里的逻辑:
|
||||
|
||||
```
|
||||
available_tokens = context_window_size - system_prompt_tokens - reserved_buffer
|
||||
|
||||
每一轮后:
|
||||
usage = response.usage (API 返回的实际使用)
|
||||
remaining = available_tokens - usage.input_tokens - usage.output_tokens
|
||||
|
||||
if remaining < threshold:
|
||||
触发 compaction (压缩历史)
|
||||
|
||||
if remaining < min_required:
|
||||
拒绝继续,返回错误
|
||||
```
|
||||
|
||||
**Threshold** 通常设得比较激进,比如还剩 20% 时就开始压缩,以避免在真的耗尽时措手不及。
|
||||
|
||||
相关文件:`query/tokenBudget.ts`
|
||||
|
||||
---
|
||||
|
||||
## Compaction:自动历史压缩
|
||||
|
||||
当 token 预算吃紧时,loop 会自动压缩历史:
|
||||
|
||||
1. **Analyzer phase**: 扫描历史消息,找出"长输出消息"(比如代码块)
|
||||
2. **Prioritize**: 对话越早,压缩优先级越高
|
||||
3. **Compress**: 使用 Claude API 总结这段历史,生成 200 行以内的 summary
|
||||
4. **Replace**: 把原始的 20 条消息替换为 1 条 summary 消息
|
||||
|
||||
这保证了 loop 可以持续运行,即使初始历史很长。
|
||||
|
||||
---
|
||||
|
||||
## Streaming 与 Tool Call 的交织
|
||||
|
||||
这是一个复杂的地方。API 可能返回:
|
||||
|
||||
```
|
||||
message {
|
||||
content: [
|
||||
{ type: "text", text: "让我先读文件..." },
|
||||
{ type: "tool_use", id: "...", name: "FileReadTool", ... },
|
||||
{ type: "text", text: "现在我看到问题了..." },
|
||||
{ type: "tool_use", id: "...", name: "FileEditTool", ... },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
所以一条消息中可能**混合了文本和 tool call**。Loop 需要:
|
||||
- 收集每个 tool_use block 的完整输入参数(可能分段到达)
|
||||
- 在 tool_use 结束时立即执行(不等待消息全部返回)
|
||||
- 继续接收后续的文本或 tool call
|
||||
- 当消息完全接收后,将所有 tool 结果作为一条 user 消息发回
|
||||
|
||||
这使得 **interleaved thinking** 成为可能:模型可以一边思考一边调用工具。
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么用 `AsyncGenerator` 而不是 callback?
|
||||
|
||||
不好的设计(callback):
|
||||
```typescript
|
||||
query(messages, {
|
||||
onMessage: (msg) => {...},
|
||||
onToolCall: (tool, input) => {...},
|
||||
onError: (err) => {...},
|
||||
onProgress: (progress) => {...},
|
||||
})
|
||||
```
|
||||
|
||||
问题:
|
||||
- 4 种不同的 callback,难以理解执行顺序
|
||||
- 消费者需要管理状态机来追踪"当前在哪个阶段"
|
||||
- 测试困难
|
||||
|
||||
**更好的设计**(Generator):
|
||||
```typescript
|
||||
for await (const event of query(messages, config)) {
|
||||
if (event.type === 'message') {...}
|
||||
else if (event.type === 'tool_use') {...}
|
||||
else if (event.type === 'error') {...}
|
||||
}
|
||||
```
|
||||
|
||||
优点:
|
||||
- 单一的 `for await...of` 循环,清晰的顺序
|
||||
- 每个 event 都是一个联合类型,编译器可以帮你检查
|
||||
- 可以在 loop 中添加复杂的条件逻辑(abort, timeout 等)
|
||||
- 易于测试(可以模拟一个生成事件的 generator)
|
||||
|
||||
Generator 让"一系列事件"变得一级公民。
|
||||
|
||||
### 为什么需要 Compaction?
|
||||
|
||||
不做 compaction:
|
||||
- 100 轮 agent 工作后,messages 数组有几千条消息
|
||||
- 提交给 API 时,prompt cache hit 率下降(因为前缀不再是"静态部分")
|
||||
- Token 浪费在重复的对话历史上
|
||||
|
||||
做 compaction:
|
||||
- 定期将"已经达成共识的历史部分"总结成一条消息
|
||||
- 保持 prompt cache 的"静态前缀"有效
|
||||
- 节省 token,延长 agent 能工作的轮数
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Query loop 就是不断问 Claude?"
|
||||
|
||||
实际:Loop 实现的是一个**两层状态机**:
|
||||
- 外层:Claude API 的请求/响应循环
|
||||
- 内层:Tool 的执行和结果收集
|
||||
|
||||
tool 执行完全不涉及 Claude API 的额外调用(除非 tool 本身调用 API)。
|
||||
|
||||
**误解 2**:"Compaction 会丢失信息?"
|
||||
|
||||
实际:Compaction 使用 Claude 自己来总结历史,所以关键信息被保留。丢失的只是"冗余的解释"或"中间尝试",这些对后续工作没用。
|
||||
|
||||
**误解 3**:"Token budget 追踪是精确的?"
|
||||
|
||||
实际:API 返回的 `usage` 是经过舍入的(某些模型),所以我们的估计可能有 ±5% 的误差。这就是为什么我们用激进的阈值(20% 时就开始压缩)而不是等到 100% 才反应。
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **Query loop 是异步生成器**,yield 事件流而不是回调
|
||||
2. **循环的关键状态转移**:QUERY → TOOL_USE → RESULT → QUERY
|
||||
3. **Token budget 必须主动管理**,compaction 是自动压缩的关键
|
||||
4. **Streaming 支持 interleaved thinking**,一条消息中可以混合文本和 tool call
|
||||
5. **Compaction 会定期触发**,保持 history 可控,并维护 prompt cache 的有效性
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `query.ts`:完整 loop 实现(必读)
|
||||
- `query/tokenBudget.ts`:Token 追踪逻辑
|
||||
- `services/tools/StreamingToolExecutor.ts`:Tool 执行编排
|
||||
- `query/stopHooks.ts`:终止条件检查
|
||||
|
||||
下一步:去了解 **Tool 系统**是如何设计的,使得这个 loop 可以灵活地执行任意 tool。
|
||||
378
docs/agentic-design/02-tool-system.md
Normal file
378
docs/agentic-design/02-tool-system.md
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
# Tool 系统:Agent 影响世界的手段
|
||||
|
||||
## 为什么需要 Tool?
|
||||
|
||||
如果 Claude 只能回复文字,它就只是个聊天机器人。要让 Claude **执行动作**(读文件、改代码、运行命令),需要一个 **Tool 接口**。
|
||||
|
||||
Tool 系统解决的问题:
|
||||
1. 如何告诉 Claude "有哪些动作可用"?
|
||||
2. 如何验证 Claude 的请求是否合法(权限检查)?
|
||||
3. 如何让多个 tool 并发执行,但结果保持有序?
|
||||
4. 如何在 tool 执行失败时恢复?
|
||||
|
||||
---
|
||||
|
||||
## 核心概念:Tool Interface
|
||||
|
||||
一个 `Tool` 必须满足这个接口(`Tool.ts`):
|
||||
|
||||
```typescript
|
||||
interface Tool {
|
||||
// 1. 模型识别 tool 的名称
|
||||
name: string // 如 "FileReadTool"
|
||||
|
||||
// 2. 模型理解 tool 的用途(出现在 system prompt 中)
|
||||
description: string
|
||||
|
||||
// 3. 模型生成的参数必须符合这个 schema
|
||||
inputSchema: JSONSchema // 如 { type: "object", properties: { path: ... } }
|
||||
|
||||
// 4. 执行 tool 的函数
|
||||
call(input: ToolInput): AsyncGenerator<ToolResult>
|
||||
|
||||
// 5. 指示这个 tool 是否"安全"(后文详述)
|
||||
isSafe?: boolean
|
||||
|
||||
// 6. 内部使用,不会发给模型
|
||||
internalMetadata?: { ... }
|
||||
}
|
||||
```
|
||||
|
||||
每个字段的意义:
|
||||
|
||||
### `name` 和 `description`
|
||||
这两个被送到 Claude API,出现在 system prompt 的 tool 列表中:
|
||||
|
||||
```
|
||||
Available tools:
|
||||
|
||||
1. FileReadTool
|
||||
Read the contents of a file from the filesystem
|
||||
Parameters: { path: string }
|
||||
|
||||
2. BashTool
|
||||
Execute a bash command in the user's shell
|
||||
Parameters: { command: string }
|
||||
```
|
||||
|
||||
Claude 看到这个列表,学会了"我可以用 FileReadTool 来读文件"。
|
||||
|
||||
### `inputSchema`
|
||||
这是 JSON Schema 格式,例如:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
path: {
|
||||
type: "string",
|
||||
description: "Absolute file path"
|
||||
}
|
||||
},
|
||||
required: ["path"]
|
||||
}
|
||||
```
|
||||
|
||||
Claude 会**严格遵守这个 schema**(模型已训练)。例如,如果你要求 `path` 必须是字符串,Claude 就不会发来 `{path: 123}`。
|
||||
|
||||
### `call(input)`
|
||||
这是 tool 的实现。它返回一个 `AsyncGenerator`,每次 `yield` 都是一个结果。例如:
|
||||
|
||||
```typescript
|
||||
async *call(input: { path: string }) {
|
||||
// 逐步产生结果(支持 streaming)
|
||||
yield { type: "start", message: "Opening file..." }
|
||||
yield { type: "content", data: file_content }
|
||||
yield { type: "done", lines_read: 100 }
|
||||
}
|
||||
```
|
||||
|
||||
为什么是 generator?因为某些 tool(如 BashTool)的输出可能很长,需要**流式返回**。
|
||||
|
||||
### `isSafe` 和权限
|
||||
|
||||
某些 tool 被标记为 `unsafe`:
|
||||
- `BashTool` ← 可以执行任意命令,危险
|
||||
- `FileEditTool` ← 可以修改文件,需要权限检查
|
||||
- `FileReadTool` ← 只读,相对安全
|
||||
|
||||
Unsafe tool 在执行前**必须通过权限检查**。
|
||||
|
||||
---
|
||||
|
||||
## Tool 注册与过滤
|
||||
|
||||
在 `tools.ts` 中,所有 tool 被注册到一个中央注册表:
|
||||
|
||||
```typescript
|
||||
const ALL_TOOLS: Tool[] = [
|
||||
AgentTool,
|
||||
BashTool,
|
||||
FileReadTool,
|
||||
FileEditTool,
|
||||
...,
|
||||
]
|
||||
```
|
||||
|
||||
然后根据用户权限和配置进行**过滤**:
|
||||
|
||||
```typescript
|
||||
// 仅公开 tool
|
||||
const PUBLIC_TOOLS = ALL_TOOLS.filter(t => !t.internalOnly)
|
||||
|
||||
// 仅 ant 用户可用
|
||||
const ANT_ONLY_TOOLS = [REPL_TOOL, CONFIGTOOL, ...]
|
||||
```
|
||||
|
||||
这样,模型只会看到当前环境允许的 tool 清单。
|
||||
|
||||
---
|
||||
|
||||
## Tool 执行 Pipeline
|
||||
|
||||
从"模型调用 tool"到"返回结果"的完整流程:
|
||||
|
||||
```
|
||||
Query Loop 收到 tool_use block
|
||||
├─ Tool 名: "FileEditTool", Input: {path: "...", newContent: "..."}
|
||||
│
|
||||
├─ 1. 验证 Tool 存在 ✓
|
||||
│
|
||||
├─ 2. 权限检查 (utils/permissions/)
|
||||
│ - Mode: auto 还是 default?
|
||||
│ - 规则匹配? (如 "FileEdit(/src/*.ts)")
|
||||
│ - YOLO 分类器? (是否 auto-approve)
|
||||
│ - 如果拒绝 → 返回 permission_denied error
|
||||
│
|
||||
├─ 3. 执行 Tool
|
||||
│ - 调用 tool.call(input)
|
||||
│ - 得到 AsyncGenerator
|
||||
│ - 逐个 yield 结果
|
||||
│
|
||||
├─ 4. 收集结果
|
||||
│ - 内容可能很长,缓冲到内存
|
||||
│ - 或流式返回给 UI
|
||||
│
|
||||
└─ 5. 发送给 Claude
|
||||
- 组合成 user 消息: "Tool result: ..."
|
||||
- 继续 Query Loop
|
||||
```
|
||||
|
||||
相关文件:
|
||||
- 权限检查:`utils/permissions/permissions.ts`
|
||||
- 执行编排:`services/tools/toolOrchestration.ts`
|
||||
- 流式执行:`services/tools/StreamingToolExecutor.ts`
|
||||
|
||||
---
|
||||
|
||||
## 并发执行:多个 Tool 同时运行
|
||||
|
||||
一条 Claude 消息中可能包含多个 `tool_use` block:
|
||||
|
||||
```
|
||||
Claude:
|
||||
1. 读文件 A (tool_use id=1)
|
||||
2. 读文件 B (tool_use id=2)
|
||||
3. 读文件 C (tool_use id=3)
|
||||
```
|
||||
|
||||
这三个 tool 可以**并发执行**,但有几个复杂性:
|
||||
|
||||
### Concurrency Gate
|
||||
|
||||
同时最多 5 个 tool 运行(可配置):
|
||||
|
||||
```typescript
|
||||
const CONCURRENCY_LIMIT = 5
|
||||
|
||||
// 队列管理
|
||||
const queue = [tool_1, tool_2, tool_3, ...]
|
||||
while (queue.length > 0) {
|
||||
const batch = queue.splice(0, CONCURRENCY_LIMIT)
|
||||
const results = await Promise.all(
|
||||
batch.map(tool => tool.call(...))
|
||||
)
|
||||
// 处理结果
|
||||
}
|
||||
```
|
||||
|
||||
### In-Order Emission
|
||||
|
||||
虽然执行可能乱序,但**结果必须按调用顺序返回**:
|
||||
|
||||
```
|
||||
调用顺序: tool_1, tool_2, tool_3
|
||||
执行时序: tool_2 ✓ (快速)
|
||||
tool_1 ✓ (较慢)
|
||||
tool_3 ✓
|
||||
|
||||
返回顺序: tool_1 结果 → tool_2 结果 → tool_3 结果
|
||||
|
||||
为什么? 因为 Claude 期望看到它调用的顺序被保留
|
||||
```
|
||||
|
||||
实现方式:使用**结果缓冲**:
|
||||
|
||||
```typescript
|
||||
const results = new Map() // id → result
|
||||
let nextIdToEmit = 0
|
||||
|
||||
for each completed tool {
|
||||
results.set(tool.id, tool.result)
|
||||
|
||||
// 检查是否可以按顺序 emit
|
||||
while (results.has(nextIdToEmit)) {
|
||||
yield results.get(nextIdToEmit)
|
||||
nextIdToEmit++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sibling Abort
|
||||
|
||||
如果一个 tool 失败了,它的**兄弟 tool 会被立即取消**:
|
||||
|
||||
```
|
||||
tool_1 → 运行中
|
||||
tool_2 → 运行中
|
||||
tool_3 → 运行中
|
||||
|
||||
tool_2 抛异常 ✗
|
||||
↓
|
||||
立即 abort tool_1 和 tool_3
|
||||
↓
|
||||
返回错误,整个批次失败
|
||||
```
|
||||
|
||||
这是一个**快速失败**策略:不要继续浪费时间在其他 tool 上,直接告诉 Claude 出了问题。
|
||||
|
||||
相关文件:`services/tools/StreamingToolExecutor.ts`
|
||||
|
||||
---
|
||||
|
||||
## Safe vs Unsafe Tool Classification
|
||||
|
||||
Tool 分两类:
|
||||
|
||||
### Safe Tool(自动执行)
|
||||
```
|
||||
FileReadTool ← 只读,没有副作用
|
||||
WebFetchTool ← 只是下载网页
|
||||
GlobTool ← 只是列文件
|
||||
GrepTool ← 只是搜索内容
|
||||
```
|
||||
|
||||
这些可以在 `auto` permission mode 下自动执行,不需要用户确认。
|
||||
|
||||
### Unsafe Tool(需要权限检查)
|
||||
```
|
||||
BashTool ← 可能执行任意命令
|
||||
FileEditTool ← 可能修改重要文件
|
||||
FileWriteTool ← 可能覆盖数据
|
||||
AgentTool ← 可能 spawn 新 agent
|
||||
TaskCreateTool ← 可能创建后台任务
|
||||
```
|
||||
|
||||
这些在执行前必须通过 `utils/permissions/` 的检查。
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么 Tool 返回 `AsyncGenerator` 而不是 `Promise<string>`?
|
||||
|
||||
不好的设计:
|
||||
```typescript
|
||||
call(input): Promise<string> {
|
||||
// 必须等待整个操作完成后才返回
|
||||
const result = await readFile(input.path)
|
||||
return result // 10 MB 的代码文件?等吧
|
||||
}
|
||||
```
|
||||
|
||||
问题:
|
||||
- 大文件时,UI 一直卡住,看不到进度
|
||||
- Claude 看不到中间步骤
|
||||
|
||||
更好的设计:
|
||||
```typescript
|
||||
call(input): AsyncGenerator<ToolResult> {
|
||||
yield { type: "progress", message: "Reading..." }
|
||||
const content = await readFile(input.path)
|
||||
yield { type: "content", data: content } // 分块返回
|
||||
yield { type: "complete", lines: 100 }
|
||||
}
|
||||
```
|
||||
|
||||
优点:
|
||||
- UI 可以**立即显示**进度消息
|
||||
- 长操作时用户看得到"不是卡住了,是在处理"
|
||||
- 便于 debug:中间消息可以被记录
|
||||
|
||||
Generator 把**长时间操作的透明度**提升为一级特性。
|
||||
|
||||
### 为什么要保持结果顺序?
|
||||
|
||||
如果乱序返回:
|
||||
```
|
||||
Claude 说:读 A, 读 B, 读 C
|
||||
我们返回:B 的结果, C 的结果, A 的结果
|
||||
```
|
||||
|
||||
Claude 会困惑:"我要的 A 呢?"
|
||||
|
||||
所以必须:
|
||||
```
|
||||
Claude 说:读 A, 读 B, 读 C
|
||||
我们返回:A 的结果, B 的结果, C 的结果
|
||||
```
|
||||
|
||||
这样 Claude 可以正确关联"我的第一个请求的响应是这个"。
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Tool 就是函数调用?"
|
||||
|
||||
实际:Tool 是一个**完整的接口**,包括:
|
||||
- 模型可见的名称和描述
|
||||
- 输入规范(schema)
|
||||
- 权限检查
|
||||
- 执行编排
|
||||
- Streaming 支持
|
||||
|
||||
函数调用只是内部实现的一部分。
|
||||
|
||||
**误解 2**:"Tool 是 OpenAI function calling 的翻版?"
|
||||
|
||||
实际:Claude Code 的 Tool 设计更深层:
|
||||
- 对权限的原生支持(OpenAI 需要自己实现)
|
||||
- 对并发执行的编排
|
||||
- 对长时间操作的 streaming 支持
|
||||
- 更细粒度的安全分类
|
||||
|
||||
**误解 3**:"所有 tool 都必须是同步的?"
|
||||
|
||||
实际:Tool 是异步的(`AsyncGenerator`),可以支持任意长的操作(包括网络请求、文件 I/O)。
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **Tool interface 是客户端与 Agent 的合约**,包含名称、描述、schema、执行函数
|
||||
2. **Permission 检查是 tool 执行前的网关**,决定是否允许
|
||||
3. **并发执行最多 5 个 tool**,但结果必须按调用顺序返回
|
||||
4. **Sibling abort 是快速失败**,一个错误就停止兄弟 tool
|
||||
5. **AsyncGenerator 使 tool 支持 streaming**,UI 可见进度,model 可见中间步骤
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `Tool.ts`:Tool interface 定义
|
||||
- `tools.ts`:Tool 注册和过滤
|
||||
- `services/tools/StreamingToolExecutor.ts`:并发执行编排
|
||||
- `utils/permissions/permissions.ts`:权限检查
|
||||
|
||||
下一步:去了解**多 Agent 协调**,看看当 Claude 想要 spawn 一个子 agent 时(通过 `AgentTool`),系统如何支持这个。
|
||||
351
docs/agentic-design/03-multi-agent-coordination.md
Normal file
351
docs/agentic-design/03-multi-agent-coordination.md
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
# 多 Agent 协调:并行与分解
|
||||
|
||||
## 为什么需要多个 Agent?
|
||||
|
||||
有些任务太复杂,一个 agent 力不从心:
|
||||
|
||||
- 用户说:"帮我把代码库从 TypeScript 迁到 Rust"
|
||||
- 一个 agent 从第一个文件开始,改 5 个小时后,context 满了
|
||||
|
||||
更好的方式:**分解任务**
|
||||
|
||||
- Coordinator agent 解析任务、制定计划
|
||||
- Worker agent 1 并行处理"转换 utils 模块"
|
||||
- Worker agent 2 并行处理"转换 API 层"
|
||||
- Worker agent 3 并行处理"转换 UI 组件"
|
||||
- Coordinator 总结结果、验证一致性
|
||||
|
||||
这就是 **多 agent 协调**。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念:Agent 类型
|
||||
|
||||
Claude Code 支持多种 agent:
|
||||
|
||||
### 1. Local Agent(进程内)
|
||||
|
||||
```typescript
|
||||
// 在当前进程中 spawn 一个子 agent
|
||||
const subAgent = await spawnAgent({
|
||||
type: "local",
|
||||
agentDef: generalPurposeAgent,
|
||||
initialMessage: "帮我测试这个函数"
|
||||
})
|
||||
```
|
||||
|
||||
特点:
|
||||
- 共享内存(同一个 Node.js 进程)
|
||||
- 上下文隔离(通过 `AsyncLocalStorage`)
|
||||
- 无网络开销
|
||||
- 最快的交互
|
||||
|
||||
### 2. Remote Agent(云容器)
|
||||
|
||||
```typescript
|
||||
const remoteAgent = await spawnAgent({
|
||||
type: "remote",
|
||||
agentDef: exploreAgent,
|
||||
initialMessage: "搜索 codebase 中的 bug"
|
||||
})
|
||||
```
|
||||
|
||||
特点:
|
||||
- 在 CCR(Cloud Container Runtime)中运行
|
||||
- 独立的资源池
|
||||
- 支持长时间运行(最多 30 分钟)
|
||||
- 网络传输延迟,但隔离度最高
|
||||
|
||||
### 3. Forked Agent(进程派生)
|
||||
|
||||
```typescript
|
||||
const forkedAgent = spawnForkedAgent({
|
||||
agentScript: "./my-agent.ts",
|
||||
args: ["--mode", "debug"]
|
||||
})
|
||||
```
|
||||
|
||||
特点:
|
||||
- 创建新的 Node.js 子进程
|
||||
- 完全隔离的 V8 引擎
|
||||
- 支持 CPU 密集任务(不会阻塞主线程)
|
||||
|
||||
### 4. In-Process Teammate(进程内同伴)
|
||||
|
||||
```typescript
|
||||
const teammate = await createTeam({
|
||||
name: "research-squad",
|
||||
agents: [agent1, agent2, agent3],
|
||||
strategy: "parallel"
|
||||
})
|
||||
```
|
||||
|
||||
特点:
|
||||
- 多个 agent 同时运行
|
||||
- 共享邮箱(互相发消息)
|
||||
- 同步协调,不需要网络
|
||||
|
||||
---
|
||||
|
||||
## Agent 的 Spawning 和隔离
|
||||
|
||||
### 怎样 Spawn 一个 Agent?
|
||||
|
||||
通过 `AgentTool` (文件:`tools/AgentTool/AgentTool.tsx`):
|
||||
|
||||
```typescript
|
||||
// Claude 调用 AgentTool
|
||||
Agent: "让我 spawn 一个 explore agent 来搜索代码库"
|
||||
|
||||
Tool Call:
|
||||
name: "AgentTool"
|
||||
input: {
|
||||
type: "spawn",
|
||||
agentDef: "exploreAgent",
|
||||
prompt: "搜索 utils/ 下的性能问题",
|
||||
isolationMode: "local"
|
||||
}
|
||||
|
||||
返回:
|
||||
agentId: "abc123"
|
||||
status: "running"
|
||||
```
|
||||
|
||||
### 上下文隔离:`AsyncLocalStorage`
|
||||
|
||||
为什么需要隔离?子 agent 的全局状态不应该污染父 agent:
|
||||
|
||||
```typescript
|
||||
// 主 agent 的全局状态
|
||||
const sessionId = "parent-session-id"
|
||||
const permissions = "default"
|
||||
|
||||
// 在子 agent 中,这些应该被**覆盖**
|
||||
asyncLocalStorage.run({
|
||||
sessionId: "child-agent-id",
|
||||
permissions: "bubble", // 特殊权限给 worker
|
||||
memoryDir: "/tmp/child-memory"
|
||||
}, () => {
|
||||
runChildAgent() // 子 agent 看到的是覆盖后的值
|
||||
})
|
||||
```
|
||||
|
||||
实现:`AsyncLocalStorage` 是 Node.js 内置的上下文隔离机制。
|
||||
|
||||
---
|
||||
|
||||
## Coordinator 模式:4 个阶段
|
||||
|
||||
启用 `CLAUDE_CODE_COORDINATOR_MODE=1` 时,agent 进入 coordinator 模式。
|
||||
|
||||
### 阶段结构
|
||||
|
||||
```
|
||||
用户请求: "帮我重构这个 monorepo"
|
||||
↓
|
||||
[1] RESEARCH PHASE
|
||||
Coordinator 说:"我需要理解项目结构"
|
||||
Worker 1 探索 package.json + tsconfig
|
||||
Worker 2 探索 src/ 目录结构
|
||||
Worker 3 探索 dependencies 图
|
||||
→ 输出: 项目现状报告
|
||||
↓
|
||||
[2] SYNTHESIS PHASE
|
||||
Coordinator 读 3 个 worker 的报告
|
||||
Coordinator 说:"基于这些发现,我的计划是..."
|
||||
→ 输出: 详细重构计划
|
||||
↓
|
||||
[3] IMPLEMENTATION PHASE
|
||||
Coordinator 把计划分解为任务
|
||||
Worker A 修改 package.json
|
||||
Worker B 更新 tsconfig
|
||||
Worker C 调整 src/ 组织
|
||||
→ 并行执行
|
||||
↓
|
||||
[4] VERIFICATION PHASE
|
||||
Coordinator 说:"让我验证改动"
|
||||
Worker 运行测试
|
||||
Worker 检查类型错误
|
||||
→ 输出: 验证报告
|
||||
↓
|
||||
完成:Coordinator 总结全流程,返回最终结果
|
||||
```
|
||||
|
||||
### 通信机制:`<task-notification>`
|
||||
|
||||
Worker 通过 XML 消息向 Coordinator 报告进度:
|
||||
|
||||
```xml
|
||||
<task-notification>
|
||||
<task-id>research-1</task-id>
|
||||
<status>complete</status>
|
||||
<summary>Found 3 monorepo packages</summary>
|
||||
<details>
|
||||
- Package A: React components
|
||||
- Package B: Utils library
|
||||
- Package C: CLI tool
|
||||
</details>
|
||||
</task-notification>
|
||||
```
|
||||
|
||||
Coordinator 看到这个消息,就知道:
|
||||
- Task "research-1" 完成了
|
||||
- 发现了 3 个包
|
||||
|
||||
相关文件:`coordinator/coordinatorMode.ts`(包含完整的 system prompt,大约 330 行)
|
||||
|
||||
---
|
||||
|
||||
## 共享 Scratchpad
|
||||
|
||||
Worker 可以在共享空间写临时数据(feature gate: `tengu_scratch`):
|
||||
|
||||
```typescript
|
||||
// Worker 1 写入
|
||||
await writeToScratchpad("project_structure.md", """
|
||||
# Monorepo Structure
|
||||
- packages/ui/
|
||||
- components/
|
||||
- styles/
|
||||
""")
|
||||
|
||||
// Worker 2 读取
|
||||
const structure = await readFromScratchpad("project_structure.md")
|
||||
|
||||
// Coordinator 读取
|
||||
const allNotes = await listScratchpad()
|
||||
```
|
||||
|
||||
优点:
|
||||
- Worker 不需要把所有信息放在 user 消息中
|
||||
- 大文件可以直接读取,不占用 context window
|
||||
- 自然的"工作台"抽象
|
||||
|
||||
---
|
||||
|
||||
## Agent 内存和记忆
|
||||
|
||||
每个 agent 可以有自己的 memory:
|
||||
|
||||
```
|
||||
~/.claude/memory/
|
||||
├── team-memory/ # 团队共享
|
||||
│ ├── shared-findings.md
|
||||
│ └── progress.md
|
||||
├── agent-abc123/ # Agent 特定
|
||||
│ ├── MEMORY.md # Agent 的长期知识库
|
||||
│ └── session-transcript.log
|
||||
```
|
||||
|
||||
**Team Memory Sync** (`services/teamMemorySync/`) 确保:
|
||||
- 所有 agent 可以读取共享的 findings
|
||||
- 不会产生数据竞争(使用 file lock)
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么用 `<task-notification>` XML 而不是结构化 IPC?
|
||||
|
||||
不好的设计(二进制 IPC):
|
||||
```
|
||||
Worker 发送: MessageType::RESEARCH_COMPLETE {
|
||||
task_id: 0x123,
|
||||
status: 0x02,
|
||||
payload: [0xAB, 0xCD, ...]
|
||||
}
|
||||
```
|
||||
|
||||
问题:
|
||||
- Claude 看不懂二进制协议
|
||||
- 如果需要调试,你必须手动反序列化
|
||||
- 扩展协议时需要修改版本号
|
||||
|
||||
更好的设计(XML):
|
||||
```
|
||||
<task-notification>
|
||||
<task-id>abc123</task-id>
|
||||
<status>complete</status>
|
||||
<summary>Found 100 bugs</summary>
|
||||
</task-notification>
|
||||
```
|
||||
|
||||
优点:
|
||||
- Claude 可以**直接阅读和生成**这种格式
|
||||
- 调试时在日志中直接看到可读的消息
|
||||
- 自文档化(XML 标签说明意义)
|
||||
- 易于扩展(添加新字段不破坏旧系统)
|
||||
|
||||
**Text is the protocol** 的又一例证。
|
||||
|
||||
### 为什么 Coordinator 要等 Research 完全结束才进行 Synthesis?
|
||||
|
||||
如果不等(流式处理):
|
||||
```
|
||||
Worker 1 报告 → Coordinator 开始 synthesis
|
||||
Worker 2 报告(晚到)→ Coordinator 需要重新 synthesis
|
||||
Worker 3 报告(更晚)→ 又得重新来一遍
|
||||
```
|
||||
|
||||
问题:浪费 token 和时间在重复的 synthesis。
|
||||
|
||||
如果等待(阶段隔离):
|
||||
```
|
||||
所有 Worker 完成 Research
|
||||
↓
|
||||
Coordinator 一次性读完所有报告
|
||||
↓
|
||||
一次 Synthesis,不需要改
|
||||
```
|
||||
|
||||
权衡:
|
||||
- 阶段隔离:时间线性(4 个阶段顺序执行)
|
||||
- 流式处理:时间可能更短(如果 worker 速度差异大),但 prompt 可能更长
|
||||
|
||||
Claude Code 选择阶段隔离,因为系统提示清晰度更重要。
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Coordinator 也会执行 tool?"
|
||||
|
||||
实际:Coordinator **只** spawn worker 和管理任务。所有的 tool 执行(读文件、改代码、运行命令)都在 worker 中进行。Coordinator 只是看消息、做决策、发指令。
|
||||
|
||||
**误解 2**:"Worker 之间可以直接通信?"
|
||||
|
||||
实际:Worker 通过 **Coordinator 中转**。这样 Coordinator 可以:
|
||||
- 理解全局状态
|
||||
- 决定优先级
|
||||
- 在冲突时仲裁
|
||||
|
||||
直接 worker-to-worker 通信会导致难以追踪的依赖和死锁。
|
||||
|
||||
**误解 3**:"多个 agent 肯定比单个快?"
|
||||
|
||||
实际:取决于任务:
|
||||
- **可并行化**(如搜索 3 个不同目录):并行快
|
||||
- **高度依赖**(后续任务需要前序结果):并行反而慢(overhead)
|
||||
|
||||
Claude Code 的 Coordinator 会选择合适的分解策略。
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **Agent 类型**:local (in-process), remote (CCR 30min), forked, teammate
|
||||
2. **上下文隔离**:通过 `AsyncLocalStorage` 分离全局状态
|
||||
3. **Coordinator 模式**:4 个阶段(research → synthesis → implementation → verification)
|
||||
4. **Worker 通信**:通过 `<task-notification>` XML,Coordinator 中转
|
||||
5. **Shared scratchpad**:大文件存储,不占 context window
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `tools/AgentTool/AgentTool.tsx`:Agent spawning 逻辑
|
||||
- `tools/AgentTool/forkSubagent.ts`:Fork 实现
|
||||
- `coordinator/coordinatorMode.ts`:Coordinator system prompt 和阶段逻辑
|
||||
- `services/teamMemorySync/`:Team memory 同步
|
||||
|
||||
下一步:了解**权限系统**,看看 Agent 怎么知道"我可以执行什么 tool"。
|
||||
317
docs/agentic-design/04-permission-system.md
Normal file
317
docs/agentic-design/04-permission-system.md
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
# Permission 系统:Agent 的沙箱
|
||||
|
||||
## 为什么需要权限管控?
|
||||
|
||||
想象 Claude 可以执行任意 bash 命令。用户说"帮我看看网站",Claude 可能:
|
||||
- 合法地执行 `curl https://example.com`
|
||||
- 也可能执行 `rm -rf /` ← 灾难
|
||||
|
||||
**权限系统的目标**:
|
||||
1. 阻止意外的危险操作
|
||||
2. 提示用户"你确定吗?"
|
||||
3. 在保留便利的同时维护安全
|
||||
|
||||
Claude Code 的 permission 系统是一个**多层防线**。
|
||||
|
||||
---
|
||||
|
||||
## 核心概念:Permission Mode
|
||||
|
||||
系统支持多种权限模式,用户可以选择:
|
||||
|
||||
| Mode | 特点 | 场景 |
|
||||
|------|------|------|
|
||||
| `default` | 每次都问用户 | 不信任 agent,想完全控制 |
|
||||
| `auto` | ML 分类器自动审批 | 信任 agent,想要流畅体验 |
|
||||
| `acceptEdits` | 自动接受文件编辑(但仍然问危险操作) | 开发工作流(改代码 OK,执行脚本要问) |
|
||||
| `bypass` | 完全自动,不问(谨慎使用)| 内部测试、自动化流程 |
|
||||
| `dontAsk` (aka `yolo`) | 拒绝所有非低风险操作 | 沙箱环境,最安全 |
|
||||
| `plan` | 特殊模式:进入计划编辑阶段 | 用户审核计划后再执行 |
|
||||
|
||||
用户在启动时选择(环境变量或配置文件):
|
||||
```bash
|
||||
# 每次都问
|
||||
claude-code --permission-mode default
|
||||
|
||||
# 自动审批
|
||||
claude-code --permission-mode auto
|
||||
|
||||
# 最安全
|
||||
claude-code --permission-mode dontAsk
|
||||
```
|
||||
|
||||
相关文件:`utils/permissions/PermissionMode.ts`
|
||||
|
||||
---
|
||||
|
||||
## Permission 决策 Pipeline
|
||||
|
||||
当 agent 试图执行一个 tool 时,系统会问:"我应该允许吗?"
|
||||
|
||||
```
|
||||
Agent 调用: BashTool { command: "npm test" }
|
||||
│
|
||||
├─ [1] Mode 检查
|
||||
│ if mode == "bypass" → ✓ 允许,返回
|
||||
│ if mode == "dontAsk" → ✗ 拒绝 Bash,返回
|
||||
│
|
||||
├─ [2] 规则匹配
|
||||
│ 检查用户配置的规则列表
|
||||
│ 如: "Bash(git *)" → 允许 git 命令
|
||||
│ "Bash(npm *)" → 允许 npm 命令
|
||||
│ "Bash(*)" → 允许任意 bash (太宽)
|
||||
│
|
||||
│ 我们的命令 "npm test" 匹配 "npm *" → ✓
|
||||
│
|
||||
├─ [3] 风险分类
|
||||
│ rule match → LOW 风险
|
||||
│ 如果没匹配到规则,分类器决定风险等级
|
||||
│
|
||||
├─ [4] YOLO 分类器
|
||||
│ 对于 MEDIUM 风险的操作:
|
||||
│ - 如果被模型认为"明显安全",自动批准
|
||||
│ - 否则需要用户确认
|
||||
│
|
||||
└─ [5] 最后手段:提示用户
|
||||
用户选择: ✓ 允许 / ✗ 拒绝 / ⚠️ 改规则
|
||||
```
|
||||
|
||||
相关文件:`utils/permissions/permissions.ts` (52 KB,核心决策逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 规则系统
|
||||
|
||||
用户可以配置规则,告诉 agent 什么是自动允许的。规则语法:
|
||||
|
||||
```
|
||||
Bash(git *) # 允许任何 git 命令
|
||||
Bash(npm install) # 只允许 npm install,不允许 npm test
|
||||
FileEdit(src/*.ts) # 允许编辑 src/ 下的 .ts 文件
|
||||
FileRead(/*) # 允许读任何文件
|
||||
```
|
||||
|
||||
规则支持 **glob 模式**(`*` 通配符)。
|
||||
|
||||
### 规则分层
|
||||
|
||||
规则可以来自多个地方(优先级从高到低):
|
||||
|
||||
1. **CLI 参数**:`--approve-file "FileEdit(src/*.ts)"`
|
||||
2. **项目配置**:`.claude/settings.json` 中的 `approvals`
|
||||
3. **用户设置**:`~/.claude/settings.json`
|
||||
4. **系统默认**:Claude Code 内置的基础规则
|
||||
|
||||
下层规则会被上层规则覆盖。
|
||||
|
||||
相关文件:`utils/permissions/permissionRuleParser.ts`, `utils/permissions/permissionsLoader.ts`
|
||||
|
||||
---
|
||||
|
||||
## YOLO 分类器:ML 自动审批
|
||||
|
||||
对于**规则未覆盖的操作**,system 用 YOLO 分类器("You Only Live Once")决定:
|
||||
|
||||
```
|
||||
输入:
|
||||
tool_name: "BashTool"
|
||||
command: "npm test"
|
||||
context: 当前任务是"修复 bug"
|
||||
|
||||
分类器说: "这个操作很像是开发者会做的正常事,批准"
|
||||
|
||||
结论: 自动允许,不问用户
|
||||
```
|
||||
|
||||
分类器基于 ML 训练,考虑:
|
||||
- Tool 类型(BashTool 比 FileReadTool 高风险)
|
||||
- 具体命令(`npm test` 比 `rm -rf` 安全)
|
||||
- 当前上下文("修复 bug" vs "浏览网页")
|
||||
|
||||
相关文件:`utils/permissions/yoloClassifier.ts` (52 KB)
|
||||
|
||||
### 分类器的风险等级
|
||||
|
||||
```
|
||||
LOW
|
||||
→ 自动允许(不问用户)
|
||||
→ 例:FileReadTool, GlobTool, WebFetchTool
|
||||
|
||||
MEDIUM
|
||||
→ 取决于 mode(auto mode 用分类器,default mode 问用户)
|
||||
→ 例:FileEditTool, BashTool (git 命令)
|
||||
|
||||
HIGH
|
||||
→ 一般不自动允许(需要用户确认)
|
||||
→ 例:BashTool (rm -rf), FileWriteTool (覆盖重要文件)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Denial Tracking:Circuit Breaker
|
||||
|
||||
如果用户连续拒绝 agent 多次,system 会自动**降级权限模式**:
|
||||
|
||||
```
|
||||
用户拒绝:
|
||||
1. "BashTool" ← Denial #1
|
||||
2. "FileEditTool" ← Denial #2
|
||||
3. "BashTool" ← Denial #3
|
||||
|
||||
连续 3 次拒绝触发!
|
||||
↓
|
||||
系统切换到 "default" mode,后续所有操作都要问用户
|
||||
|
||||
这样防止:
|
||||
- Agent 持续尝试被拒的操作
|
||||
- 用户一直看到提示烦不胜烦
|
||||
```
|
||||
|
||||
同时,系统还追踪**总拒绝数**:
|
||||
|
||||
```
|
||||
同一 session 中总共拒绝了 20 次
|
||||
↓
|
||||
系统记录日志:"这个 session 决策质量差,建议用户审视任务"
|
||||
```
|
||||
|
||||
相关文件:`utils/permissions/denialTracking.ts`
|
||||
|
||||
---
|
||||
|
||||
## 路径安全:防止路径遍历攻击
|
||||
|
||||
即使规则写得再好,攻击者也可能用**特殊编码**绕过:
|
||||
|
||||
```
|
||||
规则: FileEdit(src/*.ts) ← 只允许 src/ 目录
|
||||
|
||||
攻击尝试:
|
||||
path: "src/../../../etc/passwd" ← 试图逃逸
|
||||
path: "src%2f..%2f..%2fetc%2fpasswd" ← URL 编码
|
||||
path: "src/\u202e../../../etc/passwd" ← Unicode 隐藏字符
|
||||
```
|
||||
|
||||
系统有多层防御:
|
||||
|
||||
1. **Path Normalization**:
|
||||
```
|
||||
"src/../../../etc/passwd"
|
||||
→ 规范化
|
||||
→ "/etc/passwd"
|
||||
→ 检查是否在 src/ 内? 否 → 拒绝
|
||||
```
|
||||
|
||||
2. **URL Decoding**:
|
||||
```
|
||||
"src%2f..%2f..%2fetc"
|
||||
→ 解码
|
||||
→ "src/../../etc"
|
||||
→ 规范化 → 拒绝
|
||||
```
|
||||
|
||||
3. **Unicode Normalization**:
|
||||
```
|
||||
Unicode 隐藏字符也会被清理
|
||||
```
|
||||
|
||||
相关文件:`utils/permissions/pathValidation.ts` (62 KB)
|
||||
|
||||
---
|
||||
|
||||
## Protected Files:黑名单
|
||||
|
||||
某些文件**永远不会被自动编辑**:
|
||||
|
||||
```
|
||||
.gitconfig ← Git 配置,修改可能破坏工作流
|
||||
.bashrc, .zshrc ← Shell 配置,修改可能卡住终端
|
||||
.mcp.json ← MCP 配置,影响工具可用性
|
||||
.claude.json ← Claude Code 自己的配置
|
||||
```
|
||||
|
||||
这些文件即使通过了权限检查,也会再次提示用户确认。
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么需要 ML 分类器?
|
||||
|
||||
如果只用规则:
|
||||
```
|
||||
允许所有 Bash 命令?
|
||||
→ 太宽,不安全(用户可能 rm -rf)
|
||||
|
||||
只允许 git/npm 命令?
|
||||
→ 太窄,不便利(sudo docker pull 被拒)
|
||||
```
|
||||
|
||||
ML 分类器的优势:
|
||||
```
|
||||
可以说"这个命令看起来是在开发工作流的一部分,
|
||||
而不是破坏性操作,所以允许"
|
||||
|
||||
如果出错(classify 有假阴性),系统还有:
|
||||
- Denial tracking(连续拒绝降级)
|
||||
- Protected files(黑名单)
|
||||
- Circuit breaker
|
||||
```
|
||||
|
||||
所以 ML 不是唯一防线,而是**多层防御中的一层**。
|
||||
|
||||
### 为什么规则支持 Glob 而不是正则表达式?
|
||||
|
||||
Glob:`src/*.ts` → 简单直观
|
||||
正则:`^src/[^/]+\.ts$` → 复杂,容易写错
|
||||
|
||||
目标用户是**工程师,但不是安全专家**。Glob 的错误率更低。
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Permission mode 是全局的,改一次就生效?"
|
||||
|
||||
实际:可以在 CLI 参数中针对单个命令覆盖:
|
||||
```bash
|
||||
claude-code --permission-mode auto /commit # 这个 /commit 用 auto mode
|
||||
claude-code --permission-mode default /config # 这个 /config 用 default mode
|
||||
```
|
||||
|
||||
**误解 2**:"YOLO 分类器是完美的?"
|
||||
|
||||
实际:分类器是 **best-effort**,有假阳性和假阴性。假阴性(该拒的通过了)会被 denial tracking 捕捉到。
|
||||
|
||||
**误解 3**:"Protected files 名单是硬编码的?"
|
||||
|
||||
实际:可以通过配置扩展:
|
||||
```json
|
||||
{
|
||||
"protectedFiles": [".gitconfig", ".bashrc", ".my-precious-file"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **Permission mode**:`default`(每次问), `auto`(ML 自动), `acceptEdits`, `bypass`, `dontAsk`
|
||||
2. **决策 pipeline**:Mode → Rules → Risk Classification → YOLO → User Prompt
|
||||
3. **规则系统**:Glob 模式,分层来源(CLI > project > user > default)
|
||||
4. **YOLO 分类器**:ML 自动批准 MEDIUM 风险操作,不需要用户每次确认
|
||||
5. **Denial tracking**:连续 3 次拒绝自动降级权限,防止烦人的重复提示
|
||||
6. **路径安全**:多层防御(normalization, URL decode, Unicode clean)
|
||||
7. **Protected files**:黑名单,永远要再次确认
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `utils/permissions/permissions.ts`:完整决策逻辑
|
||||
- `utils/permissions/yoloClassifier.ts`:分类器实现
|
||||
- `utils/permissions/pathValidation.ts`:路径安全
|
||||
- `utils/permissions/denialTracking.ts`:Denial tracking
|
||||
- `utils/permissions/PermissionMode.ts`:Mode 定义
|
||||
|
||||
下一步:了解**Context 和 Memory 系统**,看看当 context window 快满时会发生什么。
|
||||
416
docs/agentic-design/05-context-and-memory.md
Normal file
416
docs/agentic-design/05-context-and-memory.md
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
# Context 和 Memory:有限的脑容量
|
||||
|
||||
## 为什么这很重要?
|
||||
|
||||
Claude 的 context window(比如 200K tokens)是**有限的**。一个长时间运行的 agent task:
|
||||
|
||||
```
|
||||
第 1 轮:用户输入(200 tokens)+ 响应(1000 tokens)
|
||||
第 2 轮:新输入(300 tokens)+ 响应(2000 tokens)
|
||||
...
|
||||
第 100 轮:?
|
||||
|
||||
总 tokens: 200 + 300 + 400 + ... + 整个历史
|
||||
|
||||
在第 50 轮时:messages 数组有 100 条消息,超过 context window 的 80%
|
||||
```
|
||||
|
||||
一旦超限,系统必须做些什么。Claude Code 的答案是:
|
||||
|
||||
1. **System prompt 优化**:区分静态和动态部分,利用 prompt cache
|
||||
2. **主动 compaction**:在超限前压缩历史
|
||||
3. **长期 memory**:用户的持久化知识库
|
||||
|
||||
---
|
||||
|
||||
## System Prompt 架构
|
||||
|
||||
不是简单的一个大 string,而是**模块化的**:
|
||||
|
||||
```typescript
|
||||
const systemPrompt = [
|
||||
// 第 1 部分:静态,会被缓存
|
||||
SYSTEM_PROMPT_PREFIX, // 关于 Claude Code 的基础指导
|
||||
TOOL_DESCRIPTIONS, // 所有 tool 的描述
|
||||
SYSTEM_PROMPT_SAFETY, // 安全指导
|
||||
|
||||
// 缓存边界 ← 这之后的部分会变化,不会被缓存
|
||||
"SYSTEM_PROMPT_DYNAMIC_BOUNDARY",
|
||||
|
||||
// 第 2 部分:动态,每个 session 不同
|
||||
USER_MEMORY, // 用户的个人知识库
|
||||
RECENT_CONTEXT, // 最近的几轮对话(总结)
|
||||
CURRENT_TASK, // 当前任务的上下文
|
||||
]
|
||||
```
|
||||
|
||||
**Cache boundary marker** 的妙处:
|
||||
|
||||
```
|
||||
初始化时: │首个请求:
|
||||
cache_key = MD5(prefix) │所有文本
|
||||
缓存大小:10 KB │2000 tokens (40 KB)
|
||||
│
|
||||
│缓存命中 10 KB
|
||||
│处理 30 KB 新文本
|
||||
│总成本:40 KB 请求
|
||||
│
|
||||
同一 session 中再次使用: │再次请求:
|
||||
cache 仍有效 │新文本 1000 tokens (20 KB)
|
||||
直接跳过了 10 KB 的重复传输 │缓存 hit(10 KB)
|
||||
成本:20 KB │总成本:20 KB 请求(节省 10 KB)
|
||||
```
|
||||
|
||||
所以,通过把 system prompt 分为静态和动态两部分,我们:
|
||||
- 减少了重复 token 消耗
|
||||
- 加快了推理速度(缓存的部分直接使用)
|
||||
- 提高了吞吐量(多个请求共享缓存)
|
||||
|
||||
相关文件:`utils/messages/systemInit.ts`, `context.ts`
|
||||
|
||||
---
|
||||
|
||||
## Token Budget 生命周期
|
||||
|
||||
### 阶段 1:初始化
|
||||
|
||||
```typescript
|
||||
const CONTEXT_WINDOW = 200_000 // 比如 200K tokens
|
||||
const SYSTEM_PROMPT_TOKENS = 5_000
|
||||
const RESERVED_BUFFER = 10_000 // 总是留出来,防止意外超限
|
||||
|
||||
available = CONTEXT_WINDOW - SYSTEM_PROMPT_TOKENS - RESERVED_BUFFER
|
||||
= 185_000 tokens // 工作预算
|
||||
```
|
||||
|
||||
### 阶段 2:追踪使用
|
||||
|
||||
每一轮 API 调用后,系统获得实际使用数据:
|
||||
|
||||
```typescript
|
||||
response = await claude.messages.create({...})
|
||||
|
||||
remaining = available - response.usage.input_tokens - response.usage.output_tokens
|
||||
|
||||
if (remaining < available * 0.2) { // 还剩 20% 以下?
|
||||
console.warn("Token budget 即将耗尽,触发 compaction")
|
||||
}
|
||||
|
||||
if (remaining < 10_000) { // 紧急状态?
|
||||
console.error("无法继续,context 严重超限")
|
||||
return ERROR_OUT_OF_TOKENS
|
||||
}
|
||||
```
|
||||
|
||||
### 阶段 3:警告和恢复
|
||||
|
||||
```
|
||||
Threshold 1(80% 已用):显示警告,继续
|
||||
↓
|
||||
Agent 执行更多 tool ← 看不到警告,继续工作
|
||||
↓
|
||||
Threshold 2(90% 已用):触发 compaction
|
||||
← 在后台自动压缩,不中断 agent 工作
|
||||
↓
|
||||
Threshold 3(99% 已用):停止接受新请求
|
||||
← 返回错误,等待用户决定
|
||||
```
|
||||
|
||||
相关文件:`query/tokenBudget.ts`
|
||||
|
||||
---
|
||||
|
||||
## Compaction:自动历史压缩
|
||||
|
||||
当 token 预算吃紧时,系统自动压缩历史。这是一个 4 步流程:
|
||||
|
||||
### 第 1 步:分析历史
|
||||
|
||||
```typescript
|
||||
// 扫描所有消息,找出"能压缩的部分"
|
||||
|
||||
messages = [
|
||||
system_prompt, // 不能压缩
|
||||
message_1, // 用户说:"帮我读个文件" → 不压缩(用户消息)
|
||||
message_2, // Claude 说:"我读了,代码如下..." → 能压缩(输出很长)
|
||||
message_3, // 用户说:"现在修复 bug" → 不压缩
|
||||
message_4, // Claude 说:"修复完,改了 5 处..." → 能压缩
|
||||
...
|
||||
]
|
||||
|
||||
优先级:越早的消息优先级越高(假设最早的对话不再相关)
|
||||
```
|
||||
|
||||
### 第 2 步:优先排序
|
||||
|
||||
```
|
||||
对能压缩的消息排序:
|
||||
1. 最早的长消息(4 messages ago)
|
||||
2. 次早的长消息(6 messages ago)
|
||||
3. ...
|
||||
|
||||
实际压缩的可能是:
|
||||
messages 1-10(最早的 10 条)被替换为 1 条 summary
|
||||
```
|
||||
|
||||
### 第 3 步:调用 Claude 做总结
|
||||
|
||||
```typescript
|
||||
const summary = await claude.messages.create({
|
||||
messages: [
|
||||
{ role: "user", content: "Summarize this conversation in 200 lines max:\n" + messagesText }
|
||||
],
|
||||
system: "You are a memory consolidation assistant..."
|
||||
})
|
||||
|
||||
// 返回:
|
||||
// "用户让我读了 package.json 和 tsconfig.json,发现项目是 TypeScript monorepo。"
|
||||
```
|
||||
|
||||
### 第 4 步:替换
|
||||
|
||||
```typescript
|
||||
// 替换前:
|
||||
messages = [msg_1, msg_2, ..., msg_100] // 100 条消息
|
||||
|
||||
// 替换后:
|
||||
messages = [msg_summary, msg_51, msg_52, ..., msg_100]
|
||||
// ^被压缩成1条 ^保留后面的消息(近期的)
|
||||
```
|
||||
|
||||
结果:从 100 条消息减少到 51 条,token 预算恢复。
|
||||
|
||||
相关文件:`query.ts` 中的 `compactMessages()` 函数,大约 200 行
|
||||
|
||||
---
|
||||
|
||||
## Memory 系统:长期知识库
|
||||
|
||||
Compaction 是**临时的**(压缩 session 内的历史)。但如果用户有**多个 session**,同一信息会被重复压缩。
|
||||
|
||||
解决方案:**持久化 memory**。
|
||||
|
||||
### Memory 文件结构
|
||||
|
||||
```
|
||||
~/.claude/memory/
|
||||
├── MEMORY.md ← 用户的长期知识库(最多 200 行)
|
||||
├── projects/
|
||||
│ └── claude-code.md ← 项目特定的知识
|
||||
├── patterns/
|
||||
│ └── async-patterns.md ← 学到的模式
|
||||
└── people/
|
||||
└── team.md ← 关于人的信息
|
||||
```
|
||||
|
||||
**MEMORY.md** 的内容示例:
|
||||
|
||||
```markdown
|
||||
# 我的知识库
|
||||
|
||||
## 工作偏好
|
||||
- 不喜欢在 main 分支上直接提交
|
||||
- 总是用 TypeScript,避免 JavaScript
|
||||
|
||||
## 项目信息
|
||||
- Claude Code 仓库: ~/projects/claude-code
|
||||
- 技术栈: TypeScript + React + Ink
|
||||
|
||||
## 编码习惯
|
||||
- 函数 <100 行为最佳
|
||||
- 避免 deep nesting,最多 3 层
|
||||
- 总是加 error handling
|
||||
```
|
||||
|
||||
### Auto-Dream:后台内存巩固
|
||||
|
||||
不是用户手动维护 MEMORY.md,而是系统**自动**从 session 中提取和更新。
|
||||
|
||||
过程("dream"):
|
||||
|
||||
```
|
||||
触发条件(三门齐全):
|
||||
1. 时间门:距离上次 dream ≥ 24 小时?
|
||||
2. 会话门:距离上次 dream,产生了 ≥ 5 个新 session?
|
||||
3. Lock 门:能获得 consolidation lock(防并发)?
|
||||
|
||||
都满足 → 执行 dream:
|
||||
|
||||
[1] ORIENT
|
||||
读取用户的 MEMORY.md
|
||||
扫描最近 N 个 session 的 transcripts
|
||||
|
||||
[2] GATHER
|
||||
提取新的学习点:
|
||||
- "用户在这个 session 中学到了异步 Rust"
|
||||
- "发现项目从 4.1 升级到了 4.2"
|
||||
- "优化了 CI/CD,速度提升 30%"
|
||||
|
||||
[3] CONSOLIDATE
|
||||
合并到 MEMORY.md:
|
||||
- 如果已经有同类笔记,合并
|
||||
- 如果是新的,添加
|
||||
- 保持在 200 行以内(做优先级选择,删除过时的)
|
||||
|
||||
[4] PRUNE & INDEX
|
||||
- 删除 24 小时内不用的 session transcript
|
||||
- 更新搜索索引(便于后续 memory 查询)
|
||||
```
|
||||
|
||||
相关文件:
|
||||
- `services/autoDream/autoDream.ts`:主逻辑(~11 KB)
|
||||
- `services/autoDream/consolidationPrompt.ts`:告诉 Claude 怎么总结
|
||||
- `memdir/memdir.ts`:Memory 目录管理
|
||||
|
||||
### 为什么自动化很关键?
|
||||
|
||||
```
|
||||
手动维护:
|
||||
用户要记得更新 MEMORY.md
|
||||
→ 大部分人会忘记
|
||||
→ Memory 逐渐变陈旧
|
||||
|
||||
自动化:
|
||||
后台每 24 小时自动 consolidate
|
||||
→ 用户无感
|
||||
→ Memory 总是最新的
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Context 使用的优化层级
|
||||
|
||||
当 context 变紧时,系统依次采取行动:
|
||||
|
||||
```
|
||||
Tier 1(还有 70% token)
|
||||
✓ 正常工作
|
||||
✗ 触发自动 compaction
|
||||
|
||||
Tier 2(还有 20% token)
|
||||
✓ 继续工作(使用压缩后的历史)
|
||||
✗ 不接受新的大型工具输出(如读 10 MB 文件)
|
||||
|
||||
Tier 3(还有 10% token)
|
||||
✓ 只允许低 token 成本的操作
|
||||
✗ 禁止高 token 成本的操作
|
||||
|
||||
Tier 4(还有 < 5% token)
|
||||
✗ 停止工作,返回错误
|
||||
→ 用户可以:
|
||||
a) 启动新 session(重置 token 预算)
|
||||
b) 手动编辑 MEMORY.md,删除过时信息
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么区分静态和动态 system prompt?
|
||||
|
||||
不区分(一大块):
|
||||
```
|
||||
发第 1 个请求:发 5000 tokens 的 system prompt + 输入
|
||||
发第 2 个请求:又发 5000 tokens 的 system prompt + 输入
|
||||
...
|
||||
浪费了大量 token 在重复发送
|
||||
```
|
||||
|
||||
区分(cache boundary):
|
||||
```
|
||||
发第 1 个请求:发 5000 tokens(静态)+ 1000 tokens(动态)
|
||||
API 缓存这 5000 tokens
|
||||
发第 2 个请求:直接用缓存的 5000 tokens + 1000 tokens(新的动态)
|
||||
节省了 5000 tokens!
|
||||
```
|
||||
|
||||
如果 session 很长(100 次请求),节省 = 5000 * 99 = 495K tokens!
|
||||
|
||||
### 为什么 Compaction 触发阈值是 20%,而不是 1%?
|
||||
|
||||
等到 1% 时才 compact:
|
||||
```
|
||||
已用 99%,只有 1% 剩余(~2000 tokens)
|
||||
这时启动 compaction,自己就需要消耗大量 tokens(调用 Claude 做总结)
|
||||
可能没有足够 tokens 来完成 compaction
|
||||
→ 失败,系统崩溃
|
||||
```
|
||||
|
||||
提前到 20% 时 compact:
|
||||
```
|
||||
已用 80%,还有 20% 剩余(~40K tokens)
|
||||
这时启动 compaction,有充足 tokens 来做总结
|
||||
完成后,token 预算恢复到 50%
|
||||
→ 继续工作,很多时间都有富足的 token 可用
|
||||
```
|
||||
|
||||
权衡:早点 compact 意味着多付出一些 token(压缩的成本),但换来系统稳定性。
|
||||
|
||||
### 为什么用后台 dream 而不是 inline compaction?
|
||||
|
||||
Inline(同步):
|
||||
```
|
||||
在 query loop 中调用 compaction
|
||||
↓
|
||||
等待 compaction 完成(5-10 秒)
|
||||
↓
|
||||
用户看到卡顿
|
||||
```
|
||||
|
||||
后台 dream:
|
||||
```
|
||||
query loop 继续运行
|
||||
↓
|
||||
在空闲时或定时触发 dream(24 小时一次)
|
||||
↓
|
||||
用户感觉不到延迟
|
||||
```
|
||||
|
||||
代价:增加系统复杂性(需要处理并发)、需要文件锁(防同时 dream)。但值得,因为用户体验好得多。
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Context window 满了就完蛋?"
|
||||
|
||||
实际:有多层缓冲和恢复机制。系统会逐步:
|
||||
1. 显示警告
|
||||
2. 触发 compaction
|
||||
3. 拒绝新操作
|
||||
4. 要求用户决定
|
||||
|
||||
完全"卡住"很少发生。
|
||||
|
||||
**误解 2**:"Compaction 会丢失所有细节?"
|
||||
|
||||
实际:Compaction 由 Claude 做,所以**关键信息被保留**。丢失的是:
|
||||
- 冗余的解释
|
||||
- 尝试失败的细节(对后续不相关)
|
||||
- 已解决的问题的讨论过程
|
||||
|
||||
**误解 3**:"MEMORY.md 会无限增长?"
|
||||
|
||||
实际:维持在 ~200 行(可配置),通过 dream 的 prune 阶段定期清理过时信息。
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **System prompt 分两部分**:静态(可缓存)+ 动态(每次变化)
|
||||
2. **Token budget 主动追踪**:到达 80% 时触发 compaction
|
||||
3. **Compaction 自动压缩历史**:用 Claude 总结 N 条消息为 1 条
|
||||
4. **Memory 系统持久化知识**:MEMORY.md 最多 200 行,用户的长期知识库
|
||||
5. **Auto-dream 后台巩固**:每 24 小时自动更新 MEMORY.md,无需用户介入
|
||||
6. **优化层级**:70% → 20% → 10% → 5% → 停止,每个阈值有对应策略
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `query/tokenBudget.ts`:Token 追踪逻辑
|
||||
- `services/autoDream/autoDream.ts`:Dream 实现
|
||||
- `memdir/memdir.ts`:Memory 目录管理
|
||||
- `utils/messages/systemInit.ts`:System prompt 组装
|
||||
|
||||
下一步:了解**Feature Gating 系统**,看看 Claude Code 怎么区分内部功能和公开功能。
|
||||
357
docs/agentic-design/06-feature-gating.md
Normal file
357
docs/agentic-design/06-feature-gating.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# Feature Gating:内部功能和公开功能的隔离
|
||||
|
||||
## 为什么需要 Feature Gating?
|
||||
|
||||
Claude Code 是一个复杂系统,有很多功能:
|
||||
|
||||
- 一些已发布、稳定、所有用户可用
|
||||
- 一些还在测试、只给内部用户("ant" 用户)
|
||||
- 一些只给特定高级用户(Max/Pro)
|
||||
- 一些通过实验 A/B 测试
|
||||
|
||||
如果不做隔离,代码会变得一团糟:
|
||||
|
||||
```typescript
|
||||
// 不好的做法
|
||||
if (user.isInternal) {
|
||||
// KAIROS 模式逻辑
|
||||
} else if (user.isMax) {
|
||||
// 高级功能
|
||||
} else if (featureFlags.has('experimental_search')) {
|
||||
// A/B 测试
|
||||
} else {
|
||||
// 默认行为
|
||||
}
|
||||
// ... 这样嵌套太深,难以维护
|
||||
```
|
||||
|
||||
Claude Code 的解决方案:**编译期和运行期双层 gating**。
|
||||
|
||||
---
|
||||
|
||||
## 编译期 Gating:Bun 的 `feature()` API
|
||||
|
||||
Bun(JavaScript 运行时)提供了一个编译期特性检测 API:
|
||||
|
||||
```typescript
|
||||
if (feature("KAIROS")) {
|
||||
// 这段代码只在 KAIROS 构建中出现
|
||||
// 其他构建完全不包含这代码
|
||||
} else {
|
||||
// 默认行为
|
||||
}
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
|
||||
```
|
||||
源代码:
|
||||
if (feature("KAIROS")) {
|
||||
launchKAIROS()
|
||||
} else {
|
||||
launchDefault()
|
||||
}
|
||||
↓ Bun 编译(--feature KAIROS)
|
||||
if (true) {
|
||||
launchKAIROS() // 只保留这段
|
||||
} else {
|
||||
// 被 dead code elimination 删除
|
||||
}
|
||||
↓ 输出的二进制
|
||||
launchKAIROS() // 这是最终的可执行代码
|
||||
```
|
||||
|
||||
### 优势
|
||||
|
||||
1. **二进制体积小**:内部功能完全不包含在公开二进制中
|
||||
2. **安全**:用户不能"解锁"内部功能(不存在于二进制)
|
||||
3. **性能**:不需要运行时检查这些条件
|
||||
|
||||
相关代码散布在整个 codebase 中,比如 `entrypoints/cli.tsx`, `bootstrap/state.ts`。
|
||||
|
||||
---
|
||||
|
||||
## Compile-Time Feature 列表
|
||||
|
||||
Claude Code 在构建时支持这些 feature flag:
|
||||
|
||||
| Flag | 启用时 | 作用 |
|
||||
|------|--------|------|
|
||||
| `KAIROS` | 启动 KAIROS mode(always-on Claude) | 全天候 assistant |
|
||||
| `PROACTIVE` | 同上(别名) | 同上 |
|
||||
| `BRIDGE_MODE` | 启用 claude.ai 远程控制 | 在 Web UI 中控制 CLI |
|
||||
| `VOICE_MODE` | 启用语音输入 | 用话筒说指令 |
|
||||
| `DAEMON` | 启用 daemon 模式 | 后台运行的 Claude |
|
||||
| `COORDINATOR_MODE` | 启用 coordinator 多 agent | 复杂任务分解 |
|
||||
| `BUDDY` | 启用 Tamagotchi 伴侣系统 | 养一个宠物 AI |
|
||||
| `WORKFLOW_SCRIPTS` | 启用工作流脚本 | 自动化重复任务 |
|
||||
| `NATIVE_CLIENT_ATTESTATION` | 启用本地证明 | 设备信任 |
|
||||
| `TRANSCRIPT_CLASSIFIER` | 启用 AFK 自动模式 | 离开电脑时自动工作 |
|
||||
| `HISTORY_SNIP` | 启用历史压缩优化 | Context 更高效 |
|
||||
| `EXPERIMENTAL_SKILL_SEARCH` | 启用技能搜索(实验) | 发现新 skill 命令 |
|
||||
| `ABLATION_BASELINE` | 科学研究 baseline | 论文用 |
|
||||
|
||||
默认只编译:`KAIROS=0, BRIDGE_MODE=0, ...`(全是 0,最小二进制)
|
||||
|
||||
内部构建时:编译脚本设置 `--feature KAIROS=1 --feature BRIDGE_MODE=1 ...`
|
||||
|
||||
相关文件:构建脚本(如 `scripts/build.sh`,如果存在),或 `bunfig.toml`
|
||||
|
||||
---
|
||||
|
||||
## 运行期 Gating:GrowthBook Feature Flags
|
||||
|
||||
编译期 feature 决定"可能性",运行期 flags 决定"激活"。
|
||||
|
||||
系统启动时,从 GrowthBook(特性管理平台)获取动态配置:
|
||||
|
||||
```typescript
|
||||
// 启动时
|
||||
const flags = await growthbook.getFeatures({
|
||||
userId: currentUser.id,
|
||||
organization: currentUser.org
|
||||
})
|
||||
|
||||
// 现在可以做运行期决策
|
||||
if (flags.has('tengu_scratch')) {
|
||||
// 启用共享 scratchpad(coordinator 模式)
|
||||
enableScratchpad()
|
||||
}
|
||||
|
||||
if (flags.has('tengu_amber_flint')) {
|
||||
// 启用 in-process teammate swarms
|
||||
enableTeammates()
|
||||
}
|
||||
|
||||
if (flags.has('tengu_penguins_off')) {
|
||||
// 禁用某个实验
|
||||
disableExperiment()
|
||||
}
|
||||
```
|
||||
|
||||
### Flag 命名约定
|
||||
|
||||
Runtime flags 以 `tengu_` 开头("tengu" 是 Claude Code 的内部代号):
|
||||
|
||||
```
|
||||
tengu_scratch # 共享 scratchpad(为 coordinator)
|
||||
tengu_amber_flint # In-process teammate
|
||||
tengu_onyx_plover # 某个实验
|
||||
tengu_penguins_off # 禁用什么功能
|
||||
...
|
||||
```
|
||||
|
||||
这样容易区分:
|
||||
- `KAIROS`(编译期)vs `tengu_kairos_v2`(运行期)
|
||||
|
||||
相关文件:`bootstrap/state.ts`(初始化),`services/analytics/`(GrowthBook 集成)
|
||||
|
||||
---
|
||||
|
||||
## 两层系统的协作
|
||||
|
||||
### 场景 1:Compile-Time Only(最常见)
|
||||
|
||||
```
|
||||
新功能 KAIROS(always-on Claude)
|
||||
↓ 编译
|
||||
├─ 构建 ant(内部):--feature KAIROS=1
|
||||
│ → KAIROS 代码被编译进去
|
||||
│
|
||||
└─ 构建 public(公开):--feature KAIROS=0
|
||||
→ KAIROS 代码被 dead-code-elimination 删除
|
||||
→ 二进制不含任何痕迹
|
||||
```
|
||||
|
||||
### 场景 2:Compile-Time + Runtime(用于 A/B 测试)
|
||||
|
||||
```
|
||||
新的 memory 系统(已编译进去)
|
||||
↓ 运行时
|
||||
├─ 用户在 tengu_new_memory=true 组(50%)
|
||||
│ → 使用新系统
|
||||
│
|
||||
└─ 用户在 tengu_new_memory=false 组(50%)
|
||||
→ 使用旧系统
|
||||
→ 收集对比数据
|
||||
```
|
||||
|
||||
### 场景 3:运行期特定用户
|
||||
|
||||
```
|
||||
高级功能(编译期编译进去)
|
||||
↓ 运行时
|
||||
├─ Max 订阅用户 → 启用计算机使用
|
||||
├─ Pro 订阅用户 → 启用高级 memory
|
||||
└─ 免费用户 → 基础功能
|
||||
```
|
||||
|
||||
相关文件:`services/policyLimits/`(根据订阅级别应用 policy)
|
||||
|
||||
---
|
||||
|
||||
## 对 System Prompt 的影响
|
||||
|
||||
Feature gating 会改变 system prompt 的内容:
|
||||
|
||||
```typescript
|
||||
const systemPrompt = [
|
||||
BASE_PROMPT,
|
||||
|
||||
// 只有在编译时启用 BRIDGE_MODE 时才包含
|
||||
feature("BRIDGE_MODE") ? BRIDGE_MODE_INSTRUCTIONS : "",
|
||||
|
||||
// 运行时检查:如果用户有 tengu_coordinator flag
|
||||
flags.get("tengu_coordinator") ? COORDINATOR_INSTRUCTIONS : "",
|
||||
|
||||
// 运行时 API beta 特性
|
||||
BETAS_NEGOTIATED_WITH_API, // 如 "interleaved_thinking"
|
||||
]
|
||||
```
|
||||
|
||||
这也影响 **prompt cache**:
|
||||
|
||||
```
|
||||
缓存键 = MD5(system_prompt_prefix)
|
||||
|
||||
如果 flag 变化,cache key 变化,旧 cache 失效
|
||||
```
|
||||
|
||||
所以,高频变化的 runtime flag 应该放在 cache boundary 之后(动态部分),而不是之前(静态部分)。
|
||||
|
||||
---
|
||||
|
||||
## Betas 协商
|
||||
|
||||
Claude API 定期发布新特性(beta),Claude Code 需要:
|
||||
|
||||
1. 请求启用这些 beta
|
||||
2. 如果启用,在 system prompt 中告诉 Claude(这个 API 支持什么新功能)
|
||||
|
||||
例子:
|
||||
|
||||
```typescript
|
||||
// constants/betas.ts
|
||||
const BETAS_REQUESTED = [
|
||||
"interleaved-thinking", // 支持在回复中穿插思考
|
||||
"structured-outputs", // 支持返回结构化 JSON
|
||||
"context-1m", // 100 万 token context
|
||||
"web-search", // 网络搜索能力
|
||||
]
|
||||
|
||||
// API 返回确认
|
||||
const BETAS_ENABLED = [
|
||||
"interleaved-thinking", // ✓ 已启用
|
||||
// "structured-outputs", // ✗ 暂无权限
|
||||
"context-1m", // ✓ 已启用
|
||||
"web-search", // ✓ 已启用
|
||||
]
|
||||
|
||||
// System prompt 中加入这些信息
|
||||
You have access to the following beta features:
|
||||
- Interleaved thinking: You can output <thinking> blocks
|
||||
- Context 1M: You can use up to 1,000,000 tokens
|
||||
- Web search: You can call the WebSearchTool
|
||||
```
|
||||
|
||||
相关文件:`constants/betas.ts`
|
||||
|
||||
---
|
||||
|
||||
## Design Decision 专栏
|
||||
|
||||
### 为什么同时用编译期和运行期?
|
||||
|
||||
只用编译期:
|
||||
```
|
||||
缺点:无法做快速的 A/B 测试(需要重新编译)
|
||||
无法根据用户身份动态启用功能
|
||||
```
|
||||
|
||||
只用运行期:
|
||||
```
|
||||
缺点:内部功能可能被反编译/逆向工程
|
||||
内部代码暴露在公开二进制中
|
||||
启动时需要网络请求拿 flags(慢)
|
||||
```
|
||||
|
||||
两者结合:
|
||||
```
|
||||
编译期决定"可能性"(物理隔离内部代码)
|
||||
运行期决定"激活"(灵活的 A/B 测试)
|
||||
最安全、最灵活
|
||||
```
|
||||
|
||||
### 为什么 Bun 的 feature() 而不是其他工具?
|
||||
|
||||
Bun 的 feature() 是编译期指令,会进行 dead code elimination:
|
||||
|
||||
```
|
||||
其他工具(如 rollup 的条件编译):
|
||||
需要额外的 webpack 插件
|
||||
编译配置复杂
|
||||
|
||||
Bun:
|
||||
原生支持
|
||||
编译快(Bun 本身就快)
|
||||
输出体积最小
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见误解
|
||||
|
||||
**误解 1**:"Runtime flag 可以改变编译期行为?"
|
||||
|
||||
实际:不能。Runtime flag 只能在编译期已包含的代码中做选择。如果代码在编译时被 dead-code-elimination 删除了,运行时再想启用也没办法。
|
||||
|
||||
```typescript
|
||||
// 如果编译时 --feature KAIROS=0,这段代码被删除
|
||||
if (feature("KAIROS")) {
|
||||
launchKAIROS()
|
||||
}
|
||||
|
||||
// 运行时即使 growthbook.flags.get("enable_kairos") == true,也没用
|
||||
// 代码不存在于二进制中
|
||||
```
|
||||
|
||||
**误解 2**:"Feature flag 对性能有开销?"
|
||||
|
||||
实际:编译期 feature flag 没有开销(代码级别选择)。运行期 flag 有极小开销(map lookup),可忽略不计。
|
||||
|
||||
**误解 3**:"所有用户都能看到内部代码?"
|
||||
|
||||
实际:不能。公开二进制中编译时被删除的代码,用户看不到。只有 ant(内部)用户的二进制才包含这些代码。
|
||||
|
||||
---
|
||||
|
||||
## 关键要点
|
||||
|
||||
1. **编译期 feature**:Bun 的 `feature()` API,dead code elimination,对二进制体积和安全性的保障
|
||||
2. **运行期 flags**:GrowthBook,动态启用特性,支持 A/B 测试
|
||||
3. **两层协作**:编译决定可能性,运行时决定激活
|
||||
4. **System prompt 影响**:不同 flag 组合 → 不同 system prompt → 不同 cache key
|
||||
5. **API Beta**:协商启用新的 Claude API 特性,在 system prompt 中告诉 Claude
|
||||
|
||||
---
|
||||
|
||||
## 深入阅读
|
||||
|
||||
- `bootstrap/state.ts`:运行时 flag 初始化(56 KB)
|
||||
- `constants/betas.ts`:Beta feature 列表
|
||||
- `constants/system.ts` 和 `constants/systemPromptSections.ts`:System prompt 组装
|
||||
- `entrypoints/cli.tsx`:编译期条件编译示例
|
||||
|
||||
---
|
||||
|
||||
## 后记
|
||||
|
||||
这 6 篇文档覆盖了 Claude Code 最核心的 agentic 设计决策。还有很多其他话题(Bridge 模式、Voice 输入、Plugin 系统等),但这些是最重要的基础。
|
||||
|
||||
如果你想进一步了解,建议:
|
||||
1. 读完这 6 篇
|
||||
2. 打开 `query.ts` 和 `Tool.ts`,对照源代码
|
||||
3. 运行 Claude Code,用 `--verbose` flag 看内部日志
|
||||
4. 探索 `tools/AgentTool/` 的源代码
|
||||
|
||||
祝学习愉快!
|
||||
113
docs/agentic-design/README.md
Normal file
113
docs/agentic-design/README.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Claude Code Agentic 设计文档
|
||||
|
||||
> **目标读者**:有一定软件工程背景、希望通过真实项目理解 agentic 系统设计的学习者。
|
||||
> **建议用时**:约 2.5 小时
|
||||
> **阅读语言**:中文正文 + 英文技术术语
|
||||
|
||||
---
|
||||
|
||||
## 这个项目是什么?
|
||||
|
||||
[Claude Code](https://claude.ai/code) 是 Anthropic 开发的 AI 辅助编程 CLI 工具。它不是一个简单的"问答机器人",而是一个完整的 **agentic system**:
|
||||
|
||||
- 它可以自主执行多轮工具调用(读文件、改代码、运行命令)
|
||||
- 它可以 spawn 子 agent 并行处理复杂任务
|
||||
- 它有 permission 管控、memory 管理、context 压缩等完整的 agent 基础设施
|
||||
|
||||
这套文档通过分析 Claude Code 的源代码,提炼出其中最有价值的 agentic 设计选择,帮助你建立对真实 agent 系统的直觉。
|
||||
|
||||
---
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 用户 / IDE / SDK │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│ 输入:用户消息
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ CLI / Entrypoint Layer │
|
||||
│ entrypoints/cli.tsx │ entrypoints/sdk/ │ entrypoints/mcp/ │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Query Loop Engine │
|
||||
│ QueryEngine.ts ←→ query.ts ←→ query/ │
|
||||
│ (状态机: QUERY → TOOL_USE → RESULT → QUERY → ...) │
|
||||
└──────────────┬────────────────────────────────┬─────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────┐ ┌───────────────────────────────┐
|
||||
│ Claude API (Streaming) │ │ Tool Execution Layer │
|
||||
│ services/api/ │ │ services/tools/ │
|
||||
│ • token budget 追踪 │ │ • StreamingToolExecutor │
|
||||
│ • compaction 触发 │ │ • 并发控制 + sibling abort │
|
||||
└──────────────────────────┘ └──────────────┬────────────────┘
|
||||
│
|
||||
┌──────────────────┼──────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ File │ │ Bash / │ │ AgentTool │
|
||||
│ Tools │ │ Shell │ │ (子 agent) │
|
||||
└─────────┘ └──────────┘ └──────┬───────┘
|
||||
│
|
||||
┌────────────────────────┘
|
||||
│ spawn
|
||||
▼
|
||||
┌────────────────────────┐
|
||||
│ 子 Agent / Coordinator│
|
||||
│ coordinator/ │
|
||||
│ tasks/Local|Remote │
|
||||
└────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 横切关注点(Cross-cutting) │
|
||||
│ Permission System │ Context/Memory │ Feature Gating │
|
||||
│ utils/permissions/ │ memdir/ autoDream│ bootstrap/state.ts │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心设计哲学
|
||||
|
||||
**1. Tool 是 Agent 触碰世界的唯一手段**
|
||||
Agent 的所有副作用(写文件、执行命令、发消息)都必须经过 Tool 接口,这使得权限控制、审计日志、测试 mock 都可以在一个地方统一处理。
|
||||
|
||||
**2. Text is the protocol**
|
||||
Agent 之间通过 `<task-notification>` XML 消息通信,memory 以 Markdown 文件存储,permission rules 是 `Bash(git *)` 这样的字符串模式。用文本作为协议,意味着 AI 模型自己也可以读懂并调试这些通信内容。
|
||||
|
||||
**3. Failure modes are first-class**
|
||||
每个长时间运行的操作都有明确的 abort 路径,例如:连续 3 次 permission denial 就回退到手动提示;并行 tool 中一个失败会触发 sibling abort;compaction 失败有 rollback 保护。
|
||||
|
||||
**4. Context window 是最稀缺的资源**
|
||||
整个系统的大量设计决策(system prompt 的静态/动态分割、token budget 追踪、auto-dream 后台压缩)都围绕着"如何最大化利用有限的 context window"展开。
|
||||
|
||||
**5. 编译期与运行期特性隔离**
|
||||
内部功能通过 Bun 的 `feature()` API 在编译时消除,不出现在公开二进制文件中;运行时行为通过 GrowthBook runtime flags 动态控制。
|
||||
|
||||
---
|
||||
|
||||
## 建议阅读顺序
|
||||
|
||||
| 编号 | 文档 | 用时 | 核心问题 |
|
||||
|------|------|------|---------|
|
||||
| [00](./00-codebase-tour.md) | 代码库目录全景 | 30 min | "这个 repo 里有什么?" |
|
||||
| [01](./01-agent-loop.md) | 核心 Query Loop | 20 min | "Agent 是如何循环运作的?" |
|
||||
| [02](./02-tool-system.md) | Tool 系统 | 20 min | "Tool 是怎么被定义和执行的?" |
|
||||
| [03](./03-multi-agent-coordination.md) | 多 Agent 协调 | 25 min | "多个 agent 怎么协作?" |
|
||||
| [04](./04-permission-system.md) | Permission 系统 | 20 min | "Agent 怎么知道自己能做什么?" |
|
||||
| [05](./05-context-and-memory.md) | Context 与 Memory | 20 min | "Context window 满了怎么办?" |
|
||||
| [06](./06-feature-gating.md) | Feature Flag 系统 | 10 min | "内部功能是怎么隐藏的?" |
|
||||
|
||||
---
|
||||
|
||||
## 如何使用这套文档
|
||||
|
||||
- **按顺序读**:00 → 01 → 02 → 03 → 04 → 05 → 06,每篇都假设你已读过前面的内容
|
||||
- **跳读**:如果你已熟悉某个概念,可以直接跳到感兴趣的章节
|
||||
- **对照代码**:每篇文档都标注了关键源文件路径,随时可以打开对照阅读
|
||||
- **关注 Design Decision 专栏**:这些是最有学习价值的地方,解释了"为什么这样设计"而不只是"是什么"
|
||||
Loading…
Add table
Add a link
Reference in a new issue