Agent 聊到第 20 轮就“失忆“?——长对话上下文丢失的排查与修复完全指南

文章目录

    • 一、先看现场:一个真实的"失忆"案例
      • [1.1 先分清:什么才算"关键信息"](#1.1 先分清:什么才算"关键信息")
    • [二、原理:Agent 到底是怎么忘的](#二、原理:Agent 到底是怎么忘的)
      • [2.1 上下文窗口是一笔"硬预算"](#2.1 上下文窗口是一笔"硬预算")
      • [2.2 信息"消失"的四种机制](#2.2 信息"消失"的四种机制)
      • [2.3 一条因果链](#2.3 一条因果链)
    • 三、排查:六步定位法(别猜,看证据)
      • [第 ① 步:固定复现,把"真正发出去的 prompt"落盘](#第 ① 步:固定复现,把"真正发出去的 prompt"落盘)
      • [第 ② 步:统计 token,找到"丢失临界点"](#第 ② 步:统计 token,找到"丢失临界点")
      • [第 ③ 步:查裁剪------两个"事故代码"](#第 ③ 步:查裁剪——两个"事故代码")
      • [第 ④ 步:查压缩------摘要是"大意",不是"契约"](#第 ④ 步:查压缩——摘要是"大意",不是"契约")
      • [第 ⑤ 步:查存储------信息有没有"住在窗口之外"](#第 ⑤ 步:查存储——信息有没有"住在窗口之外")
      • [第 ⑥ 步:查注入------信息在,但模型"看不见"](#第 ⑥ 步:查注入——信息在,但模型"看不见")
      • 加分项:装一道"校验探针"
    • 四、一张表定位根因
    • 五、修复:五个方案,按代价从低到高
      • [方案 ①:关键约束结构化外置(首选,改动小、收益最大)](#方案 ①:关键约束结构化外置(首选,改动小、收益最大))
      • [方案 ②:上下文分区------常驻区永不参与截断](#方案 ②:上下文分区——常驻区永不参与截断)
      • [方案 ③:裁剪要看价值,不看条数](#方案 ③:裁剪要看价值,不看条数)
      • [方案 ④:摘要时给一份"必须保留"清单](#方案 ④:摘要时给一份"必须保留"清单)
      • [方案 ⑤:RAG 检索 + 会话分段(兜底,不是主力)](#方案 ⑤:RAG 检索 + 会话分段(兜底,不是主力))
    • 六、修复前后对比
    • 七、一个可直接抄的最小实现
    • [八、上线前 Checklist](#八、上线前 Checklist)
    • 九、总结

一句话概括 :Agent 忘事,99% 不是模型变笨,而是你以为它记得的那些话,压根没进它这一轮看到的 prompt。

一个类比:这就像开了一下午会,说到第 20 件事时,你发现会议最开始定下的那条硬性规则没人提了。你以为是大家记性差,其实更可能是------记录本太薄被撕掉了前几页(截断)、记录员写成了摘要(压缩)、或者记录写了但没贴在墙上(注入位置不对)。


一、先看现场:一个真实的"失忆"案例

先看一段典型对话(简化版):

text 复制代码
[第 3 轮]  用户:我们这个项目用 PostgreSQL,所有 SQL 不要用 MySQL 语法。
[第 5 轮]  用户:接口返回统一用 camelCase。
           Agent:好的,我会遵守这两点。✅
   ...
[第 12 轮] Agent:下面是数据库查询代码......(还是 PostgreSQL,正常)
[第 20 轮] Agent:这是一个 MySQL 的连接示例...... ❌
[第 20 轮] 用户:???我第 3 轮就说过了,用 PostgreSQL!
           Agent:抱歉!我马上改用 PostgreSQL。

这个现场有三个非常典型的特征,值得你先记住,后面所有排查都靠它们做判断:

  1. 悄悄失效,没有任何报错------模型"理所当然"地违反了约定,它甚至不知道自己违约了;
  2. 一提醒就恢复 ------说明模型能力没问题、知识没问题,纯粹是那段话这一轮没在它眼前;
  3. 越早的信息越容易丢,中段次之,最近几轮几乎不丢。

1.1 先分清:什么才算"关键信息"

不是所有丢失都值得修。关键是分清"契约"和"闲聊":

类型 例子 丢了会怎样 要不要救
契约型(贯穿全程的约束/参数) 技术选型、合同编号、金额上限、安全规则、必用敬语、禁用某函数 后面几十轮全部跑偏,且错得毫无察觉 必须救
事实型(一次性结论) "上次那个报错是权限问题" 影响局部,追问可补 值得救
闲聊型(过程性内容) "好的"、"稍等"、寒暄、试错过程 基本无影响 不用救

所以排查前先问自己一句:丢的那条,是契约还是闲聊? 如果整个对话里根本没有"契约型"信息,那你遇到的不是 bug,是正常淘汰。


二、原理:Agent 到底是怎么忘的

2.1 上下文窗口是一笔"硬预算"

模型每轮只能看到有限 token(比如 128K)。所有东西共享同一个窗口,塞不下就必须丢:

text 复制代码
┌────────────────────────────────────┐
│ ① System Prompt     角色、规则      │  ← 相对固定
│ ② 注入的记忆 / 检索结果             │  ← RAG、长期记忆
│ ③ 历史消息(越来越长......)           │  ← 每轮 +2 条起步
│ ④ 工具调用结果(经常巨大)          │  ← 一次检索吃掉几千 token
│ ⑤ 本轮用户输入                      │
└────────────────────────────────────┘
        ↑ 只有 1 和 5 是"每轮必需",2/3/4 全是浮动的

问题在于:③ 和 ④ 只增不减。到第 20 轮,早期消息很可能已经被挤出窗口了。

2.2 信息"消失"的四种机制

这是全文的骨架,请务必区分清楚------它们的修法完全不同:

机制 一句话原理 典型表现 怎么快速识别
滑动窗口截断 只保留最近 N 条消息 早期几轮整段消失,中段完好 固定轮次必丢,不是随机
摘要压缩 旧对话被压成一段总结 "讨论过数据库选型",但 PostgreSQL 这个词没了 信息在,但被泛化了
工具结果覆写/清理 旧工具输出被替换成占位符 早期查到的数据引用不了 tool 消息变成空/[已清理]
注意力淹没(Lost in the Middle) 信息在,但埋在中部 时好时坏,换个位置就好 复制一份到 system prompt 就恢复正常

前三种是真的丢了 ,第四种是信息在、但模型"看不见"。这是最容易被误判的一类------很多人拼命加大窗口,结果毫无改善,就是因为踩的是第四种。

2.3 一条因果链

把上面的机制串起来,就是完整链路:

历史只增不减 → 逼近窗口上限 → 框架启动裁剪/压缩 → 关键信息被无差别丢弃或泛化 → 模型"失忆"。

注意"无差别"三个字:框架不知道哪句值钱,它只按"新旧"或"长度"取舍。第 3 轮那句一诺千金的选型,和一句"好的"享有完全相同的存活权。


三、排查:六步定位法(别猜,看证据)

排查的核心思想只有一句话:

别猜,去看每一轮实际发给模型的那份完整 prompt。

最容易犯的错,是盯着聊天界面复盘。界面展示的往往不是发给模型的最终内容------界面会显示全部历史,模型只收到了后面 15 条。

text 复制代码
① 固定复现,把 prompt 原样落盘
        ↓
② 统计 token,找到"丢失临界点"
        ↓
③ 查裁剪:滑动窗口 + 消息过滤
        ↓
④ 查压缩:摘要把事实泛化了吗
        ↓
⑤ 查存储:有没有外置、入没入库、召不召回
        ↓
⑥ 查注入:位置上是不是被淹没了

第 ① 步:固定复现,把"真正发出去的 prompt"落盘

做这一件事,就解决了一半问题。 在调用模型的地方加日志,记录的是拼装完成的 messages:

python 复制代码
import json

def call_model(messages, turn_id):
    # 关键:记录"即将发给模型"的内容,而不是用户界面看到的内容
    with open(f"prompts/turn_{turn_id:02d}.json", "w", encoding="utf-8") as f:
        json.dump(messages, f, ensure_ascii=False, indent=2)
    return llm.chat(messages)

⚠️ 两个注意点:① 落盘前做敏感信息脱敏 ;② 复现时固定输入序列,让问题 100% 可重现,否则后面全是玄学。

第 ② 步:统计 token,找到"丢失临界点"

对每轮的 dump 做一次体检,重点不是总 token,而是那条关键信息还在不在:

python 复制代码
for turn in range(1, 21):
    msgs = load_messages(turn)
    total = sum(count_tokens(m["content"]) for m in msgs)
    has_rule = any("PostgreSQL" in m["content"] for m in msgs)
    print(f"turn {turn:2d} | tokens: {total:6d} | 含'PostgreSQL约定': {has_rule}")

跑出来大概长这样:

text 复制代码
turn  1 | tokens:   3200 | 含'PostgreSQL约定': False
turn  3 | tokens:   6800 | 含'PostgreSQL约定': True    ← 约定进入对话
   ...
turn 18 | tokens: 124000 | 含'PostgreSQL约定': True
turn 19 | tokens: 127800 | 含'PostgreSQL约定': False   ← 从这轮起被挤出!
turn 20 | tokens: 126500 | 含'PostgreSQL约定': False

这张表直接把你分流到三条路上:

  • 某轮起彻底消失 → 截断,去 ③
  • 还在,但变成一句概括 → 摘要失真,去 ④
  • 还在且完整,模型仍然不用 → 注意力问题,去 ⑥

💡 如果连第 3 轮就没出现过,说明压根没入库,直接去 ⑤ 查写入逻辑。

第 ③ 步:查裁剪------两个"事故代码"

坑 1:按条数滑动窗口

python 复制代码
# ❌ 事故代码:只保留最近 20 条
def trim_history(messages, max_turns=20):
    return messages[-max_turns:]

到第 20 轮,第 1~5 轮被无声丢掉。特征就是"稳定到固定轮次必丢"------如果你观察到这个规律,基本可以锁定这里。

坑 2:隐形消息过滤(更隐蔽)

很多框架还会顺手做"清理",而无差别清理最致命:

python 复制代码
# ❌ 只留人类消息和 AI 回复,把所有工具返回扔掉
filtered = [m for m in msgs if m.type in ("human", "ai")]

如果第 3 轮用户说"参数在这个文档里",关键信息其实藏在那一轮的工具返回中,这句过滤就把它连根拔了。

python 复制代码
# ✅ 修复思路:工具返回先抽取关键字段,再决定丢弃
key_facts = extract_facts(tool_result)   # 例如 {"db": "PostgreSQL"}
fact_store.update(key_facts)
filtered = [m for m in msgs if m.type in ("human", "ai")]

验证方法:在 ② 找到的临界点附近,把被裁掉的消息打印出来,看里面有没有选型、参数、约定、禁令。有 → 裁剪就是直接元凶。

第 ④ 步:查压缩------摘要是"大意",不是"契约"

超过阈值时框架会自动摘要。摘要天然有损,而且它不知道哪些细节对你的业务是致命的:

text 复制代码
原始对话(第 3~8 轮):
  用户:用 PostgreSQL;密码用 bcrypt 加密;接口统一 camelCase。

自动摘要(第 15 轮被注入的历史):
  "用户讨论了数据库选型和安全要求,并确定了接口规范。"

→ PostgreSQL 没了,bcrypt 没了,camelCase 也没了。模型只能靠猜,而它通常会猜成最常见的那个(MySQL)。

根本原因是:摘要器把"一次性闲聊"和"全局常驻约束"一视同仁地压缩,而约束往往是最先被压掉的那类(因为它只是一句话,没有"过程")。

验证方法 :把摘要版历史 dump 出来,逐条核对早期轮次里的事实性内容------技术选型、数字、参数、配置项、禁止事项。凡是被泛化成"讨论了 XX"的,都是风险点。

第 ⑤ 步:查存储------信息有没有"住在窗口之外"

健康的架构里,重要约定不应该住在对话历史里:

text 复制代码
对话历史(易失:会被裁剪、会被压缩)
        ↓  抽取关键事实
外部记忆 / 状态存储(持久:每轮稳定注入)
  - tech_stack:     PostgreSQL
  - password_hash:  bcrypt
  - api_style:      camelCase

围绕这条链,问自己四个问题:

① 有没有"提取"动作? 是谁在什么时候把约定从对话里抠出来的?

② 提取后每轮都稳定注入吗,还是只在某一轮注入过一次?

③ 约定变了,记忆跟着变吗?(用户第 15 轮改成 MySQL,记忆里还是 PostgreSQL 就是新 bug)

④ 如果用了向量检索,它真的召得回吗?

python 复制代码
# 直接用第 20 轮的问题去召回第 3 轮的约定
hits = vector_store.similarity_search(query="数据库 选型 约束", k=5)
print([h.score for h in hits])   # 分数极低 → 召回失败

向量召回失败的常见原因:

  • Embedding 相似度低(切分太碎、语义没对齐、中英混杂)
  • topK 太小、相似度阈值太高
  • 早期消息入库时就没写成功(没打标签、没持久化、异步写丢了)

还有一个高频坑:槽位/实体记忆只在首轮提取。

python 复制代码
# ❌ 只在第一轮抽一次参数,之后既不刷新也不重读
if turn == 1:
    slots = extract_slots(user_input)
# 一旦这条消息被滑动窗口清掉,槽位就彻底失联

框架层的坑:局部变量没进全局状态。 如果你用 LangGraph / LangChain AgentExecutor,最常见的 bug 是------关键信息存活在某个节点的局部变量里,节点跑完就销毁:

python 复制代码
# ❌ 约束只在本函数内存活,函数返回即蒸发
def tool_step(query):
    budget = extract_budget(query)
    return result

# ✅ 写进全局 State,任何节点都能读到
class AgentState(TypedDict):
    global_constraints: dict     # 全局常驻区

另外提醒一句:用户眼里的"第 20 轮",不等于 Agent 内部的"第 20 次调用" 。Agent 内部可能已经循环调工具几十次,token 消耗远超预期,于是提前触发了滑动窗口。你按对话轮数去算临界点,会算不对。

第 ⑥ 步:查注入------信息在,但模型"看不见"

如果 ②~⑤ 都正常(信息完整地在窗口内),那问题就是注意力分配,学术上叫 Lost in the Middle :模型对上下文开头和结尾的注意力显著强于中部。

text 复制代码
❌ 不佳的布局:
   [System] [历史 40 轮......约定埋在中部] [巨型工具输出] [用户输入]
                                        ↑ 尾部抢走注意力,中部被淹没

✅ 更好的布局:
   [System + 关键约定置顶] [历史(可被裁剪)] [用户输入]

还有两个放大器,会让你误以为是"窗口太小":

  • 工具返回挤占预算 :后面几轮的大段文档/报表最占 token,而裁剪策略往往优先挤掉用户早期的对话,而不是压缩工具返回;
  • 所有消息混在一个列表里一起滑:于是"最早的"永远最先被清------这正是"稳定到 20 轮丢失"最典型的成因。

验证方法(消融实验) :把那句约定复制一份塞进 system prompt,其他一律不动。

  • 问题消失了 → 100% 是位置/权重问题,按第五章方案 ①、② 修;
  • 问题依然在 → 回去查 ③④⑤。

加分项:装一道"校验探针"

这是最便宜、最好用的观测手段:每一轮让 Agent 先说一遍它记住的约束清单。

text 复制代码
User: 帮我算下这个方案报价。
Agent 自检: 【全局约束】合同 AX-2026-001、敬语 ✓、预算 ≤ 5 万 → 继续
   ...
User: (第 20 轮)报个新方案。
Agent 自检: 【全局约束】(空)        ← 就是这一轮开始"失忆"的

它能直接告诉你失忆发生在第几轮,比翻日志快得多。上线后也可以留着,但注意别把自检内容拼进正式回复里。


四、一张表定位根因

把前面所有线索收进一张表,对着症状直接找修法:

根因 现象特征 修复方向
滑动窗口 FIFO 截断 固定轮次丢;dump 里没有早期消息 上下文分区:常驻区 + 滚动区(方案 ①②)
消息过滤误删 关键信息原本在 tool 返回里,现在整条没了 先抽取字段再丢弃(方案 ①)
摘要压缩失真 消息还在,但被泛化成"讨论了 XX" 摘要保底 + 事实清单(方案 ④)
向量记忆召回失败 历史在库里,检索不到 / 分数极低 优化切分与 Embedding;但约定别依赖召回(方案 ①)
槽位只抽首轮 首轮过后参数就"归零" 每轮校验并刷新槽位(方案 ①)
局部 State 丢失 数据只在节点变量里,没持久化 全局 State / 外部存储(方案 ①)
工具返回挤占预算 工具输出超长,用户历史先被淘汰 工具返回单独截断/摘要(方案 ③)
Prompt 权重倾斜 信息完整在窗口内,模型仍不用 常驻区置顶 + 校验探针(方案 ②)

五、修复:五个方案,按代价从低到高

先看全局权衡,再决定动哪里:

方案 改动成本 见效速度 解决哪类问题
① 关键约束结构化外置 低 立刻 截断、压缩、召回失败(治本)
② 上下文分区 低 立刻 位置淹没、常驻信息被清
③ 重要性感知的裁剪 中 立刻 工具返回挤占、按条数误裁
④ 摘要保留事实清单 低 立刻 摘要泛化
⑤ RAG / 会话分段 高 中期 超长对话(50+ 轮)、多天任务

方案 ①:关键约束结构化外置(首选,改动小、收益最大)

核心思想:别指望对话历史帮你记住约定。 用规则或轻量 LLM 把契约抽成结构化的键值对,每轮无条件注入 system prompt。

python 复制代码
# 用户约束的持久化(越简单越好,别上向量库)
fact_store = {
    "tech_stack":    "PostgreSQL",   # 用户于第 3 轮确认
    "password_hash": "bcrypt",
    "api_style":     "camelCase",
}

system_prompt = BASE_PROMPT + "\n\n## 项目约定(长期有效,优先级高于一切历史对话)\n" + \
    "\n".join(f"- {k}: {v}" for k, v in fact_store.items())

三个必须做到的细节:

  1. 注入在 system prompt 里,不是拼在历史尾部------尾部会被当成"可选上下文";
  2. 每轮都注入,不是只在提取那一轮注入;
  3. 可更新:第 15 轮用户改成 MySQL,这里要跟着改,否则就是新 bug。改的时候顺手打印一行日志,方便日后追溯。

方案 ②:上下文分区------常驻区永不参与截断

把"一个列表一起滑"改成"两个区各管各的":

text 复制代码
【全局关键约束】(置顶,永不参与滑动截断)
  - 合同号:AX-2026-001
  - 必须使用敬语
  - 预算上限 5 万元
──────────────────────────────────────
【近期对话】(滑动窗口,允许淘汰旧内容)
  - turn-15 ... turn-20

很多"稳定到 20 轮丢失"的案例,改完这一条就消失了。原因是结构的,不是参数的------加大窗口只是把 20 轮推迟到 40 轮,迟早还会犯。

方案 ③:裁剪要看价值,不看条数

python 复制代码
def smart_trim(messages, token_budget):
    pinned = [m for m in messages if m.get("pinned")]     # 已标记的关键消息
    rest   = [m for m in messages if not m.get("pinned")]

    # 先压工具返回(最占地方、信息密度最低)
    rest = compress_tool_results(rest, max_tokens=2000)

    # 还不够,就"从旧到新"渐进摘要,而不是一刀切删除
    while total_tokens(pinned + rest) > token_budget:
        rest = summarize_oldest_pair(rest)

    return pinned + rest

关键在顺序:先压工具返回 → 再摘要旧对话 → 最后才考虑丢。多数实现反过来了,先丢用户历史,把最贵的东西最先扔了。

方案 ④:摘要时给一份"必须保留"清单

压缩历史时,用 prompt 强制约束摘要器:

text 复制代码
总结以下对话,但必须**逐字保留**:
① 技术选型与工具名(如 PostgreSQL、bcrypt)
② 数字、参数、配置项、金额、编号
③ 用户明确提出的要求与禁止事项
其余过程性讨论可压缩为一句。

如果有条件,让契约根本不进摘要器(方案 ①)------这比调 prompt 更可靠,因为 prompt 约束是概率性的。

方案 ⑤:RAG 检索 + 会话分段(兜底,不是主力)

  • RAG :把早期完整对话切片入库,需要细节时按需召回。适合 50+ 轮、无法全量保留的场景。 ⚠️ 关键约定仍应走方案 ①。检索有召回失败的概率,而契约不能有概率。
  • 会话分段 + 状态快照:超长任务拆成多个会话,每次开场用"状态快照"恢复上下文,而不是拖着全部历史。这是多天任务型 Agent 的标准做法。

六、修复前后对比

还是开头那个案例,看第 20 轮实际发给模型的内容:

text 复制代码
修复前:
┌────────────────────────────────────────┐
│ System: 你是一个编程助手                  │
│ [第 1~12 轮:已摘要成一段话              │
│   "讨论了数据库选型......" ← PostgreSQL 丢失]│
│ [第 13~19 轮:完整保留]                  │
│ [巨型工具输出:8000 tokens]              │
│ [第 20 轮用户输入]                       │
└────────────────────────────────────────┘
→ 模型无从知晓约定,输出 MySQL 代码

修复后:
┌────────────────────────────────────────┐
│ System: 你是一个编程助手                  │
│ ## 项目约定(长期有效,优先级最高)        │
│ - tech_stack:    PostgreSQL             │
│ - password_hash: bcrypt                 │
│ - api_style:     camelCase              │
│ [摘要历史(只占 300 tokens)]             │
│ [第 13~19 轮:完整保留]                  │
│ [第 20 轮用户输入]                       │
└────────────────────────────────────────┘
→ 约定每轮可见,且在最前面,永不失忆

注意修复后的两个变化:约定到了最前面 (解决注意力问题),摘要只占 300 tokens(解决预算问题)。两者一起做,才叫治本。


七、一个可直接抄的最小实现

把方案 ①②③ 合成一个 ContextBuilder,约 40 行,框架无关:

python 复制代码
from dataclasses import dataclass, field

RESERVE_FOR_OUTPUT = 2000   # 给模型输出留的余量


@dataclass
class FactStore:
    """关键约束的持久化存储:只存契约,不存闲聊"""
    facts: dict = field(default_factory=dict)

    def update(self, k, v):
        if self.facts.get(k) != v:
            print(f"[fact] {k}: {self.facts.get(k)} -> {v}")   # 变更留痕
        self.facts[k] = v

    def render(self) -> str:
        if not self.facts:
            return ""
        body = "\n".join(f"- {k}: {v}" for k, v in self.facts.items())
        return f"## 项目约定(长期有效,优先级高于一切历史对话)\n{body}"


def build_prompt(system: str, facts: FactStore, history: list,
                 user_input: str, budget: int):
    pinned_block = facts.render()

    # ① 先扣掉"不可压缩"的部分
    fixed = (count_tokens(system) + count_tokens(pinned_block)
             + count_tokens(user_input) + RESERVE_FOR_OUTPUT)
    room = max(budget - fixed, 512)

    # ② 工具返回先压缩(信息密度最低,最该先动)
    history = compress_tool_results(history, max_tokens=room // 3)

    # ③ 从新到旧贪心装入,装不下的做摘要
    rolling, used = [], 0
    for msg in reversed(history):
        n = count_tokens(msg["content"])
        if used + n <= room:
            rolling.insert(0, msg)
            used += n
        else:
            break
    dropped = history[:len(history) - len(rolling)]
    if dropped:
        rolling.insert(0, {
            "role": "system",
            "content": "【更早对话摘要】" + summarize(dropped, must_keep=[
                "技术选型", "数字与参数", "用户的要求与禁止事项"
            ])
        })

    return [{"role": "system", "content": system + "\n\n" + pinned_block}] \
           + rolling + [{"role": "user", "content": user_input}]

配套两个动作,缺一不可:

python 复制代码
# 动作 1:每轮提取契约(规则优先,规则抓不到再用 LLM)
new_facts = extract_facts_by_rule(user_input) or extract_facts_by_llm(user_input)
for k, v in new_facts.items():
    facts.update(k, v)

# 动作 2:每轮落盘,方便事后追责
json.dump(prompt, open(f"prompts/turn_{i:02d}.json", "w"), ensure_ascii=False)

最后加一条回归测试,堵住复发:

python 复制代码
def test_remember_turn3_at_turn20():
    """第 20 轮问第 3 轮的事,必须答得出来"""
    answer = run_conversation(FIXED_SCRIPT, turns=20, ask="我们用什么数据库?")
    assert "PostgreSQL" in answer
    assert "MySQL" not in answer

八、上线前 Checklist

每一条都对应一个真实的坑,逐项打勾:

可观测性

  • 有每轮完整 prompt 的落盘日志,且与界面展示内容区分开
  • 日志做了敏感信息脱敏
  • 有"校验探针"能看出失忆发生在第几轮

截断与裁剪

  • 裁剪按重要性而非条数
  • 关键约束放在不参与截断的常驻区
  • 消息过滤没有无差别删除 tool 返回
  • 先压工具返回,再摘要历史,最后才丢

压缩

  • 摘要 prompt 强制保留事实性内容(选型、参数、数字、禁令)
  • 契约本身不经过摘要器

存储与注入

  • 关键约定结构化外置,每轮注入 system prompt
  • 约定更新时,外部记忆同步更新(有变更日志)
  • Agent 框架的全局 State 里有 global_constraints 字段,不依赖局部变量
  • 长对话有分段管理与状态快照机制

测试

  • 有**"第 20 轮问回第 3 轮的事"**的自动化回归测试
  • 改过裁剪/摘要策略后,这条测试必须重跑

九、总结

记住这条决策树

排查永远从同一个动作开始------看这一轮实际发给模型的 messages 里,早期那条信息还在不在:

text 复制代码
关键信息在不在这一轮的 prompt 里?
│
├─ 不在
│   ├─ 固定轮次丢 ──────────→ 滑动窗口截断(第③步)
│   ├─ 变成一句概括 ────────→ 摘要失真(第④步)
│   ├─ 整条 tool 消息没了 ──→ 消息过滤误删(第③步)
│   └─ 从第 1 轮就没有 ─────→ 压根没入库 / 局部变量(第⑤步)
│
└─ 在,但模型不用
    ├─ 埋在中部被淹没 ──────→ 常驻区置顶(第⑥步 + 方案②)
    └─ 从没进过 system ────→ 每轮注入(方案①)

修复优先级

关键约束结构化外置 > 上下文分区 > 重要性感知的裁剪 > 摘要保底 > RAG 兜底

三句话收尾

  1. 加大窗口不是解法。 它只是把 20 轮推迟到 40 轮------结构错了,参数救不了。
  2. 摘要不是记忆。 摘要是"大意",而契约需要"原文"。让需要原文的东西绕开摘要器。
  3. 对话历史是易失的缓存,不是可信的存储。凡是重要的,就该在窗口之外也有一份。

如果你用的是 LangGraph,最省事的做法是把「全局常驻信息」单独放进一个 AgentState 字段,从架构上杜绝它被滑动窗口清掉。

相关推荐
阿里云云原生17 分钟前
让 Agent 成为企业生产力丨Alibaba Cloud AgentCore 企业智能体构建与治理沙龙·杭州站精彩回顾 & PPT 下载
agent
lucas_AI25 分钟前
AGENTS.md 烂了没人管?732 次实测:Coding Agent 每个会话都在替你付「文档债」利息
agent
泯泷42 分钟前
AI 最坑的失败:不是不会做,而是"假装做完了"
人工智能·llm·agent
阿里云云原生1 小时前
满帮基于阿里云 AgentCore 打造生产级 SRE 稳定性数字人
agent
去伪存真2 小时前
稠密检索负责脑补,稀疏检索负责兜底:拆解 RAG 倒排索引的“查字典”艺术
前端·agent
喵个咪4 小时前
GoWind Admin 拆解:AI 能力层——工具循环、pgvector 知识库与机器凭证
人工智能·后端·agent
用户2018792831675 小时前
失忆顾问的会前资料袋:扒开四大 Coding Agent 的 Context,看 LLM 到底"看见"了什么
agent
走心哦5 小时前
多模型研讨 Agent 实战:让 GPT、Claude 互相挑错(附 3 个坑)
agent
泯泷5 小时前
大模型会吃掉 Harness 吗?Agent 天空上的“两朵乌云”
人工智能·llm·agent