news 2026/10/22 4:13:54

上下文工程实战:如何让AI看到该看的内容?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
上下文工程实战:如何让AI看到该看的内容?

老实说,在做了大量类 GPT 应用的落地调试之后,我越来越确认一个判断:现在很多项目的问题,不在于模型不够聪明,而在于我们“喂”给模型看的内容不太对劲。上下文工程(Context Engineering)这个词,现在讨论得越来越热,但它本质要解决的,就是一件很简单、也常常被忽视的事——在打开模型上下文窗口的那一刻,让它看到它该看到的东西,过滤掉不该看的东西。

我最初接触这个概念,是被一个典型的生产事故逼的。某个内部知识库问答助手,资料齐全、检索也做了,但用户问“上季度某条业务的赔付金额是多少”,模型煞有介事地混着三份不同年份的文档,给了一个完全错误的数据。当时第一反应是骂模型太笨,后面把这次对话的完整上下文拉出来一看才意识到:模型读到的那些片段,顺序是乱的、时效是旧的、真正相关的段落还挤在上下文最中间的位置。它不是不会回答,是根本没“看对”东西。

这篇文章,我想把上下文工程拆开讲清楚:为什么它和普通的提示词工程不是一回事、让 AI 看到“该看的东西”具体涉及哪些环节、以及我实际搭过的一套最小可用上下文管线是什么。适合正在做知识库问答、RAG 项目、或者想让长文本、多文档场景稳定输出的人参考。

1. 为什么一个看似聪明的 AI,经常“看不见”关键信息

先做一个实验,模拟一下大部分人的第一直觉。我们以为给模型的上下文窗口塞满相关资料,它就会像人读书一样,先整体浏览一遍,然后精准定位答案。但在实际的注意力机制下,模型并不是在看“整篇文档”,它是在一个固定长度的 token 序列里,为每个位置分配注意力权重。窗口开了多大,和它能不能用好这个窗口,完全是两件事。

1.1 “Lost in the Middle”:注意力会被稀释

有一类现象在业内已经反复被验证:当一个关键信息出现在一个很长上下文的大约中间位置时,模型出错的概率会显著上升;而放在开头或结尾,准确率会明显更好。这就是常说的“Lost in the Middle”效应。

我刚开始很难接受这个现象,因为直觉上“中间”也是信息,模型凭什么对中间不敏感?后来用了一个特别土的类比才真正理解:想象你坐在舞台下,聚光灯打到的地方演员表现得再卖力你都能看清,但舞台上同时有十个演员,灯光均匀撒下去,任何一个角色的表情你都看不清楚。注意力机制里,开头位置有“强先验”优势,结尾位置往往是最新指令或最近被写入的内容,而中间那几十上百个片段,互相之间还要抢注意力配额,关键的那句话很容易被周围的噪声“稀释”掉。

长上下文能力上限再高,落到一个具体输入里,信息的位置结构仍然在实实在在影响结果。

1.2 Token 预算的本质:资源分配问题

既然模型只能在一个有限窗口里处理问题,上下文工程的第一性原理其实就是资源分配。每次调用,我们付的是 token 成本,模型能利用的是窗口容量,而真正能带来答案的那一小段信息,可能只占整个上下文的百分之几。

有个朋友和我说过一句让我印象很深的话:“你的 prompt 写了 5000 字,里面只有一句话是真正有用的,那你不是在写 prompt,是在给模型制造干扰。”从系统设计角度,上下文窗口就是一个有限的“注意力预算”,检索、筛选、排序、截断、压缩、记忆管理,本质上都是在做预算的分配。

顺着这个逻辑往下走就会发现,上下文工程要解决的问题变成了三个非常具体的点:

  • 哪些内容有资格进入窗口?
  • 这些内容在窗口里按什么顺序、什么结构摆放?
  • 窗口里的内容在多轮交互中如何新增、淘汰、更新?

这三个问题,每一条都直接影响输出质量,但它们都不属于传统 prompt 工程讨论的范畴。

2. 上下文工程不是“更长的 Prompt”:它到底改了什么东西

我见过很多团队,一提到上下文管理,第一反应是堆 prompt——在系统提示词里加一堆“请严格基于资料回答”“请忽略无关内容”。这些说法不是没用,但效果很快会碰到天花板。原因很简单:你可以用措辞影响模型的输出倾向,但你很难用措辞改变模型实际收到信息的质量和结构。后者才是上下文工程的主场。

2.1 Prompt 讲的是“怎么回答”,Context 讲的是“回答的依据”

