上下文工程:让模型少猜一点

0阅读9分钟

一次线上事故教会我的事

去年我负责的一个 RAG 客服系统,上线两周后开始出现「答非所问」。用户的提问其实很简单——「我的订单什么时候到」,但模型一会儿引用三个月前的退换货政策,一会儿把内部工单备注当成回复输出给消费者。

我们第一反应是改 prompt——加了「请只根据以下内容回答」「不要输出无关信息」之类的话。改了三版,症状减轻,但没根治。后来我把整条请求拆开看,才发现问题根本不在 prompt 的措辞上:检索回来的 8 个文档片段里,有 3 个是过期政策,2 个是内部运营备注,只有 3 个跟用户问题相关;再加上系统指令和历史对话,整条上下文超过 12000 token,关键信息被夹在第 6 和第 7 个片段之间。

模型不是不想答对,是我给它塞了一桌子互相矛盾的信息,然后期待它自己挑出正确的。

这件事让我意识到,很多线上 LLM 输出问题,根源不是模型能力不够,也不是 prompt 写得不够巧妙——而是上下文本身就没组织好。

上下文工程到底在做什么

Andrej Karpathy 在 2025 年给过一个很精炼的定义:上下文工程(Context Engineering)是「为上下文窗口填充恰当信息」的手艺与科学[1]。它跟 prompt engineering 的区别在于关注点不同——prompt engineering 关心「这句话该怎么说」,上下文工程关心「模型需要知道什么」。

我更喜欢另一个类比:把 LLM 当成 CPU,上下文窗口当成 RAM,那上下文工程就是操作系统的内存管理——你得在每次推理之前,把合适的数据加载进工作区[1]。RAM 再大,装了没用的进程,CPU 也快不了。

DataHub 的一篇对比文章把这个区别拆得更细[2]:

维度Prompt EngineeringContext Engineering
核心问题「怎么措辞?」「模型需要知道什么?」
作用域单次交互系统级信息流
状态管理无状态跨轮次有状态
知识来源写死在指令里运行时动态检索
失败表现语气不对、格式跑偏幻觉、上下文溢出、引用过期数据
可扩展性规模一大就崩为规模化设计

一句话:prompt engineering 是上下文工程的子集,不是反过来。

上下文窗口的四种失败模式

LangChain 团队在总结了大量生产级 Agent 系统后,把上下文失败归纳成四种模式[1]:

上下文中毒(Context Poisoning)——一旦错误信息进入上下文,它不会自我修正。后续所有推理都建立在错误之上,越推越远。我在客服系统里看到的「把工单备注当回复」就是典型例子:一条内部标注被检索回来之后,模型在后续轮次里反复引用它,越修正越离谱。

上下文干扰(Context Distraction)——随着对话历史积累,模型开始过度依赖旧的行为模式。SwirlAI 2026 年的报告指出,当工具数超过 10 个,准确率就开始明显下降;90 个工具能产生超过 50K token 的开销[1]。

上下文混淆(Context Confusion)——无关信息导致工具选择和任务执行出错。这就是经典的「lost in the middle」问题:模型对上下文中间位置的信息注意力显著低于开头和结尾,而且这个问题不会随着上下文窗口变大而改善[3]。

上下文冲突(Context Clash)——跨轮次出现矛盾信息,比如第 1 轮说「用英文输出」,第 8 轮说「翻译成日文」,如果没有显式的冲突解决机制,推理质量就会下降。

把这四种模式对照一下自己系统的失败案例,你大概率能一一归类。我在另一个多轮对话的翻译 Agent 里还碰到过上下文冲突:前 5 轮一直在处理中文到英文的技术文档翻译,第 6 轮用户切换到日语口语对话,模型仍然按技术文档的正式风格输出日语,因为前面积累的「正式技术风格」上下文没有被清理。这不是模型笨,是我没有给它一个显式的「当前任务已切换」的信号。

上下文类型:先分清楚再放进去

回到最开始的问题:往上下文里塞东西之前,得先搞清楚每块信息的类型和优先级。我在实际项目里用的是下面这个分类:

上下文类型特征优先级示例
系统规则长期稳定,全局生效最高输出格式约束、安全策略、角色设定
任务指令当前请求的工作说明「请根据以下工单信息生成回复」
业务事实来自可信数据源数据库查询结果、API 返回值、文档片段
用户输入需要处理,但不可信用户当前消息
示例输出帮助模型学习格式和尺度Few-shot 示例
历史对话只保留对当前任务有用的部分最近 N 轮对话摘要
检索内容需要标注来源和置信度视情况RAG 返回的文档片段

