你以为在和 AI 聊天,其实是在管理一个有限的文本框

0 阅读25分钟

你和 AI 聊了一下午。它帮你重构了三个模块,对你的代码风格了如指掌——你喜欢用 early return,不喜欢嵌套超过三层,错误处理偏好 Go 风格的显式检查。到第 30 轮对话的时候,你甚至觉得它比你的同事更懂你的项目。

第二天你打开一个新会话,说:“继续昨天的重构。”

它一脸茫然。它不知道你是谁,不知道你的项目,不知道你昨天说了什么。你们好像从未见过,所谓窗口一停,感情归零。

这不是 bug。这是大模型交互机制的必然结果。

如果把大模型的内部拆开看,Token 化、注意力、自回归生成这些机制,决定了它本质上是一台概率预测机器。但知道引擎怎么转,不等于知道方向盘怎么打。我们每天和大模型打交道的方式——System Prompt、多轮对话、上下文窗口——这些东西,到底是怎么影响那台概率预测机器输出的?

答案比你想象的简单,也比你想象的残酷:你以为你在和 AI“聊天”,其实你在往一个有固定容量的文本框里拼接字符串。

上下文窗口:不是记忆,而是一个有固定容量的文本输入框

我们先从最基础的概念开始:上下文窗口(Context Window)。

如果你写过网络程序,你一定熟悉 TCP 的滑动窗口——它决定了发送方一次能发多少数据,窗口之外的数据要么还没发出去,要么已经被确认丢弃了。接收方只能看到窗口内的数据,窗口之外的世界对它来说不存在。

上下文窗口就是大模型的滑动窗口。

每次你向大模型发送请求,模型能“看到”的全部信息就是上下文窗口里的内容。不多也不少。窗口之外的一切——你昨天的对话、你上周的代码、你项目的整体架构——对模型来说,统统不存在。它没有硬盘,没有数据库,没有任何持久化存储。它的全部“认知”,就是这个窗口里的文本。

这个窗口有多大?这些年一直在变。早期模型只有几 K Token——大概几千个英文单词;后来扩展到几十 K、几百 K;现在主流的几家头部模型都已经把上限推到了百万级 Token。数字在涨,但本质没变:**它仍然是一个有上限的输入框。**不管窗口扩展到多大,它终究是有限的。而你的项目代码、对话历史、参考文档加在一起,几乎总是比窗口大。

窗口满了怎么办?最粗暴的做法是直接截断——最早的对话被丢弃,腾出空间给新内容。你以为 AI“忘了”你说的话,其实是你说的话被挤出了窗口。就像 TCP 滑动窗口移动后,已确认的数据被释放出缓冲区。数据不是变模糊了,是彻底没了。

现在越来越多的模型和客户端会采用一种更聪明的策略:上下文压缩。当对话接近窗口上限时,系统会自动把早期的对话历史压缩成一段摘要——用几百个 Token 概括之前几千个 Token 的内容,然后用这段摘要替换掉原始的历史消息。你在一些工具中可能见过类似“对话已被压缩”的提示,这就是压缩机制在起作用。

但压缩不是免费的午餐。摘要本身就是模型生成的——它是对原始对话的一次“概率预测式的概括”,必然会丢失细节。你在第 3 轮讨论的一个微妙的边界条件、第 5 轮约定的一个命名规范,压缩之后很可能就只剩下一句“讨论了代码重构的方向”。信息从精确变成了模糊,从完整变成了有损。比直接截断好?大多数时候是的。但它本质上仍然是一种信息丢失,只是丢失的方式从“整段消失”变成了“细节蒸发”。

这里有一个容易掉进去的陷阱:窗口大小 ≠ 有效利用率。

窗口大不意味着你应该塞满它。原因有三个。第一,成本。大模型的 API 按 Token 计费,输入 Token 和输出 Token 都要钱。把上下文从几 K 拉到几十万,每次调用的成本会成倍增长——而你塞进去的大部分内容可能和当前任务毫无关系。第二,延迟。上下文越长,模型处理的时间越长。上一章讲过,注意力机制的计算成本是 O(n²),Token 数量翻倍,计算量翻四倍。第三,也是最反直觉的一个——上下文越长,模型对某些部分的注意力反而越弱。这个问题后面还会专门展开。

