context 是怎么构建的?
opencode 的 context也是分为这几部分:
- System Prompt:
- environment:包含模型名称、工作目录、git 状态、平台、日期等;
- skills:加载当前 agent 可用的 skill 工具描述
- system:项目内向上查找的
AGENTS.md/CLAUDE.md/CONTEXT.md,全局~/.claude/CLAUDE.md,config.instructions中配置的文件或 URL
- Messages(历史消息 + 本轮消息):主要查询本次 session 的信息,从新的往旧的数据进行查找,分别拼接 user message 和 assistant message ;
┌─────────────────────────────────────────┐
│ System Prompt │
│ ├── 环境信息(模型名、目录、平台、日期) │
│ ├── Skills 列表 │
│ └── 指令文件(AGENTS.md / CLAUDE.md) │
├─────────────────────────────────────────┤
│ Messages(历史 + 本轮) │
│ user → assistant → user → assistant… │
└─────────────────────────────────────────┘
除此之外,上下文窗口管理还有三层机制防止上下文溢出(下面也会详细介绍):
- Compaction(压缩):如果token 数接近模型上限,那么会用全量历史调一次模型,生成结构化摘要,然后在对话中插入一个"压缩点"。下次构建 context 时,遇到这个标记就截断,只保留摘要 + 之后的消息。
- Prune(裁剪):对话结束后异步执行,从最新消息往前遍历,超过 40,000 token 保护边界之外的旧工具调用结果,将输出替换为
"[Old tool result content cleared]"。工具调用的结构(input/output 字段)保留,只清空 output 内容,这样不会破坏 Anthropic 要求的 tool_use/tool_result 配对。 - 图片/媒体单独处理:对不支持工具结果中含图片的接口(非 Anthropic/OpenAI/Bedrock),自动把图片提取出来变成单独一条合成 user 消息注入
Prompt caching
Prompt Caching 是大模型提供商提供的一种优化机制:将某些内容(通常是较长且重复的前缀)在服务端缓存起来,后续请求命中缓存后跳过对该部分的重新计算,从而降低延迟和成本(缓存命中的 token 通常价格更低)。
Prompt Caching 通过一般是在调用的时候通过给 context 打标记实现,比如当使用 Claude 的时候会选择前两条 system + 最后两条对话打标记,因为System prompt 内容几乎不变,最后两条对话消息是模型下一步推理的直接上文,命中率最高。
打了标记后,Anthropic API 会在服务端把这段内容写入缓存,下次相同请求直接复用,不重新计算。
大致装配好之后是这样:
messages = [
{ role: "system", content: system[0] }, // ← cache 标记
{ role: "system", content: system[1] }, // ← cache 标记
...历史对话消息,
...最后两条 // ← cache 标记
]
context 体积控制 prune & compact
compact 是“总结历史,重建上下文”。
prune 是“保留历史骨架,清空很老的 tool 输出正文”。
- compact: 用一条 summary 替代一大段旧对话
- prune: 不替代消息,只把旧 tool result 的大文本删掉,换成占位符
Compact
- 启动一个专门的 compaction agent
- 把当前有效历史送给模型
- 让模型生成一份可继续工作的总结
- 把这份总结存成一条 assistant message,并标记 summary: true
之后在正常对话里,filterCompacted() 会以这条 summary 为边界,只保留“summary 之后的有效历史”,见 message-v2.ts (line 810)。
什么时候用 compact
主要在上下文接近/超过模型限制时。触发点在 prompt.ts (line 547):
- 如果最近一次完成的 assistant message token 太多
- SessionCompaction.isOverflow(…) 返回 true
- 就创建 compaction 任务
if (task?.type === "compaction") {
const result = await SessionCompaction.process({
messages: msgs,
parentID: lastUser.id,
abort,
sessionID,
auto: task.auto,
overflow: task.overflow,
})
if (result === "stop") break
continue
}
Prune
SessionCompaction.prune({ sessionID })
- 倒着扫描旧消息
- 只关注 tool 的 completed output
- 保留最近一部分 tool 输出
- 对更老的 tool 输出打上 time.compacted
- 后续 toModelMessages() 会把这类输出替换成 [Old tool result content cleared],见 message-v2.ts (line 638)
tool result 防膨胀机制
第一层是“单次工具输出截断”
在 truncation.ts (line 11):
- 最大 2000 行
- 最大 50KB
- 超过就截断
- 完整输出落盘到 Global.Path.data/tool-output/…
这意味着一次工具调用就算返回特别大的文本,也不会原样塞回会话上下文。MCP 工具返回文本时就会走这层,见 prompt.ts (line 894) 附近 Truncate.output(…)。
最后会输出成:
const hint = hasTaskTool(agent)
? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse the Task tool to have explore agent process this file with Grep and Read (with offset/limit). Do NOT read the full file yourself - delegate to save context.`
: `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\nUse Grep to search the full content or Read with offset/limit to view specific sections.`
const message =
direction === "head"
? `${preview}\n\n...${removed} ${unit} truncated...\n\n${hint}`
: `...${removed} ${unit} truncated...\n\n${hint}\n\n${preview}`
第二层是“跨多轮的旧 tool output 清理”
就是上面提到到 prune:
- 倒序扫描旧的 completed tool outputs
- 保留最近的一部分
- 更老的输出标记为 compacted
- 之后在 message-v2.ts (line 638) 会被替换成 [Old tool result content cleared]
isOverflow
SessionCompaction.isOverflow() 在 compaction.ts (line 25) 会根据:
- input tokens
- output tokens
- cache tokens
判断是否接近上下文上限。
一旦超限,就自动创建 compaction 任务,把旧历史总结掉。
doom loop 检测
在 processor.ts (line 126):
- 如果连续 3 次调用同一个 tool
- 输入还完全相同
- 会触发权限确认 doom_loop
这防的是 agent 在循环里重复打同一个工具,导致历史疯狂增长。