跳转至

9. Token 经济学:上下文工程的艺术

你的 Agent 跑了一整天。

早上你让它帮你做代码审查,下午让它帮你重构几个模块,晚上让它帮你写测试用例。一天下来,你觉得效率很高——以前需要三天的工作量,一天就搞定了。

然后你打开了 API 用量面板,账单远超预期。

你仔细看了一下消耗明细,发现真正"有用"的那部分——你的代码、你的问题、AI 生成的回答——只占其中很小的一块。剩下的大头花在了什么地方?

System Prompt,每次调用都要原封不动地重新传一遍。工具描述,几十个工具的 Schema 定义,每次也要重新传。对话历史,每开新一轮,都要把之前所有的对话重新打包发出去。记忆注入,从长期记忆中检索到的项目知识和用户偏好,每次都拼在前面。中间结果,Agent 调用工具后返回的文件内容、搜索结果、代码片段,越积越多。

这些东西在每一次模型调用中都被完整地传输了一遍。同样的 System Prompt,一整天反复传了几百次;同样的工具描述,一整天反复传了几百次;对话历史越滚越大,到了几十轮之后,光是历史消息就比当下的问题大出好几个数量级。

这不是"浪费"——这是大模型的工作方式决定的。模型是无状态的推理引擎,每次调用都需要完整的上下文,它没有别的办法"知道"上一轮你说过什么。但理解了这个成本结构,你就会意识到一件事:上下文里的每一个 Token 都是有价格的,而且这个价格会随着对话的推进而累积。

这一章想讨论的,不是怎么省钱。是怎么思考一个更根本的问题——上下文窗口是模型和外部世界之间的唯一接口,它的容量是有限的,里面塞了什么、怎么排布,直接决定了模型能做成什么。在这个有限的窗口里,怎么让每一个 Token 都用在刀刃上?

9.1 Token 的成本结构

要做好上下文工程,首先要看清楚你到底在为什么付费。

大多数模型的定价分为两部分:输入 Token(你发给模型的内容)和输出 Token(模型生成的内容),输出 Token 比输入 Token 贵不少。这个定价差异直接反映了计算成本的差异——处理输入时,模型可以并行计算,所有 Token 同时进入 Transformer 的注意力层;而生成输出时,模型必须逐个生成,每生成一个 Token 都要基于之前所有已生成的 Token 做一次完整的前向传播。串行比并行慢得多,也贵得多。

这个定价结构意味着:让模型"看"很多内容(输入)的成本,远低于让模型"写"很多内容(输出)的成本。所以"给模型提供充分的上下文,让它生成精炼的输出"比"给模型很少的上下文,让它生成冗长的输出"更划算——不仅成本更低,效果通常也更好。

真正容易被忽视的成本陷阱在另一个地方——多轮对话的累积。

每一轮对话,都要把之前所有的对话历史作为输入重新传给模型。第 1 轮你只传当下这一句话,第 2 轮要传第 1 轮加新消息,第 N 轮要把前面 N-1 轮的全部历史一并打包传一次。对话越长,每一轮要付费的输入量越大——单轮看是线性增长,整段对话累计下来则是 N² 量级的总开销。一段几十轮的对话,最后几轮的输入量可能比最初几轮大出一两个数量级。

这还没算固定开销。System Prompt 和工具描述每一轮都要重新传一次,每一轮都额外加上一笔不小的固定成本。对话越长,这一笔固定成本被反复传输的次数也越多。长对话的成本之所以急剧上升,不是因为你说了很多话,而是因为每一轮都要重新传输所有历史。

多轮对话的 Token 成本:二次增长

但 Token 的成本不只是账单上的钱。

上下文越长,推理时间越长。Transformer 的注意力计算复杂度是 O(n²),上下文长度翻倍意味着计算量翻四倍。在实时交互场景中——比如你在 IDE 里等 AI 生成代码——多等两秒和多等八秒的体验差距是巨大的。与此同时,上下文越长,模型的"注意力"越分散。第二章讨论过 Lost in the Middle 现象——当上下文很长时,模型对中间位置信息的关注度会下降。塞进去的信息越多,每条信息被有效处理的概率越低。

