
做 AI 应用的同学应该都有过这种经历模型接口明明只收一个 prompt但一问多轮问题就开始失忆昨天聊的配置今天再问它一脸茫然甚至同一个会话里稍微绕个弯它就把前面的结论忘了。我在不少项目里都被这个问题折磨过最后基本都绕回到同一个解法上——给应用加一个 context-mode把上下文当成第一等公民来设计。这篇文章就把我在实际项目中做 context-mode 的完整思路、代码结构和踩坑记录整理出来给正要动手做对话式 AI 功能的你一个可参考的底稿。1. 什么场景下你才真正需要 context mode1.1 无状态接口的天然缺陷现在的 LLM 接口本质上是一个无状态函数你传一段 text 进去它给你吐一段 text 出来。它不记得你上次问了什么也不关心你之前做过什么。单轮问答没问题但只要业务逻辑是多轮对话连续操作渐进式澄清无状态就成了硬伤。我最早做的一个客服问答机器人就是典型反面教材。用户问你们的退款政策是什么机器人回答了一长串用户接着问那退货呢机器人又开始从零解释什么是退货。用户当然觉得它蠢因为正常人对话里那退货呢指的是在刚才那个退款政策背景下退货怎么处理。但接口不这么认为它只看到一句孤零零的话。这就是需要 context mode 的第一个信号你的交互存在指代省略承接这类自然语言现象。你没法要求用户每次都把问题说完整只能让系统自己去维护前面的对话脉络。1.2 context mode 和普通对话模式的本质差异很多人以为 context mode 就是把历史聊天记录拼到 prompt 里这个理解不能说错但太浅了。真做起来你会发现它至少包含三层工作记忆把多轮消息按某种结构保存起来而不是只留一个字符串。筛选每次请求前决定哪些历史信息值得带进 prompt哪些可以丢。压缩当历史太长超出模型窗口时用摘要、结构化笔记等方式保留关键信息。普通对话模式是来一句答一句context mode 是来一句先想这句和之前哪些信息有关再带着这些信息去回答。后者才是用户体感上聪明的来源。什么时候必须上 context mode我的判断标准很简单如果产品里出现用户需要重复提供信息才能继续的反馈或者测试时出现同一个事实前后矛盾的情况那就别再犹豫了直接按本文的方案做一版 context 层。2. 上下文不只是把历史消息塞回去——三层上下文结构2.1 短期上下文会话窗口内的消息管理短期上下文就是最近几轮对话的原始消息。它的作用是给模型提供刚刚发生了什么的即时信息让回答在局部保持连贯。这里有个容易被忽略的细节保存的粒度不应该是整个字符串而应该是结构化消息列表。每一条消息至少要有 roleuser / assistant / system / tool、content、timestamp、message_id 这几个字段。我见过不少项目图省事直接把{user: xxx, ai: yyy}拼成字符串存 Redis结果后面要做按时间过滤、按角色筛选、按消息 ID 精确拼接时全部抓瞎。结构化的好处是你可以自由决定这次请求带哪些消息、以什么顺序带、要不要带某个中间结果。这个灵活性在后面做上下文裁剪时非常关键。短期上下文的容量一般用轮数或token 数来控制。我习惯用 token 数做上限比如 4000 token因为不同用户消息的长度方差很大按轮数控制经常出现一轮就把窗口塞满的极端情况。2.2 中期上下文摘要与记忆压缩中期上下文解决的是窗口放不下的问题。假设窗口上限 8000 token短期上下文只能放最近 4000 token 的原始消息那再往前的内容怎么办直接丢掉会让用户觉得模型失忆全留着又会爆窗口。我的做法是滚动摘要。每积累到一个阈值比如满 3000 token就把这部分消息送去模型生成一段摘要摘要里保留用户的核心诉求、已经确认的事实、尚未解决的问题。然后把这部分原始消息从短期上下文中移出只保留摘要作为此前谈话的浓缩版。这个方法的效果相当好。用户问我之前说的那个需求你还记得吗模型可以从摘要里捞出来用户表达我不是这个意思是另一个模型也能通过摘要理解前后差异。代价是每轮多一次摘要生成调用但对绝大多数应用来说这个成本换来的连贯性非常值。2.3 长期上下文用户画像与业务知识长期上下文是很多人一开始不会想到、但真正决定产品上限的部分。它和本次会话无关而是跟这个用户/这个业务有关。举个例子用户A是制造业客户每次提问都关心交期用户B是电商客户最在意退换货。如果 context mode 只维护会话内信息换一个新会话模型又把用户A当陌生人对待。所以我在长期上下文里会维护一份轻量的用户画像内容来自历史会话的关键事实抽取比如用户所在行业关注点已购产品编号等。这层信息不一定每次都塞进 prompt而是在用户发起新会话时作为 system 消息的一部分注入。它让模型在一开始就知道在跟谁说话多轮连贯性和个性化体验都会上一个台阶。三层上下文合在一起才是完整的 context-mode 数据底座短期保连贯中期保记忆长期保个性。下面讲讲具体怎么落地。3. 一个可落地的 context-mode 实现方案3.1 核心数据结构与接口设计先定义最小可用的数据结构。我通常用一个ConversationStore类来管理会话内部维护短期消息列表和中期摘要from dataclasses import dataclass, field from typing import Dict, List, Optional import time import uuid dataclass class Message: role: str # user / assistant / system / tool content: str timestamp: float field(default_factorytime.time) message_id: str field(default_factorylambda: uuid.uuid4().hex) dataclass class Conversation: conversation_id: str messages: List[Message] field(default_factorylist) summary: str # 滚动摘要 user_profile: Dict[str, str] field(default_factorydict) current_token_usage: int 0接口层面最少需要三个方法class ConversationStore: async def add_message(self, conversation_id: str, role: str, content: str) - None: ... async def build_prompt(self, conversation_id: str, system_prompt: str) - str: ... async def compress_if_needed(self, conversation_id: str, threshold: int) - None: ...add_message负责追加消息并更新 token 统计build_prompt负责把三段上下文拼成最终发给模型的 promptcompress_if_needed负责在超阈值时触发摘要压缩。三个方法职责单一后续扩展比如接检索、接记忆库都有清晰的位置。3.2 上下文裁剪与压缩策略裁剪策略直接决定 prompt 质量和成本。我逐步摸索出一套按优先级保留的规则实践中效果比较稳定系统指令永远保留包括角色设定、业务规则、输出格式。用户最近一条消息永远保留这是本次要回答的问题。最近 N 轮原始对话保留保证局部连贯。中期摘要保留当原始对话超出预算时优先砍更早的原始消息。长期画像按需注入只在能明显提升回答质量时加入。压缩触发条件我用两个指标总 token 数超过窗口的 60%或原始消息条数超过某个阈值比如 30 条。一旦触发就调用一次摘要模型async def _summarize_messages(messages: List[Message]) - str: prompt ( 请将以下对话压缩为一段中文摘要保留用户的核心诉求、 已确认的事实、尚未解决的问题。不要遗漏关键细节。\n\n \n.join(f{m.role}: {m.content} for m in messages) ) # 调用 LLM返回摘要文本 return await llm_complete(prompt, max_tokens512)压缩完成后把被压缩的消息从列表里移除把summary更新为旧的摘要新的摘要合并结果。需要注意摘要本身也在累积所以摘要也要有上限超过就再做一次摘要的摘要。3.3 关键代码实现与调用链路完整的调用链路是这样的用户发消息 → 追加到消息列表 → 计算 token → 判断是否压缩 → 构建 prompt → 调用模型 → 把回答追加进消息列表。核心实现如下async def build_prompt(self, conversation_id: str, system_prompt: str) - str: conv self._get_conv(conversation_id) parts [f【系统指令】\n{system_prompt}] # 注入长期画像 if conv.user_profile: profile_text \n.join(f{k}: {v} for k, v in conv.user_profile.items()) parts.append(f【用户背景】\n{profile_text}) # 注入中期摘要 if conv.summary: parts.append(f【此前对话摘要】\n{conv.summary}) # 注入短期原始消息最近 10 条 recent conv.messages[-10:] if recent: history_text \n.join(f{m.role}: {m.content} for m in recent) parts.append(f【最近对话】\n{history_text}) return \n\n.join(parts)这里有几个工程细节值得说明。第一recent conv.messages[-10:]不是写死 10而是根据 token 预算动态计算能放多少条第二build_prompt只负责组装真正的 token 计算在add_message时就完成了避免每次拼接再做一次 tokenize第三所有操作都要有conversation_id做隔离多用户并发时互不干扰。4. 实测反馈与最容易踩的坑4.1 上下文污染问题这是我在多个项目里都遇到过的头号问题而且非常隐蔽。什么叫上下文污染就是 prompt 里同时存在矛盾的信息模型不知道该信哪个于是回答变得混乱。最经典的场景是用户中途改主意。比如用户先说我要买蓝色那款过了五轮又说算了还是红色吧。此时短期上下文里有两条冲突的消息中期摘要里可能还留着用户已确认买蓝色。模型看到所有历史很可能给出一个自相矛盾的回复甚至帮用户协调两个颜色。我在实盘里总结出的对策是在add_message时做一次冲突检测发现用户对同一实体的属性描述变了就主动更新摘要和画像里的旧值。更简单的兜底方案是在构建 prompt 时把消息按时间正序排列并在摘要末尾加一句如摘要与近期消息冲突以近期消息为准。这句话能显著降低模型的困惑代价几乎为零。4.2 窗口管理的边界情况第二个高频坑是窗口管理在极端输入下的崩溃。你以为 60% 阈值很安全但用户一次性贴了一篇 5000 字的文档进来直接把窗口撑爆。这种情况不能靠压缩摘要来救因为摘要模型本身也需要输入空间。我的处理是给单条消息设硬上限超过窗口 30% 的消息先做一次抽取式截断——保留首尾各 20%中间部分让摘要模型单独处理。虽然会损失一些中间细节但比整个会话崩掉强得多。另一个边界问题是工具调用function calling产生的长参数。模型调用工具时工具返回的内容往往非常大比如查数据库返回几百行这些内容一旦进入消息历史token 消耗极其惊人。我的经验是工具结果不直接进短期上下文而是先压缩成结论性描述比如查询返回 235 条记录其中符合条件是 17 条主要分布在华东区再存进消息列表。这样既保留了关键信息又不会让窗口快速膨胀。4.3 多轮场景下的评测方法很多团队在做 context mode 时最大的困惑不是实现而是怎么知道做得好不好。没有评测你只能靠感觉调参上线后发现问题又不知道是哪一层出的错。我用的评测方案是三段式第一段是指代消解测试。准备一组多轮对话在某一轮故意使用指代词比如那它大概什么时候能到看模型能否正确理解它指代的是之前提到的商品。第二段是事实一致性测试。在对话第 3 轮抛出一个事实第 8 轮再问同一个事实看模型两次回答是否一致。这里要注意一致性不是要求逐字相同而是核心信息不矛盾。第三段是抗干扰测试。在对话中插入一段无关闲聊再回到主线问题看模型是否还能记得主线信息。很多方案在无干扰时表现很好一插入噪声就露馅因为上下文里信息太多模型分不清主次。我一般会准备 50 组对话样本跑完三轮测试后统计正确率。低于 80% 就继续调高于 90% 才敢上生产。5. 进阶怎么让 context mode 真正变聪明5.1 引入检索增强的上下文基础版的 context mode 只能记住说过的话但很多业务场景需要想起来相关知识。比如客服机器人用户问的问题可能跟某个产品手册里的章节有关这个内容根本不在对话历史里模型再聪明也答不出来。解法是给 context-mode 加一层检索。我在实际项目里的做法是维护一个知识库切片索引每次用户提问时先用 embedding 把问题向量化从知识库里召回最相关的 3~5 个片段作为补充上下文放进 prompt。这层引入之后整个架构就变成了对话记忆知识检索的混合模式。经验是检索召回的内容和对话记忆要分开放对话记忆按时间组织检索内容按相关性组织两者拼进 prompt 时用清晰的标记区分。模型对用户说过什么和知识库说了什么的来源感知越清楚回答越不容易混淆。5.2 分角色记忆与业务场景适配最后一个进阶建议是分角色记忆。基础版的单一消息列表把所有内容混在一起但真实对话里有多个维度的信息用户说了什么、助手说过什么、系统内部标记了什么、工具返回了什么。混在一起会让模型难以区分事实和推测。我在最近的项目里把消息按来源打上标签构建 prompt 时按角色分组user-fact用户明确表达的诉求和提供的信息user-intent用户的意图和情绪倾向assistant-promise助手承诺过的事情比如我会帮你查一下tool-result工具返回的客观数据这个分组让模型能快速定位用户承诺过要做什么哪些是已经确认的事实。实测下来在多轮复杂任务的场景里回答准确率比混在一起提升了大约 10 个百分点。5.3 成本控制与延迟优化最后提一句跑在生产环境必须面对的成本和延迟问题。context mode 的本质是每次请求携带更多信息token 消耗自然比单轮问答高。我的优化手段有四个所有历史消息做 token 级去重相同的工具结果不重复注入。摘要压缩放在异步任务里执行不阻塞主线请求。长期画像只在会话首轮注入之后轮次不重复携带。对高频用户做增量式摘要而不是每次都全文重算。这四个手段叠加下来我其中一个项目的 token 成本降低了约 35%而用户体感基本没有下降。毕竟对于大多数产品用户要的是你记得住而不是你把所有字都背下来。回到开头那个问题——模型天然无状态但产品不能无状态。context mode 的价值就是给无状态的接口补上一个有状态的脑子让模型知道在跟谁说话、刚才聊了什么、哪些信息是可信的。这个脑子不是某一个模型的参数能解决的而是工程层面积累起来的记忆、筛选和压缩能力。按本文的三层结构、裁剪策略和评测方法做下来你应该能明显感觉到多轮对话的连贯性上一个台阶。