为什么需要上下文工程:大模型时代的应用困境
第 1 章 为什么需要上下文工程:大模型时代的应用困境
把一个大语言模型从"演示 Demo"推到"日活在生产环境跑十万次"的位置,几乎是每一个 AI 产品团队都要走一遍的路。走完这条路的团队会告诉你:最大的拦路虎不是模型不够聪明,而是你给模型的那份"输入"——也就是它每次推理时能"看到"的所有信息——越来越难以管理。你精心打磨的提示词在前几轮对话中表现出色,但到了第 20 轮、第 50 轮,模型开始"忘记"你说过的话;你把整本手册塞进 prompt 希望它能回答专业问题,结果它反而抓不住重点;你在多个工具之间切换的复杂场景里,眼睁睁看着模型开始"人格分裂",给出前后矛盾的答复。这些问题有一个共同的名字:上下文失败。
本章不打算告诉你"什么是上下文"——这个名词你已经在无数博客和文档里见过。我们要做的是回答一个更尖锐的问题:为什么"提示词工程"已不足以应对真实应用?为什么我们必须把"上下文"当作独立工程对象,而不是继续把它当作 prompt 的同义词?
1.1 提示词工程的天花板:边际收益曲线已经拐头
2023 年初,提示词工程(Prompt Engineering)还是大模型应用的"主战场"。开发者相信,只要找到那个完美的 prompt 模板,加上几个咒语般的指令("让我们一步一步思考"),模型就能给出专业、稳定、可复现的输出。这股风潮催生了大量关于"零样本思维链"、"少样本示例选择"、"ReAct 框架"的讨论,也确实让一批早期应用跑了起来。
但当场景从"对话玩具"切换到"企业级业务",同一套方法论的失效速度比预期快得多。让我们看看一组典型的边际收益曲线。假设你负责一个文档问答系统,任务是让模型基于一份 100 页的产品手册回答用户问题:
- **零样本 prompt**("请基于以下文档回答用户问题:...")—— 准确率 45%,这是基线。
- **加入思维链指令**("让我们一步一步分析...")—— 准确率 52%,提升 7 个百分点。
- **加入 3 条少样本示例** —— 准确率 58%,又提升 6 个百分点。
- **加入 8 条精心挑选的少样本示例** —— 准确率 61%,只提升 3 个百分点。
- **加入 20 条示例** —— 准确率 62%,几乎不再提升,且 prompt 长度爆炸,推理延迟翻倍。
这条曲线告诉我们一个残酷的事实:当任务复杂度上升,prompt 调优的边际收益急剧下降。你可以花一周时间把示例从 3 条扩到 20 条,但回报可能只是 4 个百分点——而用户真正在意的"那 20% 的边角案例",几乎完全不在 prompt 调优的射程之内。
为什么会这样?因为提示词工程本质上是在做"输入端的局部优化"。它假设所有相关信息都已经存在于你塞进 prompt 的那个静态字符串里。但真实应用不是这样的:
第一,真实应用的信息是动态的。用户的对话历史、上一步工具的返回结果、当前会话的环境变量、用户的偏好设置——这些信息每秒钟都在变化。提示词工程没有提供一种"程序化"的方式来组装这些信息,它依赖人工把模板写死。
第二,真实应用的失败模式是结构化的。不是"模型答错了某一题",而是"在第 N 轮之后模型开始答错所有题"。不是"模型不会思考",而是"模型思考的方向被你之前的对话带偏了"。这些失败模式不能靠改 prompt 模板解决,它们需要的是一种"运行时"的上下文管理机制。
第三,真实应用对成本和延迟敏感。把整本手册塞进 prompt 可以让准确率达到 75%,但每调用一次就消耗 8000 个 token,单次成本是普通对话的 30 倍。提示词工程没有提供"如何在有限预算下选择最有价值的上下文"这层抽象,它只能"全塞或全不塞"。
这就是提示词工程的天花板。它把"如何让模型理解任务"这个问题过度简化了,而忽略了"如何让模型在复杂环境中持续、稳定、可控地工作"这层更重要的工程命题。当我们跨过这道坎,下一步要做的就是引入一个新的工程对象:上下文(Context)。
1.2 三个失败案例:上下文管理的真实代价
抽象的论述永远不如具体的案例。下面三个案例来自我亲历或深度参与的项目,每一个都让团队花了几周时间排查,最终定位到同一个根因:上下文管理缺失。这三个案例分别覆盖了对话、代码、检索三类典型场景,它们共同勾勒出"上下文失败"的全貌。
案例一:电商客服的"人格分裂"
某零售品牌的客服系统接入了大模型,希望它能基于商品知识库回答用户的售后问题。最初的 prompt 很简单:
``` 你是一位专业的客服助手。请基于以下知识库内容回答用户问题。 知识库:[5000 字的售后政策] 用户问题:{user_question} ```
上线第一天效果不错。客服机器人能准确引用退换货政策,处理常见的物流查询。但到了第三周,运营团队收到一连串投诉:用户在第 5、6 轮对话后开始收到前后矛盾的答复。比如用户问"我退货的钱什么时候到账",模型先回答"3-5 个工作日",用户继续追问"如果是信用卡呢",模型又改口说"7-10 个工作日",但其实同一份知识库写得很清楚——信用卡也是 3-5 个工作日。人工回放对话后发现,模型在第 6 轮时"忘记"了第 1 轮提供的知识库内容,开始基于自身的通用知识胡编。
更糟糕的是,运营团队还发现,模型在某些对话中表现得"过于客气",在另一些对话中又"冷冰冰",仿佛换了一个人。这是典型的"人格分裂"——前几轮 system prompt 设定的语气被稀释,模型开始滑向它的默认行为。
根因分析:这个项目的 prompt 设计完全忽略了"对话历史的累积效应"。每一次用户输入和模型回复都会进入下一轮的上下文,但 system prompt(人格设定)和知识库内容在长对话中会被"冲淡"。Transformer 的注意力机制虽然理论上能"看到"所有 token,但在实践中它对长上下文中不同位置的注意力分布并不均匀——开头和结尾的内容往往获得更多关注,中间内容则容易被忽略。这就是著名的"迷失在中间"(Lost in the Middle)现象:当上下文中存在多个信息块时,模型更倾向于使用位于开头和结尾的信息块,而中间部分的影响会被显著稀释。
这个项目最终的解决方案不是改 prompt,而是引入了一套上下文组装层:每一轮对话前,系统会动态重建上下文,把系统提示、知识库摘要、历史对话的关键节点(而非全部)按照"重要性"重新排序。这种"运行时管理"的能力,是提示词工程完全无法提供的。
案例二:代码助手的"前文遗忘"
一家 SaaS 公司开发了一款 IDE 插件,希望帮助开发者在大型代码库中定位 bug、生成单元测试、补全函数。技术选型用了当时最强的模型,prompt 也调优得很到位——加入了"资深软件工程师"的角色设定、详细的输出格式要求、几个少样本示例。
但上线后,开发者反馈的最常见抱怨是:"它看不懂我之前问过的问题。"比如开发者先问"这个函数为什么会抛出 NullPointerException?",模型给出了详细的诊断。然后开发者继续问"那帮我写个修复方案",模型却给出了一个和上一轮毫无关联的通用模板。再比如,开发者在 IDE 中选中了一个 200 行的文件,模型在第一轮回答时能精准引用文件中的某个变量名,到第三轮就完全不记得这个变量是什么了。
根因分析:和客服案例不同,这个项目的失败点不在人格,而在"代码上下文的传递"。每一轮对话时,系统需要把开发者当前选中的代码、之前问过的相关函数、IDE 中的报错信息统统送入模型。但在最简实现里,这些信息是简单"拼接"进 prompt 的——比如"当前选中代码:{code}\n\n用户问题:{question}"。这种做法看似直接,实则忽略了几个关键问题:
- **代码上下文的"质量"被忽略**。200 行代码里可能只有 20 行和当前问题相关,但全部塞进 prompt 会让模型"找不着北"。
- **代码上下文的"位置"被忽略**。当代码块出现在对话中间而不是开头时,它在注意力分布中处于劣势——又是一个"迷失在中间"。
- **多文件场景的"关联"被忽略**。一个 bug 经常涉及多个文件,但简单拼接会让模型看不到文件之间的引用关系。
- **代码版本变化的"同步"被忽略**。开发者在 IDE 里改了一行代码,但前几轮的对话上下文还停留在旧版本上。
这个项目最终的解决方案是引入了一个代码感知上下文管理器:它会基于语法分析提取代码的"摘要"(函数签名、关键变量、依赖关系),并根据用户当前问题动态选择哪些代码块进入上下文。这个管理器本质上是一个轻量级的"检索增强生成(RAG)"系统,但它管理的不是文档,而是代码本身的结构。
案例三:研究助理的"上下文爆炸"
第三个案例来自一家 AI 辅助研究的创业公司。他们做的是"深度研究助手":用户输入一个研究主题,系统自动调度多个 agent 分头调研,最后汇总成一份报告。系统架构相当复杂:包含 5 个不同角色的 agent(检索员、阅读员、分析师、批评家、写作员),每个 agent 都使用大模型,并且彼此之间会传递大量的中间结果。
上线初期的 demo 惊艳了投资人——5 分钟生成一份关于"2024 年固态电池技术进展"的研究报告,引用了 30 多篇论文。但当真实用户开始使用时,系统频繁崩溃:要么是某一步的 agent 返回的内容超出上下文窗口(Context Window),要么是模型在最后一步"忘记"了前面调研的关键信息,要么是延迟高达数分钟因为 token 消耗爆炸。
根因分析:这个项目的失败更"系统性"——它不是某一轮 prompt 写错了,而是整个系统缺乏一个全局的上下文编排机制。每一个 agent 独立决定自己向下一个 agent 传递什么信息,结果是:
- **信息冗余**:多个 agent 都重复传递相同的事实,导致上下文被同一个信息占用多次。
- **信息失序**:原本有逻辑递进的研究流程,因为不同 agent 的输入顺序被打乱,最后一步的写作 agent 拿到的上下文是"碎片化"的。
- **窗口溢出**:当某个 agent 的中间结果超过 30000 token,下一步的模型直接报错或开始"截断"——模型只会"看到"前几千 token,剩下的全部丢失。
- **成本失控**:整个流程的 token 消耗是设计的 5 倍,因为没有"上下文压缩"机制。
这个项目最终的解决方案不是重写 prompt,而是重新设计了上下文的生命周期:从研究任务的拆解,到每一步的上下文压缩(保留关键结论、丢弃原始细节),到全局的"上下文预算"管理(每个 agent 知道自己最多能消耗多少 token)。这种"上下文作为一等公民"的设计思路,和传统软件工程中的"内存管理"思想如出一辙。
三个案例,三个不同的领域,但都指向同一个结论:当大模型应用走出"单轮对话"的舒适区,进入"长流程、多角色、动态信息"的真实场景时,提示词工程提供的工具远远不够。我们需要的是一套独立的工程方法——上下文工程——来管理模型在每一次推理时"能看到什么"。
1.3 大模型应用的三层架构:明确上下文的工程定位
要建立上下文工程的思维,第一步是把大模型应用的内部结构清晰地画出来。在我过去几年和团队一起搭建 LLM 应用的经验里,最有用的一个心智模型是三层架构:模型层(Model Layer)、上下文层(Context Layer)、工具层(Tool Layer)。这三层各司其职,共同完成一次完整的推理流程。
```mermaid flowchart TD UI[User Interface] --> CL[Context Layer] CL --> ML[Model Layer] ML --> TL[Tool Layer] TL --> ML ML --> CL CL --> UI ```
模型层是整个系统的"推理引擎"。它接收一段输入文本,输出一段输出文本——形式上看起来和 1950 年代的图灵测试机没什么两样。模型层关心的是:给定这段上下文,模型应该输出什么。它不关心这段上下文是从哪里来的、经过了怎样的加工、是不是包含了过时的信息。在我们的架构里,模型层是一个"黑盒函数"——你给它 token,它给你 token。
工具层是模型与外部世界交互的"手脚"。它负责执行模型在推理过程中决定调用的外部操作:搜索数据库、读取文件、调用 API、执行代码、查询知识库。工具层接收模型按照特定 schema 输出的"工具调用指令",执行后将结果以结构化方式返回。工具层本身不"思考",它只负责"做"。
上下文层是连接用户、工具和模型的"中枢神经"。它的工作是:在每一次模型推理之前,把"模型此刻应该看到的所有信息"组装起来。这个"组装"过程包括:
- 系统提示(System Prompt):模型的角色设定、行为约束、输出格式要求。
- 对话历史:用户和模型之前的对话内容,可能需要摘要、截断、重组。
- 工具调用结果:上一轮工具层的返回值,需要格式化、过滤、优先级排序。
- 检索增强内容:从知识库中检索到的相关文档,需要去重、压缩、标注来源。
- 实时环境信息:当前时间、用户身份、会话状态、任务上下文。
- 用户输入:当前这一轮的用户消息。
上下文层是整个系统中最复杂的部分,也是本书的核心主题。它和模型层、工具层的关键区别在于:模型层和工具层都是"被调用方",而上下文层是"调用编排方"。它决定了模型"看什么",进而决定了模型的"想什么"和"做什么"。
为什么这三层的划分这么重要?因为它们各自有完全不同的优化目标和约束条件:
- **模型层的优化目标是"能力"**——模型越聪明越好,越大越好。但这意味着成本更高、延迟更长。团队通常会权衡选择"够用就好"的模型,而不是盲目追求最强。
- **工具层的优化目标是"可靠性"**——工具调用必须稳定、可预期、可重试。一个不稳定的工具调用会让整个应用随时崩溃。
- **上下文层的优化目标是"信噪比"**——在有限的上下文窗口内(目前主流模型是 32K-200K token),最大化有用信息的密度,同时最小化噪音、冗余和误导性内容。这一层的优化空间是最大的,也是最被低估的。
一个常见的反模式是:把所有逻辑都塞进模型层,让模型自己决定"看什么"和"做什么"。比如让模型自己写代码去查询数据库、自己读取文件、自己组装提示词。这听起来很"智能",但在实践中会导致严重的不可控性——模型可能做出错误的决策、调用错误的工具、忽略关键信息。而把上下文层独立出来,本质上是把"信息管理"这个职责从模型手里拿回来,交还给确定性更高的软件系统。
理解了这三层架构之后,你就能看清为什么"提示词工程"是一个不完整的概念。提示词工程本质上只关心三层架构中"上下文层"里"系统提示"这一小块的优化——它默认对话历史、工具结果、检索内容都已经稳定地在那里了。但真实应用中,上下文层的工作远远不止写好一段 system prompt 那么简单。我们需要的是对整个上下文层进行系统化、工程化的设计。这就是上下文工程(Context Engineering) 的定义边界。
1.4 上下文即程序:一种隐式的、对模型行为的编程
有了三层架构的坐标系,我们可以提出本书最核心的一个论点:上下文是一种隐式的、对模型行为的编程语言。这个论点听起来抽象,但理解它之后,你对"如何设计 LLM 应用"会有全新的视角。
传统的编程是显式的:程序员写代码,编译器或解释器把代码翻译成机器指令,机器严格执行。代码的每一步都是确定的、可调试的、可复现的——给定同样的输入,程序总会产出同样的输出。
大模型应用的编程则完全不同。开发者不直接写"模型应该怎么思考",而是写"模型应该看到什么"。模型基于这些上下文,自己决定怎么思考、怎么输出。这种编程方式是隐式的——开发者没有直接控制模型的每一步推理,只是通过"喂什么数据"间接影响模型的行为。
这个类比可以走得更远。上下文之于大模型应用,正如源代码之于传统软件:
- **源代码定义了程序的行为**——给定输入,产生什么输出。**上下文定义了模型的行为**——给定输入,产生什么输出。
- **源代码需要管理状态、内存、I/O**——传统软件工程为此发展出了变量、作用域、垃圾回收等机制。**上下文需要管理对话历史、工具结果、知识检索**——这就是上下文工程的范畴。
- **源代码的 bug 会导致程序崩溃**——内存泄漏、空指针、死锁。**上下文的 bug 会导致模型"幻觉"、"遗忘"、"人格分裂"**——这就是本章三个案例的根源。
- **源代码的优化技术包括算法、数据结构、缓存**——传统软件工程的核心内容。**上下文的优化技术包括摘要、压缩、检索、排序**——这是本书后续章节要展开的全部内容。
把上下文当作"程序"来思考,会带来几个重要的思维转变:
第一,从"一次写好"到"持续维护"。 传统程序写完就可以部署,bug 修了就不再出现。但上下文是动态的——每次推理都需要重新组装。这意味着上下文工程不是一个"写 prompt"的一次性动作,而是一个需要持续监控、调试、优化的工作流。你需要像对待生产数据库一样对待上下文:监控 token 消耗、跟踪信息丢失、测量模型表现随上下文长度的变化曲线。
第二,从"通用模板"到"任务专属编排"。 不同任务对上下文的需求完全不同。客服场景关心"对话历史的累积效应",代码场景关心"代码结构的精准定位",研究场景关心"多步骤的信息压缩与传递"。没有一个"万能上下文模板"能覆盖所有场景。每个应用都需要根据自己的业务逻辑设计专属的上下文编排策略。
第三,从"静态字符串"到"动态数据结构"。 把上下文当作"一段很长的字符串"是最常见的误解。实际上,上下文是一个有结构的、类型化的、可编程的数据集合。它包含对话消息、工具结果、知识片段、系统指令、用户元数据等多个异构元素,每一种元素都有自己的元数据(来源、时间戳、置信度、优先级)和操作语义(压缩、过滤、替换、合并)。上下文工程的核心就是设计这些数据结构的组织方式和操作算法。
第四,从"开发者独写"到"用户共创"。 传统程序是开发者独写的。但上下文的一部分信息来自用户对话、用户上传的文件、用户的实时反馈。这意味着上下文工程需要考虑"用户行为对上下文的影响"——用户在对话中表达的新偏好应该被记录吗?用户上传的图片是否应该在后续对话中被引用?这些都不是纯技术问题,而是产品设计问题。
当我们用"上下文即程序"的视角重新审视那些失败的 LLM 应用,几乎所有问题的根因都能归结为:开发者没有把上下文当作一个严肃的、需要工程化设计的程序,而是把它当作一段可以随手拼接的字符串。他们没有考虑上下文的结构、组织、生命周期;没有监控上下文的状态;没有