所以 Token 的"成本"有三层:经济成本(账单)、性能成本(延迟)、质量成本(注意力稀释)。这意味着即使你有无限的预算,也不应该无节制地往上下文里塞信息——因为信息过载本身会让模型表现变差。

看清楚了这三层成本,回头再看那些常见的架构问题,会发现它们其实是同一类问题。该不该用 RAG?RAG 会在每次调用时注入检索到的文档片段,增加输入 Token——如果检索质量高,这些额外的 Token 能显著提升输出质量,值得;如果检索质量低,这些 Token 就是噪声,不值得。该不该用多 Agent?多 Agent 意味着多次模型调用,总 Token 消耗会成倍上升,但如果任务确实需要分解,多 Agent 的输出质量可能远超单 Agent。该不该保留完整的对话历史?完整历史保留了所有上下文,但成本是二次增长的;压缩历史节省了 Token,但可能丢失关键信息。这些决策没有标准答案,取决于具体的场景和约束。

模型选型也是一个常被忽略的决策面。同样一档复杂度的任务,用旗舰模型和用轻量模型,单次成本可能差出一个数量级。但贵不等于好——取决于任务的性质。需要复杂推理的任务(架构设计、多步重构、跨模块分析)、对正确性要求极高的任务(安全相关代码、金融逻辑)、需要长上下文理解的任务(大文件审查、全局重构),这些场景下模型能力的差异直接体现在输出质量上——便宜模型反复重试才能得到正确结果,算上重试成本反而更贵。而模式明确的任务(代码补全、格式转换、样板代码生成)、有强约束的任务(有完整的测试套件可以验证)、高频低复杂度的任务(lint 修复、import 整理),便宜模型的输出质量和贵模型差距不大,成本却低得多。

更务实的做法是把两类模型组合起来——用轻量模型做初步生成、做简单子任务,用旗舰模型做审查、做核心决策。这种"分级调度"的思路和 CDN 的多级缓存类似——热数据放快但贵的存储,冷数据放慢但便宜的存储。关键不是"选一个模型",而是"为不同的任务选合适的模型"。具体的模型定价会快速变化,记住判断框架就行:任务的复杂度和正确性要求越高,越值得用贵模型;任务的模式越明确、约束越强,越适合用便宜模型。

9.2 上下文压缩:用更少的 Token 说更多的事

看清了成本结构,下一个问题自然是:怎么降下来?

最期望的方向是压缩——用更少的 Token 传递同样的信息。但"压缩"一上手你就会发现,这不是一个动作,而是一条路。

最朴素的想法是把越拉越长的对话历史折叠成一段摘要。一段几十轮的对话,压缩后可能只剩原来的一成。但这里有一个隐藏的代价——你丢掉了讨论的过程、被否定的方案、具体的措辞和语气。所以摘要压缩的关键反而不是"怎么压",而是"什么时候压"。一种做法是固定间隔,每零散几轮压一次,简单但不够灵活;另一种是基于阈值,对话历史超过某个 Token 量才触发。还有一种更精细的做法是渐进式压缩——最近几轮保留原文,稍早一些的压缩成详细摘要,更早的压缩成简要摘要。这个思路和操作系统的页面置换策略如出一辙——最近使用的数据留在快速存储,较早的数据逐步迁移到更慢但更大的存储。在上下文工程的语境下,"快速存储"就是上下文窗口中的原文,"慢速存储"就是压缩后的摘要。

但摘要这种自然语言压缩本身也是冗余的。看这句话:"用户之前提到过,他的项目使用的编程语言是 Go,版本是 1.22,Web 框架选择的是 Gin,数据库方面使用的是 PostgreSQL 15。"一句长话说了几件事,但信息量有限。换成结构化的字段:

项目: {语言: Go 1.22, 框架: Gin, 数据库: PostgreSQL 15}

快准狠。这里发生了一件事——从摘要压缩跳到结构化压缩,压缩率又进了一个台阶。原因是事实型信息本身就是结构化的——技术栈、配置参数、项目结构、编码规范——用自然语言叙述反而是一种膀胀。

