先把话说在前面:我自己踩过一个特别典型的坑。半年前我做了一个带读网页、查资料、自动写总结这类工具的 Agent,丢进长时间会话里跑,前 10 轮表现惊为天人,说什么都能接住。可跑到第 25 轮左右,它开始记错用户偏好,甚至把一个已经完成的任务又原封不动执行了一遍。当时我第一反应是“模型智力不行”,后来把对话日志一帧一帧拉出来看,才意识到问题根本不在模型,而是上下文工程没做对。
AI Agent 的“越聊越笨”,绝大多数时候不是模型变蠢了,而是喂给模型的上下文出了问题。这个话题我观察了很久,发现凡是做 Agent 落地的人,最容易忽略、也最喜欢甩锅给模型的,恰恰就是这一层。我不打算讲模型选型,也不聊训练,只聊上下文工程里最关键的 5 个取舍。每个取舍背后都有代价,没有标准答案,只有适合当前场景的答案。适合正在做 AI Agent 搭建、天天纠结 token 预算、想让 Agent 稳定跑完长任务的开发者参考,看完可以直接拿这些取舍逻辑回自己项目里做决策。
1. 先搞清楚:Agent 是怎么“越聊越笨”的
1.1 上下文窗口不是硬盘,是白板
很多人把上下文窗口理解成“记忆硬盘”,以为窗口越大,记住的东西就越多。这个理解是错的。更准确的类比是一块白板:刚开始是干净的,你往上写流程、写需求,模型看得清清楚楚;写到后面,白板不够用了,要么新的内容把旧的盖上,要么你手动擦掉一部分。模型每次推理时,能看到的只有当前白板上的东西,而不是整个对话历史。
这解释了一个怪现象:同一个模型,你在对话框里问它简单问题,它聪明得很;丢进 Agent 里跑长任务,它却像个鱼一样只有 7 秒记忆。不是模型变了,是它眼前的白板被写满了,而且写上去的内容里,大部分是没有用的工具输出、中间思考过程、重复日志。
更麻烦的是,白板上的字迹不是越新越重要。用户的原始约束可能写在第 3 轮,到第 40 轮时已经被挤到边角甚至擦掉了,模型自然只能靠猜。猜的结果就是“一本正经地胡说八道”。
1.2 三种常见的衰减机制
我把长期跑 Agent 观察到的“变笨”过程归纳成三种机制,理解了它们,再看后面的 5 个取舍就顺了。
第一种是截断挤压。上下文窗口有硬上限,一旦达到上限,最常见的处理方式是“先进先出”,最早的内容被踢出去。早期用户说过的偏好、业务规则、禁止事项,往往就这么被悄悄挤掉了。等后面某个步骤需要用到这些信息时,模型拿不到,只能自己脑补,一补就偏。
第二种是信噪比劣化。token 总量有限,塞进来的内容里 80% 都是工具返回结果、日志、重复的系统提示,真正关键的指令只占很小一块。大家知道 Transformer 的注意力机制是“一视同仁”地扫描所有 token 的,信息密度低了,关键细节被淹没在噪声里,模型抓错重点的概率大幅上升。
第三种是错误级联。模型基于残缺上下文做出一步错误推测,这个推测会被当成新的“事实”写进后续上下文,后面的步骤全部建立在这个错误前提上。你以为它只是犯了一个错,实际上它是沿着错误一路狂奔,越聊越理直气壮。
2. 取舍一:上下文窗口留多长,才算“够用又不浪费”
2.1 窗口越大越聪明?别被参数带偏
市面上常见模型的上下文窗口从 8K、32K、128K 到 200K+ 都有。不少团队选模型时追着大窗口跑,心想“一次塞进去不会错”。但大窗口是有代价的,而且代价还不小:
首先是 token 费用。调用模型是按 token 计费的,上下文越长,每次调用的成本越高。一个 Agent 完成任务通常要调用十几二十次模型,上下文从 4K 涨到 8K,总成本直接翻倍。我见过一个自动化运营的 Agent 项目,上线一周成本爆掉,原因就是每次调用都把全部历史带上了。
其次是延迟。上下文长度直接影响推理速度,输入 token 翻倍,单次响应时间可能从 1 秒涨到 3 秒以上。用户感受是明显的“变卡了”。
再一个是注意力稀释。实验做多了你会发现,同样一个复杂指令,放在 4K 上下文里执行正确率明显高于塞在 20K 上下文中。上下文越长,模型越容易漏掉埋在中段的关键信息。所以“大窗口”只是给了你更多操作空间,不等于你应该每次都用满。
2.2 用 token 预算倒推窗口大小
我的习惯是:每个任务先算 token 预算,再定窗口策略,而不是反过来。
举个例子。假设你的 Agent 要做多轮信息收集和写作,单轮消息大约是:系统指令 1,000 token,用户输入 200 token,工具返回结果平均 600 token,模型回复 400 token。一轮完整交互大概 2,200 token。如果在一次会话里要跑 50 轮,全量保留就是 110K token,已经顶到主流窗口的上限,还没给你留任何缓冲。
这时候我会把预算拆成几块:
| 内容区块 | 预算占比 | 说明 |
|---|---|---|
| 系统级指令 | 10% | 角色、输出格式、工具使用规则 |
| 长期事实区 | 15% | 用户偏好、业务约束、项目背景 |
| 近期对话缓存 | 40% | 最近 5~10 轮原文,滑动窗口 |
| 工具结果区 | 25% | 最近任务的必要返回,其余摘要 |
| 检索增强区 | 10% | 按需检索到的历史片段 |
如果上下文窗口是 128K,我通常会把硬上限设成 96K,留 25% 余量给突发情况。达到 80% 用量时就触发压缩或归档,而不是等窗口塞满了才处理。这个比例没有标准答案,但先算清楚每一轮大概消耗多少 token,再决定“最多保留几轮原文”,会让你对窗口使用有掌控感,而不是两眼一抹黑。
3. 取舍二:原文一字不差,还是压缩后保留
3.1 原文保留的最大价值是“不失真”
要我说,原文是金,摘要只是铜。原文不会编造细节,用户第 12 轮说过“报告里不要出现百分比”,原文里就是这句话,模型读了几十轮之后依然能看到原话,准确率高得吓人。
但原文的代价也很明显:太占地方,而且充满噪声。工具返回个 JSON,一次可能就 2000 token,里面真正有用的可能就一个字段。这两千 token 既挤占了窗口,又稀释了注意力。所以一味留原文,长任务一定是死路一条,除非你的任务只有三五轮。
3.2 摘要压缩,为什么常常压不准
既然原文太贵,很多人自然想到“用模型把历史对话总结一下”。这个方向没错,但直接做会踩三个坑。
第一个坑是细节丢失。你把 10 轮对话压成 200 字,模型很可能只提取了“主要意图”,但把用户提过的精确数字、明确禁止项、指定格式全压没了。摘要适合保留“发生过什么”,但不擅长保留“必须怎么做”。
第二个坑是编造。让模型总结时,如果原文里信息不足或表述模糊,模型会自动补全合理但从未出现的细节。这个后果比截断还可怕——截断只是丢失信息,摘要可能产生虚假信息。虚假信息一旦进入上下文,后面所有推理都建立在沙地上。
第三个坑是级联失真。为了控制长度,你可能会对摘要再做摘要。摘要的摘要,误差会逐级放大,最后得到的内容可能只剩一句“用户要求写个报告”,所有细节全没了。
3.3 三级混合策略:原文和摘要各司其职
我现在的做法是分层,而不是二选一。
第一层是硬性约束区。用户偏好、业务红线、输出格式、关键参数,这些不可压缩、不可改写,永远以原文形式常驻上下文。你可以把这些内容当作 Agent 的“宪法”,摘要可以不提,但宪法必须原样保留。
第二层是近期对话区。最近 5~10 轮的对话保留原文,窗口滑动,保证模型对当前正在做的事有完整、准确的理解。这个区域直接对接“当前任务”,失真成本最高,不值得省。
第三层是历史记忆区。更早的内容做结构性摘要,而且摘要必须带模板。我做总结时会强制要求输出四个字段:已完成动作、还未完成的事、不可改变的约束、不确定/存疑的地方。有了这四个字段,即便历史再长,模型也能快速知道“哪些事不能动、哪些事还没做”,而不是只记得一句笼统的概括。
4. 取舍三:短期会话记忆,还是长期外部记忆
4.1 上下文窗口再大,也装不下 Agent 的“一生”
如果 Agent 只在一个会话里工作,前面说的窗口策略够用了。但很多 Agent 是要长期跑的:今天你跟它说“以后所有报告都用简体字”,明天它换个会话帮你干活,这句约束如果只存在于上一个会话的上下文里,它就等于从没存在过。
所以必须把上下文划分为两层:短期会话记忆和长期外部记忆。短期记忆是当前会话中的上下文窗口,相当于工作台;长期记忆是存放在外部存储的事实,相当于你的档案柜。工作台随时可以清空,档案柜要一直保留。这个区分是 Agent 能“越来越懂你”而不是“越来越笨”的前提。
4.2 第二大脑的存储选型:向量库、KV 存储、还是文件
长期外部记忆不是摆个数据库就行,要根据数据类型选存储,我的经验分三种:
语义检索型(向量数据库):适合用户没给明确关键词、需要“相关内容联想”的场景。比如 Agent 写周报时,需要从过去 30 天的对话里找出和“项目风险”相关的段落,这时候向量检索最合适。但要注意,向量相似度不等于事实正确,检索结果必须经过一道验证,不能让模型直接采信。
精确检索型(KV 数据库或关系型数据库):适合用户 ID、偏好开关、业务参数这类结构化事实。比如“用户喜欢繁体/简体”“输出格式必须为 Markdown”“禁止触达某个频道”。这些数据用结构化字段存,读取快、不漂移、不会产生歧义,可靠性远超向量方案。
原始日志型(文件或消息队列):适合保存全部原始对话和工具结果。不直接注入上下文,主要用来事后回放、排查问题。我排查 Agent 变笨时,90% 的线索都来自这里的原始记录。
顺便说一句,现在“基于 Rust 写 AI Agent”讨论度很高,有些团队甚至用 Rust 重写 Agent 引擎,就是为了让记忆检索、调度链路快一个数量级。这其实说明一个问题:决定 Agent 长期能力的,恰恰是这层外部记忆和调度架构,而不是模型窗口本身。
4.3 记忆同步的正确时机
外部记忆不是每个回合都写。高频写入会导致两个问题:一是调用成本爆增,每个回合你都要多调一次模型做“记忆抽取”;二是写入噪声太多,把有价值的事实淹没在碎片里。
我一般用两个触发条件。第一个是时间条件:每完成一个独立子任务或每 5 轮,做一次记忆抽取。第二个是异常条件:检测到用户表达了新偏好、修改了旧要求、或者给出了明确的禁止/允许指令,立刻写入外部存储。抽取的内容也不是“总结全部”,而是抽“用户画像的变化、业务约束的增减、未完成任务的更新”。这样档案柜里存的东西大部分是有用的,不是流水账。
会话启动时,再从外部存储按需加载本会话需要的长期事实。加载多少可以自己控制:全量加载一定会爆窗口,只加载与当前任务语义相关的部分即可。一句话总结:写入要克制,读取要精准。
5. 取舍四:按时间顺序全给,还是按相关性捡重点
5.1 顺序保留是“流水账”,相关性检索是“索引卡片”
默认情况下,把历史对话按时间排列喂给模型是最省事的。但这在整个上下文比较长的时候很有问题:流水账里什么都有,有用信息被自身淹没。模型需要自己从一堆无关内容里“捞针”,捞不到就是变笨。
另一种思路是:不关心顺序,只关心相关性。把上下文当作一个检索系统,每次请求时,根据当前用户问题去外部记忆里捞出最相关的 3~5 个片段,再组装进上下文。这个思路接近 RAG,也是很多 Agent 框架的默认选项。
但这里有个很容易翻车的地方:相关性排序和“任务重要性”不是一回事。用户第 3 轮说过“这份合同必须在一小时内发出”,这一条在语义上和 40 轮后的“把合同发给我”高度相关,但按时间线它早被冲掉了,如果你只按“最近消息”截取,这条关键约束就没了。所以我建议不要把顺序和检索对立起来,而是混合使用:近期对话用顺序窗口保真,历史信息用相关性检索补全。
5.2 调整 prompt 段落位置,通常可以显著提高召回
除了用什么,还要注意放在哪。我观察到一个反复出现的现象:把关键信息放在上下文的开头和结尾,模型响应相关指令的准确率明显高于放中间。这也符合对注意力机制的理解:开头有“首位效应”,结尾有“近因效应”,中间的内容最容易互相覆盖、被忽略。
实操时,我会把最终注入的上下文排成这样的顺序:固定身份角色在最前,本次任务的直接指令和约束靠后紧挨着用户消息,历史和检索片段夹在中间。这样模型每次开口前最先看到身份、最后看到当前任务,中间是参考资料。如果你有“用户明确要求 XYZ”,一定要把它放在靠近末尾的位置,别让它躺在历史摘要的中段。
5.3 检索粒度与触发时机,怎么才算“刚刚好”
检索粒度太粗,等于没检索,所有内容都进来,窗口照样爆;粒度太细,每句话都要检索一次,模型疯狂重复引用同一批内容,也是浪费。
我的经验是把检索的触发时机定在三个节点:用户新消息进入时、工具返回结果后、子任务状态切换时。每次检索的目标不是“覆盖全部历史”,而是“回答当前这一步最需要知道的信息”。
额外给一个避坑建议:检索结果不要“提头去尾”直接塞进上下文,最好在每段前后加一个明确的开始和结束标记,比如<memory>…</memory>或者简单的分隔线。这样模型能识别出“这部分是外部引用”,而不是自己的推理结论,减少把检索内容误当事实的机率。这个细节非常小,但排查时你会发现,很多“胡说八道”的根源就是模型把检索到的片段和真实结论混在一起了。
6. 取舍五:系统提示词,往重了写还是往轻了写
6.1 系统提示词绝不是越长越好
很多团队都有这个执念:把所有规则、行业术语、工具说明全塞进 system prompt,恨不得写五千字。结果就是 token 预算被吃掉一大块,同时模型要同时“遵守”几十条规则,规则一多,互相冲突或者被忽略的概率急剧上升。你把 30 条规则塞给它,它真正内化的可能只有前几条和最后几条,中间的规则全成了摆设。
我并不是说 system prompt 应该越短越好,太短也不行。角色信息缺失、工具调用格式不定义清楚,模型会自行发挥,Agent 的稳定性立马被击穿。正确的方向不是“重”或“轻”,而是“分层动态组装”。
6.2 动态组装:让系统指令在正确的时间出现在正确的位置
我把系统提示拆成三层:
- 静态层:真正不变的内容,比如“你是文档助手”“输出用 Markdown”“调用工具时必须填写必填参数”。这些内容全项目共享,写一次,数量控制在几百 token 以内。
- 动态层:每次请求时根据当前任务临时拼装,包括本次任务目标、可用的工具列表、用户当前身份与画像、本次需要遵守的特别约束。比如用户本次指定“只要结论不要分析”,那就把这条临时约束放在最靠后的位置,紧贴用户消息。
- 上下文层:历史摘要和检索片段,按前文说的排列规则注入。
一个可参考的伪代码结构长这样:
def build_prompt(session, user_message, memories): # 1) 固定身份与全局规则,放在开头 blocks = [session.global_rules] # 2) 本次任务动态指令 blocks.append(session.get_current_task_instruction()) # 3) 外部记忆与历史摘要,放在中间 blocks.append(memories.retrieve(user_message)) # 4) 近期对话原文 blocks.append(session.recent_messages(6)) # 5) 用户当前消息与“最终指令”,放最后 blocks.append(user_message) return join(blocks)这个结构本身不复杂,但它保证了每个环节各司其职:身份信息恒定,任务信息明确,历史信息按需加载,当前指令永远最后出现。比起把所有规则堆在前面让模型自己权衡,这种组装方式稳定得多。
7. 落地实操:让它成为一套可维护的框架,而不是某次灵光一现
7.1 按“热、温、冷”三层管理上下文生命周期
只懂取舍还不够,要有一个能长期跑不崩的框架。我习惯把上下文划分为热、温、冷三层:
- 热数据:当前任务正在使用的信息。比如正在写的文档内容、最近 5 轮对话、本次调用的工具结果。这些随每次请求完整保留,窗口预算给得最足。
- 温数据:上一个子任务的摘要、仍可能被引用的前置结果。以结构化摘要形式保留,量不大,按需展开。
- 冷数据:更早的全部原始记录、已完成任务的细节。放入外部记忆,只通过检索进入上下文。
这个分层也可以直接对应你选的存储结构:热数据在内存里,温数据在 KV/缓存里,冷数据在向量库或文件里。这样一套分层的生命周期,既能控制 token 预算,又能兼顾对话的连贯性,而且排查问题时有据可查。
7.2 量化“变笨”:给你的 Agent 建立一套体检指标
判断 Agent 有没有变笨,不能靠感觉。我给自己项目定的指标有四个:
- 关键信息召回率:准备一组测试问题,把早期对话中的约束作为“标准答案”,跑一轮长会话后问模型这些约束,看能答对多少。比如第 3 轮用户说了“全程不要提竞品”,到第 20 轮问一句“本项目有什么禁忌”,答对就是召回成功。
- 偏好遵从率:跑一系列生成任务,检查输出是否符合用户既定偏好,比如格式、语气、简单句。
- 重复执行率:在长任务末尾检查工具调用序列,统计有没有出现“已经完成又做一遍”的步骤。
- 中断恢复率:在第 20 轮强行打断,让它描述“当前任务进行到哪一步”,看回答和实际状态的匹配度。
出现异常时,除了看最终结果,还要保留“压缩前的原文”和“注入后的最终上下文”。很多问题的定位都靠对比这两份内容:是原始信息丢了,还是信息在压缩时被写歪了,一目了然。
8. 现场问题排查实录:三个让我头疼的场景
8.1 任务执行到一半,Agent 把用户上限参数改掉了
这个场景发生在一次自动文案生成项目上。用户最初设定“每篇文章上限 800 字”,跑到第 15 轮,Agent 突然生成了一篇 2000 字的长文。查日志发现,用户那句“上限 800 字”早被滑出窗口,Agent 只能从历史里含糊记得“好像有字数要求”,于是自己脑补了一个“越详细越好”。修法就是把它放进“硬性约束区”,任何截断都不允许碰它。这类问题的核心教训是:凡是和“业务红线”相关的参数,永远不能只靠窗口滑动来保留。
8.2 工具结果把当前用户消息冲出了窗口
另一个常见场景是:Agent 调了三次搜索工具,每次返回上千 token 的结果,结果第三次工具输出还没处理完,用户当前消息就被挤出了有效注意力范围。模型看着一堆工具 JSON 回答用户,自然答非所问。我的调整是:滑动窗口不是简单的先进先出,要优先保住“最近用户消息”和“当前任务指令”,工具输出反而可以放进一个可折叠区域,必要时先做个几十 token 的浓缩再进窗口。
8.3 摘要越抽越薄,把业务约束抽没了
还有一次是摘要链出了问题。原始摘要 1200 字,到第二轮压缩变成 300 字,用户“不要用感叹号”的要求直接蒸发。后来我改成强制模板摘要,每轮总结必须保留“不可违反约束”字段,并且这个字段只允许原文摘录、不允许改写。从那以后,无论压缩多少层,业务约束基本不会丢。这里顺便建议:所有摘要工作,最好用一个单独的模型调用来生成,不要在主对话里顺手总结,避免互相干扰。
8.4 给上下文加“标记线”的隐蔽收益
最后分享一个小技巧:在注入检索片段和外部记忆前后加上明确的标记。习惯用<memory>和</memory>标签来包裹外部信息。这个做法的两个好处,一是模型能更清楚地区分“外部引用”和“自身推论”,减少事实混淆;二是排查 prompt 时,你可以一眼看到哪些内容来自检索、哪些来自原文。别小看这个细节,它能让很多问题从“玄学”变成“可定位”。
9. 写到最后,再分享一点个人习惯
上下文工程这件事,说到底就是做权衡:窗口长度换成本,原文保真换摘要压缩,短期记忆换长期外部记忆,顺序上下文换相关性检索,系统指令的重量换动态组装。每一边都有收益,也都有不可忽视的代价。没有哪个方案是永远最优的,但一个好的 Agent 项目,一定是把每个取舍都明确写进代码、写进监控指标的。
我自己现在的习惯是:每个 Agent 项目上线第一天,就把 token 预算表、上下文分层结构、变笨体检指标写进 README。后面不管谁来维护、改了多少版,只要这套上下文策略还在,Agent 就不会跑着跑着突然“智商归零”。如果你正在被“越聊越笨”折磨,不妨先别急着换更大的模型,回去把上下文日志拉出来,对照上面这 5 个取舍逐一检查。多数时候,问题不在模型,而在你喂给它的那一堆字里。