Agentic Coding 深度解析:从架构原理到多 Agent 协作
AI 编程工具的演进,正在经历一次根本性的范式转变:从"补全光标处的代码",到"自主完成端到端工程任务"。这种转变有一个专有名词——Agentic Coding。
围绕 Coding Agent 的讨论,常见两种极端:将其神化为自主智能体,或将其贬为"不过是提示词工程"。两种判断都失之简单。理解这个转变,需要从三个层面展开:工具层(OpenCode 的能力边界)、框架层(多 Agent 协作编排)、方法论层(如何让 Agent 真正服务于工程流程)。本文从真实的架构出发,拆解 Claude Code、OpenCode 等工具的实现模式,厘清各自的设计取舍,深入探讨子 Agent 的本质与多 Agent 协作的核心问题。
什么是 Agentic Coding
传统 AI 编程助手的工作模式是响应式的:开发者提问,AI 回答;开发者选中代码,AI 补全。人始终是执行者,AI 是辅助工具。
Agentic Coding 的工作模式是自主式的:开发者描述目标,Agent 自主规划步骤、调用工具、执行操作、验证结果,直到任务完成。人退出执行循环,成为目标定义者和结果审查者。
这不是量变,是质变。一个能够自主编码的 Agent,需要具备:
- 代码理解能力:不只是文本匹配,而是理解代码的语义结构、类型关系、调用链路
- 工具调用能力:读写文件、执行命令、调用外部 API
- 规划与反馈能力:将大任务分解为步骤,根据执行结果调整计划
- 上下文管理能力:在有限的上下文窗口内,按需加载相关信息
其中,代码理解能力是基础,也是工具层最关键的差异点。关于代码理解能力的核心——LSP(语言服务协议),已在独立文章 LSP:语言服务协议与AI编程助手的代码理解能力 中详细阐述。
从单 Agent 到 Agent 团队
OpenCode 解决了单个 Agent 的代码理解问题。但工程任务的复杂度,往往超出单个 Agent 的能力边界。
理解这个边界在哪里,是理解为什么需要多 Agent 的前提。一个单独的 Agent,即便配备了 LSP 和完整的工具权限,仍然面临三个结构性限制:上下文窗口有限(无法同时持有整个大型项目的全貌)、角色混淆(规划者和执行者是同一个 Agent,容易在"想清楚"和"动手做"之间反复横跳)、无法真正并行(单线程的问答流水线,无法同时搜索文档和修改代码)。
这些限制不是模型能力的问题,而是单 Agent 架构的天花板。突破它,需要引入外部编排层——把规划、执行、验证拆给不同的角色,让它们并行工作、相互制衡。
编程 Agent 的本质
在讨论任何具体的 Agent 工具或框架之前,有必要先把"编程 Agent 到底是什么"这个问题说清楚。理解了本质,才能理解为什么各种框架要做它们做的事。
一个编程 Agent,剥去所有包装,本质上是四个要素的组合:
while loop:Agent 不是一次性的问答,而是一个持续运行的循环。每一轮循环,Agent 观察当前状态、决定下一步行动、执行行动、再观察新状态。循环在何时退出,取决于完成信号的检测——可以是文件中的特定标记(如 DONE),可以是测试通过,也可以是外部脚本的判断。
system prompt:Agent 的"人格"和"职责"由 system prompt 定义。它告诉 Agent:你是谁、你的目标是什么、你能用哪些工具、你应该遵守哪些约束。system prompt 是 Agent 行为的根本来源——同一个底层模型,配上不同的 system prompt,就变成了完全不同的 Agent。这也是为什么多 Agent 框架的核心工作,往往是精心设计每个角色的 system prompt,而非替换底层模型。
tool calling:Agent 通过工具与外部世界交互。读文件、写文件、执行 Shell 命令、调用 API——这些都是工具。工具调用是 Agent 从"思考"到"行动"的桥梁。没有工具,Agent 只能输出文本;有了工具,Agent 才能真正改变世界的状态。
persistent state(文件系统):Agent 的记忆不在上下文里,而在文件系统中。代码文件、任务列表、中间结果——这些都是持久化的状态。文件系统是 Agent 跨轮次、跨会话保持连贯性的基础。这也是为什么 Agent 框架普遍依赖文件来传递状态:task.md、boulder.json、.sisyphus/plans/——本质上都是在用文件系统弥补上下文窗口的有限性。
1 | |
一个循环统治一切
先说结论:所有 Coding Agent 的核心,都是一个 while(tool_use) 循环。
通过对 Claude Code 实际 API 流量的追踪分析,其核心逻辑可以用伪代码描述如下:
1 | |
没有复杂的状态机,没有多 Agent 协调框架,没有专门的"停止决策模块"。模型自己决定什么时候停——当它不再调用工具,循环就自然结束。
这个设计有一个被低估的优雅之处:模型可以通过"输出纯文本而不调用工具"来向用户提问或汇报进度,不需要任何额外的机制。
messages 数组:唯一的状态载体
上述伪代码中的 context,在所有主流 LLM API 里的实际形态是一个 messages 数组。没有其他格式——Agent 的全部状态,包括历史、推理过程、工具调用结果,都存储在这个数组里。
每条消息有四种角色,工具调用的插入位置是固定的:
1 | |
每一轮循环的操作模式是:模型输出 assistant 消息(含 tool_calls)→ 框架执行工具 → 把结果以 tool 角色追加 → 再次调用模型。Agent 框架在做的核心事情,就是决定往这个数组里追加什么、以什么顺序追加、以及何时停止追加。
这个数组的增长方式,取决于框架策略:
| 策略 | 描述 | 适用场景 |
|---|---|---|
| 无限叠加 | 保留所有消息,数组持续增长 | 短任务、大 context 窗口 |
| 工具结果清除 | 保留工具调用记录但丢弃返回内容,减少冗余 | 工具结果可重新获取的场景 |
| 压缩摘要 | 用 LLM 总结历史,用摘要替换原始消息数组 | 超长任务(Claude Code 的 compaction 机制) |
| 滑动窗口 | 丢弃旧消息,保留最近 N 条 | 长对话、有限 context 窗口 |
这四种策略的激进程度递增,但本质上都在做同一件事:改写 messages 数组,用一个更短的数组替代原来的数组,然后基于这个更短的数组继续推理。这就是上下文压缩的全部秘密——没有魔法,只有对数组的精确操纵。
终止条件的最简实现:不需要专门的 stop 工具或终止正则,让模型的"不调用工具"行为本身成为停止信号,是最低耦合的设计。
为什么不用 LangChain / LangGraph 构建 Coding Agent
理解了编程 Agent 的本质——while loop + system prompt + tool calling + persistent state——一个自然的问题浮出水面:既然有 LangChain、LangGraph 这样成熟的 Agent 框架,为什么主流 Coding Agent 都不用它们?
这不是一个偶然的选择,而是一个深思熟虑的架构决策。理解这个决策,需要先搞清楚三件事:框架提供了什么、Coding Agent 需要什么、以及两者之间的结构性错配在哪里。
框架提供了什么
LangChain(2022 年发布)和 LangGraph(2024 年初发布)是当前最流行的 Agent 开发框架,它们提供了多层抽象:
| 抽象层 | LangChain | LangGraph |
|---|---|---|
| 模型层 | 统一不同 LLM 提供商的接口(OpenAI、Anthropic、Google 等) | 继承 LangChain 的模型抽象 |
| 提示词层 | 模板化提示词管理,支持变量注入 | 同上 |
| 编排层 | Chains(线性流程)、Agents(ReAct 循环) | StateGraph(有向图)、条件边、持久化状态 |
| 记忆层 | 对话历史管理、自定义状态 | 图级别的状态传递 |
| 工具层 | 数百种预建工具和向量数据库连接器 | 同上 |
| 检索层 | Vector retrievers、Web retrievers(RAG 场景) | 同上 |
LangGraph 在 LangChain 基础上引入了 DAG(有向无环图)哲学,将工作流建模为 StateGraph,通过 Nodes(执行节点)和 Edges(控制流边)定义 Agent 的行为拓扑。这对需要精确控制分支和错误处理的复杂多步任务很有价值。
直接用 SDK 构建 Agent 有多简单
与框架的多层抽象形成鲜明对比的是,直接使用 LLM SDK 构建一个功能完整的 Agent,代码量惊人地少:
1 | |
这就是一个完整的 Agent。 没有 Chain,没有 Graph,没有 Memory 抽象,没有 Retriever。只有一个 while 循环、一个 API 调用、和一个工具执行函数。前文描述的 Claude Code 核心循环,本质上就是这段代码的工程化版本。
核心矛盾:框架税与上下文控制权
那么,为什么不在这个简单循环外面套一层框架呢?答案在于一个被低估的概念——框架税(Framework Tax)。
框架税是指使用框架构建 Agent 相比直接使用 SDK 所产生的额外开销,包括:
| 税种 | 具体表现 | 影响 |
|---|---|---|
| Token 税 | 框架在后台注入的额外提示词、中间件处理、状态序列化 | 生产环境中可达 2-3 倍的额外 Token 消耗 |
| 延迟税 | 多层抽象的运行时开销、状态图遍历 | 每次工具调用增加 300-400ms 延迟 |
| 认知税 | 需要理解 Chain/Agent/Memory/Graph 等框架概念 | 学习曲线陡峭,调试时需要穿透多层抽象 |
| 控制权税 | 框架决定了 messages 数组的操纵方式 | 无法精确控制每一步模型看到什么信息 |
前三种税在通用 Agent 场景中或许可以接受——用开发效率换运行效率,是合理的工程权衡。但第四种税——控制权税——对 Coding Agent 来说是致命的。
回顾前文的分析:Claude Code 的核心竞争力是上下文工程——精确控制 messages 数组的注入、定位、保护、清理、重复五个维度。这种精确控制要求框架层对 messages 数组的每一次操作都是透明的、可预测的、可定制的。
而 LangChain/LangGraph 的设计哲学恰恰相反:它们通过抽象隐藏了 messages 数组的操纵细节。Chain 自动管理消息流转,Memory 自动处理历史压缩,Agent 自动决定工具调用策略。这些"自动"在通用场景中是便利,在 Coding Agent 场景中却是障碍——你无法精确控制 system reminder 的注入时机,无法实现 Claude Code 那样的 U 形注意力利用策略,无法定制 compaction 的保留规则。
用一句话概括:LangChain 把 messages 数组当作实现细节来隐藏,Coding Agent 把 messages 数组当作核心资产来操纵。两者的设计哲学根本冲突。
Anthropic 的官方立场
Anthropic 在其广受引用的工程博客 Building Effective Agents(2024 年 12 月)中,对框架选择给出了明确建议:
“When building applications with LLMs, we recommend finding the simplest solution possible, and only increasing complexity when needed. This might mean not building agentic systems at all.”
更进一步,Anthropic 建议开发者先从 API 直接开始,而非从框架开始——理由是直接使用 API 可以确保开发者理解底层机制,避免被框架的抽象遮蔽了对模型行为的真实理解。
这个建议引发了社区争议。LangChain 的支持者认为框架能捕获社区智慧、避免重复造轮子。但 Anthropic 的核心论点——优先理解底层机制,避免为复杂度而复杂度——与 Coding Agent 领域的实践高度吻合。
主流 Coding Agent 的技术选择
事实胜于雄辩。截至 2025 年中,没有任何一个主流 Coding Agent 使用 LangChain 或 LangGraph 作为核心框架:
| 工具 | 技术栈 | 框架选择 |
|---|---|---|
| Claude Code | TypeScript + Anthropic SDK | 无框架,自建 Agent 循环 |
| Cursor | TypeScript + 多模型 SDK | 无框架,自建编辑器集成 |
| Aider | Python + LiteLLM(多模型适配层) | 无框架,自建 Git 集成的 Agent 循环 |
| OpenCode | Go + 多模型 SDK | 无框架,自建 TUI Agent 循环 |
| Codex CLI | TypeScript + OpenAI SDK | 无框架,自建沙箱化 Agent 循环 |
| Devin | 自研全栈 | 无框架,自建浏览器+终端+编辑器集成 |
这些工具的共同选择是:直接使用 LLM 提供商的 SDK,自建 Agent 循环,自行管理 messages 数组。它们宁愿从零实现上下文压缩、工具调用路由、子 Agent 调度,也不愿引入框架的抽象层。
LangChain 的真正适用场景
这并不意味着 LangChain/LangGraph 没有价值。它们在以下场景中仍然是合理的选择:
| 场景 | 为什么适合 | 典型用例 |
|---|---|---|
| RAG 应用 | 检索-生成流程相对固定,框架的预建 Retriever 和 Vector Store 集成节省大量工作 | 企业知识库问答、文档搜索 |
| 客服/对话 Agent | 对话流程可建模为状态机,LangGraph 的 StateGraph 天然适配 | 多轮对话机器人、工单处理 |
| 快速原型 | 需要快速验证想法,不关心生产级性能 | Hackathon 项目、概念验证 |
| 多模型编排 | 需要在多个 LLM 提供商之间切换,框架的统一接口有价值 | 模型评测、A/B 测试 |
关键区别在于:这些场景对 messages 数组的精确控制要求远低于 Coding Agent。RAG 应用的上下文结构相对固定(检索结果 + 用户问题),客服 Agent 的对话流程可预测,快速原型不需要考虑长对话的上下文腐化。而 Coding Agent 面对的是开放式的、长周期的、高度动态的任务,每一步的上下文配置都可能影响最终结果。
从框架到 SDK:一个正在发生的趋势
值得注意的是,2024-2025 年间,LLM 提供商自己开始发布轻量级的 Agent SDK:
- OpenAI Agents SDK(2025):强调"极简且强大"(Minimalism with power),提供 Agent 定义、工具注册、内置 ReAct 循环,但不引入 Chain/Graph 等重型抽象
- Anthropic Claude Agent SDK:秉持"给 Agent 一台计算机"的理念,内置文件系统和 Shell 访问,让 Agent 像人类一样操作计算机
这些官方 SDK 的设计理念与 LangChain 截然不同:它们不试图抽象掉 LLM API 的细节,而是在保留 API 透明性的前提下,提供最小化的便利层。这印证了一个趋势——Agent 开发正在从"框架驱动"转向"SDK 驱动",开发者越来越倾向于保留对底层机制的直接控制。
本质:Coding Agent 是上下文工程问题,不是工作流编排问题
回到最初的问题:为什么不用 LangChain/LangGraph 构建 Coding Agent?
答案可以用一句话概括:因为 Coding Agent 的核心挑战是上下文工程,而非工作流编排。
LangChain/LangGraph 解决的是"如何把多个步骤串联成工作流"的问题——定义节点、连接边、管理状态转移。这对 RAG 管道、客服流程等结构化工作流非常有效。
但 Coding Agent 的核心挑战不在于"步骤怎么串联"(一个 while 循环就够了),而在于"每一步模型看到什么信息"——如何在有限的上下文窗口里,精确配置最可能产生期望行为的 token 集合。这是一个上下文工程问题,需要对 messages 数组的每一个维度(注入、定位、保护、清理、重复)进行精细控制。
框架的抽象层,恰恰挡在了开发者和 messages 数组之间。
这也解释了为什么 Claude Code 的架构如此"简单"——14 个工具、一个 while(tool_use) 循环、一个 messages 数组。简单不是因为能力不足,而是因为复杂度被有意地放在了正确的地方:不在框架的抽象层里,而在对 messages 数组的精确操纵中。
Claude Code 的真实架构
工具集:精简而非全能
Claude Code(截至 2025 年中,基于 system prompt 追踪分析)共有 14 个工具,分为四类:
| 类别 | 工具 | 设计意图 |
|---|---|---|
| 命令行 | bash、glob、grep、ls |
bash 需要用户审批,其余不需要 |
| 文件操作 | read、write、edit、multi_edit、notebook_read、notebook_edit |
Jupyter 单独处理,因为原始格式极长 |
| 网络 | web_search、web_fetch |
基础信息获取 |
| 控制流 | todo_write、task |
计划管理与子 Agent 调度 |
没有 critic 模式,没有角色扮演,没有复杂的记忆数据库。简单是刻意的选择,不是能力不足。
TODO 列表:外化的计划
Claude Code 的第一个工具调用几乎总是 TodoWrite,创建一个结构化的任务列表:
1 | |
这个设计解决了一个关键问题:模型在执行数百步操作后会"忘记"自己在做什么。TODO 列表是外化的工作记忆,不依赖模型的上下文记忆能力。
更聪明的是,工具调用的返回结果里会附带提醒文字,要求模型继续使用 TODO 列表跟踪进度。指令在工具结果里重复出现,比只放在 system prompt 里遵从率高得多。
计划外化原则:把计划写成可被工具读写的结构化文件(TODO 列表、plan.md),比依赖模型"内化"计划更确定、更可控。内化的计划会随着上下文增长而衰减,外化的计划不会。
System Reminder:动态注入的上下文
Claude Code 会在每次用户消息后附加 <system-reminder> 块,内容随状态动态变化:
- 会话开始:注入基本行为约束(不要主动创建文件、不要做多余的事)
- TODO 列表为空时:提醒模型考虑是否需要创建计划
- TODO 列表更新后:把最新的任务状态注入上下文
这不是"提示词工程",而是程序化的上下文管理——根据 Agent 的运行状态,精确控制每一步模型能看到什么信息。
子 Agent:上下文隔离与并行
当任务复杂到单个上下文窗口装不下,Claude Code 会通过 task 工具派发子 Agent。子 Agent 是一个完整的 Claude Code 实例,接收相同的 system prompt,但有自己独立的上下文窗口。
关键设计决策:子 Agent 不接收主 Agent 的上下文历史,且在设计上被约束为不能再派发子 Agent(防止无限递归)。子 Agent 的 system prompt 中包含对其行为边界的约束,但其上下文窗口是全新的、干净的——这正是上下文隔离的核心价值。这一设计保证了行为的可预测性。值得注意的是,这个约束只针对 Agent 实例的创建——Skill(通过 SKILL.md 注入的行为扩展)则可以无限递归调用,因为 Skill 是上下文内的提示词注入,不涉及新 Agent 实例的创建。
关于子 Agent 的本质,还有一个常被误解的问题:"子 Agent"是关系描述,不是能力描述。子 Agent 可以拥有与主 Agent 完全相同甚至更强的能力,它的核心价值在于上下文隔离,而非能力弱化。详细分析见后文"子 Agent 的本质"章节。
子 Agent 的两个用途:
- 上下文管理:把大任务拆成小任务,每个子任务有干净的上下文
- 并行加速:多个独立子任务可以同时执行
安全检查:用小模型做守门员
一个鲜为人知的细节:Claude Code 在执行 bash 命令前,会把命令发给 Claude Haiku(最小最快的模型)做安全检查,判断命令是否读取或修改了敏感文件。
这个设计体现了一个重要的工程权衡:用便宜的小模型做确定性判断,不让安全检查拖慢主循环。Haiku 的输出是结构化的 XML,不是自然语言,降低了解析的不确定性。
异构模型协作:主循环用强模型做复杂推理,安全/分类等确定性任务用小模型,是成本与能力的最优分配。
Claude Code 的核心竞争力:上下文工程
上文所描述的每一个机制——system reminder 的动态注入、TODO 列表的外化记忆、子 Agent 的上下文隔离、messages 数组的增长策略——本质上都在做同一件事:精确控制每一步模型能看到什么信息。这不是"改改 prompt 就能做到的事",而是对模型行为的系统性工程,需要对模型的注意力机制、上下文衰减、工具调用模式有深刻理解。
Anthropic 在其工程博客中给出了精确定义:Context Engineering(上下文工程)是在 LLM 推理期间策划和维护最优 token 集合的策略集合。它与 Prompt Engineering 的区别不在于程度,而在于维度——Prompt Engineering 关注"如何写好一段提示词",Context Engineering 关注"在每一次推理调用中,什么样的 token 配置最可能产生期望行为"。后者是一个迭代过程,涵盖 system prompt、工具定义、messages 历史、外部数据的全部组合。
一个有力的佐证:Claude Code 的 system prompt 和工具集在每次版本更新时都会变化,这些变化直接影响模型行为。这说明 Anthropic 在持续做的是 Context Engineering,而不是一次性的提示词设计。
操纵 messages 数组的艺术
前文提到 messages 数组是 Agent 的唯一状态载体。但"操纵 messages 数组"远不止"快满了就压缩"这一件事。Agent 框架对 messages 数组的操纵,至少包含五个维度:注入、定位、保护、清理、重复。 压缩只是清理维度的一个子集。
维度一:注入(Injection)——往数组里放什么
messages 数组中有四种角色的消息,每种角色的语义和注意力权重截然不同:
| 角色 | 语义 | 注意力权重 | 是否被压缩 |
|---|---|---|---|
| system | 全局行为指令,定义模型身份和约束 | 最高——位于数组开头,享有首因效应(primacy bias) | ❌ 永远不会被压缩 |
| user | 用户输入,包含实际请求和上下文 | 高——最新的 user message 享有近因效应(recency bias) | ✅ 历史 user message 会被压缩 |
| assistant | 模型响应,包含思考过程和工具调用 | 中——随对话增长被推向中间位置 | ✅ 会被压缩 |
| tool | 工具执行结果,返回给模型 | 中——与 assistant 的工具调用配对出现 | ✅ 会被压缩或清除 |
Claude Code 的注入策略是分层的,不同类型的信息被注入到不同角色的消息中:
第一层:system prompt(不可变层)。 核心身份定义、基础行为规则、工具使用指南、安全声明。这些内容约 16,000-23,000 tokens,对所有用户共享,永远不会被压缩。Claude Code 的 system prompt 有一个精妙的设计:安全声明在 prompt 的开头和结尾都出现——利用 U 形注意力曲线的两个峰值,确保安全约束在任何上下文长度下都不会被忽略。
第二层:system prompt 动态扩展(半不可变层)。 Output Style(如 software-architect 风格指令)被追加到 system prompt 数组中。这些内容随用户配置变化,但在单次会话内保持稳定,同样不会被压缩。
第三层:<system-reminder> 注入(可变层)。 这是 Claude Code 最独特的机制。CLAUDE.md 的内容、skill 元数据、当前日期、Git 仓库状态等信息,被包裹在 <system-reminder> 标签中,作为 user message 注入到对话历史中:
1 | |
这个设计有一个关键的工程权衡:CLAUDE.md 的内容虽然用 <system-reminder> 标签标记为"系统级",但它的实际角色是 user message。这意味着它享有 user message 的注意力权重,但也承受 user message 的命运——会被压缩。这个设计选择是为了兼容 prompt caching(后文详述),但也埋下了 skill 在长对话中失效问题的种子。
第四层:工具调用与结果(动态层)。 每一轮循环产生的 assistant tool_call 和 tool result 消息。这些是数组增长最快的部分,也是压缩的主要目标。
维度二:定位(Positioning)——放在数组的哪个位置
Stanford 的 Lost in the Middle 论文(arXiv:2307.03172,引用 3700+)揭示了一个关键发现:LLM 对 messages 数组中不同位置信息的注意力呈 U 形分布——开头和结尾的信息获得最高注意力,中间位置的信息最容易被忽略。
1 | |
Claude Code 的定位策略充分利用了这个特性:
- 开头位置(primacy bias):system prompt 固定在数组最前面,核心规则前置,身份定义优先
- 结尾位置(recency bias):最新的 user message 和最近的工具调用结果自然位于数组末尾
- 中间位置(注意力低谷):历史对话、早期的工具调用结果——这些是压缩的首选目标,因为即使不压缩,模型对它们的注意力也已经很低了
这解释了一个反直觉的现象:为什么 Claude Code 的 system prompt 长达 16,000+ tokens 却不会显著影响性能。因为 system prompt 位于数组开头,享有最高的注意力权重;而且它通过 prompt caching 被缓存为不可变前缀,不会随对话增长而被"推"到中间位置。
维度三:保护(Protection)——哪些内容不能丢
并非 messages 数组中的所有内容都是平等的。Claude Code 建立了一个隐式的信息保护层级:
| 保护级别 | 内容 | 保护机制 |
|---|---|---|
| 永久保护 | system prompt、工具定义 | 位于 API 调用的顶层参数,不在 messages 数组中,永远不会被压缩 |
| 缓存保护 | system prompt + Output Style | 通过 prompt caching 创建不可变前缀,缓存后的内容不会被修改 |
| 摘要保留 | 架构决策、未解决的 bug、最近 5 个文件 | compaction 时被明确要求保留 |
| 无保护 | 历史工具调用结果、早期对话、CLAUDE.md 内容、skill 内容 | 压缩时可能被摘要化或丢弃 |
Prompt Caching 的保护作用值得单独说明。Claude API 的 prompt caching 通过前缀匹配工作:tools → system → messages 按此顺序构成缓存前缀。前缀中的任何变化都会使缓存失效。Claude Code 围绕这个机制做了一个关键的架构决策:所有用户共享相同的 system prompt。这使得 system prompt 成为一个稳定的、可缓存的前缀,减少 90% 的输入 token 成本和 85% 的延迟。
但这也意味着,任何需要个性化的内容(CLAUDE.md、skill、项目规则)都不能放在 system prompt 中——否则会破坏缓存共享。这就是为什么这些内容被降级为 user message 注入,也是它们在压缩时容易丢失的根本原因。这不是一个 bug,而是一个成本与持久性之间的工程权衡。
维度四:清理(Pruning)——从数组中移除什么
这是大多数人理解的"上下文压缩",但它实际上包含三个递进的层次:
第一层:工具结果清除(Tool Result Clearing)。 最轻量的清理形式。Agent 保留"我调用了 read_file 读取了 config.yaml“这个记录,但丢弃实际返回的文件内容。Anthropic 已将此作为 Claude Developer Platform 的正式功能发布(clear_tool_uses)。这是"最安全的轻触式压缩”——不丢失任何决策上下文,只丢弃可重新获取的原始数据。
第二层:上下文摘要压缩(Compaction)。 最核心也最复杂的清理形式。其操作可以用一句话概括:把整个 messages 数组传给 LLM,让它生成一个高保真摘要,然后用这个摘要替换原始数组,开始一个新的上下文窗口。
1 | |
压缩的艺术在于选择保留什么、丢弃什么——过于激进的压缩会丢失微妙但关键的上下文。Anthropic 在其工程博客中给出了调优建议:先最大化召回率(确保压缩 prompt 捕获了 trace 中的每一条相关信息),再迭代提升精确率(消除多余内容)。
第三层:结构化笔记(Structured Note-taking)。 不是对 messages 数组的直接操作,而是一种补偿机制:Agent 在运行过程中主动将关键信息写入外部存储(TODO 列表、memory 文件),使得即使上下文被压缩甚至完全清空,关键状态也不会丢失。
维度五:重复(Repetition)——关键指令的多点锚定
这是最容易被忽视、却可能是最重要的维度。Claude Code 的一个核心设计哲学是:关键指令不能只出现一次,必须在 messages 数组的多个位置重复出现。
具体实现包括:
- system prompt 首尾重复:安全声明在 system prompt 的开头和结尾都出现,利用 U 形注意力的两个峰值
- tool result 中嵌入提醒:每次
todo_write工具调用的返回结果中,都会附带固定文字提醒模型"keep using the TODO list to keep track"。这意味着只要模型在使用 TODO 列表,每次工具调用都会重新强化这条指令 <system-reminder>的每轮注入:CLAUDE.md 的内容不是只在会话开始时注入一次,而是在每个 user turn 中都重新注入。这确保了即使中间的 CLAUDE.md 内容被注意力衰减影响,最新一轮的注入仍然位于数组末尾的高注意力区域
为什么重复比单次注入更有效?原因有三:
- 对抗注意力衰减:单次注入的信息会随着对话增长被推到中间位置(注意力低谷),重复注入确保关键信息始终出现在高注意力区域
- 提供容错冗余:即使某一次注入被模型忽略,其他位置的重复仍然有效
- 压缩后恢复:即使历史中的注入被压缩掉,最新一轮的注入仍然完整保留
指令在工具结果里重复出现,比只放在 system prompt 里遵从率高得多——这不是经验之谈,而是 Claude Code 团队通过大量 A/B 测试验证的工程结论。
五个维度的协同:一个完整的例子
以 Claude Code 处理一个复杂编码任务为例,观察五个维度如何协同工作:
1 | |
这个例子展示了一个关键洞察:messages 数组的操纵不是某个单一时刻的操作,而是贯穿 Agent 整个生命周期的持续性工程。每一条消息的注入位置、每一次清理的时机、每一条指令的重复频率,都是精心设计的。
案例:当关键指令在信号竞争中被压制
前面讲的五个维度都是工具侧视角——Claude Code 这类 Agent 框架如何主动编排 messages 数组。但作为用户,面对同一个 context window,往往只有一个可以直接触达的入口:当前 turn 的 user message。用户发出去的那句话在数组里通常不长、没有 <system-reminder> 标签、也不会被平台自动重复注入,靠什么和已经占满前文的 system prompt、CLAUDE.md / AGENTS.md、skill description 抢注意力?这是实际用 Agentic Coding 工具时一个容易被忽视、却又直接决定"意图到底会不会被执行"的问题。
最近在一个实际项目里,围绕一套自研的 SDD 流程(暂且叫它 omospx,与系统里已注册的 openspec-* 是两个独立家族),连续出了一次偏移事故和一次忠实执行,两次的 system prompt、AGENTS.md、注册的 skill 列表完全相同,差别只在用户第一句话的关键词形态——是一个可以拿来拆 context window 信号权重的干净样本。
相同容器,相反结果
项目背景是这样的:.opencode/AGENTS.md 有大约 250 行,通篇用 openspec-explore / openspec-new-change / openspec-continue-change 这种命令级精确名推荐 openspec 流程;自研的 omospx 家族(oh-my-open-spec-*)只在 system prompt 的 skill 注册表里与 openspec-* 平级列出,没有任何额外加权。.opencode/AGENTS.md 是项目级配置,按 opencode 的触发性注入规则,模型首次 read/edit 到项目文件时,平台才通过 <system-reminder> 把这 250 行塞进对话(后文 §"控制源的分层"会专门拆这套机制)——一旦塞进来,它就坐进了对话开头的高注意力区,把 “openspec 就是这个项目的默认流程” 的 prior 持续压在模型的注意力焦点上。
在这个容器里发生的两次对话:
偏移侧(失败样本)。用户第一条消息是"为这个服务增加一个 schedulerx 任务,定时扫描 bizType 为 life 的线索,用日志打印出来"——完整的业务需求、没提任何框架。几轮 question 之后,在一次澄清回答里才提到:“我不按 agents.md ,就走普通的 omospx 流程”。Agent 随后直接调了 skill("openspec-explore")、skill("openspec-new-change")、skill("openspec-push-specs")——完全落到了 openspec 家族,自研的 omospx 总路由 skill 从头到尾没被加载,baseline / kb / rules / RED-GREEN 证据闭环全部缺失。
忠实侧(成功样本)。同一个项目、同一份 AGENTS.md,用户第一条消息主体完全一样,只是前面多了一个前缀:“基于 omospx-start,为这个服务增加一个 schedulerx 任务,定时扫描 bizType 为 life 的线索,用日志打印出来”。Agent 的第一个动作就是 skill("oh-my-open-spec-start"),随后按 omospx 的标准流程把 proposal / spec / design / tasks 四件套落到正确目录,/omospx-apply 阶段还主动跑 check-apply-completion.sh 门禁脚本、按脚本输出的 evidence_incomplete 补齐 sidecar,review-ready 未过时全程只改 md,不动业务代码——一次完整的忠实执行。
两次对话的业务请求文本逐字相同,差别只在忠实侧多了"基于 omospx-start,"这 12 字符 + 1 逗号的前缀——差别只在用户第一句话里有没有出现 omospx-start 这个带连字符的、与 skill 名字面同形的 token。
信号竞争的权重账
把 context window 里这几路信号的注意力权重定性排一下(下表里的"权重"是结构性评估,不是数值化测量):
| 信号源 | 长度 | 标签包装 | 具体度 | 位置 | 默认权重 |
|---|---|---|---|---|---|
| AGENTS.md 推 openspec | ~250 行 | <system-reminder> |
命令级精确 | 一旦注入即长驻对话开头 | 高 |
| system prompt 的 skill 注册表 | 中等 | <available_skills> |
中性(只有 name + description) | 首部、缓存保护 | 中性 |
| 用户当前 turn 的首句 | 短但可精确 | 无 | 视措辞而定 | 末尾(recency) | 视措辞决定,可高可低 |
偏移侧之所以偏,是因为用户那句"普通的 omospx 流程"在 LLM 的歧义消解里有两种合法解析:
1 | |
AGENTS.md 用命令级精度反复推 openspec、长度占据 250 行、被 <system-reminder> 标签包裹后长驻对话开头——这整套信号已经把"普通流程 = openspec"的 prior 焊死在模型的前向推理路径上。用户那 17 个汉字只出现在几轮之后的澄清回答里、没有任何结构化标签——在注意力分布上根本抢不过前面那堵墙。更致命的是 oh-my-open-spec 本身就是"oh-my- 前缀 + open-spec"的组合,而 openspec 就是把 open-spec 去掉连字符、拼成一个词——两个名字在拼读和字母序列上确实像一个家族的两种写法,模型顺手做了一次"名字差不多,意思也差不多吧"的语义跳跃,偏移就此发生。
忠实侧之所以忠实,是因为 omospx-start 这个 token 把歧义消解的空间直接压到零:
- 它是带连字符的精确名,与
oh-my-open-spec-start这个 skill 的注册名可以做字面级匹配,不需要语义跳跃 - 它出现在 user message #1 的首句主语位置,天然是当前 turn 的"主诉",而不是 question 回答的附属说明
- 它没有被"普通的"之类形容词修饰,没有给出任何可被 AGENTS.md prior 污染的歧义入口
相同的 AGENTS.md、相同的注入机制,压不过一个 12 个字符、位于数组末尾、与 skill 名字面同形的 token(omospx-start:6 字母 + 1 连字符 + 5 字母)。这里可以抽出一个和前面五个维度互补的事实——五维度讲的是工具侧怎么编排消息数组,而用户侧能动用的是另一组杠杆:定位(recency + 主语位置)+ 字面精确度,这两者的乘积在特定条件下足以反转长文本 anchor 的优势。前提是用户知道自己在发送"工程化的意图信号",而不是"自然语言的请求"。
给 OpenCode 用户的 context window 设计法则
把两次对话抽出来看,用户侧其实可以把"发一句话"当成一个意图工程问题来设计,让自己的信号在 context window 里拿到最高权重、能精准触发想要的分支、能覆盖已有的 prior、又不容易被后续注入反向覆盖。操作性结论如下:
一、用字面同形的 token,不要用框架名。能写 /omospx-start 就不要写"omospx";能写 oh-my-open-spec-apply 就不要写"那个 apply skill"。工具侧的 skill / command 是注册名匹配的,你发出的 token 和注册名字符级重合度越高,LLM 的 routing 需要的语义跳跃就越少,被 AGENTS.md 这种强 prior 截胡的概率就越低。连字符、斜杠、冒号(比如 openspec 原生 CLI 的 /opsx:explore、omospx 的 /omospx-start)这些"非自然语言字符"反而是优势——它们本身就是"这是命令级 token、不是普通名词"的信号。
二、把关键 token 放在 user message #1 的首句首位。放在首位有两个好处:一是它是当前 turn 的主语,模型会用它做意图分类;二是后续的 question / 澄清回答都挂在它下面,不会出现"框架名到第二轮才出现"这种把关键信号藏在次要位置的结构。任何埋在后续 turn 里的关键词——无论是 question 回答、tool result 还是多轮澄清的中途——都要和会话开头已经坐在 attention sink 的 system prompt / AGENTS.md 抢权重,天然吃亏。如果第一句没说清楚,最稳的做法是直接在下一条 user message 里重新发一条以精确 token 开头的指令(TUI 里就是再敲一次 Enter 发一条新消息即可,不需要开新会话),而不是继续在 question 回答里补说明——前者把信号放回 recency 区、后者仍埋在次要位置。
三、避开会引发二义解析的形容词。“普通的 X 流程”、“默认的 X 套路”、“简单的 X 方式"这类措辞本意是"X 框架的标准玩法”,但 LLM 在有强 prior 的情况下会把修饰词从后往前解析成"普通流程(恰好属于 X)"。想表达"标准玩法",直接说"按 X 的标准流程"或"按 X 的 start 流程",不要加"普通 / 默认 / 一般 / 简单"这种可与 prior 串联的形容词。
四、直接用 slash command 走硬开关。TUI 里敲 /omospx-start 和在自然语言里说"走 omospx-start"的执行路径不同——后文 §“Slash Command 的本质” 会对着文档、issue、提交历史详细拆,这里只用它那节的结论:slash command 会被工具当成 inline macro 就地展开、把对应模板整段以 user message 身份追加到对话末尾,这是一条绕过 LLM 歧义消解的强制注入路径,比任何自然语言措辞都稳。后文 §“控制源的分层"的收尾处还指出一条关键边界——slash command “没有互斥锁、关不掉旧指令源”。但在当前这个场景里,我们要的恰恰不是"关掉 AGENTS.md”,而是"把自己这条信号临时刷到最前",slash command 正好匹配这种需求。
五、冲突发生时,显式说出"我知道 AGENTS.md 推 Y,但我要 X"。当你确实要走一条与项目默认规则相反的流程时,直接把冲突写进 user message——“我不走 AGENTS.md 里的 openspec,用 omospx-start”。这一句的关键不在于说服 LLM,而在于把两个信号源都提到同一个 turn 的末尾,让 recency bias 把"用户当前明示"临时顶到长驻对话开头的 AGENTS.md 之前。如果只说半句(比如只说 omospx、不提 AGENTS.md),你的 anchor 随着对话推进会被推往中段,而坐在对话开头的 AGENTS.md 会稳定地把推理拉回旧轨道;两个名字都出现、并显式声明优先级,反向覆盖才会稳定生效。
六、用注入(injection)+ 定位(positioning)+ 重复(repetition)三个维度同时做冗余。这三个词正是前面讲的五个维度里的三个。对用户来说具体落地是:
- 注入:把精确 token / slash command 写进 user message
- 定位:放在首句首位,不要埋在后续 turn 的 question 回答或 tool result 里
- 重复:跨越多条消息时,关键 token 在每次关键决策点都重提一次(尤其是
/omospx-apply之类的阶段切换点);不要只依赖第一条消息的触达——AGENTS.md 一旦被注入就会长驻对话开头,而你的 user message 会随着对话推进被越推越往中段(注意力低谷)。你自己刷不刷自己的 anchor,直接决定你这条意图信号还能不能被模型看见
七、不要假设"上一轮说过了"还生效。任何一款 Coding Agent 的对话都有 token 预算上限,前文讲 Claude Code 压缩时提到的那套做法——当 token 逼近上限时保留 system prompt、优先摘要/丢弃历史工具结果、必要时重新注入 <system-reminder>——是这类工具处理预算压力的通用范式;OpenCode 同样面对这个问题,具体实现可能不完全相同,但"你几十轮前说过的 user message 大概率已经被推到注意力低谷、甚至已经被摘要吞掉"这件事是共同的。用户侧的对应操作是:在流程切换的那一刻,把意图再说一次。/omospx-start 开场说一次,/omospx-apply 进入实现阶段再显式说一次"继续按 omospx 流程",别让框架名在几十轮之后只剩一个孤零零的回忆。
八、意识到"没反向覆盖"往往是用户做对了事,而不是系统变稳了。忠实侧那一次对话,根本原因是用户的首句恰好是 omospx-start;如果下一次用户写成"走 omospx",AGENTS.md 的 prior 仍然有足够高的概率反向覆盖。要让反向覆盖稳定不发生,不能全靠用户侧措辞,项目侧至少需要做一件事:在 AGENTS.md 顶部加一行显式让位——“若用户在当前 turn 显式提及 omospx / /omospx-*,本文档中的 openspec-* 命令让位,优先加载 oh-my-open-spec skill”。这一行被放到 AGENTS.md 的首段,就天然接管了 AGENTS.md 自己的 recency 与 <system-reminder> 加权,从信号源头消除竞争。用户的精确措辞是辅助,AGENTS.md 的自我让位是根本——两者叠加才能让"自研流程不被默认流程覆盖"这件事可复现、可预期。
这八条串起来回答的是本节开头那个问题:用户面对一个已经被工具方精心编排过的 context window,该怎么把自己的意图信号设计得最强? 答案是把自己也当成一个"注入方"来工作——选对 token 形态、选对位置、选对时机、选对与已有 prior 的对抗方式,让一句话的权重压过长驻对话开头的 250 行 AGENTS.md(前面的忠实侧对话证明了这事可以做到)。在 Agentic Coding 日常使用里,这可能是比"多写几条规则"更接近本质的一项技能。
裸 OpenCode 的能力边界
OpenCode 是一个开源的终端 AI 编程 Agent,支持 GPT-4/5 系列、Claude 等多种模型(通过界面切换模型),内置四个角色:
| Agent | 类型 | 能力 |
|---|---|---|
| Build | 主 Agent | 全工具权限,默认开发模式 |
| Plan | 主 Agent | 只读分析,禁止文件修改 |
| General | 子 Agent | 通用研究与多步骤任务 |
| Explore | 子 Agent | 只读代码库探索,快速搜索 |
值得注意的是,表格中 General 和 Explore 被标注为"子 Agent",但这只是描述它们在 OpenCode 默认工作流中的调用关系,而非能力限制。它们本身是完整的 Agent,拥有独立的上下文和工具权限,完全可以作为主 Agent 直接使用。"子 Agent"是关系描述,不是能力描述——关于这一点的深入分析,见后文"子 Agent 的本质"章节。
Plan 与 Build:模式切换,而非 Agent 切换
在 OpenCode 的终端界面中,按 Tab 键可以在 Plan 模式和 Build 模式之间切换。切换的是同一个 Agent 的工具权限集,而不是切换到另一个独立的 Agent。底层调用的是同一个 LLM,区别仅在于工具白名单:
- Plan 模式:只读权限。Agent 可以读文件、搜索代码库、分析调用链,但禁止写文件和执行命令。这一限制强迫 Agent 只思考、不动手,输出分析报告和行动计划。
- Build 模式:全工具权限。Agent 可以读写文件、执行 Shell 命令、调用外部 API。
分离两种模式的核心原因是防止 Agent 在理解不充分时就开始修改代码。Plan 模式提供了一个只读沙箱,让 Agent 先把问题想清楚。一个实用的工作流是:先用 Plan 模式分析代码库、理解依赖关系、输出修改方案;人类确认方案合理后,再切换到 Build 模式执行。
OpenCode 的 Skills 机制(SKILL.md 文件)实现了"渐进式披露"的上下文管理——Agent 只在需要时才加载技能的完整内容,而不是一次性把所有知识塞进上下文。这与 AI Agent 领域的上下文最小化原则高度吻合:
“The model should only know what it needs to know to make the next immediate decision.”
裸 OpenCode 的天花板
裸 OpenCode 有比较明确的能力边界:
- 并不以内置并发编排为核心体验:子 Agent 可以被调用,但原生形态下并没有像外部编排框架那样,把并行调度作为主工作流来强调
- 无角色分工体系:Build 和 Plan 是模式切换,而非职责分明的团队角色
- 无跨会话状态持久化:每次会话独立,无法在多天的复杂项目中保持进度
- 无业务域知识:工具是通用的,不理解特定团队的技术栈、部署流程、业务规则
裸 OpenCode 是一把锋利的瑞士军刀,但尚未构成一支完整的工程团队。突破这些限制,需要在 OpenCode 之上引入外部编排层。
控制源的分层:强制力并非均匀分布
前面讲 OpenCode 的能力边界,默认的视角是"它有哪些工具、哪些角色"。但一旦把它当作一个要被人和项目约束的生产工具来用——挂上 CLAUDE.md、注册 skill、接 MCP、装内部工作流——就会发现另一个更重要的问题:这些约束并不是同一强度的。同一套规则,写在不同位置,实际生效的可靠性差一大截。
把一次真实任务里能直接观察到的控制源按从强到弱排一下,大致是这样:
1 | |
下层为上层提供能力,上层为下层施加约束。但"施加约束"这件事,越往上越打折。
Hook 是唯一真正无法绕过的那一层。 它是写在平台代码里的运行时拦截器,在特定事件发生时往工具结果里自动追加一段文本,不经过模型"想不想看"的判断。一次任务里能直接观测到的触发事件有四类:每次 edit/write 之后注入 LSP 编译错误;首次 read 到项目 AGENTS.md 会把整份内容以 <system-reminder> 再注入一遍;后台任务结束时通过 <system-reminder> 推送完成通知;有未完成的 todo 时跳出 [SYSTEM REMINDER - TODO CONTINUATION]。<system-reminder> 这个标签博客前文讲 Claude Code 时已经出现过——它本身不是两家的区别,两家都靠这个载体往对话里注入运行时提示。真正可观察的差别是在抽象层级:Claude Code 把这些注入散落在工具结果和 user turn 拼装逻辑里,需要从流量抓包反推;OpenCode 把它们整理成了一张具名的 Hook 清单,触发条件和注入内容对外更明示。但两家做的是同一件事:把"靠平台代码强制、不靠模型自觉"作为重要规则的底线保证——这也正是博客前文讲过的"关键指令在工具结果里重复出现,比只放在 system prompt 里遵从率高得多"在平台层的具体落地。
内置 system prompt 是强默认。 OpenCode 允许给每个主 Agent 装一份会话开头自动加载的 system prompt——裸 OpenCode 的 Build/Plan 已经自带一份基础版;如果上层编排框架(比如后文会讲的 OMO 中的 Sisyphus)要接管行为规范,会在这份基础版之上再叠一套具体得多的思考流程,例如先做 intent 分类(Trivial / Explicit / Exploratory / Open-ended / Ambiguous 等几档,不同档走不同路径)、再做代码库快速评估、再按"并行探索 + 关键决策点咨询"组织工具调用、最后按固定 checklist 收尾;委派子 Agent 时甚至会强制一个 6 段 prompt 模板——TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT,少一段就算模糊指令。关键在于:这些不是写在 CLAUDE.md 里靠模型自觉遵守的文本,而是系统 prompt 里写死的默认行为,每轮都坐在注意力序列最前端、被 prompt caching 固定下来,约束力介于 Hook 和文本规则之间。写得好的主 Agent system prompt,可以把很多本该外挂到 Hook 上的强制点拉到这一层来。
CLAUDE.md / AGENTS.md 则完全依赖模型自觉。 平台会把它注入到对话开头,但不会在模型"准备违反它"的时候拦下来——违不违反,取决于模型当下有没有把那条规则放到注意力焦点。这里还有一个反直觉的注入时机差异值得单独点一下:用户级 AGENTS.md(如 ~/.config/opencode/AGENTS.md)在会话启动时就一次性注入,整段对话里始终可见;但项目级 AGENTS.md 往往是触发性注入——要等到模型第一次 read/edit 到项目目录下的文件时,平台才通过 <system-reminder> 把它塞进来。意味着在你还没碰任何项目文件之前,项目级规则其实没进上下文,模型是按"通用 Agent"状态响应的。很多人观察到的"明明写了项目 AGENTS.md、开头几轮还是像没读到一样"的现象,就来自这里。
有了这层铺垫再看生效率。下面是单次任务样本里能看到的经验倾向(不是普遍规律、也没有大样本统计支撑,但方向上相当一致):
| 规则形态 | 单次任务里的生效情况 |
|---|---|
| 步骤完整内联写在 AGENTS.md 里(如"必须跑单测并在对话里留下命令输出") | 基本都执行到了 |
| 只写"详见某 skill"、具体步骤在未加载的 SKILL.md 里 | 经常被直接跳过 |
换句话说,写"必须执行 X 流程(详见 xxx-workflow skill)“和写"必须执行 X 流程:1. 先做 A;2. 再做 B;3. 最后 C”,这两种写法在 AGENTS.md 里看起来差不多,但模型的遵从度不在一个量级。前者在执行时需要主动 skill("xxx-workflow") 把正文拉进上下文,一旦忘了拉,整条规则就等于不存在——Skill 系统本身是"描述可见、正文按需加载"的设计,<available_skills> 块里只给每个 skill 的 name + description,完整 SKILL.md 只有在显式 skill("xxx") 调用之后才会进入上下文。后者的步骤本身就坐在对话开头的 attention sink 区域,每次都能摸到。
这条倾向有一个容易被忽视的推论:把重要流程拆到 skill 里看起来"更优雅",但代价是执行率下降。想让规则真正被执行,要么给它配一个 Hook(pre-commit 层面的硬拦截、或者编辑后自动注入的 <system-reminder>),要么把关键步骤直接内联到 CLAUDE.md——"详见 xxx"这种引用式写法,本质上是把强制力让渡给了模型的当下状态。
在进入下一节之前,还有两件和 slash command 机制直接相关的事实必须先说清楚。
第一件:Skill / Command 这一层的"正文进入上下文"有两种完全不同的路径。一种是模型自己判断时机、主动用 skill("xxx") 把正文拉进来——这是按需加载,加不加得上取决于模型当时有没有把相关 skill 的 description 放进注意力焦点;另一种是用户在 TUI 里直接敲 /xxx,由 OpenCode 把对应模板整段展开、以 user message 的身份追加到对话末尾——这是强制展开,不经过模型判断,一定会进。两种路径的入口不同、时机不同、注入位置也不同,但最终都是把同一份 template 文本放进 messages 数组。框图里用了"正文需展开"这个比较含糊的措辞,就是因为展开方式不止一种。
第二件:这些控制源之间并没有互斥锁。敲下 /xxx 展开一段新 skill 到对话末尾,它靠 recency bias 拿到短期最高优先级;但项目 AGENTS.md 并没有因此被"关掉"——两条指令源叠加存在,模型靠语义优先级自己挑最强的那条听。短期优先级会随着对话推进自然衰减,几轮之后开头那份 AGENTS.md 又会浮上来,出现"刚用 /xxx 接管了流程、几轮之后又被拉回旧流程"的现象。这不是 bug,是"指令源叠加 + 无互斥"的必然结果。下一节会详细拆 slash command 是怎么"展开到对话末尾"的——但在机制层面它能做的事和不能做的事,恰恰就被"无互斥锁"这条事实划了边界:它能把一段规则的副本临时刷到注意力焦点,但关不掉任何一个已经存在的指令源。
这个视角也能反过来解释为什么"裸 OpenCode"虽然有那么多工具和扩展机制,在团队场景里稳定落地仍然很难——能力是基础工具层的事,合规是 Hook / system prompt / AGENTS.md 的事。缺 Hook 那一层,越严厉的文字规则越容易变成装饰;真正值得在外部编排层补的,往往不是"更多工具",而是规则生效本身需要的那几个强制点——后文讲 OMO 的时候可以看到一个具体例子:它通过 OpenCode 的 UserPromptSubmit Hook 拦截输入,在触发词到达模型之前就注入整段系统级指令,本质上就是在裸 OpenCode 的 Hook 层上再加了一条自己的强制点。
Slash Command 的本质:被"展开"到对话末尾的 inline macro
聊完 messages 数组的五个操纵维度,有一个问题很值得单独拉出来说:/init、/refactor、/my-command 这种斜杠命令,到底是函数调用,还是 prompt 片段?
坊间有一个流传很广的直觉:slash command 就像 C 语言里的 #define,敲 /refactor 的时候,工具在请求发出前把它就地替换成一大段事先写好的 prompt,再把这段 prompt 追加到对话末尾。更进一步,这段 prompt 之所以要在对话中段重新注入,是为了对抗早期加载进来的 skill / CLAUDE.md 随着对话变长被推到中间位置、注意力被稀释——也就是日常说的"skill 遗忘"。
这个直觉大部分是对的。但把它说得更准确一点,需要落到三处:官方文档、opencode 源码、以及社区里能对上号的 bug。
skill 和 command 在源码里就是同一种东西
opencode 的命令加载器在 packages/opencode/src/command/index.ts。同一个文件同时处理三种"可展开的 prompt 来源":custom command(用户写在 .opencode/commands/*.md 的 markdown)、MCP prompt(MCP server 暴露的 prompt)、以及 skill(从 skill.all() 拿到的技能包)。三者最终都被装进同一个 commands 字典,共享同一个 Info 类型,只靠 source 字段区分来源:
1 | |
代码很朴素,能直接说明三件事:skill、custom command、MCP prompt 在结构上没有任何差别,TUI 里敲 / 时会被一起列出来、走同一套 template 展开逻辑;展开的产物就是 template: string,是一段文本,不是什么函数句柄;模板里支持 $ARGUMENTS、$1、$2、!`command` 和 @file 这些占位符,但所有占位符在展开阶段就地替换,最终落到对话里的仍然是一段纯文本。
opencode 官方文档自己的描述同样直白:“The content of the command file will be sent as a message to the AI assistant”[^cmd-opencode]。没有函数、没有 RPC、没有结构化调用,就是一段 prompt 以 user message 的身份被追加到对话末尾。
Anthropic 那边的证据更进一步。Claude Code 在 2026 年 3-4 月把 custom commands 合并进了 skills 体系,.claude/commands/*.md 和 .claude/skills/<name>/SKILL.md 被统一归纳成"prompt-based playbook"[^cmd-claude][^skill-claude]。官方的合并动作本身就已经说明问题:command 和 skill 是一种东西,只是触发者不同——command 由人敲 / 触发,skill 由 agent 自己判断是否调用。
所以"inline macro"在这里不是比喻,就是实现事实。
"为了抗遗忘"这个动机,官方其实没明说
翻 Anthropic 文档、opencode 文档、Claude Code 的提交历史和 issue,找不到任何官方级别的表述说 slash command 是为了对抗 skill 遗忘而设计的。官方给出的理由要朴素得多:让重复的 prompt 可以复用、让团队共享工作流模板、让 $ARGUMENTS / !command / @file 之类的占位符把动态上下文编织进固定模板。
这一点和 C 的 #define 其实一致——#define 的设计初衷也不是抗遗忘,是代码复用。只不过用着用着,大家发现它顺带能在长对话里起到"把规则重新摆到模型面前"的作用。抗遗忘是副作用,不是目标。
但副作用确实真的存在。后面就来说机制。
被稀释的不是 skill 本身,是它的细节
在展开机制之前,先把"skill 失效"这件事分清楚。前一节讲控制源分层时已经提到过一种更极端的情况:skill 正文根本没进上下文——<available_skills> 里只有 name + description,完整 SKILL.md 需要显式 skill() 调用才会被拉进来,模型没主动拉就等于那条规则从未存在过。这是"加载前"的失效,和本节要讲的"加载后"细节稀释是两回事:
- 加载前的失效:skill 正文从未进入 messages 数组。无论对话多短、注意力多集中,模型都读不到具体步骤,只能凭 description 里的一句话脑补。这是"引用式写法生效率低"的机制根因。
- 加载后的稀释:skill 正文或 CLAUDE.md 已经在对话开头,对话变长之后具体字句在 U 型曲线的中段被盖过去。这一节要讲的就是这种。
后者才是日常说的"长对话中 skill 会被遗忘",但这个说法其实不够精确。回到两篇论文:
Lost in the Middle(Liu et al. 2023, arXiv:2307.03172,后收入 TACL 2024)证明的是 U 型曲线,开头和结尾的信息注意力高、中间位置低[^lost-middle]。Attention Sink(Xiao et al. 2023, arXiv:2309.17453,后收入 ICLR 2024)证明的是另一件事:transformer 序列最开头的那几个 token 会获得异常高的 attention 权重,后续所有 token 都把它们当作"sink"来吸附注意力[^attn-sink]。2025 年的后续研究把这个机制描述为 catch, tag, release——开头 token 捕获注意力、给其他 token 打上语义标签、再释放回残差流[^attn-sink-2025]。
两个机制叠起来看 CLAUDE.md 的处境就清楚了。CLAUDE.md 以 <system-reminder> 包裹、以 user message 的身份注入到对话开头。对话短的时候它坐在 attention sink 位置、篇幅又小,模型能牢牢抓住它。对话长起来之后,开头那段的位置没变、attention 权重也没下降,“CLAUDE.md 这段内容存在"这件事不会被模型忘掉。真正被稀释的是其中的细节:具体的编码规范、函数命名约定、反复强调过的"不要用 xargs”。这些细节在 U 型曲线的中段被其他信息盖过去,不是丢了,是糊了。
这其实能解释一个很多人观察到的现象。长对话里 Agent 还"记得" CLAUDE.md 要求用 Yarn 而不是 npm,但会开始犯细节错误——忘了脚本要加 | cat,忘了不要动 public/ 目录,忘了某个具体的命名约定。它没忘记有这条规则,忘记的是规则的具体字句。
slash command 为什么能缓解这件事
关键其实不在 slash command 本身,而在它被追加到消息数组末尾这件事。
把一段 200 行的 skill 内容以 user message 的身份重新放到数组末尾,它立刻拿到 recency bias 的高 attention 权重。对模型来说相当于 skill 被刷新了一次——不是覆盖开头那份,是在末尾复制了一份刚刚进入注意力焦点的副本。
这个动作正好对应五维度里的重复:关键指令不能只出现一次。每个 user turn 开头重新注入 <system-reminder>、工具结果里夹带"keep using TODO"提醒、slash command 在用户主动触发时把 skill 内容复制到末尾——都是同一套抗稀释策略的不同实现形式。
但这里有个硬约束,也正是下一节 bug 的根源:这份"末尾副本"是静态文本。它不知道对话前半程发生过什么,也不携带任何状态。所以 slash command 能救"细节被稀释",救不了"对话中动态积累的上下文"。后者是 TODO 列表、笔记文件这类外化状态的职责,不是一段固定 prompt 能顶替的。
归谬检验:如果这套解释是对的,社区里应该能看到哪些 bug
把上面这套说法合起来,可以这样陈述:
slash command 本质是"skill 的一份被按需复制到对话末尾的 prompt 副本"。它能抵抗 lost-in-the-middle 造成的细节稀释,代价是每次触发都要把一整段文本追加进 messages 数组。
如果这个陈述为真,Claude Code 和 opencode 都已经把这套机制做到生产、跑了几十万用户,那就不可能不在社区留下痕迹。具体说应该能看到四类副作用。下面一个一个对照。
第一类是 token 膨胀和成本异常。 Claude Code v1.0.86 升到 v1.0.88 之间,/context 命令出现过明显的 token 消耗回归,用户怀疑是命令展开或系统提醒的注入逻辑变了[^bug-6324]。2.1.88 发布之后更有用户报告两小时就吃掉周配额的 66%,日常操作的 token 消耗完全不成比例[^bug-42272]。"每次 command 调用都注入一大段 prompt"的直接后果就是这个样子。
第二类是同一 skill 被重复注入。 这条命中得最直接。Claude Code v2.1.114 上有一个 WSL 下的明确 bug:同一个 skill 在 / 自动补全列表里出现 3 次,发起者亲自复核磁盘上只有一份 SKILL.md,在 issue 里给出的结论原话是 “the duplication appears to be in how the harness enumerates or injects skills into the system prompt”[^bug-51008]。把 skill 内容当 prompt 片段在 harness 层枚举、注入,就容易出这种事。另一个 issue #49170 更赤裸地展示了代价:会话刚启动,superpowers:using-superpowers 的整段 skill 说明加上 100 多个可用 skill 的清单就被无条件注入,直接吃掉 35% 的上下文窗口[^bug-49170]。skill 明明是按需触发的,harness 却选择把全套介绍一次性塞进 system prompt,结果就是 token 被大量浪费在根本用不到的 skill 上。
第三类是 prompt cache 被破坏。 这是最能把前面的机制分析推到底的证据。issue #42338 的报告者做了一组控制变量的实验:同一个约 500k token 的会话,用 --continue、/resume 等各种方式重进,即使间隔只有 2-3 秒、远小于 5 分钟的 cache TTL,cache_read 也会直接归零,整段 prompt 被完整重新缓存[^bug-42338]。他贴出的日志长这样:
1 | |
对照前面讲过的 prompt caching 机制:严格前缀匹配,从请求起点到 cache_control breakpoint 的所有内容构成缓存前缀,前缀里任何位置的变动都会让它之后的一切全部失效[^prompt-cache]。只要 harness 在重建对话时调整了一下消息顺序(比如换了一种 tool result 的序列化方式),前缀就不再字节相等,整段 cache 全作废。同周期里还有 #41663(Windows 下 cache bug 导致 token 放大 10-20 倍)[^bug-41663] 和 #40524(system prompt block 在后续 turn 拿到 cache_creation 而不是 cache_read)[^bug-40524] 作为佐证。
第四类是动态状态丢失。 这一类证据要弱一些,但也存在。opencode 的 issue #4659 把情形描述得很清楚:用户用 slash command 起手做了一大段环境 setup,一旦 compaction 触发,那段 setup 就没了——因为它本来就是一段静态 prompt,对话里后续积累的上下文它带不走[^bug-4659]。
四类预测,三类有强证据,一类有弱证据。对原假说这是相当强的支撑,同时也顺带把它的边界标出来了:slash command 作为抗稀释机制有效,但不是银弹——它要付 token 成本、会触发重复注入、对 prompt cache 很脆弱、替代不了真正的持久化状态。
别家是怎么解同一个问题的
把视野放大一点看,"如何对抗 lost-in-the-middle"其实是一个被各家重复发明的问题,给出的答案也很不一样:
| 工具 | 持久指令载体 | 注入位置与频率 | 条件激活机制 |
|---|---|---|---|
| Claude Code / opencode | CLAUDE.md / AGENTS.md(user message + <system-reminder>) |
每个 user turn 重新注入,自动 | slash command / skill,人敲 /xxx 时追加到末尾 |
| Cursor | .cursorrules(旧)/ .cursor/rules/*.mdc(新) |
进入 system message;alwaysApply: true 始终带上,否则按 glob / description 条件触发[^cursor-rules] |
MDC frontmatter 的 globs / alwaysApply,或 Agent 按 description 自主拉取 |
| Cline | .clinerules/*.md 目录(也自动识别 .cursorrules / .windsurfrules / AGENTS.md) |
多文件按字典序合并、追加到 system prompt[^cline-rules] | rule 文件 YAML frontmatter 的 paths glob,按当前上下文里打开/提及/编辑的文件激活 |
| Continue | .continue/rules/*.md(local)+ Hub rules(通过 config.yaml 的 uses:) |
按顺序拼接进 system message[^continue-rules] | frontmatter 支持 globs / regex / description / alwaysApply 四种维度 |
四家做法不同,但都默认了同一件事:规则不能只注入一次。要么每轮重复(Claude Code、opencode、Cline、Continue 的 alwaysApply),要么按条件重新触发(Cursor MDC 的 globs、Cline 的 conditional rules、Continue 的 globs/regex)。长对话里要对抗细节稀释,只有这两条路。
真正值得琢磨的差别是"激活决定权交给谁"。Claude Code 和 opencode 的 slash command 把决定权完全交给人,你不敲就不占 token,控制权最大,但也最容易忘。Cursor 的 globs、Cline 的 paths、Continue 的 globs/regex 交给静态规则去判断,规则作者预先声明"我只在编辑 src/components/** 的时候出现",引擎按上下文自动激活,不用人操心,但规则作者得会写 glob。还有一种是交给 Agent 自己判断,比如 Cursor MDC 里的 description 字段、Claude Code skill 的 description——规则作者只写一句话描述什么场景该用我,让模型每轮自己决定要不要拉进上下文,最省人力,但也最依赖模型的判断力。
Claude Code 和 opencode 选的是两端——slash command 给人、skill description 给 agent,中间不做文章。Cursor 反过来,MDC 一个 frontmatter 里把三种策略全做上(alwaysApply 管全局、globs 管文件、description 管模型自主),这也是它的规则系统看起来比别家更复杂的原因。Cline 和 Continue 走中间路线,以 glob/regex 做静态匹配为主,必要时再 fallback 到 Agent 判断。
回到实战:什么时候该用 command,什么时候别用
有几种情况适合走 command,或者把 skill 暴露成 command 让人主动触发。一是对话已经长到 50k token 以上、能明显感觉到模型在细节上开始漂移,用 /xxx 把关键规则复制到末尾立竿见影。二是 prompt 需要拼动态内容——$ARGUMENTS、!command、@file 这些占位符是 command 专属、skill 做不到的,需要把 shell 输出或文件内容编织进去时,只有 command 能干。三是 /init、/review、/refactor 这种明确的"模式切换"入口,触发时机显式化比藏在模型自己的判断里要可控得多。
反过来也有几种情况,用 command 反而会帮倒忙。核心规则已经在 CLAUDE.md / AGENTS.md 里的,就别再做一份 command 重新注入一遍——那就是字面意义上的重复注入,issue #51008 里 skill 在补全列表出现 3 次就是这么来的,指令打架比细节漂移更麻烦。规则只有两三行的没必要走 command,CLAUDE.md 里写一行的性价比远高于起一个 skill 文件。需要"记住对话里刚发生的事"的场景,也别指望 command——它是静态文本,不知道你五分钟前改了哪个文件,这种活儿是 TODO 列表、笔记文件、memory/ 目录的活儿。还有就是对 prompt cache 极端敏感的长会话,/resume 一次就可能像 issue #42338 里那样让几十万 token 重新计费,频繁 /exit + /resume 凑 cache 命中这种事少干。
说到底,command 是把静态知识刷到注意力焦点的"末尾复读机",不是保存动态上下文的"有状态函数"。前者是它被设计出来要做的事,后者另有 TODO 列表、memory 文件和结构化笔记负责。两者别搞混。
进一步的问题:会话里新捡到一个 skill,模型能当场用起来吗?
前面讨论的都是"候选 skill 列表在会话启动时就固定下来,之后靠触发策略决定什么时候展开"。但真实使用里有一种更现实的诉求:对话跑到一半才意识到需要一个新 skill——可能是刚写完 .claude/skills/<name>/SKILL.md,可能是 MCP 服务端刚注册了一批新工具,也可能是从同事那里拷了一份 skill 目录过来。能不能不退出会话,让模型当场发现这个新候选项、并自行决定是否调用?
这个问题分两层看:协议层支不支持,以及实际工具里的工程实现走到哪一步。
协议层:MCP 已经把机制留好了。 Model Context Protocol 规范里有一条 notifications/tools/list_changed:server 端工具列表变更时主动通知 client,client 收到后重新拉 tools/list、更新本地状态,prompts 和 resources 同理[^mcp-list-changed]。也就是说,“运行时动态发现工具"在协议层从来不是缺失的能力,而是设计之初就明确列入的特性。Skill 这一层在 Claude Code 里走的是另一条路:2.1.0(2026-01)引入了 skill hot-reload,文件系统变化会被 watcher 捕获、重新扫描后更新可用 skill 列表[^cc-21-hotreload]。两条路径的共同点是,协议和机制层都不要求"重启才能见新工具”。
工程实现层:各家距离"真的能"还差一截。 把协议兑现成用户可感知的行为,需要 harness 做大量额外工作,目前看每一家都踩到不同的坑。
Claude Code 2.1.0 的 hot-reload 只实现了"检测已存在 SKILL.md 文件的修改"。对"新建目录"这种情况,watcher 完全感知不到。issue #31559 的报告者直接翻了 cli.js:watcher 用的是 fs.watchFile() 绑在会话启动时已知的那些 SKILL.md 上,而不是 fs.watch() 监听 .claude/skills/ 这个父目录,所以新建子目录根本不会触发回调[^cc-31559]。同一时期 issue #18852 也反馈 .claude/ 下的大多数变更在会话里不被感知、必须等重启[^cc-18852]。官方补丁 /reload-plugins 在 plugin 场景下也有"装了新 plugin 但 reload 不到 skills"的 bug(#35641)[^cc-35641]。结果就是:老 skill 改字句能热更,新 skill 进目录不行,这是 2026 年中 Claude Code 的真实能力边界。
MCP 工具这边更微妙。notifications/tools/list_changed 在 Claude Code 的实现里只跨 turn 生效,agent 发起下一次 prompt 时才会真正把新工具纳入可选集;同一 turn 内新注册的工具即便进了内部列表,搜索还是看不到它们[^cc-mcp-31893]。VS Code 的 Copilot 更直接,mid-turn 加进来的工具是 UI 可见、agent 不可见的分裂状态[^vscode-303012]。Cursor 上报过"tools 短暂出现、随后就从 agent 上下文里消失"的回归(#149555)[^cursor-149555],以及不少 MCP server 重启后必须 Reload Window 才能刷新的实测帖[^cursor-refresh]。OpenAI Codex CLI 的 ~/.codex/skills 目录在 runtime 也不总是被注入进"Available skills"列表(#15136)[^codex-15136]。
这些 bug 看起来零散,其实指向同一个根本约束:skill 和工具候选集几乎都在 system prompt 附近注入——Claude Code 把 <available_skills> 嵌在 system prompt 或 tool description 里[^skill-injection],MCP 工具进 tools 参数——而 system prompt / tools 是 prompt cache 的前缀,前缀里任何字节变动都让整段 cache 失效。运行时加一个新 skill,等于往一个被多轮复用、动辄几十万 token 的缓存前缀里插一行,代价就是整段 cache 作废、下一轮从零重新计费。前文 issue #42338 那组 cache_read 归零的日志就是这个机制的直接后果[^bug-42338]。harness 选择"同 turn 不生效、跨 turn 生效"或者"干脆等重启",不是懒,是在躲这个代价:要么攒够一批变更再重建前缀,要么干脆把重建时机交给会话边界。
能用的妥协方案是什么。 既然新 skill 在会话里"即时进候选集"这条路被 cache 经济和 watcher 实现挡住了,实际能用的替代方案只剩两种:
- 把 skill 正文直接贴进对话。把新
SKILL.md的内容作为一段 user message 粘进去,明确告诉模型"这段指令从现在开始生效"。它不会进<available_skills>候选池,模型也不会自动按 description 去拉,但能 100% 进上下文,且拿到 recency bias 的末尾高权重。本质上就是手工版的 slash command,牺牲"按需加载"换"立刻生效"。 - 开新会话。重启 session,让 harness 在启动时重新扫描 skill 目录、重建 system prompt 前缀。付出的代价是丢失当前上下文,但能拿到"skill 描述进 system prompt、模型自主决定是否加载"这套完整语义。如果新 skill 确实重要、且当前会话的上下文不难重建,这通常是最干净的选项。
把话说利落一点:2026 年中,“会话中动态增加 skill 并让模型自主决定加载"在所有主流 Agentic Coding 产品里都只是部分支持。协议层早就留好了通道,工程层被 prompt cache 的前缀不可变性卡住,各家在"文件修改热更”“新文件热更”“跨 turn vs 同 turn”"reload 命令"上分别实现了不同的子集。想让模型当场用上一个刚发现的 skill,最可靠的办法仍然是粘贴正文或者开新会话,而不是指望候选集自己动态扩张。
[^cmd-opencode]: OpenCode Docs — Commands,访问于 2026-04-24。
[^cmd-claude]: Claude Code Docs — Slash commands,访问于 2026-04-24。
[^skill-claude]: Claude Code Docs — Extend Claude with skills,访问于 2026-04-24。
[^attn-sink]: Xiao et al., Efficient Streaming Language Models with Attention Sinks,arXiv:2309.17453,2023。后收入 ICLR 2024。
[^attn-sink-2025]: Zhang et al., When Attention Sink Emerges in Language Models: Empirical Evidence,arXiv:2502.00919,2025。
[^lost-middle]: Liu et al., Lost in the Middle: How Language Models Use Long Contexts,arXiv:2307.03172,2023。后收入 TACL 2024。
[^prompt-cache]: Anthropic — Prompt caching,持续更新。
[^bug-6324]: anthropics/claude-code#6324 — Token Usage Regression in /context Command Between v1.0.86 and v1.0.88,2025-08-22。
[^bug-42272]: anthropics/claude-code#42272 — Excessive token consumption since 2.1.88,2026-03-31。
[^bug-51008]: anthropics/claude-code#51008 — Skills appear duplicated/triplicated in the slash command list,2026-04-20。
[^bug-49170]: anthropics/claude-code#49170 — Unnecessary plugin documentation auto-loaded at session start,2026。
[^bug-42338]: anthropics/claude-code#42338 — Session resume (–continue) invalidates entire prompt cache,2026-04-02。
[^bug-41663]: anthropics/claude-code#41663 — Prompt Cache causes excessive token consumption (10-20x inflation),2026-03-31。
[^bug-40524]: anthropics/claude-code#40524 — Conversation history invalidated on subsequent turns,2026。
[^bug-4659]: sst/opencode#4659 — Sliding window context management for long-running sessions,2026。
[^cursor-rules]: Cursor Docs — Rules,访问于 2026-04-24。MDC 的 alwaysApply / globs / description 三字段行为,以官方文档 + Cursor 社区论坛的多个实测贴(如 .cursorrules 在 Agent 模式下的加载行为)互相印证。
[^cline-rules]: Cline Docs — Rules,访问于 2026-04-24。明确说明 .clinerules/ 是目录(非单文件),支持多 .md 按字典序合并、YAML frontmatter 的 paths conditional,并自动识别 .cursorrules / .windsurfrules / AGENTS.md。
[^continue-rules]: Continue Docs — How to Create and Manage Rules,访问于 2026-04-24。rule 文件 frontmatter 支持 globs、regex、description、alwaysApply 四种激活维度,行为已在官方文档中明确枚举。
[^mcp-list-changed]: Model Context Protocol — Tools(Dynamic Tool Discovery),原文:“Servers can notify clients when tools change using notifications/tools/list_changed; tools can be added or removed during runtime”,prompts 与 resources 有对应的 notifications/prompts/list_changed、notifications/resources/list_changed。访问于 2026-04-24。
[^cc-21-hotreload]: Claude Code v2.1.0 release notes 与 Extend Claude with skills,正式引入 skill hot-reload;社区实测参见 ClaudeLog — What is Skill Hot-Reload in Claude Code 与 Build Agent Skills Faster with Claude Code 2.1 Release,均明确"修改已存在 SKILL.md,保存即生效",2026-01。
[^cc-31559]: anthropics/claude-code#31559 — Skill hot-reload doesn’t detect new skill directories added mid-session,2026-03-06。报告者分析 cli.js 后指出:watcher 用 fs.watchFile() 绑在会话启动时已知的 SKILL.md 上,没有 fs.watch() 监听 .claude/skills/ 父目录,所以新建子目录不会触发 re-scan。
[^cc-18852]: anthropics/claude-code#18852 — Changes to .claude/ are not detected until session restart,2026。明确 .claude/ 目录下大多数变更在运行中会话里不被感知、需要重启。
[^cc-35641]: anthropics/claude-code#35641 — /reload-plugins does not load skills from newly installed plugins,2026。/reload-plugins 对 plugin 新装的 skills 不生效,相关讨论引用了 plugin hot-reload feature request #32399。
[^cc-mcp-31893]: anthropics/claude-code#31893 — MCP spec compliance: list_changed, progress notifications,2026。原文:"tools/list_changed: works across prompt turns (tool added → next prompt sees it); does NOT work within the same turn — if a tool is registered mid-turn, the agent cannot search it"。
[^vscode-303012]: microsoft/vscode#303012 — MCP tools added via tools/list_changed during a conversation are visible in UI but not in agent’s tool search,2026。明确"Tools added via tools/list_changed mid-turn are visible in the UI but not in the agent’s tool search. They only become available on the next turn"。
[^cursor-149555]: Cursor Forum — Cursor 2.3.41 not detecting MCP Server tools,2026。报告者描述 “After Cursor reload/restart, tools briefly become available. Tools disappear from agent context shortly after.”
[^cursor-refresh]: Cursor Forum — How to refresh mcp (Cursor 1.0) 与 MCP Hosted Dynamic Tools Refreshing,多个第三方安装文档(如 MCP Bundles — How to Add MCP Servers to Cursor、Casdoor — Connect Cursor to MCP)同步要求 “Restart Cursor completely” 或 “Developer: Reload Window” 才能刷新 MCP 工具列表。
[^codex-15136]: openai/codex#15136 — Local skills under ~/.codex/skills are not injected into runtime Available skills,2026。本地 ~/.codex/skills 目录下的 skill 未出现在 runtime 的 “Available skills” 列表中,无法显式调用。
[^skill-injection]: Claude Code 的 skill 候选注入机制有独立源码分析,见 Implementing Claude Code Skills from Scratch、Claude Agent Skills: A First Principles Deep Dive、zep-us/claude-system-prompt 以及 Agent Skills — Solve It With Code。后者明确指出:“Claude Code embeds the <available_skills> XML directly in the tool description, rather than in the system prompt”——无论注入到 system prompt 还是 tool description,都在 prompt cache 的缓存前缀范围内。
命令与配置:从单工具到可编排工作流
如果只把 OpenCode 当作一个终端里的编码助手,那么 /init、/undo、/redo、/share 这些内置命令,更多是在提升单个会话的可用性。但当工作流开始延伸到 OMO 这种多 Agent 编排层时,这些命令的意义就变了:它们不再只是“方便操作”,而是在为后续的自动化协作准备上下文、状态和回退能力。
其中最值得关注的是两类能力:
- 项目知识初始化:例如
/init与/init-deep,负责把代码库的结构、规范和目录知识沉淀成可供 Agent 读取的配置文件; - 会话控制与工作流命令:例如
/undo、/redo、/share,以及 OMO 扩展出的/ralph-loop、/refactor、/start-work,负责把“单次回答”变成“可持续推进、可回退、可验证的工程过程”。
先看 OpenCode 自带的基础命令:
| 命令 | 功能 | 使用场景 |
|---|---|---|
/init |
生成项目 AGENTS.md | 新项目初始化 |
/undo |
撤销上一条消息和文件变更 | AI 改错了,想回退 |
/redo |
重做已撤销的操作 | 撤销后想恢复 |
/share |
生成会话分享链接 | 需要分享对话给他人 |
/help |
显示帮助信息 | 忘记命令用法 |
/undo 与 /redo:安全的迭代探索
这两个命令构成了 OpenCode 的"时间旅行"机制。
工作原理(以下 ASCII 图为摘要,展示三步操作的状态变化):
1 | |
注意事项:
/undo和/redo依赖 OpenCode 内部的会话状态追踪,在 Git 仓库中效果最佳- 文件变更会被完整恢复(包括删除的文件)
- 可以连续执行多次
/undo回退多步
/share:会话分享
1 | |
分享链接包含完整的对话历史和代码变更,适合:
- 向同事展示解决方案
- 提交 Bug 报告时附上复现步骤
- 记录学习过程
OMO 内置命令详解
OMO(Oh My OpenAgent,原名 Oh My OpenCode)在 OpenCode 基础上扩展了一组更偏"工作流模板"的斜杠命令。它们的价值不只在于省输入,而在于把原本需要人手动维持的节奏——规划、循环、重构、收尾——固定成可重复执行的流程。
| 命令 | 功能 | 复杂度 |
|---|---|---|
/init |
生成根目录 AGENTS.md(OpenCode 原生,OMO 继承) | 低 |
/init-deep |
生成层级化 AGENTS.md 知识库 | 中 |
/ralph-loop |
自引用开发循环,持续工作直到完成 | 高 |
/ulw-loop |
Ultrawork 版本的 ralph-loop | 高 |
/cancel-ralph |
取消活跃的 Ralph Loop | 低 |
/refactor |
智能重构,完整工具链验证 | 高 |
/start-work |
从 Prometheus 计划开始工作 | 中 |
/init:OpenCode 原生的项目引导
/init 是 OpenCode 的内置命令,用于为项目创建 AGENTS.md 配置文件。
解决的问题:Agent 需要理解项目结构、技术栈、编码规范才能高效工作。每次新项目都要从零开始学习,效率低下。
工作原理(以下 ASCII 图为摘要,展示扫描→生成的主干流程;各分支的详细说明见下方文字):
1 | |
使用方式:
1 | |
关键特性:
- 只生成根目录文件:
/init只在项目根目录生成一个AGENTS.md - 增量更新:如果文件已存在,会保留原有内容并补充新信息
- Git 友好:生成的文件应该提交到版本控制,供团队共享
/init-deep:层级化知识库
/init-deep 是 OMO 扩展的命令,在 /init 的基础上实现了层级化的知识库生成。
解决的问题:大型项目结构复杂,单一根目录的 AGENTS.md 无法承载所有模块的细节。Agent 在处理特定模块时,需要该模块级别的上下文。
工作原理(以下 ASCII 图为摘要,展示并行分析→层级生成的主干流程):
1 | |
使用方式:
1 | |
层级化知识库的价值:
| 层级 | 作用 | 示例内容 |
|---|---|---|
根目录 AGENTS.md |
项目全局视图 | 技术栈、整体架构、部署流程 |
模块级 AGENTS.md |
模块专属知识 | 该模块的 API、依赖关系、设计模式 |
子模块级 AGENTS.md |
细节实现 | 具体函数的用法、边界情况处理 |
与 /init 的对比:
| 维度 | /init(OpenCode 原生) |
/init-deep(Oh My OpenAgent) |
|---|---|---|
| 生成范围 | 仅根目录 | 根目录 + 所有关键子目录 |
| 知识粒度 | 项目级 | 项目级 + 模块级 + 子模块级 |
| 上下文精度 | 粗粒度 | 细粒度,按需加载 |
| 适用场景 | 小型项目、单模块项目 | 大型项目、多模块项目 |
| Token 消耗 | 低 | 中等(但按需加载,不一次性全部读入) |
最佳实践:
- 新项目首次使用:先用
/init-deep建立完整知识库 - 定期更新:当项目结构有重大变化时重新运行
- Git 提交:将所有
AGENTS.md文件提交到版本控制 - 增量更新:默认模式会保留已有内容,避免覆盖人工补充的知识
/ralph-loop:持续工作直到完成
这是 OMO 中最有代表性的命令之一,实现了"自引用开发循环"。
核心机制(以下 ASCII 图为摘要,展示循环的主干结构;循环与 OMO 多 Agent 模式的关系见后续"能力层 vs 循环层"小节):
1 | |
使用方式:
1 | |
关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
--completion-promise |
DONE |
Agent 输出该标记表示任务完成 |
--max-iterations |
100 |
最大迭代次数,防止无限循环 |
适用场景:
- 复杂的多步骤任务(重构大型模块)
- 需要多次迭代才能完成的功能开发
- Bug 修复(可能需要多次尝试不同方案)
设计哲学:能力层 vs 循环层
一个常见的困惑是:既然界面已经选择了 Sisyphus (Ultraworker) 模式,Agent 已经可以自行多步操作,为什么还需要 /ralph-loop 和 /ulw-loop?
答案在于两者解决的是不同层面的问题:
| 概念 | 解决的问题 | 行为特点 |
|---|---|---|
| Sisyphus (Ultraworker) | “代理能力” | 多代理编排、能多步执行、能并行探索 |
/ralph-loop /ulw-loop |
“代理纪律” | 强制循环验证、防止假完成、必须满足停止条件 |
能力层(Sisyphus/Ultraworker) 解决的是"Agent 能不能做这件事"——它提供多代理编排、并行调度、工具调用等能力。但有能力不代表有纪律。一个有能力执行多步的 Agent,仍可能在第一次响应时就声称"我完成了",或者跳过验证环节直接结束。
循环层(ralph-loop/ulw-loop) 解决的是"Agent 会不会假完成"——它通过自引用循环机制强制 Agent:
- 执行 → 自检 → 发现问题 → 继续执行 → 再自检
- 直到满足明确的
--completion-promise条件 - 防止 Agent 在第一轮就声称完成
用一个比喻:
- Sisyphus (Ultraworker) 像一个全能的工程师:能写代码、能调试、能查文档、能并行处理多个任务
/ralph-loop像一个严格的质检流程:不信任工程师的"我做完了",必须通过验证才能结束
两者是正交互补的关系,而非替代关系。
使用建议:
| 任务类型 | 推荐组合 |
|---|---|
| 日常小改动 | 普通提示,无需特殊命令 |
| 中等功能开发 | ulw: 前缀(激活 Ultraworker) |
| 复杂多步骤任务 | ulw + 分阶段提示 |
| 追求极致/大重构 | ulw + /ralph-loop |
/ulw-loop:Ultrawork 版本的循环
/ulw-loop 是 /ralph-loop 的增强版,专为 Ultraworker 模式优化。它在标准 Ralph Loop 的基础上增加了更严格的验证机制和更智能的任务分解能力。
与 /ralph-loop 的区别:
| 特性 | /ralph-loop |
/ulw-loop |
|---|---|---|
| 验证强度 | 基础自检 | 多重验证(编译、测试、Lint) |
| 任务分解 | 简单分步 | 智能拆解为子任务 |
| 错误恢复 | 重试当前步 | 自动回退并调整策略 |
| 适用场景 | 一般复杂任务 | 高可靠性要求的重构 |
使用方式:
1 | |
/cancel-ralph:取消活跃的循环
当 Ralph Loop 陷入死循环或需要中断时,使用此命令。
1 | |
注意事项:
- 会立即终止当前循环
- 保留已完成的中间状态
- 适合在发现方向错误时及时止损
/refactor:智能重构
/refactor 是一个专门用于代码重构的命令,它集成了完整的工具链验证。
工作流程:
- 分析阶段:识别需要重构的代码块及其依赖
- 规划阶段:生成重构计划,包括目标结构和迁移步骤
- 执行阶段:逐步执行重构,每步后进行验证
- 验证阶段:运行测试、Lint、编译检查
使用方式:
1 | |
关键特性:
- 原子性保证:每一步重构都是原子的,可随时回退
- 依赖追踪:自动更新所有引用点
- 回归测试:重构后自动运行相关测试用例
/start-work:从 Prometheus 计划开始工作
/start-work 用于从已有的 Prometheus 计划文件中恢复工作状态。
使用场景:
- 中断的工作需要继续
- 多人协作时接手他人的计划
- 从长期规划中提取具体任务
使用方式:
1 | |
命令速查表
| 命令 | 用途 | 适用场景 |
|---|---|---|
/init |
生成项目知识库 | 新项目初始化 |
/init-deep |
生成层级化知识库 | 大型项目 |
/ralph-loop |
自引用开发循环 | 复杂多步骤任务 |
/ulw-loop |
Ultrawork 循环 | 高可靠性重构 |
/cancel-ralph |
取消循环 | 中断死循环 |
/refactor |
智能重构 | 代码结构优化 |
/start-work |
从计划恢复 | 继续中断工作 |
/undo |
撤销操作 | 回退错误变更 |
/redo |
重做操作 | 恢复撤销的变更 |
/share |
分享会话 | 协作与复盘 |
模式速查表
| 模式 | 特点 | 适用场景 |
|---|---|---|
| Plan 模式 | 只读分析,禁止修改 | 代码审查、方案设计 |
| Build 模式 | 全工具权限 | 日常开发、功能实现 |
| General Agent | 通用研究能力 | 多步骤调研、文档查阅 |
| Explore Agent | 快速代码探索 | 定位代码、理解结构 |
| Sisyphus (Ultraworker) | 多代理并行编排 | 复杂任务并行处理 |
| Ralph Loop | 自引用循环验证 | 防止假完成、强制迭代 |
大型重构工作流:持续数周的项目实战
在实际工程中,最考验 Agent 编排能力的不是写一个新功能,而是对一个运行多年的大型项目进行持续数周的重构。这类任务的特点是:
- 规模大:涉及数百个文件、数千处引用
- 周期长:需要多天甚至数周的持续工作
- 风险高:任何一步出错都可能导致系统不可用
- 上下文深:需要理解历史债务、业务约束、技术演进路径
下面以一个真实的"单体服务拆分"项目为例,展示如何设计一个可持续数周的大型重构工作流。
四阶段工作流设计
我们将整个重构分为四个阶段,每个阶段有明确的目标、交付物和退出条件:
阶段一:分析与规划(第 1-3 天)
目标:全面理解现有系统,制定详细的拆分方案。
关键动作:
- 代码库扫描:使用
/init-deep生成层级化知识库 - 依赖分析:识别模块间的调用关系、数据流向
- 边界划定:确定拆分后的服务边界、接口契约
- 风险评估:识别高风险区域,制定回滚预案
交付物:
plans/refactoring-plan.md:详细的重构计划diagrams/dependency-graph.mmd:依赖关系图risks/risk-register.md:风险登记册
退出条件:
- 计划经过人工评审并通过
- 所有高风险项都有对应的缓解措施
- 测试环境准备就绪
阶段二:基础设施准备(第 4-7 天)
目标:搭建新服务的基础设施,确保可以独立运行。
关键动作:
- 脚手架生成:创建新服务的项目结构
- CI/CD 配置:配置自动化构建、测试、部署流水线
- 监控接入:集成日志、指标、链路追踪
- 数据迁移脚本:编写数据库迁移脚本
交付物:
- 新服务的基础代码仓库
- 可运行的 Hello World 服务
- 完整的 CI/CD 流水线
退出条件:
- 新服务可以独立部署和运行
- 监控指标正常上报
- 数据迁移脚本在测试环境验证通过
阶段三:渐进式迁移(第 8-20 天)
目标:将功能从单体服务逐步迁移到新服务,保持系统始终可用。
关键动作:
- 流量灰度:通过网关将少量流量路由到新服务
- 双写验证:关键数据同时写入新旧存储,比对一致性
- 功能开关:使用 Feature Flag 控制新旧逻辑切换
- 每日回归:每天运行核心测试用例,确保无回归
交付物:
- 每日迁移进度报告
- 双写一致性验证报告
- 性能对比数据
退出条件:
- 100% 流量已切换到新服务
- 双写验证连续 7 天无差异
- 性能指标不低于原系统
阶段四:清理与收尾(第 21-25 天)
目标:移除旧代码,完成最终收尾。
关键动作:
- 旧代码删除:移除单体服务中的已迁移代码
- 依赖清理:移除不再需要的依赖包
- 文档更新:更新架构文档、API 文档
- 经验总结:记录本次重构的经验教训
交付物:
- 清理后的代码仓库
- 更新后的架构文档
- 重构总结报告
退出条件:
- 旧代码完全移除,系统正常运行
- 所有文档已更新
- 团队完成知识传递
每日工作节奏
在长达数周的重构过程中,保持稳定的每日工作节奏至关重要。我们采用以下节奏:
上午(9:00-12:00):执行阶段
- 运行
/ralph-loop执行当天的迁移任务 - 监控实时指标,发现异常立即暂停
- 记录遇到的问题和解决方案
下午(14:00-17:00):验证与调整阶段
- 运行回归测试,验证上午的变更
- 分析测试结果,修复发现的问题
- 更新第二天的工作计划
傍晚(17:00-18:00):同步与复盘
- 向团队同步当日进度
- 记录当日学到的新知识到
AGENTS.md - 规划第二天的具体任务
关键实践
在整个重构过程中,以下实践被证明是关键成功因素:
- 每日提交:每天的变更都提交到 Git,保持进度可追溯
- 自动化验证:所有验证步骤都自动化,减少人为疏漏
- 渐进式暴露:每次只迁移一个小模块,控制风险范围
- 双向同步:新旧系统之间的数据保持双向同步,确保可随时回退
- 知识沉淀:每天将学到的领域知识写入
AGENTS.md,避免重复探索
完整工作流图
1 | |
Ralph Loop:上下文重置驱动的迭代工程模式
在讨论完具体的命令和工作流之后,我们需要深入理解支撑这些实践的核心理念:Ralph Loop。它不仅是一个命令,更是一种应对"上下文腐化"(Context Rot)的工程模式。
源头视角:Geoffrey Huntley
Ralph Loop 的概念最早由 Geoffrey Huntley 提出。他在维护大规模开源项目时发现,AI Agent 在长对话中会逐渐"迷失"——上下文窗口被大量中间状态填满,Agent 开始重复犯错、忽略之前的约束、甚至产生幻觉。
他的解决方案非常朴素:不让 Agent 在同一个上下文中运行太久。每一轮迭代完成后,强制开启一个新的会话,只保留必要的状态文件,丢弃所有中间对话历史。
这就是 Ralph Loop 的核心思想:用进程级的上下文重置,换取长期的稳定性。
核心理念:Context Rot
Context Rot(上下文腐化) 是指随着对话轮次增加,Agent 的上下文窗口逐渐被无关信息污染,导致性能下降的现象。具体表现包括:
- 注意力分散:Agent 开始关注无关细节,忽略核心任务
- 约束遗忘:之前设定的规则被逐渐忽略
- 重复劳动:Agent 重复执行已经做过的操作
- 幻觉增加:由于上下文混乱,Agent 开始编造不存在的信息
传统的解决方法是增加上下文窗口大小,但这只是治标不治本。更大的窗口意味着更多的噪声,Agent 的注意力机制仍然会在海量信息中迷失。
Ralph Loop 的思路相反:不追求更大的窗口,而追求更干净的窗口。通过定期重置上下文,确保 Agent 每一轮都从一个清晰的状态开始。
例一:open-ralph-wiggum
open-ralph-wiggum 是 Ralph Loop 的一个参考实现。它的核心逻辑非常简单:
1 | |
关键点:
- 每一轮都启动新的
opencode run进程 - 只通过文件系统(
state.md、output.md)传递状态 - 不保留任何对话历史
例二:OMO 实现
OMO 在 open-ralph-wiggum 的基础上做了增强,主要改进包括:
- Hook 机制:在每轮迭代前后注入自定义逻辑(如运行测试、更新知识库)
- 状态管理:使用结构化的
boulder.json替代简单的文本文件 - 错误恢复:检测到失败时自动回退到上一个稳定状态
- 并行验证:在 Agent 执行下一轮的同时,后台运行验证脚本
OMO 的 Ralph Loop 流程:
1 | |
两种实现的本质差异
| 维度 | open-ralph-wiggum | OMO |
|---|---|---|
| 状态传递 | 简单文本文件 | 结构化 JSON |
| 扩展性 | 硬编码逻辑 | Hook 机制可扩展 |
| 错误处理 | 简单重试 | 智能回退 |
| 验证机制 | 无 | 并行验证 |
| 适用场景 | 个人小项目 | 团队大型项目 |
本质上,open-ralph-wiggum 证明了 Ralph Loop 理念的可行性,而 OMO 将其工程化为一个可用的生产工具。
两种循环的信号检测视角
从信号检测的角度看,Ralph Loop 和传统长对话的本质差异在于噪声累积的控制:
传统长对话:
1 | |
Ralph Loop:
1 | |
通过进程级重置,Ralph Loop 确保每一轮的噪声不会带入下一轮,从而保持长期的稳定性。
深挖三种模式
在实际使用中,Ralph Loop 有三种常见模式:
模式一:严格 Ralph Loop
特点:每轮完全重置,只保留最少量的状态文件。
适用场景:
- 任务可以清晰分解为独立步骤
- 每步的输出可以作为下一步的输入
- 对上下文依赖较低
示例:
1 | |
模式二:混合 Ralph Loop
特点:部分状态持久化,保留关键的中间结果。
适用场景:
- 任务有一定的上下文依赖
- 需要保留部分历史信息
- 平衡稳定性和连续性
示例:
1 | |
模式三:Ultrawork Ralph Loop
特点:结合 Ultraworker 的多代理能力,在每轮中进行并行验证。
适用场景:
- 高可靠性要求的重构
- 需要多维度验证的任务
- 团队级的大型项目
示例:
1 | |
本质:短跑选手的接力赛
Ralph Loop 的本质可以用一个比喻来概括:
传统长对话像一个马拉松选手,试图一口气跑完全程。但随着距离增加,体力下降、注意力分散、错误增多。
Ralph Loop像一群短跑选手的接力赛。每个选手只跑一小段,保持最佳状态,然后通过接力棒(文件系统)将状态传递给下一个选手。
这个比喻揭示了 Ralph Loop 的核心优势:
- 每轮都是最佳状态:每个"短跑选手"都从清晰的上下文开始
- 状态传递标准化:接力棒(文件系统)是标准化的状态载体
- 容错性强:一个选手失误,不影响下一个选手从头开始
- 可观测性好:每轮的结果都可以独立检查和审计
理解了这一点,就能理解为什么 Ralph Loop 成为大型重构的标准模式——它不是简单地"多跑几轮",而是通过架构级的上下文管理,解决了单 Agent 长对话的根本缺陷。
子 Agent 的本质:上下文隔离与专门化
"子 Agent"这个词在多 Agent 系统的讨论中频繁出现,却鲜有人把它说清楚。它是一个能力弱化的 Agent,类似一个 Agent 化的工具?还是一个拥有更小上下文的原始 Agent,像从主 Agent fork 出来的进程?还是一个在指挥体系里听从领导 Agent、但拥有更强资源和能力的 Agent?
这三种直觉都不完全准确。本文从 Anthropic、LangChain、Claude Code 等权威来源出发,厘清子 Agent 的真实本质,并探讨一个更深层的问题:"子 Agent"究竟是能力描述,还是关系描述?
三种直觉,三种误解
在深入定义之前,先把三种常见直觉逐一检验。
误解一:子 Agent 是能力弱化的 Agent
这种直觉来自于"子"字的字面含义——子集、子系统、子进程,往往意味着更小、更弱。但 LangChain 官方文档明确指出:
“An interesting aspect of this approach is that sub-agents may have the exact same capabilities as the main agent.”
子 Agent 可以拥有与主 Agent 完全相同的能力。它不是弱化版,不是受限版,更不是"工具的高级包装"。
误解二:子 Agent 是拥有更小上下文的 fork
这种直觉来自操作系统的进程模型——子进程从父进程 fork,继承部分状态,但资源受限。这个类比有一定道理,但关键词不是"更小",而是"独立且干净"。
Anthropic 官方工程博客描述其多 Agent 研究系统时写道:
“Subagents facilitate compression by operating in parallel with their own context windows, exploring different aspects of the question simultaneously before condensing the most important tokens for the lead research agent.”
子 Agent 拥有独立的上下文窗口(own context windows),而不是主 Agent 上下文的子集。这个上下文窗口是全新的、干净的,不是"更小的"。
误解三:子 Agent 是听从指挥但能力更强的下属
这种直觉来自军事或企业的层级模型——将领指挥士兵,但士兵在具体战术上可能比将领更专业。这个类比在某些场景下成立,但它把"子 Agent"理解成了一种永久的身份,而非一种临时的关系。
真正的问题在于:一个 Agent 是否是"子 Agent",取决于它在当前任务中的角色,而非它的固有属性。
子 Agent 的真实本质
综合 Anthropic、Claude Code、LangChain 的权威定义,子 Agent 的本质可以用一句话概括:
子 Agent 是一个完整的、专门化的 Agent 实例,运行在独立的上下文窗口中,由主 Agent 通过工具调用方式协调,其核心价值在于上下文隔离,而非能力弱化。
五个核心特征
1. 上下文隔离
这是子 Agent 最核心的价值。LangChain 的表述最为精辟:
“This provides context isolation: each subagent invocation works in a clean context window, preventing context bloat in the main conversation.”
每次调用子 Agent,都是在一个干净的上下文窗口里开始工作。主 Agent 的历史对话、工具调用记录、中间推理过程,都不会污染子 Agent 的上下文。这防止了主 Agent 的上下文膨胀(context bloat)。
2. 完整的 Agent 能力
子 Agent 不是工具(Tool)。工具是单一功能的函数,被动执行,没有决策能力,没有 Agent Loop。子 Agent 是完整的 Agent 实例,拥有:
- 独立的推理-行动循环(Agent Loop)
- 自主规划和决策能力
- 多步骤任务执行能力
- 执行期间的短期记忆
3. 无状态执行
子 Agent 是无状态的(stateless)——它不保留跨调用的记忆。每次被主 Agent 调用,都是一次全新的执行。所有跨步骤的状态,由主 Agent 维护。
4. 专门化任务处理
子 Agent 通过自定义 system prompt 定义专门职责,可以针对特定领域优化。这种专门化不是能力的限制,而是能力的聚焦。
5. 不能递归派发子 Agent
Claude Code 官方文档明确指出:
“Subagents cannot spawn other subagents, so Agent(agent_type) has no effect in subagent definitions.”
子 Agent 不能再派发子 Agent。这是一个刻意的设计约束,防止无限递归和控制流混乱,保证行为的可预测性。
一个重要的附加结论:Skill 可以无限递归
子 Agent 不能递归派发,但 Skill 可以。
这里需要区分两个概念:
| 概念 | 本质 | 递归能力 |
|---|---|---|
| 子 Agent | 完整的 Agent 实例,独立上下文窗口 | ❌ 不能递归派发 |
| Skill | System prompt 的动态注入,上下文内的行为扩展 | ✅ 可以无限递归 |
Skill(如 Claude Code 中的 SKILL.md 机制)本质上是提示词注入,而非 Agent 实例的创建。当一个 Skill 被触发时,它的内容被注入到当前 Agent 的上下文中,Agent 按照 Skill 的指令行动。这个过程发生在同一个上下文窗口内,不涉及新 Agent 实例的创建。
因此,Skill A 可以触发 Skill B,Skill B 可以触发 Skill C,形成任意深度的调用链——这是纯粹的上下文内行为扩展,不存在"无限递归创建 Agent 实例"的风险。
这个区别揭示了一个深层设计哲学:Agent 实例的创建是重量级操作(需要独立上下文窗口),应该被约束;而上下文内的行为扩展是轻量级操作,可以自由组合。
"子 Agent"是关系描述,不是能力描述
这是本文最重要的洞见。
当我们说某个 Agent 是"子 Agent"时,我们描述的是它在当前任务中与其他 Agent 的关系,而不是它的固有属性或能力等级。
以 OMO(Oh My OpenAgent)的 Agent 团队为例:
1 | |
在这个架构图里,Prometheus、Oracle、Librarian 看起来是 Sisyphus 的"子 Agent"。但这个判断是错误的,或者说是不完整的。
Prometheus、Oracle、Librarian 都可以独立运行。
- 用户可以直接对话 Prometheus,让它制定战略规划,完全不经过 Sisyphus
- 用户可以直接调用 Oracle 进行架构咨询
- Librarian 可以作为独立的文档研究 Agent 被任何工作流调用
它们之所以在某些场景下表现为 Sisyphus 的"子 Agent",仅仅是因为 Sisyphus 在那个特定的任务中扮演了编排者的角色,通过工具调用的方式协调了它们。一旦任务结束,这种"主-子"关系就消失了。
这与传统软件工程中的"子系统"概念截然不同。子系统是静态的架构关系,一旦被设计为子系统,就永远是子系统。而 Agent 的"主-子"关系是动态的、任务级别的。
类比:项目中的角色 vs 职位
一个更直观的类比:
在一个软件项目中,张三是"项目经理",李四是"开发工程师"。但在另一个项目中,李四可能是项目经理,张三是开发工程师。他们的职位(固有属性)没有变,但他们在不同项目中的角色(关系描述)是不同的。
Agent 的"主-子"关系正是如此。Prometheus 的固有属性是"战略规划专家",但它在不同的任务中可以是:
- 独立运行的规划 Agent(直接与用户对话)
- Sisyphus 的子 Agent(被 Sisyphus 通过工具调用协调)
- 某个更高层编排器的子 Agent(在更复杂的多层架构中)
子 Agent vs Tool:本质区别
既然子 Agent 不是弱化的 Agent,那它和 Tool 的区别是什么?
| 维度 | Tool | 子 Agent |
|---|---|---|
| 本质 | 单一功能的函数/方法 | 完整的 Agent 实例 |
| 上下文 | 无独立上下文 | 拥有独立上下文窗口 |
| 决策能力 | 无,被动执行 | 有,可自主规划 |
| 循环能力 | 无 | 拥有 Agent Loop(推理-行动循环) |
| 记忆 | 无 | 执行期间有短期记忆 |
| 复杂度 | 单步操作 | 多步骤复杂任务 |
| 状态 | 无状态 | 执行期间有状态,调用间无状态 |
何时用 Tool,何时用子 Agent?
- Tool:任务是单步的、确定性的、不需要推理的(读文件、执行命令、调用 API)
- 子 Agent:任务需要多步骤推理、需要上下文隔离、需要专门化处理
实践含义
理解子 Agent 的本质,对实际的多 Agent 系统设计有直接影响:
1. 不要把"子 Agent"设计成永久的层级关系
如果你的系统里有一个 Agent 永远只能被另一个 Agent 调用,永远不能独立运行,这往往是过度设计的信号。好的 Agent 应该是可以独立运行的,"主-子"关系是任务级别的协调,而非架构级别的约束。
2. 上下文隔离是选择子 Agent 的核心理由
当你考虑是否要用子 Agent 时,最重要的问题不是"这个任务需要多强的能力",而是"这个任务需要独立的上下文吗?"如果任务足够简单,一个 Tool 就够了;如果任务需要多步骤推理但上下文不需要隔离,在主 Agent 内处理就够了;只有当任务需要多步骤推理且需要上下文隔离时,才需要子 Agent。
3. Skill 是上下文内的行为扩展,子 Agent 是上下文外的任务委托
这两种机制解决的是不同层面的问题。Skill 扩展的是当前 Agent 的行为能力,子 Agent 委托的是独立的任务执行。前者是"让我学会新技能",后者是"把这个任务交给专家去做"。
4. 协调者 Agent 应该"不写代码"
在多 Agent 团队的设计中,一个关键原则是:协调者 Agent 应当专注于调度与决策,而不直接参与执行细节。
这个设计有两层深意:
第一,保护协调者的全局视野。 执行细节(如代码实现、文件操作、调试过程)会迅速填满上下文窗口。如果协调者亲自下场执行,它会在技术细节中逐渐丢失对整体任务目标、约束条件和进度安排的掌控。就像一个项目经理如果整天埋头写代码,就会忘记关注项目风险、资源协调和交付时间线。
第二,确保执行者的上下文纯净。 当协调者将任务派发给子 Agent 时,子 Agent 只接收到最小化的任务描述——它需要做什么、遵循什么约束、返回什么格式。子 Agent 的上下文窗口不会被协调者的历史对话、中间推理、或其他无关任务污染。这种"干净"的上下文让子 Agent 能够高度专注地完成单一任务。
这正是 Divide and Conquer(分治) 思想在 Agent 系统中的体现:
| 层级 | 职责 | 上下文特征 |
|---|---|---|
| 协调者(Orchestrator) | 任务分解、进度追踪、质量把控、异常处理 | 保留全局任务图、约束条件、各子任务状态 |
| 执行者(Executor) | 具体实现、细节处理、单任务完成 | 仅包含当前子任务的输入、工具、输出规范 |
执行层往往是"重"的——涉及大量代码、多轮工具调用、错误重试。调度层必须是"轻"的——保持敏捷,随时准备调整策略。如果让协调者同时承担执行,这两层会互相干扰:执行细节膨胀会挤压全局规划的上下文空间,而频繁的全局状态查询又会打断执行的连贯性。
因此,"协调者不写代码"不是能力的限制,而是架构的自觉——通过严格的角色分离,让系统获得更好的可预测性和可维护性。
5. 上下文消耗的恶性循环与"就改一行"陷阱
如果这篇文章你只记住一件事,记这个:中等复杂度以上的任务,协调者绝不写代码。
为什么?因为 LLM 的上下文窗口是有限的。Agent 执行长任务时,窗口里慢慢堆起来的是代码 diff、编译错误、测试输出、lint 报告、重试记录……到第 40 次 tool call,早期的关键信息已经被压缩或丢弃了。Agent 开始"忘记"最初的架构决策,做出跟前面矛盾的改动。
这是一个恶性循环:
1 | |
一旦进入这个循环,就很难出来。到第 60 次 tool call,Agent 可能已经忘了最初的任务目标是什么了。你让它加个用户认证功能,它却在那儿纠结一个完全不相关的 import 路径问题。
解法是把 Agent 拆成两层。 协调者(Coordinator)只管规划、委派、汇总,一行代码都不碰;执行者(子 Agent)每次从干净的上下文开始,拿到精确的 prompt,干完就释放。子 Agent 不知道之前发生了什么,但它拿到的 prompt 里包含了它需要知道的一切。任务完成后,详细上下文被丢掉,协调者只保留一段摘要。信息经过了压缩和筛选,而不是无差别地堆在上下文里。
破坏这个原则最常见的方式是"只是快速改一下"。 协调者发现一个小问题,心想"这么简单的东西不用启子代理,我直接改了"。一次编辑变成了五次(因为牵扯出其他地方),五次变成二十次,上下文就被消耗殆尽了。
这个陷阱看起来很合理——“就改一行而已嘛”——但你低估了代码的牵连性。如果你发现协调者正在用 Edit 或 Write 工具修改源代码,立刻停下来,启动子代理。没有例外。
复杂度分层决策框架
不同复杂度的任务,执行方式也不同:
| 复杂度 | 判断标准 | 执行方式 | 示例 |
|---|---|---|---|
| 简单 | 能用一句话描述,且不包含"和"字 | 协调者直接执行 | 改 typo、加行日志 |
| 中等 | 需要清单来跟踪改了哪些地方 | 委派给子 Agent | 多文件一致性修改 |
| 复杂 | 需要做设计决策和权衡 | 委派 + Git Worktree 隔离 | 重构、新模块开发 |
Git Worktree 隔离相当于仓库的临时副本,子 Agent 在独立工作区执行结构性变更,成功了合并,失败了丢掉,不污染主分支。
模型选型的经济账
委派子 Agent 时还有一个容易被忽略的杠杆:不是所有任务都需要用最强的模型。
一个"重命名变量"的任务和一个"重构认证模块"的任务,对模型能力的要求完全不同。前者要的是快和便宜,后者要的是深度推理。如果所有子代理都用同一个顶级模型,既浪费钱又浪费时间。
协调者在委派时可以根据任务性质指定模型:
| 任务类型 | 推荐模型 | 理由 |
|---|---|---|
| 快速执行类(改 typo、简单重命名) | Claude Haiku | 响应快、成本低 |
| 深度推理类(复杂重构、架构级变更) | GPT-5.3 Codex / Claude Opus | 质量远比速度重要 |
| 代码检索类(大型代码库中定位文件) | Gemini 3 Flash | 速度第一 |
这种分层模型策略让系统在保证质量的同时,显著降低运行成本。
OMO:Prompt Engineering 构建的虚拟团队
OMO(Oh My OpenAgent,原名 Oh My OpenCode)通过精心设计的 system prompt 和 Hook 机制,在 OpenCode 内部构建了一支由 11 个专业 Agent 组成的虚拟团队。这不是简单的工具集合,而是一个基于 Prompt Engineering 的组织架构。
三层架构:规划层、执行层、工作者层
OMO 的 Agent 团队分为三个层次,各司其职:
1 | |
规划层负责将用户需求转化为结构化的执行计划。执行层负责任务的编排和调度。工作者层负责具体的执行工作。
多轨主 Agent:四个入口的选择逻辑
OMO 提供了四个用户可直接选择的主 Agent 入口,对应不同的工作模式:
| 入口 | 适用场景 | 核心特点 |
|---|---|---|
| Prometheus | 需求不明确,需要先规划 | 只做计划,不写代码 |
| Atlas | 已有计划,需要执行 | 按 Todo 列表波次执行 |
| Hephaestus | 需要深度自主执行 | 单 Agent、单上下文,禁止委托 |
| Sisyphus | 日常多 Agent 协作 | 委派优先,多上下文隔离 |
这四条路径彼此独立、互不委托。路由决策权保留给人类用户——Agent 不具备自主判断"当前任务更适合另一条路径"并切换的能力。
Sisyphus vs Hephaestus:委派优先 vs 深度自主
四个主 Agent 中,Sisyphus 和 Hephaestus 的对比最能体现"多轨"设计的核心张力:
| 维度 | Sisyphus | Hephaestus |
|---|---|---|
| 核心哲学 | 委派优先、并行默认、证据驱动 | 禁止询问、不中途停下、100% 完成 |
| 底层模型 | Claude Opus(编排能力优先) | GPT-5.3 Codex(推理能力优先) |
| Todo 列表 | ✅ 使用 boulder.json 管理 |
❌ 没有 Todo 列表 |
| 子 Agent 委托 | ✅ 可委托多种子 Agent | ❌ 禁止委托子 Agent |
| 上下文模式 | 多上下文:主 Agent 持续存在,子 Agent 每次重建 | 单上下文:全程一个 Agent |
| 探索方式 | 按 Todo 顺序,Atlas 调度时探索 | 写代码前强制并行探索(2-5 个 Explore Agent) |
| 中途交互 | 可通过 Todo 状态观察进度 | 不中途停下,直到 100% 完成 |
Hephaestus 是 OMO 中唯一一个"单 Agent、单上下文"的主 Agent。 它的核心设计哲学是:给目标,不给菜谱——用户只需描述最终目标,Hephaestus 自主决定如何达成,中途不停下来询问。
Sisyphus 是 OMO 的核心主编排器,代表"委派优先"的执行哲学。 它的核心设计哲学是:能委派就不自己做——Sisyphus 把任务拆解成结构化的 Todo 列表,然后委派给最合适的子 Agent 去执行。
两者的互斥性,恰恰反映了广度优先和深度优先在执行策略上的根本不兼容——你不能同时既并行委派又深度自主。
OMO Agent 角色规格表
OMO 完整的 11 个 Agent 各有明确的模型选型和职责边界:
| Agent | 默认模型 | 温度 | 模式 | 核心职责 |
|---|---|---|---|---|
| Sisyphus | claude-opus-4-6 | 0.1 | primary | 主编排器,接收用户请求后进行意图分类、委派任务、验证结果 |
| Hephaestus | gpt-5.3-codex | 0.1 | primary | 自主深度执行器,端到端完成复杂任务,不中途停下 |
| Prometheus | claude-opus-4-6 | 0.1 | all | 战略规划师,只做计划不写代码,输出 .sisyphus/plans/*.md |
| Atlas | claude-sonnet-4-6 | 0.1 | primary | Todo 列表编排器,按波次并行调度任务执行 |
| Oracle | gpt-5.2 | 0.1 | subagent | 只读高智商顾问,用于架构决策和疑难调试 |
| Metis | claude-opus-4-6 | 0.3 | subagent | 规划前顾问,在 Prometheus 生成计划前做 gap 分析 |
| Momus | gpt-5.2 | 0.1 | subagent | 计划审查员,验证计划的可执行性和引用正确性 |
| Librarian | glm-4.7 | 0.1 | subagent | 外部文档/代码搜索,克隆仓库、查官方文档、搜 GitHub |
| Explore | grok-code-fast-1 | 0.1 | subagent | 内部代码库搜索,回答"X 在哪里"类问题 |
| Multimodal Looker | gemini-3-flash | 0.1 | subagent | 多模态文件分析,处理 PDF/图片/图表 |
| Sisyphus-Junior | claude-sonnet-4-6 | 0.1 | all | 分类任务执行器,由 category 系统派生,不能再委派 task() |
温度差异值得注意:大多数 Agent 使用 temperature: 0.1(确定性输出),但 Metis 使用 0.3——作为"前置分析师",它需要更多创造性来发现潜在问题和盲点。
协作拓扑形成了清晰的分层结构:
1 | |
OMO 的一次循环:从请求到代码落地
以一个具体例子拆解 OMO 内部的执行过程。
场景:在 OpenCode 终端输入:
1 | |
第一步:Hook 拦截,识别 ulw 魔法词
OMO 通过 OpenCode 的 UserPromptSubmit Hook 拦截输入。检测到 ulw(ultrawork 的缩写)后,Hook 在提示词前注入系统级指令,激活完整的多 Agent 协作流程。这一注入对用户透明——Sisyphus 收到的实际提示词已包含"启动并行子 Agent、强制完成 Todo、使用 LSP 重构"等完整指令。
架构洞察:中间件/拦截器模式
从实现角度看,OMO 是一个典型的中间件/拦截器模式。它不直接实现 AI 对话逻辑,而是通过 OpenCode 提供的各个生命周期钩子点(UserPromptSubmit、PostToolUse 等)注入自己的逻辑。整个插件本质上是一个巨大的"拦截器集合"——每个钩子点对应一个拦截层,OMO 在这些层上叠加规划、调度、验证等能力,而 OpenCode 的核心对话引擎完全不感知这些注入的存在。
第二步:Prometheus 规划(规划层)
Sisyphus 框架首先调用 Prometheus(规划师),将需求拆解为结构化的 Todo 列表:
1 | |
Todo 列表是 OMO 的核心状态——写入 boulder.json(或 .sisyphus/ 目录下的状态文件),而非对话历史。这使得 OMO 能跨会话恢复:即使中途关闭终端,Todo 列表仍保存在文件中。
第三步:并行子 Agent 执行(工作者层)
Atlas(指挥官) 读取 Todo 列表,识别无依赖关系的任务并行执行。Todo 1(扫描调用方)和 Todo 2(查文档)互相独立,Atlas 同时启动两个子 Agent:
1 | |
两个 Agent 并发工作,各自完成后将结果汇报给 Atlas。这是"并行"的真实含义——多个 AI 进程同时运行,而非顺序问答。
第四步:Sisyphus-Junior 执行核心编码
Atlas 将 Explore 和 Librarian 的结果(调用方列表 + API 文档)传给 Sisyphus-Junior(主力执行者),开始重构:
1 | |
LSP 的介入是关键:Sisyphus-Junior 通过 LSP 的 publishDiagnostics 接口获得编译级别的反馈,确保重构不破坏任何引用,而非依赖人工跑测试验证。
第五步:Todo 执行器强制收尾
OMO 内置 Todo 执行器(Todo Enforcer):通过 PostToolUse Hook,每次工具调用结束后检查 Todo 列表,若有未完成项,则将"继续完成剩余 Todo"的指令注入到下一轮提示词。这一机制确保 Agent 持续工作直到任务完成,而非依赖模型的自律性。
流程 ASCII 图:多轨主 Agent 入口
OMO 有四个用户可直接选择的主 Agent 入口,对应不同的工作模式。以下展示核心路径的完整流程。
路径一:Sisyphus ulw 路径(委派优先,Todo 驱动)
1 | |
路径二:Hephaestus 路径(自主深度执行,推理优先)
Hephaestus 是与 Sisyphus 并列的第二个主 Agent,使用 GPT-5.3 Codex 模型,专为深度架构工作和复杂调试设计。它的核心哲学是"给目标,不给菜谱"——用户只需描述最终目标,Hephaestus 自主决定如何达成,中途不停下来询问。
1 | |
boulder 机制:对抗 Context Rot 的注意力刷新
**Context Rot(上下文腐化)**是 Transformer 架构的数学属性:随着上下文增长,模型对中间位置信息的注意力最弱(Lost-in-the-Middle 效应),每个 Token 分到的注意力权重趋近于零(注意力稀释),语义相似但无关的内容会主动误导模型(干扰项叠加)。
研究表明,每个 AI Agent 的成功率在 35 分钟后都会下降,任务时长翻倍,失败率翻四倍。这是因为 35 分钟后,Agent 通常已经读取了 15-30 个文件,上下文中有效信号的占比已经低到临界点。
boulder 机制的应对策略非常直接:通过 PostToolUse Hook,在每次工具调用结束后,将"继续完成剩余 Todo"的指令重新注入到提示词的尾部。这意味着:
- 无论上下文已经积累了多少历史,当前任务目标始终出现在上下文的最后一条消息
- 模型在每次推理时,都能以最高注意力感知到"我还有什么没做完"
- Todo 列表本身存储在
boulder.json文件中(持久化状态),而非依赖上下文记忆
这是一个精妙的设计:用文件系统承载状态,用尾部注入保持注意力。boulder 机制没有试图解决 Context Rot(那需要改变 Transformer 架构),而是绕过了它——通过持续将任务目标"刷新"到注意力最强的位置,让 Context Rot 的影响无法积累到足以干扰任务执行的程度。
多维度委派:像造房子一样同时发展
问题:往不同维度委派不同的 tuned 过的 Agent 工程师团队,是不是像造房子一样,长宽高同时发展的最优模式?
答案:这个类比非常准确,但需要补充一个关键约束条件。
传统软件开发的瓶颈之一是串行依赖:前端等后端 API,后端等数据库 Schema,测试等功能完成。这种串行性不是因为工程师不够聪明,而是因为人类工程师的注意力是单线程的——同一时间只能深度专注于一件事。
多 Agent 架构打破了这个约束。OMO 的 11 个 Agent 按职责维度分工:
- 规划维度(Prometheus):负责任务分解,不参与执行
- 执行维度(Sisyphus-Junior):负责代码实现,不主导规划
- 搜索维度(Librarian):负责文档查询,不干扰编码
- 审查维度(Atlas):负责代码审查,不参与生成
这确实像造房子:地基、框架、水电、装修可以在不同维度同时推进,只要接口约定清晰(墙的位置、管道走向),各工种互不干扰。
但"造房子"类比也揭示了一个关键约束:并行的前提是接口的提前约定。如果前端和后端同时开发,必须先约定好 API 契约;如果多个 Agent 并行执行,必须先约定好文件边界和状态格式。OMO 的 .sisyphus/plans/ 目录和 boulder.json 正是扮演这个"建筑图纸"的角色——它们定义了各 Agent 的工作边界,使并行成为可能。
所以更精确的表述是:多维度委派是最优模式,但前提是有一个强规划层(Prometheus)先把"图纸"画好。没有图纸的并行,不是造房子,是各自为战。
计划-委派-执行-测试-审查:瀑布流的复活?
问题:当前的计划→委派→执行→测试→审查模式,是不是重新复现了当年瀑布流的工程项目管理模式?
答案:形似瀑布,但本质不同——关键差异在于迭代速度和反馈机制。
表面上看,Agentic Coding 的工作流确实与瀑布模型高度相似:
| 瀑布模型 | Agentic Coding |
|---|---|
| 需求分析 | Prometheus 规划,生成 Todo 列表 |
| 系统设计 | 技术方案 Agent 输出架构文档 |
| 编码实现 | Sisyphus-Junior 并行执行 |
| 测试验证 | 测试 Agent 部署到测试环境验证 |
| 部署上线 | 部署 Agent 执行发布流程 |
但瀑布模型的核心问题不是"有没有阶段",而是阶段之间的反馈延迟:需求分析完成后,要等几个月才能看到测试结果,发现问题时已经积累了大量错误的实现。
Agentic Coding 的关键差异在于:
- 迭代速度:一个完整的"计划→执行→测试"循环可以在分钟级完成,而不是月级。这使得"发现问题→修正方向"的成本极低。
- 并行反馈:测试 Agent 不需要等所有功能完成才开始工作,可以在执行 Agent 完成部分功能后立即介入验证。
- 动态规划:Prometheus 的规划不是一次性的,而是可以根据执行结果动态调整——这是瀑布模型最根本的缺陷所在(需求冻结)。
更准确的类比是:Agentic Coding 更像极速迭代的敏捷开发,只是把"Sprint"的时间单位从两周压缩到了几分钟,把"团队成员"从人类工程师替换为了专职 Agent。它保留了敏捷的核心——快速反馈、持续验证——而不是瀑布的核心——阶段冻结、线性推进。
当然,有一个场景确实更接近瀑布:当任务足够大、规划足够复杂时,Prometheus 的初始规划会变得非常重要,一旦规划方向错误,后续所有 Agent 的执行都会偏离。这是 Agentic Coding 目前最需要人类介入的环节——不是执行,而是规划的正确性验证。这也是为什么 Human-in-the-Loop 在企业级框架中被显式设计为"需求澄清"阶段:让人类在规划阶段介入,而不是在执行阶段救火。
两种循环的检验对象:它们到底在检验谁?
问题:外部 ralph CLI 和 OMO 内部 /ralph-loop 这两种循环,检验的是西西弗斯的完成状态,还是西西弗斯的子 Agent 的完成状态?
这个问题触及了 Agentic Coding 中一个常被忽视的结构性盲点。要回答它,需要先厘清一个前提:在 OMO 的多 Agent 架构中,实际执行任务的是子 Agent,而不是西西弗斯本身。西西弗斯是编排者,它把具体工作委派给 Sisyphus-Junior、Explore、Librarian 等子 Agent 去完成。
那么,当任务未完成时,未完成的是谁?是子 Agent——是 Sisyphus-Junior 没有真正重构完 formatDate,是 Explore 没有找到所有调用方。西西弗斯自己只是一个调度层,它感知不到子 Agent 是否真正完成了任务,只能看到子 Agent 的文字报告和文件变更。
这就引出了两种循环的检验对象问题。
外部 ralph CLI(open-ralph-wiggum)检验的是西西弗斯 session 的输出。
外部 ralph 是 Shell 层面的进程级循环脚本。每次迭代,它启动一个全新的西西弗斯 session,等待该 session 结束,然后检查 session 的 stdout 输出中是否包含完成标记(默认是 <promise>COMPLETE</promise>):
1 | |
关键在于:外部 ralph 看不到子 Agent 的 session。子 Agent 在西西弗斯内部运行,它们的输出被西西弗斯消化后,只有西西弗斯的最终输出才暴露给外部 ralph。完成标记 <promise>COMPLETE</promise> 是由西西弗斯输出的,不是子 Agent 输出的。
OMO 内部 /ralph-loop 检验的也是西西弗斯 session 的输出。
/ralph-loop 是 OMO 通过 Stop Hook 实现的内部循环机制。当西西弗斯 session 尝试退出时,Stop Hook 拦截这个退出信号,检查 session 输出中是否包含完成标记(如 <promise>DONE</promise>):
1 | |
与外部 ralph 一样,Stop Hook 监听的是整个西西弗斯 session 的输出,而不是任何子 Agent session 的输出。子 Agent 的 session 在西西弗斯内部是隔离的,Stop Hook 无法直接感知它们的状态。
两种循环的本质共同点:都只能检验西西弗斯层面。
| 维度 | 外部 ralph CLI | OMO /ralph-loop |
|---|---|---|
| 实现位置 | Shell 进程层(外部) | OpenCode Stop Hook(内部) |
| 检验对象 | 西西弗斯 session 的 stdout | 西西弗斯 session 的输出 |
| 完成标记输出者 | 西西弗斯(主 Agent) | 西西弗斯(主 Agent) |
| 子 Agent 可见性 | 不可见 | 不可见 |
| 会话管理 | 每次迭代启动全新 session | 同一 session 内循环 |
这揭示了一个深层的结构性问题:两种循环都依赖西西弗斯的"自我报告"。西西弗斯说"完成了"(输出完成标记),循环就退出;西西弗斯没说完成,循环就继续。但西西弗斯自己对子 Agent 的感知,也只是子 Agent 的文字报告——这是一个信任传递链:
1 | |
链条上的每一环都是"荣誉系统"(honor system):没有机制能自动验证子 Agent 是否真正完成了任务,也没有机制能验证西西弗斯是否正确评估了子 Agent 的工作。如果子 Agent 伪完成(声称完成但实际未完成),西西弗斯可能会被误导,进而输出完成标记,导致循环提前退出。
这就是为什么 Human-in-the-Loop 在 Agentic Coding 中不可或缺——不是因为 Agent 不够聪明,而是因为完成状态的验证链条本质上是软约束,最终需要人类的眼睛来做最后一道确认。
Agentic Coding 的人机分工
Agentic Coding 改变的不只是工具,而是人在研发流程中的角色定位。
在传统开发模式下,开发者是执行者:写代码、调试、部署、验证。在 Agentic Coding 模式下,开发者是架构师和审查者:
- 定义目标:描述要解决的问题,而不是描述解决步骤
- 设定约束:告诉 Agent 不能做什么(如不能修改某个核心模块)
- 审查结果:在 Agent 完成任务后,判断结果是否符合预期
- 介入纠偏:当结果偏离预期时,提供更精确的指导
这种分工有一个关键前提:人类的"品味"。品味不是审美,而是决策力——在众多可行方案中判断"哪个是对的选择",尤其体现为"选择不做"。AI 擅长执行与优化,但缺乏责任意识与经验直觉;人类凭借实践经验积累、业务理解与后果承担能力,把控质量边界与必要性判断。
多工具生态融合:配置文件如何协同
从单工具到多 Agent 团队,这条演进路径的终点并不是"选一个最好的工具",而是让多个工具协同工作——这正是"从工具到团队"这个命题的自然延伸:当 Agent 本身已经开始组队,工具之间的配置知识也需要统一管理,否则团队协作的收益会被配置漂移抵消掉。
一个现实的问题是:团队可能同时使用 Claude Code、Cursor 和 OpenCode(配合 OMO)。每个工具都有自己的配置体系——Claude Code 用 CLAUDE.md,Cursor 用 .cursorrules,OMO 用 AGENTS.md 和 SKILL.md。真正的挑战不是"配置写在哪",而是如何避免同一份项目知识在多个文件里重复、漂移和失真。
更稳妥的做法是把它理解为一个分层配置体系:
- 项目全局知识放在
AGENTS.md,作为跨工具共享的主入口; - 工具专用规则放在
CLAUDE.md、.cursorrules之类的轻量文件里,只保留该工具独有的行为约束; - 领域技能放在
SKILL.md中,按需加载; - 任务计划与进度放在
.sisyphus/plans/等工作目录中,负责跨会话延续执行。
换句话说,多工具协作的关键不是"每个工具都配一遍",而是建立单一真相源 + 工具侧薄包装的结构:共识写一次,工具按需引用。这样,Agentic Coding 才不会从"工程协作系统"退化成"配置文件迷宫"。
如果只用 OpenCode,如何继承 Cursor 和 Claude Code 的 Rule 遗产?
如果你只用 OpenCode,但项目里已经有了 .cursorrules 和 CLAUDE.md,不必推倒重来。可以按以下步骤渐进迁移:
第一步:分类审计现有规则
把 .cursorrules 和 CLAUDE.md 里的内容按"通用性"分类:
| 类型 | 特征 | 示例 |
|---|---|---|
| 项目共识 | 与工具无关,描述业务、架构、约定 | "所有 API 返回格式为 { code, data, message }" |
| 工具行为约束 | 特定工具的交互模式 | “Cursor 的 Tab 补全触发条件” |
| 编码规范 | 语言/框架层面的风格 | “TypeScript 项目使用 4 空格缩进” |
第二步:迁移项目共识到 AGENTS.md
把"项目共识"类内容提取出来,写入 AGENTS.md。这是跨工具共享的主入口,格式建议:
1 | |
第三步:保留工具专用规则
.cursorrules 和 CLAUDE.md 不要删除,但只保留该工具独有的行为约束。例如:
1 | |
第四步:建立引用关系
在工具专用规则文件里,显式引用 AGENTS.md:
1 | |
第五步:用 SKILL.md 承载领域技能
如果项目有特定领域的复杂规则(如 React 组件开发规范、API 设计指南),可以拆分为独立的 SKILL.md,放在 skills/ 目录下。OpenCode 的 SKILL 机制支持按需加载,避免 AGENTS.md 臃肿。
1 | |
迁移后的结构对比:
| 迁移前 | 迁移后 |
|---|---|
.cursorrules 包含所有规则(重复) |
.cursorrules 只保留 Cursor 特有约束(薄包装) |
CLAUDE.md 包含所有规则(重复) |
CLAUDE.md 只保留 Claude Code 特有约束(薄包装) |
无 AGENTS.md |
AGENTS.md 作为单一真相源(项目共识) |
| 无 SKILL 机制 | skills/*/SKILL.md 按需加载(渐进披露) |
关键原则:
- 不要删除原文件:保留
.cursorrules和CLAUDE.md,但只保留工具特有的内容 - 先审计再迁移:理解每条规则的目的,避免无脑搬运
- 渐进式迁移:可以先迁移高频使用的规则,低频规则后续再处理
- 测试验证:迁移后让 Agent 执行几个典型任务,验证规则是否生效
这样,你既继承了 Cursor 和 Claude Code 的规则遗产,又避免了多工具协作时的配置漂移问题。
小结:Agentic Coding 真正改变的是什么?
回过头看,Agentic Coding 真正带来的变化,不只是"AI 能写更多代码",而是软件工程的组织方式开始被重新包装为可调用、可验证、可并行的流程。
从裸 OpenCode 到 Ralph Loop,再到 OMO 和更偏企业化的 Agent 框架,演进路径其实很清晰:
- 第一阶段解决"单个 Agent 能不能理解并修改代码";
- 第二阶段解决"任务能不能在验证约束下持续推进";
- 第三阶段解决"多个角色、多个上下文、多个知识源能不能像团队一样协作"。
这也是为什么我更愿意把 Agentic Coding 看成一次从工具到组织的迁移。代码生成当然重要,但更重要的是:规划、委派、验证、知识沉淀这些原本分散在人类团队中的动作,正在被重新抽象为一套可执行的协作系统。
结合前文对子 Agent 本质的分析,我们可以得出几个关键结论:
- 子 Agent 是完整的 Agent,可以拥有与主 Agent 相同甚至更强的能力,其核心价值在于上下文隔离而非能力弱化
- "子 Agent"是关系描述,不是能力描述——Prometheus、Oracle、Librarian 这样的 Agent 可以独立运行,也可以在特定任务中作为子 Agent 被协调
- 主-子关系是动态的、任务级别的,不是静态的架构约束,好的 Agent 设计应该让每个 Agent 都能独立运行
- 协调者不写代码是架构的自觉,通过严格的角色分离让系统获得更好的可预测性和可维护性
- 上下文隔离是选择子 Agent 的核心理由,中等复杂度以上的任务,协调者绝不写代码,以避免上下文消耗的恶性循环
理解了这些,你就能更清晰地设计多 Agent 系统:让每个 Agent 都能独立运行,让"主-子"关系服务于任务,而不是被架构固化。
参考资料
核心概念
- Context Rot 研究:Long Context is Not All You Need: Diagnosing and Mitigating the Performance Degradation of Long-Context LLMs(Chroma,2025)——测试了 18 个前沿模型,发现所有模型都会随上下文增长而性能下降,提出了"35 分钟墙"和 Lost-in-the-Middle 效应的量化数据。
- Everything is a Ralph Loop:ghuntley.com/loop/(Geoffrey Huntley,2026)——Ralph Loop 设计哲学的源头文章,提出"软件开发是陶轮上的黏土"和"编程循环而非砌砖"的核心论断。
- 子 Agent 的本质:本文第三部分深入分析了子 Agent 的上下文隔离特性、关系描述本质以及与 Skill 的区别。
- 框架税与 SDK-first 构建:本文"为什么不用 LangChain / LangGraph 构建 Coding Agent"章节分析了 Agent 框架的抽象开销与 Coding Agent 对上下文精确控制的结构性冲突。
工具与框架
- OpenCode:opencode.ai / GitHub: opencode-ai/opencode(原仓库
sst/opencode已迁移至此)——开源终端 AI 编程 Agent,本文讨论的工具层基础。 - open-ralph-wiggum:GitHub: Th0rgal/open-ralph-wiggum——Ralph Loop 的标准外部 CLI 实现,每次迭代启动全新 OpenCode 进程,实现进程级上下文重置。
- Oh My OpenAgent(OMO):GitHub: code-yeongyu/oh-my-openagent——通过配置文件和 Prompt Engineering 在 OpenCode 内部构建多 Agent 虚拟团队,包含 Sisyphus、Prometheus、Atlas 等 11 个角色。
- Anthropic Engineering Blog:How we built our multi-agent research system(2025)——Anthropic 官方对多 Agent 研究系统的工程实现详解。
- Claude Code 官方文档:Create custom subagents——Claude Code 关于子 Agent 创建的官方文档。
- LangChain 官方文档:Subagents——LangChain 对子 Agent 概念的官方定义和使用指南。
- LangChain Blog:Benchmarking Multi-Agent Architectures——LangChain 对多 Agent 架构的性能基准测试。
- Langfuse AI Agent 对比:AI Agent Framework Comparison(2025-03)——对 LangChain、LangGraph、OpenAI Agents SDK 等框架的系统性对比分析。
- VoltAgent 框架综述:AI Agent Frameworks(2025)——对主流 Agent 框架优劣势的全面评估。
延伸阅读
- LSP 与 AI 编程助手:LSP:语言服务协议与AI编程助手的代码理解能力——本文的姊妹篇,详细阐述代码理解能力的底层机制。
- Multi-Agent Performance Research:Anthropic 的研究表明,多 Agent 架构可将性能提升 90.2%(参见 Anthropic 博客:Building effective agents),核心原因是主 Agent 的上下文保持干净,而非子 Agent 更聪明。