但结构化压缩不是万能的。讨论过程、决策理由、权衡分析这些信息的价值恰恰在于它的"叙事性"。"我们选择 gRPC 而不是 REST,因为内部服务调用频率高,gRPC 的二进制序列化和 HTTP/2 多路复用在这个场景下性能优势明显"——这段话如果压缩成 通信协议: gRPC,事实保留了,决策的理由却丢了。所以结构化压缩的边界很明确:事实型信息用结构化,决策型信息保留叙事。

压缩是把信息变小,还有一个思路是只放需要的信息——选择性注入。上一章讨论记忆检索时提到过这个问题——不是把所有记忆都注入上下文,而是只注入和当前任务相关的记忆。同样的原则适用于所有类型的上下文信息。工具描述是一个典型的例子,一个 Agent 可能注册了几十个工具,全部加起来占据的空间不小。但在一次具体的任务中,Agent 可能只会用到其中几个。如果能预判出哪几个会用到,只注入这几个的描述,节省下来的空间相当可观。

但选择性注入有一个风险:预判错了怎么办?Agent 在执行过程中发现需要一个没被注入的工具,它就无法使用这个工具。一种折中方案是核心工具始终注入,边缘工具按需注入——这反过来也要求你对自己的工具集有分层意识:哪些是一定会用到的,哪些是有场景才用。

一路走下来,有一件事逐渐清晰起来:不同类型的信息适用不同的压缩策略。

System Prompt 是行为约束——它定义了模型的角色、规则和边界。这部分信息的每一个字都可能影响模型的行为,不应该被压缩。保持原文。工具描述是能力定义——这部分可以做选择性注入(只注入相关工具),但每个工具的描述本身不应该被压缩——参数的名称、类型、约束都是精确信息,压缩可能导致模型误解工具的用法。对话历史是上下文背景——这部分最适合做摘要压缩,保留关键的决策和结论,丢弃讨论的细节。工具调用结果是中间数据——一个文件可能返回几千 Token,但其中真正有用的可能只是几行,可以做最激进的压缩。记忆注入是历史知识——这部分在写入长期记忆时就做过一轮提取,通常不需要进一步压缩,但需要控制注入的数量。

这就是分层压缩的思路:对不同类型的信息采用不同的压缩策略,而不是一刀切。 压什么、压多狠,取决于这部分信息扮演什么角色。这本身就是一个工程挑战——你需要一个能区分上下文不同部分类型和重要性的系统。

用一个实际场景来感受分层压缩的效果。假设 Agent 在第 15 轮对话中需要修改一个 Go 函数,此时上下文中积累了以下信息:

压缩前(约 3200 Token):

[对话历史 - 第1-14轮完整记录]
用户(第1轮): 帮我看看 pkg/auth/handler.go 这个文件的结构
Agent(第1轮): 我来读取这个文件... [返回完整文件内容,287行]
用户(第2轮): 这个 LoginHandler 函数太长了,帮我重构一下
Agent(第2轮): 我先分析一下这个函数的职责... LoginHandler 做了三件事:
  1. 参数验证 2. 调用认证服务 3. 生成JWT Token
  建议拆成三个函数...
用户(第3轮): 好的,按你说的拆
Agent(第3轮): [生成完整的重构代码,包含三个新函数,共89行]
...
[第4-14轮:讨论测试用例、错误处理细节、代码风格调整]

压缩后(约 800 Token):

[工作摘要]
任务: 重构 pkg/auth/handler.go 的 LoginHandler 函数
已完成: LoginHandler 已拆分为 validateLoginParams()、authenticateUser()、
  generateToken() 三个函数。已通过测试。
当前状态: 用户要求进一步优化错误处理模式
关键决策: 错误处理使用 fmt.Errorf 包装,不使用自定义 error 类型
技术栈: {语言: Go 1.22, 框架: net/http, 认证: JWT}

