ARTICLE DETAIL

资讯详情

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

context-mode实战:如何设计LLM上下文策略提升回答质量

context-mode实战:如何设计LLM上下文策略提升回答质量 做 AI 应用的人应该都有过这种困惑同一个模型同一个问题有时候回答精准得像读心术有时候又蠢得像刚睡醒。我一开始以为是 prompt 写得不够好后来把系统提示词反复改了七八版效果还是忽上忽下。直到我把目光从 prompt 本身挪开开始研究 context-mode才算真正找到了问题所在。context-mode简单说就是应用在处理请求时给模型“喂”上下文的方式。它决定哪些信息进上下文、哪些不进、以什么顺序进、按什么优先级进。很多工具之所以有时候聪明有时候笨不是模型脑子不行而是切换上下文模式的逻辑有问题。这篇文章我不打算做概念科普就结合我自己在项目里调试 context-mode 的真实过程拆解一下这个抽象名词背后的具体机制以及在落地时最容易踩的坑。1. 我见过的 context-mode 翻车现场同一套代码两种人格先说一个我印象特别深的例子。当时我在做一个文档问答工具用户上传 PDF 之后可以在对话框里追问。最初版实现非常简单用户每问一个问题就把整本 PDF 的文本全部塞进上下文再做向量检索。结果很有意思当文档短的时候比如十几页回答质量非常高一旦换成三四百页的技术手册模型就开始“失忆”——你上午问过它第二章的内容下午问第五章时它连第二章提过什么都忘了。后来我看日志发现每次请求发出去之前程序都会把“整本 PDF 文本 当前问题 最近五轮历史”拼成一个大字符串。模型上下文窗口是固定的文档一长系统提示词被挤到几乎看不见历史对话也被截断了唯一完整保留下来的就是那本 PDF 的全文。也就是说模型确实“看到”了用户问的所有材料但它不知道用户之前关心过什么也不知道自己当前的任务优先级是什么。这其实就是 context-mode 设计失败的典型症状没有按内容类型分层没有区分“必须给”和“可以不给”的信息只用一种粗放方式把所有内容灌进去。换一个 context-mode 思路之后同样的模型、同样的 PDF效果立刻不一样了。具体做法我在后面会详细写这里想先强调一个核心认知——context-mode 不是“要不要用”的问题而是“怎么设计多层读取策略”的问题。什么算多层读取策略你可以把它理解成一个图书馆的管理员用户问问题的时候不是把整座图书馆的书都搬到他面前而是先看他手上已经在翻哪本书再去书架上找最相关的章节最后把找到的内容按“哪段最重要”的顺序放在桌面上。工具和模型的差别很多时候就在这个“管理员”称不称职。context-mode 就是这个管理员的工作模式。2. 拆开 context-mode 的壳它到底改了什么很多人以为 context-mode 是一种界面功能或者某种固定的产品形态。但实际上context-mode 改的是用户每次请求发出之前那些准备发给模型的 token 的组装方式。我用一个兼容通用 LLM 接口的视角来拆解。假设你的应用调用一个标准的 chat 接口请求体里的 messages 是长这样的[ {role: system, content: 你是某某工具的助手请根据文档回答……}, {role: user, content: 请解释第五章的配置项}, {role: assistant, content: 第五章提到的配置项有……}, {role: user, content: 那第二章和第五章有什么关系} ]这个结构看起来简单但“什么内容放到 system 里”“什么内容放 user 里”“历史对话保留几轮”这些决定最后完全由你的 context-mode 策略来定。不同策略下同一个问题发送给同一个模型结果可能天差地别。2.1 上下文不是越多越好而是越“对”越好有一次我做 A/B 对比发现 context-mode 设置为“把300页文档全部放进去”时回答准确率只有 42%设置为“只放检索到的相关片段 摘要 结构化索引”时准确率反而到了 78%。原因在于注意力机制的本质弱点上下文越长有效信息占比越低。模型要在几千个 token 里找到真正相关的那十几个 token难度远高于在几百个 token 里做同样的事。所以 context-mode 的核心任务之一是对上下文做“减法”——不是减少信息总量而是提高信息密度把最重要的东西放到模型最容易注意到的地方。2.2 system、user、assistant 三者的分工决定了模式的骨架在标准 chat 接口里system 消息用于设定全局规则user 消息是用户当前诉求assistant 消息是模型之前的回复。context-mode 设计的第一原则就是搞清楚每个信息块应该放哪个角色。比如用户上传了一份手册手册里有几个常见问题。如果你把“常见问题”的原文直接塞进 user 消息里模型会认为这是用户现在想问的问题如果你把它塞进 system 消息里模型会认为这是产品规则或者回答风格参考。同样的内容放进不同的角色槽位模型的反应是完全不同的。我在自建工具里通常遵循这样一个分配逻辑system 里放任务定义、输出格式约束、回答风格、前置知识比如核心术语表、当前模式的有效范围。user 里放用户当前问题、和这个问题强相关的资料片段、历史对话的摘要而不是全部原文。assistant 里放模型之前的关键答复要点用于保持一致性但不放完整的历史原文除非确实需要连续推理。这个分配逻辑我在多个项目里都验证过稳定有效。很多翻车案例都是因为把“资料”和“当前问题”混在一个 user 消息里导致模型分不清“你在告诉我背景”和“你在问我问题”。2.3 context-mode 的边界不是模型开关而是输入工程策略有个更容易混淆的点有人把 context-mode 理解成模型的某种运行开关比如“开启了就聪明关了就笨”。其实模型没有这种开关context-mode 只是你的应用在组织输入时的策略集合。这就像同一个员工你给他一份清晰的简报再让他做决策和他拿到一个塞满原始数据的硬盘让他自己翻结果是完全不同的。context-mode 本质就是那个“整理简报”的环节。所以后续你在任何产品里看到“上下文模式”之类的选项都可以把它理解为这个产品在决定发送什么内容给模型时采用了不同的策略。3. 三种主流 context-mode 的画像与适用边界聊完原理说说我实际用过的几种 context-mode 变体。它们不是标准分类但足够覆盖大多数应用场景。3.1 跟随模式最常用也最容易膨胀这是绝大多数聊天工具的默认模式。用户发一条应用把之前的完整对话记录 用户新消息 系统设定全部发给模型。优点是实现简单、上下文连贯适合日常对话、头脑风暴、代码讨论这类场景。缺点是对话一长token 消耗会快速增长而且越早的内容对后续回答的干扰越大。我在做一个客服机器人时开始就用的跟随模式跑了三天发现两个问题第一用户把问题从 A 换到 B 之后模型还老是记着 A 相关的信息不放回答 B 问题时夹带 A 的内容第二平均每轮请求的 token 消耗越来越大成本直线上升。解决办法是在跟随模式里加了一个“滑动窗口”只保留最近六轮对话更早的内容通过每轮生成的摘要来压缩。def build_conversation_context(history, max_turns6): recent history[-max_turns:] summary summarize(history[:-max_turns]) return [{role: system, content: f早期对话摘要{summary}}] recent代码不复杂但效果立竿见影。token 消耗大概降了 30%回答质量没有下降反而因为干扰少了准确度略有提升。3.2 检索模式在开放知识场景下的主力检索模式是知识库问答、RAG 应用里的标准选择。它的逻辑是用户问题先经过检索系统向量搜索、BM25 等拿到最相关的资料片段再把这些片段和用户问题拼起来发给模型。在测试中我通常把检索模式分为三种变体变体做法适用场景单片段模式只取最相关的一个片段答案可在单一章节找到、资料之间冗余度低多片段拼接模式取 Top-K 个片段按相关性排序拼接答案分散在多处、需要模型综合多个来源检索原文保留模式片段之外再附加原文位置索引和少量上下文需要引用、需要溯源、用户可能追问细节我最推荐的是“多片段拼接”但这里有个重要参数要注意Top-K 不是越大越好。K3 和 K8 的效果差距没有你想象的大但错误引用的概率会随 K 增大而上升因为检索系统排在后面的结果和用户问题的实际相关性其实很低把这些片段塞进去相当于给模型制造了噪声。3.3 结构化记忆模式用来解决“持久个性”问题还有一类场景用户希望模型记住自己的偏好、历史习惯、项目约定。比如你一个月前告诉模型“我的代码风格是函数命名用动词开头”现在换个新会话模型应该还记得。这种场景适合用结构化记忆模式做法是把长期信息从对话历史里抽出来存在独立的记忆区。每次请求时先从记忆区检索和当前问题相关的条目注入到 system 消息里。我在自建工具里用过一个非常朴素的实现维护一个 JSON 文件每条记忆带标签。{ user_preference: [ {id: 1, tag: coding_style, content: 函数名使用动词开头如 getData、setStatus}, {id: 2, tag: doc_format, content: 输出markdown格式带表格优先} ] }读取时按 tag 匹配比如用户问题里出现“函数”这个词就把 tag 为 coding_style 的条目取出来塞进 system。这个实现虽然简单但比“把所有历史全部传给模型”稳定得多也不占用太多上下文窗口。不过结构化记忆模式有它的代价记忆的更新策略不好写容易出现“记了不该记的”“忘了该记的”。我踩过最严重的坑是模型把用户某次随口说的“我不喜欢 Python”当成永久偏好存了下来之后每次问答都带着这个预设导致用户后续说要用 Python 时模型产生了前后矛盾。后来我给记忆条目录入条件加了一个过滤只有用户在明确表达偏好或规则时才允许写入记忆区。4. 日常落地时我盯紧的四个关键参数无论用哪种 context-mode你都需要关心几个通用参数。这些参数决定了一个模式是否真的能稳定工作。4.1 上下文窗口预算先分块再填内容不要等到请求快超限了才去截断。我习惯在每次构建请求前先给不同类别的内容分配预算。假设窗口上限是 8000 token我会这样分system 消息600 token7.5%当前问题200 token2.5%检索片段3000 token37.5%历史对话摘要1500 token18.75%记忆条目700 token8.75%模型输出预留2000 token25%这个比例不是固定的但预留 25% 给模型输出这一点极为重要。很多“模型回答到一半断了”的问题其实是把上下文撑太满导致输出空间不足。4.2 截断策略头尾保住中间压缩上下文组装时系统提示词和当前问题是“头”绝对不能丢历史对话是“中间”可以压缩。这个优先级顺序我踩过两次坑才真正记住有一次为了保留完整历史我把系统提示词截断了一半结果模型完全忘记了自己的输出格式约束返回了一堆未格式化的裸文本下游解析程序直接崩了。4.3 模式切换的触发条件多个 context-mode 可以共存但切换必须慢。不要在用户发第二条消息时就从检索模式切成跟随模式这样模型会丢失前一条消息的关键状态。我目前的做法是用规则判断用户当前意图。如果问题里带“引用”“根据文档”“第几章”这类词走检索模式如果是“继续”“那如果改成XX呢”这类连续性表达走跟随模式如果是新话题或者跨会话走结构化记忆模式。切换时把上一模式的输出摘要带入新模式保证状态不断裂。4.4 Debug 可视化必须能“看到”每次请求组装了什么这是我最想强调的一点。context-mode 的调试难点在于模型拿到什么你没直接看到出了问题很难判断是模型问题还是上下文问题。我建议所有接 LLM 的应用在开发环境输出完整的请求日志包括本次使用哪种 modeSystem 消息的全文User 消息的原文和注入的片段来源历史摘要的生成时间Token 占比统计有了这层可视化排查问题会轻松一个数量级。如果没有这层日志你只能对着模型乱猜效率极低。5. 我在自建工具里设计 context-mode 的完整思路前面说的都是概念下面分享一个我在实际项目里从头设计 context-mode 的完整过程。这个项目是一个面向内部团队的文档问答助手规模不大但足够有代表性。5.1 第一步列出可能进入上下文的信息源我先把所有可能作为上下文的信息源列成一个清单系统提示词固定规则用户实时输入当前文档的全文/片段文档的目录结构最近 N 轮对话用户长期偏好记忆工具调用结果比如查数据库、调 API 的返回模型之前生成结果的摘要清单列出来后我给每个信息源打上三个标签必须给、按需给、不给。系统提示词是“必须给”用户实时输入是“必须给”最近对话是“按需给”视话题连续性而定文档全文是“不给”除非文档很短比如 1000 token。这个打标签的过程看似简单但它逼迫你明确每一类信息的优先级而不是一股脑全塞进去。5.2 第二步定义组装管线接下来我把“怎么把信息源拼成最终请求”定义成一条管线。每次用户提问都走同样的流程意图分类判断用户当前问题属于哪一类资料查询、连续讨论、新话题。模式选择根据意图分类结果选择检索模式、跟随模式还是结构化记忆模式。资料获取如果是检索模式执行检索取 Top-K 片段。摘要生成如果历史过长生成历史摘要。记忆匹配如果问题涉及长期偏好从记忆区取相关条目。预算分配按 4.1 的比例分配 token 给各模块。组装发送按 system/user/assistant 结构拼装校验总 token 不超限。这套管线写成一两个函数逻辑简洁清晰也方便后续扩展新模式。5.3 第三步一个可直接参考的组装示例下面是一个简化版但可直接参考的组装函数用 Python 伪代码写def build_messages(query, document, history, memory, moderag): system_parts [你是文档助手回答请基于提供资料并标注来源章节。] user_parts [f用户问题{query}] if mode rag: fragments search_document(document, query, top_k5) system_parts.append(以下是与问题相关的资料片段) for i, frag in enumerate(fragments): user_parts.append(f片段{i1}{frag.content}来自第{frag.chapter}章) system_parts.append(如果资料不足请直接说明无法回答。) elif mode chat: summary generate_summary(history[:-6]) recent history[-6:] if summary: system_parts.append(f早期对话摘要{summary}) user_parts.extend(recent) # 记忆注入 relevant_memories match_memory(memory, query) if relevant_memories: system_parts.append(用户长期偏好) system_parts.extend([m.content for m in relevant_memories]) system_content \n.join(system_parts) user_content \n.join(user_parts) messages [{role: system, content: system_content}] messages.append({role: user, content: user_content}) return messages注意一点我在这个示例里把所有资料片段放进了 user 消息而不是 system。为什么因为资料是“当前问题相关的输入”不是“全局规则”放进 user 可以让模型把它当作本次请求的具体材料而不是适用于所有对话的设定。这一取舍我在对比测试里验证过效果差距明显。如果资料和系统规则混在一起模型在长对话中会倾向于忽略资料而只遵循系统规则回答反而变得很空泛。6. 避坑清单context-mode 的五个典型翻车场景最后分享几个我在真实项目里遇到的翻车案例。这些都是自己踩过的写出来给你们省点学费。6.1 检索结果排错模型答非所问有一阵子我的问答助手在用户问“如何在 Mac 上安装 Python”时答出来的却是“在 Windows 上安装 Python 的步骤”。排查日志发现检索器把那段 Windows 资料排到了第一位因为两段资料都包含“安装 python 环境”这几个词向量相似度接近。问题不在模型而在于检索打分没有考虑文档版本和适用平台。我后来加了一组过滤规则检索前先用正则识别平台关键词mac/windows/linux强制把平台匹配的文档加权不匹配的直接降权。这种简单规则比花里胡哨的 rerank 模型好使还省资源。6.2 “全量塞入”导致的高成本幻觉另一个项目里我的同事为了“不错过任何信息”把每个用户的全部邮件都塞进上下文。结果 token 消耗飙升账单翻了三倍而且模型开始在回答里“编造”邮件内容——因为上下文太长它无法准确关联所有邮件只能根据平均印象生成貌似合理的内容。这就是 context-mode 设计里最经典的两个矛盾成本与控制。信息多了模型不是更聪明而是更容易幻觉。后来我强制设了上下文上限单封邮件只取第一段主题行最后一段更早的邮件用摘要代替全文幻觉率立刻降了一半。6.3 历史记忆污染当前任务在结构记忆模式里我遇到过一个有点尴尬的案例。用户前一天让助手用“诙谐口吻”写文案第二天换了任务——让助手写严谨的技术报告。但由于记忆区里保存了“诙谐口吻”的偏好助手第二天还在报告里加冷笑话用户直接投诉。根本原因是记忆条目缺少“任务类型”标签。我给记忆加了一层分类每个条目除了内容还有一个作用域字段比如 style、format、knowledge、requirement。生成上下文时按当前任务类型过滤作用域。如果当前任务是“技术报告”就只取 requirement 和 knowledge不取 style 类记忆。6.4 对话轮数截断导致上下文断裂还有一次用户在连续问答中忽然提到“我刚才说的那个问题”模型完全不知道他指的是哪个问题。排查发现我的滑动窗口只保留了最近三轮而用户指的那个问题在第四轮。缺失的中文信息用简单的摘要也没能兜住。从那以后我调整了窗口逻辑不单纯按轮数截断而是增加“核心实体追踪”。每轮对话结束后抽取问题中的核心实体列表比如“Python安装”“数据库连接”存进一个临时状态。之后即使那一轮被滑动窗口截掉当前轮请求里也会追加上最近五轮的实体索引保证模型对话题焦点有基本认知。6.5 模式自动切换的误判模式切换是高级功能但也是最容易出错的环节。我做过一个自动切换规则用户问题体量超过50个字就切检索模式少于10个字就切跟随模式。结果用户发了一段200字的背景说明然后问“你觉得怎么样”助手直接跑去检索资料完全忽略了用户的真实意图是讨论而不是查询。后来我把“字数量词”改为“意图识别”用一个二分类模型把用户问题分为“查询型”和“讨论型”。查询型走检索讨论型走跟随。效果提升非常明显。当然二分类模型也有误判的时候所以我还加了一个回退机制不管自动分类成什么只要用户消息里有“你觉得”“你怎么看”“能不能分析下”这类表达默认走讨论型不做检索。context-mode这个设计思路我实践了大半年最大的体会是它没有一劳永逸的配置每个应用都要根据自己的场景去定义“哪些信息重要、哪些可以丢掉”。我的个人建议是先从最简单的两种模式开始一个是短对话场景的跟随模式一个是知识密集场景的检索模式跑通流程后再逐步加入结构化记忆、自动切换。在基础模式没调稳之前不要急着上复杂的智能切换否则你会在“不知道哪个环节出问题”的泥潭里浪费大量时间。另外每次做 context-mode 调整都记得做前后对比测试用固定的几组问题集跑一遍记录准确率、token 消耗、响应时间。我见过不少人花一周调出了复杂策略结果效果还不如最朴素的方案。context-mode 的评判标准永远只有一个——你的实际应用效果有没有变好。这点值得时刻记在脑子里。
返回列表