先说一个我最近处理的真实项目。某团队要做内部知识库问答,业务方提了个很朴素的需求:“让模型记住用户的偏好,下次直接推荐。”结果开发同学的做法是:把用户所有历史记录、全部产品文档、每一条聊天消息,一股脑拼接进提示词,塞给模型。跑了一个月,token费用暴涨,回答却越来越飘,连“用户上个月买过什么”这种最基本的问题都经常答错。
这个场景几乎是我过去一年遇到最多的误解。大家嘴上说的“模型记忆”,跟实际情况差了十万八千里:大模型本身没有记忆,它只有一个“临时工作台”,也就是上下文窗口(Context);而想让模型真正利用海量私有知识,得靠另一套机制——检索增强生成(RAG)。很多人把这两个词混在一起,以为都在解决“记忆”问题就能随便用。实际上,它们解决的是完全不同层面的问题,搞不清楚边界,项目必踩坑。
这篇文章不聊概念,聊透:Context到底是什么、它的限制在哪里、RAG又是怎么工作的、两者该怎么配合,以及我一路上踩过的那些坑。
1. 大模型没有“记忆”,Context只是每次请求的临时工作台
1.1 会话界面给你的“记忆错觉”
几乎所有对话类应用都会让你产生一种错觉:这个模型“记得”你刚才说了什么。你上午问它“帮我写个招聘JD”,下午又说“把那个JD里的福利部分改成双休”,它居然能接上话。这不是因为模型记住了你,而是因为客户端在背后做了件事:把上午的对话记录全部翻出来,重新拼接进下午的请求里,模型相当于又把上午的内容从头读了一遍。
用一个生活类比:你走进一家咨询室,咨询师每次见你之前,助手都会把之前的聊天记录打印好放在桌上。你感觉咨询师“记得你”,其实他只是每次都重新读一遍你的档案。区别在于,人类读一遍上千字的档案还能抓住重点,但大模型读完整段历史之后,注意力早就被长文本里的各种噪声稀释得七零八落。
这就是为什么很多开发者会在多轮对话的中段突然发现模型“失忆”——不是它忘了,而是历史记录太长,关键信息已经淹没在token堆里了。会话界面制造的“记忆错觉”,本质上是一个由消息拼接制造的假象。
1.2 无状态的接口:模型每轮都在“重新读一遍”
很多大模型服务接口在设计上就是无状态的。每次你调用它,它都从第一个token开始重新推理,不会因为你昨天问过“帮我总结一下这份合同”就对今天的请求有任何额外倾向。所有的对话历史、业务上下文、用户偏好,都必须由调用方自己维护,再显式地交给模型。
在实际工程里,多轮对话的维护就是维护一个不断增长的message数组。第一轮是“用户问+模型答”,第二轮在数组尾部追加“用户又问+模型再答”,每次把整个数组重新发给模型。业务上所谓的“记忆”,本质上就是调用方在拼命往这个数组里塞历史。
这里有一个很多新手想不通的关键点:模型不是“想不起来你上周说过什么”,而是“根本不知道你上周说过什么”。想不起来是记忆存储已经存在但提取失败,从来不知道就等于没有任何记忆。这两者的区别直接决定了架构选型——如果误以为模型有长期记忆,你会在提示词层面反复折腾;一旦接受模型每轮都是白纸,你才会认真去设计外部记忆组件,比如摘要、缓存、完整历史压缩,或者干脆上RAG。
1.3 为什么必须把这件事掰扯清楚
我问过不少团队,他们做知识库问答时的心路历程几乎一样:先是想把知识库整个塞进提示词,发现塞不下;然后把窗口换大,发现费用爆炸、效果变差;最后才考虑RAG。这整个弯路,根源就是没想明白“模型没有记忆”这件事。
当你真正接受“每次请求都是一张白纸”之后,思考方式就变了:你不再纠结“模型能不能记住”,而是开始问“这次请求需要给它看哪些内容”。这个转变是整个大模型应用架构设计的分水岭。Context和RAG,本质上就是“给模型看什么”的两种答案。
2. Context窗口:你以为的“内存”,其实是敲满字的工作台
2.1 窗口大小到底意味着什么
上下文窗口通常用token数来衡量。token可以简单理解成模型读取文本的基本单位,一个中文token大致对应一到两个汉字。市面上常见的窗口规格从4K到128K甚至更高,听起来很宽裕,但你实际换算一下就会发现,真实容量没那么乐观。
| 窗口规格 | 大约能容纳中文 | 实际适用场景 |
|---|---|---|
| 4K | 约6000-8000字 | 简短对话、单文档摘要 |
| 32K | 约4-6万字 | 中长文档阅读、多轮问答 |
| 128K | 约15-20万字 | 长文档批阅,但仍装不下真正的知识库 |
一个中型公司的产品手册、规章制度、FAQ合起来,往少了说也有几十万字。128K的窗口装一份完整的产品手册就已经够呛,更不用说你还要在同一个请求里塞对话历史、系统提示词和用户问题。所以,窗口再大,它也不是你装知识的仓库。
2.2 上下文越长,注意力反而越稀
大模型的底层机制是注意力机制:每个token在生成的时候,都要计算自己跟上下文里其他所有token的关联程度。上下文一旦变长,注意力的总量并不会等比例增加,而是被分摊到越来越多的token上。结果就是,关键信息被淹没在大量无关内容里,模型读到后面时,对前面内容的“关注度”已经所剩无几。
有一个在业内讨论很多的现象:当关键信息出现在长上下文的中段时,模型的提取效果往往最差;反而是出现在开头和结尾的内容更容易被抓住。这很像人类阅读体验——一篇文章看太久,开头和结尾记得清楚,中间部分只剩模糊印象。很多项目为了让模型“记住”所有细节,拼命堆Context,结果适得其反,就是这个原理在作怪。
2.3 长上下文的代价远不止费用
很多人只看到窗口变大带来的token费用上升,却忽略了两个更隐蔽的代价。
第一个是计算延迟。上下文越长,模型在生成每个token时需要的计算量越大。请求从发出到首个token返回的等待时间会明显变长,这在一个需要实时交互的客服机器人场景里是难以接受的——用户问一句话,转圈十几秒,体验直接崩掉。
第二个是服务端的缓存压力。长上下文在推理过程中要维护额外的中间状态,这些状态占用的显存和内存会随上下文长度快速增长。哪怕模型接口宣称支持128K,实际并发一高,服务端照样扛不住,最终体现为限流、排队、接口超时。
所以,盲目扩大Context窗口不是免费午餐,它更像是在一张越来越拥挤的工作台上写作业——桌面大了,但你真正需要的那支笔反而更难找,找笔的时间和力气也花得更多。
2.4 你真正可用的“有效Context”只有窗口的一部分
更扎心的是,窗口大小并不等于你能用来装业务内容的长度。一个典型的请求里,至少还要放这几样东西:
- 系统提示词(定义角色和行为规则)
- 少样本示例(few-shot,给模型参考格式)
- 工具定义(如果接了函数调用)
- 输出格式约束
- 当前用户问题
这些内容加起来随随便便几千token。我见过一个项目,系统提示词加工具定义就占了将近一万token,名义上32K的窗口,实际留给业务上下文的只剩不到一半。所以在项目里我一般建议:按窗口容量的百分之七十左右做规划,留出给系统提示词、历史记录和输出余量的空间。如果你用64K的窗口,业务内容规划到40-50K就该打住,而不是硬塞到接近上限。
这也是为什么,当一个任务需要处理的“记忆”规模超过几万字时,你就该认真考虑RAG了。
3. RAG:给大模型装一块可以随时更换的“外挂硬盘”
3.1 RAG的核心思想:先检索,再让模型读
RAG全称是检索增强生成(Retrieval-Augmented Generation),拆开看就是三步:先从外部知识源里检索出跟问题最相关的文本片段,再把这些片段作为额外上下文拼进提示词,最后让模型基于这些检索到的内容来生成回答。
这个思路跟Context有一个根本区别:Context是“把所有东西都端上来”,RAG是“先想想哪些菜有用,只端那几盘上来”。模型本身依然没有任何记忆,知识全部存放在模型之外的知识库里,每次需要的时候,现取一段放进来。这就像给模型接了一块外挂硬盘,里面的资料可以随时更新、随时更换,模型本身完全不需要被重新训练。
3.2 为什么需要RAG:模型的知识是“参数里的化石”
大模型知道什么,是在训练阶段决定的。训练完成那一刻,它的知识就基本固定了——像化石一样封存在参数里。你问它某个常见知识点,它能回答,是因为那句话早早出现在训练数据里。但如果是上周刚发布的新政策、你们公司内部的报销流程、某份合同里约定的付款条款,这些内容根本不可能出现在它的训练数据里,模型自然一无所知。
有人会说:那就重新训练或者微调呗。但微调一个模型的成本、时间和风险,对一个普通业务团队来说通常太高了,而且每两天更新一次资料库,总不能天天微调。RAG提供了另一条路:知识存在外部数据库里,模型每次回答前现查现用,资料更新了,只需要把新文档重新切分、向量化、写入数据库,下一次提问立刻就会用到,完全不需要动模型本身。
3.3 一条完整RAG链路,切成五步看
我习惯把RAG分成离线准备和在线推理两个阶段。离线阶段负责把文档变成可检索的索引,在线阶段负责实时检索和生成。
离线阶段大概是这样的流程:
- 文档加载:把PDF、Word、Markdown等格式的知识文档统一读进来。
- 文本切分:把长文档切成适合检索的片段,这个步骤对最终效果影响极大。
- 向量化:用嵌入模型把每个片段转换成一个向量。
- 建索引:把这些向量存入向量数据库,建立索引供检索使用。
在线阶段则是:
- 向量化用户问题,得到问题向量。
- 在向量库里检索最相似的若干个片段。
- 对检索结果做重排,去掉不相关的内容。
- 把保留的片段拼进提示词,让模型生成回答。
一个最小可用的RAG问答骨架,用伪代码写出来大概是这样的:
# 伪代码示意:最小RAG问答骨架 from vector_store import VectorStore from embedding import embed_texts from llm import chat # 离线准备阶段 docs = load_documents("knowledge/") chunks = split_documents(docs, chunk_size=500, overlap=50) vectors = embed_texts(chunks) store = VectorStore() store.add(chunks, vectors) # 在线推理阶段 question = "我们公司的报销流程是什么?" related = store.search(question, top_k=4) related = rerank(question, related)[:2] prompt = build_prompt(question, related) answer = chat(prompt)千万别小看这个骨架,真实项目里每一个环节都能单独拉开一篇长文来聊。我见过很多项目跑通了这个骨架就以为大功告成,结果上线之后效果惨不忍睹,问题百分之百出在上面这些“看起来很简单”的步骤里。
3.4 检索质量决定生成质量,多花时间在这上面不亏
有一个很容易被忽视的真相:RAG系统的效果上限,主要取决于检索质量,而不是生成技巧。如果检索召回的片段跟问题根本不相关,你提示词写得再好,模型也只能一本正经地胡说八道;如果召回的片段把关键细节切断了,模型再怎么“聪明”也拼凑不出完整答案。
这就是为什么我碰到团队花大量时间调生成prompt、调输出格式,却不愿意认真处理切分策略和检索阈值时,总忍不住劝他们调转方向。生成侧的优化是在最后几米冲刺,检索侧的优化才是决定这条路能不能走通的地基。
4. 什么时候硬怼Context,什么时候上RAG:一套实用的取舍逻辑
4.1 适合让Context承担的工作
Context窗口并不是洪水猛兽,有些场景它就是最合适的工具。
第一类是即时性的信息。比如当前这一轮对话刚提到的内容,用户刚上传的这份文件里的细节,这些都只需要在当次请求中生效,没有必要进知识库。第二类是对话连贯性。多轮聊天中,为了让模型知道前因后果,保留近期几轮对话是必要的。第三类是格式约束和示范。系统提示词、少样本示例这些“规则型内容”,天然就应该放在Context里。
一个判断标准特别好用:如果这个信息只存在当前对话里、以后大概率不会再被其他用户用到,那就放进Context;它是一次性的、局部的。硬塞进RAG知识库反而是浪费存储和检索成本。
4.2 适合让RAG承担的工作
反过来,RAG擅长的是那些“任何用户都可能问、内容可能随时更新、总量很大”的知识。典型场景包括:
- 企业内部规章制度、产品文档、FAQ,动辄几十万字。
- 需要频繁更新的内容,比如每周更新的价格表、每天新增的政策解读。
- 不同用户、不同场景下需要检索不同片段的知识库。
这类知识放进Context窗口根本装不下,装得下也维持不了时效性。RAG的价值在于把知识体量从“窗口限制”中解放出来,让它只受数据库容量的限制——一个几十万甚至几百万条记录的向量库,对现代数据库来说毫无压力。
我用过一张对比表帮团队快速做决策:
| 维度 | 适合用Context | 适合用RAG |
|---|---|---|
| 数据规模 | 几千token以内 | 几万字起步,上不封顶 |
| 更新时间 | 当次对话有效 | 随时更新、立刻生效 |
| 数据来源 | 对话现场产生 | 历史文档、外部资料 |
| 访问模式 | 所有人看同样的内容 | 不同问题检索不同片段 |
| 成本构成 | token费用高 | 存储和检索费用为主 |
4.3 混合记忆架构:系统层、会话层、知识层各干各的
很多成熟的实践不是二选一,而是把两者叠起来用。我把这种方案叫“三层记忆架构”:
第一层是系统层。放角色设定、回答规则、禁止事项这些跟具体数据无关的固定指令。这一层永远存在,用系统提示词实现。
第二层是会话层。放当前对话的近期历史,通常只保留最近几轮,再加一个历史摘要。很多人喜欢把全天对话全部塞进去,其实没有必要,可以把更早的对话先压缩成一段摘要,显著降低token占用。
第三层是知识层。放RAG检索回来的片段。这一层的核心是控制数量:检索结果再丰富,最终进入提示词的片段也要做二次筛选,只保留跟当前问题最相关的两三段。
请求拼装的最终结构大致是:
System: 你是一名客服助手,只依据提供的资料回答,不知道的明确说不清楚。 Context: [对话历史摘要 + 最近3轮对话记录] Retrieved: [RAG召回并经重排后的2段片段] User: [当前用户问题]这套结构帮我解决过很多“窗口不够用”的抱怨。真正的问题从来不是窗口太小,而是你不知道窗口里的内容该由谁负责、各占多少比例。
5. 我在项目里踩过的坑和调优手记
5.1 坑一:把Context窗口撑满,回答质量反而崩了
有段时间我接手一个产品手册问答项目,最初的方案是想直接用128K窗口硬塞整本手册。切换到大窗口之后,效果不升反降:问“包装规格”的时候,模型居然会引用到“售后服务政策”段落里去,答得驴唇不对马嘴。
后来我仔细分析,发现关键信息分布在文档中段,正好落在注意力最容易被稀释的位置。当时我做的第一个改动,不是换更强的模型,而是把文档切成小段,先用RAG检索出相关片段,只把前几段放进Context。改动之后,同一批测试问题集的正确率明显回升。这件事给我的教训是:窗口越大不代表效果越好,信息的“位置”比“总量”更重要。
5.2 坑二:文档切分粒度不对,检索召回一整页“废料”
另一个团队的项目用的是最朴素的切分方式:按固定500字硬切文档。结果有一份“设备操作流程说明”,刚好在“第一步”和“第二步”之间被拦腰截断。用户问“第二步该怎么操作”,检索召回了包含“第二步”的那一小段,但缺失了第一步的前置条件,模型只能凭空脑补。
我后来把切分策略改成了“按章节层级优先切分”,每章内部再按段落和语义边界切,并且在相邻片段之间留了重叠(overlap),让切分处不丢失上下文信息。改完之后,同一个问题的召回命中率明显改善。切分是RAG里最容易偷懒、但回报率最高的环节。
5.3 坑三:检索召回一堆相似重复的内容,生成结果被“同源污染”
还有一个常见问题:知识库里有多个版本的同一份文档,比如三月份版、四月份版、最新修订版。用户问一个流程问题,top-k召回的五条结果全是在讲同一件事,只是表述各不相同。模型面对多条相似片段,很容易被其中过时的信息带偏,生成出前后矛盾的回答。
解决方案也不复杂:入库前做去重,按更新时间和文档版本过滤,排序时使用最大边际相关性(MMR)让召回结果彼此保持差异,而不是一味追求相似度最高。这种问题如果不在召回侧处理,生成侧调再多次prompt也白搭。
5.4 调优优先级建议:先看请求结构,再优化检索,最后碰Prompt
经过几次踩坑,我总结了一套自己的调优顺序,没遇到特殊情况基本不打破:
第一,先统计请求结构。每次调用前,打印出系统提示词、对话历史、检索片段和用户问题分别占了多少token,先在纸面上看清楚窗口是怎么被消耗掉的。
第二,优化检索侧。切分是否合理?重叠参数用了多少?top-k是否过大?召回结果里有多少是真正跟问题相关的?这个阶段应该用一个固定的测试问题集反复跑,记录每次召回的命中情况。
第三,最后才动生成侧的prompt。这时候检索质量已经稳定,再去调语气、格式、输出规范,效果才可观测。
一个小技巧:我会把每个问题的完整请求体——包括最终拼好的提示词和检索召回片段——都保存成日志。排查问题的时候,直接看日志比问模型“你为什么不这样回答”高效得多。请求体是结果,日志才是证据。
根据我个人这些项目下来的感受,判断一个任务该用Context还是RAG,标准其实很简单:如果这份信息只有一份、且就在当前对话里,用Context;如果它可能是任何一份历史文档、任何一条今天刚更新的业务规则,用RAG。最后再分享一个小习惯:任何新的“记忆”方案接入生产前,先用一套固定的测试问题集跑一轮召回命中率,每改一次切分或检索策略,就把上一轮答错的案例重新过一遍。Context和RAG不是两选一的敌人,它们是配合使用的手和眼。希望这篇能让你少走一段我走过的那条弯路。