☰
context-mode实战:从固定上下文到检索增强的LLM应用优化指南
2026/10/7 16:41:48 网站建设 项目流程

前几天有个朋友跟我吐槽,说他们公司花了两个多月做的智能客服,一上线就被用户骂成“人工智障”:用户连续问了几轮之后,模型就开始忘掉最开始的需求,回答越来越跑偏,最后甚至为了凑答案开始编政策。我说你把会话日志发我看看,结果一眼就看出问题——他们把整段对话历史无脑拼进 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_k=k) 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, k=5) 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/10
V2摘要 + 滑动窗口1.4s8k16%7.5/10
V3检索增强 + 引用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% 的问题:

  1. 打印最终 prompt,看内容是否完整、顺序是否正确。
  2. 检查历史消息裁剪是否生效,很多事故是因为代码被改成了默认全量填充。
  3. 检查检索结果与当前问题是否相关,有时候向量库数据过期,命中了一堆旧政策。
  4. 检查上下文总量是否过大,中间信息是否被注意力机制稀释。
  5. 检查是否有无关上下文干扰,比如把按钮文案、页面源码这些噪音也塞进了 prompt。

这套五步法我用了很多次,每次都能定位到具体原因。本质上,大模型应用的很多“玄学问题”,最后都落在上下文工程这个非常现实的工程环节上。

6. 一点个人经验:context-mode 是长期工程

项目做完到现在,我逐渐形成一个感觉:context-mode 不是一次性设计完就结束的东西,它是一个需要持续观测和迭代的长期工程。

最核心的心得是“主动取舍”。模型的记忆和能力都有限,应用层必须替它做减法。每往上下文里放一段内容,都要问一句:这条信息对这个任务真的必要吗?它放在哪个位置最有效?它的生命周期是多久,用完要不要清理?想清楚这三个问题,context-mode 的设计就不会太差。

另外提一个后续可以尝试的方向:上下文缓存和动态模式选择。对于高频重复的固定指令和检索片段,可以走缓存,降低成本;对于不同类型的用户问题,可以由路由规则选择不同的上下文策略,比如简单问题走固定模式,复杂问题走检索模式。这些都是在当前项目基础上的合理扩展,难度不大,收益却很直接。

最后分享一个小技巧:无论用什么框架、什么模型,都建议在应用入口加一个统一的“上下文构建层”,把系统指令、历史消息、检索内容、用户画像是如何组装的和底层模型 API 分离开。这样后续切换模型、调整策略时,你只需要改这个构建层,而不用动业务代码。这个习惯帮我省了非常多的返工时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询