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


相关推荐
遇码1 小时前
2026-11-01-daw-retry-banner-body
人工智能·ai·rust·数据分析·大模型·agent
极限挑战哥2 小时前
共享电单车联营平台分账架构实践:二清合规与自动分账零人工方案
java·大数据·人工智能·微信小程序·架构·微信公众平台
威哥爱编程2 小时前
Python 智能体开发实战 10:本地跑得好好的,一并发就崩?把它做成一个能上线的服务
python·agent
威哥爱编程2 小时前
Python 智能体开发实战 09:改了提示词,到底是变好还是变坏?搭一套能打分的离线评测
python·agent
吴凡wudianmei3 小时前
从 0 搭一个 AI Agent(三):SSE 流式输出,让 AI 边想边说
架构·llm·openai
文谦南宁AI普工3 小时前
拆解 DeepSeek dsh-superagent:用 60 行代码模拟它的授权门控与 observe 模式
aigc·agent·ai编程
小七-七牛开发者4 小时前
AI Agent 的安全风险,正在从 Prompt 扩展到运行环境
ai·agent·security·权限管理·cursor
各类产品分享14 小时前
工业级 AR 巡检系统选型:核心参数与架构实现要点
架构·ar·ar巡检
阿里云云原生5 小时前
概率、插座与 5000 个爱因斯坦:专访阿里云研发副总裁周琦
agent
AlanAshley5 小时前
实时湖仓与区块链:数据架构的信任革命
架构·区块链