Skip to main content

模型窗口有128K,预算该设多大

上一篇讲压缩时,用了几个现成的数:会话记录超过80,000字符触发压缩,最近至少保留20,000字符原文,摘要目标6,000字符。篇末留下的问题是:这些长度该怎样设置,又怎样给人设、工具描述和本轮新增的内容留出空间。

这几个数在配置文件里只对应一项。打开application.yaml,agent.memory下是这样的:

agent:
memory:
context-window-chars: 100000
summary-enabled: true
long-term-enabled: true
evictable-tools: [ search_knowledge ]

裁剪线、压缩线、最近保留多少原文、摘要写多长,全部由context-window-chars乘上固定比例算出来。看到这一行,读者通常会有两个疑问:模型的上下文窗口是按token算的,这里为什么按字符;假设比特严选要换接一个标称128K token窗口的模型,这个数该填多少。

这两个疑问要一起回答,因为答案都来自同一件事:Ragent面向的是什么场景。

一个用户一次咨询会问多少轮​

Claude Code、Codex这类编码助手,一次任务要读整个仓库、跑测试、看几千行日志,需要的上下文越大越好,常常要把模型窗口用到接近上限。用到这个程度,就得按token精确计算还剩多少空间。

比特严选不是这样。一个用户来问AirPods左耳没声、问退货、比两款耳机,一次咨询几十轮就到头了。看本机数据库里没有压缩、也没有裁剪过的59个会话、140轮问答:每轮往会话记录里新增的内容,中位数2,063字符。再看存档表t_agent_context_compaction:2026-10-03到10-06的16个比特严选会话,第一次压缩都落在第22~35轮。也就是说,一个常规用户整场咨询下来,多半碰不到压缩。

对这样的场景,预算要满足的是三件事:常规用户问几十轮不被压缩打断;每次调用带的内容有一个可以估算的量级,费用心里有数;离所用模型的窗口留足余量。做到这三件事,取一个整数就够了,不需要把窗口吃满,也不需要精确到token。

下面先说为什么按字符算就够,再说这个整数怎么定、怎么对窗口核算。

为什么按字符算,不按token​

1. 准确的token数只有调用之后才有​

裁剪和压缩判断的都是会话记录的长度。会话记录是框架为下一轮继续处理保存的消息列表,也就是AgentState里的context:用户提问、助手回答、思考内容、工具调用和工具结果都在里面。它和给页面回放用的聊天记录t_agent_message是两份数据。

一段文字算多少token,由供应商的分词器决定,每家不一样。程序在发请求之前拿不到准确值;准确的数只有一个来源,就是每次调用之后供应商回填的usage,框架把其中的输入token数inputTokens存在这次返回的助手消息上。要按token判断长度,只能拿上一次调用回填的数当这一次的估计,而这一轮新加进来的工具结果还不在里面。

字符数不一样:程序随时能数,数出来的值不随模型变。Ragent选了字符,换模型时计数方法不用动,只需要重新核一次预算。字符口径在AgentContextChars,下面是它对一条消息里每个内容块的计数方式,注释是为本文加的:

private static int of(ContentBlock block) {
if (block instanceof TextBlock text) {
// 用户提问、助手回答:正文字符数
return length(text.getText());
}
if (block instanceof ThinkingBlock thinking) {
// 思考内容也算,它会随请求回传给模型
return length(thinking.getThinking());
}
if (block instanceof ToolUseBlock toolUse) {
// 工具调用:工具名加入参
return length(toolUse.getName())
+ (toolUse.getInput() == null ? 0 : toolUse.getInput().toString().length());
}
if (block instanceof ToolResultBlock result) {
// 工具结果:正文里每段文本相加
return ofOutput(result);
}
// 图片这类非文本块按零计
return 0;
}

类注释写的是“字符数作为token的粗代理”。代理可以粗,前提是实际用量离窗口很远,误差不会撞到上限。下一节用真实会话的数据看这个距离有多远。

2. 按字符算够用的前提:离窗口很远​

要回答“离窗口有多远”,得看会话记录最长的那一刻,也就是压缩前最后一次调用,整次请求在供应商那里算了多少token。这个数不用换算,供应商每次调用完都会告诉你。

2.1 token数记在哪​

每次模型调用返回后,供应商会随响应回填一段usage,框架把它存在这次返回的助手消息上,和消息一起落进t_agent_state。下面是一条真实记录,取自2026-10-06一个比特严选会话里压缩前的最后一次调用,只保留了角色、时间和usage:

