ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型上下文管理实战:context-mode架构、常见坑与调参方法

大模型上下文管理实战:context-mode架构、常见坑与调参方法 1. 先搞清楚断片发生在哪context-mode 解决的问题边界如果你做过聊天机器人、AI 助手、企业知识库问答这类产品一定听过用户这样吐槽它是不是把我忘了昨天刚说过的需求今天又当作新问题处理同一个问题换个说法答案就完全不一样了。在早期阶段我会把这些归咎于模型能力不行但做的时间越长越发现真正的问题往往出在上下文上。而 context-mode就是一套专门解决模型该记住什么、该忘掉什么、用哪部分上下文来回答的设计方案。先把这个概念说清楚。大模型本身是无状态的它每一次回答都是基于当前输入的一段文本上一次聊了什么如果你不主动喂给它它根本不记得。所谓 context-mode就是你在应用层实现的一系列机制把历史对话、用户画像、业务规则、检索到的知识等按照一定策略打包成模型能理解的内容再塞进提示词里。它不只是把对话历史带上这么简单还要考虑带多少、带哪些、按什么顺序带、什么时候该丢弃。我见过很多团队把 context-mode 理解成多轮对话这是个常见的误区。多轮对话通常是面向一次会话内把用户说的话和助手回复按顺序拼接起来而 context-mode 的视野更大它要处理的是跨会话的长期记忆、不同用户之间的信息隔离、系统级规则和会话级信息的优先级冲突。一句话概括多轮对话是记忆的容器context-mode 是记忆的管理器。那这篇文章适合谁看如果你正在做基于大模型的产品并且开始遇到回复不稳定、上下文混乱、用户信息串号这类问题那这篇文章基本就是为你写的。我会从问题边界、三层上下文结构、生产级落地方案、踩坑排查链路、多智能体扩展、以及效果评估与调参这几个角度把我实际做过的项目经验完整拆开来讲。顺便说明一句文中的方案和参数都是基于我个人的工程实践你可以把它当作一个经过验证的参考基线不必照抄。1.1 没有上下文时用户到底在抱怨什么把用户的抱怨归类之后你会发现没有上下文其实是一个笼统的说法背后对应着完全不同的技术缺口。我梳理过一线反馈出现频率最高的差不多是这四类它不知道我是谁用户已经在小程序里登录了账号但助手仍然把他当作陌生人不记得他的岗位、偏好和权限。昨天说的事今天忘了上次会话里已经确认过的结论这次打开又从头问起用户需要反复陈述一遍背景。我换个说法它就听不懂用户第一次说请帮我把季度汇报改得精简些第二次说上次那份报告你帮我压缩一下系统无法把两次表达关联到同一个对象。同一个问题每次答案都不一样同样的提问上午一个版本下午一个版本甚至 10 分钟内两次提问得到截然不同的说法用户会直接质疑系统的可靠性。这四类问题分别对应不同的上下文层次第一类缺的是用户级画像第二类缺的是跨会话持久记忆第三类缺的是语义关联和实体抽取第四类缺的是系统级规则一致性。你可以做一个简单的映射表作为设计 context-mode 时的需求清单用户可感知的抱怨缺失的上下文层典型解决手段不知道我是谁用户级上下文维护用户画像、身份标签昨天的事忘了存储型记忆 / 跨会话记忆长期记忆库按用户维度写入换个说法听不懂实体级上下文抽取实体与核心意图构建索引每次答案都不同系统级上下文固化系统指令收敛随机性我建议你在做任何技术方案之前先做这个归类动作。很多项目一开始就上重技术直接接向量数据库、搞 RAG结果发现用户投诉的还是它不知道我是谁。原因很简单向量检索解决的是知识召回解决不了身份识别。context-mode 的优先级应该是先补齐身份和规则再做知识召回。1.2 context-mode 与传统多轮对话的区别这里值得单独展开因为我从面试和同行交流里发现不少人对这两个概念的边界是模糊的。传统多轮对话的实现通常是一个消息数组你一言我一语地往上堆堆到超过模型窗口再暴力截断。它的优点是简单缺点也很明显没有主次之分没有时效之分更没有安全边界。context-mode 在架构上会引入管理动作。比如同样面对 20 轮历史对话传统做法是把 20 轮全部塞进去context-mode 的做法是先判断这些历史里哪些是用户确认过的关键决定哪些是已经过时的中间讨论哪些是与当前问题无关的寒暄然后只保留前三类里的前两类中间讨论用一句摘要替代。这套取舍 优先级的机制就是 context-mode 与普通多轮对话的本质差异。举个我实际做过的例子。我们给企业运维团队做过一个故障排查助手第一个版本就是简单的多轮对话把最近几轮历史拼进提示词。上线第二天就出了问题用户上午报告了一个网络故障排查到一半去吃午饭下午回来继续追问另一个数据库问题结果因为历史里还带着上午的网络故障记录助手把两个故障混在一起分析给出了一堆莫名其妙的建议。后来我们改成 context-mode用一个会话主题切换检测来识别用户是否开启了新议题一旦检测到新主题就把旧主题压缩成一行摘要并降权问题立刻缓解。所以传统多轮对话解决的是连续的上下文而 context-mode 要解决的是动态变化的上下文。2. context-mode 的三层结构会话、用户、系统级上下文怎么分工做 context-mode 最忌讳的是一股脑把所有信息丢给模型。不同信息有不同的生命周期、不同的敏感程度、不同的更新频率把它们混在一个袋子里模型的表现必然不稳定。我习惯把它拆成三个层次会话级、用户级、系统级每一层单独存储、单独控制注入逻辑。2.1 三层上下文模型会话级上下文指的是当前这次对话内的内容包括用户最近的提问、助手给出的答案、工具调用的结果。它的特点是生命周期短通常会话结束就可以丢弃最多留一个摘要给下次会话作为开场白。用户级上下文指的是跨会话存在的用户画像比如用户所在部门、常用术语、历史偏好、权限范围。它的生命周期是长期的要写入持久化存储并且要允许用户主动查看和修改。系统级上下文指的是产品层面的固定规则、术语表、安全红线、知识库索引所有用户在所有会话里都应该遵守的那部分内容。三者的分工可以用一个生活化的类比来理解。会话级上下文是两个人当下在聊什么用户级上下文是你对这位朋友的长期了解系统级上下文是聊天双方共同遵守的社交礼仪和话题边界。三个都缺一不可但它们的存储方式差别很大上下文层典型内容存储位置更新频率注入策略会话级最近 N 轮对话、临时状态内存 / Redis每轮更新按时间倒序最近优先用户级画像、偏好、权限、事实关系库 / 键值库低频有变动才更新按需抽取关键事实系统级规则、流程、词表、安全红线配置文件 / 知识库低频人工维护每次固定注入放提示词最前面在实操时有一点特别重要用户级上下文不要整包注入。一个老用户的画像可能有几百条事实全塞进去既浪费 token又会让模型抓不住重点。正确做法是每次请求时先做一次画像相关度筛选只挑出与当前问题相关的 5 到 10 条事实注入。比如用户问报销流程你就注入他的部门、报销偏好、历史审批人用户问服务器部署你就注入他的环境权限和常用集群而不是把他在系统里留过的所有信息都搬出来。2.2 上下文窗口与 token 预算的取舍三层上下文都往提示词里塞很快就撞上模型上下文窗口的天花板。很多团队一上来就买大窗口模型觉得窗口大了问题就解决了但窗口大不等于效果好窗口大反而更容易让模型注意力分散。我自己做项目时的原则是窗口再大也要像一个吝啬的房东一样把每一寸空间都规划好用途。有一版我们用的模型上下文窗口是 128K看似很大但我仍然把单次请求的 token 预算控制在一个范围内因为窗口里要同时容纳系统指令、检索到的知识片段、工具调用的中间结果以及模型要生成的回答空间。我的通用预算分配是这样的系统规则占 20% 到 30%用户相关画像占 10% 到 15%会话历史占 40% 到 50%外部检索来的知识占 10% 到 20%剩下至少留 10% 给模型生成输出。你可以把它想象成一次多人会议系统规则是主持人的开场白用户画像是参会者的名牌会话历史是刚才的争论记录检索知识是摆在桌上的参考资料而你不可能让会议纪要占掉所有时间总得给结论留出空间。具体到计算假设某次请求可用输入窗口是 8K token我会这样切分输出预留 800系统规则留 1800用户画像留 800会话历史留 3000检索知识留 1600。如果某次检索到的资料特别多我不会硬塞而是先做一遍重排只取与当前问题最相关的片段。这里面的核心逻辑是模型的注意力资源是有限的你喂给它的信息越杂乱它越容易把一些无关的历史细节当作当前指令执行导致所谓的上下文污染。宁可少带也要精带。3. 一个生产级 context-mode 的落地方案存储、打包与注入理论说完了下面进入实战部分。我以一个企业知识库助手为例完整讲讲我是怎么把 context-mode 落到生产环境的。这个项目的要求是员工可以问内部制度、IT 流程、报销规则等相关问题系统需要记住员工上次问到哪里、是否已确认过某个关键结论同时还要根据员工的部门和职级显示不同的信息。3.1 需求定义与场景拆解先做需求拆解。我们把场景抽象成三类第一类是新问题问答员工问一个从未问过的制度问题系统需要去内部知识库检索并给出答案。这种场景下主要用到系统级上下文和检索知识。第二类是追问与确认员工就上一个问题继续深入比如先问年假怎么算再问那离职时未休的年假能折算吗。这里需要带上会话级上下文让模型理解那指的是什么。第三类是跨会话延续员工上次问完没有结束今天继续问同一个主题甚至引用上次得到的结论你说按工龄分档我是 8 年工龄应该休几天。这里就必须引入用户级上下文和会话摘要。拆完之后我们给每一类场景定了不同的 context-mode 策略。新问题问答走精简模式只注入系统规则和检索知识追问与确认走完整模式注入最近 10 轮对话跨会话延续走摘要模式注入上次会话摘要加上用户画像中的关键事实。不同模式之间由后端一个意图识别模块自动切换用户无感。这个设计的关键是不要对所有请求都用同一套上下文打包策略否则低成本的简单问题也会背上沉重的历史包袱既浪费 token又增加出错概率。3.2 存储选型内存、Redis、向量库、关系库怎么选上下文要存储存储选型是绕不开的一步。我在这类项目里见过四种主流方案各有适用场景放在一起对比会更直观存储方案适合存放的上下文优点缺点我的使用建议进程内存单机短会话、临时状态快零依赖重启丢失无法横向扩展只存非关键中间状态Redis会话历史、会话摘要、短期用户状态快支持过期时间不好做复杂检索会话级上下文首选关系型数据库用户画像、权限、确认过的事实事务强、支持查询不擅长语义检索长期事实类上下文首选向量数据库知识片段、历史对话的语义索引支持相似度检索写延迟高过滤条件弱只放知识和语义记忆这个项目里我们最终用了组合方案Redis 存会话数据和最近摘要关系库存用户画像和权限向量库存内部制度文档的切片。有朋友问过我既然向量库也能存东西为什么不全塞进去原因有两个第一向量检索是按语义相似度召回它不是精确查询你把员工工号、部门这类确定信息放进去检索结果可能张冠李戴第二向量库的元数据过滤能力普遍弱于关系库做权限控制时很容易漏。所以我的经验是确定性的上下文放确定性的存储语义型的上下文放向量库两者各自发挥优势不要互相替代。3.3 核心实现上下文打包与注入策略存储定下来之后关键在代码层面的上下文管理器。我写过一个比较通用的实现思路核心逻辑是先分层记录再按预算裁剪最后按固定顺序拼接import json import re from typing import List, Dict class ContextManager: MAX_INPUT_TOKENS 8000 OUTPUT_RESERVED 800 TOKEN_ESTIMATE_RATIO 3.2 # 中英文混合场景平均每个token约3.2个字符 def __init__(self, redis_client, db_client, vector_client): self.redis redis_client self.db db_client self.vector vector_client def _estimate_tokens(self, text: str) - int: return int(len(text) / self.TOKEN_ESTIMATE_RATIO) 1 def _build_system_prompt(self) - str: # 系统级上下文固定规则每次最先注入 return load_system_rules() def _build_user_profile(self, user_id: str, query: str) - str: # 从关系库读取用户事实做一个简单的相关度过滤 facts self.db.query_user_facts(user_id) relevant [f for f in facts if any(k in query for k in f.keywords)] return 用户已知信息 json.dumps(relevant[:8], ensure_asciiFalse) def _load_recent_history(self, session_id: str, max_rounds: int 10) - List[Dict]: return self.redis.lrange(fsession:{session_id}:history, -max_rounds * 2, -1) def _build_retrieved_knowledge(self, query: str, top_k: int 5) - str: docs self.vector.search(query, top_ktop_k, threshold0.72) return \n.join(f[知识{i1}] {d.content} for i, d in enumerate(docs)) def build_prompt(self, user_id: str, session_id: str, query: str) - str: parts [] budget self.MAX_INPUT_TOKENS - self.OUTPUT_RESERVED system_prompt self._build_system_prompt() parts.append((system, system_prompt, self._estimate_tokens(system_prompt))) profile self._build_user_profile(user_id, query) parts.append((profile, profile, self._estimate_tokens(profile))) knowledge self._build_retrieved_knowledge(query) parts.append((knowledge, knowledge, self._estimate_tokens(knowledge))) history self._load_recent_history(session_id) history_text \n.join( f{用户 if msg[role] user else 助手}: {msg[content]} for msg in history ) parts.append((history, history_text, self._estimate_tokens(history_text))) final_parts [] used 0 # 按优先级依次放入预算不足时先截断会话历史而不是牺牲系统规则 for name, content, tokens in parts: if used tokens budget: if name history: content self._trim_history(content, budget - used) elif name knowledge: content content[: max(1, (budget - used) * self.TOKEN_ESTIMATE_RATIO)] else: continue final_parts.append(f{name}_marker:{content}) used self._estimate_tokens(content) return \n\n.join(final_parts)这段代码里有一个容易被忽略的设计拼接顺序。我固定把系统规则放在最前面然后是用户画像、检索知识最后才是会话历史。这么做的原因有两个。第一模型对提示词前部的注意力通常更强系统规则这种必须遵守的内容要放在显眼位置否则容易被后面的长历史稀释第二会话历史的优先级最低一旦 token 预算不够第一个被裁剪的就是它而不是系统规则。很多新手把历史放在最前面结果模型越聊越被带偏往往就是这个顺序问题。注入策略上还有一个细节不要在每轮对话里都把用户画像完整更新一遍。画像写入应该由独立的事件触发比如用户明确说出偏好、系统确认了一个关键事实而不是把每一次用户发言都当成画像素材。否则画像会迅速膨胀里面充满大量低质量噪音。我们的做法是设置一个画像置信度机制同一个事实被用户确认两次以上才写入长期存储单次出现的事实只放在会话级上下文里随时可被丢弃。4. 上线后最容易被低估的四个坑污染、过期、串号与越权context-mode 这层设计平时看似风平浪静真正出问题的时候往往很隐蔽。我在上线后陆续踩过不少坑挑四个最有代表性的展开讲其中第一个我给出完整的排查链路后面几个讲清楚根因和修复方式。4.1 一次上下文污染事故的完整排查过程背景是这样的系统上线第三周有用户反馈他问一个跟年假申请相关的问题助手却突然提到了会议室预订流程。这个回答从单句看是通的因为两个流程里都提到了提交审批这个共同动作但从业务上看完全是两码事。用户当时就说了一句这个助手有点精神分裂这让我们非常紧张。我当时的排查思路是沿着数据链路一步步追没有直接改模型提示词因为问题大概率不在模型而在喂给模型的上下文。第一步查模型调用日志确认这次请求实际注入的提示词内容。日志显示系统规则没问题会话历史也基本对得上可疑的是检索知识部分里面混入了一段标题为审批流程常见问题的通用文档其中一个大段讲的是会议室预订。第二步定位这段知识是怎么被检索出来的。我去向量库查了这次的检索候选列表结果发现相似度最高的 5 个片段里前 3 个确实是年假相关第 4 个和第 5 个是审批流程通用片段里面包含了预订流程的内容而我的召回阈值设的是 0.6太低把这两个无关片段也放行了。第三步检查元数据过滤发现这两个片段属于一个公共文档集合没有绑定部门和场景标签所以被当成了万能知识提供给所有用户。根因浮出水面不是模型错而是检索召回范围太宽、阈值太低、元数据过滤缺失。修复动作有三步一是把检索阈值从 0.6 调到 0.72重新跑一遍历史问题集确认没有明显漏召回二是给所有知识切片补齐场景标签检索时强制按场景过滤三是在注入知识时对候选片段做一次重排剔除与当前问题明显不同主题的内容。这个事故给我最大的教训是context-mode 的每一路输入都要可追溯日志里必须能看到到底哪一段上下文最终被使用了否则排查起来就是大海捞针。4.2 过期信息、角色混淆与越权读取第二个坑是信息过期。用户级上下文里存了一条用户所在部门是市场部三个月后他转岗到产品部系统还在用旧画像回答导致所有涉及权限的答案都是错的。修复方案是给每条长期记忆加上更新时间戳回答涉及的关键事实要做时效校验同时在用户主动告知变更时必须走一次事实确认流程而不是直接覆盖。第三个坑是角色混淆。同一个用户他在对外场景是客户在对内场景是员工两条身份对应的权限完全不同。有一次助手把内部员工的休假制度讲给了外部客服人员听其实就是因为只按 user_id 取上下文没有区分当前场景身份。我后来把所有用户级上下文都改成复合键场景维度加身份维度比如 external:customer:10001 和 internal:employee:10001 两套上下文互相隔离。第四个坑是越权读取。多用户共享同一套系统级知识时A 用户检索到的内容可能包含面向 B 用户的私有信息。我在代码里加了一层强制过滤所有知识切片在进入提示词之前先按当前用户的权限标签做一次交集运算没有权限标签的切片直接丢弃。这一步绝对不能省尤其在涉及薪资、绩效、合同这类敏感内容的场景里。这四类坑我用一张表收一下方便你对照自查坑现象根因修复方案上下文污染回答混入无关内容检索阈值低、元数据过滤缺失调阈值、加场景标签、做重排信息过期用旧画像回答新问题长期记忆没有时效校验时间戳校验、变更确认流程角色混淆场景身份混用上下文未按场景隔离复合键隔离身份上下文越权读取用户看到不该看的信息检索结果未做权限过滤注入前做权限交集运算5. 进阶从单人对话到多智能体协作的上下文隔离设计如果 context-mode 只服务一个对话场景做到上面那一步基本就够了。但我在做项目后期发现它真正复杂的地方是在多智能体协作里。多个智能体各自有分工又要共享一部分高层信息上下文如果一刀切共享会产生严重的串扰如果一刀切隔离又会让协作失去同步。这一节讲我摸索出来的一套隔离与共享规则。5.1 让上下文带上角色视角我最早做的多智能体场景是一个内容创作团队一个策划智能体负责定主题一个写作智能体负责写初稿一个润色智能体负责改稿。最初我把三个智能体共用一套完整上下文效果很差策划阶段生成的灵感发散内容会干扰写作阶段的严谨表达。后来我给上下文加了一个角色视角字段每个智能体只读取与自己职责相关的部分内容。策划智能体看用户需求和市场信号写作智能体看策划结论和素材库润色智能体看初稿和风格指南。实现上其实不复杂就是在提示词模板里按角色做条件拼接。角色视角的本质是不要把所有上下文都视为客观事实有些上下文只是某个角色的中间产物。比如策划智能体说这篇稿件可以走幽默风格这句话对于写作智能体是有效指令但对于润色智能体可能只是背景信息。把所有上下文一律当成事实喂给所有智能体是很多协作失败的根本原因。5.2 多智能体之间如何共享与隔离上下文我采用的是一种黑板模式。所有智能体共享一个公共上下文区里面只放经过确认的结论和任务状态比如主题已确定素材已收集完成初稿等待润色。而每个智能体的私有上下文区存放的是它自己读取到的原始信息和中间推理过程。公共区与私有区严格分开私有区内容不允许被其他智能体直接读取其他智能体只能看到公共区里对外公开的结论。这样做的好处是显而易见的。第一中间推理过程的噪音不会被传播出去比如写作智能体在思考时提到的某个备选标题不会干扰策划智能体的判断第二公共区的信息经过确认可信度高避免了一个智能体把另一个智能体的随口推测当成事实第三权限边界清晰每个智能体只拥有它需要知道的上下文。这套设计在考试里有个类似的表达叫最小知识原则应用到生产线也同样成立。从存储角度看我在逻辑上加了一个复合键来唯一标识一条上下文归属三元组格式是agent_id scope_type owner_id其中scope_type只有两个取值public表示公共区private_owner表示某个智能体的私有区。代码层面就是往 Redis key 里拼前缀成本非常低但决策边界非常清晰。如果后续智能体数量多了还可以在这个三元组之上再加一层命名空间方便按团队隔离。6. 用数据说话context-mode 上线后的效果复盘与调参记录最后这部分是效果复盘。很多人做完 context-mode 觉得感觉变聪明了但这不能作为交付标准我习惯用指标说话并且把上线后的调参过程记录下来下面是那套企业知识库助手的实际复盘。6.1 评估指标怎么定不要只盯着 BLEU 或 ROUGE生成式模型的评估如果只看 BLEU、ROUGE 这类文本重叠指标在 context-mode 场景里会有严重误导。因为用户满意的回答往往不是和标准答案逐字接近而是正确利用了上下文。我更看重这几个指标上下文命中率在回答中确实使用了历史信息或用户画像的比例用人工抽检加日志标记的方式统计。重复提问率用户在多轮对话中对同一个问题追问超过一次的占比这个指标下降说明上下文记忆有效。信息串扰率回答中混入与当前主题无关的历史信息的比例这是 context-mode 特有的负面指标。身份与权限准确率涉及用户身份和权限的回答是否正确这是安全底线要求做到 100%。我比较推荐的做法是每一条线上回答都打上使用了哪些上下文来源的日志字段然后按周做人工抽检把抽检结果和日志字段做关联分析。这样你能知道哪些上下文来源最可能导致错误进而做针对性调整。6.2 最影响体验的三个参数运营了大半年之后我总结出三个最影响体验的参数你可以把它当作调参起点。第一个是检索阈值。阈值调低召回多但噪音也大阈值调高精度提升但可能漏掉有用信息。我们在实验集上跑了一圈0.72 是一个不错的平衡点比初始的 0.6 降低了大量污染同时召回率只损失了约 3%。第二个是历史保留轮数。历史保留越多模型对当前话题的判断越稳定但也更容易被无关内容带偏。我们验证下来普通问答场景保留最近 6 到 10 轮效果最好超过 10 轮信息串扰率会明显上升少于 6 轮用户追问时模型又容易失去前文线索。第三个是会话摘要触发间隔。不是每轮都做摘要而是当对话达到一定长度或检测到主题切换时才做。我们的配置是对话超过 12 轮未结束时把前 8 轮压缩成一个结构化摘要或者当主题切换检测置信度超过 0.8 时立刻对上一主题做摘要。直接压缩整个上下文而不分主题是很多摘要方案失败的原因。6.3 我的调参记录与最终效果这里贴一段当时的调参记录省得大家重复走弯路。第一版上线时检索阈值 0.6上下文串扰率不到一周就报警我把阈值调到 0.7串扰率明显下降但出现了该召回没召回的情况比如用户问报销上限时相关文档没有进入检索结果。后来我意识到问题不在阈值而在知识切片时把报销规则和报销流程切成了两个片段导致语义表达不完整。调整切片策略按章节层级切片而不是按固定字数切片之后再把阈值定到 0.72效果才稳定下来。历史保留轮数的调整也有类似过程。最初保留 20 轮用户满意度反而低于保留 6 轮时因为模型被大量旧信息干扰经常把一个多小时前的话题当作当前主线。我们加入主题切换检测之后才敢把保留轮数重新放开到 10 轮同时配合会话摘要才做到该记得的都记得该忘的都能忘。最终那一版的脱敏数据大概是这样上下文命中率从开源的 41% 提升到 76%重复提问率下降了约 32%信息串扰率从 18% 降到 4% 以内。这个结果不算惊艳但对我们这种中小规模团队已经是稳定可用的状态。说实话这套机制跑到现在我最大体会是 context-mode 从来不是模型本身的问题而是工程取舍问题你愿意花多少 token、多少存储、多少排查成本去换取一段看起来真的记得你的用户体验。而判断一个 context-mode 设计得好不好就一条标准——用户不需要主动解释自己系统也能把事办对。能做到这一点的团队基本都在上下文管理上下了真功夫。
返回列表