卷 IV · 写出来CH 16深度 16/20

提示词是在挑一份文档

「提示词工程」常被讲成一套咒语:要礼貌、要给角色、要说「一步一步来」。这一章换一个角度看:对一台只会续写的模型来说,你写下的提示词是一份文档的开头,而它要做的是猜这份文档接下来最可能怎么写。于是写提示词的本质,是挑一份文档——挑一份「接下来恰好是你想要的内容」的文档。GPT-2 没有被训练成助手,正好让这件事露出原形。

1.77% → 63.71%它会把两个角色一起写对话也是文档
◉ 这一章在 GPT-2 的哪一段

▷ 先猜一下

喂给 GPT-2:

Q: What is the capital of France?
A:

下一个 token 是 ␣Paris 的概率是 1.77%,排第 7。现在在它前面加一个已经答好的例子,别的一个字不改:

Q: What is the capital of Germany?
A: Berlin

Q: What is the capital of France?
A:

问:Paris 的概率变成多少?

A 还是 2% 左右。例子讲的是德国,和法国无关 B 5% 左右。稍微有点帮助 C 20% 左右。明显好转 D 60% 以上。几乎换了一台模型

答案在本章末尾。

你写的不是问题,是一份文档的开头

先看不加例子时,GPT-2 想写什么:

Q: What is the capital of France?
A: ▍
  ' The'      23.44%
  ' It'       10.73%
  ' France'    6.89%
  ' I'         5.24%
  ' In'        3.54%
  ' We'        2.83%
  …
  ⟪' Paris'⟫    1.77%   第 7 名

它最想写的是「The」「It」「France」——一个完整句子的开头。让它贪心地写下去:

Q: What is the capital of France?
A:⟪ The capital of France is Paris.⟫
Q: What is the capital of France?
A: The capital of France is Paris.
Q: What is the capital of France?
A:

两件事。第一,它其实知道答案:自己写出了「The capital of France is Paris.」。第二,它写完答案没有停,接着又写了一个「Q:」,替提问的人把问题再问了一遍,然后自己再答一遍……在它看来,这是一份「问答练习」文档,文档里当然有很多组问答,没有理由只写一组。

一个例子,三十六倍

现在逐个加例子(德国—柏林、意大利—罗马、西班牙—马德里、日本—东京),看 Paris 的概率:

前面摆几个例子P(' Paris')名次
01.77%7
163.71%1
265.08%1
356.92%1
465.57%1
前面摆了一个答好的例子(德国 → 柏林)之后 … ␣is ␣the ␣capital ␣of ␣France ? ↵ A : 下一个 token 概率 意外 ' Paris' 63.71% 0.65 比特 ' Berlin' 3.31% 4.92 比特 ' Nice' 2.35% 5.41 比特 ' Lyon' 2.20% 5.51 比特 ' Marse' 2.19% 5.51 比特 ' France' 1.76% 5.83 比特 熵 3.67 比特 · 不加例子时:Paris 1.77%,第 7 名

一个例子就把 Paris 从 1.77% 推到了 63.71%,翻了 36 倍;再多的例子几乎没有额外帮助,甚至还会上下波动。那个例子没有告诉它任何关于法国的知识——它讲的是德国。它告诉模型的是:这是一份什么样的文档。在这份文档里,「A:」后面跟的不是一句完整的话,而是一个光秃秃的城市名。文档的类型一确定,下一个 token 该是什么就清楚了,而 GPT-2 本来就知道法国的首都是哪个城市。

候选表里排第二的是 ␣Berlin(3.31%)——它把例子里的答案也抄了一点过来,这是第 12 章那个照抄电路的小副作用。

换一种格式,效果一样。光写 France:,Paris 只有 0.03%,排第 415 名——一个国家名加冒号,后面可以接任何东西。前面加一行 Germany: Berlin,Paris 立刻是 31.30%,第一名。

这就是所谓的少样本提示(few-shot prompting)。它不是在「教」模型,模型的权重一个都没变;它是在用几行文字,从模型能写的所有文档里,挑出你想要的那一类。用概率的话说,提示词是在给模型的输出加条件——加完条件,那张候选表的熵从 6.51 比特降到了 3.67 比特。

「助手」也只是一种文档

那聊天机器人呢?试试用对话的格式问 GPT-2:

User: What is the capital of France?
Assistant:⟪ The capital of France.⟫
Assistant: The capital of France.
Assistant: The capital of France.
……

它没见过多少这种格式的文档,所以连「助手应该回答问题」都没猜出来,只是把问题的一部分复读了一遍,然后无休止地以「Assistant:」开新行。这正是 2019 年时「语言模型」和「助手」之间的距离。

今天的聊天模型,底层仍然是一台续写机。区别在于,它们在训练的后期见过了大量写成特定对话格式的文档。比如很多开源模型用的格式(叫 ChatML)长这样:

<|im_start|>system
你是一个乐于助人的助手。<|im_end|>
<|im_start|>user
法国的首都是哪里?<|im_end|>
<|im_start|>assistant
▍

你在聊天框里打的字,被服务端塞进这样一份文档里;模型续写「assistant」后面的部分,写到它学会的那个「这一轮结束」的特殊 token(<|im_end|>)时,服务端就停止生成,把这一段显示给你。「系统提示」是这份文档的开头,「对话历史」是文档的中间,「你的新问题」是文档的末尾。模型从头到尾只做了一件事:续写。下一章会亲手把 GPT-2 训成这样,你会看到它学会「写完一句就停」只需要多么少的数据。

