# 5.1 看模型每一步在想什么：候选 token、概率与采样

这份代码回答三个问题。模型每一步在哪些 token 之间选、各有多大把握；温度和 top-p 改的是什么；同一个问题问几次、答案不一样时，从概率上能不能事先看出它哪里没把握。

它对你给的提示做三件事：

1. 温度 0 生成一遍（每步都选概率最高的 token），逐步列出选中的 token、它的概率、排在后面的几个候选，以及要凑满 90% 的概率需要多少个候选。
2. 在几个温度下各采样 5 次，把回复里概率低于 0.3 的 token 用【token|概率】标出来。
3. 温度 1 再加上 top-p 0.9 采样 5 次，和不加 top-p 的那一组对比。

这里的概率一律指模型原始分布（温度 1）下的概率，所以同一个 token 在不同温度的回复里标出的数字相同，便于比较。

## 需要什么设备

本地小模型，下面两档都能跑：

- Apple Silicon Mac：用 MPS。默认模型 Qwen3.5-2B 约 4.2 GB，建议 16 GB 内存；8 GB 内存的 Mac 换成 `--model Qwen/Qwen3.5-0.8B`。
- 一张 NVIDIA 显卡：用 CUDA，8 GB 显存就够。

没有这两样也能用 CPU 跑，只是慢。AI Builder Space 上的模型目前都不返回 logprobs（2026 年 10 月核实），所以这份代码没有 API 版。

## 安装与运行

```bash
cd 5_1_logprobs
uv venv && source .venv/bin/activate      # 没有 uv 就用 python3 -m venv .venv
uv pip install -r requirements.txt        # torch 和 transformers，约 1 GB
python logprobs.py                        # 第一次会下载模型；之后在 M3 Ultra 上约 20 秒跑完
python logprobs.py --prompt "你自己的问题" --prompt "另一个问题"
python logprobs.py --model Qwen/Qwen3.5-0.8B --temps 0.5 1.0 --samples 10
```

运行时可能看到几行 `falling back to its reference PyTorch implementation` 的提示，意思是 Qwen3.5 的线性注意力层没有用加速版实现，结果是对的，只是慢一些，可以忽略。在 NVIDIA 显卡上可以再 `uv pip install flash-linear-attention` 换成加速实现（我们在 RTX 5090 上就是这样跑的）；Mac 上目前没有对应的加速实现。

## 该看到什么

默认两个提示，一个有确定答案（法国的首都），一个要模型编具体信息（推荐一家上海的独立咖啡馆，给店名和地址）。我们的样例输出在 `sample_output/`（2026-10-23，Qwen3.5-2B；`mps_m3ultra_*` 是 Mac Studio，`cuda_rtx5090_*` 是一张 RTX 5090）。下面的数字来自 Mac：

- **有确定答案的问题**：温度 0 时，除了第一个 token（“法” 0.69、“法国” 0.20，只是措辞的差别），后面每步的概率都在 0.97 以上，“巴黎”是 0.99。温度 0.3 和 0.7 下各采样 5 次，5 次的回复完全相同。
- **要编具体信息的问题**：温度 0 的回复是“推荐上海独立咖啡馆‘阿拉比卡’，地址位于上海市徐汇区衡山路 100 号”。店名第一个字“阿”的概率只有 0.02，要凑满 90% 的概率得算上 5,371 个候选；门牌号的数字也都在 0.3 上下。连温度 0.3 采样 5 次，都给出 5 个不同的店名和地址。模型说得一样流畅，概率却显示它在店名和门牌号上完全没有把握，这就是看 logprobs 能发现“自信地编”的地方。
- **温度**：温度 1.5 时，有时开头几步还正常，一旦抽中一个极低概率的 token，后面就变成多种语言夹杂的乱码，回复里出现几十个概率接近 0 的 token。温度 1.0 加上 top-p 0.9 之后，同一个法国首都的问题 5 次都回到“法国的首都是巴黎”，因为 top-p 先把概率加起来只占最后 10% 的那一长串候选砍掉再抽样。
- **一个细节**：transformers 先做温度、再做 top-p，所以温度 1.5 加 top-p 0.9 仍然会出乱码。温度把分布摊平以后，“凑满 90%”需要的候选数变得极多，top-p 就几乎不起作用了。不同推理框架、不同 API 的处理顺序可能不同。

**同一个模型、温度 0，换一台机器结果也可能不同。** 我们在一张 RTX 5090（CUDA）上跑了同一份代码（`sample_output/cuda_rtx5090_*`）。法国首都那题和 Mac 上几乎一样（“法” 0.70、“法国” 0.20）。咖啡馆那题第 3 步，Mac 上“独立” 0.24、左引号 0.21，显卡上左引号 0.24、“独立” 0.22，前两名只差零点零几，bf16 在两种硬件上的舍入差异就足以让排名对调。于是显卡上的温度 0 回复变成了“推荐上海‘老上海’咖啡馆，地址位于黄浦区南京东路步行街附近”，店名第一个字“老”的概率只有 0.01。温度 0 只保证每步选当时概率最高的那个，前几名挨得很近时，结果对硬件、精度、批大小都很敏感。

采样有随机性，你的回复和我们的不会逐字相同；`--seed` 可以换随机种子。我们换过两个种子重跑，上面的现象都在：咖啡馆问题在每个温度下都是 5 次 5 个不同的答案，法国首都在温度 0.7 以下每次都是同一个答案，温度 1.5 有时出乱码有时不出。

## 换成你自己的数据

用 `--prompt` 换成你工作里真会问的问题，最好一个有标准答案、一个要模型给具体名字、数字或引用的。看两件事：温度 0 的逐步表里，哪些位置的概率掉到 0.3 以下、凑满 90% 要几百上千个候选；采样几次，答案是不是在这些位置上变来变去。这些位置往往就是需要你核实、或者需要你在上下文里直接给出事实的地方。

结果的 JSON 存在 `results/` 下，包括逐步的候选表和每次采样的原文。

## 温度那张图的数据

`temperature_step1.py` 取一个提示第 1 步的整张词表 logits（加载方式同 `logprobs.py`），分别除以 0.5、1.0、1.5 再做 softmax，记下前五名候选和“其余全部”的概率，存到 `sample_output/temperature_step1_<设备>.json`。教材 5.1 的温度图读的就是 `sample_output/temperature_step1_mps.json`（2026 年 10 月，Mac Studio M3 Ultra）。温度 1.0 那一列和 `logprobs.py` 第 1 步的概率一致。

