《OpenCode 源码解析》加餐:核心逻辑主循环 —— 一场 AI 餐厅的服务之旅

0 阅读1分钟

用餐厅服务的比喻,带你彻底理解 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 时,想象一下厨房里忙碌的场景吧! 🍳👨‍🍳