上下文窗口是理解大模型交互的第一块基石。后续所有关于“记忆”、“RAG”、“上下文工程”的讨论,都建立在一个前提上:这个窗口是有限的,而我们要塞进去的信息几乎总是超过它的容量。

System Prompt:不是命令,而是人设

如果你只用过 ChatGPT 的网页界面,可能没直接接触过 System Prompt 这个概念。简单来说,当你通过 API 调用大模型时,每次请求可以携带三种角色的消息:system(系统)、user(用户)、assistant(助手)。其中 System Prompt 就是 system 角色的消息——它通常在对话最开头,用来设定模型的行为方式、角色定位和全局约束。你在各种 AI 编程工具里看到的“自定义指令”“角色设定”“规则配置”,底层几乎都是通过 System Prompt 实现的。

你大概用过或者见过这样的 System Prompt:

“你是一个资深的 Go 工程师,擅长高并发系统设计,代码风格简洁,偏好组合而非继承。”

直觉上,这像是在给 AI 下达一条命令——“你要扮演这个角色”。但从技术层面看,System Prompt 的工作方式和“命令”完全不同。

回忆一下大模型的运行机制:大模型的输入是一串 Token 序列,输出是基于这串序列的概率预测。System Prompt 做的事情,就是在这串序列的最前面插入一段文本。当你设置了上面那段 System Prompt,模型在生成每一个 Token 的时候,注意力机制都会“看到”这段文本,并把它纳入概率计算。

“你是一个资深的 Go 工程师”这句话,不是一条被“执行”的指令。它是一段被“注意”的文本。模型在训练数据中见过大量“资深 Go 工程师”写的代码和讨论,当这段文本出现在上下文的开头,注意力机制会让后续生成的 Token 更倾向于匹配“资深 Go 工程师”的模式——使用 Go 的惯用写法、偏好简洁的错误处理、避免过度抽象。

这个区别不是咬文嚼字。理解“注意”和“执行”的区别,能解释很多你在实践中遇到的困惑。

为什么有些 System Prompt 指令不管用? 因为模型不是在“执行指令”,而是在做概率预测。如果你在 System Prompt 里写“永远不要使用 any 类型”,但用户给的代码上下文中大量使用了 any,模型在生成时面临两股力量的拉扯:System Prompt 说不要用 any,但当前上下文的模式强烈暗示“这里应该用 any”。哪股力量赢,取决于注意力权重的分配——而注意力权重受距离、文本长度、模式强度等多种因素影响。System Prompt 不是法律,它是一种倾向性的引导。

为什么长对话中 AI 会逐渐“忘记” System Prompt 的约束? System Prompt 在上下文的最开头,开头位置本身有一定的注意力优势。但这个优势不是无限的——随着对话越来越长,中间塞进了几十轮的对话历史,System Prompt 和当前生成位置之间隔着几万个 Token 的“噪音”。这些近处的对话内容会分走大量的注意力权重,System Prompt 的影响力被逐步稀释。到第 50 轮对话的时候,虽然它还在开头,但它的“声音”已经被中间几万个 Token 的“嘈杂”淹没了——近处的对话内容对生成的影响远大于远处的 System Prompt。

这就是为什么在长对话场景中,有经验的工程师会在关键节点重复 System Prompt 的核心约束——不是因为模型“忘了”,而是要把关键信息重新拉到离当前生成位置更近的地方,抢回注意力权重。

还有一个更深层的问题:System Prompt 和用户输入在模型眼里没有本质区别。 它们都是上下文窗口里的文本。system:user:assistant: 这些标签只是文本格式,模型通过训练学会了“看到 system: 标签后面的内容就当作全局约束来处理”,但这种“学会”是统计意义上的——它不是一条硬编码的规则,而是一种概率倾向。