{
"role": "ASSISTANT",
"timestamp": "2026-10-06 10:54:13.041",
"usage": {
"inputTokens": 50872,
"cachedTokens": 49920,
"outputTokens": 1638,
"totalTokens": 52510,
"time": 8.082
}
}

inputTokens就是这次请求发给模型的全部输入折成的token数:人设、工具声明、长期记忆块、会话记录,一个不少。它是供应商算的,和项目按字符数出来的长度是两个口径。cachedTokens是其中命中前缀缓存的部分,这一篇用不到,放到讲缓存那篇再说。

2.2把字符数和token数对到同一个时刻​

每次压缩都会在存档表t_agent_context_compaction留一条记录,记着压缩前的会话记录有多少字符。上面那条助手消息的时间是10:54:13,同一个会话的存档表里,10:54:16有一条第7代压缩记录,压缩前82,371字符。两条记录相隔三秒,说明这次调用就是会话记录涨到82,371字符时发出的那一次。于是这一刻的两个数都有了:会话记录82,371字符,整次请求50,872 token。

照这个办法把存档表的102条记录都对一遍,能对上16条。这16条正好来自开头那16个会话,每个会话一条,都是这个会话的最后一次压缩。

时刻会话记录(字符,存档表)整次请求的输入(token,usage回填)
压缩前最后一次调用80,357~88,68537,247~50,872,中位43,536
压缩后第一次调用20,817~28,68712,519~20,153

2.3到压缩线也只用了窗口的三到四成​

压缩前最后一次调用,整次请求最多50,872 token,不到128K的四成,而且这个数已经把人设、工具声明和长期记忆块都算进去了。把范围放大到本机库里2026-09-29到10-10的全部543次调用,输入最大的一次就是这个50,872,一半的调用在8,090以内。

字符口径的误差,在这个距离上碰不到窗口。如果场景换成要吃满窗口的,这个前提就不成立了,那时才需要按token判断。

3. 少带内容本身就是目的​

有的模型标称128K、256K甚至更大的窗口,预算能不能跟着放大?能,但没有必要,有两个原因。

一是费用。输入token按量计费,会话记录越长,每次调用带上的历史越多。命中前缀缓存的部分按折扣价算,上面那条记录里大部分输入就命中了缓存,但折扣不等于免费。预算定在100,000字符,每次调用的输入就有了一个可以估算的量级,几十轮的会话花多少钱心里有数。

二是回答质量。输入越长,模型越容易忽略其中的部分内容,窗口大不等于每一段都被同样认真地读过。比特严选的会话里,早期查过的产品介绍、试过的排查步骤,压成一份摘要之后对回答当前问题的帮助不一定比原文差,而每轮带的内容少了很多。

所以预算是一个主动取小的数,而不是窗口允许多大就填多大。

这个数怎么定,又怎么对窗口核算​

1. 预算的定义:会话记录最多长到多少字符​

先把这个数量的范围说清楚。一次请求的输入由四部分组成:人设、工具声明、长期记忆块、会话记录。context-window-chars量的是最后一项,也就是会话这一侧随对话增长的那部分。字段注释把范围写得很明确:

/**
* 会话上下文工程预算(字符),换模型只需要动这一个数
* 不是模型标称窗口:人设、工具 schema、输出预留不走这份账,填值时先扣掉固定开销
* 长期记忆注入块(上限见 resolveMemoryMaxChars)同样不走这份账,填值时一并扣掉
* 下限 8000:再小保留段装不下摘要(0.2 × 8000 > 摘要下限 1500)
*/
@Min(8000)
private int contextWindowChars = 1_200_000;

它在项目里的定义是会话上下文工程预算:会话记录最多允许长到多少字符。

只量会话记录,是由用途决定的。这个数只用来给裁剪和压缩划线,而这两层能缩的只有会话记录:人设是配置好的,工具声明由当前可用的工具决定,长期记忆块有自己的上限。线划在能缩的那一片上,比例才有意义;要是把固定部分也算进基数,换一份人设、中途加载一本技能手册,两条线就跟着挪,可被裁的内容却没变。整次请求加起来离窗口还有多远,不靠这个数判断,靠供应商回填的usage,前面“离窗口很远”那节量过。

会话记录超过这个数,程序不会报错。代码里没有哪一处按它拒绝请求,它只决定什么时候裁剪、什么时候压缩。

解锁付费内容,👉戳​