Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions docs/comparison/agent-tool-access-control-deep-dive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Qwen Code 改进建议 — Agent 工具细粒度访问控制 (Fine-grained Tool Access Control)

> 核心洞察:随着智能体系统(Agent System)支持后台执行(Async Agents)和多智能体协同(In-process Teammates),并非所有 Agent 都应该拥有相同的工具权限。如果后台执行的“只读代码探索” Agent 能够调用 `AskUserQuestionTool`,它将永远阻塞并在后台挂起等待用户回复;如果它能调用 `ExitPlanModeTool`,甚至会意外破坏主流程的状态机。Claude Code 实现了多层、白名单与黑名单组合的细粒度工具权限控制;而 Qwen Code 目前只能做到“全量赋予”或粗粒度的硬编码列表。
>
> 返回 [改进建议总览](./qwen-code-improvement-report.md)

## 一、为什么需要多层工具访问控制?

在单体(Single Agent)架构下,主干交互 Agent 拥有全部工具权限是没有问题的。但是一旦引入后台子代理(Subagent)或多 Agent 协作网络(Swarm),权限隔离就变得生死攸关。

**Qwen Code 现状与潜在风险**:
假设你想衍生一个子 Agent 在后台自动整理和归纳过去一周的代码修改。
在当前架构下,如果它获得了所有的基础工具:
1. **死锁风险**:它错误地调用了 `ask_user` 或 `agent_tool` (尝试再衍生子 Agent),会导致后台任务挂起或递归爆炸。
2. **状态越权**:它调用了 `exit_plan_mode`,破坏了当前主进程(Coordinator)原本的阶段控制逻辑。
3. **破坏最小权限原则**:本来只用搜索(grep)的 Agent,被赋予了修改文件(edit/write)或跑高危命令(shell)的能力。

## 二、Claude Code 的解决方案:三层门控 (Triple-Gate Control)

Claude Code 在 `constants/tools.ts` 中精心设计了三层基于集合(Sets)的隔离墙。这决定了在运行时,底层 `filterToolsForAgent` 函数会给 Agent 装配何种能力:

### 第一层:全局禁止名单 (ALL_AGENT_DISALLOWED_TOOLS)
这是不可逾越的红线。任何脱离主交互循环被独立 `spawn` 出来运行的 Agent(不论前后台),都**绝对禁止**使用这些工具:
- `TaskOutputTool`:它只能由调度主脑使用。
- `ExitPlanModeTool` / `EnterPlanModeTool`:状态机专有。
- `AskUserQuestionTool`:交互界面专有(后台无标准输入)。
- `AgentTool` (spawn subagent):防止产生失控的递归子代理。

### 第二层:异步白名单 (ASYNC_AGENT_ALLOWED_TOOLS)
对于普通的后台异步任务,它实行的是**白名单制(Allowlist)**,只允许严格安全的、用于完成基本开发闭环的工具集:
- `FileReadTool`, `WebSearchTool`, `GrepTool`, `GlobTool` (只读搜集)
- `FileEditTool`, `FileWriteTool`, `BashTool` (操作执行)

### 第三层:同进程协作增权 (IN_PROCESS_TEAMMATE_ALLOWED_TOOLS)
当后台 Agent 不是一个孤独的异步任务,而是通过 `AsyncLocalStorage` 拉起的“同进程协作队友 (Teammate)”时,它需要与其他 Agent 交流或分配任务。系统会为它**额外开放**特定协同工具:
- `TaskCreateTool`, `TaskGetTool`, `TaskListTool`, `TaskUpdateTool` (操作共享任务树)
- `SendMessageTool` (向调度主脑或其他队友发消息)

不仅如此,Claude Code 还支持在 Agent 的配置文件(Frontmatter)中使用 `tools:` 列表手动缩小权限,或使用 `disallowedTools:` 显式排除某工具,实现了动态与静态结合的极强可配性。

## 三、Qwen Code 的改进路径 (P1 优先级)

为了迎接真正安全的多 Agent 协作时代,Qwen Code 必须重写其工具下发逻辑。

### 阶段 1:定义常量约束层
1. 新建 `packages/core/src/tools/toolAccessPolicies.ts`。
2. 定义三个不可变的权限集:`GLOBAL_DISALLOWED_TOOLS`, `BACKGROUND_AGENT_ALLOWLIST`, `SWARM_AGENT_ALLOWLIST_EXTENSION`。