不同类型用明确的标题或 XML 标签分隔。这不只是给人看——模型对结构化标记的敏感度远高于自然语言过渡句。下面这个对比就很说明问题:

# ❌ 混在一起:模型分不清哪部分是约束,哪部分是数据
prompt = f"""
你是一个客服助手。
用户之前说过他想退货。
以下是公司信息:{company_policy}
用户说:{user_message}
请回答用户的问题。
"""
 
# ✅ 结构化分隔:每种信息有自己的区域
prompt = f"""
<system>
你是一个客服助手。只回答与订单相关的问题。
如果信息不足,明确告知用户并请求补充。
</system>
 
<company_policy>
{company_policy}
</company_policy>
 
<conversation_history>
{summarized_history}
</conversation_history>
 
<retrieved_context source="knowledge_base" confidence="high">
{retrieved_docs}
</retrieved_context>
 
<user_message>
{user_message}
</user_message>
"""

区别在哪?第二种写法里,模型不需要猜「这段是公司政策还是用户在说话」。每种信息都有自己的围栏,指令优先级也一目了然。

上下文密度:不是越多越好

上下文窗口再大,也不意味着该把能找到的东西全塞进去。每个 token 都有成本——不只是 API 费用,还有延迟和注意力稀释。Redis 的工程博客引用了一项研究:15000 词的上下文会让延迟增加 7 倍以上[3]。更麻烦的是质量损失:你检索了 10 个文档,每个 500 token,光输入就 5000 token,但其中 7 个跟问题只有微弱相关——模型不但多花了钱,还更容易漏掉真正重要的那 3 个。

我现在的做法是三步:先筛选,再排序,再压缩

# ❌ 全量检索灌入
docs = retriever.search(query, top_k=20)
context = "\n".join([d.content for d in docs])
 
# ✅ 筛选 + 排序 + 压缩
# 第一步:用相关性阈值过滤
docs = retriever.search(query, top_k=20, score_threshold=0.75)
 
# 第二步:按相关度和时效性排序
docs = sorted(docs, key=lambda d: d.score * d.recency_weight, reverse=True)
 
# 第三步:只取前 5 个,截断过长片段
docs = docs[:5]
context = "\n".join([d.content[:500] for d in docs])

对于 RAG 场景,还有一个我踩过坑才记住的点:别只传正文。把片段的来源、时间、置信度和适用范围一起传给模型,它能做出更准确的判断:

# ❌ 只传内容
retrieved = "退换货需在签收后 7 天内申请。"
 
# ✅ 传内容 + 元数据
retrieved = """
[来源: 2026年Q2退换货政策, 生效日期: 2026-04-01, 置信度: 0.92, 适用范围: 大陆地区标准订单]
退换货需在签收后 7 天内申请。
"""

加上这些元数据之后,模型就不会拿三年前的政策去回答当下的问题。

信息流全貌:一次请求的上下文组装

把上面这些拼在一起,一次完整的请求组装流程大概长这样:

流程图画布 · 115%
Mermaid 流程图加载中...

这里有几个值得注意的设计选择:

路由判断放在最前面——不是每次请求都需要完整的 RAG 流程。简单问候或者闲聊,塞一堆检索文档进去反而干扰模型。

检索和压缩并行——文档筛选和历史摘要可以同时进行,因为它们之间没有依赖。这在异步框架里能省下几百毫秒。

输出质量有兜底——如果第一轮输出质量不过关,不是重新写 prompt,而是精简上下文再试。因为大部分质量问题都是上下文太杂导致的。

实际落地时,这个流程还需要一个容易被忽略的环节:上下文版本快照。我见过太多「昨天还好好的,今天突然崩了」的线上问题,最后定位到原因是知识库更新后,某个文档片段的格式变了,或者某条系统指令被运营同事改了一个词。给每次请求的上下文结构打快照,出问题时能快速回溯,这个成本不高但回报很大。

上下文压缩:Write / Select / Compress / Isolate

LangChain 的 Lance Martin 总结了生产级 Agent 系统的上下文管理框架[1],我觉得可以简化成四个动作:

