用餐厅服务的比喻,带你彻底理解 OpenCode 的主循环
🎭 场景设定:AI 餐厅
想象 OpenCode 是一家高级的 AI 餐厅:
餐厅角色
OpenCode 组件
职责
顾客
用户
提出需求
服务员
Session/Prompt
接收订单,协调流程
主厨
LLM (Claude/GPT)
烹饪(思考/生成回复)
配菜员
Tool Registry
准备食材(工具)
厨房手册
System Prompt
烹饪规范和要求
订单历史
Message History
之前的对话记录
传菜员
Stream Processor
实时传递菜品
收银台
Token/Cost Tracking
计算费用
🎬 第一幕:顾客进店(Step 1-3)
┌─────────────────────────────────────────────────────────────┐
│ AI 餐厅入口 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 顾客(用户):"我想要一份优化后的代码!" │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Step 1: 迎宾台(输入处理) │ │
│ │ 服务员记录:顾客要点什么,有什么特殊要求 │ │
│ │ - 解析 text: "帮我优化代码" │ │
│ │ - 解析 @explore: "先分析一下" │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Step 2: 安排座位(Agent 选择) │ │
│ │ 服务员判断: │ │
│ │ - 这是代码优化任务 → 安排到 "build" 主厨 │ │
│ │ - 顾客还叫了 @explore → 需要子厨房配合 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Step 3: 查看 VIP 卡(配置加载) │ │
│ │ 检查顾客的偏好设置: │ │
│ │ - 能使用哪些厨具?(工具权限) │ │
│ │ - 预算多少?(模型选择) │ │
│ │ - 过敏信息?(安全限制) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
🎬 第二幕:厨房准备(Step 4-6)
Step 4: 检查厨房空间(会话状态)
厨房经理巡视:
┌────────────────────────────────────────────────────────┐
│ "让我看看厨房还放得下多少订单..." │
│ │
│ 当前状态: │
│ ├── 已用空间: 15% ████ │
│ ├── 剩余空间: 85% ████████████████████████████████ │
│ └── 状态: ✅ 空间充足,无需清理 │
│ │
│ 如果空间不足 (>90%): │
│ └── 🧹 触发 "大扫除"(Compaction) │
│ └── AI 生成摘要,清理旧订单 │
└────────────────────────────────────────────────────────┘
Step 5: 准备厨房手册(System Prompt)
主厨拿到今天的《烹饪手册》:
┌────────────────────────────────────────────────────────┐
│ 📚 烹饪手册(System Prompt) │
├────────────────────────────────────────────────────────┤
│ │
│ 第一章:你是谁(Environment) │
│ "你是 OpenCode,最好的编程助手..." │
│ "工作目录:/Users/project" │
│ "今天是:2025-03-17" │
│ │
│ 第二章:烹饪风格(Provider Prompt) │
│ "保持简洁,不要emoji" │
│ "使用 TodoWrite 规划任务" │
│ "优先使用专用工具而非 bash" │
│ │
│ 第三章:特色技能(Skills) │
│ "可用技能:analyze-page-flow, content-summarizer..." │
│ "需要时通过 skill 工具加载" │
│ │
│ 第四章:本店规矩(AGENTS.md) │
│ "ALWAYS USE PARALLEL TOOLS" │
│ "Prefer single word variable names" │
│ │
└────────────────────────────────────────────────────────┘
Step 6: 准备厨具(Tool List)
配菜员准备工具箱:
┌────────────────────────────────────────────────────────┐
│ 🧰 工具箱(可用工具) │
├────────────────────────────────────────────────────────┤
│ │
│ 基础工具: │
│ ✅ read - 读取文件 │
│ ✅ edit - 编辑文件 │
│ ✅ bash - 执行命令(需确认) │
│ ✅ todoWrite - 任务管理 │
│ │
│ 网络工具: │
│ ❌ websearch - 顾客没开 VIP(禁用) │
│ │
│ MCP 外购食材: │
│ ✅ filesystem - 文件系统 MCP │
│ ❌ github - 今天断货了(连接失败) │
│ │
└────────────────────────────────────────────────────────┘
🎬 第三幕:主厨烹饪(Step 7)—— 核心循环!
┌──────────────────────────────────────┐
│ 🍳 主厨开始烹饪 │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 看订单 + 烹饪手册 + 工具箱 │
│ │
│ 顾客:"优化代码" │
│ 手册:"你是专家..." │
│ 工具:[read, edit, bash...] │
│ │
│ 🔥 开炒!(streamText 开始) │
└──────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 📝 文字 │ │ 🔧 工具 │ │ 🤔 思考 │
│ 输出 │ │ 调用 │ │ 过程 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
"我来分析..." "调用 read" "让我想想..."
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 读取文件 │ │
│ │ 获取内容 │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ "文件内容是..." │
│ │ │
└───────────────┼───────────────┘
▼
┌──────────────────────────────────────┐
│ 🔔 菜品完成(finish-step) │
│ │
│ - 计算成本(Token/Cost) │
│ - 拍照记录(Snapshot) │
│ - 生成账单(Usage) │
└──────────────────────────────────────┘
循环控制:出菜还是继续?
┌──────────────────────────────────────────────────────────┐
│ 循环结束判断 │
├──────────────────────────────────────────────────────────┤
│ │
│ 主厨汇报:"菜品已完成!" │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 检查:顾客还需要什么? │ │
│ │ │ │
│ │ Case 1: 直接上菜(finish reason = stop) │ │
│ │ "这是您的优化方案..." │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ result = "stop" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 🎉 退出循环,送餐给顾客! │ │
│ │ │ │
│ │ Case 2: 需要再加工(调用了工具) │ │
│ │ "让我先读取文件..." │ │
│ │ [调用 read 工具] │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ result = "continue" / "tool-calls" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 🔄 回到循环开始,继续烹饪! │ │
│ │ │ │
│ │ Case 3: 厨房满了(需要 compaction) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ result = "compact" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 🧹 触发大扫除,然后继续 │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
🛑 五种"收工"信号详解
主厨(LLM)在烹饪结束时会给出一个停止原因,就像厨师汇报"这道菜做完了":
信号
含义
餐厅比喻
处理方式
stop
AI 自然完成
"主菜已上齐,无需再加"
🎉 正常退出循环
end_turn
Anthropic 专属的自然完成
"Claude 主厨说回合结束"
🎉 映射为 stop,正常退出
tool-calls
AI 调用工具
"需要再去准备食材"
🔄 执行工具,继续循环
length
达到长度限制
"说话太多,被强制打断"
🚫 退出(可能需要提示用户)
content-filter
内容被过滤
"这道菜不符合规定被撤下"
🚫 退出
💡 关于
end_turn:这是 Anthropic Claude 模型内部的判断机制——模型通过训练学会了何时自然结束一个回合。它不是由某个可见的 Prompt 控制,而是模型的"本能"。Anthropic 并未公开这部分的系统 Prompt。
📋 其他强制停止情况
除了主厨的收工信号,还有以下情况会强制结束循环:
┌──────────────────────────────────────────────────────────┐
│ 强制停止场景 │
├──────────────────────────────────────────────────────────┤
│ │
│ 1. ❌ 权限被拒绝 │
│ └── 顾客说:"我不同意你使用菜刀(bash)!" │
│ └── 且配置:experimental.continue_loop_on_deny = false │
│ └── 结果:立即停止,result = "stop" │
│ │
│ 2. 💥 发生错误 │
│ └── 厨房设备故障(API 错误、网络问题) │
│ └── 结果:立即停止,result = "stop" │
│ │
│ 3. 📄 结构化输出完成 │
│ └── 配置了 JSON Schema,AI 成功调用 StructuredOutput │
│ └── 结果:完成任务,退出循环 │
│ │
└──────────────────────────────────────────────────────────┘
🎬 第四幕:子厨房(Subtask)
当顾客叫了 @explore(配菜专员):
┌──────────────────────────────────────────────────────────┐
│ 主厨房(Build Agent) │
│ │
│ 顾客:"@explore 先分析一下" │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Step 8: 开设子厨房(Subtask) │ │
│ │ │ │
│ │ 主厨:" explore 专员,你来处理这部分" │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 创建隔离的子厨房(子 Session) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 🏪 子厨房(Explore Agent) │ │ │
│ │ │ │ │ │
│ │ │ 职责:专门负责搜索和分析 │ │ │
│ │ │ │ │ │
│ │ │ 限制: │ │ │
│ │ │ - ❌ 不能修改菜单(禁用 todowrite) │ │ │
│ │ │ - ❌ 不能再开分店(禁用 task) │ │ │
│ │ │ │ │ │
│ │ │ 工具: │ │ │
│ │ │ ✅ read, glob, grep, bash │ │ │
│ │ │ ❌ edit, write(只读) │ │ │
│ │ │ │ │ │
│ │ │ 工作: │ │ │
│ │ │ 1. 🔍 glob 找文件 │ │ │
│ │ │ 2. 📖 read 读代码 │ │ │
│ │ │ 3. 📝 生成分析报告 │ │ │
│ │ │ │ │ │
│ │ │ 完成后返回主厨房: │ │ │
│ │ │ "分析完毕,这是报告..." │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ └──────────────────────────┼─────────────────────────┘ │
│ │ │
│ ▼ │
│ 主厨拿到分析报告,继续烹饪主菜... │
│ │
└──────────────────────────────────────────────────────────┘
🎬 第五幕:循环继续(Step 9)—— 订单历史不断增长
第一轮烹饪完成,但还需要更多食材:
┌──────────────────────────────────────────────────────────┐
│ 第二轮烹饪开始 │
├──────────────────────────────────────────────────────────┤
│ │
│ 第一轮: │
│ 主厨:"让我读取文件..." │
│ [调用 read 获取代码] │
│ │ │
│ ▼ │
│ 结果:✅ 获取到代码内容 │
│ │ │
│ ▼ │
│ result = "continue" → 🔄 不退出,继续循环 │
│ │
│ ═══════════════════════════════════════════════════ │
│ │
│ 第二轮: │
│ 主厨看到代码后:"明白了,需要这样修改..." │
│ [调用 edit 修改文件] │
│ │ │
│ ▼ │
│ 结果:✅ 文件已修改 │
│ │ │
│ ▼ │
│ result = "continue" → 🔄 继续,运行测试 │
│ │
│ ═══════════════════════════════════════════════════ │
│ │
│ 第三轮: │
│ 主厨:"让我测试一下..." │
│ [调用 bash 运行测试] │
│ │ │
│ ▼ │
│ 结果:✅ 测试通过 │
│ │ │
│ ▼ │
│ result = "continue" → 🔄 继续,生成最终回复 │
│ │
│ ═══════════════════════════════════════════════════ │
│ │
│ 第四轮: │
│ 主厨:"优化完成!这是修改总结..." │
│ [纯文本输出,没有调用工具] │
│ │ │
│ ▼ │
│ result = "stop" → 🎉 完成,退出循环! │
│ │
└──────────────────────────────────────────────────────────┘
📚 关键:订单历史(Messages)如何增长
每一轮循环,服务员手中的订单历史都在增加:
┌──────────────────────────────────────────────────────────────┐
│ 订单历史增长示意图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ Round 1: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [顾客] 帮我优化代码 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [主厨] 让我先读取文件... + [调用 read 工具] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [工具结果] 文件内容是: function foo() {...} │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════ │
│ │
│ Round 2: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [顾客] 帮我优化代码 │ │
│ │ [主厨] 让我先读取文件... + [调用 read 工具] │ │
│ │ [工具结果] 文件内容是: function foo() {...} │ │
│ │ [主厨] 明白了,需要这样修改... + [调用 edit 工具] │ │ ← 新增 │
│ │ [工具结果] 文件已修改 │ │ ← 新增 │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════ │
│ │
│ Round 3: │
│ │ [顾客] 帮我优化代码 │ │
│ │ [主厨] 让我先读取文件... │ │
│ │ [工具结果] 文件内容是... │ │
│ │ [主厨] 明白了,需要这样修改... │ │
│ │ [工具结果] 文件已修改 │ │
│ │ [主厨] 让我测试一下... + [调用 bash 工具] │ │ ← 新增 │
│ │ [工具结果] 测试通过 │ │ ← 新增 │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ⚠️ 每轮都完整重建!不是追加,是重新组装所有历史 │
│ │
└──────────────────────────────────────────────────────────────┘
🔧 Prompt 的三部分变化
组件
变化情况
说明
烹饪手册(System Prompt)
基本不变
只更新时间等动态信息,核心规范保持一致
订单历史(Messages)
✅ 持续增长
每轮追加:主厨回复 + 工具结果
厨具清单(Tools)
可能变化
权限变化或 MCP 状态变化时更新
🧹 厨房大扫除(Compaction)的时机
当订单历史太长(Token 超过阈值),就会触发"大扫除":
┌──────────────────────────────────────────────────────────────┐
│ 大扫除过程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 清理前: │
│ [顾客] 帮我优化代码 │
│ [主厨] 读取文件 + [结果: 1000行代码...] │
│ [主厨] 修改文件 + [结果: 已修改] │
│ [主厨] 运行测试 + [结果: 500行输出...] │
│ ...(太多太长了!) │
│ │
│ 清理后: │
│ [顾客] 帮我优化代码 │
│ [主厨] 读取文件 + [结果: [旧内容已清理]] │
│ [主厨] 修改文件 + [结果: 已修改] │
│ [主厨] 运行测试 + [结果: [旧输出已清理]] │
│ [总结] AI 生成:"已读取、修改、测试文件,一切正常" │
│ │
│ 💡 老工具结果被替换为占位符,保留结构但节省空间 │
│ │
└──────────────────────────────────────────────────────────────┘
🎯 总结:循环的本质
┌──────────────────────────────────────────────────────────────┐
│ 🔄 主循环 = 多轮对话 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 每次循环 = 一次 "思考-行动-观察" 的完整周期 │
│ │
│ Round 1: 思考 → 读取文件 → 观察结果 │
│ Round 2: 思考 → 修改文件 → 观察结果 │
│ Round 3: 思考 → 运行测试 → 观察结果 │
│ Round 4: 思考 → 生成回复 → 🏁 完成! │
│ │
│ 就像是: │
│ - 医生问诊(检查 → 判断 → 开药 → 复查) │
│ - 厨师做菜(准备 → 烹饪 → 调味 → 出菜) │
│ - 侦探破案(调查 → 分析 → 验证 → 结论) │
│ │
│ 直到任务完成(result = "stop")才退出! │
│ │
└──────────────────────────────────────────────────────────────┘
🍽️ 完整流程图(餐厅版)
顾客进店(用户输入)
│
▼
迎宾接待(Step 1-3:输入处理、Agent选择、配置加载)
│
▼
检查厨房空间(Step 4:Compaction检查)
│
├── 空间不足 → 🧹 大扫除(Compaction)
│
▼
准备烹饪手册(Step 5:System Prompt)
│
▼
准备厨具(Step 6:Tool List)
│
▼
┌─────────────────────────────────────────────────────┐
│ 🍳 主循环开始 │
│ │
│ while (true) { │
│ │
│ 1. 主厨看订单(组装 messages) │
│ ├── 烹饪手册(System Prompt)← 每轮重新组装 │
│ ├── 订单历史(Messages) ← 每轮增长 │
│ └── 厨具清单(Tools) ← 可能变化 │
│ │
│ 2. 开始烹饪(streamText) │
│ ├── 输出文字(text-delta) │
│ ├── 调用工具(tool-call) │
│ └── 思考过程(reasoning) │
│ │
│ 3. 出菜判断(finish-step) │
│ │ │
│ ├── "stop"/"end_turn" → 🎉 完成,退出循环! │
│ │ │
│ ├── "tool-calls" → 🔄 执行工具,继续循环 │
│ │ └── 工具结果加入订单历史 │
│ │ │
│ ├── "length"/"content-filter" → 🚫 异常退出 │
│ │ │
│ ├── 权限拒绝/错误 → 🚫 强制停止 │
│ │ │
│ └── "compact" → 🧹 大扫除,继续循环 │
│ │
│ } │
│ │
└─────────────────────────────────────────────────────┘
│
▼
送餐上桌(返回结果给用户)
│
▼
顾客用餐(用户查看结果)
🎯 核心要点速记
┌─────────────────────────────────────────────────────────────────┐
│ OpenCode 主循环核心 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 🔁 循环本质: │
│ while(true) { 思考 → 行动(工具) → 观察(结果) → 判断 } │
│ │
│ 📝 Messages 增长: │
│ 每轮新增:Assistant 消息 + Tool Results │
│ 完整重建:每轮重新组装整个 Prompt │
│ │
│ 🛑 停止条件(5个信号 + 2个强制): │
│ 正常:stop, end_turn, length, content-filter │
│ 继续:tool-calls │
│ 强制:权限拒绝, 发生错误, 结构化输出完成 │
│ │
│ 🧹 Compaction: │
│ 旧工具结果 → "[内容已清理]" 占位符 │
│ 保留结构,节省 Token │
│ │
│ 🎭 餐厅比喻记忆法: │
│ 顾客=用户, 主厨=LLM, 订单历史=Messages, │
│ 厨房空间=Context, 大扫除=Compaction │
│ │
└─────────────────────────────────────────────────────────────────┘
下次当你使用 OpenCode 时,想象一下厨房里忙碌的场景吧! 🍳👨🍳