多轮对话为什么不是简单把聊天记录存数据库

做 AI Chat 时,一个很自然的想法是:用户发一条消息,助手回一条消息,把它们存进数据库。下一次用户继续聊,就把历史记录取出来。

这当然需要做,但它只解决了“记录保存”的问题,没有解决“模型应该看什么”的问题。

多轮对话真正复杂的地方在于:LLM API 通常不会自动记住之前发生过什么。每次调用模型时,应用都要决定把哪些上下文重新发给模型。数据库里的聊天记录只是原材料,真正影响模型回答的是本次请求组装出来的上下文。

所以,多轮对话不是简单的消息表 CRUD,而是上下文工程。

数据库存的是历史,不等于模型看到的上下文

聊天记录数据库通常保存:

  • 会话 ID。
  • 用户消息。
  • 助手消息。
  • 创建时间。
  • 模型信息。
  • 消息状态。
  • 附件或文件引用。
  • 工具调用记录。
  • 错误和重试信息。

这些数据用于页面展示、会话恢复、审计、搜索和用户体验。

但模型不会直接读取你的数据库。模型只会处理你本次 API 请求里传进去的内容。

也就是说,数据库里的历史可能有 100 条,但你这次只传了最近 8 条,模型就只能参考这 8 条。你把所有 100 条都传进去,模型才可能参考全部历史,但这又会带来 Token、成本、延迟和干扰问题。

存储历史和使用历史,是两件不同的事。

每次请求都要重新构造上下文

一次多轮对话请求通常不只是当前用户消息。

应用可能会组装:

  • 系统提示词。
  • 当前用户消息。
  • 最近几轮对话。
  • 对话摘要。
  • 用户长期偏好。
  • 当前页面状态。
  • 文件内容或检索片段。
  • 工具列表。
  • 工具执行结果。
  • 输出格式要求。

这些内容组合在一起,才是模型真正看到的上下文。

这意味着你需要一个上下文组装策略。它要回答几个问题:

  • 哪些历史消息必须保留原文?
  • 哪些历史消息可以丢弃?
  • 哪些历史应该总结成较短内容?
  • 哪些用户偏好应该作为长期记忆注入?
  • 当前任务是否需要文件、页面状态或检索结果?
  • 工具调用结果应该保留多久?
  • 如果上下文超长,优先裁剪什么?

这些决策不能靠“从数据库查出全部消息然后拼起来”解决。

聊天记录会迅速超过上下文窗口

在 Demo 里,多轮对话可能只有三五轮。到了真实产品里,用户会持续追问、修改、让模型重写、上传文件、要求解释细节。

很快会遇到问题:

  • 历史消息越来越长。
  • 助手回答比用户输入更长。
  • 文件内容和检索结果占据大量 Token。
  • 工具调用过程产生额外上下文。
  • 系统提示词和输出格式约束每次都要带上。

上下文窗口再大,也不是无限的。即使没有超过模型限制,长上下文也会增加成本和延迟,并且可能降低回答质量。

因此,多轮对话必须有裁剪策略。

常见策略包括:

  • 只保留最近 N 轮。
  • 保留最近消息,同时保留一份对话摘要。
  • 按重要性保留关键消息。
  • 删除失败重试、寒暄、重复确认等低价值消息。
  • 对工具结果做压缩,只保留结论和关键 ID。
  • 对长文档使用检索,而不是把全文一直放进上下文。

裁剪不是简单删除,而是在预算内保留当前任务最需要的信息。

历史消息可能干扰当前任务

很多人以为上下文越完整越好,但多轮对话里,历史也可能成为噪音。

例如:

  • 用户前面让模型用英文回答,现在希望改成中文。
  • 前面讨论的是 A 项目,现在切换到 B 项目。
  • 早期消息里有错误假设,后面已经被纠正。
  • 助手之前生成过不准确内容,如果继续传入,模型可能沿用错误。
  • 用户曾经随口提到一个偏好,但当前任务并不适用。

模型会受到上下文影响。你传进去的历史越多,就越需要处理冲突、过期和低相关信息。

这就是为什么上下文组装要关注“当前任务相关性”,而不是机械保留全部历史。

多轮对话还包含工具和状态

