上下文工程实战:从筛选到组装,让大模型精准获取知识
1. 上下文工程到底在解决什么问题1.1 从一个真实翻车现场说起去年帮一个做智能客服的团队做调优他们的系统接入了大模型知识库文档堆了三千多份产品文档、FAQ、历史工单、内部规范全塞进去了。上线第一周就出问题用户问“退款要几天到账”模型回答的是“根据《内部财务结算管理办法》第7条跨月退款需财务总监审批后T3个工作日处理”——用户直接炸了这是内部文档根本不该出现在用户侧回答里。问题出在哪不是模型不行是喂给模型的东西不对。他们把整个知识库做成了一个大向量库检索的时候按相似度Top-K召回结果内部财务文档和用户FAQ的向量距离很近一起被捞出来了。模型看到什么就说什么它没有“该不该看”的判断力。这就是上下文工程要解决的核心问题在正确的时间把正确的内容以正确的形式放进模型的上下文窗口里。它不是提示词工程换个马甲提示词工程关注的是“怎么说”上下文工程关注的是“给什么看”。1.2 上下文窗口不是垃圾桶很多人对大模型上下文的理解还停留在“能塞多少塞多少”的阶段。现在主流模型的上下文窗口动辄128K、200K token看起来很大但实际用起来你会发现两个残酷的事实第一上下文越长模型注意力越涣散。这不是玄学有明确的研究支撑——斯坦福之前有个实验把关键信息放在长上下文的不同位置模型对中间部分的召回率明显低于开头和结尾业内叫“Lost in the Middle”现象。你塞了10万字进去模型真正“看到”的可能只有头尾那两三万。第二上下文越长成本越高延迟越大。按token计费的模型每多塞1K token都是真金白银。一个日活一万的客服系统每次请求多塞2000 token一个月下来多出来的成本够养一个初级工程师。所以上下文工程的第一性原则是不是给得越多越好而是给得越准越好。这跟做推荐系统是一个道理用户不需要看到所有商品只需要看到此刻最可能下单的那几个。1.3 上下文工程的四个核心动作我把上下文工程拆成四个动作后面每个章节都会围绕这四个动作展开筛选从海量信息源里挑出候选内容这一步决定“有没有”排序把候选内容按重要性排好这一步决定“先看谁”压缩把选中的内容精简到最小必要信息量这一步决定“看多少”组装按特定结构拼装进上下文窗口这一步决定“怎么看”这四个动作环环相扣任何一个环节出问题最终效果都会打折。下面逐个拆解。2. 筛选怎么让AI只看到该看的东西2.1 元数据过滤是第一道闸门最有效也最容易被忽略的筛选手段是元数据过滤。上面那个客服翻车的案例如果他们在入库的时候给每份文档打上visibility: internal或visibility: public的标签检索时先按用户身份过滤一遍根本不会出问题。元数据能打什么标签我一般建议至少打这几类标签类型示例值作用可见性public / internal / confidential权限隔离时效性2024-01 / 2024-06过滤过期内容来源faq / manual / ticket区分内容类型语言zh / en多语言场景业务域refund / shipping / account缩小检索范围元数据过滤的好处是确定性——它不依赖向量相似度这种概率性判断是硬规则。硬规则先过一遍能把候选集从几千份砍到几十份后面的向量检索才有意义。实操中我踩过的坑是元数据字段设计得太细维护成本爆炸。一开始设计了二十多个字段结果运营同学根本填不全最后一半文档的元数据是空的。后来精简到五个核心字段配合自动化打标用规则小模型覆盖率才上去。提示元数据字段宁少勿多每个字段都要有明确的过滤场景没有使用场景的字段不要加。2.2 向量检索的召回策略元数据过滤之后剩下的候选集用向量检索做语义召回。这里有几个关键参数需要调Top-K怎么定太小会漏太大会引入噪声。我的经验值是如果后续有重排序环节Top-K可以设大一点20-50让重排序去精挑如果没有重排序Top-K设小一点3-8直接给模型。客服场景我一般用Top-20召回重排序取Top-5。相似度阈值要不要设要设而且很重要。向量相似度低于某个阈值的内容说明跟问题根本不相关硬塞进去只会干扰模型。阈值定多少取决于你的embedding模型一般余弦相似度0.7是个起点实际要根据业务数据调。我通常的做法是拿一批标注好的问答对跑一遍看相似度分布取一个能过滤掉明显不相关内容的点。要不要做混合检索强烈建议做。纯向量检索对关键词匹配不敏感用户问“RMB-2024-001这个订单”向量检索可能召回一堆讲订单的通用文档但真正包含这个订单号的文档反而排不到前面。混合检索向量BM25关键词能互补实践中召回率能提升15%-30%。2.3 分块策略决定检索粒度文档怎么切块直接决定了检索的精度。切太大一块里混了好几个主题检索到了也有一半是噪声切太小一块里信息不完整模型看了也答不上来。我试过几种分块策略各有适用场景固定长度分块按500 token一刀切简单粗暴适合结构松散的文档。缺点是经常把一句话切断。按段落分块以自然段为单位保留语义完整性适合结构清晰的文档。缺点是段落长度不均有的段落特别长。递归分块先按大标题切再按小标题切再按段落切直到每块小于阈值。这是目前最推荐的方式LangChain和LlamaIndex都有现成实现。语义分块用embedding判断相邻句子的语义相似度相似度骤降的地方切一刀。效果最好但计算成本高适合对精度要求极高的场景。实际项目中我一般用递归分块打底块大小控制在300-500 token块之间保留10%-20%的重叠overlap避免边界信息丢失。重叠部分看起来浪费但实测对召回质量提升明显。2.4 多路召回的组合拳单一召回策略总有盲区生产环境我一般上多路召回向量召回语义相似的内容关键词召回包含特定术语、编号、名称的内容元数据召回符合特定标签的内容热门召回高频访问的内容适合FAQ场景多路召回的结果合并后去重再进入排序环节。合并时要注意不同路的分数不可比不能直接相加一般用倒数排名融合RRF或者各自归一化后再加权。3. 排序与压缩把上下文窗口用在刀刃上3.1 重排序模型是性价比最高的投入召回环节追求的是“不漏”排序环节追求的是“精准”。重排序Rerank用一个小模型对召回的候选做精细打分把最相关的排到最前面。为什么不能直接用向量相似度排序因为向量检索用的是双塔模型query和document分别编码交互发生在最后一步精度有限。重排序用的是交叉编码器Cross-Encoderquery和document拼在一起过模型能捕捉更细粒度的交互精度明显更高。主流的重排序方案有专用重排序模型如BGE-Reranker、Cohere Rerank开箱即用效果稳定LLM打分让大模型给每个候选打相关性分数灵活但成本高、延迟大规则加权结合业务规则调整分数比如时效性高的文档加分、官方文档加分我的建议是预算允许就上专用重排序模型这是投入产出比最高的一环。实测下来加了重排序之后最终答案的准确率能提升20%以上而延迟只增加几十毫秒。3.2 上下文压缩的三种思路排序之后候选内容还是可能超出上下文预算。这时候需要压缩。压缩不是简单截断而是保留核心信息、去掉冗余。思路一抽取式压缩。从原文中抽出最相关的句子拼成精简版。实现方式可以用小模型做句子级相关性打分保留高分句子。优点是保留原文表述不会引入错误缺点是可能丢失上下文连贯性。思路二生成式压缩。让大模型把长文档改写成短摘要。优点是压缩率高、可读性好缺点是有幻觉风险可能改写错信息。我一般只在非关键场景用这种方式。思路三结构化压缩。把非结构化文本转成结构化字段比如把一段产品描述转成{价格, 规格, 适用场景}。这种方式压缩率最高但需要针对每个业务域设计schema前期投入大。实际项目中我常用的是抽取式为主、结构化为辅的组合。关键信息用抽取保留原文辅助信息用结构化压缩。3.3 上下文预算的分配策略上下文窗口是有限资源怎么分配要有策略。我一般按这个比例分系统指令5%-10%定义角色、输出格式、约束条件对话历史20%-30%保留最近N轮对话更早的做摘要检索内容50%-60%这是核心留给筛选排序后的知识用户当前输入5%-10%输出预留10%-15%给模型生成留空间这个比例不是固定的要根据场景调。比如多轮对话场景对话历史的比例要调高单轮问答场景检索内容可以占到70%。对话历史的处理是个难点。全保留会挤占检索空间全丢弃会丢失上下文。我的做法是最近3-5轮保留原文更早的用一个小模型做滚动摘要摘要控制在200字以内。3.4 位置编排的学问前面提到“Lost in the Middle”现象模型对上下文中间部分的信息召回率低。利用这个特性编排时要把最重要的内容放在开头或结尾。我的编排顺序一般是系统指令开头模型必看最相关的检索内容紧跟指令强化注意力次相关的内容中间即使召回率低也有个兜底对话历史靠后用户当前问题结尾紧邻生成位置这个顺序不是绝对的但核心原则是最重要的信息放在模型注意力最集中的位置。4. 组装与实操一套可复现的上下文工程流水线4.1 整体架构设计把前面讲的串起来一套完整的上下文工程流水线长这样用户输入 ↓ 查询理解意图识别、实体抽取、查询改写 ↓ 元数据过滤权限、时效、业务域 ↓ 多路召回向量 关键词 元数据 ↓ 结果合并去重 ↓ 重排序Cross-Encoder打分 ↓ 上下文压缩抽取式 结构化 ↓ 预算分配与位置编排 ↓ 拼装成最终Prompt ↓ 送入模型生成每一层都有优化空间但不要一上来就全上。我的建议是先跑通最小闭环再逐层加码。最小闭环就是元数据过滤 向量召回 简单排序 直接拼装。这个版本一天就能搭起来先看效果再决定往哪个环节投入。4.2 关键环节的代码实现以查询改写为例这是提升召回质量的重要一步。用户的问题往往口语化、有指代、信息不全直接拿去检索效果差。查询改写把用户问题转成更适合检索的形式。def rewrite_query(user_query, chat_history, llm_client): 将用户口语化问题改写为适合检索的查询 处理指代消解、信息补全、关键词提取 prompt f你是一个查询改写助手。根据对话历史把用户的最新问题改写成适合检索的独立查询。 要求 1. 消解代词指代如它、这个替换为具体对象 2. 补全省略的信息 3. 保留关键实体和术语 4. 输出改写后的查询不要解释 对话历史 {chat_history} 用户最新问题{user_query} 改写后的查询 response llm_client.generate(prompt, max_tokens100, temperature0) return response.strip()查询改写看起来简单但效果立竿见影。我做过对比测试加了查询改写之后召回率提升约25%尤其是多轮对话场景提升更明显。再来看重排序的实现from sentence_transformers import CrossEncoder class Reranker: def __init__(self, model_nameBAAI/bge-reranker-base): self.model CrossEncoder(model_name) def rerank(self, query, candidates, top_k5): 对候选文档重排序 candidates: [{content: str, metadata: dict}, ...] pairs [[query, c[content]] for c in candidates] scores self.model.predict(pairs) # 结合业务规则加权 for i, c in enumerate(candidates): # 官方文档加分 if c[metadata].get(source) official: scores[i] * 1.1 # 时效性加分 if c[metadata].get(freshness) recent: scores[i] * 1.05 ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [c for c, s in ranked[:top_k]]重排序模型选型上中文场景我推荐BGE-Reranker系列有base和large两个版本base够用且快large精度更高但慢一些。英文场景Cohere Rerank效果很好但需要API调用。4.3 上下文组装模板组装环节我一般用一个结构化模板把各部分内容按位置放好def assemble_context(system_prompt, retrieved_docs, chat_history, user_query): 组装最终上下文 # 检索内容格式化 docs_text for i, doc in enumerate(retrieved_docs, 1): docs_text f[文档{i}] {doc[content]}\n if doc.get(metadata, {}).get(source): docs_text f来源{doc[metadata][source]}\n docs_text \n # 按位置编排 final_prompt f{system_prompt} ## 参考资料 {docs_text} ## 对话历史 {chat_history} ## 用户问题 {user_query} 请基于以上参考资料回答用户问题。如果参考资料中没有相关信息请如实告知不要编造。 return final_prompt这个模板有几个细节值得说参考资料放在对话历史之前。因为参考资料是本次回答的核心依据放在前面能让模型优先关注。对话历史放在后面起辅助理解作用。每条文档标注来源。这样模型在回答时可以引用来源增加可信度也方便排查问题。明确指令“不要编造”。这是防止幻觉的关键虽然不能完全杜绝但能显著降低编造概率。4.4 参数调优的实操记录分享一组我在客服场景调优的实际参数供参考参数初始值调优后调整理由召回Top-K1030召回不够漏掉相关内容重排序Top-K105太多噪声干扰模型相似度阈值0.60.720.6召回太多不相关内容分块大小1000400大块信息冗余严重分块重叠080边界信息丢失对话历史轮数全部最近5轮早期对话挤占空间上下文预算无限制8K token控制成本和延迟调优过程不是一次性的是持续迭代的。我一般会建一个评测集包含100-200个典型问题每次调整参数后跑一遍评测看准确率、召回率、延迟、成本四个指标的变化。没有评测集的调优就是盲调。5. 常见问题与排查技巧实录5.1 模型答非所问怎么办这是最常见的问题排查思路按这个顺序走第一步看检索结果。把召回和重排序后的内容打印出来人工判断这些内容能不能回答用户问题。如果检索结果本身就不对问题出在检索环节跟模型无关。第二步看上下文组装。检索结果对了但组装后模型没看到可能是位置编排有问题或者被截断了。检查最终prompt里关键内容是否完整。第三步看模型输出。检索对、组装对模型还是答错可能是指令不够明确或者模型能力不够。尝试强化指令或者换更强的模型。我遇到过一个典型案例检索结果完全正确但模型就是不用自己编答案。排查发现是系统指令里写了“请简洁回答”模型为了简洁就跳过了参考资料自己总结。把指令改成“请基于参考资料回答可以适当展开”问题解决。指令的措辞对模型行为影响巨大要反复打磨。5.2 上下文超长被截断上下文超长是高频问题尤其是多轮对话场景。排查和解决思路先算账统计各部分token占比看谁是大头。我一般会写个函数统计系统指令、检索内容、对话历史、用户输入的token数一目了然。再砍冗余检索内容去重、对话历史做摘要、系统指令精简。最后调预算如果砍完还是超就要考虑降低召回数量或者换更大窗口的模型。有个技巧是动态预算根据用户问题的复杂度动态调整各部分预算。简单问题少给检索内容复杂问题多给。实现方式可以用一个小模型判断问题复杂度或者用规则问题长度、实体数量粗略估计。5.3 检索结果重复度高多路召回合并后经常出现重复内容浪费上下文空间。去重策略精确去重内容完全相同的直接去掉近似去重用SimHash或MinHash判断近似重复保留一条语义去重用embedding算相似度超过阈值的只保留一条我一般用精确去重语义去重组合。精确去重快处理明显重复语义去重处理表述不同但意思相同的内容。语义去重的阈值设0.9左右太低了会误删有用信息。5.4 时效性内容处理知识库里的内容有保质期过期内容会误导模型。处理方式入库时打时效标签每份文档标注生效日期和失效日期检索时过滤当前日期不在有效期内的直接排除排序时加权越新的内容权重越高回答时提示如果引用了较旧的内容在回答中注明时间有个坑是有些内容没有明确的有效期比如产品规格只要产品没下架就一直有效。这类内容我一般打permanent标签不参与时效过滤。5.5 常见问题速查表问题现象可能原因排查方向解决手段答非所问检索结果不相关打印召回内容调阈值、加查询改写编造答案参考资料不足检查召回数量增加召回、强化指令回答不完整上下文被截断统计token占比压缩内容、调预算回答过时检索到旧内容检查时效标签加时效过滤响应慢上下文过长看token数压缩、减召回成本高每次塞太多看平均token优化压缩、缓存多轮对话混乱历史处理不当看历史保留策略滚动摘要、限轮数权限泄露元数据过滤缺失检查过滤逻辑加权限标签、强制过滤5.6 几个反直觉的经验经验一召回数量不是越多越好。我试过把Top-K从5加到20准确率反而下降了。原因是噪声内容干扰了模型判断。后来加了重排序Top-K设30召回、5输出效果最好。召回要宽输出要精。经验二分块不是越小越好。分块太小单块信息不完整模型看了也答不上来。我试过200 token的分块召回率上去了但答案质量下来了。400-500 token是个比较平衡的点。经验三重排序不是万能的。如果召回阶段就没召回到正确内容重排序再强也没用。召回决定上限排序决定下限。两个环节都要抓。经验四评测集比调参重要。没有评测集你调参就是凭感觉今天调好了明天可能又坏了。建一个100-200题的评测集每次改动跑一遍用数据说话。这个投入前期看起来麻烦后期省的时间是十倍百倍。6. 上下文工程的边界与延伸6.1 什么场景不需要上下文工程不是所有场景都需要复杂的上下文工程。如果你的场景是单轮简单问答不需要外部知识知识量很小全部塞进上下文也不超预算对延迟极度敏感加任何环节都不可接受那简单的提示词工程就够了不用上上下文工程这套。技术选型要看场景不要为了用而用。6.2 上下文工程和微调的关系有人问既然上下文工程这么麻烦为什么不直接微调模型把知识灌进去我的看法是两者解决不同问题不是替代关系。微调解决的是“模型会不会”的问题——让模型学会某种能力、某种风格、某种格式。上下文工程解决的是“模型知不知道”的问题——让模型在特定时刻获取特定知识。知识是动态的今天的产品价格明天可能就变了微调跟不上这个变化速度。而上下文工程可以实时更新知识库改完立即生效。所以知识类的内容用上下文工程能力类的内容用微调两者配合使用效果最好。6.3 后续可以扩展的方向这套流水线跑通之后还有几个方向可以继续深挖方向一自适应上下文。根据问题类型自动调整策略事实型问题重检索推理型问题重历史创意型问题重指令。这需要先做问题分类再路由到不同的处理流程。方向二上下文缓存。高频问题的上下文可以缓存下次遇到同样问题直接复用省去检索和组装的开销。缓存key可以用查询改写后的文本命中率更高。方向三多模态上下文。现在很多场景需要处理图片、表格、PDF这些内容的检索和压缩比纯文本复杂得多。图片需要OCR或视觉embedding表格需要结构化解析PDF需要版面分析。这是个大工程但价值也大。方向四上下文质量监控。上线之后要持续监控上下文质量比如检索命中率、重排序分数分布、上下文利用率等指标。发现异常及时告警避免悄无声息地劣化。我个人在实际操作中的体会是上下文工程最难的从来不是技术实现而是对业务的理解。你得知道用户会问什么、哪些内容重要、哪些内容敏感、什么算好答案。这些判断没有标准答案只能泡在业务里慢慢磨。技术方案可以抄业务理解抄不来。所以如果你刚开始做别急着上复杂架构先花时间把业务摸透把评测集建好后面的技术选型自然就清晰了。