我们可以把一次完整的大模型调用拆成两部分:模型“看到什么”,和模型“照着什么原则去说”。Prompt 偏向于定义原则、语气、输出格式、角色约束;Context 则负责决定输入材料里到底放哪几个 chunk、放多少、怎么排列、信息如何与当前用户问题对齐。

举个很常见的例子。同样是回答“某产品的退货政策”,你可以把 prompt 写成:“你是客服助手,请依据政策回答,不知道就说不知道。”但如果上下文里只有一份 2022 年的旧政策,prompt 写得再漂亮,模型也只能依据旧政策回答,还很可能因为旧政策里恰好有“支持退货”字样就照单全收。Prompt 决定模型怎么看问题,Context 决定模型看什么材料。

这是两种完全不同层级的干预:一个是行为约束,一个是信息供给。上下文工程正是补上了“信息供给”这一块的系统性方法。

2.2 上下文工程的三个维度:来源、组装、生命周期

为了不把概念说得太虚,我在实际项目中习惯把上下文工程拆成三个可执行的维度:

来源工程(Source Engineering):决定哪些内容值得进入候选池,包括检索系统的构建、数据切分粒度、元数据设计、时效性识别、权限过滤等。这部分决定了“有没有好东西可选”。来源做不好,后面的排序和组装都是无米之炊。

组装工程(Assembly Engineering):决定候选内容以什么顺序、什么层级结构进入模型上下文,包括 chunk 排序、指令的位置、检索块与用户问题的间隔、是否需要摘要化前置等。这部分直接干预注意力分配。

生命周期工程(Lifecycle Engineering):解决的是跨轮次问题,也就是对话过程中,哪些旧信息要保留、哪些要压缩、哪些该被新检索结果替换,本质上就是长期记忆与短期上下文的管理。

很多人以为上下文工程等于 RAG,其实不太准确。RAG 更多是解决“从哪里找信息”,而上下文工程是覆盖“找到之后怎么组织、怎么让模型真正利用、以及随着对话演进如何动态维护”的完整链路。

了解这一层之后,才有办法落到技术选型和代码实现。否则很容易陷入那种“检索过程很 fancy,最终答案却稀烂”的尴尬局面。

3. 让 AI 看到该看的东西,具体靠哪几步

到这一节,我开始讲实际操作。这三步是目前我觉得性价比最高、见效最明显的核心链路:检索、重排、组装。缺一步,整套流程的稳定性都会下降。

3.1 第一步:检索——别只看向量相似度

很多刚接触知识库问答的人,会把“语义检索”当成唯一的召回手段。给文档切块、embedding、向量库检索 top_k,然后直接拼进 prompt。这套流程在小规模、干净数据上还可以;一旦文档量大、内容重叠度高、存在多版本、多时间线,纯向量的召回质量就会非常不稳定。

原因在于,语义相似度并不等于答案相关度。用户问“今年端午节的放假安排”,一个讲去年端午调休政策的段落,语义上也非常相关,但答案完全过时。再比如,用户问“某产品支不支持离线模式”,一段介绍产品整体特性、提到“支持离线”但具体说明在另一段的文本,也可能被高相似度召回,但实际信息密度很低。

所以,我的第一个建议是混合召回。向量召回负责“语义相关性”,关键词召回负责“精确匹配”,时间戳字段负责“时效过滤”,把三路的候选结果合并后去重。尤其对于时效敏感的业务数据(政策、价格、活动规则),时间衰减因子必须参与召回阶段,而不是等到重排阶段才救火。

3.2 第二步:重排——不要迷信“Top-K 就是最好的 K”

召回阶段返回的前 K 个结果,只是“看起来比较像”,并不是“最该被模型看到的前 K 个”。正确的做法是:召回阶段多取一些候选(比如 Top 30),然后用一个更精细的排序模型或规则,把真正与问题最相关的片段排在前面。

我早期在线下项目里,重排逻辑多半是这么做的:

def rerank_candidates(query, candidates, top_k=5): # 先按语义相关度打分 for c in candidates: c.score = calculate_semantic_similarity(c.text, query) # 再做关键词加权,弥补向量在精确匹配上的不足 overlap = count_keyword_overlap(query, c.text) c.score += 0.15 * overlap # 时效惩罚:过期时间越久,惩罚越大 if c.timestamp is not None: age_days = (today - c.timestamp).days c.score -= 0.02 * age_days # 元数据加权:优先权威来源或指定文档类型 if c.source_weight: c.score *= c.source_weight candidates.sort(key=lambda x: x.score, reverse=True) return candidates[:top_k]

