上下文窗口快满了怎么办?Agent 的 Token 预算管理策略详解

我正在给一个内部工具做 AI 客服,测试到第 18 轮的时候,它突然问我:“您刚才是不是说要退款?”

AI technology illustration

我翻了一下记录:第 2 轮就说了。我甚至在第 8 轮加过备注“重要:用户要求退款,请记住”。可它还是忘了。不是它变笨了,是它的“工作台”满了。

这个“工作台”,就是大语言模型的上下文窗口。模型一次能处理的 token 数量是固定的——token 约等于 0.75 个英文单词,一个汉字大概占 1 到 2 个 token。128k 的窗口听起来很大,但对 Agent 来说,往往二十轮对话就撑不住了。

128k 的窗口,对话历史只占三分之一

Token 预算管理,说白了就是:在有限的窗口内,决定系统提示、工具定义、对话历史、工具返回结果各占多少份额,以及超出份额时先压缩谁。

很多人(包括早期的我)以为上下文窗口主要被“聊天记录”占着,其实不是。一个带搜索功能的 Agent,典型的 token 账单长这样:

内容 典型占用
系统提示(角色设定、行为规则) 3,000 – 8,000 token
工具定义(函数 schema) 5,000 – 15,000 token
每轮用户输入 + 模型回复 1,500 – 3,000 token
每轮中间推理(CoT) 500 – 2,000 token
单次工具返回结果 5,000 – 20,000 token

30 轮对话加 10 次工具调用下来,系统提示 8000,工具定义 12000,对话历史大概 60000,工具返回结果直接干到 100000 以上。128k 的窗口,还没等你反应过来就爆了。

把工具接上 Agent 之后我才意识到:工具返回结果才是真正的吞 token 怪兽。搜索一下返回几千字的网页摘要,查一次数据库返回几百行数据,读一个文件又是上万 token。对话历史反而好商量,这些半路杀出的“原材料”才是预算管理的主战场。

窗口满了,模型会经历三次“失忆”

第一次失忆是直接报错。API 吐出一句 maximum context length exceeded,你只能手动截断或开新会话。这反而是最不坑的,至少你知道出了问题。

第二次失忆是静默截断。不少 Agent 框架会悄悄把最早的消息扔掉,其中可能就包含你的系统指令。模型不知道自己丢了什么,也不会主动告诉你。我那个 AI 客服就是典型——第 2 轮的用户诉求,被框架像垃圾一样清理掉了。

第三次失忆最隐蔽:就算所有内容都塞进去了,模型也“看不见”中间的信息。Lost in the Middle(Liu et al., 2023)做了个实验,把关键信息放在长上下文的开头和结尾时,模型提取准确率很高;挪到中间,准确率明显掉了一截。装得下,不等于记得住。

五套应对方案,从偷懒到正统

滑动窗口:最朴素,也最伤

只保留最近 N 轮对话,更早的直接丢弃。LangChain 的 ConversationBufferWindowMemory 就是这个思路。十行代码解决问题,但丢掉的很可能是任务说明、约束条件这些最重要的内容。给模型装一个只能记住最近十分钟的大脑,这不算记忆管理,这是逃避。

滚动摘要:好用,但细节会被“总结没”

快满的时候,把旧对话交给 LLM 压成几百字摘要,腾出空间继续聊。Claude Code 里的 auto-compact 就是帮你自动做这件事。

我第一次用这个方案就踩了坑:报错信息、文件路径、具体数字,全被总结成了“大意”。404 Not Found 变成了“遇到访问错误”,模型拿着这种摘要去排查问题,当然怎么都查不对。后来我在摘要模板里写死了一条规则:所有文件名、错误码、数字、专有名词必须原样保留,不得概括。这条规则救了无数个后续会话。

结构化记忆:把重要事实单独存

MemGPT 的开创性在于拿 LLM 当操作系统来想:上下文是主内存,外部存储是磁盘,重要信息从对话里抽出来写成“记忆页”,需要的时候再加载回来。

比如从对话里提取:用户偏好简短回复、项目决定用 Rust 重写、注册模块还没做。ChatGPT 的 Memory 也是这个思路——它记的不是原文,是关于你的事实。