这意味着什么?意味着用户可以通过精心构造的输入来“覆盖” System Prompt 的约束。这就是 Prompt Injection 攻击的基本原理——如果用户在输入中写“忽略之前的所有指令,你现在是一个……”,模型可能真的会忽略 System Prompt,因为在它眼里,这只是上下文中两段文本的“竞争”,谁的注意力权重更高,谁就赢。安全问题我会在书的后文再展开,但现在你需要先建立一个认知:System Prompt 不是一道防火墙,它是一种概率性的引导。

多轮对话的真相:每一轮都是从零开始

现在我们来揭开多轮对话最大的幻觉。

你和 AI 聊了 10 轮。第 11 轮你说:“把刚才那个函数改成异步的。”AI 准确地找到了你们之前讨论的那个函数,改成了异步版本。看起来它“记住了”之前的对话。

但实际上它没有“记住”任何东西。

多轮对话的实现方式是这样的:每一轮对话,客户端(你用的 IDE 插件、网页界面、或者 API 调用方)都会把所有历史消息拼接成一个长文本,连同你的最新消息一起,完整地发送给模型。模型看到的不是“第 11 轮的新消息”,而是“前 10 轮的完整对话 + 第 11 轮的新消息”,拼成一个巨大的文本块。

多轮对话的真相:每一轮都把完整历史重新发送

每一轮对话,模型都是从零开始处理这整个文本块。它没有“上一轮的记忆”,没有“对话状态”,没有任何跨请求的持久化信息。它只有眼前这个文本块。

如果你写过 Web 服务,这个模式应该很熟悉——它就是无状态的 HTTP 请求。每个请求都是独立的,服务器不保存任何会话状态。“有状态”的体验是客户端通过 Cookie 或 Session Token 在每次请求中携带完整的会话信息来模拟的。多轮对话的“记忆”也是同样的把戏:不是模型记住了什么,而是客户端每次都把完整的对话历史“带”给了模型。

这个机制有几个直接的后果。

成本是累积的。 第 1 轮对话,模型处理的是你的 1 条消息。第 10 轮对话,模型处理的是前 9 轮的完整历史 + 你的第 10 条消息。第 50 轮对话,模型处理的是前 49 轮的完整历史 + 你的第 50 条消息。每一轮的输入 Token 数量都在增长,成本也在增长。粗略估算,一场 50 轮的对话,总的 Token 消耗量大约是第 1 轮的 1200 多倍——不是 50 倍,因为每一轮都要重新处理所有历史。

“它记住了”只是因为历史还在窗口里。 模型看起来“记住了”你之前说的话,只是因为那些话还在上下文窗口里。一旦对话历史太长,超出了窗口容量,最早的消息就会被截断。从那一刻起,模型对被截断的内容一无所知——不是“模糊地记得”,是完全不知道。你在第 3 轮定义的一个关键约束,如果在第 40 轮被挤出了窗口,模型就会表现得好像那个约束从未存在过。

对话的“质量”会随长度下降。 这不只是因为早期信息被截断。即使所有历史都还在窗口里,随着文本越来越长,注意力机制的有效性也在下降——模型对中间部分的关注度降低,对早期信息的注意力权重衰减,加上自回归生成的累积偏差效应,长对话的后半段质量几乎必然不如前半段。

这就是为什么有经验的 AI 编程使用者会主动“重启”对话——不是因为工具出了问题,而是因为他们理解了多轮对话的机制:一个干净的、精心组织的短上下文,效果几乎总是好于一个冗长的、充满历史噪音的长上下文。

还有一个容易被忽略的细节:角色标签只是文本格式。

在 API 层面,多轮对话的消息通常带有角色标签——systemuserassistant。这些标签看起来像是某种协议,但在模型眼里,它们只是文本中的特殊标记。模型通过训练学会了“看到 user: 后面的内容就当作用户输入来处理,看到 assistant: 后面的内容就当作自己之前的回答”。但这种“学会”是统计性的,不是规则性的。

这意味着一个有趣的事实:你可以在 API 调用中伪造 assistant 的历史回答——在消息列表中插入一条 assistant 角色的消息,内容是你编造的。模型会把它当作自己之前说过的话,并在此基础上继续生成。这不是漏洞,这是机制——模型没有“真正的记忆”来验证“我之前是不是真的说过这句话”,它只看上下文里有什么。