这套逻辑虽然简单,但已经能解决大量“检索出来是相关文档、却不是答案所在段落”的问题。业务稍微复杂之后,还可以用专门的重排模型,或者引入 token 级权重去微调。

3.3 第三步:组装——把关键信息放在“灯光最强”的位置

重排之后,就进入组装阶段。这里的核心原则是把最重要、最可能是答案来源的片段放在上下文的最前面(高注意力区域),或者紧挨在用户问题之前(结尾区域),避免让关键内容沉到中间位置。

我一般会把 context 组装成一个带编号的材料块,并用明确的块边界和引用标记帮助模型定位:每个材料块前面标“材料1”“材料2”,再让模型在答案末尾引用对应编号。这么做的直接收益是:模型在生成时,可以在不同材料之间做明确的对比和取舍,而不是把几段内容混成一段含糊的答案。

def assemble_messages(question, top_chunks): messages = [{"role": "system", "content": "你是一个严谨的问答助手。请优先引用给定材料作答;如果材料中没有足够信息,请明确回答“资料不足”,禁止自行推断。"}] chunk_block = [] for idx, chunk in enumerate(top_chunks, 1): chunk_block.append(f"材料{idx}:\n{chunk.text}") # 关键材料放最前面,同时也在用户问题前再放一次最重要的摘要 messages.append({"role": "user", "content": "\n\n".join(chunk_block)}) messages.append({"role": "user", "content": f"问题:{question}\n请只基于上述材料回答,并标明引用。"}) return messages

有人可能担心把材料全塞进 user 会污染对话历史,但实际跑下来,只要上下文结构清晰,模型完全能理解“第一段是材料、第二段是问题”的格式。相比之下,模糊的拼接反而更容易让模型遗漏信息。这里有个更技巧性的点:你可以把重排后得分最高的材料块在组装时重复放一次——放在最前面作“锚点”,这在新一轮对话的关键问题里,经常能明显提升定位准确率。

4. 一个真实的最小闭环:让“边查边看”成为标准动作

这套思路不能只在理论上成立,我把它做成了一个不上生产环境但足够验证方案的最小系统:一个面向内部资料库的问答辅助工具。核心诉求是,用户问一个问题,系统能自动检索、排序、组装上下文,然后交给模型输出。整个 pipeline 只有 5 个阶段,但每一步都踩过不少坑。

4.1 Pipeline 的整体结构

输入是用户问句;输出是模型最终回答,同时附带它实际使用的材料编号,方便人工复查。阶段划分如下:

  1. 查询预处理:抽取关键词、识别问题类型(事实型、对比型、汇总型);
  2. 混合召回:向量召回 + 关键词召回 + 时效过滤,取候选 30 条;
  3. 重排:用上一节的 scoring 方案,保留最相关的 5-8 条;
  4. 上下文组装:按“最相关材料在前、问题放最后”的结构生成 messages;
  5. 模型生成:调用模型回答,并强制输出引用的材料编号。

每个阶段都记录了结构化日志,这一步非常关键。为什么?因为没有日志,你永远不知道模型是在“基于上下文回答”,还是在“凭训练记忆瞎猜”。加一行日志,把所有送入模型上下文的内容存下来,排查准确率问题时能省一半时间。

4.2 一个朴素但有效的实现骨架

def answer_with_context(question): # 1. 查询预处理 query_keywords = extract_keywords(question) # 2. 混合召回 vec_results = vector_db.search(question, top_k=30) kw_results = keyword_index.search(query_keywords, top_k=30) merged = merge_and_dedup(vec_results, kw_results) # 3. 时效处理:默认过滤 120 天前的政策类内容 merged = filter_by_recency(merged, max_age_days=120) # 4. 重排并取 top 6 ranked = rerank_candidates(question, merged, top_k=6) # 5. 组装上下文 messages = assemble_messages(question, ranked) # 6. 调模型 final_answer = llm_chat(messages) return final_answer, [m.text for m in ranked]

这套代码如果跑在干净、切分合理的小规模文档库上,效果已经很能打。但它只解决“单轮问答”的上下文问题,一旦对话多轮化,事情就没那么简单了。

4.3 多轮对话怎么做:别让历史话题把上下文挤爆

