AI 应用后端工程化:从原型到可交付系统 · 第 04 篇 · 第一章 · 服务基础
从会话历史和 Token 缓冲区出发,理解模型为什么没有天然记忆,以及应用应该保留什么。
用户问“那第二种方案呢”,模型却不知道“第二种”指什么,最直觉的解释是模型失忆了。严格来说,模型从未拥有过这段记忆:每次调用都是一次新的计算,只有应用重新发送的消息才会进入它的视野。
因此,对话记忆不是模型特性,而是应用的数据选择问题。系统要决定保存哪些消息、下一轮发送哪些内容、窗口装不下时舍弃什么,以及不同会话之间如何隔离。
先把保存和发送分开
数据库可以保存完整聊天历史,但不代表每次都要把全部历史发送给模型。前者服务于审计、界面和恢复,后者受上下文窗口、成本和相关性限制。
flowchart LR
History[(完整历史)] --> Select[相关性选择]
Profile[(长期事实)] --> Select
Current[当前问题] --> Select
Select --> Budget{Token 预算}
Budget --> Recent[近期原文]
Budget --> Summary[早期摘要]
Budget --> Facts[关键事实]
Recent --> Prompt[本轮上下文]
Summary --> Prompt
Facts --> Prompt
一个实用分层是:最近几轮保留原文,较早内容压缩成摘要,明确的用户偏好或任务事实单独存储。这样既能保持局部语气,又不会让一百轮寒暄挤掉当前任务所需的资料。
Token 缓冲区在做什么
按消息条数截断很容易出错。一条包含长文档的消息可能比二十条短对话更大,因此更可靠的限制单位是 Token。缓冲区会从最新消息向前累计,接近预算时停止;还要为系统提示、工具定义和模型输出预留空间。
def select_recent(messages, count_tokens, budget: int):
selected = []
used = 0
for message in reversed(messages):
cost = count_tokens(message["content"])
if used + cost > budget:
break
selected.append(message)
used += cost
return list(reversed(selected))
这只是最基础策略。它保护了窗口,却可能在边界上切掉重要前提。例如用户先规定“所有金额使用人民币”,几十轮后这条消息被淘汰,模型便会恢复默认表达。进一步设计通常会把稳定规则从普通历史中提取出来,作为结构化事实单独注入。
预算也不应等于模型最大窗口。假设模型支持 128K,上下文全部塞满会增加费用和延迟,信息之间还会互相干扰。工程目标不是“尽量装满”,而是“用足够少的信息支持本轮判断”。
会话 ID 是隔离边界
记忆实现中最危险的错误不是少记一句,而是串会话。若缓存键只使用用户 ID,同一用户同时打开两个任务,它们的上下文会混在一起。若连用户 ID 都没有,不同用户甚至可能读到彼此消息。
一个记忆键至少要表达租户、用户、应用和会话。服务端不能完全相信客户端传来的会话 ID,还要验证当前身份确实拥有该会话。删除会话、导出记录和继续对话,都需要走同一所有权检查。
摘要会损失信息
摘要不是无损压缩。模型可能忽略一个数字、把未决定事项写成已确认事实,或者在多次滚动摘要后逐渐偏离原文。适合摘要的是背景和进度,不适合只保留摘要的是审批结果、法律条款、精确参数和工具执行证据。
可以让摘要带结构,例如“目标、已完成、约束、待决定、证据链接”,并保存它覆盖的消息范围。出现争议时仍能回看原文。对重要状态,数据库字段和事件记录比自然语言摘要更可靠。
常见的伪记忆
最简单的实现把全局数组当聊天历史。单机演示看似正常,一旦多进程部署,每个进程拥有不同数组;服务重启后历史消失;并发请求还可能交叉写入。另一个常见问题是把工具返回的几万字原样保留,每轮都重复付费。
正确方向是明确生命周期:请求内的临时状态放内存,会话状态存可持久化介质,长期事实需要独立更新策略,大结果保存外部引用和短摘要。不同生命周期不应由一个叫 memory 的万能字典承包。
从聊天迁移到别的系统
这套思想同样适用于购物车、表单草稿和分步任务。完整事件用于恢复,当前视图只投影必要状态;稳定事实与短期交互分开;每个状态都绑定明确所有者。所谓上下文工程,本质上与传统系统的状态管理一脉相承。
练习可以准备一段十轮对话,其中第一轮包含不可丢失的约束,第六轮包含一篇长文。分别使用“保留最近五条”和“Token 缓冲加关键事实”生成下一轮输入,比较是否仍能正确回答。把最终发送给模型的消息列表保存下来,而不是只记录答案。
两次提交展示的转折
记忆一旦进入执行链,问题就从“有没有历史”升级为“历史属于谁、占多少预算、怎样失效、能否恢复”。越早回答这些问题,越不容易在数据增长后被迫重写整个会话层。