判断“聊到第十轮就忘了开头”这件事,先别急着调上下文窗口,也先别假定长期记忆坏了。更稳的做法是把这句话拆成两条链路:一条是会话上下文,客户端每轮把历史消息拼进请求;另一条是跨会话记忆,模型之外的存储异步写入、下一轮再检索注入。然后用“开头那句偏好在第几轮第一次不再被引用”这个可观测的轮次,定位问题落在哪一层。只有轮次确定下来了,改动才有归因价值;否则同时调窗口和记忆阈值,之后无论好坏都说不清是哪一处起的作用。
先别动参数:把开头那句偏好做成可判定的探针,逐轮记录它第一次不被引用发生在第几轮;再关掉长期记忆复跑一遍。失效轮次不变,问题在会话上下文的拼装与截断;失效消失,问题在长期记忆的写入条件或检索注入。
先记录开头那句话在第几轮开始失效
“忘了”是个模糊感受,需要先变成轮次号。做法是在会话第一句放一条可判定的偏好,比如“回复一律用无序列表,不要用表格,称呼我为老周”。选它是因为行为可验证:返回里有没有用列表、有没有用表格、有没有出现称呼,一眼能看出来,不依赖模型自称“我记得”。
接着按固定序列提问,每轮只问一个问题,避免一个问题里塞多个诉求导致无法判断是哪句丢了。记录方式建议直接用一张表或一个 CSV 文件,字段固定为:轮次、提问内容、返回里是否体现开头信息、证据片段(把返回中体现或不体现的那一句原样抄下来)、备注。证据片段这一列不能省,它决定了后面复跑时能不能做前后对比。
turn, question, used_opening, evidence_quote, note
1, 帮我列三个做笔记的工具, yes, "按你的要求用列表写", 正常
2, 这三个怎么选, yes, "老周,建议先看第二点", 称呼仍在
3, 换个角度再说说, no, 直接给了表格, 首次失效?
判定标准建议写死:只要返回里出现了被明确禁止的形式(表格),或者称呼消失,就记为 not used。连续两轮不复现才算稳定失效,单轮波动先记为待观察,避免把一次措辞变化当成链路故障。
分清会话上下文和跨会话记忆两条链路
这两条链路经常被混成一句“记忆不好使”,但它们的证据位置完全不同。会话上下文是每次请求时客户端自己拼的,你能在请求体里看到全部内容;跨会话记忆是外部存储加检索注入,你在请求体里只能看到被检索回来的片段被拼进去的那一部分。分开看,才能知道该查日志还是查存储。
| 对比项 | 会话上下文 | 跨会话记忆 |
|---|---|---|
| 写入时机 | 每轮请求前由客户端把历史消息追加进 messages | 某一轮结束后(通常是异步)由服务抽取事实写入存储 |
| 生命周期 | 跟随当前会话,受窗口上限和截断策略约束,会话结束即散 | 跨会话保留,可被后续任意会话检索到,受作用域(用户/会话/助手)约束 |
| 可观察位置 | 请求体的 messages 条数与长度、服务端请求日志、token 计数、是否有截断标记 | 记忆列表接口或管理页面的写入记录、检索命中片段、注入位置在 prompt 中的那一段 |
对照这张表看第一步的记录表:如果失效轮次大致与历史消息被截断的位置吻合(通常是 token 到上限附近,或者历史只保留最近 N 轮),嫌疑在会话上下文;如果失效轮次和截断位置对不上,或者从第一轮起就没生效,嫌疑在写入或检索。
只留当前会话、关掉长期记忆再跑一遍
这一步是为了把两条链路物理隔离。配置位置一般在两处之一:客户端或网关的全局记忆开关,或者每次请求里的记忆字段。不同实现的字段名不一样,常见的是 memory.enabled、use_memory、long_term_memory 之类,先在配置文件或请求构造代码里搜 memory 关键字确认自己的名字,不要照抄别人的字段名。
POST /v1/chat
{
"session_id": "probe-20240101-a",
"memory": { "enabled": false },
"messages": [
{ "role": "user", "content": "回复一律用无序列表,不要用表格,称呼我为老周" },
{ "role": "user", "content": "帮我列三个做笔记的工具" }
]
}
复跑步骤建议按顺序做:新建会话(不要复用旧 session_id,否则旧历史会带进来)、确认记忆开关为关闭、用完全相同的开头句和提问序列重跑、记录到第二张表、把两张表的失效轮次并排放。同时检查请求体里 messages 的实际长度,看在失效那一轮前后,历史是不是被裁掉了开头的系统提示或第一条用户消息。
结果判读:关掉长期记忆后失效轮次明显提前或消失不了但位置和第一步一致,说明问题在会话上下文;关掉后开头信息反而一直保留到更后面、或者问题消失,说明原本的失效是被记忆链路里的某些内容干扰,或者记忆开关根本没起作用、需要先确认配置是否真的生效。
打开长期记忆,检查写入触发条件
如果关掉长期记忆后问题消失,就要回到写入侧。多数实现的记忆写入不是每轮都触发,而是一组条件同时满足才抽事实入库。逐项核对下面这张清单,字段名不确定的先用占位符在自己的配置里找对应项。
- 最小轮次 {min_turns}:会话是否达到触发写入所需的轮数再聊的,第一轮就期待它记住可能本来就不成立。
- 重要性阈值 {importance_threshold} 或评分模型:开头那句偏好是否被判为“值得记”,格式偏好、称呼类信息常被判定为低价值。
- 显式记忆指令:是否需要用户说“记住这一点”才写入,如果是,测试时加一句显式指令做对照。
- 消息长度下限 {min_message_length}:过短的首句可能被跳过。
- 作用域 {scope}:写入的是 user 级还是 session 级,session 级在关闭会话后就查不到。
- 异步延迟 {write_delay}:写入是后台任务,聊完立刻查存储可能还没落库。
- 去重与覆盖策略:同一事实重复写入是否被合并,或旧的被新的覆盖。
验证顺序是:先聊够轮次,再去记忆列表接口或管理页面查这条偏好是否存在。查不到,问题在写入侧,改触发条件;查得到但下一轮请求里没有被拼进 prompt,问题在检索侧,改检索参数或相似度阈值,而不是继续调写入阈值。
确定层次后只改对应那一层的参数
一次只改一项,这是能不能归因的前提。会话上下文层可动的是历史保留轮数、窗口上限、是否保留首条系统消息;记忆层可动的是最小轮次、重要性阈值、检索条数、相似度门槛。两类参数混着改,复测结果再好也无法说明是哪一处修的。
| 改动项 | 旧值 | 新值 | 复测轮次 | 开头信息是否被引用 | 备注 |
|---|---|---|---|---|---|
| 历史保留轮数 | 6 | 12 | 1-15 | 第 11 轮起失效 | 失效点后移,未消除 |
| 最小写入轮次 | 5 | 2 | 1-10 | 稳定引用 | 仅此一项生效 |
风险边界也要写进记录:加大历史保留会推高每轮请求的体积和成本,长会话里更容易撞上窗口上限;调低重要性阈值会让大量琐碎信息入库,检索时更容易命中噪声,表现为回答里出现与本轮无关的旧内容。改动后除了看开头信息是否被引用,也要顺手看一眼返回里有没有混进不属于当前话题的片段,两边一起判断才算完整。