同样的对话里,用户从模型 A 切到模型 B 后,第二句提问完全接不上前面的内容。这类现象在使用 Spring AI 这类框架构建多模型应用时并不少见,但直接去改某个全局配置项往往解决不了问题。因为“上下文丢失”通常不是一个配置开关的问题,而是一个状态管理边界的问题。
先建立一个最基本的判断前提:主流的对话补全接口是无状态的。模型不会替调用方保留会话历史,每一次请求都要由应用层把完整的消息列表带过去。所谓上下文,是应用层构造出来的逻辑对象。既然模型端没有隐式记忆,切换模型后上下文丢失,问题必然出在应用层的某个环节,而不是出在模型本身。
很多团队的第一反应是寻找某个“官方配置项”来开启跨模型记忆。这类配置在架构层面的作用非常有限。真正可靠的记忆机制,只能建立在应用自己的存储层上。这个判断与具体框架无关,也与具体模型无关。
拆解状态链路,找出断点
一个完整的多模型切换流程可以拆成五段:
用户输入 → 读取该会话历史 → 按目标模型转换消息格式 → 提交请求 → 把本次问答写回历史存储
这五段里任何一段出错,最终表现都是“上下文丢失”,但症状略有差异,对应不同的排查方向。
| 症状 | 最可能出错环节 | 排查点 |
|---|---|---|
| 切换后完全不记得之前内容 | 历史存储错位或会话标识断裂 | 会话 ID 是否一致;历史是否重新从独立存储加载 |
| 普通对话记得,工具调用结果不记得 | 消息格式映射不完整 | 工具请求与工具结果在历史中是否成对保留 |
| 系统指令突然失效 | system 消息未随切换传递 | 发送前检查 system 消息是否存在且数量为一条 |
| 回答整体退化但没有完全失忆 | 截断策略不一致 | token 预算变化后,首条消息是否被截成残留片段 |
这里可以做一个明确的工程判断:多模型切换时的上下文丢失,按根因划分只有三类——状态存储错位、消息适配不完整、截断策略不一致。先判定属于哪一类,再动手改代码,比逐个尝试配置项更有效。
修复核心:状态与客户端分离
很多问题案例的共同点是:会话历史被保存在某个模型客户端对象内部。一个客户端对应一个模型,切到新模型时创建了新客户端,旧客户端里的历史自然就“丢”了。
修复的方向不是切换时做数据搬运,而是从结构上解除耦合:让模型客户端保持无状态,把消息历史统一放在应用自己的会话存储中。切换模型只改变一个决定——这次的请求把历史消息提交给哪个目标模型。
下面是一段不绑定任何框架 API 的通用示意:
session = store.load(sessionId)
history = session.messages
requestMessages = adapter(targetModel).fromInternal(
history + userMessage(sessionId, text)
)
response = client(targetModel).complete(requestMessages)
store.append(sessionId, userMessage, response)
这个结构有三个约束:
- 每个模型客户端不保存会话状态,可被安全创建和销毁;
- 历史消息以应用自定义的规范结构保存,不使用某个模型的私有格式;
- 每次请求都从存储层重新加载历史,而不是从上一个客户端对象里读取。
做到这三点之后,切换模型就退化为一个适配器路由问题,上下文丢失的主要来源被直接切开。
重构时也不需要一步到位。如果现有代码已经把历史消息存在客户端内,可以先在切换点增加一个显式的历史导出/导入动作,验证数据没有丢,再把存储位置逐步迁移到独立会话仓库。中间态可以保留,但必须保证只有一个数据源,避免双写不一致。
适配器的职责包括:角色枚举映射、system 消息整理、工具调用消息重建、顺序与时间字段的兼容处理。它应当是纯函数,输入应用内部消息列表,输出目标模型可接受的消息列表,不要在适配器里读写会话状态。
三个容易被忽略的边界条件
即使整体结构正确,切换时仍有三处细节会造成上下文不完整。
system 消息的携带。 不同模型对 system 消息的表达方式不完全一样,有的要求它固定在消息列表首位,有的把它当作普通消息处理。切换模型时,如果适配逻辑没有显式保留 system 消息,它很容易在格式转换中消失。建议在提交前检查目标请求里 system 消息存在且只有一条。
工具调用历史的成对性。 发生过工具调用的对话,历史里包含两类特殊消息:模型请求调用工具的 assistant 消息,以及工具返回结果的 tool 消息。这两类消息在不同模型之间的字段结构差异很大。如果映射后工具结果缺失或顺序错误,模型会认为工具调用没有发生过。这类消息是多模型切换时最容易暴露映射问题的部分,排查时应优先检查。
截断后的首条消息完整性。 历史超出 token 预算时需要截断。简单按条数删除最早消息,消息列表可能以一条残缺的 assistant 或 tool 消息开头,这类“不完整消息对”容易让后续请求出现异常。截断策略应保证列表第一条是完整的 system 或 user 消息,必要时成对丢弃半截的 assistant/tool 消息。
把场景固化成回归验证
上下文丢失这类问题,单靠上线后人工验证很难覆盖所有切换组合。建议把固定场景固化成回归测试,不依赖特定模型。
固定一个测试会话,包含一条 system 消息、三轮问答、一次工具调用。执行切换序列:模型 A → 模型 B → 模型 A。每次切换后断言:
- 会话 ID 保持不变,历史消息条数连续递增;
- system 消息始终只有一条,且位置未被改变;
- 向模型询问“你刚才调用的工具返回了什么”,模型能正确引用工具结果;
- 每次请求实际提交的消息条数和 token 数符合预期。
最后一点尤其重要。上下文丢失不总是“完全不记得”,有时只是截断导致的部分退化。如果不记录请求载荷,这种无声退化很难被察觉。建议把 sessionId,以及提交前的消息条数、token 数一起写入日志或链路追踪。用户反馈上下文异常时,可以快速判断问题发生在读取阶段、转换阶段还是提交阶段。
对框架内置记忆组件如何判断
Spring AI 这类框架往往会提供封装好的消息记忆能力(无论官方命名是 ChatMemory 还是其他叫法,本质都是消息历史的存取与注入)。在用它支撑多模型切换前,可以用四个问题验收:
- 记忆是否绑定在某个模型客户端上?绑定即意味着切换即丢;
- 是否按会话 ID 隔离?不能隔离会导致不同会话互相串话;
- 切换模型时,历史消息是否经过同一个格式归一化入口;
- 截断策略是否在所有目标模型间保持一致。
如果四个问题中有一个答不上来,更稳妥的做法是在自己的存储层接管会话状态,而不是依赖框架组件的默认行为。这里真正值得关注的是数据流,不是 API 封装。
多模型切换时的上下文丢失,本质上不是模型之间的兼容问题,而是应用层对会话状态管理边界的选择问题。把历史存储、消息适配和截断策略这三条边界理清楚,稳定可验证的结构会取代一个又一个的临时补丁。