单轮场景下,我们只需要筛选“这一问”该看的材料。但真实助理产品一定是多轮的,用户可能先问 A 产品的报价,再问 B 产品的对比,最后还要回到刚才的 A 产品问题。如果每一轮都把检索到的所有上下文 + 全部历史对话一起塞进模型,窗口很快就会爆,而且模型会被越来越长的历史搅糊涂。

我在这个项目里用了一个很务实的策略:多轮记忆只保留精简的“事实清单 + 最近两轮原始对话”。具体来说,每一轮结束时,让模型输出几条结构化的关键信息,比如“用户当前关注的产品、已确认的条件、待确认的事项”,把它存为“记忆摘要”,下一轮再作为上下文的一部分注入。这么做的效果是:历史里真正的决定因素保留下来,冗长的过程文本全部丢掉。

记忆摘要的生成,也用一次上下文工程,而不是一个固定模板:

summary_prompt = ( "对话历史如下。请提炼出3到5条对话状态:" "包含用户当前的目标、已经明确的约束条件、以及尚未回答的问题。" "不要复述全文,提取关键变量即可。" )

这套“检索上下文 + 记忆摘要 + 当前问题”的组合,基本能覆盖大多数多轮问答场景,也让上下文窗口始终处在可管理的规模。

5. 踩坑记录:上下文工程最常见的五个坑与我的排查路径

再好的方法论,最后还是要面对各种意外。我从大量测试和线上问题里沉淀了五个最容易让人挠头的坑,每一个都配有完整的排查链路,不是单纯给结论。

5.1 检索命中了,但答案还是不对——问题出在“顺序”

现象是:你手动验证检索结果,明明第一条就是正确答案所在的段落,模型最终却答错了。排查时先把这次请求实际写入模型上下文的内容 dump 出来,你会发现关键段落在窗口中间,前后还堆着好几段高相似度的干扰文本。

这条的排查链路很简单:加日志 → 查看实际上下文 → 检查位置 → 调整组装顺序。我在上面的组装方案里把所有材料按相关性倒序放在问题前,理论上关键材料在开头。但如果你用的是纯 RAG 框架,很可能默认把检索结果按“原文出现顺序”拼接,这时就要在框架的 prompt 组装环节做重排,而不是改检索参数。

5.2 资料是旧的,模型永远答出过时信息

知识库一旦涉及政策、价格、活动规则,时效性问题和相关度问题几乎是并列第一的。刚开始我以为靠“把最新文档更新到库里”就完事了,但旧文档不会自动消失,语义相似度依然很高,召回照样把它捞上来。

我后来在文档入库时强制要求打三份元数据:更新时间、生效时间、来源优先级。每次检索结束后,先做一轮“时效前置过滤”,再做重排。还有一个更狠的兜底:检索结果里如果有两篇文档讲同一件事但时间不同,直接删旧保新。这样从源头避免模型“看到两个矛盾的答案”。

5.3 检索质量差,却指望 prompt 兜底

有一种幻觉是,上下文工程翻车的时候,可以通过在 system prompt 里写再多的“请忽略无关内容”“请严谨回答”来补救。我可以负责任地说,这条路走不通。Prompt 约束只是输出侧的行为约束,当上下文里的信息本身噪声极大、相关片段只有一句时,模型几乎一定会被高注意力权重的无关片段带偏。

排查链路:先看检索 top 10 的命中质量,如果 top 5 里没有一条真正相关,说明问题出在召回/切分/Embedding,而不是 prompt 写不好。这时候花一周调 prompt,不如花一天优化检索词扩展和切分粒度。

5.4 多轮对话滚雪球,模型开始“忘记”早期信息

这个坑在长会话里特别明显。用户聊了 20 轮,我们把过去所有轮次原文都塞进上下文,结果模型会过度关注最近一轮的内容,记不清 10 轮前明确说过的需求。这就是典型的“上下文生命周期”管理不到位。

我的排查链路是:检查每一轮的 token 消耗曲线,如果上下文基本只增不减,那问题就是没有压缩机制。另一个验证方法是做“回问测试”:在 20 轮之后,问一个基于第 3 轮事实的问题,看模型能否答对。答不出来,就是记忆机制没设计好,而不是模型变笨了。

5.5 只评估“最终答案”,不评估“AI 看到的内容”

最后一个坑,也是最隐蔽的。很多团队上线前只评测最终答案准确率,不看模型实际读到的上下文质量。于是很多问题被误判为“模型能力不够”,实际上是上下文侧已经歪了。

我现在每个评测样本都会保存三个视图:召回命中情况、重排后顺序、实际送入模型的 messages。只有当“重排后的 top 1 是否等于答案所在段落”这一项指标稳定了,再谈 prompt 优化才有意义。换句话说,评估上下文本身,比评估答案更前置、更本质。