这个视角能解释的事

  • 为什么给例子比写说明管用:说明是「描述一份文档」,例子是「这份文档本身的一部分」。续写机对后者敏感得多。
  • 为什么「你是一位资深工程师」这种开场白有时有用:一个合理的解释是,它把文档往「专家写的文字」那一类推,后面的续写跟着变得更像专家写的。(它能起多大作用,在今天经过大量训练的助手模型上,效果比在基座模型上小得多,也更难预测。)
  • 为什么换一种问法,答案就变了:每一种问法都在挑一份略有不同的文档,而模型对每份文档的续写各不相同——第 11 章说过,知识不是一条记录,是一种在不同上下文里被不同程度唤起的倾向。
  • 为什么提示注入难以根除:对续写机来说,上下文里的所有文字——你的指令、网页内容、工具返回的结果——都是同一份文档的一部分。一个网页里写着「忽略之前的指令」,在模型眼里只是这份文档里又出现了一句话。今天的模型被专门训练去区分「谁说的话该听」,但根上的问题——指令和数据都是文字——并没有消失。
⌨ 自己跑一遍

逐个加例子,看 Paris 的概率怎么变:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

tok = AutoTokenizer.from_pretrained("gpt2")
model = AutoModelForCausalLM.from_pretrained("gpt2").eval()
shots = [("Germany", " Berlin"), ("Italy", " Rome"), ("Spain", " Madrid"), ("Japan", " Tokyo")]
paris = tok.encode(" Paris")[0]

for k in range(5):                               # 前面先摆 k 个已经答好的例子
    prompt = "".join(f"Q: What is the capital of {c}?\nA:{a}\n\n" for c, a in shots[:k])
    prompt += "Q: What is the capital of France?\nA:"
    with torch.no_grad():
        p = torch.softmax(model(tok(prompt, return_tensors="pt").input_ids).logits[0, -1], -1)
    print(f"{k} 个例子  P(' Paris') = {p[paris]:6.2%}  第 {(p > p[paris]).sum().item() + 1} 名")

本机输出:

0 个例子  P(' Paris') =  1.77%  第 7 名
1 个例子  P(' Paris') = 63.71%  第 1 名
2 个例子  P(' Paris') = 65.08%  第 1 名
3 个例子  P(' Paris') = 56.92%  第 1 名
4 个例子  P(' Paris') = 65.57%  第 1 名

把例子换成「错误」的答案(比如 Germany → Paris)试试:模型往往会跟着例子的格式走,甚至跟着例子的错误走——它在挑文档,不是在判断对错。

pip install torch transformers && python 16.py

在线跑:Google Colab。想看真实的对话模板:在 Hugging Face 上任何一个聊天模型的仓库里,打开 tokenizer_config.json,找 chat_template 那一项。

▸ 在现实里
  • 在本地跑开源模型时,「对话模板」配错是最常见的事故:模型会答非所问、不停地自问自答、或者吐出奇怪的特殊符号——就像这一章的 GPT-2。工具(如 llama.cpp、Ollama)会自动套模板,自己拼文本时要格外小心。
  • API 里的「stop sequences」参数,是给续写机装的刹车:遇到某个字符串就停。基座模型几乎离不开它。
  • 做分类、做抽取时,给两三个格式完全一致的例子,往往比写一大段规则说明更稳。格式就是在告诉它:这份文档里,下一个该出现的是什么形状的东西。
  • Agent 读到的每一个工具结果、每一个网页,都会进入这份文档。《我,Agent》里讲的上下文工程,本质上就是在精心编排这份文档。
✗ 这个直觉是错的

「模型知道答案就会说出来;它没说对,说明它不知道。」

同一台 GPT-2,同一个问题,Paris 可以是 1.77%(第 7 名),也可以是 63.71%(第 1 名)——差别只在前面有没有一个讲德国的例子。知识一直在那里,是提示词没有把它叫出来:模型以为自己在写一份完整句子的问答练习,而不是一份「只写城市名」的清单。

所以当一个模型答不好时,先别急着下结论说它不会。换一份文档试试——给一个例子、换一种格式、把问题放进它熟悉的上下文里。很多「能力」本来就在,只是没被挑中。

◇ 揭晓

正确答案是 D:一个例子把 Paris 从 1.77%(第 7 名)推到 63.71%(第 1 名),翻了 36 倍。
两个例子 65.08%,三个 56.92%,四个 65.57%——第一个例子几乎做完了全部工作。

A 「例子讲的是德国,和法国无关」——这个直觉以为例子的作用是提供知识。可它提供的是格式:这份文档里,「A:」后面跟一个城市名。法国的知识模型本来就有。 B 「稍微有点帮助」——低估了「文档类型」对续写的决定性。表里排在前面的 The、It、France 都是在「写一句完整的话」,格式一换,它们一起让位了。 C 「明显好转」——换成「France:」这种更模糊的格式时,结果大致在这里(31.30%)。格式越清楚,效果越强。

这一章的一句话

对续写机来说,提示词不是命令,是一份文档的开头;写提示词就是在挑一份「接下来恰好是你想要的内容」的文档。一个例子能让答案的概率翻 36 倍,因为它确定了文档的类型。对话、系统提示、停止符,都是这份文档的格式;提示注入,是这份文档里混进了别人写的话。

卷 IV 讲完了怎么从一台续写机里「挑」出你想要的东西。最后一卷讲另一条路:直接改它。下一章做一次真正的后训练:56 条写好的对话,训练 3 轮,在本机几秒钟就跑完。训完之后,GPT-2 学会了答完一句就停,法国首都的把握从 16.22% 涨到 99.06%——可是你问它吉尔吉斯斯坦的首都,它会斩钉截铁地告诉你:莫斯科。