### 阶段 2:改造动态装配逻辑
1. 在 `agent-core.ts` 初始化 `toolsList` 的阶段,引入一个类似 `filterToolsForAgent(agentConfig)` 的流水线:
```typescript
let availableTools = allRegisteredTools;

// 如果是后台 Agent,仅保留在 白名单 中的工具
if (agentConfig.isBackground) {
availableTools = availableTools.filter(t => BACKGROUND_AGENT_ALLOWLIST.has(t.name));
}

// 全局剔除绝对禁止的工具
availableTools = availableTools.filter(t => !GLOBAL_DISALLOWED_TOOLS.has(t.name));

// 叠加自定义配置排除 (Denylist)
if (agentConfig.disallowedTools) {
availableTools = availableTools.filter(t => !agentConfig.disallowedTools.includes(t.name));
}
```

### 阶段 3:工具层感知 (Context-Aware Tools)
1. 对于特定的交互工具(比如 `ask_user` 工具本身),它可以在其内部 `execute` 时,额外检查 `getCurrentAgentContext().role`,如果发现是后台角色则直接抛错,提供双保险。

## 四、改进收益评估
- **实现成本**:低。无需引入新的库,只需要理顺工具注册表的分发逻辑,几百行代码即可实现。
- **直接收益**:
1. **避免后台假死**:根绝了后台异步代理滥用交互工具导致任务卡住的问题。
2. **多 Agent 状态解耦**:彻底分清“调度者(Manager)”与“执行者(Worker)”的工具边界。
3. **增强防御编程**:对开发者暴露了更清晰、更最小权限的插件/Agent 定义 Schema(通过开放 `disallowedTools`)。
63 changes: 63 additions & 0 deletions docs/comparison/api-retry-fallback-deep-dive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Qwen Code 改进建议 — API 指数退避与降级重试 (API Backoff & Fallback Retry)

> 核心洞察:代码 Agent 执行复杂任务通常需要连续几十次 API 调用。任何一次瞬态网络故障(如 500、502)、服务限流(429)或模型过载(529)都不应该导致整个长任务立即失败并丢失进度。Claude Code 拥有极其精细的网络错误处理、指数退避、429 尊重和 529 模型降级机制,保证了极高的稳定性;而 Qwen Code 的重试机制相对单薄,缺乏降级与特殊错误码的智能处理。
>
> 返回 [改进建议总览](./qwen-code-improvement-report.md)

## 一、架构对比与稳定性痛点

### 1. Qwen Code 现状:基础重试
在 `packages/core/src/utils/retry.ts` 中,Qwen Code 提供了一个基础的 `retryWithBackoff` 函数:
- **最大次数**:默认 `maxAttempts: 7`。
- **触发条件**:仅在 HTTP 状态码为 429 或 5xx 时重试。
- **退避策略**:简单的基础时间乘以 2,加上 `30%` 的抖动 (Jitter)。

**目前的痛点与缺失**:
1. **不尊重 Retry-After**:当遭遇 HTTP 429 Rate Limit 时,服务端通常会在 Header 中返回 `retry-after`(告诉客户端应该等多少秒后再试)。Qwen Code 忽略了该字段,直接盲目重试,容易被服务端彻底封禁。
2. **无模型降级 (Fallback)**:在晚高峰等服务过载(HTTP 529 Overloaded)时,盲目重试大模型(如 `qwen-max`)往往徒劳无功。如果任务直接失败,用户体验极差。
3. **网络层异常未覆盖**:未特殊处理底层的 `ECONNRESET` 或 `EPIPE` 等 TCP 连接被重置的错误(这类错误通常需要禁用 HTTP Keep-Alive 重建连接)。
4. **Token 过期无法自愈**:在耗时数小时的长会话中,如果遭遇 401/403,Qwen Code 会直接抛错。而它本应挂起当前请求,触发 Auth 模块刷新 Token 后再无缝重试。

### 2. Claude Code 解决方案:高弹性网络层
Claude Code 的重试网关(位于 `services/api/withRetry.ts`)具有企业级的异常容错能力:

#### 机制一:智能读取 Retry-After
```typescript
// Claude Code: 解析 429 限流
if (status === 429 && headers.has('retry-after')) {
// 优先遵循服务端指导的等待时间,而不是自己的指数退避
const delayMs = parseRetryAfter(headers.get('retry-after'));
await sleep(delayMs);
}
```