[当前文件状态 - 仅保留修改区域]
// pkg/auth/handler.go (重构后,第45-89行)
func validateLoginParams(r *http.Request) (*LoginRequest, error) { ... }
func authenticateUser(ctx context.Context, req *LoginRequest) (*User, error) { ... }
func generateToken(user *User) (string, error) { ... }

从 3200 Token 压缩到 800 Token,压缩率 75%。丢失了什么?第 1-14 轮的讨论细节、被否决的方案、中间版本的代码。保留了什么?当前任务状态、关键决策、技术约束、最新的代码结构——这些是第 15 轮决策所需要的全部信息。

分层压缩策略:不同信息,不同对待

9.3 注意力的分配规律

压缩解决了"放多少"的问题。但还有一个同样重要的问题:"放在哪里"。

同样的信息,放在上下文的不同位置,模型对它的处理效果可能完全不同。这不是玄学,而是 Transformer 注意力机制的固有特性。

第二章简要提到过 Lost in the Middle 这个现象,这里展开讨论它对上下文工程的实际影响。研究者做过一个经典实验:给模型一组文档,其中只有一篇包含正确答案,然后问模型一个问题。当正确答案在列表的开头或结尾时,模型的准确率最高;当正确答案在列表的中间时,准确率显著下降。

这个现象的原因和 Transformer 的注意力计算方式有关。上下文开头的 Token 在所有后续 Token 的注意力计算中都会被"看到",积累了大量的注意力权重;上下文结尾的 Token 离输出位置最近,在生成输出时有天然的位置优势;而中间位置的 Token 既没有开头的累积优势,也没有结尾的位置优势,容易被"淹没"。这个现象对上下文工程的启示是直接的:信息的位置和信息的内容同样重要。

基于注意力分配的规律,上下文的组织应该遵循一个原则:把最重要的信息放在注意力最集中的位置。

上下文的开头(System Prompt 区域)是注意力最强的位置。应该放什么?行为约束、角色定义、核心规则——这些是模型在整个推理过程中都需要遵守的"硬约束"。如果你希望模型"绝对不要生成 SQL 删除语句",这条规则应该放在 System Prompt 里,而不是放在对话历史的某个角落。

上下文的结尾(最近的用户消息)是注意力第二强的位置。应该放什么?当前任务的具体指令和关键信息。"帮我重构这个函数,要求保持接口不变,内部实现改用策略模式"——这条指令应该是用户消息的最后一部分,而不是被一大段背景信息淹没。

上下文的中间是注意力最弱的位置。应该放什么?背景信息、参考资料、历史对话——这些信息是"有了更好,没有也不致命"的辅助信息。即使模型对中间位置的关注度较低,这些信息仍然会在一定程度上影响模型的输出——只是影响力不如开头和结尾的信息那么强。

一个简单的记忆法:约束在前,背景在中,指令在后。

这里还有一个更深的问题——注意力是一种有限资源。上下文中的每一条信息都在"竞争"模型的注意力,信息越多,每条信息分到的注意力越少。这意味着往上下文里添加信息不是没有代价的——即使这条信息本身是"正确的"和"相关的",它的存在也会稀释模型对其他信息的注意力。

举个极端的例子:你给模型一个几十行的函数,让它找出其中的 bug。如果你只给这几十行函数,模型的注意力完全集中在这段代码上,找到 bug 的概率很高。但如果你同时把这个函数所在的整个文件都丢进去(几千行代码),模型的注意力被稀释到了这几千行代码上,对那几十行关键代码的关注度反而下降了,找到 bug 的概率可能反而降低。这就是信噪比的概念——上下文中有用信息和总信息的比例,信噪比越高,模型的表现越好。

提高信噪比有两种方式:增加信号、减少噪声。在实践中,减少噪声通常比增加信号更有效——减少噪声不仅节省了 Token,还提高了模型对剩余信息的注意力分配。这引出上下文工程的核心原则:上下文质量 > 上下文数量。 少而准的信息,胜过多而杂的信息。不是"给得越多越好",而是"给得越准越好"。这个原则和很多人的直觉相反——直觉告诉我们"信息越多,AI 越了解情况,输出越好",但实际情况是:信息过载会让 AI 的表现变差,它在海量的信息中迷失了方向,抓不住重点,输出变得泛泛而谈或者偏离主题。

