失忆的金融助手:为什么上下文是一切AI系统的幽灵底座
第一章 失忆的金融助手:为什么上下文是一切AI系统的幽灵底座
1.1 一次不存在的截断:第三十七轮对话后的认知崩塌
2023 年 11 月的一个周三下午两点十七分,某头部券商智能投研系统的告警面板亮起了一盏黄灯。这盏灯本身并不危险——它只意味着该系统在过去一小时内为一位高净值客户生成的第三份调仓建议被风控规则拦截了。但黄灯亮起的瞬间,值班的产品经理还没意识到,他正在见证的不是一次普通的策略分歧,而是一台已经"精神失常"的 AI 在三个小时里持续输出错误的投资建议。
故事的主人公是一位机构客户——一家中型产业基金的风控总监。他的真实风险偏好是"严格厌恶单票超过组合 8% 的集中度敞口",他在上午十一点零三分与投研助手展开对话。最初的几轮一切正常:助手帮他梳理了当日的宏观信号,筛选出符合其偏好的几只标的,并就两个行业的供需格局做了简短分析。然而当对话推进到第二十几轮时,客户开始对其中一只标的提出质疑——他怀疑这家公司的应收账款周转率被美化。助手进入深度分析模式,开始调用财务数据库、提取历史公告、对比同业数据。
第三十七轮。
这一轮的精确长度是 14,287 tokens。它之所以重要,是因为该券商所使用的大模型 API 的上下文窗口上限是 16,000 tokens,而工程团队在上下文管理模块中实现了一个简单的策略:保留最新的 4,000 tokens 作为"近期记忆",把更早的内容压成一个 1,500 tokens 的摘要,与系统提示词和检索到的文档拼装在一起送入模型。这个策略从产品上线六个月以来一直运行良好,团队甚至把它当作"已经解决的问题"在季度汇报里一笔带过。
但在这一轮,事故发生了。
客户在第三十七轮的输入是:"回到最开始,我再强调一次我的风险偏好——单票不能超过组合的 8%,你再帮我看看刚才那两只标的,假设它们占组合 12% 的话,应该减仓多少?"
注意"最开始"三个字。这三个字在人类的对话理解里,几乎是一种求救信号——它意味着用户已经意识到对话走得太远,正在试图让助手回到某种共识基线上。但对于一个已经被截断的上下文窗口而言,这三个字出现在视野里的时候,前几轮里那段长达四百字、详细描述风险偏好的原始陈述,已经被压缩成了一行摘要:"用户偏好中等风险,关注流动性。"原始偏好中关于"8% 单票上限"的硬约束,丢失了。
助手并不知道这件事。
它以一如既往的专业口吻给出了回答:基于两只标的各占组合 12% 的假设,建议各减仓 4 个百分点,使其回到 8% 的均衡配置。这句话看似合理,但它建立在两个失真的事实之上:第一,用户从未同意过 8% 是"均衡"配置,而只是把它当作上限;第二,助手在同一轮对话里还做了另一件事——基于截断后仅存的、不完整的客户持仓信息,给出了一份新的标的推荐清单,其中包含了一只原本因客户行业禁入规则而被排除的股票。这条规则同样在摘要中丢了。
下午三点,客户依据这份建议执行了调仓。四点零五分,市场出现突发消息,那只被错误推荐的标的开盘跳水,客户在半小时内损失超过两百万。四点二十一分,客户经理收到投诉电话,整个事件最终被定性为"P0 级生产事故"。
事后复盘的根因报告里有一行极其刺眼的文字:"上下文窗口截断策略未对关键约束(hard constraints)做防丢失保护。" 但这只是表层。更深一层的问题在于:当时参与复盘的所有人——产品经理、算法工程师、风控负责人、客服主管——对于"上下文"这个概念本身的理解都不一致。有人说"上下文就是对话历史",有人说"上下文是提示词工程",有人说"上下文是 RAG 召回的内容",还有人把"上下文窗口"和"上下文工程"混为一谈。正是这种概念上的模糊,让那条仅用三行 Python 实现的截断逻辑,在长达六个月的时间里没有引发任何警觉。
这家券商不是孤例。在 2023 年下半年到 2024 年上半年这一年时间里,几乎所有部署 LLM 产品的团队都遇到过类似的事故。它们的共同点不是模型不够大、不是 prompt 写得不够好、不是检索效果不够准,而是工程团队始终没有把"上下文"作为一个独立的、值得为之设计专门工程学科的对象来对待。我们给 GPU 集群设计了复杂的调度系统,给训练数据设计了严格的质量控制流程,给推理服务设计了多级缓存与限流降级,唯独给上下文——这个每秒钟都在被组装、被压缩、被截断、被检索、被注入的运行时对象——留下了大量粗糙的、不可见的手工实现。
本章的任务,就是把"上下文"这个幽灵般的工程对象推到灯光下。
1.2 上下文的四重含义:会话、检索、工具与系统的递归嵌套
要理解为什么上下文是一切 AI 系统的"底座",我们首先要承认一件令人不安的事实:在任何一个生产级的 LLM 系统里,"上下文"这个词至少承载着四种完全不同的含义。它们在代码里以不同的字段出现,在数据流里以不同的生命周期流转,在工程团队内部由不同的角色负责。它们之间通过隐式的、递归的嵌套关系组装在一起,而正是这种嵌套关系,让任何一个环节的失误都可能像开篇故事里那样悄无声息地蔓延到下游。
第一种是会话上下文(Conversational Context)。它是最直观、最容易被理解、也最容易被误以为"已经解决"的那种上下文。说它直观,是因为它就是我们通常所说的"对话历史":用户和助手在多轮交互中积累起来的所有消息。说它容易被误以为已经解决,是因为大多数团队都会在自己的系统里塞进一个简单的消息队列,附加上一个粗暴的截断或滑动窗口策略,然后告诉自己"对话记忆问题已经搞定了"。但这种做法忽略了一个事实:会话上下文并非一个简单的有序列表,而是一个有层级、有摘要、有引用关系、有实体追踪的复杂结构。当用户在第三十七轮说出"回到最开始"时,模型需要的不是第三十七轮之前的所有 token,而是一个跨越了三十七轮的一致性状态——谁在说话、说过什么承诺、设过什么约束、引用过哪条外部信息。
第二种是检索上下文(Retrieval Context)。它来自 RAG(Retrieval-Augmented Generation)系统在每次调用时从外部知识库中召回到的文档片段、向量匹配结果、SQL 查询返回值。它的生命周期与一次具体的模型调用严格绑定——通常在请求开始时组装,在响应结束后就被丢弃,除非工程团队主动把它写入长期存储。它的难度不在于"召得准不准"——这是检索领域的经典问题,已经被研究了二十多年——而在于召回来的内容如何与会话上下文、与其他上下文类型发生冲突时该如何仲裁,以及在多轮对话中如何让"这一次召回的结果"与"上一次召回的结果"保持一致而不至于让用户陷入"为什么你一会儿这么说一会儿那么说"的困惑。
第三种是工具上下文(Tool Context)。它来自模型在推理过程中对外部工具的调用——调用搜索引擎、查询数据库、执行代码、访问内部 API。每一次工具调用都会向模型返回一个观察结果(observation),这个结果会以特定格式插入到上下文中,成为模型下一轮推理的依据。工具上下文的特殊之处在于它是"由模型自己生产"的内容,它的质量既取决于工具本身的可靠性,也取决于模型是否正确地解读了工具返回的格式。在生产链路中,工具上下文往往是体积最大、最容易撑爆窗口、最容易被截断的那种类型,而它一旦被截断,又会让模型陷入"我已经调用过工具但忘了结果"的失忆状态。
第四种是系统上下文(System Context)。它包含系统提示词、角色设定、输出格式约束、安全护栏、工具使用说明等。它通常在每一次调用开始时由系统注入,对模型而言具有最高的优先级。它的工程挑战不在于"写得对不对"——这属于提示词工程的范畴,已经有了大量最佳实践——而在于它如何与其他三类上下文在同一个 token 预算下共存。当系统提示词本身就长达两三千 tokens(这种情况在严肃的 B 端产品中并不罕见),留给会话、检索、工具三类上下文的预算就会被严重挤压,进而引发开篇那样的截断灾难。
这四类上下文之间的关系不是并列,而是递归嵌套。下图展示了它们在一次典型的 Agent 调用中的组装关系与生命周期边界。
```mermaid flowchart TD A[User Input] --> B[Context Composer] S[System Prompt] --> B B --> C[Assembled Prompt] C --> D[LLM Inference] D --> E{Model Decision} E -->|Direct Reply| F[Response to User] E -->|Tool Call| G[Tool Executor] G --> H[External APIs] H --> I[Tool Observation] I --> B F --> J[Conversation Memory] D --> K[Retrieval Trigger] K --> L[Vector Store / DB] L --> M[Retrieved Chunks] M --> B J --> B ```
这张图揭示了三件关键的事。第一,Context Composer 是整个系统的咽喉——所有四类上下文都要经过它才能进入模型,但它的内部逻辑在大多数生产系统里只是一段不到两百行的胶水代码,承载着远超其复杂度的责任。第二,每一类上下文都有自己的生命周期:系统上下文在每次调用开始时重建,工具上下文在调用结束后被压缩或丢弃,会话上下文在调用之间由 Conversation Memory 维护,检索上下文则按需触发。三者通过不同的回路与 Composer 相连,但任何一条回路上的延迟或失败都可能让最终送入模型的上下文处于不完整状态。第三,上下文之间会发生冲突——检索到的文档可能与用户最新的指令矛盾,工具返回的结果可能推翻模型此前的假设,而系统提示词里硬编码的角色定义可能在长对话中被模型"忘记"。这些冲突的仲裁逻辑,目前散落在 Prompt 工程、RAG 调优、Agent 框架设计的各个角落,没有一个统一的工程学科来管理它们。
当我们用"四象限"这四个字来概括上述分类时,真正的重点不是分类本身,而是分类背后那个尚未被命名、尚未被建制的工程领域:如何设计、组装、维护、回收、压缩、审计这一整套上下文体系。这正是本书全部十四章将要回答的问题。
1.3 注意力机制的依赖性诅咒:为什么 Transformer 没有记忆却需要记忆
要理解为什么上下文工程在 LLM 时代变得如此重要,我们必须回到一个被广泛误解的技术事实:Transformer 架构本身没有记忆。
这句话听起来与我们的日常经验矛盾——GPT-4 在几十轮对话里表现得像是有记忆的,Claude 在长文档分析中表现得像是有记忆的,Gemini 在百万级上下文里表现得像是有记忆的。但这些"记忆"都不是 Transformer 架构本身提供的,而是外部系统(通常是产品层的上下文管理模块)塞进 prompt 里的。当我们把 Transformer 的 self-attention 机制剥到最纯粹的形态,它做的只有一件事:在一次前向传播中,为每一个 token 计算它与其他所有 token 的相关性权重,然后基于这些权重做加权求和。
这意味着 attention 是一种"有状态的计算,但没有持久状态"。它是 stateful,因为它在计算第 $i$ 个位置的表征时,确实依赖于前面所有位置的信息;它又没有 persistent state,因为当这一次前向传播结束、KV cache 被清空(或被新的会话覆盖)之后,之前的所有信息都不会以任何形式残留在模型的权重里。下一次推理开始时,模型面对的仍然是一片空白——除了被显式塞进 prompt 里的那些 token。
这构成了我们称之为"依赖性诅咒"(Dependency Curse)的核心张力:模型的智能高度依赖于显式提供的、可被检索的信息环境,但模型本身没有能力主动维护这个环境。
让我们把这一点用数学语言说得更精确一些。给定一个长度为 $n$ 的 token 序列 $x_1, x_2, \ldots, x_n$,Transformer 的 self-attention 层为每个位置 $i$ 计算:
$$\text{Attention}(Q_i, K, V) = \text{softmax}\left(\frac{Q_i K^{\top}}{\sqrt{d_k}}\right) V$$
其中 $Q_i = x_i W^Q$,$K = X W^K$,$V = X W^V$,$X$ 是整个输入序列的 embedding 矩阵。这个公式揭示了一个深刻的工程含义:所有 $n$ 个位置的信息都必须同时存在于前向传播的输入中,才能让位置 $i$ 的计算结果有意义。如果某个位置 $j$ 的信息因为上下文窗口截断而被剔除,那么任何依赖位置 $j$ 的位置 $i$ 的表征都会无声地发生退化。这种退化不会产生错误信号——它只是让模型变得更"不确定",更倾向于幻觉,更容易接受 prompt 里的诱导。
这就是为什么"上下文截断"在生产链路中是一种极其危险的故障模式:它不会让模型报错,只会改变模型的行为,而这种改变在大多数情况下从模型输出的字面意义上是看不出来的。 模型仍然会用同样的口吻、同样的格式、同样的"自信度"回答问题——只是它的回答建立在残缺的信息之上。
依赖性诅咒还解释了为什么"模型变大"并不能解决这个问题。直觉上,我们可能会认为把上下文窗口从 8K 扩展到 128K、再到 1M,就能一劳永逸地解决记忆问题。但这是一种误解。窗口的扩展解决的是"能塞多少 token"的问题,它并没有解决"塞进来的 token 里哪些是关键的、哪些是冗余的、哪些是相互冲突的、哪些应该被压缩或丢弃"的问题。事实上,随着窗口的扩大,新的问题会出现:在 100 万 token 的海洋里,模型如何找到真正相关的那 5 个事实?这种"大海捞针"(needle-in-a-haystack)问题在 2023-2024 年间被反复讨论过,它揭示的是注意力机制在长序列上的稀释效应——窗口越大,单个相关 token 所能分配到的注意力权重就越小,模型就越容易"忽略"关键信息。
换句话说,窗口的物理扩展没有改变 Transformer 对上下文的"硬依赖"性质,反而让"如何管理这个上下文"变得更加复杂。 这就是为什么我们必须把"上下文工程"从"模型工程"中分离出来,作为一个独立的学科来对待。
1.4 三代记忆观的演化:从图灵纸带到 GPT-3 的 in-context learning
上下文工程并不是一个新发明的事物。它是人类对"机器记忆"这个命题长达七十余年思考的最新形态。理解这条思想史的脉络,有助于我们看清当前正在发生的范式转移并不是孤立的,而是这一长期探索的必然延续。
第一代:符号主义的无限记忆纸带。 1950 年,图灵在他那篇著名的论文《计算机器与智能》中提出了一个后来被称为"图灵机"的思想实验:一台机器由一条无限长的纸带、一个读写头和一组状态转移规则组成。纸带就是机器的"记忆"——它可以无限延伸,可以被任意读写,因此从理论上说,这台机器没有"遗忘"的概念,只要纸带还在,过去的所有信息都随时可以被调取。这种"无限外存"的设想深刻影响了此后半个多世纪的 AI 研究:无论是 1960 年代的 Lisp 机器、1980 年代的专家系统,还是 1990 年代的智能体研究,"记忆"都被默认视为一种外置于计算过程的、可被任意访问的数据结构。在这种范式里,上下文管理不是一个工程问题,而是一个数据结构设计问题——设计好链表、哈希表、数据库模式,记忆问题就解决了。
第二代:连接主义的分布式上下文。 2003 年,Bengio 等人在《A Neural Probabilistic Language Model》中提出了一个革命性的想法:把"上下文"从显式的数据结构转换为模型权重里的分布式表征。在他们的设想中,语言模型不再依赖外部记忆,而是通过学习把上下文信息"压缩"进自身的参数里。当模型看到 $w_{i-1}, w_{i-2}, \ldots, w_{i-n+1}$ 这 $n-1$ 个前文词时,它会把这 $n-1$ 个词的联合信息编码进一个固定维度的隐藏状态向量中,然后用这个向量预测下一个词 $w_i$。这个隐藏状态向量,就是"分布式上下文"。它解决了图灵纸带范式的两个问题:第一,存储效率远高于显式数据结构;第二,相似上下文会自动激活相似的隐藏状态,从而具备一定的泛化能力。但它也付出了代价:上下文被"凝固"进了模型权重,无法被实时更新、无法被外部系统干预、无法按需检索。在某种意义上,分布式上下文让"上下文工程"这个概念变得无意义——因为没有什么外部的工程对象需要管理。
第三代:In-Context Learning 与"显式上下文的回归"。 2020 年,GPT-3 的论文《Language Models are Few-Shot Learners》揭示了一个让整个 NLP 社区震惊的现象:当你把若干个"输入-输出"示例直接拼在 prompt 里,模型不需要任何权重更新,就能学会这些示例中蕴含的模式,并在新的输入上正确应用。这就是 in-context learning(ICL)。它的工程含义是颠覆性的:模型的能力不再完全由预训练阶段的权重决定,而是显著地、动态地取决于推理时被显式提供的上下文。 模型权重变成了"潜能",上下文变成了"激活条件"——同一组权重,在不同上下文下可以表现出截然不同的能力。
这一发现彻底改写了我们对"记忆"的定义。它告诉我们:在大模型时代,真正的记忆是显式的、可被设计的、运行时的上下文,而不是被固化在权重里的分布式表征。后者是一种"压缩记忆",它代表了模型在预训练阶段见过的所有数据的某种平均;前者是一种"激活记忆",它代表了模型在当前这一次推理中所需要调用的特定信息。两者不是替代关系,但在工程实践中,后者正在变得越来越重要,因为它更可控、更可调试、更可审计。
从图灵纸带到 Bengio 的分布式上下文,再到 GPT-3 的 in-context learning,我们看到了一条清晰的演化轨迹:"上下文"这个对象在 AI 系统中的地位,从外置数据结构,到内化表征,再到运行时激活条件,经历了从边缘到中心、从被动到主动的迁移。 而每一次迁移,都伴随着工程范式的转变。当下正在发生的,正是 in-context learning 范式之下的工程化——如何高效、可控、可靠地把运行时上下文组装起来。这个工程化任务,至今没有一个被广泛接受的名称。在本书中,我们称它为模型上下文工程(Model Context Engineering, MCE)。