引言 七个藏在源码里的 Token 机制 Token 估算:五个工具用了同一个魔法数字——它有多不准? 隐形起步价:你开口之前,工具已经替你花了多少? 服务商陷阱:工具显示的用量,为什么不能直接信? 自动压缩:你以为省了钱,但账单是这么算的 Prompt 缓存:能省 90% 的机制,为什么你没用上? 思考模式:"自适应思考"不等于省钱,真相是? 命令输出洪水:一条命令,怎么把后续对话全拖慢了? 两类 Agent 的本质差异:能做 ≠ 适合做 结语 可操作参考清单
引言
2026 年 3 月 31 日,Claude Code v2.1.88 发布时,一个 60MB 的 source map 文件被意外打包进了 npm 发布包。
1906 个源文件、51 万行代码,完整暴露,Claude Code 被开源。
全网炸了。备份仓库数小时内冲到 50K star,登上 GitHub 热榜第一。公众号、抖音、X 全在讨论。有人扒出了 8 大隐藏功能、26 个未公开指令、6 级安全架构,还有一个愚人节电子宠物彩蛋。
激动冷静下来,我觉得真正有意思的,不是彩蛋,是这个——
Agent 的核心引擎,其实只有三行伪代码:
while true:response = LLM(context) // 调模型if no tool_calls: break // 纯文本回复 → 结束result = execute(tool_calls) // 执行工具,结果塞回 context
没有什么神秘框架。五个工具,三种语言,核心循环大体都可以抽象为这个结构。
但围绕 context 的管理代码,比引擎本身多出不止一个量级。token 怎么估算、什么时候压缩、怎么让缓存命中、工具输出怎么管控……这些决策,才是每一分钱花在哪里的真正关键。
我们翻了五个开源 AI Agent 的源码,想看看它们各自是怎么管理 Context 的。结果发现——
它们围绕同一类约束,给出了高度相似的应对。
同样在主要路径上用"字符数 ÷ 4"估算 token——TypeScript 这样写,Rust 这样写,Python 也这样写。同样的固定底座在每次请求里隐形计费。同样的上下文压缩权衡——压缩本身也要花钱。同样的 Prompt Cache 分段设计——前缀不变才能命中缓存。
这不是某家公司的技术选择,是整个行业围绕"Context 总会用完"这个约束做出的相同应答。你每天在用的各类AI编程助手同样在这套机制下精心设计——了解这些约束,能帮你把它们用得更好。
本文的目的:用源码把这些约束讲清楚,让你在日常使用中做出更聪明的选择——用更少的 token,把事情做得更好。前文《万字干货|AI Token 消耗深度认知:原理 + 实验 + 最佳实践,一篇全搞定!》建立了理论框架,本文用五个开源仓库的真实代码逐条验证,并补充了几个理论层面难以发现的实现细节——两篇互为补充,先读哪篇都完整。
五个参照工具(均开源、跨语言,结论相互印证):
Claude Code(社区重写版,仓库名 claw-code,Rust + Python):Claude Code 架构的社区逆向重写,你可以理解为被开源的 Claude Code,2 小时破 50K star。本文中"Claude Code(社区重写版)"特指此仓库;"Claude Code"单独出现时指 Anthropic 官方产品(TypeScript)。 OpenClaw(就是你的"龙虾",近期火了好久的消息平台 AI 代理):接入 WeChat / WhatsApp / Telegram(TypeScript) OpenCode:同类开源编码 Agent,面向终端编码场景(TypeScript / Bun) Codex CLI:OpenAI 官方开源编码 Agent,Apache 2.0(Rust + TypeScript) Deep Agents:LangChain 官方 Python Agent 框架(Python / LangGraph)
不需要会看代码。源码片段只是用来说明"这是真的,不是猜的"。
┌─────────────────────────────────────────────────────────────────────┐│ 工具关系定位图 │├──────────────────────┬──────────────────────────────────────────────┤│ 主力参照 │ Claude Code(社区重写版,Rust) Claude Code 架构社区重写 ││ (源码开放, │ ↑ 与官方 Claude Code 最接近的公开实现 ││ 重点解析) │ OpenClaw 即"龙虾",消息平台 Agent,TS ││ │ ↑ 多 Provider / 多用户并发场景 │├──────────────────────┼──────────────────────────────────────────────┤│ 辅助参照 │ OpenCode CLI 编码 Agent,TypeScript ││ │ ↑ 面向终端编码场景的开源参照 │├──────────────────────┼──────────────────────────────────────────────┤│ 次要验证 │ Codex CLI OpenAI 官方,Rust + TS ││ (第三方独立印证) │ Deep Agents LangChain 官方,Python │└──────────────────────┴──────────────────────────────────────────────┘
两个主力工具的核心差异:
SYSTEM_PROMPT_DYNAMIC_BOUNDARY | ||
这张表只涵盖了 token 维度的差异——两类 Agent 在工具集、权限模型、编码专属优化等方面还有更深层的设计分歧,后文“两类 Agent 的本质差异”一节会展开。
七个藏在源码里的 Token 机制
围绕 Context 管理,这五个工具共同暴露了七个反直觉的 token 机制——从估算偏差到压缩成本,从缓存陷阱到工具洪水。每一条都有对应源码,每一条都有可操作的用户含义。
下图是 Agent 完整运行流程,标注了七个机制各自的触发节点——可作为阅读后文的索引图。最后,我们还会从编码 Agent 和消息 Agent 的设计哲学差异出发,串联这些机制在不同场景下的实际影响:
Token 估算:五个工具用了同一个魔法数字——它有多不准?
你可能遇到过这样的情况: 工具显示"上下文还有空间",但账单已经比预期高很多;或者压缩触发得比你预想的晚,对话已经很重了才开始处理。
背后的原因是:工具在判断上下文是否"满"的时候,用的是估算,不是精确计数。
而估算的方式,五个工具高度一致——
Claude Code(社区重写版,rust/crates/runtime/src/compact.rs,第 326–338 行,Rust):
fn estimate_message_tokens(message: &ConversationMessage) -> usize {message.blocks.iter().map(|block| match block {ContentBlock::Text { text } => text.len() / 4 + 1,ContentBlock::ToolUse { name, input, .. } => (name.len() + input.len()) / 4 + 1,ContentBlock::ToolResult { tool_name, output, .. }=> (tool_name.len() + output.len()) / 4 + 1,}).sum()}
做得最细——逐 block 拆分,每种内容块(文本 / 工具调用 / 工具结果)单独估算,每个 block 还加了 +1 作为基础开销补偿。这是五个工具里精度最高的实现。
OpenClaw(src/agents/compaction.ts,第 14 行;ui/src/ui/views/usage-metrics.ts,第 9–12 行,TypeScript):
// compaction.tsexport const SAFETY_MARGIN = 1.2; // 20% buffer for estimateTokens() inaccuracy// usage-metrics.tsconst CHARS_PER_TOKEN = 4;function charsToTokens(chars: number): number {return Math.round(chars / CHARS_PER_TOKEN);}
同样用 chars/4,但在 compaction 触发逻辑里乘以 1.2 倍安全系数,注释明确写明"补偿 estimateTokens() 的低估"——承认了估算的系统性误差,并在压缩决策层做了补偿。
OpenCode(packages/opencode/src/util/token.ts,辅助验证,TypeScript):
const CHARS_PER_TOKEN = 4export function estimate(input: string) {return Math.max(0, Math.round((input || "").length / CHARS_PER_TOKEN))}
Codex CLI(codex-rs/utils/string/src/truncate.rs,次要验证,Rust):
const APPROX_BYTES_PER_TOKEN: usize = 4;pub fn approx_token_count(text: &str) -> usize {text.len().saturating_add(APPROX_BYTES_PER_TOKEN.saturating_sub(1))/ APPROX_BYTES_PER_TOKEN}
Deep Agents(deepagents/middleware/filesystem.py,次要验证,Python):
# Using 4 chars per token as a conservative approximation (actual ratio varies by content)NUM_CHARS_PER_TOKEN = 4
五个工具,三大语言,五个团队——同一个魔法数字:4。
这不是巧合。调用 tokenizer 精确计数,每次要几毫秒;用字符数除以 4 估算,只需几微秒。轻量估算是整个行业在精度和性能之间做出的统一权衡。
这个数字也和前文理论框架的分词器描述直接对应:Claude 等模型使用的 BPE(Byte Pair Encoding)分词器,对英文文本平均每个 token 约 3.5 个字符——chars/4 是对这个比例的近似取整。工具开发者没有更好的办法:精确 tokenize 要依赖外部库且有性能损耗,所以这个估算本身就是行业对"分词器精度无法廉价获取"的现实应对,而非懒惰。
补偿方式各有侧重:Claude Code(社区重写版)做得最细,逐 block 单独估算,加基础补偿;OpenClaw 加了 1.2 倍安全系数,在压缩决策层兜底;OpenCode 裸用;Codex 向上取整;Deep Agents 注释说是"保守估算"。
这个估算值直接控制着 Compaction 的触发时机——估算偏低,就意味着压缩比你预期的保守,上下文实际比工具显示的更满。
精度误差速查表(经验估算,供参考):
| 低估幅度明显 | ||
注:上表为基于常用 tokenizer 经验整理的参考值,不同模型和分词器下实际偏差有所不同,仅供方向性参考。
对用户的意义: 大量使用中文时,chars/4 会明显低估实际 token 消耗。当工具显示"上下文还有空间"但响应已经明显变慢时,中文内容造成的估算偏差是常见原因之一。
隐形起步价:你开口之前,工具已经替你花了多少?
你可能遇到过这样的情况: 明明只发了一句"帮我看看这个函数",token 消耗远超预期。打开计费明细,输入 token 里有大量"说不清楚是什么"的消耗。
这是工具在每次请求里注入的固定底座——在你开口之前就已确定。
Claude Code(社区重写版,rust/crates/runtime/src/prompt.rs,第 37–40 行,Rust)——最接近官方 Claude Code 原始架构的公开实现:
pub const SYSTEM_PROMPT_DYNAMIC_BOUNDARY: &str = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";const MAX_INSTRUCTION_FILE_CHARS: usize = 4_000; // 单个 CLAUDE.md ≈ 1,000 tokenconst MAX_TOTAL_INSTRUCTION_CHARS: usize = 12_000; // 所有指令文件合计 ≈ 3,000 token
指令文件(CLAUDE.md 等)按顺序消耗预算,超出 12,000 字符后的内容会被截断并标注 [truncated](第 374 行)。底座极为克制——把上下文空间最大程度留给对话。
OpenClaw 的底座更重,额外多了 Bootstrap 文件(src/agents/pi-embedded-helpers/bootstrap.ts,第 85–87 行,TypeScript):
export const DEFAULT_BOOTSTRAP_MAX_CHARS = 20_000; // 单文件 ≈ 5,000 tokenexport const DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 150_000; // 总计 ≈ 37,500 tokenexport const DEFAULT_BOOTSTRAP_PROMPT_TRUNCATION_WARNING_MODE = "once";
Bootstrap 文件就是项目级 AI 配置文件(AGENTS.md、CLAUDE.md 等),总预算上限 37,500 token。注意截断策略:先到先得,文件按顺序消耗预算,靠后的文件可能被完全跳过——超出时会发一次警告("once" 模式),不是每次都提示。
OpenCode(packages/opencode/src/session/prompt.ts,辅助验证,TypeScript):
// 每个工具的 description + JSON Schema 被序列化注入每次请求for (const item of await ToolRegistry.tools(...)) {const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))tools[item.id] = tool({ description: item.description, inputSchema: jsonSchema(schema) })}// MCP 工具与内置工具完全对等注入,全量注入,不管本轮用不用for (const [key, item] of Object.entries(await MCP.tools())) {tools[key] = item}
关键在最后一行:MCP 工具是全量注入的,不管本轮对话用不用。接入的 MCP server 越多,每次请求的底座就越高。
此外,OpenCode 还支持把远程 URL 写入 instructions 配置作为指令源(packages/opencode/src/session/instruction.ts,第 127–141 行):
// 每次对话启动时实时 fetch 远端指令,5 秒超时fetch(url, { signal: AbortSignal.timeout(5000) }).then((res) => res.ok ? res.text() : "").then((x) => x ? "Instructions from: " + url + "\n" + x : "")
这意味着 instructions 里配置一个 HTTPS URL,每次对话都会实时拉取并拼进底座——远端文件越大,底座越重,而且是随时可能悄悄变重的底座,不像本地文件能直接看到大小。
起步成本对比:
Claude Code(社区重写版)起步成本 OpenClaw 起步成本───────────────────────────── ─────────────────────────────────────System Prompt ~2,000 tok System Prompt ~2,000 tok指令文件 最多 ~3,000 tok Tool Defs ~2,500 tokBootstrap Files 最多 ~37,500 tok───────────────────────────── ─────────────────────────────────────小计 最多 ~5,000 tok 小计 最多 ~42,000 tok
对用户的意义: 底座的钱在你开口之前就花出去了——所以简单的问题也不便宜。AGENTS.md / CLAUDE.md 写得越长,底座越重;用不到的 MCP server 断开连接,起步成本立刻降低。
附:指令文件的加载时机——底座不是一次性的
上面说的"底座"还只是起步成本。五个工具在指令文件的加载时机上有一个关键分歧,直接决定了上下文是否会在对话过程中悄悄增长:
Claude Code(社区重写版,rust/crates/runtime/src/prompt.rs,第 192–213 行,Rust)——启动时全量加载:
fn discover_instruction_files(cwd: &Path) -> std::io::Result<Vec<ContextFile>> {let mut cursor = Some(cwd);whileletSome(dir) = cursor {// 向上逐级扫描 CLAUDE.md / CLAUDE.local.md / .claude/instructions.mddirectories.push(dir.to_path_buf());cursor = dir.parent();}Ok(dedupe_instruction_files(files)) // 去重后全部加入底座}
启动时一次性扫描完毕,之后不再变化——底座大小在第一句话前就确定了。
OpenCode(packages/opencode/src/session/instruction.ts,第 144–191 行;packages/opencode/src/tool/read.ts,第 118 行,TypeScript)——Read 工具触发,按需增量注入:
// instruction.ts 第144-159行:先查哪些已经注入过了export function loaded(messages) {const paths = new Set<string>()for (const part of msg.parts) {if (part.type === "tool" && part.tool === "read" && !part.state.time.compacted)if (part.state.metadata?.loaded) for (const p of loaded) paths.add(p)}return paths // ← 已注入过的路径集合}// instruction.ts 第168-191行:每次 read 触发,向上逐级找,首次才注入export async function resolve(messages, filepath, messageID) {const already = loaded(messages)while (current.startsWith(root) && current !== root) {const found = await find(current) // 找 AGENTS.md / CLAUDE.mdif (found && !already.has(found)) // ← 没见过的才注入results.push({ filepath: found, ... })current = path.dirname(current)}}// read.ts 第118行:每次 Read 工具调用时触发const instructions = await InstructionPrompt.resolve(ctx.messages, filepath, ctx.messageID)
Agent 每读一个新目录下的文件,就可能触发该目录及所有上层目录的 AGENTS.md 注入——且彩蛋是:OpenCode 还会尝试加载 ~/.claude/CLAUDE.md(instruction.ts 第 26–28 行),如果你同时装了 Claude Code,其全局配置会被一并读入。
OpenClaw(src/auto-reply/reply/post-compaction-context.ts,第 58–145 行,TypeScript)——Compaction 后重新注入关键 section:
// post-compaction-context.ts 第58-87行// Read critical sections from workspace AGENTS.md for post-compaction injection.const agentsPath = path.join(workspaceDir, "AGENTS.md")// 提取配置的关键 Section(默认:Session Startup + Red Lines)// 每次 Compaction 之后重新注入,确保 Agent 不会因为历史被压缩而忘掉关键规则
这是一个有趣的设计:Compaction 删掉了历史,但 AGENTS.md 里标记为"每次启动必看"的 section 会在 Compaction 后自动重新注入。既防止遗忘,又控制了重注入的内容范围(只注入指定 section,不是全文)。
Codex CLI(codex-rs/instructions/src/fragment.rs,第 4 行;codex-rs/core/src/codex.rs,第 532 行,Rust)——启动时加载 AGENTS.md,Skill 按用户 @mention 触发:
// fragment.rs 第4行:AGENTS.md 注入的标记前缀pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for ";// codex.rs 第532行:启动时加载用户指令let user_instructions = get_user_instructions(&config).await;
AGENTS.md 在启动时作为 UserInstructions 注入;Skill(@skill-name)则在用户消息里显式 mention 后才触发注入(codex-rs/core-skills/src/injection.rs,第 24–57 行)。
Deep Agents(libs/deepagents/deepagents/middleware/skills.py,第 560–579 行,Python)——真正的渐进式披露(metadata first):
# skills.py 第560-579行:SKILLS_SYSTEM_PROMPT 模板# Skills follow a **progressive disclosure** pattern:# - 底座里只注入 skill 的名称 + 描述(metadata)# - 需要使用某个 skill 时,再 read 其 SKILL.md 获取完整指令# 1. Recognize when a skill applies# 2. Read the skill's full instructions (path shown in skill list)# 3. Follow the skill's instructions
五个工具里唯一在系统层面实现"按需加载全文"的——底座只放 skill 的名称和描述,等 Agent 判断需要用某个 skill 时再读完整的 SKILL.md,避免一次性把所有 skill 的全文都塞进上下文。
五种加载策略对比:
Claude Code(社区重写版): 启动时全量扫描,底座固定,对话中不变OpenCode: Read 工具触发增量注入,底座随对话"生长"(去重保护防重复)OpenClaw: 启动加载 + Compaction 后重注入关键 section,动态但有范围限制Codex CLI: 启动加载 AGENTS.md;Skill 需用户显式 @mention 触发Deep Agents:底座只放 skill metadata,完整内容按需 read(最节省上下文)
对用户的多一层意义: OpenCode 的底座会随对话"生长"——Agent 读的目录越多,注入的 AGENTS.md 就越多。如果你的项目目录层级很深、每层都有 AGENTS.md,这个增量注入带来的隐性消耗可能超过你的预期。
服务商陷阱:工具显示的用量,为什么不能直接信?
同一个字段名 inputTokens,在 Anthropic 里不包含缓存 token,在 OpenRouter 里包含——这不是文档措辞的差异,是两个完全不同的数字。
这不是工具 bug,也不是你算错了。根本原因是:不同 Provider 对同一个字段的定义不一样,工具侧只能做归一化处理,最终扣费逻辑由 Provider 决定。
OpenClaw 在 normalizeUsage() 里构建了一个多 Provider 兼容层(src/agents/usage.ts,第 88–136 行,TypeScript)——光是"缓存读取"这一个字段,就有五种命名方式:
export function normalizeUsage(raw?: UsageLike | null): NormalizedUsage | undefined {// 某些 provider(OpenAI 格式)会把 cached_tokens 从 prompt_tokens 里预先扣除// 当 cached_tokens > prompt_tokens 时结果为负数——Clamp 到 0const rawInput = asFiniteNumber(raw.input ?? raw.inputTokens ?? raw.input_tokens ?? raw.promptTokens ?? raw.prompt_tokens,);const input = rawInput !== undefined && rawInput < 0 ? 0 : rawInput;const cacheRead = asFiniteNumber(raw.cacheRead ??raw.cache_read ??raw.cache_read_input_tokens ?? // Anthropic 官方raw.cached_tokens ?? // Moonshot / Kimiraw.prompt_tokens_details?.cached_tokens, // Kimi K2 嵌套字段);}
负数保护和五路字段兼容——某些 Provider 在返回 prompt_tokens 时已经扣除了缓存 token,如果工具再减一次,就会变成负数。这不是极端情况,而是真实发生在生产环境的问题。
OpenCode 源码里有一段注释直接承认了这个跨 Provider 问题(packages/opencode/src/session/index.ts,第 816–823 行,辅助验证,TypeScript):
// OpenRouter provides inputTokens as the total count of input tokens (including cached).// Anthropic does it differently though - inputTokens doesn't include cached tokens.// It looks like OpenCode's cost calculation assumes all providers return inputTokens// the same way Anthropic does... so it's causing incorrect cost calculation for// OpenRouter and others.const excludesCachedTokens = !!(input.metadata?.["anthropic"] || input.metadata?.["bedrock"])const adjustedInputTokens = safe(excludesCachedTokens ? inputTokens : inputTokens - cacheReadInputTokens - cacheWriteInputTokens,)
OpenRouter 的 inputTokens 包含缓存 token,Anthropic 的 inputTokens 不包含——同一个字段名,两个完全不同的语义。OpenCode 的注释自己都说"这会造成 OpenRouter 的成本计算不正确"——这不是 bug,是行业没有统一规范造成的。
对用户的意义: 对账时,以 Provider 后台的账单为准,工具内显示的用量只做参考。切换 Provider 或走中转路由时,两个数字之间的差异往往来自字段语义不同,不是计算错误。
自动压缩:你以为省了钱,但账单是这么算的
你可能遇到过这样的情况: 对话进行到后期,每一步响应都比前面慢、而且更贵;触发了"上下文压缩"之后,以为能省钱,但账单反而更高。
这两个现象背后是两件事:雪球机制,以及"压缩本身也要花钱"。
雪球机制的根源在于工具调用的数据结构。每次 Agent 调用一个工具(读文件、写代码、执行命令),会产生一对:tool-call(调用请求)+ tool-result(执行结果)。这一对会永久留在上下文,后续每一轮对话都要重新把它们发给模型。任务越复杂,工具调用越多,每一轮的输入就越重——这是雪球效应的本质。
这不是某一家工具的设计选择——五个仓库的主循环代码,语言各不相同,但结构完全一致:每轮开始时取出完整历史,拼上新结果,整包发给 LLM。
Claude Code(社区重写版,rust/crates/runtime/src/conversation.rs,Rust):
loop {let response = client.send_message(&self.session.messages, ...).await?;self.session.messages.push(result_message); // 工具结果追加,下轮连同完整历史一起重发if response.stop_reason == StopReason::EndTurn { break; }}
OpenCode(packages/opencode/src/session/prompt.ts,第 296 行,TypeScript):
while (true) {let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID))// 每轮从数据库 stream 出全部消息(含所有历史工具调用和结果)if (!["tool-calls","unknown"].includes(lastAssistant.finish)) break}
Codex CLI(codex-rs/core/src/codex.rs,第 5877、5929 行,Rust):
loop {let sampling_request_input: Vec<ResponseItem> = sess.clone_history();// clone_history():把完整历史克隆进本次请求,工具结果追加后下轮再次 clone}
OpenClaw(src/agents/compaction.ts + pi-embedded-helpers,TypeScript):工具调用和结果作为 { type: "toolCall" } / { type: "toolResult", toolCallId } 成对追加进 messages 数组——Compaction 之前不会离开上下文。
Deep Agents(libs/deepagents/deepagents/graph.py,Python / LangGraph):基于 LangGraph 的 AgentState.messages,工具调用和结果分别作为 AIMessage(含 tool_calls)和 ToolMessageappend 进状态,每次 LLM 调用时完整传入。
五种语言、三个框架,指向同一件事:工具结果写进去就不出来了,直到 Compaction 把它删掉(而那意味着信息损失)。这正是前文 §4 雪球效应呈现"平方级增长"的代码级根因。
当上下文接近上限时,工具会触发 Compaction(压缩)。但五个工具选择了不同的策略:
Claude Code(社区重写版)的策略:删旧消息 + 本地摘要(rust/crates/runtime/src/compact.rs,第 75–113 行,Rust)
pub fn compact_session(session: &Session, config: CompactionConfig) -> CompactionResult {// 把超出 max_estimated_tokens 的旧消息移除// 对被移除的消息生成本地摘要,插入 session 开头let summary = summarize_messages(removed);...}fn summarize_messages(messages: &[ConversationMessage]) -> String {// 纯本地摘要,不调用 LLM,直接提取文件名 / 操作关键词let key_files = collect_key_files(messages);...}
关键:摘要是本地生成的,零额外 API 成本。summarize_messages() 函数会提取被压缩消息的范围统计(user / assistant / tool 条数)、出现过的工具名、最近 3 条用户请求首段文字、关键词匹配出的 pending work(todo / next / pending 等关键词触发)、以及文件路径列表(最多 8 个)——是纯 Rust 规则提取,不调用任何 LLM。结构化信息(工具列表、时间线骨架、关键文件)保留完整,但每条消息内容截断至 160 字符(summarize_block() 第 214 行),长代码输出和工具结果的具体细节会丢失。
OpenClaw 的策略:调用 LLM 生成摘要(src/agents/compaction.ts,第 133 行,TypeScript)
export const SUMMARIZATION_OVERHEAD_TOKENS = 4096;// 摘要生成本身也消耗 token:指令 + system prompt + 历史摘要 + 包装标签
不是删,而是"读完历史 → 调用 LLM 生成摘要 → 用摘要替代原始历史"。信息保留更完整,但:
每次压缩 = 一次额外 API 调用,有 4096 token 的基础开销(以 Claude Sonnet 为例,约 0.01~0.02 美元/次) 上下文越大,摘要本身越贵 OpenClaw 还有三级降级策略:正常摘要 → 跳过超大消息(标注"[Large role omitted]")→ "Summary unavailable due to size limits."
OpenCode 的策略:两阶段压缩(packages/opencode/src/session/compaction.ts,辅助验证,TypeScript)
// compaction.ts 第31行:isOverflow() 用——计算可用空间时预留给输出的安全边距const COMPACTION_BUFFER = 20_000// compaction.ts 第51-52行:prune() 用——控制工具结果删除的边界const PRUNE_MINIMUM = 20_000 // 至少削减这么多才执行const PRUNE_PROTECT = 40_000 // 保留最近 40K tokens 的工具调用不动
向后遍历,找旧 tool-result 直接删除,可删量超过 20K 才执行。
但 OpenCode 同时也支持 LLM 摘要式 Compaction——且 Compaction 有独立的专属 Agent,可以单独配置使用更便宜的模型(packages/opencode/src/session/compaction.ts 第 132–135 行;packages/opencode/src/provider/provider.ts 第 1333–1358 行):
// compaction.ts:优先用 compaction agent 自己配置的 modelconst agent = await Agent.get("compaction")const model = agent.model? await Provider.getModel(agent.model.providerID, agent.model.modelID): await Provider.getModel(userMessage.model.providerID, userMessage.model.modelID)// provider.ts:getSmallModel() 的候选优先级(精简展示,实际含7个候选)let priority = ["claude-haiku-4-5", "claude-haiku-4.5","3-5-haiku", "3.5-haiku","gemini-3-flash", "gemini-2.5-flash","gpt-5-nano",]
这意味着:只要在配置里把 compaction Agent 的 model 指向一个便宜的小模型(如 Haiku),压缩任务就会用小模型完成,主对话继续用大模型。这是一个有实际效果、但几乎没人知道的系统性降本手段。
Codex CLI 的第三条路:Remote Compact(codex-rs/core/src/compact.rs,次要验证,Rust)
// OpenAI provider 走云端压缩,其他 provider 走本地摘要pub(crate) fn should_use_remote_compact_task(provider: &ModelProviderInfo) -> bool {provider.is_openai()}
OpenAI 官方工具有一个独特选项:把历史消息发回 OpenAI 服务端做云端压缩。压缩质量最高——但也是一次额外 API 调用,成本最高。
五种策略对比:
反直觉的结论:压缩不是免费的安全网。 智能压缩策略本身有成本,而且上下文越大,压缩本身越贵。
附:Compaction 的触发阈值和开关——五个工具都可以干预
每个工具的 Compaction 不是铁板一块,都暴露了配置接口:
Claude Code(社区重写版,rust/crates/runtime/src/compact.rs,第 4–15 行,Rust):
pub struct CompactionConfig {pub preserve_recent_messages: usize, // 保留最近几条消息不压缩,默认 4pub max_estimated_tokens: usize, // token 估算超过此值才触发,默认 10_000}
两个参数直接控制触发时机——max_estimated_tokens 调大,压缩触发更晚;preserve_recent_messages 调大,保留更多近期消息不被摘要。
OpenCode(packages/opencode/src/session/compaction.ts,第 33–48 行,TypeScript):
export async function isOverflow(input: ...) {const config = await Config.get()if (config.compaction?.auto === false) return false // ← 完全关闭自动压缩const reserved =config.compaction?.reserved // ← 用户自定义预留量?? Math.min(COMPACTION_BUFFER, ProviderTransform.maxOutputTokens(input.model))return count >= (input.model.limit.input - reserved)}
compaction.auto = false 完全关闭;compaction.reserved 调大,让压缩更早触发,为模型输出预留更多空间。
OpenClaw(src/agents/pi-embedded-runner/compact.ts,第 402 行;src/config/types.agent-defaults.ts,第 316 行,TypeScript):
// compact.ts 第402行:Compaction 可以单独指定模型const compactionModelOverride = params.config?.agents?.defaults?.compaction?.model?.trim()// 格式:"provider/model-id",例如 "anthropic/claude-haiku-4-5"// types.agent-defaults.ts 第316行reserveTokensFloor?: number // 压缩预留 token 下限,0 表示禁用下限保护
OpenClaw 的 Compaction 配置项最丰富:可以单独指定压缩用的模型(用便宜的小模型做摘要)、调整预留 token 下限。这和 OpenCode 的 compaction agent 配置思路相同——把摘要任务交给小模型,主对话用大模型。
Codex CLI(codex-rs/core/src/codex.rs,第 5667、5963、6164 行;codex-rs/app-server-protocol/src/protocol/v2.rs,第 720–721 行,Rust)——有两个独立可配置的 Token 阈值:
// codex.rs 第5667行:auto_compact_token_limit 是独立于上下文窗口上限之外的压缩触发点let auto_compact_limit = model_info.auto_compact_token_limit().unwrap_or(i64::MAX);// codex.rs 第5963行:total_usage_tokens 达到该阈值即触发自动压缩let token_limit_reached = total_usage_tokens >= auto_compact_limit;// v2.rs 第720-721行:两个阈值均可通过 API 协议独立配置pub model_context_window: Option<i64>,pub model_auto_compact_token_limit: Option<i64>,
还有一个 effective_context_window_percent(codex-rs/codex-api/tests/models_integration.rs,第 93 行),默认值为 95——实际上下文上限 = context_window × 95%,预留 5% 给输出。此外有一个特殊触发场景:切换到上下文窗口更小的模型时,若当前 token 用量超过新模型的 auto_compact_token_limit,立即触发压缩——换个更便宜的小模型来处理子任务,可能瞬间触发一次额外的压缩 API 调用。
Deep Agents 未在源码里发现用户侧的 Compaction disable 或阈值配置项。
对用户的意义: 文件读写多的任务(读代码、写代码、反复修改),后期每一步都会比前面慢。大任务拆成多个短对话——雪球没机会滚大,每轮成本从底座重新开始。如果你在用 OpenCode 或 OpenClaw,把 Compaction 专用模型配置为 Haiku 级别的小模型,是一个几乎零副作用的系统性降本手段。使用 Codex CLI 时注意:在同一任务中途切换到上下文窗口较小的模型,可能触发预期外的自动压缩,产生额外 API 调用成本。
Prompt 缓存:能省 90% 的机制,为什么你没用上?
Anthropic 的缓存读取价格约为正常输入价格的 1/10——但五个工具的源码告诉我们,这个折扣不是默认生效的。它能不能命中,完全取决于工具有没有专门设计。
Claude Code(社区重写版,rust/crates/runtime/src/prompt.rs,第 37 行 + 第 143 行,Rust)——用最显式的方式声明缓存分界:
pub const SYSTEM_PROMPT_DYNAMIC_BOUNDARY: &str = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";// build() 方法(第 143 行附近):sections.push(get_simple_intro_section(..)); // 固定前缀 ← 缓存命中区sections.push(get_simple_system_section());sections.push(get_actions_section());sections.push(SYSTEM_PROMPT_DYNAMIC_BOUNDARY.to_string()); // ← 分界标记sections.push(self.environment_section()); // 动态部分(cwd, date, git status...)sections.push(render_project_context(..));sections.push(render_instruction_files(..));
__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__ 让缓存系统明确知道"这之前的内容不会变"——固定前缀可缓存,动态部分每次重新计费。
OpenClaw 走的是另一条路(多模式 Cache Retention + 诊断工具,TypeScript):在 src/agents/pi-embedded-runner/extra-params.ts 第 73 行声明了三档 Cache Retention 配置:"none" / "short"(5 分钟)/ "long"(1 小时),同时配合 docs/concepts/session-pruning.md 里的 cache-ttl 上下文修剪机制——在 Cache TTL 过期时自动修剪对话历史,避免重新缓存整个上下文带来的写入成本。还内置了 Cache Trace 诊断工具,通过 SHA-256 对比相邻两轮消息摘要,让你验证缓存是否真的命中——其他工具几乎都做不到这一点。
OpenCode(packages/opencode/src/session/llm.ts,第 88–93 行,辅助验证,TypeScript):
// rejoin to maintain 2-part structure for caching if header unchangedif (system.length > 2 && system[0] === header) {const rest = system.slice(1)system.length = 0system.push(header, rest.join("\n"))}
把 System Prompt 拆成两部分——header(固定)+ 动态内容。每次检查 header 是否变化,没变则保持结构,让第一段作为稳定前缀命中缓存。和 Claude Code(社区重写版)的 DYNAMIC_BOUNDARY 本质相同,实现方式更隐式。
但 OpenCode 的缓存设计不止于此。provider/transform.ts 的 applyCaching() 函数(第 174–176 行)还额外对最后 2 条非 System 消息打上缓存标记:
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2) // 前 2 条 systemconst final = msgs.filter((msg) => msg.role !== "system").slice(-2) // 最后 2 条对话// 两组消息都打上 cacheControl: { type: "ephemeral" }
Anthropic 最多支持 4 个缓存断点,这个实现精确利用了这一上限:System 前缀 2 个 + 对话尾部 2 个。对用户的实际意义是:对话轮次越多,每次请求里"最后 2 条之前的全部历史"越大,缓存覆盖面就越广——长对话的缓存收益比短对话高得多。
Codex CLI(次要验证,Rust):未看到独立的 Prompt Caching 结构性处理代码;conversation_id(codex-rs/app-server-protocol/src/protocol/v1.rs,第 78 行)是会话线程标识符,用于历史管理和恢复,不是直接作用于 Provider 侧缓存前缀的结构性设计。Deep Agents(次要验证,Python):configurable_model.py 保留了 Anthropic 专属 cache_control 字段(_ANTHROPIC_ONLY_SETTINGS,第 46 行),切换到非 Anthropic provider 时自动剥除,缓存由 Anthropic SDK 层处理,开发者基本零感知。
五种思路对比:
Claude Code(社区重写版): DYNAMIC_BOUNDARY → 显式标记分界,最直观OpenClaw: 多模式 + 诊断工具 → 手工配置 + 可验证命中率(唯一能验证的)OpenCode: 2-part + 尾部标记 → 前缀稳定 + 对话历史大量命中缓存Codex CLI: 会话 ID 驱动历史管理 → 缓存由 Provider 侧自主处理Deep Agents:Anthropic cache_control 字段透传 → SDK 层兜底,切其他 provider 自动剥除
共同揭示的真相:缓存命中不是"默认有",是工具专门设计的结果。前缀内容稳定,是所有策略的核心前提。
附:Cache Write 也有成本——缓存不是纯赚
Prompt Caching 的完整账单由两部分组成:写入(cache write) 和 读取(cache read)。
读取约为正常输入价格的 1/10——这是大家熟知的"省 90%" 写入约为正常输入价格的 1.25 倍——比普通输入还贵 25%
也就是说,第一轮对话(写入缓存那一次)不仅不省,还会多花。收益要从第二轮开始用 cache read 抵回来。在 OpenClaw 的 session-pruning.md 里提到的 cache-ttl 修剪机制,核心动机正是这里:如果会话长时间空闲、缓存 TTL 过期,下一次请求就需要重新写入缓存——这次写入是真实的费用支出,而不是省钱。所以 OpenClaw 在 TTL 接近过期时主动修剪对话历史,就是为了减小"重新写入"那一次的 cacheWrite token 量。
OpenClaw Heartbeat 的隐性成本与"空跳过"优化(src/auto-reply/heartbeat.ts,第 8、23 行,TypeScript):
// heartbeat.ts 第8行:默认每 30 分钟触发一次export const DEFAULT_HEARTBEAT_EVERY = "30m";// heartbeat.ts 第23-52行:检测 HEARTBEAT.md 是否"实质为空"export function isHeartbeatContentEffectivelyEmpty(content: string | undefined | null): boolean {// 文件仅含空行 / 注释行(# ...)/ 空列表项(- [ ])时视为空// 返回 true → 跳过本次 Heartbeat API 调用,零 token 消耗}
Heartbeat 每 30 分钟发一次 API 调用,目的是让 system prompt(含所有工具定义)保持在 Provider 的 Prompt Cache 里——避免缓存 TTL 过期后重新写入的高额费用。但设计了一个精妙的成本优化:如果 HEARTBEAT.md 检测为"实质为空"(只有空行或注释),则跳过 API 调用,保活成本为零。也就是说:HEARTBEAT.md 内容越精简,30 分钟的"缓存保活"越不花钱——这是 OpenClaw 里少数做到"不调用就不计费"的机制之一。此外,Heartbeat 的 HEARTBEAT_PROMPT 明确要求 Agent "Do not infer or repeat old tasks from prior chats"——防止 Agent 在保活请求里重复历史上下文,也是一层 token 节省设计。
前文 §8 重点讲了"缓存命中可省 90%",但 Cache Write 的成本(比正常输入贵 25%)在理论层面较难直接观测——OpenClaw 的 Heartbeat 机制恰好是这个成本在代码层面最清晰的体现:之所以要每 30 分钟主动 ping 一次,就是因为让缓存自然过期后重新写入是真实的额外支出,Heartbeat 本质上是在用小代价(一次保活请求)换掉大代价(完整 cacheWrite 重新计费)。
对用户的意义: 如果你的工具提供了 cache 统计,查看 cache.read 占总输入 token 的比例——如果接近 0,说明缓存基本没有命中,值得关注。频繁修改项目级配置文件(如 .cursorrules、AGENTS.md)可能会改变 System Prompt 的固定前缀,进而影响缓存命中率。短暂离开再回来继续对话时,如果空闲时间超过了缓存 TTL(Anthropic 默认约 5 分钟 / 1 小时),你会为重新写入缓存多付一次成本——这正是 OpenClaw cache-ttl 修剪机制设计的背景。
思考模式:"自适应思考"不等于省钱,真相是?
在 OpenClaw 的源码里,adaptive(自适应思考)的实现只有一行:return "medium"。它不会根据任务难度调整,每次都是 medium 级别的思考消耗。
Thinking Token(也叫 Reasoning Token)是模型"想一想再回答"产生的消耗。它和正常输出 token 分开计算,但很多工具把它合并进 output 字段显示——所以你在账单里看到 output 很高,不一定是"输出了很多内容",可能很大一部分是思考消耗。
OpenClaw 提供了七级 ThinkLevel 旋钮(src/agents/pi-embedded-runner/utils.ts,第 4–17 行,TypeScript):
// ThinkLevel:off / minimal / low / medium / high / xhigh / adaptive(7 级)export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {// "adaptive" 在 pi-agent-core 层映射为 "medium"// Pi SDK provider 会将其转译为 thinking.type: "adaptive" + output_config.effort: "medium"// 仅对支持该模式的模型(Opus 4.6 / Sonnet 4.6)生效if (level === "adaptive") {return "medium"; // adaptive 固定映射为 medium}return level ?? "off";}
注意:adaptive 不是"按需思考",而是固定映射为 medium——名字容易误解,实际是固定档位的思考消耗,不会根据任务难度动态升降。
Claude Code(社区重写版)未在源码中看到类似 OpenClaw 那样显式暴露的 Thinking 档位控制逻辑——Thinking 参数直接透传 Anthropic API,由调用层决定。不提供手动的 ThinkLevel 旋钮,因而也没有额外的 thinking budget 分配开销。
OpenCode 对思考 token 做了动态上限控制(packages/opencode/src/provider/transform.ts,辅助验证,TypeScript):
// 根据模型输出上限动态计算 thinking budgetbudgetTokens: Math.min(16_000, Math.floor(model.limit.output / 2 - 1)),// 或对更大上限的模型:budgetTokens: Math.min(31_999, model.limit.output - 1),
这个 budget 防止模型无限"想"下去——设定了思考 token 的天花板。
OpenCode 的计费细节(packages/opencode/src/session/index.ts,第 849–862 行,TypeScript)——Reasoning Token 和超 200K 分层计费:
// index.ts 第849-852行:上下文超过 200K 时切换到更高费率const costInfo =input.model.cost?.experimentalOver200K && tokens.input + tokens.cache.read > 200_000? input.model.cost.experimentalOver200K // ← 超 200K token:启用高费率: input.model.cost// index.ts 第861-862行:Reasoning Token 按输出价格计费,合并进 output 字段// charge reasoning tokens at the same rate as output tokens.add(new Decimal(tokens.reasoning).mul(costInfo?.output ?? 0).div(1_000_000))
两件事叠在一起:第一,某些 Claude 模型(如 Claude 3.7 Sonnet)超过 20 万 token 上下文后输入单价会有阶梯涨价,OpenCode 源码里通过 experimentalOver200K 字段明确处理了这个分层。第二,Reasoning Token 在代码里被按输出价格计费,且合并进 output 字段统一显示——这就是为什么账单里 output 数字看起来异常高时,很可能有相当一部分是思考消耗而不是实际输出。
这里的计费代码(.mul(costInfo?.output ?? 0))正是前文 §7 所论述的"Thinking Token 按输出价格计费"在源码层面的直接实现——Reasoning Token 和真实输出内容共用同一个单价变量,没有任何折扣。
对用户的意义: Thinking Token 合并在 output 字段里,是账单里最难追踪的一项消耗。别把 adaptive 当省钱开关——它每次都在用 medium 级别的思考消耗。长对话还要额外注意:超过 20 万 token 上下文后,每轮的输入单价可能悄悄跳涨一档。
命令输出洪水:一条命令,怎么把后续对话全拖慢了?
一条 git log,几千行输出进了上下文——从此以后,对话的每一轮都要把这几千行重新发给模型。就像往背包里塞了一本百科全书,之后每次出门都要把它一起扛着走。
这不是你的错,也不是工具的 bug。是工具调用结果进了上下文,而上下文的内容再也出不去(除非 Compaction 把它删掉,但那也意味着信息丢失)。
对于这个"工具输出洪水"问题,五个工具选择了不同的哲学:
Claude Code(社区重写版)的策略:指令文件截断 + 告知(rust/crates/runtime/src/prompt.rs,第 366–376 行,Rust)
fn truncate_instruction_content(content: &str, remaining_chars: usize) -> String {let hard_limit = MAX_INSTRUCTION_FILE_CHARS.min(remaining_chars);let trimmed = content.trim();if trimmed.chars().count() <= hard_limit {return trimmed.to_string();}let mut output = trimmed.chars().take(hard_limit).collect::<String>();output.push_str("\n\n[truncated]");output}
截断的对象是指令文件(CLAUDE.md 等),超出单文件 4,000 字符上限后追加 [truncated] 标记,让 Agent 知道内容被裁剪了。值得注意的是:在已核到的 claw-code 源码中,这个截断保护是针对 system prompt 构建阶段的指令文件,并非所有工具调用结果的通用上限——通用工具输出的大小控制由调用层按需处理。
OpenClaw 的策略:截断 + 详细提示(src/agents/pi-embedded-runner/tool-result-truncation.ts,第 19–34 行,TypeScript)
export const HARD_MAX_TOOL_RESULT_CHARS = 400_000; // 单条工具结果上限 ≈ 100K tokenconst TRUNCATION_SUFFIX ="\n\n⚠️ [Content truncated — original was too large for the model's context window. " +"The content above is a partial view. If you need more, request specific sections or use " +"offset/limit parameters to read smaller chunks.]";
不仅截断,还用详细的 ⚠️ 提示告诉 Agent"内容被截了,请用 offset/limit 读小块"——这样 Agent 不会基于不完整信息做出错误结论。同时还保留"重要尾部"的逻辑(检测错误信息 / JSON 结构 / 摘要行)。
OpenCode 的策略:截断 + 本地卸载(packages/opencode/src/tool/truncation.ts,辅助验证,TypeScript)
MAX_BYTES = 50 * 1024 // ≈ 50 KiB(51,200 bytes)MAX_LINES = 2_000 // 2000 行
超限后完整内容写入本地磁盘(~/.opencode/data/tool-output/),上下文里保留截断预览 + 文件路径,并附上提示:
const hint = hasTaskTool(agent)? `The tool call succeeded but the output was truncated. Full output saved to: ${filepath}\n` +`Use the Task tool to have explore agent process thisfilewith Grep andRead (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}\n` +`Use Grep to search the full content or Read with offset/limit to view specific sections.`
与 Deep Agents 的卸载哲学相同——信息完整保留,Agent 可按需读取;落点是本地磁盘而非虚拟文件系统。
Deep Agents 的策略:双阈值卸载 + 工具参数截断(deepagents/middleware/filesystem.py,第 577–578 行;deepagents/middleware/summarization.py,第 631–655 行,次要验证,Python)
# filesystem.py 第577-578行:两个独立的驱逐阈值tool_token_limit_before_evict: int | None = 20000, # 工具结果超过 2 万 token 驱逐human_message_token_limit_before_evict: int | None = 50000, # 用户消息超过 5 万 token 驱逐# 超出阈值时,把内容写入后端文件系统,上下文里只保留引用file_path = f"/large_tool_results/{sanitized_id}"# 模型收到的是:# "Result was too large. Saved to /large_tool_results/<call_id>. Use read_file to inspect."
Deep Agents 还有一个更精细的工具参数截断机制:不等到整体压缩,而是当对话超过阈值(默认 20 条消息时)就对早期消息里的大型工具调用参数单独截断——只保留前 20 个字符加省略标记("...(argument truncated)"),工具结果本身不动。这相当于在不丢弃信息结构的前提下,把已经不再需要的旧参数细节抹掉,属于"外科手术式"的 token 回收,比整体压缩的代价小得多。
不是丢弃,而是"搬走"。原始内容完整保留在虚拟文件系统里,上下文里只放一个路径引用。Agent 可以按需读取,不读就不占用上下文。
五种策略的本质对比:
没有绝对最优的策略。 最危险的场景是:Agent 不知道自己"没看全",就做出了结论——这也是 Claude Code(社区重写版)和 OpenClaw 都选择在截断时明确告知 Agent 的原因。
对用户的意义: 对于需要处理大量文本的任务(日志分析、代码搜索、文件读取),提前在提示词里限定范围("只看最近 50 条"、"只搜 src/ 目录"、"先列出文件结构再精读"),比让工具的截断或卸载机制来兜底,能让 Agent 在更干净的上下文里工作。
两类 Agent 的本质差异:能做 ≠ 适合做
OpenClaw 能读写文件、能跑 bash、能编辑代码——那还要编码 Agent 干什么?
这个问题很多人问过。答案藏在源码里:两类 Agent 面对的不是同一个优化目标,代码层面的差异比你想象的大。
一、底座差一个数量级:同一句话,起步价相差 8 倍
前文已经算过账:Claude Code(社区重写版)底座最多约 5,000 token,OpenClaw 底座最多约 42,000 token。
根源不是谁写得臃肿。看 system prompt 的内容结构就清楚了——
Claude Code(社区重写版)的 system prompt 只有四个 section(rust/crates/runtime/src/prompt.rs,第 134–156 行,Rust):
pub fnbuild(&self) -> Vec<String> {sections.push(get_simple_intro_section(..)); // ① 身份(编码助手)sections.push(get_simple_system_section()); // ② 系统规则sections.push(get_simple_doing_tasks_section()); // ③ 编码任务守则sections.push(get_actions_section()); // ④ 操作安全// 然后是动态边界 + 环境信息 + 指令文件}
四个 section,全部围绕"怎么写好代码"。
OpenClaw 的 system prompt 有十几个 section(src/agents/system-prompt.ts,第 423–678 行,TypeScript):
const lines = ["You are a personal assistant running inside OpenClaw.","## Tooling", // 24+ 个工具定义"## Tool Call Style", // 调用规范"## Safety", // 安全守则"## OpenClaw CLI Quick Reference", // CLI 指令"## Skills", // 技能系统"## Memory Recall", // 记忆检索"## OpenClaw Self-Update", // 自更新"## Workspace", // 工作区"## Sandbox", // 沙箱指导"## Reply Tags", // 消息回复标签"## Messaging", // 多渠道消息路由"## Voice (TTS)", // 语音"## Silent Replies", // 静默回复协议"## Heartbeats", // 心跳保活"## Runtime", // 运行时信息// ... 还有 Reactions、Reasoning Format、Project Context 等];
多出来的每一个 section 都不是冗余——Messaging 是为了多渠道消息路由,Heartbeat 是为了缓存保温,Memory Recall 是为了长期记忆,Silent Replies 是为了消息平台不发多余消息。但对一个编码任务来说,这些全是"不需要但必须付费"的底座。
二、工具集差异:10 个 vs 24+,定义本身就是 token
Claude Code(社区重写版)内置 10 个工具(rust/crates/tools/src/lib.rs,第 53–250 行):
pub fn mvp_tool_specs() -> Vec<ToolSpec> {vec![// 文件操作:bash、read_file、write_file、edit_file// 代码搜索:glob_search、grep_search// 信息获取:WebFetch、WebSearch// 任务管理:TodoWrite// 子Agent:Agent]}
全部围绕代码读写。没有 message、cron、browser、canvas、image_generate、memory_search。
OpenClaw 有 24+ 个核心工具(src/agents/system-prompt.ts,第 239–300 行):
const coreToolSummaries = {read, write, edit, apply_patch, grep, find, ls, // 文件操作(7 个)exec, process, // 命令执行(2 个)web_search, web_fetch, browser, // 网络(3 个)canvas, nodes, cron, message, gateway, // 平台能力(5 个)agents_list, sessions_list, sessions_history, // 会话管理(6 个)sessions_send, sessions_spawn, subagents,session_status, image, image_generate, // 其他(3 个)};
前文说过——工具定义是全量注入的,不管本轮用不用。每个工具定义(name + description + JSON Schema)约 100–300 token,10 个 vs 24+ 个,仅工具定义就差约 2,000–4,000 token。
三、编码 Agent 有而消息 Agent 没有的专属优化
这是最关键的差异。编码 Agent 不只是"功能少",而是在编码路径上做了消息 Agent 没做的优化:
① git 状态作为底座的一部分(零额外调用)
Claude Code(社区重写版,rust/crates/runtime/src/prompt.rs,第 73–80 行):
pub fndiscover_with_git(cwd, current_date) -> Self{context.git_status = read_git_status(&context.cwd); // git status --shortcontext.git_diff = read_git_diff(&context.cwd); // staged + unstaged diff// 启动时自动注入 system prompt,Agent 开口前就知道代码当前状态}
OpenClaw 没有这个机制。在 OpenClaw 里,Agent 要知道代码状态需要先调一次 exec(git status),再调一次 exec(git diff)——两轮额外工具调用,两轮额外 token。
② 项目类型自动检测 + 编码规则生成
Claude Code(社区重写版,rust/crates/rusty-claude-cli/src/init.rs,第 63–78 行、219–239 行):
struct RepoDetection {rust_workspace: bool, rust_root: bool,python: bool, package_json: bool, typescript: bool,nextjs: bool, react: bool, vite: bool, nest: bool,// ...}// 检测后自动生成 CLAUDE.md,包含编译/lint/测试命令// Rust 项目 → "cargo fmt, cargo clippy --workspace, cargo test --workspace"// TypeScript → "npm run build, npm run lint, npm run test"
初始化时扫描项目结构,自动生成包含语言、框架、验证命令的 CLAUDE.md。OpenClaw 没有项目感知初始化——它的初始化面向的是消息渠道配置(Telegram token、Signal 号码等)。
③ edit_file 返回结构化 patch
Claude Code(社区重写版,rust/crates/runtime/src/file_ops.rs,第 60–78 行):
pub struct EditFileOutput {pub old_string: String,pub new_string: String,pub original_file: String, // 完整原文保留pub structured_patch: Vec<StructuredPatchHunk>, // 精确 hunkpub git_diff: Option<Value>, // git diff 格式}
编辑后返回结构化的 patch(old_start/old_lines/new_start/new_lines),Agent 后续引用改动时不需要重新读整个文件。OpenClaw 的 apply_patch 是另一种设计——它面向的是"一次性多文件批量补丁"场景,适合大规模操作但不是逐步精细编辑的最优路径。
④ 三级渐进式权限:编码路径零摩擦
Claude Code(社区重写版,rust/crates/runtime/src/permissions.rs,第 4–9 行):
pub enum PermissionMode {ReadOnly, // 读文件、搜索 → 自由执行,零确认WorkspaceWrite, // 写文件、编辑 → 需要第二级权限DangerFullAccess, // bash 危险命令 → 需要最高级权限}
编码任务最常见的路径——"读代码 → 分析 → 改代码"——前两步在 ReadOnly 下无需任何确认。OpenClaw 的权限模型面向的是多用户消息安全,粒度更细(按工具类别、按用户身份),但编码路径上会多出审批摩擦。
⑤ system prompt 里的编码最佳实践
Claude Code(社区重写版,rust/crates/runtime/src/prompt.rs,第 468–481 行):
fn get_simple_doing_tasks_section() -> String {// "Read relevant code before changing it and keep changes tightly scoped"// "Do not add speculative abstractions, compatibility shims, or unrelated cleanup"// "Do not create files unless they are required to complete the task"// "Be careful not to introduce security vulnerabilities"}
这些指令直接硬编码在 system prompt 里,不占额外 token(属于固定底座),却确保每次编码都遵循最佳实践。OpenClaw 的 system prompt 把同等位置让给了消息路由规则和多渠道协议。
四、上下文管理的哲学差异
两类 Agent 对"上下文里什么最值钱"的判断完全相反:
编码 Agent 认为"当前代码最值钱"——底座尽可能小(省空间给代码内容),压缩选择零成本的本地规则摘要(compact.rs 第 113–197 行,纯 Rust 逻辑,不调 LLM),因为编码任务的历史对话价值衰减快,三步前读的文件可能已经不相关了。
消息 Agent 认为"对话记忆最值钱"——底座虽重但通过 Heartbeat 保温缓存来回收成本(heartbeat.ts,每 30 分钟保活),压缩选择 LLM 摘要(compaction.ts 第 3 行调用 generateSummary),因为消息场景的用户可能明天回来问"上周我们讨论的那个事"——历史语义必须保留。
五、选择参考:什么时候用哪个
一句话总结:OpenClaw 能编码,但每次编码都要为消息平台的全部能力买单;Claude Code 能且只能编码,但每一分 token 都花在刀刃上。 能做 ≠ 适合做——"用什么工具"和"做什么任务"匹配起来,才是真正省钱的方式。
结语
51 万行代码泄露之后,全网在扒隐藏功能、安全架构、愚人节彩蛋。
但翻完五个 Agent 的源码,最深的感受是——
Agent 的引擎只有三行伪代码。但围绕 Context 的管理代码,比引擎本身多出一个数量级。
五个工具,三大语言,五个团队,给出了同一批答案。七个机制之外,延伸出以下细节同样值得关注:
Token 估算都用 chars/4——TypeScript、Rust、Python,同一个魔法数字,不是巧合固定底座每次请求都在,对话还没开始就在计费 Provider 字段语义不统一,换渠道时工具显示的用量可能失准 工具调用结果会累积,长任务的雪球效应是真实的 Prompt Caching 能大幅省钱,但得工具设计支持、前缀稳定才能命中 Thinking Token 合并在 output 里,是账单里最难追踪的消耗,但有旋钮可以调 命令输出洪水无处不在——截断保上下文,卸载保信息,各有权衡 指令文件加载时机各不同:Claude Code(社区重写版)启动时全量扫描固定底座;OpenCode 的底座随 Read 工具调用"生长";OpenClaw 在 Compaction 后重注入关键 section;Deep Agents 只放 skill metadata、完整内容按需读——五种设计,token 消耗特性迥异 部分工具存在主对话之外的后台作业:例如 Codex CLI 存在独立的 memory 流水线,是与当前对话线程解耦的异步后台作业 200K 分层计费:超过阈值后输入单价悄悄跳涨,Reasoning Token 按输出价格计费并藏在 output 账单里 Compaction 可配置:Claude Code(社区重写版)/ OpenCode / OpenClaw 均暴露了触发阈值配置;OpenCode 和 OpenClaw 还支持单独指定 Compaction 使用的模型,把摘要任务交给便宜的小模型是系统性降本手段;Deep Agents 未发现用户侧的 Compaction 阈值或关闭配置项 编码 Agent 和消息 Agent 面对同一套约束,但优化方向相反:底座 5K vs 42K、10 个工具 vs 24+、零成本本地压缩 vs LLM 摘要——能做 ≠ 适合做,选对工具本身就是最大的省钱手段
这些不是某个工具的特有问题。这是 LLM-based Agent 的共同底层约束,理解它们,能让你在使用各类AI编程工具时做出更精准的决策。
从三行伪代码到七个机制,Claude Code(社区重写版)的 Rust 实现和 OpenClaw 的 TypeScript 实现从两种设计哲学印证了同一件事:Context 管理才是 token 消耗的真正战场——而选对工具类型,是进入这个战场之前最重要的一步。
理解这些,不是为了避开工具,而是用得更明白——知道钱花在哪里,才能在合适的时候做合适的选择。
可操作参考清单
以下是基于本文内容的参考方向,供结合实际场景判断使用。
选对工具类型(最优先)
纯编码任务(改函数、修 bug、重构文件)优先用编码 Agent——底座成本比消息 Agent 低约 8 倍,工具集专为代码路径优化,git 状态自动感知、结构化 patch、渐进式权限都是编码专属设计 需要跨天记忆、定时任务、多渠道消息、浏览器操作、图片生成时,消息 Agent 才是正确选择——这些能力编码 Agent 没有内置,勉强用反而多绕路
编码 Agent 类工具(OpenCode、Claude Code 等 AI 编程助手)
接入了多少个 MCP server?每个 server 的工具定义都会加重底座——用不到的断开,有助于降低每次请求的起步成本 如果工具提供了 cache 统计,查看 cache.read占总输入 token 的比例——接近 0 时值得关注,频繁变动的配置文件可能是原因之一大任务拆成多个独立短对话,比一个超长对话更能控制雪球效应——每次重启对话,上下文从底座重新开始 如果你在用 OpenCode 或 OpenClaw,可以把 Compaction(上下文压缩)单独配置为使用更便宜的小模型(如 Haiku 级别):OpenCode 在 agents.compaction.model里指定,OpenClaw 在agents.defaults.compaction.model里填"provider/model-id"——主对话继续用大模型,压缩任务交给小模型,几乎没有副作用
关于指令文件 / 项目配置(通用)
如果你的工具支持项目级 AI 配置文件(如 .cursorrules、AGENTS.md、项目级系统提示),总字符量越大,靠后的配置越可能被截断或忽略——这是先到先得的预算机制,不是按重要性排序的。重要规则放前面,精简总长度,比多写规则更有效使用 OpenCode 时,如果项目目录层级较深且每层都有 AGENTS.md,底座会随 Agent 读取文件的深入而增量增重——考虑将规则集中到根目录一份,避免底座随对话"悄悄生长"Thinking / Reasoning 等级配置中, adaptive(自适应)是固定映射到中等级别,不等于"按需思考"——简单任务可以手动调到low或minimal,明确不需要思考时设为off
通用(所有 Agent 工具)
让 Agent 执行 grep、git log、find等命令时,主动加 flag 限制输出规模:git log --oneline -20、find . -maxdepth 2、grep -m 50——大输出一旦进了上下文就出不去,后续每轮都要重读集中连续工作比频繁中断更省钱:Anthropic 的缓存 TTL 默认为 5 分钟(短缓存)或 1 小时(长缓存),空闲超过 TTL 后再回来,第一次请求需要重新写入缓存(写入成本比普通输入高 25%)——缓存收益要从第二轮开始才能抵回(注:这里说的是同一个对话内不要中途长时间挂起;上面"大任务拆成多个短对话"说的是跨任务之间主动拆分以控制雪球——两者针对不同维度,并不矛盾) 对话上下文接近 200K token 时,是开新会话的重要信号——部分 Claude 模型在超过 200K 后输入单价会阶梯上涨,继续扛着对话的边际成本比重新开始更高 切换 Provider(如从 Anthropic 直连换到 OpenRouter)后,工具内显示的用量数字可能因字段语义不同而有偏差——最终账单以 Provider 计费为准 工具显示的上下文"还有空间"是估算值( chars/4),中文等多字节内容的实际 token 消耗可能明显高于工具显示值——偏差方向是低估,具体幅度因内容和模型而异
以上清单建议结合具体场景判断,不必全部执行——理解背后的机制比执行每一条更重要。
附录:本文参考的五个开源仓库
| OpenCode | ||
| OpenClaw | ||
| Codex CLI | ||
| Deep Agents | ||
| Claude Code |
米哈游 ZZZ 绝区零 渲染细节新发现 网友发现《黑神话:悟空》误将钢筋扫描至游戏中,如何评价游戏场景实景扫描这样的做法?是怎么办到的?
收藏转发,GPU、CPU、内存等150+游戏开发性能分析优化干货合集! Unity3D游戏开发中100+效果的实现和源码大全 - 收藏起来肯定用得着 非广告!6年老号福利,描边、景深、泛光、投影等60+游戏后处理效果实现合集!
声明:本文来自公众号:游戏开发技术教程(GameDevLearning),转载请附上原文链接及本声明。
关注【游戏开发技术教程】
游戏开发技术、技巧、教程和资源,答疑解惑,内推面试

夜雨聆风