ARTICLE DETAIL

资讯详情

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

context-mode实战:从固定上下文到检索增强的LLM应用优化指南

context-mode实战:从固定上下文到检索增强的LLM应用优化指南 前几天有个朋友跟我吐槽说他们公司花了两个多月做的智能客服一上线就被用户骂成“人工智障”用户连续问了几轮之后模型就开始忘掉最开始的需求回答越来越跑偏最后甚至为了凑答案开始编政策。我说你把会话日志发我看看结果一眼就看出问题——他们把整段对话历史无脑拼进 prompt上下文越长模型越糊涂。这个场景太典型了所以我特别想认真聊聊 context-mode 这个东西。context-mode 不是一个单一功能而是一套决定“哪些内容放进上下文、以什么顺序放、保留多久、如何更新”的策略集合。它直接决定了大模型应用的稳定性、成本和幻觉率。这篇文章基于我最近做过的知识问答助手、客服机器人、长文档分析工具这类项目把对 context-mode 的底层理解、四种核心实现、参数计算和踩坑记录完整整理一遍。适合正在做 LLM 应用的开发者、产品经理以及刚接触上下文工程的同学参考。1. context-mode 是什么一个项目三种模式1.1 它不是上下文窗口而是“上下文编排策略”很多人第一次听说 context-mode会误以为它是指模型的上下文窗口大小——比如 128k、200k 这些数字。这个概念完全不一样。模型自带的上下文窗口只是“容量上限”而 context-mode 是应用层主动编排上下文的方式。模型本身在生成回答时并不会去判断“哪一句历史重要、哪一段资料过时”它只负责吃掉你喂给它的所有 token然后尽力推理。所以真正决定应用质量的是你怎么组织这个上下文。我习惯把 context-mode 拆成三个问题放什么、放多少、怎么更新。放什么固定人设、对话历史、业务知识、工具调用记录哪些必须进哪些可以省略。放多少token 预算怎么分给指令多少、给历史多少、给资料多少。怎么更新旧内容直接删掉还是压缩成摘要还是按需检索后再拼入。任何大模型应用本质上都在做这三个决策。做得好模型又稳又省做不好模型就会像一个记忆力差且极易被带偏的人。1.2 最常见的三种 context-mode我在项目里最常打交道的是固定上下文模式、会话上下文模式和知识上下文模式。它们分别对应不同的使用场景。固定上下文模式Fixed Context是最基础的一种把系统人设、行为规则、输出格式、少量示例静态拼在 prompt 开头每次请求都会注入。这种模式用于确保 AI 的角色稳定不会聊到一半开始胡说八道。会话上下文模式Conversational Context面向多轮对话需要带上历史消息才能让模型记得“我们刚才聊到哪了”。但整段历史不能无限增长所以通常配合滑动窗口和摘要机制。知识上下文模式Knowledge Context面向问答根据用户当前问题从外部知识库检索相关内容动态拼入上下文。RAG检索增强生成用的就是这种模式。现实中大多数应用不是单用一种而是组合使用。比如一个售后客服机器人通常是固定人设 会话历史 知识库检索结果三合一。我们之前做的上下文管理模块本质上就是一个可配置组装层——根据业务场景决定各类内容按什么顺序、什么比例进入模型视野。1.3 谁应该认真研究 context-mode如果你只是用现成的聊天产品可能不需要管这些。但如果你是以下角色context-mode 就是绕不开的课题大模型应用开发者你要为回答质量和成本负责每一段进上下文的 token 都影响账单。产品经理用户体验上的“AI 突然变傻”“AI 健忘”“AI 乱编”根源几乎都在上下文策略。提示工程师提示词写得好只是一部分怎么把动态内容优雅地布局到上下文里才是工程化难点。可以说context-mode 是大模型从“能跑”到“跑得稳”的必经之路。2. 为什么上下文的底层逻辑决定成败2.1 token、窗口和注意力三个不可模糊的基础概念聊 context-mode绕不开三个基础概念token、上下文窗口和注意力机制。token 是模型处理文本的最小单位。英文里一个单词可能拆成一到两个 token中文则大致一个汉字约 0.6 到 1.5 个 token具体取决于分词器和模型。你不能用“字数”去估算上下文消耗更靠谱的方式是直接用模型配套的 tokenizer 统计。上下文窗口是单次请求所能容纳的最大 token 总量输入和输出共享这个空间。也就是说如果你把输入塞到接近窗口上限模型能回应的空间就非常小了轻则截断重则直接把回答压成几个字。注意力机制更关键。模型在推理时会计算每个 token 与当前位置的相关性权重但它们的注意力不是均匀分布的。研究发现长文本中位于中间位置的信息容易被“稀释”这也就是业内常说的 lost in the middle。你在上下文中间偷偷塞一条关键业务规则很可能被模型无视。所以 context-mode 不仅要管“放哪些内容”还要管“按什么顺序放”。关键信息优先放在开头或者靠近最新用户问题的位置模型响应率会高很多。2.2 成本模型上下文每多一个 token 都在烧钱很多人做应用时最容易忽略的就是上下文带来的成本膨胀。输入 token 和输出 token 的计费通常不是 1:1主流大模型的输出单价一般是输入的 3 到 5 倍。而 context-mode 影响最大的恰恰是输入侧。举个具体计算例子。假设某次请求包含系统指令800 tokens历史消息6000 tokens检索资料2000 tokens用户问题400 tokens那么输入合计就是 9200 tokens。假设模型输出约 800 tokens。我按当前主流模型大致 $2 / 百万 tokens 的输入单价、$10 / 百万 tokens 的输出单价来估算输入成本9200 / 1000000 × 2 0.0184 美元输出成本800 / 1000000 × 10 0.008 美元单次请求成本约 0.0264 美元单看一次请求好像不贵但如果你的应用每天有 10 万次请求那一天就是 2640 美元一个月约 8 万美元。这里还没算额外的向量检索和存储成本。更关键的是其中那 6000 tokens 的历史消息可能一半以上都是寒暄和冗余信息。如果你能把历史压缩到 2000 tokens单次输入成本直接下降三分之一以上。context-mode 的价值不只是让模型变聪明更是让成本结构变健康。2.3 为什么不能一味追求“把一切塞进上下文”有些团队迷信长上下文模型遇到问题就加大上下文觉得“塞得越多模型就越懂”。我踩过这个坑结果并不好。第一是成本失控。上下文每多一千 token每天的成本都是指数级叠加。第二是注意力稀释。模型对超出一定量的历史内容记忆效果会衰减。你塞了 50 轮对话进去它未必真的“记住”反而可能被早先的无关闲聊带偏。第三是延迟变高。输入 token 越多首字响应越慢用户体感非常明显。一个客服机器人如果每次都背负 20k 的上下文再好的回答也会被网络耗时拖累。真正好的 context-mode目标是“刚刚好”只保留当前任务必需的信息删掉噪音把预算花在刀刃上。3. 四种 context-mode 的实现细节3.1 固定上下文模式控制 AI 的人设和边界固定上下文模式适合所有需要“稳定角色”的场景。系统指令就是最典型的实现。我以前做客服机器人的时候第一版系统指令写得很随意结果模型经常自作主张。后来我把指令结构化效果立刻不一样SYSTEM_PROMPT ( 你是企业售后客服助手「小贝」。\n 职责解答产品使用问题、收集售后诉求。\n 边界不讨论价格细节不承诺赔偿不编造未公布政策。\n 输出风格先结论后依据用列表分点说明。\n 信息不足时必须明确说这个问题我需要转人工处理。 ) def build_fixed_context(user_text: str) - list[dict]: return [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_text}, ]固定上下文看起来简单但有两个容易被忽略的细节。第一固定内容本身也要控制长度。规则应该写成“做什么、不做什么”的要点而不是长篇大论的人设小作文。系统指令每多一百 token就挤压了后续业务内容的空间。第二固定并不等于死板。你可以在构建 prompt 的时候把动态变量注入进去比如用户昵称、会员等级、当前页面、历史标签让同一套固定指令在不同场景下产生差异化表现。def build_fixed_context(user_text: str, profile: dict) - list[dict]: personalized SYSTEM_PROMPT ( f\n当前用户{profile[nickname]}会员等级{profile[level]} ) return [ {role: system, content: personalized}, {role: user, content: user_text}, ]这种模式是所有复杂 context-mode 的地基建议先从它入手。3.2 滑动窗口模式多轮对话的修剪策略多轮对话中最常见的问题是历史消息无限堆积。处理方式最直接的就是滑动窗口只保留最近 N 条消息更早的扔掉。我在设计对话上下文时不会简单地按条数截断而是按 token 数截断这样更可控from typing import List, Dict MAX_HISTORY_TOKENS 5000 def estimate_tokens(text: str) - int: # 中文场景可按 1 个字符约 0.7 token 估算 # 线上环境建议直接用官方 tokenizer 统计 return max(1, int(len(text) * 0.7)) def build_conversation_messages( history: List[Dict[str, str]], user_query: str ) - List[Dict[str, str]]: keep [] total 0 for msg in reversed(history): tokens estimate_tokens(msg.get(content, )) if total tokens MAX_HISTORY_TOKENS: break keep.insert(0, msg) total tokens keep.append({role: user, content: user_query}) return keep为什么从尾部开始倒序遍历因为最近的对话通常比最初的寒暄更有价值。如果从头部开始截很可能留了一堆开场废话却把用户刚说的关键诉求剪掉了。但滑动窗口也有明显缺陷被剪掉的历史里可能藏着重要前提。比如用户在第 3 轮说过“我要的是安卓版”到第 20 轮问“这个按钮在哪里”如果第 3 轮已经被剪掉了模型就会默认你在问 iOS 版本。所以更成熟的做法是“摘要 最近 N 轮”。模型或规则脚本先把旧历史浓缩成一段摘要然后把摘要作为 system 内容注入history_summary 用户已确认购买安卓版正在咨询安装步骤 summary_message {role: system, content: f以下是对早期对话的摘要{history_summary}}摘要 滑动窗口的配合能让模型既保有早期关键信息又不至于被冗长历史压垮。这是多轮对话类应用最值得优先采用的 context-mode。3.3 检索增强上下文知识问答的标准姿势当应用需要回答专业问题时无论上下文窗口多大你都不可能把整本手册塞进去。检索增强上下文就是解决这个问题先根据问题检索相关片段只把命中内容拼入上下文。标准的实现流程是知识切片、向量化、相似度检索、结果组装。def build_rag_context(query: str, k: int 5) - str: query_vec embed(query) hits vector_db.search(query_vec, top_kk) chunks [] for i, hit in enumerate(hits, 1): chunks.append(f[{i}] {hit[text]}) return \n\n.join(chunks)拿到检索片段之后再组合成最终请求prompt f 仅依据下面资料回答问题答案中用 [编号] 标注引用来源。 如果资料不足以回答直接说“根据现有资料无法确认”。 {rag_context} 问题{query} 这里有一个很多人第一次做会忽略的问题检索出来的资料不一定都相关。如果你把 top-10 的结果全部塞进去无关片段反而会成为噪音。我的经验是先做一轮粗召回再做一轮重排只保留最相关的 3 到 5 段。重排环节可以用更小更快的模型也可以用规则过滤比如检查关键词重叠度、时间新鲜度。另外检索片段在 prompt 里的位置也有讲究——放在用户问题附近比放在一长串固定指令后面更有效因为越靠近最终任务模型的注意力权重越高。3.4 分层记忆模式短期、长期、全局当应用需要长期服务同一个用户时单一上下文模式是不够的。我会做分层记忆把信息按生命周期拆成三层层级生命周期典型内容存储位置注入时机会话级单次会话当前对话历史、临时状态内存 / Redis每次请求用户级长期用户偏好、账号信息、历史诉求摘要用户画像库会话开始时全局级业务级产品文档、政策、知识库向量库 / 文档系统检索命中时每次构建请求时按层级依次组装def build_context(user_id, session_id, query): layer1 build_system_prompt() # 固定上下文 layer2 get_user_profile(user_id) # 用户级记忆 layer3 get_recent_history(session_id) # 会话级记忆 layer4 build_rag_context(query) # 全局知识 return assemble(layer1, layer2, layer3, layer4)用户级记忆特别适合在会话开始时一次性注入。比如用户上次问过“你们有没有企业版”第二天再来时我们可以把这条历史诉求摘要放在开头模型自然就会延续上次的语境。全局知识按需注入这是避免上下文臃肿的关键。只有用户问到价格才检索价格相关的文档只有用户问到安装才检索安装手册而不是把所有内容都常驻。4. 实操案例搭建带 context-mode 的知识对话助手为了把前面的理论落到地面我拆一个最近做的“产品知识对话助手”实现过程。这个项目经历三版迭代每一版改动不算大但效果差距明显。4.1 第一版固定人设 全量历史失败版本第一版图省事直接把系统指令和全部会话历史拼起来发给模型。核心代码只有几行messages [{role: system, content: SYSTEM_PROMPT}] messages.extend(history) messages.append({role: user, content: query})刚开始测试感觉还行聊到第 5 轮都正常我就放松了警惕。结果用户多聊几轮之后问题开始出现输入 token 迅速膨胀第 20 轮时已经接近 18k tokens。模型开始遗忘早期关键信息比如用户在第 2 轮就说过“我要的是基础版”到第 15 轮还在推荐高级版。响应时间变长单次请求从 1 秒左右涨到 2.8 秒。因为上下文里有太多无关寒暄模型偶尔会把玩笑话当成业务需求。这一版的问题不是模型不行而是我根本没做 context-mode直接把模型当成了无限记忆体。4.2 第二版滑动窗口 摘要稳定版本第二版我加入了滑动窗口和摘要机制。当会话历史超过 6000 tokens 时先把最旧的部分交给模型生成一段不超过 150 tokens 的摘要然后只保留摘要和最近 8 轮消息继续后续对话。def compress_old_history(old_messages: List[Dict]) - str: text \n.join(m[content] for m in old_messages) response llm.chat(messages[ {role: system, content: 将以下对话压缩成客观摘要保留关键信息、决策和用户诉求不超过150字。}, {role: user, content: text}, ]) return response这一版上线后效果立竿见影单次请求输入 token 从 18k 降到 8k 左右响应时间降到 1.4 秒模型健忘的问题基本消失。但还有一类问题解决不了——用户问特定产品参数时模型只能凭训练记忆回答遇到未公开或新上市的产品回答靠不住。4.3 第三版检索增强 引用可用版本第三版我加入知识检索。把产品手册、常见问题文档切成约 300 字一个片段放入向量库。用户提问时先检索 top-5 片段再和系统指令、最近历史一起组装。关键步骤是给检索结果编号并要求模型在回答中标注引用来源def build_third_version_messages(query, history): context build_rag_context(query, k5) system_prompt ( SYSTEM_PROMPT \n以下是本轮任务相关的产品资料回答必须基于资料并用 [编号] 标注引用\n\n context ) messages [{role: system, content: system_prompt}] messages.extend(history[-8:]) messages.append({role: user, content: query}) return messages这个版本上线后幻觉率大幅下降用户明显感觉回答“更靠谱了”因为每个关键结论都能追溯来源。成本反而更低因为检索片段 2000 tokens 虽增加了输入但历史被压缩得更彻底总输入稳定在 6k 左右。4.4 三个版本的实测对比我在内部做了小范围人工评测重点看回答准确率和用户满意度。数据是相对结果不同模型不同场景会有差异但趋势很有参考价值版本上下文策略平均响应时间单次输入 token幻觉率满意度V1固定人设 全量历史2.8s18k31%6.2/10V2摘要 滑动窗口1.4s8k16%7.5/10V3检索增强 引用1.1s6k6%8.8/10三版代码改动加起来不超过 200 行但体验差距非常明显。这也验证了我一直坚持的观点优化大模型应用先优化上下文而不是先换更大的模型。5. 常见问题与排查速查表5.1 高频问题速查表做 context-mode 过程中我整理了一份问题排查表基本覆盖了日常开发会遇到的大部分坑症状可能原因排查方法解决方案模型越来越健忘历史裁剪策略简单粗暴打印最终 prompt检查被裁剪内容位置改用“摘要 最近 N 轮”回答突然开始胡说检索内容损坏或切块过碎检查向量检索命中的片段是否可读重新清洗切片数据设置 chunk 重叠响应速度慢固定指令太长 历史太多统计每次请求的输入 token 数压缩系统指令收紧窗口成本明显偏高每轮都塞全量历史查看 cost 面板观察走势做 token 预算固定上下文和历史的配比调 API 频繁报窗口溢出输入 输出超过模型限制查看错误码计算 max_tokens对输入做最大预留输出不能占满窗口模型不遵守指令系统指令被埋进长上下文中间检查 prompt 结构顺序把系统指令放开头关键规则放开头5.2 三个最值得收藏的避坑技巧第一估算 token 不要用“字数”当实际标准。中文文本用 len(text) 乘以 0.7 只是一个粗略估算法真正上线前一定要用官方 tokenizer 统计。你可以在代码里做一个预计算缓存把常见文本的 token 数提前算好运行时直接查表省时省力。第二每次调试时把最终发给模型的 prompt 原样打印出来。很多问题光看代码发现不了一打印 prompt立刻就能看到顺序错乱、摘要丢失、检索资料排在最后等问题。我会在日志里专门记录 prompt 的 token 分布方便事后复盘。第三不要试图在 prompt 里用语气词强制模型“记住”。比如“请一定要记住用户是会员”这种话作用会因为上下文位置而异。真正有效的是把关键信息放在开头或贴近用户问题的位置也可以通过结构化字段重复强调而不是靠自然语言喊口号。5.3 当模型“越来越笨”时的五步检查法如果某一天你发现应用效果明显下滑按照这个顺序排查基本能定位 80% 的问题打印最终 prompt看内容是否完整、顺序是否正确。检查历史消息裁剪是否生效很多事故是因为代码被改成了默认全量填充。检查检索结果与当前问题是否相关有时候向量库数据过期命中了一堆旧政策。检查上下文总量是否过大中间信息是否被注意力机制稀释。检查是否有无关上下文干扰比如把按钮文案、页面源码这些噪音也塞进了 prompt。这套五步法我用了很多次每次都能定位到具体原因。本质上大模型应用的很多“玄学问题”最后都落在上下文工程这个非常现实的工程环节上。6. 一点个人经验context-mode 是长期工程项目做完到现在我逐渐形成一个感觉context-mode 不是一次性设计完就结束的东西它是一个需要持续观测和迭代的长期工程。最核心的心得是“主动取舍”。模型的记忆和能力都有限应用层必须替它做减法。每往上下文里放一段内容都要问一句这条信息对这个任务真的必要吗它放在哪个位置最有效它的生命周期是多久用完要不要清理想清楚这三个问题context-mode 的设计就不会太差。另外提一个后续可以尝试的方向上下文缓存和动态模式选择。对于高频重复的固定指令和检索片段可以走缓存降低成本对于不同类型的用户问题可以由路由规则选择不同的上下文策略比如简单问题走固定模式复杂问题走检索模式。这些都是在当前项目基础上的合理扩展难度不大收益却很直接。最后分享一个小技巧无论用什么框架、什么模型都建议在应用入口加一个统一的“上下文构建层”把系统指令、历史消息、检索内容、用户画像是如何组装的和底层模型 API 分离开。这样后续切换模型、调整策略时你只需要改这个构建层而不用动业务代码。这个习惯帮我省了非常多的返工时间。
返回列表