9.4 长文本策略:当信息确实很多时怎么办

上一节说"少即是多",但有些场景下信息确实很多,而且都是必要的。

你要让 AI 审查一个很大的代码文件。你要让 AI 分析一份几百页的技术文档。你要让 AI 理解一个包含上百个文件的项目结构。这些信息不能简单地"删减"——删掉任何一部分都可能导致 AI 遗漏关键信息。

怎么办?

最直接的应对是分块处理(Chunking)——把长文本切成小块,每块独立处理。一个很大的代码文件,按一定行数切成若干块,每块独立发给模型分析,最后把所有块的分析结果汇总。分块处理的优势是简单直接,不需要复杂的算法,只需要一个切分逻辑和一个汇总逻辑,每块的上下文长度可控,模型能充分关注每一块的内容。

但一刀切下去,跨块的关联就丢了。一个 bug 的根因在第 1 块,症状在第 7 块,分块处理永远发现不了这个关联——因为模型在处理第 7 块时看不到第 1 块的内容。分块的粒度也是个需要权衡的参数:块太大,单块的 Token 数量过多,模型的注意力被稀释;块太小,跨块的上下文丢失更严重,而且调用次数增加,总成本上升。对于代码文件,一个更好的分块策略是按语义边界切分——按函数、按类、按模块切分,而不是按固定行数切分,这样每一块都是一个语义完整的单元,减少了跨块上下文丢失的问题。

要把跨块的视角找回来,就有了 Map-Reduce。它是分块处理的升级版,分两个阶段:Map 阶段对每个分块独立处理,生成中间结果(比如对每个代码块独立做审查,生成每块的问题列表);Reduce 阶段对所有中间结果做汇总处理,把所有块的问题列表合并、去重、排序,生成最终的审查报告。关键改进在于 Reduce 阶段——它提供了一个"全局视角"的机会,虽然 Map 阶段的每个块是独立处理的,但 Reduce 阶段可以看到所有块的中间结果,有机会发现跨块的关联。当然,Reduce 的质量取决于 Map 的质量——如果 Map 阶段遗漏了某个关键信息(因为它在单块的上下文中不够显眼),Reduce 阶段也无法弥补。

要先建立全局再下沉到细节,就有了层次化处理。第一层,对每个代码块生成一段摘要——"这个模块负责用户认证,包含 login、logout、refreshToken 三个函数,依赖 jwt 库和 userRepository"。第二层,把所有摘要拼接在一起,形成整个项目的"概览",基于这个概览,模型可以理解项目的整体结构和模块间的依赖关系。第三层,如果需要深入某个具体模块,再把该模块的完整代码注入上下文,结合概览信息做详细分析。层次化的优势是模型先建立全局理解,再深入局部细节——这和人类阅读大型代码库的方式很相似。劣势是摘要的质量决定了全局理解的质量:某个模块的摘要遗漏了关键信息,模型在全局层面就会对这个模块产生错误理解,后续的分析都会受到影响。

要在顺序处理中保持上下文连续,就有了滑动窗口(Sliding Window)。用一个固定大小的窗口在长文本上滑动,每次处理窗口内的内容,相邻窗口之间留出一定的重叠。重叠部分确保了相邻窗口之间的上下文连续性——后一个窗口能"看到"前一个窗口结尾的一部分内容,不会在窗口边界处丢失上下文。滑动窗口适合"逐段分析"的场景——比如逐段翻译一篇长文档、逐段审查一个长文件,但不适合需要"全局视角"的场景,因为每个窗口只能看到局部内容。

这四种策略不是互斥的,它们可以组合使用。一个常见的组合是层次化处理 + 按需深入。先用层次化处理建立全局理解,识别出需要重点关注的模块,再对这些模块用完整的上下文做详细分析。这样既有全局视角,又有局部深度,同时控制了总体的 Token 消耗。