#### 机制二:529 过载降级 (FallbackTriggeredError)
当检测到服务端严重过载(如连续 3 次返回 529 错误)时,抛出专门的 `FallbackTriggeredError`,并**自动无缝降级到备用的小型模型**(如从 `claude-3-5-sonnet` 降级到 `claude-3-haiku`)继续执行当前不那么重要的工具输出总结任务。
这保证了“降级总比彻底宕机强”。

#### 机制三:网络底座容错与持久化重试
针对底层的 `ECONNRESET` / `UND_ERR_SOCKET`,Claude Code 知道这是底层的 Keep-Alive 遇到了失效连接,它会在重试时自动带上特殊的标志,让底层网关禁用 Keep-Alive 重建纯净的 TCP 连接。
另外,它还支持配置“持久化重试模式(Persistent Retry)”——在 CI/CD 或后台无人值守环境下,它可以每隔 5 分钟重试一次,永远不轻易放弃任务。

## 二、Qwen Code 的改进路径 (P1 优先级)

为了确保 Qwen Code 在不稳定的网络环境下依然能像一台“不知疲倦的机器”一样完成任务,我们需要升级网络重试层。

### 阶段 1:增强基础 Retry 逻辑
1. 升级 `utils/retry.ts`,提升 `maxAttempts` 至 10 次。
2. 在捕获 Error 时,深度解析 Axios/Fetch 的 `response.headers`。如果遇到状态码 `429`,提取 `retry-after` 字段,作为本次 `delay` 的绝对时间(如果该字段存在)。

### 阶段 2:底层网络异常与 Token 自愈
1. 捕获特定的系统级错误码(如 `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`)加入重试白名单。
2. 处理 401 Unauthorized:拦截到 401 时,不要立即失败,而是触发一个 `eventEmitter.emit('TOKEN_REFRESH_REQUIRED')`。等待(await)凭证刷新器获取到新的 Token 后,修改请求头并重新发起请求。

### 阶段 3:引入模型降级 (Fallback)
1. 在 `Config` 中允许用户配置主模型和备用模型(如 `model: qwen-max, fallbackModel: qwen-turbo`)。
2. 在 `client.ts` 的 `generateContent` 包装器中:如果遇到连续 N 次 529 或 500 服务器错误,捕获并捕获并捕获后,自动将请求的 `model` 参数替换为 `fallbackModel`,并在 TUI 上打印一条黄色警告:“主模型过载,已自动降级为 qwen-turbo 以保证任务继续”。

## 三、改进收益评估
- **实现成本**:中。需要改造底层 HTTP 调用的拦截器和重试函数,代码量约 200 - 300 行。
- **直接收益**:
1. **极高的任务完成率**:长任务(如几十步的大型重构)再也不会因为中间某一次微小的网络抖动或服务端限流而前功尽弃。
2. **平台合规性**:尊重 API 的 `Retry-After`,大幅降低因为野蛮重试被云厂商账号封禁的风险。
3. **出色的弱网/晚高峰体验**:在 API 调用最拥堵的时间段,通过自动降级依然能保持 Agent 活跃运行。
66 changes: 66 additions & 0 deletions docs/comparison/atomic-file-write-deep-dive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Qwen Code 改进建议 — 原子文件写入与事务回滚 (Atomic File Write & Persistent Storage)

> 核心洞察:CLI Agent 的长期稳定性很大程度上取决于其持久化存储(会话历史、记忆缓存、项目配置)的健壮性。如果因为电脑意外断电或 OOM 导致写入进程中断,直接使用 `fs.writeFile` 极易留下半写文件,导致整个会话 JSON 彻底损坏报废。Claude Code 全面采用了“`temp + rename` 原子写”以及对超大结果(如几十 MB 的命令输出)的“Persist to disk”降级策略;而 Qwen Code 的绝大部分模块依然在使用有风险的直接覆写机制。
>
> 返回 [改进建议总览](./qwen-code-improvement-report.md)

## 一、架构对比与数据损坏风险

### 1. Qwen Code 现状:直接写入与内存膨胀
Qwen Code 中核心的配置存储和会话存档,大量使用了 `fs.writeFile` 或同步的 `fs.writeFileSync`:
- **半写风险(Torn Writes)**:当写入一个 5MB 的会话历史 `history.json` 时,如果写入中途 Node.js 崩溃或遭遇断电,文件中可能只包含了一半的 JSON 结构,导致该文件彻底损坏且无法恢复,用户丢失所有对话进度。
- **历史内存膨胀**:如果一个工具(比如一次失败的 `npm build` 或者一个 `grep`)返回了 5 万行的日志(2MB 文本),这些数据会被直接原封不动地塞进 `history` 数组中持久化。这不仅导致下次启动读取缓慢,还会立刻撑爆大模型的输入上下文。