策略做什么适用场景成本
Write(外化)把中间结果写到 scratchpad 或长期记忆跨会话记忆、多步任务状态追踪
Select(筛选)只检索当前任务最需要的信息大知识库、工具数超过 10 个
Compress(压缩)摘要、裁剪、Prompt Cache长对话、成本敏感、接近窗口上限
Isolate(隔离)子任务交给专属 Agent,只回传摘要复杂多步任务、安全隔离需求

四个策略不一定全上。我的经验是分阶段引入:

第一阶段:先做 Write + Select。 给 Agent 一个 scratchpad,让它把中间推理写下来;RAG 加相关性阈值过滤。这两步就能解决大部分「失忆」和「知识噪声」问题。

第二阶段:加 Compress。 先做 Prompt Cache——Anthropic 和 Google 都支持 API 级别的 prompt 缓存,实现成本几乎为零,生产环境报告节省 75%-90% 的费用[1]。然后再加对话历史摘要和自定义裁剪逻辑。

第三阶段:按需 Isolate。 只有当工具数超过 10 个且无法缩减、任务分支复杂度超出单 Agent 管理能力、或有明确安全隔离需求时,才引入多 Agent 架构。过早做隔离反而增加系统复杂度。

Claude Code 的做法值得参考:当上下文使用量达到 95% 时,它会自动触发压缩摘要,然后在一个新的上下文窗口里继续工作[1]。

Write 策略看起来简单,但有个细节容易踩坑:scratchpad 的内容也需要定期清理。我早期实现过一个 Agent,让它把每步推理结果写到 scratchpad 里,结果跑了 20 步之后,scratchpad 本身就变成了上下文噪声源。后来加了个规则:每完成一个子任务,把已完成的步骤从 scratchpad 移到长期归档,只保留当前活跃步骤——这才稳定下来。

上线前的上下文检查清单

每个涉及 LLM 的项目上线前,我都会跑一遍这个清单。不是什么高深方法论,就是踩过足够多坑之后攒下来的「别在这些问题上翻车」:

结构与分隔

  • 系统指令、业务事实、用户输入、检索内容是否用 XML 标签或 Markdown 标题明确分隔?
  • 模型是否能清楚区分「可信信息源」和「用户输入」?
  • 关键指令是否放在上下文的开头或结尾,而非埋在中间?

检索与过滤

  • RAG 检索是否设置了相关性阈值(建议 0.7 以上)?
  • 检索结果是否附带来源、时间、置信度等元数据?
  • 是否对检索结果做了截断或摘要,而非全量灌入?

历史与状态

  • 对话历史是否做了摘要压缩,而非全量保留?
  • 长对话场景是否有上下文窗口的硬上限保护?
  • 跨会话信息是否通过外部记忆存储,而非依赖上下文窗口?

安全与兜底

  • 是否测试过「信息不足」时模型能否拒答或追问,而非硬编?
  • 输出是否能追溯到具体的上下文来源?
  • 是否有上下文精简后的降级重试策略?

成本与性能

  • 是否开启了 Prompt Cache(适用于重复系统指令)?
  • 单次请求的 token 数是否有监控和告警?
  • 是否定期审查检索管道,清理过期或低质量的文档片段?

这 16 项不需要一次全做到,但至少应该在上线前过一遍。你会发现大部分线上问题都能在这里找到对应的检查点。

写在最后

上下文工程做得好,prompt 本身反而会更短——因为你已经把该给的信息结构化地放在了该放的位置,不需要在 prompt 里反复强调「请只根据以上内容回答」「请不要输出无关信息」。

这个领域的核心思路其实就一条:把上下文窗口当作稀缺资源来管理,每个 token 都要能回答「它为什么在这里」。 模型的能力是固定的,你能控制的是喂给它什么。与其期待模型在混乱的信息里自己理出头绪,不如在工程层面把信息组织清楚。

这不是什么新理论,就是工程该有的样子。


参考资料

  1. Effective Context Engineering for AI Agents — Anthropic Engineering Blog
  2. Context Engineering vs Prompt Engineering — DataHub
  3. Context Window Management for LLM Apps: Developer Guide — Redis Engineering Blog
  4. State of Context Engineering in 2026 — Towards AI
  5. Context Engineering Guide 2026: Beyond Prompting — ShareUHack
  6. Context Window Management Strategies (2026) — SurePrompts
  7. Context Engineering: Going Beyond Prompt Engineering and RAG — The New Stack
  8. Context Engineering vs. Prompt Engineering — Data Science Collective, Medium

评论 0

0 / 1000