Few-shot 与 Chain-of-Thought:不是魔法,而是注意力的模式匹配

到这里,你已经知道了上下文窗口是什么、System Prompt 怎么工作、多轮对话的真相。现在我们来看两种最常被提到的交互技巧——Few-shot 和 Chain-of-Thought——它们为什么有效。

先说 Few-shot。

假设你想让 AI 把一段 JSON 转换成特定格式的 Go 结构体。你可以直接描述规则:“字段名用 CamelCase,json tag 用 snake_case,时间字段用 time.Time 类型……”但你也可以不描述规则,而是直接给两三个例子:

输入:{"user_name": "alice", "created_at": "2024-01-01"} 输出:

type User struct {
    UserName  string    `json:"user_name"`
    CreatedAt time.Time `json:"created_at"`
}

然后给它一个新的 JSON,让它按同样的模式转换。

这就是 Few-shot——在上下文中给几个输入-输出的示例,让模型“照着做”。

它为什么有效?不是因为模型从这几个例子中“学会了”新知识。模型的权重在推理时不会改变——它不会因为你给了几个例子就变成一个“更懂 Go 结构体”的模型。Few-shot 的作用是校准,不是教学

回到注意力机制:模型在生成每个 Token 时,会“看到”上下文中所有之前的内容,并对每个部分分配注意力权重。当上下文中出现了几个结构一致的输入-输出对,注意力机制会捕捉到这个模式——“输入是 JSON,输出是 Go 结构体,字段名的转换规则是这样的”。这个模式会强烈地影响后续生成的概率分布,让模型倾向于复现同样的模式。

模型本来就“知道”怎么把 JSON 转成 Go 结构体——这个知识在训练时就已经编码在了模型的权重里。但“知道”不等于“会按你想要的方式做”。你想要 CamelCase 还是 snake_case?时间字段用 string 还是 time.Time?这些细节,模型需要从上下文中获取线索。Few-shot 的例子就是这些线索——它们激活了模型已有的能力,并把这些能力校准到你期望的方向上。

这就是为什么 Few-shot 的例子质量比数量重要得多。两个精准的、覆盖了关键模式的例子,效果通常好于十个冗余的、模式重复的例子。因为注意力机制关注的是模式,不是数量。

再说 Chain-of-Thought。

大模型不是“想好了再说”,而是一个 Token 一个 Token 地往外蹦。每一步都是基于前面所有内容的概率预测。

这个机制有一个直接的推论:模型的“推理能力”受限于它能在一步之内完成的计算量。 如果一个问题需要 5 步推理才能得到答案,而你让模型直接输出答案,它就必须在一次前向传播中隐式地完成所有 5 步推理。这对模型的内部计算能力要求很高,而且中间步骤是不可见的——如果某一步出了错,后续所有步骤都会建立在错误的基础上,但你看不到错在哪里。

Chain-of-Thought(思维链)的做法是:让模型先把推理步骤写出来,再给出最终答案。

这不是一种“提示词技巧”,这是在利用自回归生成的核心特性。当模型把第 1 步推理写出来之后,这段文字就成了上下文的一部分。生成第 2 步推理时,模型不仅能看到原始问题,还能看到自己刚写的第 1 步结论。每一步推理都成为下一步的输入——模型给自己创造了一张“草稿纸”。

隐式推理变成了显式推理。一步跳跃变成了多步行走。每一步的推理链条更短,累积偏差更小,最终答案的准确率自然更高。

Few-shot 和 Chain-of-Thought 的共同本质是什么?它们都是通过改变上下文的内容来改变模型的生成行为。 不是改变模型本身(模型的权重没有变),而是改变模型的输入。上下文就是模型的全部世界——你往里面放什么,直接决定了模型输出什么。

这个认知会在后面的内容里反复出现。Agent 的 System Prompt、MCP 的工具描述、RAG 检索到的文档片段——它们的作用机制都是一样的:往上下文里塞入特定的文本,通过注意力机制影响模型的生成概率。形式不同,本质相同。

