Skip to main content

01 - 窗口里装了什么

前置:无。本篇是专题起点。

本篇回答:一次请求的 token 具体花在哪五个地方,它们各自怎么增长,以及在动手优化之前该先量什么。

本篇会用到的词

意思
渲染顺序一次请求拼成 token 序列时的固定顺序:toolssystemmessages。它决定了什么在前缀里
增长阶一项开销随轮次增长的方式:常数(每轮固定)、线性(每轮加一点)、或者更快
有效利用率上下文里真正对当前这一步有用的 token 占比。它是这一层唯一值得优化的目标
计数接口count_tokens,在不真正发起推理的前提下算出这次请求有多少输入 token

一、上下文工程不是提示工程

两者的分界很清楚:

分界不在"写得好不好",在"这件事发生在什么时候"提示工程对象:一条指令怎么写发生在:开发时,写一次改几次产物:一段文本改进方式:改写、加示例、调结构上下文工程对象:这一轮带哪些 token 进来发生在:运行时,每一轮都要决策产物:一段�代码逻辑改进方式:砍、挪、换顺序、换加载时机
Agent 把这两件事的分量彻底改变了:单轮应用里上下文基本是固定的,提示工程就是全部;而 Agent 每一轮都在往上下文里追加工具结果,到第 N 轮时上下文的绝大部分是运行时产生的,跟你当初写的那段提示词没什么关系。

二、五个去向

按渲染顺序排列。左边两项在前缀里,改动它们会打掉整个缓存(06 篇)① 工具定义渲染在最前面增长阶:常数但常数可能很大五个 MCP 服务约 55K② 系统提示词渲染在工具之后增长阶:常数几百到几千占比通常最小③ 对话历史用户与助手的文本增长阶:线性每轮几百被压缩针对的主要是它④ 工具结果文件内容、检索结果增长阶:线性但单次几千到几万长任务里的绝对大头⑤ 记忆与检索每轮注入的几条增长阶:常数几百占比小但位置敏感①和④是两个不同的问题:①是"还没干活就已经付掉的固定成本",④是"干活过程中累积的可变成本"。前者靠延迟加载治(05 篇),后�者靠裁剪和卸载治(03、04 篇)—— 用错手段是这一层最常见的浪费。
增长阶决定了优化的优先级。常数项在第 1 轮就全额付了,且每一轮重复付;线性项在前几轮不起眼,到第几十轮才成为主导。两者都要治,但时机不同。

三、量一遍:一次真实构成

不要凭感觉猜。count_tokens 可以在不发起推理的前提下算出这次请求的输入量:

// 逐项分别数,才知道钱花在哪。只数总量等于什么都没数。
const parts = {
// 工具定义单独数:把 tools 传进去、messages 给一条空的最小消息
tools: await client.messages.countTokens({
model: "claude-opus-5", tools, messages: [{ role: "user", content: "." }],
}),
// 系统提示词同理
system: await client.messages.countTokens({
model: "claude-opus-5", system, messages: [{ role: "user", content: "." }],
}),
// 完整请求
full: await client.messages.countTokens({
model: "claude-opus-5", tools, system, messages,
}),
};
// 对话历史 + 工具结果 ≈ full − tools − system(那条 "." 的量可以忽略)

在一个典型的编码 Agent 上跑这段代码,第 1 轮和第 40 轮的构成完全是两回事:

两条都是 100% 满宽,右侧标绝对量 —— 看的是构成比例的变化系统提示词工具定义对话历史工具结果记忆与本轮输入第 1 轮工具定义 90%13.3K第 40 轮工具结果 82%171.3K第 1 轮里工具定义 12,000 token —— 用户还没说完第一句话,成本已经付掉九成。到第 40 轮它降到 7%,但绝对量一点没变,而且每一轮都重复付了一遍。工具结果那 140,000 token 里,绝大部分是十几轮前读过、早已用完的文件内容。这张图直接指出两个下手处:把工具定义变成按需加载(05 篇),把用完的工具结果清掉(03 篇)。
百分比堆叠图容易掩盖一件事:第 40 轮里工具定义只占 7%,看起来不值得优化,但它的绝对量和第 1 轮一模一样,而且被重复计费了 40 次。看构成比例决定"这一轮谁最占地方",看绝对量决定"总共为它付了多少钱"。

四、有效利用率:这一层唯一值得优化的目标

省 token 本身不是目标 —— 把上下文砍到只剩一句话,token 最省,效果最差。真正的目标是提高有效利用率

有效利用率 = 对当前这一步真正有用的 token ÷ 这一步实际发出的 token

它没法精确测,但可以估。一个可操作的近似做法:

# 对一批真实请求,把上下文按块拆开,让一个便宜模型逐块判断
# 「这一块对回答当前这个问题有没有帮助」,统计有用块的 token 占比。
#
# 判断的粒度很重要:
# - 太粗(整段历史算一块)→ 结论永远是"有用",测不出东西
# - 太细(每条消息一块)→ 会把提供背景的铺垫消息误判成无用
# 实践中按「一次工具调用及其结果」为一块比较合适。
def effective_ratio(request, judge_model="claude-haiku-4-5"):
blocks = split_into_blocks(request) # 按工具调用/结果切
useful = [b for b in blocks if judge(b, request.current_question, judge_model)]
return sum(t(b) for b in useful) / sum(t(b) for b in blocks)

典型结果会比直觉低得多。一个跑到第 40 轮的编码 Agent,有效利用率常在 10% 到 20% —— 也就是说八成以上的输入 token 对当前这一步没有帮助,但它们依然在消耗注意力(02 篇)和费用。

这个数字的用处不在绝对值,在改造前后的对比:做了一轮裁剪之后,有效利用率有没有上去、任务成功率有没有跟着上去。只看 token 省了多少,会得出"砍得越狠越好"的错误结论。

五、预算怎么分配

一个可用的起点,按上下文窗口的百分比给:

去向建议上限超了怎么办
工具定义5%(或 10K token,取小)上延迟加载与工具搜索(05 篇
系统提示词2%通常不是问题;如果超了,多半是把本该做成工具描述的内容写进去了
记忆与检索注入5%收紧召回条数,安全类记忆单独走强制注入
对话历史20%触发压缩(03 篇
工具结果剩下的全部触发裁剪或卸载(0304 篇)

三条原则:

  1. 常数项要卡死上限。工具定义和系统提示词是每轮重复付的,它们超标的代价随轮次线性放大
  2. 给工具结果留最大的一块,因为它是任务真正在推进的证据;但也正因为量大,它必须有清理机制
  3. 不要把窗口用满。留出 20% 到 30% 的余量:模型在接近上限时的表现下滑最明显,而且突发的大工具结果需要有地方放

六、小结

  • 提示工程管的是开发时写一段文本,上下文工程管的是运行时每一轮的取舍;Agent 让后者成为主要矛盾
  • 五个去向里,工具定义是"还没干活就付掉的常数",工具结果是"干活过程中累积的变量",两者要用不同手段治
  • 动手前先用 count_tokens 逐项量一遍;第 1 轮的构成和第 40 轮完全不同,只看其中一个会做错决策
  • 优化目标是有效利用率而不是总 token;只看省了多少,会得出"砍得越狠越好"的错误结论
  • 常数项卡死上限,工具结果留最大一块但必须有清理机制,窗口留 20% 到 30% 余量

下一篇:02 - 上下文腐烂,为什么"反正窗口够大,多塞点没坏处"这个假设不成立。

← 回到 专题索引