图书管理员奇遇记 —— 什么是上下文(Context)
第 1 章 图书管理员奇遇记 —— 什么是上下文(Context)
1.1 健忘的图书管理员:一个没有上下文的早晨
清晨八点,阳光穿过图书馆(Library)的玻璃窗,洒在积满灰尘的木桌上。九岁的小米(Mia)推开橡木大门,看见一位头发花白的老爷爷——图书管理员佩奇先生(Mr. Page)——正呆呆地盯着面前一张空白的纸条。
"早上好,佩奇先生!我想找一本关于……呃……"小米突然卡壳了,因为她自己也忘了刚才想说什么。
佩奇先生推了推圆框眼镜,温和地笑了笑:"小米啊,你又来了。我有一个不太好的消息要告诉你——我的记忆像金鱼一样(I've got a memory like a goldfish),每次只能记得一张纸条上写的内容。如果你只告诉我一个词,我会猜错;如果你告诉我一段话,我就能猜得很准。"
小米睁大眼睛:"一张纸条?一段话?这和 AI 有什么关系?"
"所有的关系。"佩奇先生从抽屉里拿出一叠彩色便签纸,"你知道吗?那个叫做 ChatGPT、Claude、Gemini 的大语言模型(Large Language Model,简称 LLM),本质上就是一个……图书管理员。它读的不是纸,而是 token;它记的不是句子,而是向量。但它和我一样,没有'上下文'(Context),就什么也猜不出来。"
这个看似简单的开场,藏着本章要回答的两个核心问题:
1. AI 模型里的"上下文"到底是什么意思?(What does "context" really mean inside an AI model?) 2. 为什么没有上下文,AI 就答不出好问题?(Why can't AI answer well without context?)
在接下来的八个小节里,我们会一层一层地拆开这两个问题。你会看到 token(词片段)、Context Window(上下文窗口)、Prompt(提示词)这些专业术语是如何像乐高积木一样拼在一起的。最棒的是,你只需要一台电脑、一段 30 行左右的 Python 代码,就能亲手验证:给模型多一点上下文,它的回答真的会变好。
让我们正式开始这段旅程。
---
1.2 上下文的定义:在语言模型眼中,"上下文"是什么?
让我们先把"上下文"这个词本身说清楚。
在日常汉语里,"上下文"指的是一段文字的前后环境。比如你读到"他吃了一碗__",空格处填"米饭"还是"面条",要看你前面写的是"中午在食堂"还是"晚上在餐厅"。这种"前面的内容影响后面的理解"的现象,就是上下文在起作用。
在 AI 领域,这个词的含义完全一样,只不过换了一个更精确的说法:
> 上下文(Context):在语言模型生成回答之前,它能"看到"的所有文字。这些文字包括系统提示、用户输入、历史对话、检索到的文档,以及模型自己之前生成的片段。
用更技术一点的英文表述:
> Context: The entire sequence of tokens that a language model conditions on when predicting the next token. Formally, if $C = (t_1, t_2, \ldots, t_n)$ represents the context, the model computes a probability distribution over the next token $t_{n+1}$.
我们把这串 token 序列叫做 $C$,把模型要预测的下一个 token 叫做 $t_{n+1}$。模型的任务就是:看到 $C$,猜出 $t_{n+1}$ 最可能是什么。
回到小米和佩奇先生的图书馆。如果小米只说一句"我想要一本关于__",佩奇先生眼前就一张便签纸(这一张纸条就是"context"),那他只能瞎猜——关于恐龙?关于太空?关于怎么做蛋糕?可能性无穷无尽。
但如果小米说:"我想要一本关于__,因为我们班下周要科学课展示,我想介绍一种会变色的动物。"——现在佩奇先生面前有六张便签纸(六张纸条组成的 context),他立刻就能锁定答案:《变色龙》。
这就是上下文的力量:它把"无穷可能"压缩成"一个高概率答案"。语言模型的工作原理,从最抽象的层面看,就是在做这件事——只不过它每秒能做几十亿次猜测,而且是用数学公式来猜。
下面这张图展示了一个简化的、从用户输入到模型输出的完整上下文流动过程:
```mermaid flowchart LR A[User Input 用户输入] --> B[Tokenizer 分词器] B --> C[Token IDs 数字序列] H[History 历史对话] --> D C --> D[+ Concatenate 拼接上下文] D --> E[Model Inference 模型推理] E --> F[Next Token 下一个Token] F --> G[Output Text 输出文字] F --> H ```
请仔细看这张图的两个关键点:
- **"History 历史对话"也会成为上下文的一部分**。也就是说,模型第一轮回答的内容,到了第二轮,就变成了它的"记忆"。
- **新生成的 token 也会回流到上下文里**,形成一种自我喂食(self-feeding)的循环。这就是为什么 AI 能够"接着写下去"——它每写一个字,就把那个字加进自己的 context 里。
理解了这张图,你就掌握了本章一半的内容。下面我们要把图里的每一个盒子拆开看。
---
1.3 Token:模型看到的不是"字",而是"积木"
你也许会问:模型既然是"读文字",为什么不直接读"今天天气真好"这七个汉字,而是要把它切成更小的片段?
答案是:语言模型从来不按"字"或"词"工作,它按 token 工作。
Token 是模型处理的最小单位,可以是一个完整的汉字、一个英文单词、一个标点符号,也可以是单词的一部分。常见的切分算法叫做 BPE(Byte Pair Encoding,字节对编码),它的核心思想是:先按字符切,再把出现频率最高的相邻字符对合并,直到达到预设的词表大小。
举个例子。同一个句子"今天天气真好",不同模型可能切成不同的样子:
| 模型(Model) | 切分结果(Tokens) | Token 数量 | | -------------------- | ------------------------------- | ---------- | | 一个中文友好模型 | `["今天", "天气", "真", "好"]` | 4 | | 一个英文主导模型 | `["今", "天", "天", "气", "真", "好"]` | 6 | | 另一个英文主导模型 | `["今", "天天", "气", "真", "好"]` | 5 |
英文的 token 切分更复杂。比如单词 `"unhappiness"` 可能会被切成 `["un", "happiness"]` 或者 `["un", "happy", "ness"]`。这看起来很别扭,但好处是:词表(Vocabulary)大小可控。一个有 5 万个 token 的词表就能覆盖几乎所有人类语言,而如果用"整词"做单位,词表可能需要几百万。
让我们用 Python 实际看看 token 长什么样:
```python
一个简化的中文 token 切分演示
注意:这只是教学演示,真实模型用 BPE 算法
text = "今天天气真好,我们一起去公园玩"
按字符切分
char_tokens = list(text) print("按字符切分:", char_tokens) print("Token 数量:", len(char_tokens))
按词切分(简单的空格/标点分割)
import re word_tokens = re.findall(r'[\u4e00-\u9fff]+|[a-zA-Z]+|\d+|[^\s\w]', text) print("按词切分:", word_tokens) print("Token 数量:", len(word_tokens)) ```
运行这段代码你会看到:"今天天气真好,我们一起去公园玩"按字符切是 13 个 token,按词切是 6 个 token。同一个句子,token 数量可以差很多。这一点很重要,因为它直接影响你能塞进 Context Window 的内容长度。
为什么要强调 token?因为接下来你会反复看到一个词——Context Window——它的单位就是 token,不是字符,不是单词,不是汉字。
---
1.4 预测下一个 Token:图书管理员的猜字游戏
现在我们来到全章最核心的一节:语言模型到底在做什么?
答案用一句话就能说完:它在玩一个"猜下一个 token"的游戏。
形式化地说,如果当前上下文是 $C = (t_1, t_2, \ldots, t_n)$,模型要计算的是条件概率:
$$P(t_{n+1} \mid t_1, t_2, \ldots, t_n)$$
这个公式的意思是:"在看到前面 $n$ 个 token 的情况下,下一个 token 是 $t_{n+1}$ 的概率是多少?"
对于每个可能出现的下一个 token,模型都给出一个概率。所有概率加起来等于 1。最后模型选择概率最高的那个(或者按概率采样一个)作为输出。
让我们用一个具体的例子走一遍这个过程。假设上下文是:
``` 今天天气真 ```
可能的下一个 token 和它们的(假想的)概率:
| 下一个 token | 概率 | | ------------ | ---- | | 好 | 0.62 | | 不错 | 0.18 | | 糟糕 | 0.08 | | 冷 | 0.05 | | 晴 | 0.04 | | 其他所有词 | 0.03 |
模型会选概率最高的"好",于是输出变成"今天天气真好"。
但是!如果上下文只有"天气"两个字,情况就完全不同了:
``` 天气 ```
| 下一个 token | 概率 | | ------------ | ---- | | 预报 | 0.15 | | 不错 | 0.12 | | 寒冷 | 0.10 | | 晴朗 | 0.10 | | 真好 | 0.08 | | 其他所有词 | 0.45 |
概率分布变得扁平了——最高的才 0.15,模型选什么都显得"没把握"。这就是没有足够上下文时 AI 容易"胡说八道"的根本原因:它没有足够的信息来锁定一个高置信度的答案。
让我们回到佩奇先生的图书馆。这个猜字游戏其实就是小米和佩奇先生的对话:
- **没有上下文时**,小米只说"今天天气真",佩奇先生能想到 50 种合理的接法。
- **有完整上下文时**,比如小米说"妈妈说明天要带我去野餐,所以我想知道今天天气真",佩奇先生几乎只能接"好"。
Transformer 架构(ChatGPT、Claude 这些模型背后的"大脑")之所以强大,就是因为它能高效地从很长的上下文中找出关键线索,然后给出高置信度的预测。但它的核心任务,从头到尾,就是这一个:猜下一个 token。
你可以把这个事实记在心里:大语言模型没有"理解",它只有"预测"。 当然,这并不意味着它"愚蠢"——一个能在几十亿参数下精确预测下一个 token 的系统,表现出来就是看起来像在"理解"。这是另一个深刻的哲学问题,我们留到后面的章节再讨论。
---
1.5 Context Window:图书管理员的书桌有多大?
现在我们已经知道了:模型需要"看到"上下文才能做出好预测。那么问题来了——它一次最多能看多少?
这就是 Context Window(上下文窗口) 的概念。
继续用图书馆的比喻:
> Context Window 就是佩奇先生书桌的大小。 他的书桌只能摊开 $N$ 张便签纸(每张便签纸就是一个 token)。便签纸越多,他能"看到"的线索就越多;便签纸越少,他就越容易猜错。
不同模型的 Context Window 大小差别很大:
| 模型(Model) | Context Window (tokens) | 约等于多少字 | | -------------------------- | ----------------------- | ------------ | | GPT-3.5(早期版本) | 4,096 | 约 2,000–3,000 个汉字 | | GPT-4 | 8,192 | 约 4,000–6,000 个汉字 | | GPT-4 Turbo / Claude Sonnet | 128,000 | 约 60,000–90,000 个汉字 | | Claude 3.5 / GPT-4o | 200,000 | 约 100,000+ 个汉字 | | Gemini 1.5 Pro | 1,000,000(实验性) | 约 500,000+ 个汉字 |
数字看起来很惊人,但请注意两个隐藏的限制:
第一,token 不是字。一个汉字通常对应 1–2 个 token,一个英文单词通常对应 0.75–1.5 个 token。所以 Context Window 是按 token 计算的,不是按字数计算的。
第二,越长不一定越好。即使一个模型宣称支持 200K token 的窗口,它对窗口中间部分的回忆能力(recall)往往比开头和结尾要差。这种现象叫做 "Lost in the Middle"(迷失在中间),是由 Stanford 和 Samaya AI 的研究者在 2023 年的论文中正式提出的。简单来说,模型就像一个只能看清书桌两端的人——中间的内容容易被"忽略"。
这就引出了一个非常实际的问题:当对话变长,超过 Context Window 时该怎么办?
三种常见的策略:
1. 截断(Truncation):只保留最新的 N 个 token,丢掉最老的。 2. 摘要压缩(Summarization):把旧的对话压缩成一段简短的摘要,再放进上下文。 3. 检索增强(RAG, Retrieval-Augmented Generation):把旧的内容存在一个外部数据库里,需要时再临时检索出来。
这三种策略听起来是不是很耳熟?没错,它们就是你即将在第 2 章和第 3 章学到的"长上下文"和"RAG"技术。本章只是抛砖引玉。
---
1.6 Prompt vs Context:开胃菜 vs 整桌菜
这是初学者最容易混淆的一对概念。让我们把它讲清楚。
Prompt(提示词) 是你这一轮输入给模型的文字。它是上下文的起点,但不是上下文的全部。
Context(上下文) 是模型在生成回答前能"看到"的所有文字。它包括:
1. System Prompt(系统提示):开发者预设的"角色说明",比如"你是一位耐心的数学老师"。 2. User Prompt(用户提示):你这一轮说的话。 3. History(历史对话):之前所有轮次的 user 输入和 assistant 回复。 4. Retrieved Context(检索内容):从外部数据库或文档里临时拉来的相关片段。 5. Tools / Function Call Results(工具调用结果):调用计算器、搜索引擎等工具后返回的数据。
用一个生动的比喻:
> Prompt 是你递给图书管理员的那张便签纸("我想要一本关于变色龙的科普书")。 > Context 是图书管理员此刻能看到的全部资料(包括他记得的昨天你们聊过什么、他刚才从书架上找到的目录、他从百科全书里抄下来的一段话……)。
所以,Prompt 是 Context 的子集。每次你按下回车键,模型实际上是在处理一整摞便签纸,而不只是你刚写的那一张。
这听起来可能有点抽象,让我们写一段伪代码看清楚:
```python
伪代码:展示 Context 的真实构成
def build_context(user_message, history, system_prompt, retrieved_docs): context = []
1. 系统提示(最先)
context.append(system_prompt)
2. 检索到的外部资料
for doc in retrieved_docs: context.append(f"[参考资料] {doc}")
3. 历史对话(按时间顺序)
for turn in history: context.append(f"[用户] {turn.user}") context.append(f"[助手] {turn.assistant}")
4. 当前用户消息(最后)
context.append(f"[用户] {user_message}")
return "\n".join(context) ```
调用 `build_context("推荐一本书", history=[...], system_prompt="你是图书管理员", retrieved_docs=[...])` 返回的那一大段文字,就是模型真正看到的 Context。而 `"推荐一本书"` 只是 Prompt——它被塞在 Context 的最末尾。
理解这个区分之后,你就能明白为什么"调教"AI 不能只看 Prompt 写得好不好——**你可能忽略了 System Prompt、History、甚至