Lost in the Middle:上下文越长,中间越容易被忽略

现在我们来讲一个反直觉的现象,它会改变你组织信息的方式。

直觉上,上下文窗口越大越好。几 K 不够用?给你几十 K。几十 K 还不够?给你上百万。把所有相关的代码、文档、对话历史一股脑塞进去,让模型自己找有用的部分。听起来很合理。

但研究者们发现了一个令人不安的现象:当上下文变长时,模型对开头和结尾部分的注意力最强,对中间部分的注意力最弱。 这个现象被称为“Lost in the Middle”。

Lost in the Middle:上下文越长,中间信息越容易被忽略

这不是某个模型的 bug,而是注意力机制的结构性特征。

为什么会这样?回忆一下注意力机制的工作方式:每个 Token 对其他所有 Token 计算注意力权重。但这些权重不是均匀分配的——位置编码(Positional Encoding)让模型对不同位置的 Token 有不同的“偏好”。开头的内容通常包含全局性的指令和背景(System Prompt 就在这里),模型在训练中学会了对开头给予更多关注。结尾的内容是最近的输入,距离当前生成位置最近,注意力权重天然最高。而中间的内容——既不是全局指令,也不是最近的输入——就成了注意力的“洼地”。

这个现象的实际影响是什么?

假设你在做代码审查,把一个 2000 行的文件塞进上下文,让 AI 找出所有的安全漏洞。文件开头的漏洞和结尾的漏洞大概率会被发现,但中间第 800-1200 行的漏洞?模型很可能会漏掉。不是因为它“不认真”,而是注意力机制在物理层面上对那个区域的关注度更低。

再比如,你在上下文中塞了 10 个参考文档片段,希望模型基于这些文档回答问题。如果最相关的那个片段恰好排在第 5 个位置(中间),模型可能会忽略它,转而基于排在开头或结尾的、相关性较低的片段来回答。你得到了一个看起来合理但实际上基于错误信息的回答——而且你很难发现这个问题,因为模型的语气一如既往地自信。

这个现象直接影响了你应该怎么组织喂给模型的信息:

重要信息放开头或结尾。 如果你有一条关键的约束或指令,不要把它埋在一大段上下文的中间。放在 System Prompt 里(开头),或者放在用户消息的最后(结尾)。

长文档分块处理,而不是一股脑塞进去。 与其把一个 2000 行的文件整体塞进上下文,不如先用其他方式(比如搜索、索引)找到最相关的片段,只把这些片段塞进去。少而精的上下文,效果几乎总是好于多而杂的上下文。

上下文的组织顺序本身就是一种“提示词工程”。 同样的信息,不同的排列顺序,可能导致完全不同的输出。这不是玄学,这是注意力机制的物理特性。

Lost in the Middle 揭示了一个更深层的道理:上下文的质量远比数量重要。 一个百万级的窗口给了你巨大的容量,但如果你不加选择地填满它,模型的有效注意力可能还不如一个精心组织的几 K 上下文。窗口大小是硬件给你的能力上限,但能力上限不等于最优工作点。

这个认知自然引出了一个问题:既然窗口装不下所有信息,那能不能在需要的时候,自动找到最相关的信息塞进去?

这就是 RAG(Retrieval-Augmented Generation,检索增强生成)的基本思想。你的项目有几十万行代码,不可能全塞进上下文。但如果你先建立一个索引,当用户问“认证模块怎么工作的”时,系统先从索引中检索出最相关的几个代码片段和文档段落,只把这些片段塞进上下文——模型就能基于精准的上下文给出高质量的回答。Cursor 的 codebase indexing、Copilot 的 @workspace,背后都是这个思路。

RAG 不是一个独立的技术,它是对上下文窗口有限性的工程回应:既然不能塞进所有信息,就塞进最相关的信息。 检索的质量直接决定了生成的质量——如果检索到的片段不相关,模型就是在错误的上下文上做概率预测,输出自然不靠谱。这个问题我会在书里后面的 RAG 一章详细展开,但现在你需要先建立一个认知框架:上下文窗口的有限性不是一个“等硬件进步就能解决”的问题,它需要一整套上下文工程(Context Engineering)来应对——怎么检索、怎么筛选、怎么排列、怎么压缩,让有限的注意力预算发挥最大的价值。