选哪种,取决于任务的性质。需要全局视角的,选层次化或 Map-Reduce;需要逐段处理的,选滑动窗口;各部分可以独立处理的,分块处理就够;需要先全局后局部的,层次化 + 按需深入。

9.5 Prompt Caching:缓存不变的部分

回到开头那个场景。你的 Agent 一天调了几百次模型,每次都传输了一模一样的 System Prompt 和工具描述。这些内容在这几百次调用中一个字母也没变。有没有办法只“传输”一次,后续调用复用之前的计算结果?

这就是 Prompt Caching 的思路。

模型处理输入 Token 时,会在内部生成一系列中间计算结果(在 Transformer 架构中,这些结果叫做 Key-Value Cache,简称 KV Cache)。如果两次调用的输入有相同的前缀——比如都以相同的 System Prompt 开头——那么这个前缀对应的 KV Cache 可以被缓存下来,第二次调用时直接复用,不需要重新计算。

打个比方:你每天早上去同一家咖啡店,点的饮品不同,但每次都要先报你的会员号、确认你的偏好(少糖、燕麦奶)。如果咖啡店记住了你的会员信息和偏好,你每次只需要说"今天要拿铁"就行了——不需要每次都重复那些不变的信息。Prompt Caching 做的就是这件事:记住不变的前缀,每次只处理变化的部分。

当缓存命中时,前缀部分的 Token 按折扣价计费,首 Token 延迟也会明显下降。对于多轮对话场景,Prompt Caching 的节省更加显著——每一轮对话的输入都包含之前所有轮次的历史,而这些历史在相邻两轮之间只有最后一轮是新增的,前面的部分完全相同。如果缓存了前面的部分,每轮只需要为新增的内容付全价。对话越往后,受益越大。

缓存命中需要满足几个条件:前缀必须完全一致(逐 Token 匹配,一个字符的差异都会导致缓存失效);前缀长度需要达到一定的最小门槛;两次调用的间隔不能超过缓存的存活时间(在高频调用场景下,缓存几乎不会过期)。具体的门槛和存活时间不同提供商不一样,也随时在变,但原理同岑一样。

Prompt Caching 不只是一个省钱的技巧,它对上下文的组织方式有直接的设计启示。首先,把不变的内容放在前面,把变化的内容放在后面——这是 Prompt Caching 能够生效的前提,缓存是基于前缀匹配的,只有前缀相同才能命中。如果你把变化的内容(比如用户消息)放在 System Prompt 前面,那么每次调用的前缀都不同,缓存永远无法命中。这个原则和注意力分配的原则恰好一致——重要的约束放在开头(System Prompt),变化的指令放在结尾(用户消息)。Prompt Caching 给了这个原则一个额外的经济学理由。

其次,保持 System Prompt 的稳定性。如果你频繁修改 System Prompt(比如每次调用都动态生成不同的 System Prompt),缓存就无法命中。所以应该把 System Prompt 中不变的部分(角色定义、核心规则、工具描述)和变化的部分(动态注入的记忆、任务特定的指令)分开——不变的部分放在最前面,变化的部分放在后面。同样的道理也适用于工具描述的顺序:把高频使用的核心工具描述放在前面,偏门的扩展工具放在后面。这样即使扩展工具的描述发生变化(按需注入不同的扩展工具),前面核心工具的部分仍然可以命中缓存。

当然,Prompt Caching 不是万能的。缓存有过期时间,两次调用间隔太长,缓存就可能已被清除,所以在高并发场景下命中率通常很高,在低频调用场景下命中率可能很低。前缀必须完全匹配,哪怕有一个 Token 的差异也不行,这意味着动态内容(比如时间戳、随机数)不能出现在前缀中。此外,不是所有模型和平台都支持 Prompt Caching——这是一个相对较新的特性,不同提供商的支持程度和实现方式可能不同。

9.6 上下文工程的思维方式

回顾一下本章讨论的内容:Token 的成本结构、上下文压缩的手段、注意力分配的规律、长文本处理的策略、Prompt Caching 的原理。这些看起来是一堆零散的"技巧",但它们背后有一个统一的思维框架。