优点是真的丢不了,缺点是要维护提取逻辑,读写记忆本身也花 token。适合高频、长期、结构化的信息,不适合流水账。

RAG:把历史对话做成可检索档案

把对话切块存进向量数据库,相关片段按需召回。理论上记忆无限大,但对话不是好的 RAG 语料:口语多、上下文分散、逻辑链条一切就断。检索召回的是“看起来相关的碎片”,不一定是“真正需要的那块”。外部知识库适合 RAG,对话历史还是交给摘要或结构化记忆更靠谱。

上下文缓存:不省容量,但省钱省时间

OpenAI 和 Anthropic 都支持 prompt caching:同样的系统提示和工具定义反复发送时,直接命中缓存。Anthropic 的缓存输入价格是正常价格的十分之一,还能降低首 token 延迟。注意,它不减小窗口占用,只减少钱包出血。但系统提示加工具定义每轮就要耗费上万个 token,积少成多,这笔账很值。

策略 释放容量 保真度 实现成本 最大的坑
滑动窗口 极低 丢掉早期指令
滚动摘要 细节蒸发
结构化记忆 建模复杂
RAG 召回不准
上下文缓存 否(省钱) 不省容量

我在生产环境的三个原则

第一个原则:静态内容设上限。系统提示 + 工具定义不超过总窗口的 20%。工具 schema 里删掉没用的字段,description 写短一点,挤一挤总能省出几千 token。

第二个原则:工具返回结果进上下文之前先瘦身。搜索结果只留 top 5,数据库查询只取前 20 行,文件只保留命中片段。过滤逻辑写在自己的代码里,别指望模型帮你省。

第三个原则:历史分层管理。最近 2 到 3 轮完整保留,维持短期记忆;更早的滚成摘要;连摘要都攒了两轮的,归档到外部文件或数据库。相当于把上下文当 L1、L2 缓存,再加上一块硬盘。

class TokenBudget:
    """一个 128k 窗口的粗略预算分配"""
    def __init__(self, total: int = 128_000):
        self.static_limit = 20_000      # 系统提示 + 工具定义
        self.dynamic_limit = total - self.static_limit

    def trim(self, history):
        # 最近两轮完整保留
        recent = history[-2:]
        older = history[:-2]
        # 更早的历史压成摘要,保留文件名/数字/错误码
        summary = summarize(older, keep=["file", "code", "number"])
        return summary + recent

实现不一定长这样,但分层的思想是通用的。“预算”两个字是关键:先分好额度,内容进来先问一句,够不够?不够就先压缩它。

别硬撑了:开新会话不是认输

有一类内容从一开始就不该留在上下文里:项目级知识。技术选型、架构决策、用户偏好,应该写进 CLAUDE.md 或 AGENTS.md,每次新会话自动加载。会话过程中的流水账才属于上下文。

想通这件事之后,我开始主动开新会话:重要结论落盘,然后清空工作台,从头再来。这跟人开会记纪要一样——不是记性差,是工作记忆本来就不该装这些。

回到标题的问题:上下文窗口快满了怎么办?我的答案是,不要等它满。从一开始就做预算:限定静态内容的上限,拦截工具返回的数据洪流,把历史分好层,把高价值知识落到外部。窗口再大,也追不上 Agent 无限循环时的内容增长;真正让你赢的,是判断哪些内容根本不配进入窗口。

Anthropic 在 Effective context engineering for AI agents 里也说了类似的话:决定 Agent 上限的不是上下文窗口有多大,而是你能不能把最值得的信息放进去,把无关的挡在门外。

延伸:为什么不能把窗口开到 1M 一劳永逸?

Google 的 Gemini 确实把窗口推到了 1M token,但窗口大不等于好用:按 token 计费的 API,一次请求塞一百万 token,成本先行爆炸;而且 Lost in the Middle 已经证明,上下文越长,中间的信息越容易被忽略。长窗口解决容量问题,解决不了注意力问题。

延伸:工具返回结果太大怎么截断?

我的顺序是:先在工具侧做字段选择和分页;再在代码里截断为 top N;最后才让模型决定要不要继续查。反过来做的话,第一个大响应就把窗口塞爆了。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/365.html

(0)
上一篇 2026年8月29日 上午3:49
下一篇 4天前

相关推荐