为 Agent 管好一张“上下文预算”:从数条数到数 token

为 Agent 管好一张"上下文预算":从数条数到数 token

写 Agent 的人迟早会遇到这么一件事:

模型开始"忘事"了------明明第一轮告诉过它的事,第五轮它就当没发生。于是你往 messages 数组里拼命塞历史,模型又突然报"上下文超长"。

我先给结论:大模型是无状态的,它"记得"的所有事情,都只是你每次脉塞进 messages 数组里的那些文字。 而这张 messages 有大小上限,还按 token 收费。所以每一轮对话,我们手里就一张"上下文预算",怎么花,决定了 Agent 会不会失忆、会不会爆窗。

这篇不扯理论,我用一个真实的 LangChain + js-tiktoken 小项目,讲清楚一个核心概念------截断(truncation),本质上是在 token 预算内做取舍

一个概念:你要截断的不是"消息",是"预算"

新手最容易的直觉是:上下文太长 → 删掉最早的几条 → 完事。

js 复制代码
// ❌ 拍脑袋型:按条数删
const trimmed = allMessages.slice(-4);

这看着很美,但它有一个致命盲区:一条消息和一条消息,token 的量差得远。 一句"好的"是 2 个 token,一篇"红烧肉做法"是 900 个 token。你敢保证删除"最早的 4 条"之后,剩下的就一定不超窗口?

所以正确的问题不是"还剩几条",而是:还剩多少 token,这些 token 是不是最新、最该被模型看到的内容。

"上下文预算"拆开就两半:

text 复制代码
预算 = 上下文窗口上限 − 模型输出要预留的空间
     └──────── 能用来塞历史的部分 ────────┘

我们管理的,是后面那一格:花得越聪明,Agent 记性越好、越不会超窗。

起点:消息数量截断(先能跑,再谈优化)

最容易上手的一版,就是简单 slice,只保最靠后的 n 条:

js 复制代码
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import { HumanMessage, AIMessage } from '@langchain/core/messages';

const history = new InMemoryChatMessageHistory();
const maxMessage = 4;

// 把 8 条消息塞进 history
for (const msg of messages) {
  msg.type === 'human'
    ? await history.addMessage(new HumanMessage(msg.content))
    : await history.addMessage(new AIMessage(msg.content));
}

const allMessages = await history.getMessages();
const trimmed = allMessages.slice(-maxMessage);
console.log(`保留 ${trimmed.length} 条`);

跑出来保留 4 条,思路清晰,也够演示。但它默认"每条消息一个 token 权重",这恰恰是最危险的地方------消息长短不一,按条数截断和按预算截断是两回事。

转折:token 数量截断(上精度)

要真正不超预算,必须先知道"这条消息到底占多少 token"。

js-tiktoken 拿到 OpenAI 的 cl100k_base 编码器,就能把任何一段文字换算成真实的 token 数:

js 复制代码
import { getEncoding } from 'js-tiktoken';
const enc = getEncoding('cl100k_base');   // 不同模型编码器不同

function countTokens(messages, encoding) {
  return messages.reduce((total, msg) => {
    const content = typeof msg === 'string' ? msg.content : JSON.stringify(msg.content);
    return total + encoding.encode(content).length;
  }, 0);
}

有了这把"秤",再交给 LangChain 的 trimMessages 去自动找那个"刚好不超过 maxTokens 的最大消息集合":

js 复制代码
import { trimMessages } from '@langchain/core/messages';

const trimmed = await trimMessages(allMessages, {
  maxTokens: 100,                                   // 预算上限
  tokenCounter: async (msgs) => countTokens(msgs, enc),  // 我用 tiktoken 精确计
  strategy: 'last',                                 // 保留最新,裁掉最旧
});

体会一下这两版的心智差异:

消息数量截断 token 数量截断
判断依据 条数 token 预算
是否知道超不超窗 不一定 精确
保留的 最近 n 条 不超过预算的最近一段
成本 几乎为零(slice) 需要编码器,略贵

麻烦一点,但这是一次从"感觉够"到"算得准"的升级。

一个反直觉的点:中文字符 ≠ token

这一节我想单独拎出来,因为真做中文 Agent 的迟早会踩。

很多人会拿"字符数"当 token 数来估。但 token 不是按字符切的:

text 复制代码
"hello"      ≈ 1 个 token
"我就是李四"   ≈ 6~7 个 token(中文往往一个字≈1~2 token)

同一句话,用字符数估和用 token 数估,结果完全不同。所以我上面坚决引入 tiktoken 而不是 content.length------估算的误差,会在长对话里累积成爆窗或白掏的钱。

总结下来,Agent 的 Memory 无外乎三种手段:截断、总结、检索。这一篇只挑了截断------它是零依赖者的第一课:在有限的上下文预算里,先学会正确地"放掉"。


相关推荐
xierui12312318 分钟前
Anthropic安全复盘:会操作电脑的 Agent 如何分阶段上岗
人工智能·网络安全·架构·系统架构
飞鸟真人18 分钟前
亿级用户IM系统 之 接入网关负载均衡架构(一、网络结构与DPVS部署)
架构·负载均衡·亿级im系统
烬羽22 分钟前
把 Agent 的记忆写进文件:内存 vs 文件,两把钥匙搞定多会话
架构·langchain·agent
judezh24 分钟前
一次 agent 请求要经过 12 个服务,它们分别在替你做什么
微服务·架构
QUOR25 分钟前
Zorv AI 内置浏览器技术架构与开发指南(新版)
架构·github
SL_staff26 分钟前
风控规则不该写代码:一个开发者视角的规则引擎实践拆解
java·架构·全栈
OpsEye33 分钟前
公有+私有化混合大模型架构,流量治理底层原理科普
网络·架构
武子康33 分钟前
RoboLab 解读:机器人策略评测为什么不能只看二元成功率
人工智能·llm·agent