*注:虽然 Qwen Code 在 `packages/core/src/utils/atomicFileWrite.ts` 中实现了一个 `atomicWriteJSON`,但目前仅在实验性的 Arena 模块中使用,核心引擎仍处于裸奔状态。*

### 2. Claude Code 的解决方案:原子事务与防膨胀脱水
Claude Code 设计了两套底座机制来保证长对话的数据安全:

#### 机制一:全局原子写 (Atomic Rename)
所有的关键持久化(无论是 Session、TeamMemory 还是 Config),一律强制走包装好的原子写函数:
```typescript
// 1. 写到与目标同一磁盘的隐藏临时文件
await fs.writeFile(`${filePath}.tmp`, data);

// 2. 利用 POSIX 系统的 fs.rename() 的原子性,瞬间替换目标文件
await fs.rename(`${filePath}.tmp`, filePath);
```
由于 `rename` 在操作系统层是原子的,即使在此刻拔断电源,用户也只会看到“全新的文件”或“完好无损的旧文件”,绝不存在被写破了一半的损坏状态。

#### 机制二:大结果持久化降级 (Persist to Disk)
当检测到工具调用的输出(如 `BashTool` 的标准输出或大文件的 `FileReadTool`)超过了 `50K chars`(约 8MB 内存上限)时:
1. Claude Code **不会** 把这段文本放进历史消息记录(Message Array)中。
2. 而是调用 `persistToolResult` 函数,用 `SHA-256` 算个 Hash,将这 8MB 文本单独持久化到本地 `.claude/tool-results/` 目录下。
3. 在真实的对话历史中,只保存一个极短的“脱水占位符(Stub)”:
```xml
<persisted-output>
Preview (first 2KB): npm WARN deprecated...
Full output saved to: ~/.claude/tool-results/mcp-bash-1738...
</persisted-output>
```
这避免了 OOM 崩溃,也防止了无意义的海量垃圾日志污染下一次会话的大模型 Context。

## 二、Qwen Code 的改进路径 (P1 优先级)

保证用户数据(尤其是耗时几小时的 Coding Session 存档)绝对不丢失,是 Agent 工具的及格线要求。

### 阶段 1:全域推行原子写 (Rollout Atomic Write)
1. 找出项目中所有的 `fs.writeFileSync`、`fs.writeFile`(尤其是针对 `.qwen/sessions/`、`.qwen/config.json` 等高价值资产的操作)。
2. 全部替换为 `utils/atomicFileWrite.ts` 中提供的方案。
3. 确保临时文件和目标文件处于同一挂载点(Volume),否则 `rename` 会退化为非原子的 `copy+delete`(此时仍需处理跨盘回滚逻辑)。

### 阶段 2:引入 ToolResult 脱水存储
1. 新建 `utils/toolResultStorage.ts`,定义一个 `MAX_INLINE_RESULT_CHARS` 阈值(建议为 100,000,约 25K Tokens)。
2. 在 `agent-core.ts` 收集 `TOOL_RESULT` 事件时,对 `result.length > 阈值` 的内容进行截断(保留前后各 10% 的摘要供大模型判断执行是否成功)。
3. 将完整内容异步写入到磁盘的 `.qwen/tool-results/` 目录下。

### 阶段 3:会话恢复清理机制
1. 当用户运行 `/clear` 清除缓存或退出长会话时,启动一个无阻塞的后台 Worker。
2. 清理超过 7 天的 `tool-results` 脱水文件,防止长期运行占用过多硬盘空间。

## 三、改进收益评估
- **实现成本**:低。Qwen Code 已有 `atomicWriteJSON` 基础工具,只需大规模重构替换。持久化降级逻辑只需几十行代码。
- **直接收益**:
1. **告别丢存档**:彻底根绝由于异常退出导致的 "Session JSON parse error" 致命 Bug。
2. **避免内存 OOM**:让 Agent 可以安全执行极长输出的打包或编译命令,无需担心缓冲区撑爆 Node.js 内存。
3. **降低 Token 费用**:只给大模型看长命令的头尾截断,阻止了无效日志刷屏带来的天价 Token 账单。
Loading