AI 到底在做什么:从一句提示词到一段代码
如果你最近让 AI 写过代码,这几件事大概都发生过:同一个需求,昨天一次就对,今天再问一次,它却顺手改了你的文件名,还加了一个你从没提过的配置项;提示词写得很清楚,它还是抓错了重点;同一个会话刚开始回复飞快,聊到后面明显变慢。
先玩一个你一定做过的游戏:有人念出"床前明月光",你脑子里会同时冒出"疑、明、月、光、霜"好几个字,但你几乎能肯定,下一个一定是"疑"。
AI 干的就是这件事,只是它一次能列出几万个选项,还要给每个选项标一个"像不像"的分。这张打分的清单,就是这篇要讲的分数表:每写一个字之前,它都先把清单列一遍,再挑一个写下来。
这张表有两个特点,很容易被忽略。
一,它不是随手排的,是算出来的。 你给的每一句话、它前面已经写出的每一个字,都会影响这张表。你写得越具体、越贴近你要的答案,正确的那个选项就越容易被排到前面。
二,它不是排一次就固定住的。 每写一个字,它就把整张表重新算一遍,再挑下一个。
而你从头到尾只能看到被挑中的那一个字。那张表,它从来不给你看。
开头第一个现象——昨天一次就对,今天又不对——答案就在这里。
常见的说法是"随机,没规律,别较真"。这话不对,但也不算全错:从表里挑哪一个,确实是随机的;算出这张表,不是随机的。 同一句开头,只要它前面已经写出的内容不变,算出来就是同一张表。会飘的不是算表,是从表里挑哪一个。
所以这篇要回答三个问题:
- 这张表是怎么算出来的?
- 为什么每次算出来都可能不一样?
- 这个"不一样",我该怎么对付?
读完你会知道两件很实用的事:AI 答错时,它是压根没想到那个答案,还是想到了、只是排在第一的那个不是它——这两种情况,你的应对方法完全相反。调温度、多喂点资料(上下文)这些操作,你也会知道它们到底在改什么,不用再靠猜。
1、先看一眼全貌
这一篇不要求你记住任何一个公式,公式只是配图。真正要装进脑子的是一条主线:
收到一句话 → 把它变成数字 → 算出每个候选该得多少分 → 按分数挑字 → 一个字一个字写下去。
NOTE
下面这个实验完全跑在浏览器里:不联网、不调用任何商业模型、也不加载真实权重。它用的是一组手工编出来的小数字(不是任何模型训练出来的),把"分块 → 表示 → 注意力 → 概率 → 生成"这条链路真的跑一遍,让你看见中间的数值怎么变。用它理解机制,不要用它去评价真实模型的能力。
它一共有 12 步,你可以边读边点:
这 12 步其实是三段。第一段把你的话变成模型能算的数字,第二段算出每个候选该得多少分,第三段才是挑字、写字,顺手把重复计算省掉。
12 步分三段下面就跟着它走一遍。
2、第一段:把你的话变成模型能算的数字
2.1 分块:AI 眼里的字,不是你眼里的字
模型读不懂"变量"这两个汉字。它读到的是一串编号。
这个编号叫 token(词元):把一段连续文本切成一小块一小块,每一块换成一个 ID。
新人几乎一定会在这里踩一个坑:token 既不等于一个汉字,也不等于一个英文单词。 它是介于两者之间的"子词"碎片,具体怎么切取决于模型自己的 tokenizer。
下面这组粗略经验值,能让你对不同内容的"贵贱"有个感觉:
看这张表,你会立刻理解两件事。
第一,中文比英文"贵"。同样意思的一段话,中文消耗的 token 往往明显更多。"你好世界"这 4 个汉字,在很多 tokenizer 下就是 4 个 token;而 "hello world" 大约 2 个。
第二,代码比散文"贵"。标识符短、标点多、缩进密集,一个在散文里很完整的英文单词,放进代码里常被切成好几块。
WARNING
还有一个更反直觉的事实:tokenizer 是会变的。以 Claude 为例,4.7 及更新的模型换用了新的 tokenizer,官方写明同样的文本会比旧模型多出大约 30% 的 token(具体涨幅取决于内容本身)。所以"我上次数过是这么多"这种经验,隔几个版本就可能失效。
这也顺带解释了另一个经典困惑:为什么同一段内容,有人算出来 3000 token,有人算出来 5000?因为计费单位是 token,不是字节,而 token 怎么切取决于模型自己的 tokenizer。精确数字要拿官方的 token 计数接口现算。
到第 1 步,你可以改改输入框里的文字,切成英文、中文、Emoji,看看块数怎么变。
2.2 向量:模型读的是数字,不是文字
拿到 ID 之后,模型会去一张巨大的表里查,把 ID 换成一串数字。这串数字叫向量(embedding)。
这一步很多人会想不通:怎么把"你好"变成 0.3、-0.7 这样的东西?
打个比方。你要给一箱衣服贴标签,不能写"这是件短袖",你得写"尺码:L;颜色:白;季节:夏"。一个词也一样,它没法被直接贴上"人类""代码""形容词"这种标签,但可以被拆成一组"特征值"。哪些数值高,说明这个符号在训练数据里更常跟某一类用法一起出现。
所以模型并不"认识"一个词,它只是知道这个词长得像哪些词。
WARNING
但不要试图解释这些数字的具体含义。真实模型的向量有几百上千维,是训练自己长出来的,没有任何一个人规定"第 7 维代表语法"。演示里缩到 4 维,只是为了让你能看见它变化;看懂色块的大小,不等于看懂了语义。
到第 2 步,你会看到这些数字变成了一排色块。
2.3 位置:同样的词,换个位置就是另一回事
"我去"和"去我",字完全一样,意思完全不同。
注意力机制本身是不知道顺序的——它只会把一堆数字按权重加起来,谁在前谁在后它并不关心。所以顺序信息必须额外注入。
最经典的做法是位置编码:给每个位置生成一组数字,和词向量逐维相加。这样,"第 3 个位置上的'我'"就有了属于自己的表示。
NOTE
演示用的是四维正弦位置编码,这是原始 Transformer 论文里的经典写法。现代模型大多改用旋转位置编码(RoPE)之类的方案,不一定是直接相加,但**"位置必须被单独编码"这件事是一致的**。
到第 3 步,点一下"交换前两个位置",你会看到同一个符号在两个位置上的输入不一样了。
3、第二段:模型怎么"翻阅"你给的资料
到这一步,模型手里已经是一排数字了。接下来它要回答一个问题:我当前位置,该看谁?
3.1 三个角色:提问、报索引、给内容
注意力机制的核心,是给每个位置准备三份材料:
- Query(查询):我想找什么
- Key(键):我这里有什么
- Value(值):我真正的内容
怎么理解?想象你去图书馆查资料。你脑子里有一个问题(Query),每本书脊上贴着一个标签(Key),而书里才是正文(Value)。
做法是:拿你的问题去和每本书的标签做一次比对,越匹配,得分越高。然后按得分高低,从每本书的正文里各取一点,按比例混合起来,凑成你要的答案。
如果只记一句,就记这句:每个位置都去问一圈所有位置"你跟我有多相关",然后按相关度把大家的话混起来听。
落到公式上就三步:
- 打分:Query 和所有 Key 做点积,得到一个相关度分数
- 归一化:把分数过一遍 softmax,变成加起来等于 1 的权重
- 加权求和:按这个权重,把所有 Value 混合起来
写成公式,就是我们熟悉的那一行:
Attention(Q, K, V) = softmax(QKᵀ / √d) · V那个 √d 是干什么的?它只有一个作用:把分数拉回可控范围。 维度越高,Query 和 Key 点积出来的数就越大;不除一下,后面 softmax 会全部塌到 0 或 1,训练直接崩掉。
到第 4 步,你可以点开得分矩阵,验证某一次点积算得对不对;也可以切换"查询位置",看权重怎么跟着变。
3.2 不偷看未来:为什么模型能一个字一个字地写
上面这套机制有个问题:如果生成第 5 个字时,它能看到第 6 个字,那答案就等于抄了。
所以解码器会加一层因果掩码:预测当前位置时,不允许读取它右边的任何位置。实现上,就是把这些位置的分数在 softmax 之前设成负无穷,权重直接变成 0。
WARNING
注意,这不是"给未来的位置打一个低分"。低分意味着"稍微参考一下",而掩码是"完全不许看,出局"。这是两件完全不同的事。
到第 5 步,演示会给出"移除因果掩码"的对照按钮,你可以亲手把未来位置放开看看。
3.3 多头、前馈、残差:还有三件事
真实模型还会做三件事,细节可以先跳过,只要知道它们在干什么:
- 多头:同一段内容,用几组不同的"看法"各看一遍,最后拼起来
- 前馈网络:给每个位置单独做一次非线性变换(演示里是 4 维 → 8 维 → 4 维)
- 残差与归一化:加工完把原来那份也留着一起用,再把数值范围拉回来,避免层数一多就数值爆炸
到第 6、7、8 步,你会依次看到这三样分别长什么样。
但有一条必须记住,因为它是很多人误用的根源:
WARNING
注意力权重高,不等于这就是模型做决定的原因。 权重是"聚合信息时各占多少比例"的中间量,不是一份可读的因果解释。
所以别在团队里说"这个头是事实头""那个头是语法头"——这类命名听着专业,实际上是把自己的猜测包装成了结论。
3.4 顺手回答一个老问题:为什么"上下文越长越差"
2023 年斯坦福团队做过一个很干净的实验:把一段关键信息放在长上下文的不同位置,看模型能不能找出来。结果是一条 U 形曲线——信息在开头和结尾时表现最好,在正中间时明显变差。
论文自己也点出了这个形状:它跟人类记忆里的首因效应、近因效应很像。我们自己回忆一份会议纪要,开头和最后说的部分也确实记得最牢。
所以"上下文越长越差"是有位置的:变差的不是长度,是中间那一段。
推论很实用:最重要的约束、验收条件,放在提示词的开头或结尾,别埋在中间。至于上下文一次该给多少、怎么拆成几步问,下一篇专门讲。
4、第三段:从分数到文字
这是全文最实用的一段。
4.1 logit 不是概率
模型算完最后一步,会给词表里的每个候选打一个分,这叫 logit。
logit 有两个反直觉的特点:
- 它可以是负数
- 它加起来不等于 1
所以 logit 不是概率。它更像"考试得分":考了 80 分,不代表你有八成把握会选这个答案,只是说它比那个 59 分的选项更值得考虑。
真正的概率是 softmax 之后的结果,它把一堆分数变成加起来等于 1 的分布。
WARNING
两个常见误解:其一,得分是负的候选,概率并不为零;其二,最大的候选概率也未必接近 100%。当模型"不确定"的时候,它内部其实是"60% 像 A,25% 像 B,15% 像 C",而不是"就是 A"。
到第 9 步,你可以选中任意一个候选,拖动滑杆改它的 logit,然后看后面整排概率怎么重排;也可以点"恢复网络得分"退回去。
4.2 温度:只改变概率的"形状",不改变"知识"
温度(temperature) 是最值得你搞懂的一个参数。它做的事非常单一:
pᵢ = softmax(zᵢ / T)就是把分数先除以一个数,再去 softmax。就这么一步。
- T 很小(比如 0.2):分布被拉尖,最大概率更接近 1,输出更稳定、更保守
- T 很大(比如 1.8):分布被抹平,长尾词也拿到了机会,输出更发散
- T 趋近 0:等价于永远选最高分,也就是贪心解码,同一个输入每次输出完全一样
到这里必须划一条线:
WARNING
温度不是准确率开关,它是一个随机性旋钮。
调低温度不会让模型"知道更多"。候选集合从头到尾一个都没变过——它只是让模型更倾向于挑那个本来就排第一的候选。
很多人排查问题时第一反应是"把温度调低一点试试",这通常是在用错误的方式解决错误的问题。模型答错,大概率是候选里根本就没有正确答案(它压根不知道,或者你的上下文不足以让它推出来),这时候调温度没有任何用。
那实际该怎么设?
现在主流 API 的默认 temperature 大致在 0.7 ~ 1.0,top-p 保持 1.0(HuggingFace 的生成参数文档里,temperature 和 top_p 的默认值都是 1.0)。
按场景这么分:
到第 10 步,拖动温度滑杆,实时看分布怎么变;再点"抽样 100 次",看有限次抽样的频率和理论概率差多少。
4.3 top-p:另一种改法,思路完全不同
还有一个常被和温度混为一谈的参数:top-p(也叫 nucleus sampling)。
它的做法不是"重新分配每个候选的权重",而是直接删候选。按概率从高到低累加,保留累计概率刚过 p 的那一小撮,剩下的长尾全部剔除,然后在这批候选里重新归一化。
一句话对比:
- 温度:给每个候选重新分配权重,一个都不删
- top-p:把靠后的候选整个删掉
所以 top-p = 0.9 的意思是"只保留最可能的那 90% 概率质量"。它的好处是自适应——模型有把握的时候候选本来就少,犹豫的时候候选自然就多;风险是 p 调太低,可能把候选砍到只剩一两个。
这也正是它们不该一起调的原因:两个机制作用在流水线的不同位置。温度先把分数摊开或收紧,top-p 再在这摊开后的分布上删掉长尾。叠在一起,你没法凭直觉判断"我把温度调到 0.5"最后会剩下哪些候选。
NOTE
演示里只有温度这一个旋钮,因为它是新手真正需要动手理解的那个。top-p 你只需要记住上面那两行对比。
4.4 自回归:它是一个词一个词往前拱的
模型生成的方式是自回归(autoregression):
已经写出的那些字(术语叫"前缀") → 预测下一个词元 → 把这个词元接到后面 → 再预测下一个这解释了很多日常现象。
为什么同一个提示词会给出不同答案? 因为每一步都有"从分布里抽一个"的动作。第一步抽得不一样,后面所有的条件都跟着变了。这不是不稳定,这是它的设计。
为什么改一个词会全盘皆变? 因为已经写出的那些字变了,后面每一步的分布都跟着变。生成是一条会分叉的链,不是一次性的整体计算。
为什么"多跑几次"有时真的有用? 因为你在不同的分支上采样,等于把"最可能的那一条路径"扩展成了"好几条可能的路径"。前提是你能判断哪条更好——这就又回到了人的判断力上。
下面这张图就是那条会分叉的链。
为什么同一个提示词会答出不同结果到第 11 步,你可以手选一个候选追加进去,然后撤销上轮、换一个候选,观察下一轮的分布是怎么跟着变的。
5、长对话为什么变慢、变贵
这一节解释一个你一定有体感的现象:同一个会话刚开始时回复飞快,聊到后面明显变慢。
5.1 缓存了什么
按 3.1 那套注意力算法,生成第 1000 个 token 时,前面 999 个 token 的 Key 和 Value 都要重新算一遍——明明结果每次都一样。
KV Cache 要解决的就是这件事:算过的 K/V 存下来,只为新位置算新的。
NOTE
为什么缓存是安全的?因为因果掩码的存在,历史位置的 K/V 永远不会因为后面新增内容而改变。所以缓存是一次优化,不是质量上的妥协。到第 12 步,你可以亲手比对两条路径的 logits。
5.2 但缓存不是免费
省掉的是"重复计算",省不掉的是"读取"。
每生成一个新 token,模型都要把历史上所有的 K/V 从显存里读出来,送到当前查询面前。上下文越长,每一步要搬的数据就越多。
所以长上下文推理卡的是显存带宽,不是算力。一次请求其实分成节奏完全不同的两段:
- prefill(预填充):把你输入的提示词一次性并行处理完,算力瓶颈,耗时随输入长度大致线性增长
- decode(解码):一个一个地吐字,显存带宽瓶颈,现代 GPU 上大约每秒几十到一百多个 token
下面这张图画的就是这个分工:
一次请求分成两段,瓶颈完全不同缓存要占多少显存?可以直接算。以 Llama 3 70B 为例:
80 层 × 8 个 KV 头 × 128 维 × 2(K 和 V)× 2 字节(FP16)≈ 320 KB / token上下文到 10 万 token,光是缓存本身就要 32 GB 显存。这就是长上下文为什么是显存问题,而不只是速度问题。
还有一个容易被忽略的账单事实:输出 token 的单价远高于输入。以 Claude 为例,当前主流型号正好是 5 倍(Opus 4.7 是输入 $5、输出 $25 每百万 token)。所以"让它少写废话"不只是省时间,是真的省钱。
5.3 你能主动开的缓存:提示词缓存
先把两件事分清楚:提示词缓存和 KV Cache 不是同一个东西,只是背后是同一个想法。
KV Cache 在模型内部:一次对话里算过的 K/V 留在显存里给下一轮用,省的是"这一轮内部重复算"。提示词缓存在服务商那一层:同一段前缀被存下来,下一次请求直接读,省的是"这一轮开头又重算了一遍"。
对开发者的区别只有一个:KV Cache 不用你管,提示词缓存你要主动去开。
主流 API 现在都支持提示词缓存(prompt caching)。以 Claude 为例:
- 缓存命中读取:基础输入价的 0.1 倍(省 90%)
- 5 分钟缓存写入:基础输入价的 1.25 倍
- 1 小时缓存写入:基础输入价的 2 倍
写入比读取贵,所以 5 分钟档只要命中一次就回本,1 小时档要命中两次。
这背后有一条很重要的工程常识:缓存是按前缀匹配的。官方的说法很直白:缓存键是把断点之前的内容做哈希算出来的,所以断点之前动一个字,后面全部作废。
所以正确的做法是:
系统提示 → 工具定义 → 稳定的项目背景 → 用户本次的问题把会变的东西(当前时间、用户 ID、动态检索结果)统统放到最后面。如果你在前面塞了会变的内容,后面全部作废,缓存命中率直接归零——而这个变化在部署 diff 里是完全看不出来的。
WARNING
具体倍率各家会调整,换模型也可能变。上面是当前的数字,用之前请以官方定价页为准。但**"稳定前缀放前面、动态内容放后面"这个原则不会变**。
6、这些原理,能变成什么动作
知道了机制,才能把它翻译成日常操作。下面五条可以直接抄走:
五条能直接抄走的动作下面这个模板可以直接抄走:
请根据这段函数与失败用例,先定位遗漏的输入条件,再提出最小修复。
不要用降低温度代替错误分析,也不要补猜未提供的业务规则;
修改后运行对应测试,说明哪些结果已验证、哪些仍未知。这个模板的每一句,都对应上面某一条机制:
- "先定位遗漏的输入条件,再提出最小修复"——先确认正确答案进了候选,再谈怎么改
- "不要用降低温度代替错误分析"——温度只改概率的形状,不改候选集合
- "不要补猜未提供的业务规则"——候选里没有的东西,不会因为你的提示而出现
- "说明哪些已验证、哪些仍未知"——用外部检查代替自我感觉
7、最后
理解原理的意义,不是给模型贴一个标签。
你说"它就是一只概率鹦鹉"也好,说"它其实很可靠"也好,都帮不了你把下一段代码写对。真正有用的只有一件事:它让你能判断,什么时候该信它、什么时候必须自己去验。
回到开头那个问题:同一个需求,昨天一次就对,今天再问一次怎么就改了你的文件名?
现在你知道答案了——它不是时灵时不灵,是每写一个字,它都要重排一次那张分数表,而每次排出来的表都可能不一样。你要做的不是等它变稳定,而是把会飘的地方收窄:上下文给准、要求写清、结果让它自己验。
这和上一篇的结论是一回事:AI 编程把门槛降到了地板,但天花板捅到了大气层。地板那部分,它一个人就能走完;天花板那部分,永远需要有人站在上面看着。
参考资料
- Lost in the Middle: How Language Models Use Long Contexts(TACL 2023;"信息在开头和结尾表现最好、在正中间明显变差"的 U 形曲线):https://arxiv.org/abs/2307.03172
- Anthropic 定价(4.7 及更新模型换用新 tokenizer、同一文本多出约 30% token;缓存读写倍率;输出 token 单价为输入的 5 倍):https://platform.claude.com/docs/en/docs/about-claude/pricing
- Anthropic Prompt caching(缓存按前缀匹配,断点之前动一个字后面全部作废):https://platform.claude.com/docs/en/docs/build-with-claude/prompt-caching
- Anthropic Token counting(token 计数接口;新旧 tokenizer 的差异约 30%):https://platform.claude.com/docs/en/docs/build-with-claude/token-counting
- HuggingFace Text generation(temperature 与 top_p 的默认值都是 1.0):https://huggingface.co/docs/transformers/main_classes/text_generation