diff --git a/docs/comparison/context-compression-deep-dive.md b/docs/comparison/context-compression-deep-dive.md index d599ea45..569bc79b 100644 --- a/docs/comparison/context-compression-deep-dive.md +++ b/docs/comparison/context-compression-deep-dive.md @@ -149,37 +149,82 @@ done_messages ──→ 总 token > max_tokens (1024)? --- -## 三、Claude Code:三层压缩体系 +## 三、Claude Code:三层压缩体系(源码验证) -> 来源:本仓库现有 Claude Code 文档对 compact 相关接口的记载 + 二进制分析上下文;补充见 `docs/tools/claude-code/02-commands.md` 与 `docs/tools/claude-code/EVIDENCE.md` -> -> 注:本仓库 `Claude Code` 证据页目前未系统收录压缩实现细节;`compact-2026-01-12` 这一标识符目前主要出现在仓库内部文档整理中,尚未建立稳定的外部公开文档溯源。因此,以下若涉及阈值、小版本行为、接口标识或 prompt 细节,应理解为“基于仓库现有文档与二进制分析上下文的整理”,而非完整源码级钉证。 +> 来源:v2.1.89 反编译源码分析(`services/compact/` 目录,~2,600 行) ### 三层设计 -| 层 | 名称 | 触发条件 | 作用 | -|---|------|---------|------| -| 1 | **微压缩** | 工具输出过长时 | 截断/摘要长工具输出,不等对话膨胀 | -| 2 | **自动压缩** | ~95% 容量 | 整个对话历史发送给 LLM 生成摘要 | -| 3 | **手动压缩** | `/compact [指令]` | 用户在任务边界主动执行 | +| 层 | 名称 | 触发条件 | 作用 | 源码 | +|---|------|---------|------|------| +| 1 | **MicroCompact** | 每次 API 调用前检查 | 选择性清除旧 turn 工具结果内容,保留对话结构 | `microCompact.ts` (531 行) | +| 2 | **API Context Management** | input_tokens > 180K | 服务端原生策略(`clear_tool_uses` / `clear_thinking`) | `apiMicrocompact.ts` (154 行) | +| 3 | **Full Compaction** | ~93% 上下文窗口 | 整个对话摘要为 9 章节结构化文本 | `compact.ts` (1,396 行) | -### 摘要 Prompt +**MicroCompact 两种变体**: + +| 变体 | 条件 | 机制 | +|------|------|------| +| Cached MicroCompact | Prompt cache 有效(<60 分钟) | `cache_edits` API 删除工具结果,**不破坏缓存前缀** | +| Time-Based MicroCompact | 空闲 >60 分钟 | 直接清除内容(缓存已过 TTL) | +**可清除的工具类型**(源码: `microCompact.ts#L40-L50`):Read、Bash、PowerShell、Grep、Glob、WebSearch、WebFetch、Edit、Write。不在此列表的工具(Agent、Skill、MCP)结果不会被清除。被清除的内容替换为 `'[Old tool result content cleared]'` 标记。 + +### 自动触发阈值(源码验证) + +```typescript +// 源码: services/compact/autoCompact.ts#L72-L91 +AUTOCOMPACT_BUFFER_TOKENS = 13_000 // 距上限 13K 触发 +WARNING_THRESHOLD_BUFFER_TOKENS = 20_000 // 警告缓冲区 +POST_COMPACT_TOKEN_BUDGET = 50_000 // 压缩后文件附件预算 ``` -"请编写对话摘要。目的是提供连续性,使你能在未来上下文中继续推进任务…… -写下任何有帮助的信息,包括状态、下一步、经验教训等。 -必须包裹在 标签中。" + +以 200K 上下文为例:有效窗口 180K,自动触发 = 180K - 13K = **167K tokens(~93%)**。 + +### 摘要 Prompt(9 章节,源码验证) + +``` +// 源码: services/compact/prompt.ts#L19-L26 +CRITICAL: Respond with TEXT ONLY. Do NOT call any tools. +Tool calls will be REJECTED and will waste your only turn. ``` +摘要输出必须包含 9 个章节(源码: `prompt.ts#L66-L127`): + +1. Primary Request and Intent — 用户原始意图 +2. Key Technical Concepts — 技术概念 +3. Files and Code Sections — 文件和代码片段(含 snippets) +4. Errors and fixes — 错误和修复 +5. Problem Solving — 问题解决过程 +6. All user messages — 所有用户消息(非工具结果) +7. Pending Tasks — 未完成任务 +8. Current Work — 当前工作(详细) +9. Optional Next Step — 可选的下一步(含直接引用) + +Thinking 关闭,`max_output_tokens = 20,000`(`COMPACT_MAX_OUTPUT_TOKENS`)。 + +### 压缩后恢复(源码验证) + +压缩不仅是摘要——还自动重注入关键上下文(源码: `compact.ts#L541-L585`): + +| 恢复项 | 预算 | 单项限制 | +|--------|------|----------| +| 最近读取文件 | 50,000 tokens | 5 个文件,每个 ≤5,000 tokens | +| 已调用 Skill | 25,000 tokens | 每个 ≤5,000 tokens | +| 活跃 Plan 文件 | 无限制 | — | +| 工具/指令 delta | — | — | +| Agent 列表(MCP 等) | — | — | + ### 自定义焦点 ```bash /compact 保留数据库迁移相关讨论 ``` -按本仓库现有 API 文档与二进制分析整理,当前资料将其描述为非阻塞体验。 +### 缓存优化 -除三层压缩外,Claude Code 还通过 Prompt Caching 降低系统提示与稳定前缀的重复开销。这意味着 Claude 的长会话续航不能仅归因于“~95% 晚触发”,还应把缓存视为压缩之外的重要减载手段。 +- **Forked Agent 路径**(`compact.ts#L1179-L1248`):摘要复用主对话的 prompt cache 前缀 +- **缓存断裂检测**(`microCompact.ts#L362-L367`):有意删除时标记,防止误报 cache miss --- @@ -261,26 +306,59 @@ done_messages ──→ 总 token > max_tokens (1024)? --- -## 六、Qwen Code:分叉继承 Gemini 压缩框架,但常量细节待统一 +## 六、Qwen Code:单层手动压缩(源码验证) + +> 来源:v0.15.0 开源源码分析(`packages/core/src/services/chatCompressionService.ts`,368 行) + +### 压缩阈值(源码验证) + +```typescript +// 源码: chatCompressionService.ts#L24-L30 +COMPRESSION_TOKEN_THRESHOLD = 0.7 // 70% 上下文时允许压缩 +COMPRESSION_PRESERVE_THRESHOLD = 0.3 // 保留最后 30% 历史 +MIN_COMPRESSION_FRACTION = 0.05 // 至少 5% 可压缩才执行 +``` -> 来源:`docs/tools/qwen-code/EVIDENCE.md`(确认基于 Gemini CLI 分叉)+ 本仓库其他对比分档 +**无自动触发**。仅当用户执行 `/compress` 且满足 70% 阈值时执行。 -Qwen Code 的上下文压缩框架总体上沿袭 Gemini CLI:包括 `ChatCompressionService`、Hook 事件里的 `PreCompact`、以及整体的权限 / 沙箱 / telemetry 基础设施继承关系。 +### 分割算法 -但就“当前默认阈值”与“早期资料中的继承表述”而言,本仓库现有证据需要分层处理: +基于**字符数**(非 token 数)计算分割点(源码: `chatCompressionService.ts#L45-L92`): +- 累计字符数找到 70% 位置 +- 向后搜索到安全分割点(user 消息边界) +- 不在工具调用序列中间切断 -- `docs/tools/qwen-code/05-settings.md` 已将 `model.chatCompression.contextPercentageThreshold` 的默认值写为 **0.7(70%)** -- 多篇更早的对比文档仍把其概括为 **50%(沿袭 Gemini 默认阈值)** -- `docs/comparison/qwen-code-feature-gaps.md` 还记录了一个更具体的失败处理线索:`hasFailedCompressionAttempt` 布尔断路器——一次压缩失败后,后续非强制压缩会跳过 +### 摘要 Prompt(XML 结构) + +```xml + + + 单句目标 + 关键事实(bullet points) + 文件状态:READ/MODIFIED/CREATED/DELETED + 最近操作和结果 + 步骤计划 [DONE]/[IN PROGRESS]/[TODO] + +``` + +Thinking **开启**(未禁用),无最大输出 token 限制。 + +### 压缩后恢复 + +```typescript +// 源码: chatCompressionService.ts#L263-L274 +extraHistory = [ + { role: 'user', parts: [{ text: summary }] }, // 摘要作为 user 消息 + { role: 'model', parts: [{ text: 'Got it. Thanks...' }] }, // 确认响应 + ...historyToKeep, // 最后 30% 历史 +] +``` -因此,更稳妥的结论是: +**无文件/Skill/Plan 重注入**。压缩后需重新 Read 文件。 -- **架构层面**:Qwen Code 继承了 Gemini 的压缩框架 -- **当前设置层面**:项目内可直接引用的设置文档默认值为 **70%**;“50%”更适合视为早期继承关系或旧文档表述 -- **实现细节层面**:失败断路器等内部分析线索仍未统一汇总到 `docs/tools/qwen-code/EVIDENCE.md` 主证据页,仍应以分叉源码逐项复核 -- **系统治理层面**:Qwen 并非只靠压缩管理长会话;`LoopDetectionService` 与 `PreCompact` Hook 说明它把压缩放在更大的 loop / session 管理栈里 +### 与 Gemini CLI 的继承关系 -这也是本文在总览表中将 Qwen Code 写为“框架继承 + 当前设置默认值 70%”而非简单复写 Gemini 数值的原因。 +Qwen Code 继承了 Gemini CLI 的 `ChatCompressionService` 框架,但当前默认阈值为 **70%**(Gemini CLI 为 50%)。`hasFailedCompressionAttempt` 断路器、`LoopDetectionService` 和 `PreCompact` Hook 均继承自上游。 --- @@ -290,7 +368,7 @@ Qwen Code 的上下文压缩框架总体上沿袭 Gemini CLI:包括 `ChatCompr | Agent | 已证实控制面 | 已证实生命周期/骨架 | 仍未知 | |------|-------------|-------------------|------| -| **Claude Code** | `/compact [指令]`、`PreCompact` / `PostCompact`、仓库内部文档记载的 compact 接口标识 | 三层压缩体系、`` 输出约束 | 精确阈值常量、完整 compact prompt、接口标识的稳定外部公开溯源、微压缩算法细节 | +| **Claude Code** | `/compact [指令]`、`PreCompact` / `PostCompact`、三层压缩(MicroCompact/API/Full)、9 章节摘要 Prompt、`cache_edits` API、后压缩 5 文件重注入 | 三层压缩体系、自动触发 ~93%、`COMPACT_MAX_OUTPUT_TOKENS=20,000` | 已通过 v2.1.89 反编译源码验证,见本文"三、Claude Code"节 | | **Copilot CLI** | `/compact`、`infiniteSessions.backgroundCompactionThreshold`、`bufferExhaustionThreshold` | infinite sessions、checkpoint titles 作为会话骨架 | 默认阈值数值、手动与后台 compact 是否共用同一实现 | | **Codex CLI** | `/compact`、`compact_prompt`、`model_auto_compact_token_limit`、`model_context_window` | `thread/compact/start`、`thread/compacted` 事件 | 默认 compact prompt、默认阈值、`enable_request_compression` 与摘要 compact 的准确关系 | diff --git a/docs/comparison/shell-security-deep-dive.md b/docs/comparison/shell-security-deep-dive.md new file mode 100644 index 00000000..0733f423 --- /dev/null +++ b/docs/comparison/shell-security-deep-dive.md @@ -0,0 +1,373 @@ +# Shell 安全模型 Deep-Dive + +> AI Agent 执行 Shell 命令时,如何防止注入攻击、越权操作和恶意代码执行?本文基于 Claude Code(v2.1.89 反编译)和 Qwen Code(v0.15.0 开源)的源码分析,对比两者在命令验证、AST 分析和权限决策方面的安全哲学差异。 + +--- + +## 1. 安全哲学对比 + +| 维度 | Claude Code | Qwen Code | +|------|------------|-----------| +| **核心策略** | 多重检测器 + 模式匹配 + AST 辅助 | AST-first 读写分类 | +| **检查数量** | 20+ 项枚举检查 | 1 项核心判定(read-only?) | +| **决策模型** | 3 态(allow / ask / deny) | 2 态(allow / ask) | +| **失败方向** | fail-closed(解析失败 → ask) | fail-closed(AST 失败 → ask) | +| **验证位置** | 命令执行前(内联) | 工具权限评估时 | +| **引用分析** | 3 种引用提取变体 | AST 原生(无需引用提取) | +| **子命令图谱** | 最小化(git/find/sed/awk) | 全面(10+ 工具,52 个 git 子命令) | + +--- + +## 2. Claude Code:多层检测器管线 + +### 2.1 验证管线架构 + +验证分三个阶段执行(源码: `bashSecurity.ts#L2518-L2586`): + +``` +命令输入 + ↓ +阶段 1: Early Validators(4 个,可 early-return 'allow') + ├── validateEmpty → 空命令 allow + ├── validateIncompleteCommands → 不完整命令检测 + ├── validateSafeCommandSubstitution → 安全 heredoc allow + └── validateGitCommit → 安全 git commit allow + ↓ +阶段 2: Main Validators(19 个,顺序执行) + ├── validateJqCommand → jq 注入 + ├── validateObfuscatedFlags → Unicode/编码混淆 + ├── validateShellMetacharacters → 危险元字符 + ├── validateDangerousVariables → 变量重定向攻击 + ├── validateCommentQuoteDesync → 注释/引号不同步 + ├── validateQuotedNewline → 引号内换行 + ├── validateCarriageReturn → CR 注入 + ├── validateNewlines → 命令换行 + ├── validateIFSInjection → IFS 环境变量操控 + ├── validateProcEnvironAccess → /proc/environ 读取 + ├── validateDangerousPatterns → 命令替换 + heredoc + 反引号 + ├── validateRedirections → 写重定向(> >>) + ├── validateBackslashEscapedWhitespace → 转义空白 + ├── validateBackslashEscapedOperators → 转义运算符 + ├── validateUnicodeWhitespace → Unicode 空白字符 + ├── validateMidWordHash → 词中 # 号 + ├── validateBraceExpansion → 花括号展开 + ├── validateZshDangerousCommands → 18 个 Zsh 命令 + └── validateMalformedTokenInjection → 畸形 token + ↓ +阶段 3: Deferred Non-Misparsing Validators(2 个) + ├── validateNewlines (non-misparsing) + └── validateRedirections (non-misparsing) + ↓ +结果: { behavior: 'allow' | 'ask', checkId, isBashSecurityCheckForMisparsing } +``` + +> 源码: `tools/BashTool/bashSecurity.ts`(2,592 行) + +### 2.2 引用提取系统 + +Claude Code 在正则分析前先提取三种引用变体(源码: `bashSecurity.ts#L119-L174`): + +| 变体 | 说明 | 用途 | +|------|------|------| +| `withDoubleQuotes` | 移除 `'...'` 保留 `"..."` 内容 | Shell 变量跟踪 | +| `fullyUnquoted` | 移除所有引号内容 | 大多数安全检查 | +| `unquotedKeepQuoteChars` | 引号内容替换为引号标记(`''`/`""`) | 词中 # 检测(需要引号邻接信息) | + +**引用状态机**:逐字符扫描,跟踪单引号/双引号状态,处理反斜杠转义。单引号内反斜杠为字面量(Bash 语义正确)。 + +### 2.3 Tree-Sitter AST 辅助 + +```typescript +// 源码: utils/bash/treeSitterAnalysis.ts(506 行) +type TreeSitterAnalysis = { + quoteContext: QuoteContext // AST 级引用分析 + compoundStructure: CompoundStructure // &&, ||, ;, pipeline 检测 + hasActualOperatorNodes: boolean // 区分 \; (参数) 和 ; (运算符) + dangerousPatterns: DangerousPatterns // $(), 反引号, ${}, heredoc, 注释 +} +``` + +**关键价值**:消除 `find -exec \;` 的误报。Tree-sitter 将 `\;` 解析为 word 节点(参数),而非 `;` 运算符。正则无法区分这两者。 + +### 2.4 Heredoc 安全判定 + +安全 Heredoc 模式(源码: `bashSecurity.ts#L317-L513`): + +```bash +# 允许的模式(单引号/转义定界符 = 无展开) +$(cat <<'EOF' +文件内容(字面量,无变量展开) +EOF +) + +# 拒绝的模式(无引号定界符 = 有展开) +$(cat < { + const command = stripShellWrapper(this.params.command) + try { + const isReadOnly = await isShellCommandReadOnlyAST(command) + if (isReadOnly) return 'allow' // 只读 → 自动允许 + } catch (e) { + debugLogger.warn('AST read-only check failed, falling back to ask:', e) + } + return 'ask' // 非只读或 AST 失败 → 询问 +} +``` + +**设计哲学**:不枚举危险模式,而是判断"是否只读"。只读 = 安全,非只读 = 询问。 + +### 3.2 AST 解析器 + +```typescript +// 源码: qwen-code/packages/core/src/utils/shellAstParser.ts(1,248 行) +// 使用 web-tree-sitter + tree-sitter-bash.wasm +await Parser.init({ locateFile: () => resolveWasmPath('tree-sitter.wasm') }) +parserInstance.setLanguage(await Parser.Language.load( + resolveWasmPath('tree-sitter-bash.wasm') +)) +``` + +**WASM 路径解析**(源码: `shellAstParser.ts#L590-L668`):处理多种部署场景(源码/转译/打包),探测多个候选目录。 + +**容错**:WASM 初始化失败时回退到 regex checker(`shellReadOnlyChecker.ts`,364 行)。 + +### 3.3 只读命令白名单(41 个) + +```typescript +// 源码: shellAstParser.ts#L41-L76 +// 只读根命令: +awk, basename, cat, cd, column, cut, df, dirname, du, echo, env, find, git, +grep, head, less, ls, more, printenv, printf, ps, pwd, rg, ripgrep, sed, +sort, stat, tail, tree, uniq, wc, which, where, whoami +``` + +### 3.4 子命令级分析(深度图谱) + +| 工具 | 只读子命令 | 阻止的操作 | +|------|-----------|-----------| +| **git**(52 个子命令映射) | blame, branch, cat-file, diff, grep, log, ls-files, remote, rev-parse, show, status, describe | `remote add/remove/rename`, `branch -d/-D/--delete` | +| **find** | 默认只读 | `-delete`, `-exec`, `-execdir`, `-ok`, `-okdir`, `-fprint*` | +| **sed** | 默认只读 | `-i`, `--in-place`, `e` 命令(execute), `w` 命令(write) | +| **awk** | 默认只读 | `system()`, 文件写入, 管道输出, `getline`, `close()` | +| **npm/yarn/pnpm** | `list`, `outdated`, `view`, `info` | `install`, `publish`, `run` | +| **docker** | `ps`, `images`, `inspect` | `run`, `exec`, `rm`, `stop` | +| **kubectl** | `get`, `describe`, `logs` | `apply`, `delete`, `edit` | + +> 源码: `shellAstParser.ts#L161-L531`(完整子命令映射,10+ 工具) + +### 3.5 AST 节点级分析 + +递归遍历 AST 节点(源码: `shellAstParser.ts#L914-L991`): + +| AST 节点类型 | 判定 | +|-------------|------| +| `command` | 检查根命令 + 命令替换 | +| `pipeline` | 所有命令都只读 → 只读 | +| `list` (&&, ||) | 所有命令都只读 → 只读 | +| `redirected_statement` | 阻止写重定向(>, >>, &>, &>>, >|) | +| `subshell` | 所有内部命令都只读 → 只读 | +| `variable_assignment` | 纯赋值 → 安全 | +| `negated_command` | 分析内部命令 | +| `if` / `while` / `for` / `case` | **保守拒绝**(控制流不视为只读) | +| `function_definition` | **拒绝** | +| `declaration_command` | **拒绝**(可修改环境) | + +### 3.6 权限规则提取 + +用户批准命令后,Qwen Code 提取**最小范围**的权限规则(源码: `shellAstParser.ts#L1050-L1202`): + +``` +extractCommandRules('git clone https://github.com/foo/bar.git') + → ['git clone *'] // 通配子命令参数 + +extractCommandRules('npm outdated') + → ['npm outdated'] // 无参数不加通配符 + +extractCommandRules('git clone foo && npm install') + → ['git clone *', 'npm install'] // 复合命令分拆为多条规则 +``` + +### 3.7 PTY 执行模型 + +Qwen Code 使用 PTY(伪终端)执行命令(源码: `shellExecutionService.ts`,1,032 行): + +```typescript +// 源码: shellExecutionService.ts#L596-L609 +const ptyProcess = ptyInfo.module.spawn(executable, args, { + name: 'xterm', + cols: 80, rows: 30, + env: { TERM: 'xterm-256color', PAGER: 'cat', GIT_PAGER: 'cat', QWEN_CODE: '1' }, + handleFlowControl: true, +}) +``` + +| 维度 | 详情 | +|------|------| +| 渲染节流 | 100ms 间隔 | +| 二进制检测 | 前 4096 字节嗅探 | +| 信号处理 | SIGTERM → 200ms → SIGKILL(POSIX)/ `taskkill /f /t`(Windows) | +| 回退 | PTY 不可用时降级为 `child_process.spawn` | + +--- + +## 4. 逐维度对比 + +### 4.1 检测方法 + +| 维度 | Claude Code | Qwen Code | +|------|------------|-----------| +| 主要方法 | 正则模式匹配 + AST 辅助 | AST-first 读写分类 | +| 检查器数量 | 25+(Early + Main + Deferred) | 1(`isShellCommandReadOnlyAST`) | +| AST 角色 | 辅助(消除误报) | 核心(主决策路径) | +| 回退 | 无(双路并行) | regex checker(WASM 失败时) | +| 误报处理 | Tree-sitter 消除 `find -exec \;` 等误报 | AST 原生解析,无此问题 | + +### 4.2 安全覆盖 + +| 攻击类型 | Claude Code | Qwen Code | +|----------|------------|-----------| +| 命令替换($()、${}) | ✅ 12 种模式检测 | ✅ AST 检测 | +| IFS 注入 | ✅ 专项检查 | ❌ 不检测(非只读判定范畴) | +| Unicode 空白 | ✅ 专项检查 | ❌ 不检测 | +| 控制字符 | ✅ 专项检查 | ❌ 不检测 | +| Zsh 特定命令 | ✅ 18 个命令阻止 | ❌ 不检测 | +| 花括号展开 | ✅ 专项检查 | ❌ 不检测 | +| 混淆标志 | ✅ 专项检查 | ❌ 不检测 | +| 写重定向 | ✅ 专项检查 | ✅ AST 检测 | +| 管道/复合命令 | ✅ 元字符检查 | ✅ AST 递归分析 | +| git 危险操作 | ✅ 最小检查 | ✅ 52 个子命令映射 | + +### 4.3 权限决策 + +| 维度 | Claude Code | Qwen Code | +|------|------------|-----------| +| 决策输出 | allow / ask / deny(via misparsing flag) | allow / ask | +| 权限持久化 | 多层规则来源(8 级) | 权限规则提取(最小范围通配) | +| 学习机制 | 用户可实时更新 session/project/user 规则 | 用户批准后提取规则建议 | +| 子命令粒度 | 基础(git/find/sed/awk) | 全面(10+ 工具,52+ git 子命令) | + +### 4.4 执行模型 + +| 维度 | Claude Code | Qwen Code | +|------|------------|-----------| +| 执行方式 | `child_process.spawn` + 可选 sandbox | PTY(node-pty)+ xterm 渲染 | +| 输出捕获 | stdout/stderr 分离 | headless terminal 缓冲区重放 | +| 超时 | 120s 默认(可配置) | 120s 默认(`DEFAULT_FOREGROUND_TIMEOUT_MS`) | +| 沙箱 | 可选(`shouldUseSandbox()`) | 无独立沙箱 | +| ANSI 处理 | strip-ansi 后处理 | xterm Terminal 原生解析 | + +--- + +## 5. 安全哲学分析 + +### Claude Code:枚举已知威胁 + +**优势**: +- 覆盖面广——每种已知攻击类型有专项检测 +- IFS 注入、Unicode 空白、Zsh 命令等边缘攻击均有防护 +- Tree-sitter 辅助消除正则误报 + +**风险**: +- 正则模式可能遗漏新型攻击模式 +- 25+ 检查器的维护成本高 +- 引用提取状态机的边缘 case 复杂 + +### Qwen Code:分类已知安全 + +**优势**: +- AST 分析精确——不存在正则误报 +- 代码简洁——核心判定仅 1 个函数 +- 子命令图谱全面——git 52 个子命令逐一分类 + +**风险**: +- 不检测 IFS 注入、Unicode 空白等非"读写分类"维度的攻击 +- 控制流(`if`/`while`/`for`)保守拒绝——可能误拒安全的循环命令 +- 只读白名单需持续维护——新工具(如 `jq`)需手动添加 + +### 对比总结 + +``` +Claude Code: "这些模式是危险的" → 枚举危险 → 未匹配则允许 +Qwen Code: "这些模式是安全的" → 枚举安全 → 未匹配则询问 +``` + +两者都是 fail-closed(不确定时拒绝/询问),但枚举方向相反。 + +--- + +## 6. 关键源码文件 + +### Claude Code + +| 文件 | 行数 | 职责 | +|------|------|------| +| `tools/BashTool/bashSecurity.ts` | 2,592 | 多层验证管线(25+ 检查器) | +| `utils/bash/treeSitterAnalysis.ts` | 506 | Tree-sitter AST 辅助分析 | +| `utils/bash/heredoc.ts` | — | Heredoc 提取与验证 | +| `utils/bash/shellQuote.ts` | — | Shell 引用解析 | +| `tools/BashTool/shouldUseSandbox.ts` | 154 | 沙箱决策逻辑 | + +### Qwen Code + +| 文件 | 行数 | 职责 | +|------|------|------| +| `packages/core/src/utils/shellAstParser.ts` | 1,248 | AST 解析 + 只读判定 + 子命令映射 + 规则提取 | +| `packages/core/src/utils/shellReadOnlyChecker.ts` | 364 | 正则回退(WASM 失败时) | +| `packages/core/src/tools/shell.ts` | 706 | Shell 工具入口 + 权限决策 | +| `packages/core/src/services/shellExecutionService.ts` | 1,032 | PTY 执行 + 输出捕获 | +| `packages/core/src/permissions/shell-semantics.ts` | 1,686 | 语义分析(命令 → 虚拟文件/网络操作) | + +--- + +## 7. 设计启示 + +1. **AST-first 更精确但覆盖面有限**:Qwen Code 的 AST 分析消除了正则误报,但不覆盖 IFS/Unicode/Zsh 等维度——理想方案是 AST 为主 + 专项检查为补充 +2. **子命令映射是高杠杆投入**:Qwen Code 的 52 个 git 子命令映射让用户几乎无需为 git 操作确认权限——这是 Claude Code 可借鉴的 +3. **权限规则提取降低审批疲劳**:Qwen Code 的 `extractCommandRules()` 自动建议最小范围规则(如 `git clone *`),而非让用户手动配置 +4. **枚举方向决定维护成本**:枚举"安全"(Qwen Code)更易维护(新工具默认拒绝),枚举"危险"(Claude Code)覆盖面更广但需持续更新 + +> **免责声明**: 以上分析基于 2026 年 Q1 源码(Claude Code v2.1.89、Qwen Code v0.15.0),后续版本可能已变更。 diff --git a/docs/comparison/tool-parallelism-deep-dive.md b/docs/comparison/tool-parallelism-deep-dive.md new file mode 100644 index 00000000..71458969 --- /dev/null +++ b/docs/comparison/tool-parallelism-deep-dive.md @@ -0,0 +1,315 @@ +# 工具并行执行 Deep-Dive + +> 当模型一次返回多个工具调用时,Agent 如何执行它们?串行逐个执行还是智能并行?本文基于 Claude Code(v2.1.89 反编译)和 Qwen Code(v0.15.0 开源)的源码分析,对比两者在工具执行并发模型、依赖分析和流式处理方面的架构差异。 + +--- + +## 1. 问题定义 + +现代 LLM 可在一次响应中返回多个 `tool_use` block。例如模型可能同时请求: + +``` +tool_use: Read("src/main.ts") +tool_use: Read("src/config.ts") +tool_use: Grep("TODO", "src/") +tool_use: Bash("npm test") +``` + +前三个是只读操作,可安全并行;第四个可能依赖前三个结果。Agent 如何处理这种混合场景? + +--- + +## 2. Claude Code:智能分批 + 流式执行 + +### 2.1 并发配置 + +```typescript +// 源码: services/tools/toolOrchestration.ts#L8-L12 +function getMaxToolUseConcurrency(): number { + return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10 +} +// 默认最大 10 个工具并发,可通过环境变量调整 +``` + +### 2.2 分批算法 + +Claude Code 将工具调用分为**连续的批次**,每批要么全部并行,要么单独串行: + +```typescript +// 源码: services/tools/toolOrchestration.ts#L91-L116 +function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] { + return toolUseMessages.reduce((acc, toolUse) => { + const isConcurrencySafe = tool?.isConcurrencySafe(parsedInput.data) ?? false + // fail-closed: 解析失败时视为不安全 + + if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) { + acc[acc.length - 1].blocks.push(toolUse) // 追加到当前并行批次 + } else { + acc.push({ isConcurrencySafe, blocks: [toolUse] }) // 新批次 + } + return acc + }, []) +} +``` + +**分批规则**: +- 连续的并发安全工具 → 合并为一个并行批次 +- 遇到非并发安全工具 → 独立为一个串行批次 +- 非并发安全工具后的并发安全工具 → 新的并行批次 + +**示例**: + +``` +输入: [Read, Read, Grep, Edit, Read, Read] +分批: [Read, Read, Grep] → [Edit] → [Read, Read] + ↑ 并行批次(3个) ↑ 串行 ↑ 并行批次(2个) +``` + +### 2.3 isConcurrencySafe() 分类 + +每个工具定义自己是否并发安全: + +```typescript +// 源码: Tool.ts#L402, L759 +// 默认实现(fail-closed) +isConcurrencySafe: (_input?: unknown) => false +``` + +| 工具 | 并发安全 | 原因 | +|------|:--------:|------| +| FileReadTool | ✅ | 纯读取 | +| GlobTool | ✅ | 纯读取 | +| GrepTool | ✅ | 纯读取 | +| WebFetchTool | ✅ | 无副作用 | +| WebSearchTool | ✅ | 无副作用 | +| BashTool | ⚠️ 条件 | 仅当命令被判定为只读时 | +| FileEditTool | ❌ | 文件修改 | +| FileWriteTool | ❌ | 文件写入 | +| AgentTool | ❌ | 子进程副作用 | +| 其他 | ❌ | 默认不安全 | + +### 2.4 并行执行路径 + +```typescript +// 源码: services/tools/toolOrchestration.ts#L152-L177 +async function* runToolsConcurrently(toolUseMessages, ...): AsyncGenerator { + yield* all( + toolUseMessages.map(async function* (toolUse) { + // 标记为执行中 + toolUseContext.setInProgressToolUseIDs(prev => new Set(prev).add(toolUse.id)) + // 执行工具 + yield* runToolUse(toolUse, ...) + // 标记完成 + markToolUseAsComplete(toolUseContext, toolUse.id) + }), + getMaxToolUseConcurrency(), // 并发上限: 10 + ) +} +``` + +`all()` 是自定义的并发 AsyncGenerator 合并器,限制最大并发数。 + +### 2.5 上下文修改队列(防竞态) + +并行执行的工具可能修改共享上下文(如文件状态缓存)。Claude Code 使用**队列化**策略: + +```typescript +// 源码: services/tools/toolOrchestration.ts#L31-L62 +// 并行批次:上下文修改先队列化,批次结束后按工具顺序串行应用 +const queuedContextModifiers: Record = {} +for await (const update of runToolsConcurrently(...)) { + if (update.contextModifier) { + queuedContextModifiers[toolUseID].push(modifyContext) + } +} +// 批次完成后: +for (const block of blocks) { + for (const modifier of queuedContextModifiers[block.id] ?? []) { + currentContext = modifier(currentContext) // 按工具顺序串行应用 + } +} +``` + +```typescript +// 源码: services/tools/toolOrchestration.ts#L118-L150 +// 串行批次:上下文修改立即应用 +for (const toolUse of toolUseMessages) { + for await (const update of runToolUse(toolUse, ..., currentContext)) { + if (update.contextModifier) { + currentContext = update.contextModifier.modifyContext(currentContext) // 立即 + } + } +} +``` + +### 2.6 StreamingToolExecutor(流式路径) + +当 `config.gates.streamingToolExecution` 开启时,工具在 API 响应**流式到达时**就开始执行,无需等待完整响应: + +```typescript +// 源码: services/tools/StreamingToolExecutor.ts#L129-L150 +private canExecuteTool(isConcurrencySafe: boolean): boolean { + const executingTools = this.tools.filter(t => t.status === 'executing') + return ( + executingTools.length === 0 || // 无工具执行中 + (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe)) // 都是安全的 + ) +} +``` + +**工具状态机**: + +``` +queued → executing → completed → yielded +``` + +**Bash 错误级联**: + +```typescript +// 源码: StreamingToolExecutor.ts#L359-L363 +if (tool.block.name === BASH_TOOL_NAME) { + this.hasErrored = true + this.siblingAbortController.abort('sibling_error') + // Bash 失败时取消同批次其他工具(隐式依赖假设) +} +``` + +仅 Bash 工具的错误会级联取消兄弟工具——因为 Bash 命令常有隐式依赖(如 `mkdir` 失败后续命令无意义)。 + +### 2.7 查询循环集成 + +```typescript +// 源码: query.ts#L561-L568, L1366-L1382 +const toolUpdates = streamingToolExecutor + ? streamingToolExecutor.getRemainingResults() // 流式路径 + : runTools(toolUseBlocks, ...) // 非流式路径(partitionToolCalls) + +for await (const update of toolUpdates) { + yield update.message // 逐条 yield 结果 + toolResults.push(...) // 收集 +} +``` + +--- + +## 3. Qwen Code:类型分流 + 顺序执行 + +### 3.1 执行模型 + +Qwen Code 不按并发安全性分批,而是按**工具类型**分流: + +```typescript +// 源码: qwen-code/packages/core/src/core/coreToolScheduler.ts#L1303-L1314 +// Agent 工具 → 并发(独立子代理,无共享状态) +const taskCalls = callsToExecute.filter(c => c.request.name === ToolNames.AGENT) +// 其他所有工具 → 顺序 +const otherCalls = callsToExecute.filter(c => c.request.name !== ToolNames.AGENT) + +const taskPromise = Promise.all( + taskCalls.map(tc => this.executeSingleToolCall(tc, signal)), // 并发 +) +const othersPromise = (async () => { + for (const toolCall of otherCalls) { + await this.executeSingleToolCall(toolCall, signal) // 顺序 + } +})() +await Promise.all([taskPromise, othersPromise]) // 两组同时执行 +``` + +### 3.2 工具状态机(7 状态) + +``` +validating → scheduled → awaiting_approval → executing → success / error / cancelled +``` + +```typescript +// 源码: coreToolScheduler.ts#L86-L163 +type Status = 'validating' | 'scheduled' | 'awaiting_approval' + | 'executing' | 'success' | 'error' | 'cancelled' +``` + +### 3.3 权限流程 + +5 阶段权限评估(源码: `coreToolScheduler.ts#L842-L940`): + +``` +L3(Tool 默认) → L4(PermissionManager 策略) → L5(ApprovalMode) → Hooks → 非交互处理 +``` + +### 3.4 无流式工具执行 + +Qwen Code 等待完整 API 响应后才开始工具执行,不支持流式到达时即执行。 + +--- + +## 4. 逐维度对比 + +| 维度 | Claude Code | Qwen Code | +|------|------------|-----------| +| **并发模型** | 按 `isConcurrencySafe()` 智能分批 | 按工具类型分流(Agent 并发 / 其他顺序) | +| **默认并发上限** | 10(`CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY`) | Agent 工具无限制 / 其他为 1 | +| **安全判定** | 每个工具实现 `isConcurrencySafe(input)` | 仅 Agent 工具硬编码为并发安全 | +| **分批策略** | 连续并发安全工具合并为一批 | 全局分两组(Agent vs 其他) | +| **流式执行** | ✅ 工具在 API 响应流到达时开始执行 | ❌ 等待完整响应 | +| **上下文修改** | 并行时队列化,批次后串行应用 | 顺序执行,立即应用 | +| **错误级联** | Bash 错误取消同批次兄弟 | 无级联,各工具独立 | +| **工具状态机** | 4 状态(queued/executing/completed/yielded) | 7 状态(含 validating/awaiting_approval) | +| **进度显示** | 并行工具各自独立显示进度 | 顺序显示当前执行工具 | + +--- + +## 5. 性能影响 + +### 5.1 典型场景对比 + +| 场景 | Claude Code | Qwen Code | +|------|------------|-----------| +| 模型返回 5 个 Read 调用 | **并行**执行,~1× 延迟 | **顺序**执行,~5× 延迟 | +| 模型返回 3 个 Read + 1 个 Edit + 2 个 Read | 批次 1: 3×Read 并行 → 批次 2: Edit 串行 → 批次 3: 2×Read 并行 | 6 个工具顺序执行 | +| 模型返回 2 个 Agent 调用 | 并行(如果 Agent 工具 `isConcurrencySafe` 返回 true) | **并行**(Agent 工具始终并发) | +| 模型返回 1 个 Bash + 1 个 Read | Bash 串行 → Read 串行(Bash 非并发安全) | 顺序执行(同) | + +### 5.2 大规模代码探索 + +当模型需要探索大型代码库时(典型 pattern:多个 Glob + Grep + Read),Claude Code 的并行执行优势显著: + +``` +Claude Code: [Glob₁ + Glob₂ + Grep₁ + Grep₂ + Read₁ + Read₂ + Read₃] + → 一个并行批次,~1× 延迟(受最慢工具限制) + +Qwen Code: Glob₁ → Glob₂ → Grep₁ → Grep₂ → Read₁ → Read₂ → Read₃ + → 7× 延迟(顺序执行) +``` + +--- + +## 6. 关键源码文件 + +### Claude Code + +| 文件 | 行数 | 职责 | +|------|------|------| +| `services/tools/toolOrchestration.ts` | ~189 | 分批算法 + 并行/串行执行路径 | +| `services/tools/StreamingToolExecutor.ts` | ~531 | 流式工具执行 + 状态机 + Bash 错误级联 | +| `Tool.ts` | L402, L759 | `isConcurrencySafe()` 接口定义 + 默认实现 | +| `utils/generators.ts` | L32-L72 | `all()` 并发 AsyncGenerator 合并器 | +| `query.ts` | L561-L568 | 流式 vs 非流式路径选择 | + +### Qwen Code + +| 文件 | 行数 | 职责 | +|------|------|------| +| `packages/core/src/core/coreToolScheduler.ts` | 1,710 | 工具调度器(Agent 并发 / 其他顺序) | +| `packages/core/src/agents/runtime/agent-core.ts` | L485 | `processFunctionCalls` 调用调度器 | +| `packages/core/src/agents/runtime/agent-events.ts` | L27-L40 | 工具事件类型定义 | + +--- + +## 7. 设计启示 + +1. **`isConcurrencySafe()` 比类型分流更精确**:Claude Code 允许每个工具根据输入参数动态判断安全性(如 Bash 只读命令可并行),而 Qwen Code 硬编码 Agent 为唯一并发类型 +2. **流式执行缩短总延迟**:Claude Code 在 API 响应流到达时就开始执行工具,不等完整响应,进一步重叠 I/O +3. **上下文修改队列化是并行执行的前提**:没有竞态保护,并行工具可能产生不一致的文件状态缓存 +4. **Bash 错误级联**是一个值得借鉴的设计:Bash 命令失败后取消兄弟工具,避免无意义执行 + +> **免责声明**: 以上分析基于 2026 年 Q1 源码(Claude Code v2.1.89、Qwen Code v0.15.0),后续版本可能已变更。