6. 上下文工程的边界和它值得投入的地方

写到这里,可能会给一种“上下文工程是万能的”错觉。其实不是。上下文工程不能解决的问题也很明确:如果模型本身不具备某项推理能力,再好的上下文也只能提供信息,不能替代推理;如果知识库里压根没有答案,上下文工程也不能无中生有。

但它的价值恰恰在于,在不动模型参数的前提下,把模型能力的发挥上限往上抬了一大截。对小团队来说,这是成本最低的效果优化路径——不需要重训模型、不需要堆更多算力,只需要把“模型看到的输入”当做一个严肃的设计对象。

我现在判断一个 AI 应用团队有没有真正成熟的工程能力,其实就看两点:第一,他们有没有一套完整的上下文生命周期管理方案;第二,他们面对一次错误输出,是先去查上下文日志,还是先改 prompt、骂模型。前者是工程思维,后者是玄学调参。

再说一个最近体会特别深的点:上下文工程不应该跟提示词工程割裂开。两者要同时设计——prompt 负责定义模型“怎么想”,context 负责保证它有“正确的材料去想”。我在新的项目里,已经把这两件事整合成一个上线前 checklist:先确认上下文满足“相关度最高、时效最新、位置最优、长度可控”四要素,再谈输出格式和风格约束。

如果你正在做知识库问答、Agent 记忆管理或者任何依赖大模型长期记忆的任务,我建议你也从“模型看到了什么”这个视角开始做一次全面审计。很多时候,答案质量的瓶颈根本不在模型,而在上下文里那些该看不该看的内容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 4:13:47

GitHub热榜深度解读:从star增长到开源项目筛选与学习指南

每天早上一杯咖啡的功夫,除了看天气,我还会固定刷一眼 GitHub 的热门日榜。2026年4月4日的榜单照例五花八门:和 AI 沾边的项目占了大半屏,后排挤着几个云原生的调度组件,时不时还冒出一个小而美的命令行工具。很多人刷…

作者头像 李华
网站建设 2026/10/22 4:12:02

Spring Boot整合Modbus实战:从RTU到TCP的工业数据采集指南

先把结论放前面:Spring Boot 整合 Modbus 这件事,说难不难,说简单也不简单。难的地方不在 Spring Boot 本身,而在于你对 Modbus 协议栈的掌握程度、对设备端行为特性的理解,以及怎么把一套轮询机制优雅地嵌进 Spring B…

作者头像 李华
网站建设 2026/10/22 4:11:58

红外目标检测实战:YOLO+动态规划实现稳定轨迹关联

红外图像里的目标检测,一直是视觉工程里又爱又恨的活儿。爱的是它能在夜间、雾天、强逆光这些可见光几乎报废的场景里稳定输出,恨的是它给你看的图实在谈不上友好:单通道灰度、对比度低、边缘糊成一片,目标往往就是一团亮斑&#…

作者头像 李华
网站建设 2026/10/22 4:11:58

WPS AI高效办公实战:从加班做表到对话出稿的进阶指南

1. 这门课到底在解决什么问题1.1 从“加班做表”到“对话出稿”的转变逻辑我第一次接触AI办公类课程是在两年前,当时市面上大多数内容还停留在“教你写提示词”的阶段,讲得玄乎其玄,但落到WPS这个具体工具上,几乎没人讲透。甘彬老…

作者头像 李华
网站建设 2026/10/22 4:11:33

光功率是什么?光纤链路故障排查与光功率计实用指南

1. 先聊一个实际问题:光功率是什么,为什么每个搞网络的人都绕不开它1.1 从一次深夜断网的经历说起有阵子我在运维一个跨楼宇的办公网络,核心交换机到各楼层配线间靠的全是单模光纤。某个周五晚上,楼里三层和五层的同事同时跟我反馈…

作者头像 李华
网站建设 2026/10/22 4:11:30

VTK 8.2.0编译包接入实战:MSVC2019+Qt 5.15.2 Win64环境配置与避坑

简介:面向Windows x64平台上使用VTK与Qt/PCL进行点云显示开发的工程师,此编译包专门解决PCL 1.11.1自带VTK库缺少Qt插件QVTKWidgetPlugin的痛点,可实现在Qt界面中直接嵌入VTK渲染窗口,省去在Qt与VTK版本匹配上的反复试错。压缩包为…

作者头像 李华