上下文窗口是模型和外部世界之间的唯一接口。模型不能直接访问你的文件系统、不能直接查询你的数据库、不能直接阅读你的文档——它能"看到"的,只有上下文窗口里的内容。而这个窗口的容量是有限的,即使是现在动辄几十万 Token 的长窗口,在一个复杂的 Agent 任务中也可能不够用——System Prompt、工具描述、对话历史、记忆注入、工具调用结果,这些加在一起很容易占满一半,留给实际任务的空间可能只有另一半。所以上下文工程的核心问题是:在有限的空间里,怎么让模型看到它最需要看到的信息?

这个问题可以分解为三个维度的权衡:

放多少(信息量)。 信息太少,模型缺乏必要的上下文,输出质量差。信息太多,Token 成本高、延迟大、注意力分散,输出质量也差。存在一个"最优信息量"——刚好够模型做出高质量的判断,不多也不少。

放什么(信息质量)。 不是所有信息都同等重要。和当前任务直接相关的信息是"信号",不相关的信息是"噪声"。上下文的信噪比越高,模型的表现越好。选择性注入、分层压缩,都是提高信噪比的手段。

放在哪(信息位置)。 同样的信息放在不同的位置,效果不同。约束放在开头,背景放在中间,指令放在结尾。这不是随意的安排,而是基于注意力分配规律的工程决策。

这三个维度不是独立的——它们相互影响。减少信息量(压缩)可能降低信息质量(丢失细节)。提高信息质量(精选内容)需要额外的计算(相关性判断)。优化信息位置需要理解不同类型信息的重要性层次。

一个常见的误解是:设计好 System Prompt 和上下文模板,就算完成了上下文工程。实际上,上下文工程是一个动态的过程——不同的任务阶段需要不同的上下文组织策略。在任务的"理解"阶段,用户刚提出需求,Agent 需要理解任务是什么,上下文应该侧重于任务描述、项目背景、相关的历史决策。在任务的"执行"阶段,Agent 已经理解了任务,正在调用工具执行,上下文应该侧重于工具描述、当前的执行状态、中间结果。在任务的"验证"阶段,Agent 完成了执行,需要检查结果,上下文应该侧重于原始需求、验收标准、执行结果的对比。同一个任务的不同阶段,"最需要看到的信息"是不同的。一个好的上下文工程系统应该能够根据任务阶段动态调整上下文的内容和组织方式。

如果你做过网络编程,会发现上下文工程和网络带宽优化有很多相似之处。上下文窗口对应网络带宽——都是有限的,你不能无限制地往里面塞数据。Token 对应数据包——每个都有传输成本。上下文压缩对应数据压缩——用 gzip 压缩 HTTP 响应,用更少的字节传输同样的信息。选择性注入对应按需加载——不是一次性把所有资源都下载下来,而是用到什么加载什么。Prompt Caching 对应 HTTP 缓存——不变的资源缓存在本地,只请求变化的部分。信噪比对应有效载荷比——数据包中有效数据和协议头的比例,协议头是必要的开销,但应该尽量减小。注意力分配对应 QoS(服务质量)——不同类型的流量有不同的优先级,关键流量应该得到优先处理。

这个类比不是巧合。上下文工程和网络优化面临的是同一类问题——在有限的传输通道中,怎么让最重要的信息以最高效的方式到达目的地。


本章从 Token 的成本结构出发,逐步展开了上下文工程的完整图景——压缩手段、注意力规律、长文本策略、缓存机制,最终收束为一个统一的思维框架:在有限的空间里,让每一个 Token 都发挥最大的价值。

但上下文工程解决的是"怎么高效利用窗口"的问题。它有一个前提假设:你已经有了需要放进窗口的信息。可是有一类信息,模型在训练时根本没有见过——你公司的内部文档、你项目的代码库、你行业的专有知识、上周刚发布的新技术规范。这些信息不在模型的参数里,也不在你的对话历史里。你需要一种机制,在运行时把这些外部知识"注入"到模型的上下文中——上下文工程告诉你"怎么高效地利用窗口空间",知识注入告诉你"用什么内容来填充这个空间"。