OpenCode 学习笔记 04:高级功能
多 Agent、权限系统、重试机制、Snapshot 与子任务
January 22, 2026·8 min read·Yimin
#AI#Agent#OpenCode#TypeScript#Permission#Snapshot
从单一 Agent 到多 Agent 协作,从无约束到精细权限控制,从脆弱到健壮。
本篇目标
| 目标 | 实现 |
|---|---|
| 多 Agent | ✅ tiny-agent 实现 |
| 权限系统 | ✅ tiny-agent 实现 |
| 错误重试 | ✅ tiny-agent 实现 |
| Snapshot | 📖 原理介绍 |
| 子任务并发 | 📖 原理介绍 |
一、多 Agent 系统
1.1 为什么需要多 Agent?
问题:单一 Agent 什么都做,但不是什么都做得好。
用户: 帮我写一个排序算法
AI: [写代码] ✅ 擅长
用户: 搜索项目里哪里用了这个函数
AI: [搜索代码] 勉强可以,但不够高效
用户: 帮我做个计划
AI: [规划] 需要特殊约束,否则会到处改文件
解决方案:不同任务使用不同的 Agent。
1.2 OpenCode 的 Agent 类型
| Agent | 用途 | 特点 |
|---|---|---|
| build | 默认 Agent,编写代码 | 全能,可以读/写/执行 |
| plan | 规划任务 | 只能写 .opencode/plans/*.md |
| explore | 搜索代码库 | 只读,不能修改文件 |
| general | 通用子任务 | 作为子 Agent 被调用 |
| compaction | 压缩上下文 | 隐藏,系统自动调用 |
| title | 生成标题 | 隐藏,系统自动调用 |
1.3 Agent 定义结构
interface Agent {
name: string // 名称
description?: string // 描述(给 AI 看)
mode: "primary" | "subagent" | "all" // 模式
hidden?: boolean // 是否隐藏
prompt?: string // 专用 System Prompt
permission: Ruleset // 权限规则
model?: { providerID, modelID } // 指定模型
temperature?: number // 温度
}
1.4 Primary vs Subagent
┌─────────────────────────────────────────────────────────┐
│ Primary Agents │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ build │ │ plan │ │ custom │ ← 用户直接交互 │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Subagents │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ explore │ │ general │ ← 通过 task 工具调用 │ │
│ │ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
1.5 explore Agent 的约束
// explore 只允许只读操作
permission: {
"*": "deny", // 默认禁止所有
grep: "allow", // 允许搜索
glob: "allow", // 允许列表
list: "allow", // 允许目录
bash: "allow", // 允许命令(查看用)
read: "allow", // 允许读取
websearch: "allow", // 允许网页搜索
codesearch: "allow", // 允许代码搜索
webfetch: "allow", // 允许获取网页
}
1.6 tiny-agent 实现
简化设计:支持 build/explore 两种模式切换。
You: mode explore
📋 切换到 explore 模式(只读)
You: mode build
📋 切换到 build 模式(默认)
二、权限系统
2.1 为什么需要权限?
AI: 我来帮你删除这个文件... rm -rf /
用户: 😱
需求:
- 防止危险操作
- 保护敏感文件(如 .env)
- 让用户确认某些操作
2.2 权限动作
| 动作 | 含义 | 行为 |
|---|---|---|
allow | 允许 | 直接执行 |
deny | 拒绝 | 抛出错误 |
ask | 询问 | 等待用户确认 |
2.3 权限规则结构
interface Rule {
permission: string // 权限类型(工具名或特殊权限)
pattern: string // 匹配模式(支持通配符)
action: "allow" | "deny" | "ask"
}
// 示例规则
const rules: Rule[] = [
{ permission: "*", pattern: "*", action: "allow" }, // 默认允许
{ permission: "read", pattern: "*.env", action: "ask" }, // 读 .env 要确认
{ permission: "bash", pattern: "rm *", action: "deny" }, // 禁止 rm
]
2.4 规则匹配顺序
规则列表:
1. { permission: "*", pattern: "*", action: "allow" }
2. { permission: "read", pattern: "*", action: "allow" }
3. { permission: "read", pattern: "*.env", action: "ask" }
匹配时从后往前找,最后一条匹配的规则生效:
- read("src/index.ts") → 匹配 #2 → allow
- read(".env.local") → 匹配 #3 → ask
2.5 特殊权限类型
| 权限 | 说明 |
|---|---|
edit | 写/编辑文件 |
bash | 执行命令 |
external_directory | 访问项目外目录 |
doom_loop | 检测死循环 |
task | 启动子任务 |
skill | 使用 skill |
question | 向用户提问 |
plan_enter/exit | 进入/退出规划模式 |
2.6 OpenCode 的默认权限
const defaults = {
"*": "allow", // 默认允许所有
doom_loop: "ask", // 死循环要确认
external_directory: {
"*": "ask", // 访问外部目录要确认
"/tmp/truncate/": "allow", // 截断目录允许
},
question: "deny", // 禁止提问(build agent)
plan_enter: "deny", // 禁止进入规划
read: {
"*": "allow",
"*.env": "ask", // .env 文件要确认
"*.env.*": "ask",
"*.env.example": "allow", // 但 example 允许
},
}
2.7 tiny-agent 实现
简化设计:基于工具名的简单规则。
interface Permission {
tool: string
action: "allow" | "deny" | "ask"
}
// explore 模式的权限
const explorePermissions: Permission[] = [
{ tool: "read", action: "allow" },
{ tool: "grep", action: "allow" },
{ tool: "glob", action: "allow" },
{ tool: "bash", action: "ask" }, // 命令要确认
{ tool: "write", action: "deny" },
{ tool: "edit", action: "deny" },
]
三、错误处理与重试
3.1 AI 调用会失败
常见失败原因:
├── 网络问题
├── Rate Limit(请求太频繁)
├── Provider 过载(Overloaded)
├── Token 超限
└── 服务器错误
3.2 重试策略
指数退避(Exponential Backoff):
重试 1: 等待 2s
重试 2: 等待 4s
重试 3: 等待 8s
重试 4: 等待 16s
...
最大等待: 30s(无 header)或按 header 指定
3.3 OpenCode 的重试实现
// 计算等待时间
function delay(attempt: number, error?: APIError) {
// 1. 优先使用 API 返回的 retry-after-ms
if (error?.responseHeaders?.["retry-after-ms"]) {
return parseFloat(error.responseHeaders["retry-after-ms"])
}
// 2. 或者使用 retry-after(秒)
if (error?.responseHeaders?.["retry-after"]) {
return parseFloat(error.responseHeaders["retry-after"]) * 1000
}
// 3. 否则使用指数退避
const base = 2000 // 2秒
const factor = 2
return Math.min(base * Math.pow(factor, attempt - 1), 30000)
}
3.4 可重试的错误类型
| 错误 | 是否重试 | 说明 |
|---|---|---|
Overloaded | ✅ | Provider 过载 |
too_many_requests | ✅ | Rate Limit |
rate_limit | ✅ | Rate Limit |
server_error | ✅ | 服务器错误 |
no_kv_space | ✅ | KV 缓存空间不足 |
invalid_request | ❌ | 请求参数错误 |
authentication | ❌ | API Key 错误 |
3.5 tiny-agent 实现
async function callWithRetry<T>(
fn: () => Promise<T>,
maxRetries = 3
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn()
} catch (error) {
if (!isRetryable(error) || attempt === maxRetries) {
throw error
}
const delay = 2000 * Math.pow(2, attempt - 1)
console.log(`⚠️ 重试 ${attempt}/${maxRetries},等待 ${delay}ms...`)
await sleep(delay)
}
}
throw new Error("Unreachable")
}
四、Snapshot 与回滚
4.1 为什么需要 Snapshot?
AI: 我来帮你重构代码...
AI: [修改了 50 个文件]
用户: 不对!我要撤销!
AI: ...
问题:AI 可能做出错误的修改,需要能够回滚。
4.2 OpenCode 的 Snapshot 设计
核心思想:使用独立的 Git 仓库跟踪文件变化。
项目目录 Snapshot Git
/project/ ~/.local/share/opencode/snapshot/{projectID}/
├── src/ ├── objects/
├── package.json ├── refs/
└── ... └── ...
每次 AI 操作前:
1. git add . (添加所有文件)
2. git write-tree (写入 tree 对象,返回 hash)
3. 保存 hash 到消息
4.3 Snapshot 操作
| 操作 | 说明 |
|---|---|
track() | 记录当前状态,返回 hash |
patch(hash) | 获取从 hash 到现在的变更文件列表 |
restore(hash) | 恢复到 hash 状态 |
revert(patches) | 按 patch 列表逐文件恢复 |
diff(hash) | 获取从 hash 到现在的 diff |
4.4 为什么用独立 Git?
优点:
├── 不影响用户的 Git 历史
├── 不需要用户 commit
├── 可以跟踪 .gitignore 的文件
└── 独立于用户的 Git 操作
缺点:
├── 需要额外存储空间
└── 可能和用户 Git 操作冲突(极少)
4.5 回滚流程
用户: undo
1. 获取当前消息的 snapshot hash
2. 获取变更的文件列表 (patch)
3. 对每个文件:
- 如果文件在 snapshot 中存在:恢复内容
- 如果文件在 snapshot 中不存在:删除文件
4. 更新会话状态
4.6 tiny-agent 不实现 Snapshot
原因:
- 需要复杂的 Git 操作
- 用户可以用
git checkout或编辑器撤销 - MVP 阶段不需要
五、子任务与并发
5.1 什么是子任务?
Task 工具:让主 Agent 启动子 Agent 执行特定任务。
用户: 帮我分析这 5 个文件的代码质量
主 Agent:
├── Task(explore, "分析 file1.ts") ─┐
├── Task(explore, "分析 file2.ts") │ 并发执行
├── Task(explore, "分析 file3.ts") │
├── Task(explore, "分析 file4.ts") │
└── Task(explore, "分析 file5.ts") ─┘
│
▼
收集结果,生成报告
5.2 Task 工具参数
const parameters = z.object({
description: z.string(), // 简短描述(3-5 词)
prompt: z.string(), // 详细任务说明
subagent_type: z.string(), // 子 Agent 类型
session_id: z.string().optional(), // 继续已有会话
})
5.3 子任务特点
| 特点 | 说明 |
|---|---|
| 无状态 | 每次调用独立,除非传 session_id |
| 权限继承 | 子 Agent 不能超越父 Agent 权限 |
| 结果返回 | 子 Agent 的输出返回给父 Agent |
| 并发执行 | 多个 Task 可以同时运行 |
5.4 并发 vs 顺序
并发(推荐):
User: 分析 5 个文件
AI: [同时调用 5 个 Task] ← 快
顺序:
User: 先分析 A,根据结果分析 B
AI: [Task A] → [等结果] → [Task B] ← 有依赖时必须顺序
5.5 tiny-agent 不实现子任务
原因:
- 需要复杂的会话管理
- 需要并发控制
- MVP 阶段复杂度太高
六、完整流程
┌─────────────────────────────────────────────────────────┐
│ 用户输入 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 1. 选择 Agent (build / plan / explore / custom) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 2. 检查权限 (allow / deny / ask) │
└─────────────────────────────────────────────────────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
allow ask deny
│ │ │
│ 等待用户确认 │
│ │ 抛出错误
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ 3. 创建 Snapshot (记录当前状态) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 4. 执行工具 (可能失败 → 重试) │
└─────────────────────────────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
成功 失败
│ │
│ 可重试?
│ ┌────┴────┐
│ 是 否
│ │ │
│ 等待后重试 抛出错误
▼
┌─────────────────────────────────────────────────────────┐
│ 5. 返回结果 / 用户可撤销 (undo) │
└─────────────────────────────────────────────────────────┘
七、OpenCode vs tiny-agent 对比
| 功能 | OpenCode | tiny-agent |
|---|---|---|
| 多 Agent | 6 种内置 + 自定义 | build/explore 切换 |
| 权限系统 | 复杂规则 + 通配符 | 简单工具级别 |
| 错误重试 | 指数退避 + header | 指数退避 |
| Snapshot | 独立 Git 仓库 | ❌ 不实现 |
| 子任务 | Task 工具 + 并发 | ❌ 不实现 |
八、测试方法
测试模式切换
You: mode explore
📋 切换到 explore 模式
You: 写入一个文件 ← 应该被拒绝
AI: ❌ explore 模式不允许写入
You: mode build
📋 切换到 build 模式
You: 写入一个文件 ← 应该允许
AI: ✅ 已创建文件
测试权限
# explore 模式下
You: 执行 ls
AI: ⚠️ 需要确认执行命令 (y/n)?
测试重试
# 模拟 Rate Limit(需要故意触发)
⚠️ 重试 1/3,等待 2000ms...
⚠️ 重试 2/3,等待 4000ms...
✅ 请求成功
九、总结
| 完成项 | 说明 |
|---|---|
| ✅ 多 Agent | build/explore 模式切换 |
| ✅ 权限系统 | 工具级别的 allow/deny/ask |
| ✅ 错误重试 | 指数退避重试机制 |
| 📖 Snapshot | 原理介绍(不实现) |
| 📖 子任务 | 原理介绍(不实现) |
核心收获:
- 多 Agent 分工:不同 Agent 擅长不同任务,权限也不同
- 权限三态:allow(放行)、deny(拒绝)、ask(确认)
- 优雅重试:指数退避 + 响应 header 指导
- Snapshot:独立 Git 跟踪,支持任意回滚
- 子任务并发:Task 工具启动子 Agent,并行执行