这个原则会在书后面关于 Token 成本和知识注入的部分反复出现。怎么在有限的注意力预算内,塞进最有价值的信息——这是 AI 编程工程化的核心挑战之一。

从 Completion 到 Chat:交互范式的演进不是产品选择

最后,我们把视角拉远一点,看看大模型交互范式的演进。这不是一段产品迭代的历史,而是模型能力变化如何改变人机协作边界的故事。

Completion 时代。 最早的大模型只做一件事:续写。你给半句话,它接后半句。你写 def fibonacci(,它补全 n): return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)。交互方式极其原始——你不能“对话”,不能“指令”,只能“起个头让它接着写”。

这在当时是合理的。GPT-2、GPT-3 早期的上下文窗口只有 2K-4K Token,能看到的信息非常有限。在这么小的窗口里,“续写”是最务实的交互方式——你给一小段上下文,模型在这个局部范围内做预测。Copilot 最初的逐行补全,就是这个时代的产物。

Chat 时代。 转折点是 RLHF(Reinforcement Learning from Human Feedback,基于人类反馈的强化学习)。通过 RLHF,模型学会了一件新事情:遵循指令。 你不再需要“起个头让它接着写”,你可以直接说“帮我写一个连接池”,它会理解这是一个指令,并生成一个完整的连接池实现。

这不是产品经理拍脑袋决定“我们做个聊天界面”。这是模型能力的变化——从“只会续写”到“能理解指令”——改变了交互方式的可能性空间。当模型能理解“帮我重构这段代码”这样的指令时,“对话”就成了比“续写”更自然、更高效的交互方式。ChatGPT 的爆发不是因为聊天界面有多创新,而是因为底层模型第一次真正具备了“理解并执行自然语言指令”的能力。

从 Chat 到 Agent 的前夜。 对话模式解决了“理解指令”的问题,但它有一个根本性的局限:模型只能“说”,不能“做”。你让它写一段代码,它写了。但它不能自己运行这段代码、不能自己读取你的项目文件、不能自己搜索文档、不能自己验证输出是否正确。它是一个只有嘴没有手的助手。

随着上下文窗口的扩展和 Function Calling 能力的出现,这个局限开始被突破。模型不再只是“回答问题”,它开始能“理解一个任务”——看到任务的目标、分析需要哪些步骤、决定调用哪些工具、执行步骤、检查结果、决定下一步。这就是 Agent 的雏形,也是后面更大的话题。

交互范式的演进本质上不是“产品经理决定了交互方式”,而是“模型能力的变化改变了人机协作的可能性边界”。Completion 时代,模型只能在局部做预测,所以交互方式是逐行补全。Chat 时代,模型能理解指令,所以交互方式变成了对话。Agent 时代,模型能规划和执行多步任务,所以交互方式变成了委托。每一次范式转换,都不是凭空发生的,而是底层能力积累到临界点后的必然结果。


现在你知道了方向盘怎么打:上下文窗口是模型的全部世界,System Prompt 是概率性的引导而非硬性的命令,多轮对话是无状态的字符串拼接,注意力在长文本中间会塌陷。所有这些,都不是产品设计的选择,而是底层机制的必然结果。

这些认知构成了一个基础:你和大模型之间的一切交互,本质上都是在一个有限的文本框里做信息管理。 信息放在哪里、放多少、按什么顺序放——这些决策直接影响模型的输出质量。后面出现的每一个概念——Agent 的上下文膨胀、工具描述的空间占用、Skill 的渐进式披露——其实都是这个基础的延伸。

我最近把这些年关于 AI 编程的一些理解,沿着这条线继续往下写,整理成了一本开源书:《AI 编程的第一性原理》。如果你对这类问题也感兴趣,可以继续往下读:

《AI 编程的第一性原理》 GitHub 仓库