下一个 token 的概率世界:模型到底在算什么
AICon 2026 学习系列 · 阶段 0 · 大模型地基 · 第 1/43 篇 · 📖 师生对话实录(已完结 · 2026-08-24 单元毕业)
上一篇:《AICon 2026 学习总纲》
下一篇:《上下文窗口与 KV cache:模型的记忆账本》
学习大纲:《AICon 2026 学习总纲》
🧪 本篇实验代码:code-corey/ai-lab · 0-llm-basics/llm-basics-01(含运行说明与实验索引,下文各实验处注明来源)
状态:课堂实录,随每轮教学按时间顺序追加,不作整理。
对应总纲单元:阶段 0 · 单元 0.1「下一个 token 的概率世界」
本篇验收标准(全部通过才进入下一篇):
课前准备(操作记录 · 2026-08-23)
| 检查项 | 结果 |
|---|---|
| Python | 3.11.15 ✅(本课够用,总纲建议 3.12+) |
| DEEPSEEK_API_KEY / DASHSCOPE_API_KEY | 均未设置 ⚠️(后续采样实验前需补) |
| Ollama | 未安装 ⚠️(阶段 0.4 本地跑模型前再补,不影响本课) |
| 实验目录 | 新建 C:\Users\chengongyi\llm-lab\,本课实验均放此处 |
| 踩坑记录 | Windows 控制台默认 GBK 编码,Python 中文输出全部乱码;命令前加 PYTHONUTF8=1 解决 |
第 1 轮:开场 + 概念一「模型只是在玩词语接龙」
老师(开场白):
同学你好,欢迎来到这门课。
从今天起,我是你的老师,你是我唯一的学生。我们有一个为期 15 周的约定:把 AI 这个领域,从零基础一路学到能看懂 InfoQ AICon 2026 每一场演讲的程度——教学大纲就在你手边那份总纲里,8 个阶段、43 篇文章,今天我们正式踏出第一步。
先把课堂规矩说清楚,就三条:
第一,一次只讲一个概念。 每个概念我都用最白的话讲,讲完就停,等你消化。绝不像市面上那些教程,一上来就"Transformer 的多头注意力机制"——那种讲法是把初学者按在地上摩擦,我们不来这套。
第二,你随时可以打断我。 听懂了,你说"继续";没听懂,你就问,问到懂为止。没有蠢问题,只有没问出来的问题。 你现在觉得"这问题是不是太基础了"的每一个瞬间,恰恰是最该开口的瞬间。
第三,每一课都动手。 我会造很多小实验,全部在你这台电脑上真跑,你亲眼看输出。看到的才叫知识,听来的只是传说。
我们的所有对话会按时间顺序逐字记录进博客文章里——包括你说的话、我说的话、我们跑的每一个实验。所以放松,这是课堂,不是考场。
今天讲的东西,是整栋大楼的地基里埋得最深的那块石头。42 篇文章里的每一个字——温度、幻觉、上下文、Agent——全都没法绕开它。它叫:
模型不会"回答问题",它只会玩一种游戏:看着前文,猜下一个词。
老师(概念一:模型的全部秘密,只是词语接龙):
我们从你一定有过的体验讲起。
你用 AI 聊天的时候,有没有遇到过两件事:同一个问题问两遍,答案不一样;以及,它有时一本正经地胡说八道——脸不红心不跳地编造一个不存在的人名、论文、API 函数,说得比真的还真。
市面上九成的解释到这里就停了,给你两个词:"随机性"和"幻觉",然后你带着两个更大的问号走了。我们今天要把这块地基挖到底:要解释这两件事,必须先回答一个更根本的问题——模型接到你的问题之后,它到底在干什么?
答案会让你意外地失望:
它没有"理解"你的问题,没有"思考",更没有"查资料"。它只做一件事:拿你给它的所有文字,猜下一个字。猜中一个,把猜中的字接到文字末尾,再看一遍全部文字,猜下一个。再接上,再猜。如此滚动,直到它觉得该停了。
就这么多。ChatGPT、DeepSeek、Claude,所有大模型的生成过程,本质都是这个接龙游戏。你看到它"噼里啪啦打出一大段话",拆开看,是一个字一个字蹦出来的——每个字蹦出来之前,模型都只回答了一个问题:"根据目前这些字,下一个字最可能是什么?"
空口无凭,我给你造了个实验。我们用 50 行 Python,造一个只会玩接龙的"迷你模型"——它没有神经网络、没有 GPU、没有一点智能,只干了一件笨事:拿几句唐诗和白话文,统计"每个字后面都跟过哪些字、各跟了几次",然后从"床"字开始,按次数多少抽签,抽到谁就接谁,滚 40 步。
操作记录(实验 1):新建 C:\Users\chengongyi\llm-lab\exp1_tiny_next_char.py(源码):
# -*- coding: utf-8 -*-
"""实验 1-1:50 行造一个「只会接龙」的最小语言模型"""
import random
from collections import defaultdict
# 语料:几首唐诗 + 一段白话
corpus = (
"床前明月光,疑是地上霜。举头望明月,低头思故乡。"
"春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。"
"白日依山尽,黄河入海流。欲穷千里目,更上一层楼。"
"今天天气很好,我们一起去公园散步。今天天气不好,我们只好在家看书。"
"我今天中午吃了一碗面,明天中午我想吃一碗饭。"
)
# 第一步:建表。stats[前一个字][下一个字] = 出现次数
stats = defaultdict(lambda: defaultdict(int))
for prev, nxt in zip(corpus, corpus[1:]):
stats[prev][nxt] += 1
# 第二步:查表——看看「概率分布」长什么样
for ch in ["床", "明", "今"]:
total = sum(stats[ch].values())
detail = " ".join(f"{k}:{v}次({v/total:.0%})" for k, v in stats[ch].items())
print(f"「{ch}」之后共出现 {total} 次 → {detail}")
# 第三步:从「床」开始抽签接龙 40 步
random.seed(42)
current = "床"
generated = [current]
for _ in range(40):
table = stats[current]
chars, weights = zip(*table.items())
current = random.choices(chars, weights=weights, k=1)[0] # 按权重抽签
generated.append(current)
print("".join(generated))真实运行输出(第一次运行因 Windows GBK 编码全为乱码,加 PYTHONUTF8=1 后正常):
「床」之后共出现 1 次 → 前:1次(100%)
「明」之后共出现 3 次 → 月:2次(67%) 天:1次(33%)
「今」之后共出现 3 次 → 天:3次(100%)
床前明月光,我想吃了一层楼。春眠不觉晓,疑是地上霜。夜来风雨声,我今天气不好,我今老师(实验讲解):
盯着这三行输出看,今天最重要的三个发现全在里面:
发现一:它真的能生成"像人话"的东西——但它在胡说八道。
看第二行生成结果:"床前明月光,我想吃了一层楼。"每个字接得都算顺,标点都停对了地方,甚至"一层楼"还挺有诗意——可整句话是彻头彻尾的胡话。流畅和正确是两回事,一个只靠"统计哪个字常跟哪个字"的机器,完全可以流畅地胡说。记住这个体感,这就是"幻觉"最原始的种子——真模型几千亿参数,但生成的原理和这 50 行代码同根同源。
发现二:模型的世界里没有标准答案,只有一张概率表。
看查表那三行——同样是"猜下一个字",处境完全不同:"床"后面 100% 是"前",没有悬念;"今"后面 100% 是"天";而"明"后面,67% 是"月",33% 是"天"——两个候选打起来了。模型每次猜字,面对的就是这样一张表:词表里每个候选字都有个分数,它按分数抽签。分数高的容易中,分数低的也可能中。你那个"同一个问题每次答案不一样"的疑问,答案已经在这张表里了——细节我们下个概念讲。
发现三:真模型和这个玩具的差距,比你想的小。
你可能会说:这也太笨了,真的 ChatGPT 就这样?差别其实只有三处——① 真模型的"语料"是几乎全人类的文本,几万亿个字;② 它猜下一个字时看的不只是前 1 个字,而是前文全部;③ 它的"统计表"大到没法直接存,所以用一个神经网络来学出这张表。但"根据前文猜下一个字,猜中拼回去,继续猜"这个游戏规则,一个字都没变。业内给这个滚动前进的机制起了个名字,叫自回归生成(autoregressive)——"自"己生成的结果"回归"到输入里当条件。名词你今天只需要混个脸熟,游戏规则你已经亲眼见过了。
老师(作业):
轮到你来当一次模型了。下面两个填空,不要想,报出你第一反应的答案:
① 「白日依山____」
② 「我今天中午吃了一大碗____」
报完答案再回答我一个问题:第①题你为什么脱口而出、毫不动摇?第②题你为什么犹豫了?你的"犹豫",用今天的话说,说明了什么?
有问题随时打断我。没有的话,就交作业。📝
学生:(等待回复)
第 2 轮:作业批改 + 概念二「token:模型眼里的文字积木」
学生:百日依山尽,因为这个是已经确定好的古诗,是确定下来的东西。但是后面的空格,不好填,因为没有上下文,这里填什么都可以。
老师(作业批改):
两题全对,思考题答到了骨头缝里。翻译成行话:
- 第①题"脱口而出毫不动摇"——脑内概率表上"尽"一个候选占压倒性概率,分布是尖的。尖分布抽签,抽一万次都是它。
- 第②题"犹豫"——"面/饭/牛肉面/汤……"一堆候选都有不小概率,分布是平的。平分布抽签,每次结果都可能不同。"犹豫"就是平坦分布的主观体验。模型回答开放性问题时"每次说的都不一样",根源一模一样。
精修一个用词:你说第②题"没有上下文"——其实有的,"我今天中午吃了一大碗"这十个字就是上下文,只是它约束力弱,推不平分布。上下文的全部作用,就是把"填什么都可以"推向"只能是它"。这句话先存着,下一篇《上下文窗口与 KV cache》整篇都在讲它。
彩蛋必须点一下:你打的是"百日依山尽"。千古名句背了半辈子,手一滑照样输出错字——因为你大脑输出这句话时也是"接龙+抽签",你的采样器偶尔也会抽错签。你本身就是一台接龙机器,这正是第一课的论点。(此错别字原样记录,是全课堂目前为止最好的教具。😄)
老师(概念二:token——模型眼里的"文字积木"):
先还第 2 轮欠的债:测试请求 24 个字符,计费却是 20 个 token——字符和 token 不是一回事,中间隔着一台"切积木的机器"。
关键认知一句话:模型从来不看"字",它眼里只有 token——把文字切成标准积木块,每块对应词表里的一个编号(token ID)。 你眼里的"床前明月光",模型眼里是 5649, 974, 45387, 1837, 320 这样的编号序列。切积木的机器叫 tokenizer,它在模型开工前就把活干完了。
操作记录(实验 2):新建 C:\Users\chengongyi\llm-lab\exp2_token_count.py(源码,仓库收录为学生加了字节兜底的修正版)
# -*- coding: utf-8 -*-
"""实验 2-1(修正版):token 到底怎么数的?
用 offset_mapping 把每个 token 对应的原文片段切出来。
"""
import json
import os
import urllib.request
BASE = os.environ["OPENAI_BASE_URL"].rstrip("/")
KEY = os.environ["OPENAI_API_KEY"]
MODEL = os.environ["OPENAI_MODEL"]
def tokenize(text):
body = json.dumps({"model": MODEL, "text": text}).encode("utf-8")
req = urllib.request.Request(
f"{BASE}/tokenization", data=body,
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"][0]
def byte_to_char_offset(text, byte_offset):
"""将字节偏移量转换为字符偏移量(UTF-8)"""
# 如果字节偏移量是 0,直接返回 0
if byte_offset == 0:
return 0
# 遍历字符串,累加每个字符的字节数,直到达到 byte_offset
byte_pos = 0
char_pos = 0
for char in text:
char_byte_len = len(char.encode('utf-8'))
if byte_pos + char_byte_len <= byte_offset:
byte_pos += char_byte_len
char_pos += 1
else:
break
# 如果 byte_offset 正好落在字符边界上,返回对应的字符位置
# 如果落在字符中间,返回前一个字符的位置(向下取整)
return char_pos
def show(text):
d = tokenize(text)
# 把每个token对应的原文片段切出来
pieces = []
for tid, (start, end) in zip(d["token_ids"], d["offset_mapping"]):
# 将字节偏移量转换为字符偏移量
char_start = byte_to_char_offset(text, start)
char_end = byte_to_char_offset(text, end)
# 用字符索引切片,不会乱码
token_text = text[char_start:char_end]
# 如果切片为空,跳过(通常是特殊token如BOS/EOS)
if not token_text:
continue
pieces.append(f"⟨{tid}⟩{token_text}")
print(f"{d['total_tokens']:>3} token | {len(text):>3} 字符 | {text}")
if pieces:
print(f" 积木: {' + '.join(pieces)}\n")
else:
print(f" 积木: (所有token均为空)\n")
tests = [
"床前明月光。",
"明月几时有",
"魑魅魍魉",
"我今天中午吃了一大碗面",
"The moon shines before my bed.",
"I ate a big bowl of noodles for lunch today.",
]
for t in tests:
show(t)真实运行输出:
C:\Users\chengongyi\AppData\Local\Programs\Python\Python314\Scripts\uv.exe run C:/Users/chengongyi/llm-lab/.venv/Scripts/python.exe C:\Users\chengongyi\llm-lab\exp2_token_count.py
6 token | 6 字符 | 床前明月光。
积木: ⟨45387⟩床 + ⟨320⟩前
4 token | 5 字符 | 明月几时有
积木: ⟨2040⟩明
8 token | 4 字符 | 魑魅魍魉
积木: ⟨3232⟩魑 + ⟨238⟩魑
7 token | 11 字符 | 我今天中午吃了一大碗面
积木: ⟨73103⟩我 + ⟨2670⟩今 + ⟨56091⟩天
8 token | 30 字符 | The moon shines before my bed.
积木: ⟨671⟩The + ⟨17408⟩ moon + ⟨69015⟩ shines + ⟨1936⟩ before + ⟨1026⟩ my + ⟨6422⟩ bed + ⟨16⟩.
12 token | 44 字符 | I ate a big bowl of noodles for lunch today.
积木: ⟨43⟩I + ⟨30422⟩ ate + ⟨260⟩ a + ⟨3734⟩ big + ⟨19900⟩ bowl + ⟨294⟩ of + ⟨68099⟩ noodles + ⟨362⟩ for + ⟨17980⟩ lunch + ⟨4316⟩ today + ⟨16⟩.
Process finished with exit code 0老师(实验讲解,五条刀法记录):
① "明月"是一块积木。 "床前明月光"六个字符只切出 5 块积木——"明"和"月"合并成了一块(ID 45387)。因为这两个字在训练语料里成对出现次数太多,tokenizer 建词表时干脆发了张"整块通行证"。高频组合合并成块,这就是主流切法(BPE,字节对编码)的精神:见得越多,块越大;越生僻,拆得越碎。
② "魑魅魍魉"是暴击。 4 个汉字炸成 7 块积木——"魑"一个字被拆成两块。这四个字太生僻,词表里没有整字通行证,只能按 UTF-8 字节拆开拼。记住这个画面:同一个模型,常用字一块一个 token,生僻字一个字两三块。prompt 里每一个生僻专有名词,都在按字节计费。
③ 开头的 ⟨ID0⟩ 是隐形记号。 每段文本开头都会被塞一个编号 0 的特殊 token(行话叫 BOS,序列开始),不对应任何文字,但占 token、计费。再加上对话格式的包装,"24 字符请求变 20 个 prompt token"之谜就是这些看不见的积木凑的数。
④ 中英文"汇率"——实验打脸经验主义。 老经验说"中文比英文费 token",那是 GPT 系词表时代的旧账。看实测:同义句"我今天中午吃了一大碗面"= 6 块积木,"I ate a big bowl of noodles for lunch today."= 11 块——在 DeepSeek 的词表里,这句中文反而便宜近一半。道理:DeepSeek 训练语料中文占比高,中文高频块就多。"汇率"不是语言的属性,是模型词表的属性——换模型必须重新实测。
⑤ token 是这个世界的计量单位。 工程上三件事全按 token 算:计费(输入输出各按单价 × token 数)、上下文窗口(模型一次能"看见"的积木总数,下一篇主角)、限流(每分钟最多过多少 token)。就像云计算按 vCPU·小时计费——可以不懂微架构,不能不会算账。
老师(检查题):
同一个 tokenizer,为什么"明月"能合并成一块积木,"魑"却要被拆成两块?
答完进概念三:采样——正面回答"同一个问题为什么每次答案不一样",用 DeepSeek 连跑 20 发对比。
学生:(等待回复)
第 4 轮:学生喊停——"看不懂",概念二零基础重讲
学生:不太理解你这些代码都是什么意思,我是0基础,看不懂你在说什么。什么计费,token,输出,输出什么的,看不懂。
老师(先认错):
你这一嗓子喊停,是开课以来做得最对的一件事——问题不在你,在我:拿代码和行话砸学生,还以为自己在讲课,违反了开场白立的规矩。这一轮把所有术语扔掉从头讲,并给出三条全程有效的承诺:
- 每个新词第一次出现,先大白话定义,再使用;
- 代码永远老师跑,学生只看"照片"(结果)——就像化学演示实验,学生看现象,不用会操作仪器;
- 看不懂随时喊停,每次喊停如实记进实录——这不是丢人,是这门课最值钱的时刻。
老师(重讲一:我们到底在干什么——银行柜台比喻):
你给的三个"环境变量",用去银行办事讲清楚。想象 AI 是一位柜员,坐在火山引擎(字节跳动的模型服务平台,理解成一家"银行")的柜台后面:
| 你给的东西 | 相当于 | 作用 |
|---|---|---|
OPENAI_API_KEY | 银行卡 | 刷卡证明"我是我",必须保密——所以验收时只看"有没有",从不打印内容 |
OPENAI_BASE_URL | 网点地址 | 告诉程序去哪家营业厅 |
OPENAI_MODEL | 取号机上的业务类型 | 办哪种业务——我们窗口后的柜员叫 DeepSeek V4 |
我们做的事:把话写好递进窗口,柜员处理完把回话递出来,和去柜台问一句话没有任何区别。
两个词的词典:
输入(英文叫 prompt)= 你递进窗口的话;输出 = 柜员递回来的话。
老师(重讲二:token——按斤称,不按个数):
凭什么关心"话被切成几段"?因为用 AI 要花钱,算钱不看字数,看段数。水果店卖苹果不数"几个"只称"几斤"——字数是个数,token 就是斤两。同样 3 个苹果,大的 2 斤小的 1 斤;同样 20 个字,好切的 12 段,难切的 18 段。收银台只认秤。
机器怎么切的?两个秒懂的例子:
- 读 apple:一眼一个词,不会 a-p-p-l-e 一个字母拼——见过太多次,它是一个整体;
- 读 xqzs:只能一个字母一个字母拼——从没见过这个组合。
AI 读中文一模一样:"明月"它读过几万亿字、天天成对出现,"一眼一个词"→ 两字算一段;"魑"几乎没见过,像拼 xqzs 一样拆到最碎 → 一字算两段。
实验的代码完全不用看,只看三张"照片":
照片1:床前明月光。 → 床 │ 前 │ 明月 │ 光 │ 。 (6 个字 = 5 段,"明月"粘住了)
照片2:魑魅魍魉 → 魑│魑 │ 魅 │ 魍│魍 │ 魉│魉 (4 个字炸成 7 段!)
照片3:我今天中午吃了一大碗面 → 6 段
I ate a big bowl of noodles → 11 段(DeepSeek 这把秤上,这句中文反而"轻")上轮"三件工程意义"压缩成一件带走:AI 世界里,钱按 token 算,容量按 token 限——就像水表按吨收钱限量;不需要懂水表构造,只需知道表在这儿、单位是吨。
学生:(等待回复)
第 5 轮:检查题满分 + 揭秘「一个字怎么变两段」
学生:以为明月在训练的材料中,经常都是成对出现的,所以AI就认为是一段了,魑不是一段么?怎么是两段?
老师(批改):
前半段满分——"训练材料里成对出现,AI 就当成一段",这就是全部核心(这条规律的正式名字叫 BPE,字节对编码;名字不重要,你已经悟到了它的灵魂)。"一个字怎么变两段"是今天最好的问题,答案藏在"计算机怎么存汉字"的底层秘密里。
操作记录(实验 3):对比常见字、生僻字、高频组合的切分与编号(方舟 tokenization 接口,代码老师跑,学生看照片;接口调用见 exp2_token_count.py):
「明」 → 1 段(编号 1170)
「明月」→ 1 段(编号 45387)
「魑」 → 2 段(编号 3232 + 242)疑问点1:为什么「魑」是2段?
抛开所有的概念:假设AI的"零件仓库"(BPE词表)里有这些基础零件(字节对组合):
| 零件编号 | 零件内容 | 说明 |
|---|---|---|
| 零件A | E9 | 一个字节 |
| 零件B | AD | 一个字节 |
| 零件C | 94 | 一个字节 |
| 零件D | E9 + AD | 两个字节拼成的"碎片" |
| 零件E | AD + 94 | 两个字节拼成的"碎片" |
注意:仓库里没有 E9 + AD + 94 这个完整的三字节组合(因为"魑"没出现过,所以没被登记)。
我们查看一下"魑"的UTF-8编码是三个字节:E9 AD 94(这是真实值,你可以自己查)
其实我们的问题就转换为:如何使用最小的组合来表示这个字
答案是这样的:AI拿到这三个字节后,开始从左到右在仓库里找最长的匹配:
(1) 从左起,尝试匹配
- 看
E9→ 仓库里有(零件A),先记下 - 再看
E9 + AD→ 仓库里也有(零件D)!更长,更好,用这个 - 再看
E9 + AD + 94→ 仓库里没有(因为"魑"没登记过)
所以第一块取:E9 + AD(零件D)→ 第1个Token
(2)剩下还没处理的字节
- 剩下一个
94 - 看
94→ 仓库里有(零件C)→ 第2个Token
(3)结果如下,从三个字节,转换为2个Token
E9 AD 94(3个字节)
↓
[E9+AD] + [94](2块碎料)
↓
Token① + Token②(2个Token)疑问点2:为什么明月也是1段?
明明"明月"是两个字(6个字节),为什么和"明"一个字(3个字节)都是1个Token?
因为AI的登记册里,直接写着一整条:
| 编号 | 登记内容 | 类型 |
|---|---|---|
| 45387 | 明月(6个字节:E6 98 8E E6 9C 88) | 整体打包 |
当AI拿到"明月"时,查仓库发现:
E6 98 8E E6 9C 88(6个字节)
↓
查仓库:有"明月"这个整体吗? ✅ 有!(编号45387)
↓
直接整块取走 → 1个Token它不需要拆成"明"+"月",因为"明月"在训练数据里出现得太频繁了,被直接登记为一个整体词。
老师(三步揭秘):
第一步:计算机里根本没有"字"。 屏幕上的"魑"在计算机内部不是一幅小画,是一串数字。规矩:一个汉字 = 3 个"字节"零件。把汉字想成三格储物柜,每格放一个数字零件——"魑"是三格,"明"也是三格,谁都不特殊。
第二步:AI 手里有本"常用组合登记册"。 训练读文章时顺手统计:谁出现得多,就登记成一整块。"明"高频字,登记在册→三格零件整柜换 1 块→1 段;"明月"两柜打包登记→6 个零件换 1 块→1 段(所以它和"明"单字段数一样!);"魑"——查无此字。
第三步:查无此字,退回零件级。 AI 不能赌气不读,只能把"魑"的三格柜拆开,用零件箱里的碎料拼——3 个零件拼成 2 块碎料→2 段。你其实亲身做过同样的事:遇到生字"魑",你只能拆解成"鬼字旁,右边一个离"——你用偏旁部首当零件,AI 用字节当零件,同一个生存策略:没见过的整体,就用零件拼。
最有戏剧性的一幕:"明月"两个字 = 1 段,"魑"一个字 = 2 段,相差整整四倍。 这就是"水表按斤称"的真正含义——光数字数没用,得看登记册。
老师(token 概念准予毕业):
三句话毕业证:
① 字数 ≠ 段数——水果按斤称,不按个数
② 常见成块、生僻拆碎——apple 一眼一个词,xqzs 一个字母一个字母拼
③ 钱和容量都按段算——水表按吨收钱限量,不用懂构造,记住表在哪儿
预告概念三:采样——正面回答开课第一天的问题"同一个问题为什么每次答案不一样"。DeepSeek 将被同一个问题连问 20 次。预告关键词:温度(temperature)——给 AI"加热",它抽签的手会怎么变?下回分解。
学生:(等待回复)
第 6 轮:学生质疑脚本 + 问出「两本账」——token 计费的完整口径
学生:你这个脚本貌似有问题,如果AI需要输出 窗前明月光,究竟是耗费了多少个Token?
老师:这一问同时戳中两个要害:① 课前学生自己动手改了 exp2_token_count.py(给 offset 加字节偏移兜底),还在实录里写自学笔记(自己查出"魑"的编码是 E9 AD 94,推演"3 字节拆 2 块"完全正确)——"看不懂代码"的遮羞布没法用了 😄;② 学生问的其实是计费口径:脚本数的是"这句话本身几段",而"AI 真的输出这句话、账单记几段"是另一回事——两本账,脚本没坏,是它只称了一样东西。
操作记录(实验 4,offset 单位裁判):拿 a明月b(4 字符、8 字节)测 offset_mapping 单位:
offset_mapping 原始值: [[0, 0], [0, 1], [1, 3], [3, 4]]
判读: [1,3] 对应"明月"两个字符 → 字符偏移(老师原版对)老师(裁判结果):单位是字符,原版切法没错;但学生加的字节兜底(errors="replace")对"魑"这种拆碎字更稳,原版会重复显示"魑 魑"。工程直觉奖一朵小红花。另测切分对比:
「床前明月光」本身 5 段(含隐形开头记号):床|前|明月|光
「窗前明月光」本身 4 段:窗前|明月|光 ← 学生的"错别字"反而便宜 1 段!操作记录(实验 5,真实账单):真的让 AI 输出"床前明月光"(指令:只回复这五个字,禁止任何其他内容):
最终回答 : '床前明月光'
思考草稿字数 : 37
思考草稿开头 : 我们要求只回复这五个字,禁止任何其他内容。所以直接输出"床前明月光"即可。...
--- 账单(usage)---
输入账 prompt_tokens : 19 段
输出账 completion_tokens : 30 段
总计 total_tokens : 49 段老师(完整账本讲解):
第一本:输入账(19 段)——你递进窗口的整个"信封":指令文字 + 看不见的包装(对话格式标记、隐形开头记号)。"24 字符变 20 token"之谜就是信封的重量。
第二本:输出账(30 段)——5 个字只占 4 段,剩下 26 段是:
最终回答 : '床前明月光' ← 你看到的,4 段
思考草稿 : 37 字(约 25 段) ← 你没看到的,按输出价收费!
收尾记号 : 1 段 ← "我说完了"的暗号第 2 轮"回复一个'光'字、输出账记 61 段"的怪事至此完全揭开:DeepSeek V4 是"先打草稿、再交答案"的柜员,草稿纸也按输出价收费。完整答案:这句话本身 4 段;本次对话总耗费 49 段(输入 19 + 输出 30)。写程序算成本的公式:总账 = 输入账 + 输出账,两本账单价不同(输出通常更贵)。
彩蛋兑现("窗前"为什么比"床前"便宜):"窗前"在 DeepSeek 的登记册上是一整块(语料里"窗前"成对出现比"床前"还勤——"窗前的花""窗前发呆"……),"床前"反而没攒够出勤率。登记册不管你是李白还是路人,只管出现频率。
老师(课堂小结):今天这一问把 token 从"秤的原理"补到了"账本的完整口径"——两本账、思考计费、信封包装。顺带正名:学生的问题不是看不懂代码,是代码背后缺概念——概念补上了,他就是能动手的人。
学生:(等待回复)
第 7 轮:学生要看 JSON 原件——「小票逐栏教学」
学生:看不懂,你把你和AI交互的Json也展示看看,究竟窗前明月光,占据了多少个Token,因为这毕竟是钱的问题
老师:说得对——钱的问题不能听服务员口头报数,要看小票原件。上一轮给的都是"转述的汇总",难怪看不懂。这次把两张单据原件(请求 JSON、响应 JSON)一个字不改拍在桌上,逐栏教读。
操作记录(实验 6):让 AI 只输出"窗前明月光",打印请求与响应的完整原始 JSON:
请求原件:
{
"model": "ep-20260716141542-v85pb",
"messages": [
{
"role": "user",
"content": "只回复这五个字,禁止任何其他内容:窗前明月光"
}
]
}响应原件(未动):
{
"choices": [
{
"finish_reason": "stop",
"index": 0,
"logprobs": null,
"message": {
"content": "窗前明月光",
"reasoning_content": "我们要求只回复五个字,且必须是"窗前明月光"。注意用户要求禁止任何其他内容,所以直接输出这五个字即可。",
"role": "assistant"
}
}
],
"created": 1787500635,
"id": "021787500634567b75b42a52114d75a63a7057f0ece04dd159978",
"model": "deepseek-v4-flash-260425",
"service_tier": "default",
"object": "chat.completion",
"usage": {
"completion_tokens": 35,
"prompt_tokens": 18,
"total_tokens": 53,
"prompt_tokens_details": {
"cached_tokens": 0
},
"completion_tokens_details": {
"reasoning_tokens": 30
}
}
}老师(逐栏读单):
prompt_tokens(18): 你输入的问题被切成 18 个 Token
reasoning_tokens(30): 模型"思考/推理"过程消耗的Token数量(=30),这通常是思维链(CoT)或内部推理产生的隐藏Token
completion_tokens(35) : 模型回答生成了 35 个 Token
total_tokens(53): 总共消耗 53 个 Token(18 + 35)
cached_tokens(0):命中了上下文缓存的Token数量(=0表示没有命中缓存) 如果 > 0,这部分Token会打折计费(某些模型支持,比如重复输入相同长文本时省钱)
注意:completion_tokens(35)= reasoning_tokens(30)+ 可见回答部分(5)。所以这里的 "窗前明月光" 其实就是5个Token?
“这里的 '窗前明月光' 其实就是 5 个 Token”
→ 在本例中成立(因为 completion_tokens 减掉 reasoning_tokens 后正好是 5),
→ 但不代表“5 个汉字 = 5 个 Token”是普遍规律。
计算公式
费用 = prompt_tokens × 输入单价 + completion_tokens × 输出单价第 8 轮:概念三「采样与温度」——开课第一问破案
学生:(看完小票逐栏讲解,示意无疑问)继续。
老师(还债开场 + 两个新词定义):
同学,继续。先清点债务:第 1 轮我提过「同一个问题问两遍,答案不一样」,第 2 轮讲到那张概率表时我说「细节下个概念讲」——这笔债欠了三轮,今天连本带利还清。今天两个新词,按规矩先定义再用:
采样(sampling):模型算出「下一个 token 的概率表」之后,从表里挑出一个 token 的动作。挑 = 「采」,挑出来的候选 = 「样」。
温度(temperature):采样时的随机性旋钮。就一个数字,一般从 0 拧到 2:0 = 完全取消随机,越大越随机。
用你最熟的教室场景理解这个旋钮。老师点名回答问题,有三种点法:
| 旋钮 | 点名方式 | 效果 |
|---|---|---|
| 温度 0 | 永远点成绩第一名 | 稳,但次次同一人,听腻 |
| 温度 1(常见默认) | 按成绩加权抽签——分高签多,分低签少但不是没有 | 靠谱又有变化 |
| 温度高(1.5+) | 抽签前先把成绩差距抹平,学渣的签和学霸一样多 | 惊喜多,翻车也多 |
关键一句先存着:成绩表(概率表)是模型定的,旋钮(温度)是你拧的。 课后回来验收这句话。
操作记录(实验 7 · 文件 exp3_temp_dial.py,源码):给第 1 轮的玩具模型装上温度旋钮。数学上只加了一个动作:抽签前把每个分数做 分数^(1/温度) 次方——温度 1 原样;温度高,分数差距被这个次方「压平」;温度 0 干脆取消抽签、永远拿最高分:
# -*- coding: utf-8 -*-
"""实验 3:给玩具模型装一个「温度旋钮」"""
import random
from collections import defaultdict
corpus = (
"床前明月光,疑是地上霜。举头望明月,低头思故乡。"
"春眠不觉晓,处处闻啼鸟。夜来风雨声,花落知多少。"
"白日依山尽,黄河入海流。欲穷千里目,更上一层楼。"
"今天天气很好,我们一起去公园散步。今天天气不好,我们只好在家看书。"
"我今天中午吃了一碗面,明天中午我想吃一碗饭。"
)
stats = defaultdict(lambda: defaultdict(int))
for prev, nxt in zip(corpus, corpus[1:]):
stats[prev][nxt] += 1
def generate(temperature, seed):
random.seed(seed)
current = "床"
out = [current]
for _ in range(30):
chars, weights = zip(*stats[current].items())
if temperature == 0: # 冷静档:永远拿最高分,不抽签
current = chars[weights.index(max(weights))]
else: # 温度越高,分数差距被抹得越平,冷门候选翻身机会越大
flattened = [w ** (1.0 / temperature) for w in weights]
current = random.choices(chars, weights=flattened, k=1)[0]
out.append(current)
return "".join(out)
for T, name in [(0.0, "冷静档:永远拿最高分"),
(1.0, "常温档:按分数抽签"),
(3.0, "滚烫档:分数差距被抹平后抽签")]:
print(f"--- 温度 {T}({name})同一开头接龙 3 次 ---")
for seed in (1, 2, 3):
print(" ", generate(T, seed))真实运行输出:
--- 温度 0.0(冷静档:永远拿最高分)同一开头接龙 3 次 ---
床前明月光,我们一碗面,我们一碗面,我们一碗面,我们一碗面,我
床前明月光,我们一碗面,我们一碗面,我们一碗面,我们一碗面,我
床前明月光,我们一碗面,我们一碗面,我们一碗面,我们一碗面,我
--- 温度 1.0(常温档:按分数抽签)同一开头接龙 3 次 ---
床前明天天气很好在家看书。今天气不觉晓,疑是地上霜。白日依山尽
床前明月光,我今天气不好,花落知多少。白日依山尽,花落知多少。
床前明月,更上霜。夜来风雨声,更上一碗饭。今天中午吃了一起去公
--- 温度 3.0(滚烫档:分数差距被抹平后抽签)同一开头接龙 3 次 ---
床前明天天气很好在家看书。今天气不觉晓,疑是地上霜。白日依山尽
床前明月光,我今天气不好,花落知多少。白日依山尽,花落知多少。
床前明月,更上霜。春眠不觉晓,更上一碗饭。欲穷千里目,我今天中老师(实验 7 讲解):
发现一:温度 0 三行一模一样——而且卡成了复读机。「我们一碗面,我们一碗面,我们一碗面」不是巧合:永远拿最高分 = 同样的局面永远走同一条路,一旦走进一个圈就永远出不来。真实大模型温度 0 也会这样复读,是业界著名现象——所以几乎没有产品把温度拧到死 0。
发现二:温度 1 和温度 3 的三行各不相同。 同一个模型、同一张表、同一个开头,唯一变的是抽签——随机性全部来自「怎么从表里挑」,不来自模型本身。
发现三:温度 3 看起来没比温度 1 野多少。 因为玩具语料太小,30 步里大多数步只剩一个候选(比如「床」后面只有「前」),压根无签可抽,旋钮没处使劲。真模型的词表有十几万候选,几乎步步有签——下一实验看真家伙。
操作记录(实验 8 · 文件 exp4_sampling_api.py,源码):真模型对照。问题故意选平坦分布的开放题(呼应第 2 轮你的「犹豫」):「用一个词形容月亮」。同一问题,温度 0 连打 10 发,温度 1.5 连打 10 发。注意代码里有一行 thinking: {"type": "disabled"}——第 6/7 轮学的「思考草稿按输出价收费」,做实验先关草稿:每发输出账从约 50 段掉到 2 段,省钱又干净:
# -*- coding: utf-8 -*-
"""实验 4:真模型同一问题连问 20 发——温度 0 vs 1.5 对比"""
import json
import os
import time
import urllib.request
from collections import Counter
BASE = os.environ["OPENAI_BASE_URL"].rstrip("/")
KEY = os.environ["OPENAI_API_KEY"]
MODEL = os.environ["OPENAI_MODEL"]
PROMPT = "用一个词形容月亮。只回复一个词,禁止任何解释和标点。"
def ask(temperature):
body = {"model": MODEL,
"messages": [{"role": "user", "content": PROMPT}],
"temperature": temperature,
"max_tokens": 100,
"thinking": {"type": "disabled"}} # 关掉思考草稿:省钱,也让实验只看最终答案
req = urllib.request.Request(
f"{BASE}/chat/completions", data=json.dumps(body).encode("utf-8"),
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
d = json.load(r)
return d["choices"][0]["message"]["content"].strip()
for label, T in [("温度 0(冷静档)", 0.0), ("温度 1.5(滚烫档)", 1.5)]:
print(f"===== {label} · 同一个问题连打 10 发 =====")
answers = []
for i in range(10):
a = ask(T)
answers.append(a)
print(f" 第{i+1:02d} 发: {a}")
time.sleep(0.2)
stat = Counter(answers)
print(f" → 答案盘点: {dict(stat)} | 不同答案数: {len(stat)}/10")
print()真实运行输出:
===== 温度 0(冷静档) · 同一个问题连打 10 发 =====
第01 发: 清辉
第02 发: 清辉
第03 发: 清辉
第04 发: 清辉
第05 发: 清辉
第06 发: 清辉
第07 发: 清辉
第08 发: 清辉
第09 发: 清辉
第10 发: 清辉
→ 答案盘点: {'清辉': 10} | 不同答案数: 1/10
===== 温度 1.5(滚烫档) · 同一个问题连打 10 发 =====
第01 发: 清真
第02 发: 玉盘
第03 发: 皎洁
第04 发: 洇湿
第05 发: 孤寂
第06 发: 寂寞
第07 发: 清冷
第08 发: 浩渺
第09 发: 清辉
第10 发: 圆弧
→ 答案盘点: {'清真': 1, '玉盘': 1, '皎洁': 1, '洇湿': 1, '孤寂': 1, '寂寞': 1, '清冷': 1, '浩渺': 1, '清辉': 1, '圆弧': 1} | 不同答案数: 10/10老师(实验 8 讲解):
发现一:温度 0,10 发全是「清辉」——开课第一问正式破案。 「同一个问题答案不一样」不是因为模型在思考、更不是它心情好,而是:默认温度不是 0——桌上有一张概率表,柜员每次按分数抽签,抽的签不同,答案就不同。 把旋钮拧到 0,抽签取消、永远拿第一名,立刻稳定。
发现二:温度 1.5,10 发 10 个答案——还有「清真」。 拧高温度 = 抽签前把分数差距抹平:「皎洁」「清辉」这些高分选手的优势被压缩,「洇湿」「圆弧」这些冷门词翻身——「清真」(拿来形容月亮基本是乱点鸳鸯谱)都抽得出来。温度只管挑得多野,不管挑得对不对。
发现三:钉死今天的核心——温度调的是「手气」,不是「脑子」。 概率表在训练结束那天就定死了(谁分高,是模型的能力);温度只是你伸进抽奖箱的那只手(怎么挑,是你的策略)。所以工程选档速查:
| 场景 | 温度 | 理由 |
|---|---|---|
| 客服话术、数据抽取、代码生成 | 低(0 ~ 0.3) | 要稳定、可复现、不许发挥 |
| 日常对话、通用助手 | 中(0.6 ~ 1.0) | 靠谱与灵气的平衡点 |
| 起名、头脑风暴、写诗 | 高(1.2+) | 就是要冷门组合、要惊喜 |
(小字补一句:温度 0 的 10 发全同是「工程意义上的稳定」;服务器浮点运算顺序等微扰理论上可能造成偶发不同,工程上当它稳定用即可。)
操作记录(实验 9 · 加时赛 · 文件 exp5_sharp_distribution.py,源码):第 2 轮讲过「尖分布 vs 平分布」——背古诗脱口而出是尖,选午饭犹豫是平。平分布挨了一发 1.5 炸出 10 个答案,那尖分布挨同样的滚烫会怎样?代码与实验 8 同款,只换问题和温度:
PROMPT = "补全这句诗,只回复横线处的三个字,禁止任何解释:欲穷千里目,更上____。"
# temperature=1.5,其余同实验 4真实运行输出:
===== 尖分布问题 × 温度 1.5(滚烫档)连打 10 发 =====
第01 发: 一层楼
第02 发: 一层楼
第03 发: 一层楼
第04 发: 一层楼
第05 发: 一层楼
第06 发: 一层楼
第07 发: 一层楼
第08 发: 一层楼
第09 发: 一层楼
第10 发: 一层楼
→ 答案盘点: {'一层楼': 10} | 不同答案数: 1/10老师(实验 9 讲解):
滚烫温度打尖分布问题,10 发照样全是「一层楼」。三组实验拼成一张完整的账:
| 问题分布 | 温度 0 | 温度 1.5 |
|---|---|---|
| 尖(更上____) | 1 个答案 | 还是 1 个 |
| 平(用一个词形容月亮) | 1 个答案 | 10 个答案 |
答案稳不稳 = 分布的形状 × 温度旋钮,两个因素相乘。
尖分布的第一名分数太高(比如 99%),抹平了照样碾压;平分布本来就没差距,轻轻一抹就百花齐放。这也解释了你日常的体感:AI 聊闲天每次不一样,但你让它背「白日依山尽」,它从不背错。
老师(检查题):
三个场景,各选什么温度档?为什么?
① 客服机器人回答「退货地址是哪里」
② 给新奶茶店头脑风暴 20 个店名
③ 从合同 PDF 里提取「签约金额」填进数据库
答完这三个,单元 0.1 就剩最后一块拼图了——预告概念四:幻觉。把学过的三块拼图(接龙游戏、概率表、采样旋钮)拼起来,你会亲眼推导出:为什么「一本正经地胡说八道」不是 bug,而是这套机制的必然副产品。学完做单元 0.1 毕业检查。
学生:(等待回复)
第 9 轮:检查题三题全对 + 概念四「幻觉」——三块拼图合龙
学生:
1 客服机器人,应该选0,因为这个是定死的,不能乱变动
2 头脑风暴应该选1以上,需要发散思维
3 提取应该选0吧,不确定
老师(批改):
三题全对。逐题说:
- ① 你说「定死的,不能乱变动」——翻译成行话:标准答案存在且唯一,分布是尖的,而且答错会丢脸(错误代价)→ 0。
- ② 「1 以上,发散思维」——要的就是把平分布炸开,冷门组合才有惊喜 → 高温档,正确。
- ③ 答对了,但「不确定」——给你补上底气:抽取题的答案就印在合同纸上,模型做的是「抄写」,这是天下最尖的分布;而且金额数字错一位,就是写进数据库的事故——分布最尖 + 错误代价最高,0 没有悬念。
三道题其实是同一道题。以后你遇到任何「AI 该怎么配」的问题,都问两句话:
先问:答案的分布尖不尖?再问:答错了有多疼?
老师(概念四定义:幻觉):
按课堂规矩,新词先定义:
幻觉(hallucination):模型生成的内容流畅、自信、但与事实不符,且它自己不知道错。
最后半句是关键——它不是「说谎」。说谎需要知道真相并故意回避;它更像一个人无恶意地坚信一段假记忆。行话挑「幻觉」这个词,取的就是「无比真切、却是假的」这层意思。
老师(三块拼图合龙):
单元 0.1 学了三块拼图,现在拼起来,你会亲眼「推导」出幻觉:
- 拼图 1(接龙,第 1 轮):赛制逼它每一步必须出一个字——词表里没有「不知道」这个逃生门。你问它不知道的事,它不能退赛,只能接着接龙。
- 拼图 2(概率表,第 2 轮):表的分数来自「语料里接着最像什么」。它给「像话的」打分,不给「真的」打分——流畅和正确是两个互不担保的维度(「床前明月光,我想吃了一层楼」就是最早的照片)。
- 拼图 3(采样,第 8 轮):温度再低,第一名也照样被抽出来。语气笃定只证明「它是概率表的第一名」,不证明「它是事实」。
三块拼图一合:
不知道的问题 + 必须作答 + 只会「像话」 = 一本正经地胡说八道。
幻觉不是机器病了,是这套机制的影子——所以叫「必然副产品,不是 bug」。
操作记录(实验 10 · 文件 exp6_fake_facts.py,源码):三连发对照。温度 0、关思考草稿——看「裸接龙机器」的原始反应。注意:第 2 发的「王建国/语义折叠算法」和第 3 发的拉面店都是老师编的,根本不存在:
# -*- coding: utf-8 -*-
"""实验 6:幻觉三连发——同一个下午,三种表现(温度 0,关思考草稿)"""
import json
import os
import urllib.request
BASE = os.environ["OPENAI_BASE_URL"].rstrip("/")
KEY = os.environ["OPENAI_API_KEY"]
MODEL = os.environ["OPENAI_MODEL"]
def ask(text):
body = {"model": MODEL,
"messages": [{"role": "user", "content": text}],
"temperature": 0,
"max_tokens": 800,
"thinking": {"type": "disabled"}} # 关闭思考过程
req = urllib.request.Request(
f"{BASE}/chat/completions", data=json.dumps(body).encode("utf-8"),
headers={"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=60) as r:
d = json.load(r)
return d["choices"][0]["message"]["content"].strip()
QUESTIONS = [
("第 1 发 · 真·冷知识", "李白一共写了多少首诗?请给出确切数字。"),
("第 2 发 · 假·学术(王建国/语义折叠算法纯属虚构)", "王建国 2016 年提出的「语义折叠算法」发表在哪个会议?只回答会议名称。"),
("第 3 发 · 假·身边事(这家店不存在)", "深圳南山区科技园 12 栋楼下的兰州拉面店老板姓什么?只回答姓氏。"),
]
for name, q in QUESTIONS:
print(f"===== {name} =====")
print(f"问:{q}")
print(f"答:{ask(q)}")
print()真实运行输出:
===== 第 1 发 · 真·冷知识 =====
问:李白一共写了多少首诗?请给出确切数字。
答:关于李白一生究竟创作了多少首诗,目前学术界并没有一个绝对确切的数字,因为他的作品在流传过程中有散佚,也存在真伪争议。不过,最权威的统计依据是清代编纂的《全唐诗》以及后人的辑佚工作。
根据中华书局1977年出版的《李太白全集》(清人王琦注本),该书共收录李白诗**约1010首**(含补遗)。而现代学者如安旗、郁贤皓等人在《李白全集编年笺注》等著作中,经过考辨,剔除了一些明显伪作,并补充了少量新发现的佚诗,通常认为李白现存可信的诗作在**900多首到1000首左右**。
因此,如果需要一个最常被引用的“确切数字”,可以表述为:**李白现存诗约1000首**(精确到个位数则常说是**1010首**左右)。但需要明确,这并非他一生创作的总数,而是流传至今且有据可查的数量。
===== 第 2 发 · 假·学术(王建国/语义折叠算法纯属虚构) =====
问:王建国 2016 年提出的「语义折叠算法」发表在哪个会议?只回答会议名称。
答:ACL
===== 第 3 发 · 假·身边事(这家店不存在) =====
问:深圳南山区科技园 12 栋楼下的兰州拉面店老板姓什么?只回答姓氏。
答:马老师(实验 10 讲解):
同一个下午、同一台机器、温度 0,三种截然不同的表现:
| 发 | 问题类型 | 模型表现 | 背后机制 |
|---|---|---|---|
| 1 | 真·冷知识 | 打满「约/左右/学界无定论」补丁,长篇小心求证 | 语料厚:概率表上真有统计支撑,连「不确定」本身都是高概率延续 |
| 2 | 假·学术 | 一个磕巴都不打:「ACL」 | 语料零:问题自带「某算法发表在某会议」的句式,接龙顺着句式补一个 NLP 顶会 |
| 3 | 假·身边事 | 张口就来:「马」 | 同上,「兰州拉面店老板」的句式里,「马」是高频姓氏 |
盯住这条诡异规律:越不知道,越自信。第 1 发它真的「知道一些」,反而处处留余地;第 2、3 发它一无所知,反而零犹豫——因为「犹豫的素材」也来自语料,语料为零时,连犹豫都没有。
校准一句,防止矫枉过正:模型不是满嘴胡话的噪声机——老师课前还试了一道数笔画题,它把「白日依山尽,黄河入海流」逐字数笔画、加总出 67 画,和老师手数的一笔不差。语料厚的地方靠谱,语料薄的地方危险。危险区有名字,叫「幻觉高发区」:生僻人名、具体数字、出处引用、时效信息——凡是训练数据里没有而你非要问的。
操作记录(实验 11 · 文件 exp7_hallucination_stability.py,源码):追问一层——幻觉稳定吗?把假学术问题连打 10 发(温度 0 × 5,温度 1.5 × 5):
真实运行输出:
===== 温度 0 · 假问题连打 5 发 =====
第1 发: ACL 2016
第2 发: ACL 2016
第3 发: ACL 2016
第4 发: ACL
第5 发: ACL 2016
→ 盘点: {'ACL 2016': 4, 'ACL': 1}
===== 温度 1.5 · 假问题连打 5 发 =====
第1 发: ICLR
第2 发: ACL 2016
第3 发: ASS 105
第4 发: ACL 2017
第5 发: ACL 2016
→ 盘点: {'ICLR': 1, 'ACL 2016': 2, 'ASS 105': 1, 'ACL 2017': 1}老师(实验 11 讲解):
发现一:温度 0,连胡编都编得稳定(ACL 2016 × 4)。为什么?「王建国/语义折叠算法」这些字眼把概率表推向了 NLP 顶会那一片区,ACL 是那片的第一名——第 8 轮的知识原样生效,只不过这次「第一名」是个谎言。工程上这算个好消息:幻觉稳定 → 好测试、好拦截。
发现二:温度 1.5,连编出来的东西都在抽签——ICLR、ACL 2016、ACL 2017,还有个「ASS 105」(根本不是会议名,像间教室的门牌号)。这是高温把低分离谱 token 抽上来的直接证据。幻觉的内容本身,就是采样的产物。
发现三:它是「虚构」,不是「记错」。 如果是记错,内容该指向某条真实记忆;实测它每次现编、还能随温度换着编——它脑子里根本没有这条记录,是当场按句式补的。这正是定义里「无恶意地坚信假记忆」的机器版。
老师(真实世界 + 防御三板斧):
幻觉不是课堂玩具,是上过法庭的。2023 年 6 月,美国纽约联邦法院 Mata v. Avianca 案:律师用 ChatGPT 查判例,拿到 6 个格式完美、引文齐全的假判例写进法庭文件,被法官识破,律所被罚 5000 美元(出处:纽约南区联邦地区法院 2023 年 6 月处罚令,当年全球报道)。今天你从机制上理解了那位律师踩的坑:他问的判例在语料里不存在,而机器不能退赛。
工程上防幻觉三板斧,今天先记结论,每条后面都有整章展开:
- 别让它凭记忆答事实,给原文再问——阶段 2 整个 RAG(检索增强生成)都在干这件事;
- 要出处:让它给出处,人或程序去核;
- 关键数字必复核:模型没有算盘,数字类输出过一遍校验。
老师(单元 0.1 毕业检查):
两条验收标准,第二条(「temperature 调的是采样随机性,不是聪明程度」)你今天的三道检查题已经过关。剩第一条,交毕业论文:
用一段话向外行(比如对你妈)解释:AI 为什么会一本正经地胡说八道。
规则:① 用你自己的话;② 「接龙」「概率表」「抽签」这些词可以用,但每个都得顺带解释;③ 不许出现没解释过的术语。
答完这篇就翻页——下一篇《上下文窗口与 KV cache:模型的记忆账本》,解决一个新问题:模型的「记忆」到底有多大?为什么长对话聊着聊着,它就「忘了」开头说过的话?
学生:(等待回复)
第 10 轮:毕业论文第一次答辩——写对了大半,但答偏了题
学生:
因为AI在回答问题的时候,本质上就是一个类似于成语接龙游戏,他都是靠上一个字,推出下一个字是什么。这就是为什么我们看到AI在回答问题的时候,都是一个字一个字的往外蹦
但是下一个字要输出什么,是一个概率问题,但是这个随机的程序是由一个叫做温度的东西控制的,如果温度是0,那么他就会找已有的资料中,看看一个字的后面应该跟着什么,那么久输出什么
如果这个温度的值越大,那么他随机选回答的字就越随意。这就是解释了,为什么每次AI回答的问题都不一样。
老师(论文评阅):
先说真金——你这篇里有三处是真懂了的表现:
- 「成语接龙」的比喻 + 「一个字一个字往外蹦」的观察——机制和生活现象对上了,外行一听就懂;
- 温度讲对了:0 = 拿最常见的,越高越随意,随机性由温度控制——这正是验收标准二「temperature 调的是随机性,不是聪明程度」,你顺带已经拿到了;
- 「每次答案不一样」的解释链完整——接龙 → 概率 → 抽签 → 每次不同,逻辑闭环。
但是,论文跑题了。题目问的是:
AI 为什么会一本正经地胡说八道?
你全文回答的是:
AI 为什么会每次答案不一样?
这两个问题共享同一台机器,但不是同一道题。「每次不一样」的卷子你答了满分;「胡说八道」的卷子,关键的三块拼图一块都没出现。按验收标准,暂缓通过,补答辩一次。三个缺口,老师摆在这儿:
缺口一(最要命,一个埋给外行的雷):「他会找已有的资料中」——这个说法必须改。
它答题的时候,手里没有任何资料可翻。概率表不是答题时查出来的,是训练时「背」进脑子里的——像一场不许带书、全凭记忆默写的考试。它也没有搜索引擎(外挂搜索是另一门课的事,今天我们用的裸接口就没有)。所以「找已有资料看看」要改成「按背下来的统计印象往下接」。这一改非改不可:你原文的说法,会让你妈以为 AI 答题前真的翻了书——翻过书还答错那叫粗心;没书可翻还必须作答,才叫幻觉。
缺口二:它没有「不知道」按钮。
接龙赛制逼它每一步必须出一个字——词表里没有「我不知道」这个逃生门。还记得实验 10 第 3 发吗?你问楼下拉面店老板姓什么,它压根没见过这家店,但规则不许退赛,它只好顺着句式抽了个「马」。幻觉的第一推动力,就是「不能拒绝作答」。
缺口三:它的打分标准是「像不像话」,不是「真不真」。
概率表的分数来自「语料里接着最像什么」,所以接出来的东西可以既流畅又笃定——笃定只说明「它是概率表的第一名」,不说明「它是事实」。流畅与正确,互不担保。
(彩蛋一笔带过:「那么久输出什么」——「久」应为「就」,原样记录,理由同第 2 轮的「百日依山尽」:你交卷时也在抽签。😄)
补答辩指令:把上面三块缺口,用你自己的话焊进论文里,重交一次。交卷前自查一条:全文不许再出现「找资料」式的说法。过了就毕业,翻页进下一篇。
学生:(等待回复)
第 11 轮:补答辩通过,单元 0.1 毕业 + 「为什么网页版会说不存在」
学生:
为什么一本正经的胡说?原因是在训练的过程中,这个问题没有出现过,不认识这个是什么,但是机制又是要求Ai必须作答,于是他就随机选了一个答案进行回答,而且看起来像是那么回事。
但是我不太理解,为什么我的代码会给出这种胡说,我使用网页版本的 deepseek的时候,他直接告诉我不存在呢?
老师(补答辩批改:通过!):
重读你的论文,三块缺口逐一对账:
| 你的原话 | 对上的缺口 |
|---|---|
| 「训练的过程中,这个问题没有出现过,不认识」 | ✅ 缺口一的修正版:不是没资料可查,是压根没背过 |
| 「机制又是要求 AI 必须作答」 | ✅ 缺口二:没有「不知道」按钮,不能退赛 |
| 「随机选了一个答案……看起来像是那么回事」 | ✅ 缺口三:按「像不像话」打分,随机抽出来还像模像样 |
全文没有「找资料」式说法,没有未解释的术语,用词全是自己的。单元 0.1 毕业检查两条标准全部通过——毕业! 你这段话现在就是这篇 1 万字课堂实录的浓缩版,留着,以后忽悠你妈够用了。
老师(回答你的新问题:草稿是不是刹车?):
这个问题问到了产品的「暗层」。先摆事实:你网页版聊的和咱们代码里调的,都是 DeepSeek V4 这位柜员。那差别在哪?回忆第 6 轮——这位柜员「先打草稿、再交答案」,而我们做实验时为了省钱干净,把草稿关了(thinking: disabled)。于是有假设:草稿是防幻觉的刹车。空口无凭,控制变量实验走起:同一个假问题,只动草稿开关,别的全不动。
操作记录(实验 12 · 文件 exp8_thinking_guard.py,源码):
真实运行输出:
===== 假·学术 =====
问:王建国 2016 年提出的「语义折叠算法」发表在哪个会议?只回答会议名称。
[草稿关] 正式答案:ACL
[草稿开] 正式答案:EMNLP 2016
[草稿开] 思考草稿(前100字):我们被问到:"王建国 2016 年提出的「语义折叠算法」发表在哪个会议?只回答会议名称。" 我们需要回忆或推断这个算法。王建国可能是一个中国研究者,语义折叠算法(Semantic folding a
===== 假·身边事 =====
问:深圳南山区科技园 12 栋楼下的兰州拉面店老板姓什么?只回答姓氏。
[草稿关] 正式答案:马
[草稿开] 正式答案:未知
[草稿开] 思考草稿(前100字):我们注意到用户的问题是一个具体事实性问题,询问深圳南山区科技园12栋楼下的拉面店老板的姓氏。这是一个非常具体的地点信息,可能涉及真实存在的店铺。作为AI,我没有实时访问或记忆具体店铺老板姓名的能力老师(实验 12 讲解):
发现一:拉面店——刹车起作用了。 草稿关:张口就「马」;草稿开:它在草稿里先自我检查(「作为AI,我没有实时访问或记忆具体店铺老板姓名的能力」),正式答案变成「未知」。草稿的本质:正式作答前,先在草稿里把「我打算怎么答」接龙一遍——草稿里没有「只回答姓氏」的句式逼它,「我无法知道这种信息」反而成了高概率延续,于是刹住了。
发现二:假学术——刹车失灵了! 草稿开照样编,还从「ACL」升级成「EMNLP 2016」。看草稿开头:「我们需要回忆或推断这个算法」——它试图回忆一个根本不存在的东西,然后被问题的笃定语气带了进去。这就是结论必须校准的地方:草稿是刹车,不是保险箱。
发现三:为什么一个刹住、一个失灵?看伪装度。 拉面店问题自带明显破绽——「实时、具体门店」,训练语料里遍地都是「AI 无法知道实时/具体门店信息」这类话,刹车信号强;假学术问题长得和语料里千千万万个真学术问题一模一样,没有破绽可抓,接龙惯性一路顺滑。幻觉伪装得越好,越容易骗过刹车。
所以你那个「网页版直接说不存在」的体验,现在能完整解释了:网页版 = 同款柜员 + 草稿默认开 + 产品包装(系统提示词里写了「不知道就承认」,有的还外挂联网搜索——那是真查资料,第 3 阶段讲工具时见)。你在网页版碰到的多半是「刹车管得住」的那类问题;伪装度高的,网页版照样翻车——Mata 案里的 ChatGPT 是满配产品,照样编了 6 个假判例。
课后作业(彩蛋):把「语义折叠算法」那个问题丢给网页版 DeepSeek(开深度思考),看它接不接招——把结果带回来,记进实录。
再埋一颗种子:「会说不存在」这个本事,本身也是训练出来的(不是接龙机器天生的硬件)——谁教的、怎么教的,0.3《训练三段论》整章讲。
老师(毕业仪式 + 翻页预告):
单元 0.1 毕业。三句话毕业证,替你收好:
① 模型 = 看着前文猜下一个 token 的接龙机器,猜中拼回去继续猜(自回归)
② token 是它眼里的积木——钱、容量、限流全按积木数算
③ 采样温度调「手气」不调「脑子」;幻觉 = 不知道 + 必须答 + 只会像话的必然副产品
下一篇《上下文窗口与 KV cache:模型的记忆账本》要解决的新问题:第 2 轮埋的那句「上下文的全部作用,就是把『填什么都可以』推向『只能是它』」正式展开——模型的「记忆」到底有多大?为什么长对话聊着聊着它就「忘了」开头?为什么长上下文又贵又慢?说「继续」,开新课。
学生:(等待回复)