现代 AI 应用里的多轮对话,经常不只是用户和助手两种消息。

还可能包含:

  • 工具调用请求。
  • 工具执行结果。
  • 文件上传状态。
  • 检索命中的文档片段。
  • 前端页面选区。
  • 用户确认或拒绝操作。
  • 后端任务进度。
  • 中断、重试和继续生成。

这些内容是否进入下一次上下文,需要谨慎设计。

例如,工具返回了一大段订单详情。为了页面展示,你可能要完整存储它;但下一次发给模型时,也许只需要订单号、状态、金额和异常字段。再例如,用户上传的 PDF 可以存为文件引用,真正问答时通过检索拿相关片段,而不是每轮都把 PDF 全文塞回去。

数据库负责保存事实,模型上下文负责支持当前生成。两者的粒度和形态不一定相同。

前端工程师的工程影响

前端在多轮对话里不只是渲染消息列表。

首先,前端要表达上下文状态。用户需要知道当前对话是否继承历史、是否引用了某个文件、是否基于某个页面选区、是否已经丢弃或总结了较早内容。

其次,前端要处理消息状态。AI 消息可能处于生成中、已完成、失败、已取消、重试中、部分输出、工具执行中等状态。简单的 messages.map() 远远不够。

再次,前端要支持用户修正上下文。用户可能需要编辑上一条消息、删除某段历史、切换文件、重新生成、从某条消息分叉出新会话。

最后,前端要理解“显示历史”和“发送上下文”不是同一个数组。页面可以展示完整历史,但请求时只发送经过后端策略处理后的上下文。

这对状态管理、交互设计和 API 协议都会产生影响。

实践建议

第一,把消息存储模型和上下文构造模型分开设计。

消息表可以完整记录会话历史,但请求模型前应该有单独的 context builder,负责根据当前任务组装上下文。

第二,为消息增加类型和状态。

不要只用 rolecontent 处理所有情况。真实系统通常需要区分用户消息、助手消息、系统事件、工具调用、工具结果、附件引用、错误状态和生成状态。

第三,从一开始记录 Token 用量。

每次请求记录输入 Token、输出 Token、保留了哪些历史、裁剪了哪些内容。否则很难调试“为什么这轮贵”“为什么模型忘了前文”“为什么回答被旧内容影响”。

第四,使用摘要而不是无限历史。

长会话可以维护一份滚动摘要,把较早对话压缩成当前任务仍然需要的背景。摘要不是完美记忆,但比无限拼接历史更可控。

第五,让用户能感知上下文边界。

如果旧消息被总结、文件未被引用、上下文过长导致部分历史被省略,产品应该用合适的方式提示。用户不知道模型看到了什么,就很难建立稳定预期。

第六,针对不同场景使用不同策略。

客服对话、写作助手、代码协作、文档问答、Agent 工作流,对历史的依赖不同。不要用同一个“最近 N 条消息”策略解决所有问题。

常见误区

误区一:把聊天记录存下来,就等于模型有记忆。

数据库保存的是历史,模型记住的是本次请求里的上下文。没有被重新传入或检索注入的内容,模型无法可靠使用。

误区二:每次都传全部历史最安全。

全部历史会增加成本和延迟,也可能引入冲突和噪音。更长不一定更准确。

误区三:只保留最近 N 条就足够。

最近消息不一定最重要。用户早期给出的目标、约束、姓名、项目背景,可能比最近几句寒暄更关键。

误区四:摘要可以完全替代原文。

摘要会丢信息,也可能引入偏差。关键指令、事实、代码、数字、用户确认等内容,有时需要保留原文或结构化保存。

误区五:前端只需要展示消息,不需要理解上下文。

前端负责用户如何输入、修改、继续、重试、上传、引用和理解模型行为。上下文策略最终会体现在交互体验里。

小结

多轮对话不是简单把聊天记录存进数据库。数据库解决的是持久化,模型调用需要的是经过选择、裁剪、总结和补充的上下文。

真正可用的 AI Chat 需要同时设计消息存储、上下文组装、Token 预算、历史裁剪、摘要记忆、工具状态和前端交互反馈。只有把“显示给用户的历史”和“发送给模型的上下文”区分开,多轮对话才会从 Demo 走向可维护的工程系统。