跳到主要内容

opencode — 本课题摘录

读了哪几篇: 01-agent-loop(主循环)、02-tool-system(工具系统)、05-context-compaction(上下文压缩)。 其余两篇(容错编辑、权限系统)本轮没读——属工具层与权限课题。

这是 TypeScript 写的终端编码 agent,跟我们同语言;它对本课题的最大贡献是"停止条件不能只信厂商"这条踩过的坑。

它对本课题回答了什么

决定四:停止条件不能只信厂商给的"结束原因" —— 一条真实踩过的坑

它的退出判断不是一个条件,是三个:

上一条助手消息「已正常结束」
且 没有待办工具
且 用户消息早于它
→ 退出

关键在第二个条件。 它的注释明说:有些厂商在助手消息里明明包含工具调用时,也会把结束原因标成"已停止"。 (依据:Agent 库 · opencode · Agent 主循环 —— 退出判断除了看 finish 还要看 hasToolCalls,注释明说 "Some providers return 'stop' even when the assistant message contains tool calls",这是真实踩过的坑)

所以除了看厂商给的结束原因,还要自己数一遍消息里有没有未完成的工具调用。

这一条要抄进配方,而且是硬约束: 停止判据必须建立在"我自己能观察到的事实"上(消息里有没有工具调用),不能只建立在"厂商说什么"上。 这跟 fara 的"步数耗尽被标成完成"是同一类问题的两个位置——状态字段会撒谎。

它的停止条件本身是:"既无工具调用又正常结束"。

决定一:每一轮重新组装,不复用上一轮

每轮循环:取(压缩后的)历史 → 选模型 → 解析本轮可用工具 → 拼系统提示 → 起流式处理。

注意"取压缩后的历史"是在循环顶部做的——压缩的结果通过历史本身生效,循环体不知道压缩这回事。

决定三:每个工具包成统一外壳

每个工具都被包成"参数解码 → 执行 → 输出截断"的统一外壳。 (依据:Agent 库 · opencode · 工具系统 —— 每个工具用 Tool.define 包成「schema 解码 → 执行 → 输出截断」的统一外壳,注册表收集内置/插件/自定义工具并按模型与 agent 过滤后喂给模型)

"输出截断"被放进这个外壳里,而不是让每个工具自己管——这是个正确的位置选择:所有工具都会遇到"结果太大",不该每个工具重复实现。

工具注册表收集内置、插件、自定义三类来源,按模型与 agent 过滤后再喂给模型。

决定一:压缩分成"总结"和"裁剪"两件事

这个拆分很重要,别家常把它们混为一谈:

动作做什么
总结让模型读早期历史,写一段"到目前为止发生了什么"的摘要,作为一条特殊记录插进去
裁剪把摘要之前的旧消息从"喂给模型的历史"里剔掉(原始记录仍在库里,只是不再发给模型),但保护最近若干轮和某些关键工具输出

(依据:Agent 库 · opencode · 上下文压缩 —— 压缩分「总结」(让模型写一段摘要插入)与「裁剪」(把摘要之前的旧消息从喂给模型的历史里剔掉,原始记录仍在 SQLite)两件事)

注意:原始记录仍在库里,只是不再发给模型。

这正好是 codex(真改历史)和 deepagents(完全不改、只投影)之间的中间态: 物理上不删,逻辑上不发。 三种做法排成一条谱系,这一份最实用。

决定一补充:裁剪的预算常量 —— 本课题最具体的一份

它不是"砍到摘要为止"那么粗暴,而是有一套明确的常量:

常量含义
最小裁剪量20,000低于这个 token 量不值得裁
保护段40,000这一段最近历史受保护、不裁
保底轮数2至少保留最近 2 轮完整对话
保留的最近内容2,000 – 8,000一个区间
工具输出上限2,000 字符裁剪时把工具输出压到这个上限
免裁工具技能类它含长效指令

(依据:Agent 库 · opencode · 上下文压缩 —— 裁剪的预算常量包括 PRUNE_MINIMUM=20000、PRUNE_PROTECT=40000、DEFAULT_TAIL_TURNS=2、保留最近内容 2000-8000 token、工具输出压到 2000 字符、skill 工具输出免裁因为它含长效指令)

它给的直觉:越近的越完整保留,越远的越敢压。

"技能类工具的输出免裁"这一条,跟 agent-skills-spec 的"防压缩"是同一条规则的两个实现——一个写在规范里,一个写在常量表里。

这张常量表对我们直接有用。 我们要写压缩时,"从哪个量级开始裁""保几轮""工具输出压到多少", 这是唯一一份把数字写出来的参照。

决定四补充:溢出检测有两个触发点,而且能被厂商报错触发

触发点怎么触发
流处理中每一步结束后检查本步用量是否溢出,是则提前停掉当前流
循环顶部下一轮看到上一步溢出,排一个压缩任务
厂商直接报错厂商报"上下文超限"时,转成"需要压缩"

(依据:Agent 库 · opencode · 上下文压缩 —— 压缩触发点在流处理中(每步 step-finish 后检查用量溢出则提前停流)与循环顶部(排压缩任务),另外 ContextOverflowError 也能触发压缩(除非用户关了自动压缩))

第三个触发点值得记:不要只靠自己估算 token 数。 自己估会不准,厂商的报错是最后一道真实信号——要把它转成"该压缩了",而不是让它冒成一个失败。

"找出已完成的压缩点"是靠配对判定的: 有压缩标记的用户消息 + 对应的、已结束且无错误的摘要消息,两者配成一对才算数。

这个配对判定是必要的——压缩本身也可能失败,失败的压缩不能被当成已完成。

它没回答什么

  • 不用原生工具调用怎么办——它假设模型支持。
  • 循环状态怎么整个存下来续跑——历史在库里,但没有"从中断点精确续跑"这层。
  • 工具清单怎么按情况增删——有"按模型与 agent 过滤",但没讲每轮变化。

坑与代价

  • 压缩是有损的,跟 codex 一样。它用"保护最近若干轮 + 免裁长效指令"来控制损失面,但摘要之前的细节仍然对模型不可见了。
  • 预算常量是写死的。 不同模型的窗口差几十倍,一套常量未必都合适。

    判断(无锚): 这些数字应该按模型窗口比例算,而不是绝对值。比如"保护最近 20% 的窗口"而不是"保护 40000 token"。 如果错,会错在: 如果实际瓶颈不是窗口大小而是成本(小窗口模型也可能很贵),那按绝对 token 数控制反而更直接。

  • "停止条件不能只信厂商"意味着要自己维护一份"未完成工具"的账。 这份账要跟流式解析配合,不能等整段收完再数。