Agent 多轮对话上下文超限详解:越聊越笨的根因、DeepSeek 三层递进策略与踩坑实战
摘要:Agent 聊到后面答非所问,不是模型变笨,是上下文窗口被工具结果塞爆了。本文从 token 估算、DeepSeek 三层递进(spill/prune/compact)、multi-step 循环超限踩坑三个维度展开,附代码示例与排查思路。
多轮 Agent 上线后,经常出现一个诡异现象:前几轮对话好好的,聊到某个节点突然答非所问,甚至直接报 400 错误。
日志看多了,规律出来了。
不是模型变笨,是它的“短期记忆”被塞爆了。
上下文窗口是什么:Agent 的工作台模型
模型的上下文窗口有硬上限。你可以把它理解成 Agent 的工作台——用户说的话、模型自己的回复、工具返回的结果,全摆在台面上。
台面就那么大。
东西堆满了,就得扔。坑的是,它不会告诉你扔了什么,也不会告诉你它已经扔了。

多轮对话里,每一轮的工具结果都会累积。
第一轮读了三个文件,文件全文留在台面上;第二轮查接口,返回的 JSON 又堆上去;第十轮,台面可能已经堆了几十万字。
这时候模型找不到你最新那句话了。表现就是“变笨”。
Agent 上下文占用估算:token 怎么算才准
我自己的代码里有一个专门估算 token 占用的工具函数。规则如下:
估算规则:
中文字符 ÷ 1.5 ≈ token 数
英文字符 ÷ 4 ≈ token 数
JSON 特殊字符({},[],")每个算 1 个 token
最终 × 3 倍安全系数(防止低估)
为什么乘以 3?
因为真实的 tokenizer 比字符估算产生更多 token。尤其是 JSON 格式的工具返回,花括号、引号、冒号每个都单独算 token。一个看起来不大的 JSON 响应,实际 token 数可能是字符估算的三到四倍。
这个 3 倍系数,是线上事故换来的。
DeepSeek 上下文管理三层递进策略详解
DeepSeek 在 Harness 里用三层递进解决上下文超限问题。每一层我都趟过,讲真,这个顺序设计得很讲究。
| 层级 | 名称 | 做什么 | 调模型? | 原文还在? |
|---|---|---|---|---|
| 第一层 | 外溢(spill) | 大结果存成文件,模型只看摘要和路径 | 不调 | 在,能取回 |
| 第二层 | 裁剪(prune) | 机械掐头去尾,中段永久消失 | 不调 | 没了 |
| 第三层 | 压缩(compact) | 调模型写摘要,替换原文 | 调,花钱 | 对模型消失 |
顺序是从便宜到贵。
裁剪完会重新量一次压力,降下去了就不调模型。压缩最贵,信息损失也最大。
第一层:外溢(spill)—— base64 剥离实战
外溢的思路:大结果存文件,模型只看摘要和路径。
我们的 Agent 有图片生成功能。一张图的 base64 编码,一兆多。
如果把它塞进历史记录,下一轮请求直接爆。
我的做法是在持久化历史之前,把所有 base64 数据剥掉,替换成一行占位符:
匹配规则:data:[任意类型];base64,[200个字符以上的编码串]
替换为:[image:base64-stripped]
简单粗暴,但有效。模型知道“这里曾经有张图”,但不用背着一兆数据到处跑。
实现就是一段正则替换,放在持久化之前执行。
一轮对话省下来的 token,够多聊好几轮。
第二层:裁剪(prune)与第三层:压缩(compact)取舍
裁剪是机械掐头去尾,中段永久消失。
不调模型,便宜。
压缩是调模型写摘要替换原文,贵,但对模型来说原文消失了。
我自己的压缩策略:当历史消息估算超过 2 万 token,调一个便宜模型(gemini-flash),把老对话总结成 200-400 字的摘要,替换原始历史。
摘要里保留关键事实、实体 ID、决策结论。
这样后续对话还能接上,不会断片。
DeepSeek 压缩策略的一个关键设计:原文不删
第三层压缩有一个设计,是我最开始没料到的。
压缩完之后,原文不删。
你在别的 Agent 里敲 /compact,发生的是一大段对话被换成摘要,原文没了。
DeepSeek 不是。
被压掉的内容一条不少地留在日志原位,只是从“模型能看到的那一面”上被遮住了。摘要是另外新写的一条,上面标着“我遮住了第几条到第几条”。
这意味着什么?
出了问题你能倒带。
你能看到“模型当时看到的版本”和“实际发生了什么”的差异。

这个设计在排查问题的时候,简直是救命。
Multi-step 循环上下文超限踩坑:只在开头检查一次
我自己的实现没这么优雅。
我的做法是硬顶——设一个 15 万 token 的上限:
MAX_INPUT_TOKENS = 150_000
超了就砍。
但有个坑我踩了很久。
这个检查只在对话开始前做一次。
而多步工具调用是循环的。每一步都可能追加新的工具结果。
流程大概是:
用户发消息
→ trimMessages() 检查一次 ✓ 没超
→ 第一步:模型调工具,返回 5000 token 结果,追加到消息列表
→ 第二步:模型又调工具,又返回 8000 token,又追加
→ 第三步:模型再调工具,再返回 12000 token,再追加
→ 第四步:💥 超出上下文限制,模型返回 400 错误
用户看到的就是 Agent 突然死了。
修复方案:每一步开始前重新 trim
后来的修法是在 prepareStep 回调里,每一步开始之前都重新 trim 一次:
prepareStep({ stepNumber, messages }) {
const trimmed = trimMessages(messages, systemTokens)
if (trimmed !== messages) → 用修剪后的版本继续
}
注释里我写了一行:
// initial trimMessages() only runs once before streamText,
// while the multi-step loop keeps appending fresh tool results
这是线上真实事故换来的教训。
不是看文档能学到的。
Agent 上下文管理排查思路
下次你的 Agent 突然变笨,别急着骂模型。
先看台面是不是堆满了。
排查顺序:
- 估算当前历史消息的 token 占用,确认是否接近窗口上限
- 检查工具返回结果,尤其是 JSON 和 base64 类大对象是否进了历史
- 检查 trim 或 compact 逻辑是否只在对话开始时执行,multi-step 循环里有没有重复检查
- 如果用了 compact,确认原文是否保留,能不能倒带排查
本文作者来自 Adgine 团队,提供 GEO 服务(让品牌被 AI 推荐),官网 https://adgine.cn
更多推荐

所有评论(0)