大模型为什么每次回答都不一样?一文搞懂 Temperature、Top K 与 Top P

同一句提示词连续问三遍,大模型为什么会给出三种不同的回答?

我最开始把这种现象简单理解成"模型会随机说话"。但继续往下学才发现,随机并不等于毫无规律。模型每生成一个 Token,都会先得到一组候选分数,再按照采样规则选出下一个 Token。TemperatureTop KTop P,控制的正是这个过程。

这篇文章不把参数当成几个孤立的旋钮,而是从一次 Token 选择出发,看看它们分别改变了什么,最后再用一个 LangChain.js 小实验观察低温与高温输出的差别。

一、模型不是在"想一句完整的话"

假设上下文是"你好",模型正在预测下一个 Token。为了方便理解,我们先虚构一组概率:

候选 Token
概率 60% 18% 12% 7% 3%

"吗"的概率最高,但这不代表它每次都必然被选中。只要生成过程采用采样,其他候选仍然有机会出现,只是机会不同。

这里说的是 Token,不一定是一个完整汉字或单词。它也可能是词的一部分、标点或者空格。模型每选出一个 Token,就会把它加入上下文,再继续预测下一个,直到生成结束。

理解这一点后,三个参数的分工就清楚多了:

  • Temperature 改变候选之间的概率差距;
  • Top K 按候选数量缩小范围;
  • Top P 按累计概率缩小范围。

二、Temperature:改变概率分布的"形状"

模型先产生一组原始分数,也就是 logits。Temperature 会参与 softmax 计算:

text 复制代码
p_i(T) = exp(z_i / T) / Σ_j exp(z_j / T)

不必被公式吓到。可以把它拆成三步:

  1. z_i 是第 i 个候选 Token 的原始分数;
  2. 每个分数先除以温度 T
  3. softmax 再把这些分数转换成总和为 1 的概率。

T 是较小的正数时,原本的分数差距会被放大,概率分布变得更"尖":高分候选更占优势,输出通常更集中。

T 较大时,分数差距被缩小,概率分布变得更"平":更多候选有机会被抽中,输出通常更发散。

因此,Temperature 不是"把某个低概率词直接选出来",而是先重塑整组概率,再交给采样过程选择。

还要注意两个边界:

  • Temperature 的合法范围由模型供应商决定,不能笼统地写成永远是 0~1
  • temperature: 0 如何处理也取决于具体 API,有的服务会走近似确定性的路径,有的服务不接受 0。

所以,调参前先看自己使用的模型文档,比记住一个固定范围更可靠。

三、Top K:只留下前 K 名候选

Top K 的规则很直接:先按概率从高到低排序,只保留前 K 个候选,其余候选本轮不参与采样。

还是用前面的概率举例。若设置 topK = 3,候选集只剩下:

text 复制代码
吗 60%    啊 18%    呀 12%

"呢"和"哦"会被排除。随后,系统会对留下的候选重新归一化,再进行采样。

Top K 的优点是候选数量稳定,容易理解;局限也很明显:无论当前概率是特别集中还是特别分散,它都机械地保留 K 个。

更重要的是,并非每个模型 API 都提供 Top K。参数是否存在、叫 topK 还是 top_k、应该写在构造参数还是透传字段里,都要以供应商和 SDK 文档为准。

四、Top P:让候选数量跟着概率变化

Top P 也叫 nucleus sampling(核采样)。它不固定候选数量,而是从最高概率开始累加,直到累计概率达到阈值 P

假设设置 topP = 0.85

text 复制代码
吗 60%              累计 60%
吗 + 啊             累计 78%
吗 + 啊 + 呀        累计 90%  -> 达到 85%,停止

这一轮会留下 3 个候选。如果下一轮的概率更集中,也许只需 1~2 个候选就能达到 85%;如果概率更分散,则可能保留更多候选。

这也是 Top P 在许多通用大模型 API 中更常见的原因:候选集能随当前上下文动态变化。

五、三个参数到底是什么关系?

参数 改变什么 直觉效果 注意事项
Temperature 整组概率的相对差距 低温更集中,高温更发散 范围和 0 的处理由供应商决定
Top K 候选集的最大数量 只在前 K 名中选择 很多 OpenAI 兼容接口并不支持
Top P 候选集的累计概率范围 按当前分布动态保留候选 P 越小,候选范围通常越窄

它们不是互相替代的同一个参数,但也没有"Temperature 和 Top K 绝对不能同时很大或同时很小"这样的硬规则。

实际系统常见的处理顺序是:先用 Temperature 改变概率分布,再按 Top K 或 Top P 过滤候选,最后采样。但具体服务可能只暴露其中一部分参数,也可能规定不要同时修改多个采样参数。

最稳妥的做法是:选择一个保守基线,每次只改变一个参数,用固定评测集观察结果。

六、不同任务可以从什么参数开始?

下面不是"标准答案",只是便于启动实验的参考值。不同模型即使接收同名参数,表现也可能不同。

任务 Temperature 起点 Top P 起点 观察重点
代码生成、结构化抽取 0.1~0.3 0.80~0.90 格式稳定、约束遵守、可执行性
基于资料的事实问答 0.2~0.4 0.85~0.95 引用一致、答案完整、拒答边界
普通文案与总结 0.5~0.7 0.90~0.98 可读性、重复度、信息保真
创意写作与角色表达 0.8~1.0 0.95~1.00 新颖度、连贯性、跑题率

如果供应商建议只调 Temperature 或 Top P,就先遵循它的建议。一次同时改两个变量,会让你很难判断究竟是哪一个参数带来了变化。

七、用 LangChain.js 做一次对比实验

原学习示例使用了两套 ChatOpenAI 配置:低温负责"严谨",高温负责"创意"。这个实验思路很好,但本地安装的 @langchain/openai@1.5.5 有一个兼容性细节需要修正。

在这个版本中,ChatOpenAI 的顶层构造参数会读取 temperaturetopP,却不会读取顶层的 topK。也就是说,下面这种写法虽然 JavaScript 不会报错,topK 实际上不会进入请求:

javascript 复制代码
new ChatOpenAI({
  temperature: 0.8,
  topK: 4,
});

如果模型供应商支持 Top K,需要按照它的 SDK 或透传参数文档配置,不能默认所有 OpenAI 兼容接口都支持。为了让本文示例与当前本地依赖匹配,下面改用已支持的 temperature + topP

js 复制代码
import "dotenv/config";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts";
import { ChatOpenAI } from "@langchain/openai";

const modelName = process.env.DEEPSEEK_MODEL;

if (!process.env.DEEPSEEK_API_KEY || !process.env.DEEPSEEK_API_URL || !modelName) {
  throw new Error(
    "请先配置 DEEPSEEK_API_KEY、DEEPSEEK_API_URL 和 DEEPSEEK_MODEL",
  );
}

function createModel({ temperature, topP }) {
  return new ChatOpenAI({
    model: modelName,
    temperature,
    topP,
    maxTokens: 600,
    apiKey: process.env.DEEPSEEK_API_KEY,
    configuration: {
      baseURL: process.env.DEEPSEEK_API_URL,
    },
  });
}

const storyPrompt = PromptTemplate.fromTemplate(`
请写一篇短篇散文,主题:{theme}
风格温柔治愈,篇幅 200 字左右,不要分段,文字细腻、有画面感。
`);

const outputParser = new StringOutputParser();
const preciseChain = storyPrompt
  .pipe(createModel({ temperature: 0.2, topP: 0.8 }))
  .pipe(outputParser);
const creativeChain = storyPrompt
  .pipe(createModel({ temperature: 0.9, topP: 0.95 }))
  .pipe(outputParser);

async function runChain(label, chain, theme, rounds = 3) {
  console.log(`\n=== ${label} ===`);
  for (let round = 1; round <= rounds; round += 1) {
    const text = await chain.invoke({ theme });
    console.log(`\n第 ${round} 次:\n${text}`);
  }
}

async function main() {
  const theme = "秋日山野晚风";
  await runChain("低温模式", preciseChain, theme);
  await runChain("高温模式", creativeChain, theme);
}

main().catch((error) => {
  console.error("调用失败:", error);
  process.exitCode = 1;
});

运行前需要配置三项环境变量:DEEPSEEK_API_KEY 使用自己的密钥,DEEPSEEK_API_URL 使用服务商提供的兼容接口地址,DEEPSEEK_MODEL 使用服务商实际支持的模型名。不要把未经确认的模型名称写死在代码里,也不要把真实密钥提交到仓库。

安装依赖后运行:

bash 复制代码
pnpm install
node main.mjs

示例让每条链连续生成 3 次。观察时不要只比较"哪篇更好",还可以记录:

  • 相同意象和句式的重复程度;
  • 用词是否更大胆;
  • 是否偏离"秋日山野晚风"的主题;
  • 多次输出之间的差异有多大。

这才是调节随机性的意义:把感觉变成可以重复观察的实验。

八、降低 Temperature 能解决幻觉吗?

不能。

降低 Temperature 往往能让输出更稳定,却不能让模型突然知道它原本不知道的事实。一个错误答案也可能以非常稳定、非常自信的方式重复出现。

如果业务关心事实可靠性,还需要组合其他手段:

  • 用 RAG 为回答提供可追溯资料;
  • 用工具调用查询数据库或实时接口;
  • 用结构化输出和业务规则约束结果;
  • 建立包含正确答案、拒答场景和边界样本的评测集;
  • 对高风险任务增加人工复核。

Temperature 管的是"怎么抽样",不是"知识从哪里来"。把这两个问题分开,才能避免对参数产生不切实际的期待。

九、我的调参清单

以后再遇到需要控制大模型随机性的需求,我会按下面五步做:

  1. 先定义任务目标:要稳定、完整、有创意,还是兼顾几项?
  2. 选择保守基线:先使用供应商默认值或较低 Temperature。
  3. 一次只改一个变量:否则无法判断变化来自哪里。
  4. 同一输入重复采样:单次输出不能代表参数的整体效果。
  5. 用指标评估质量:同时看正确率、格式通过率、重复度、跑题率和人工偏好。

总结

大模型的生成不是从词库里随便抓一个词,而是在每一步计算候选分数、形成概率分布,再按照采样规则选出下一个 Token。

  • Temperature 重塑概率差距;
  • Top K 用固定数量限制候选;
  • Top P 用累计概率动态限制候选。

参数没有脱离模型、任务和评测集的"万能搭配"。先理解每个旋钮到底改了什么,再通过重复实验观察效果,才是开发者更有效、也更靠谱地控制 AI 随机性的方式。

相关推荐
LucianaiB3 小时前
毕业老学长给我留的最后一句话是:“AI 写的,别挂我名。” 我直接 神(Seed Evolving) 来!助我!
后端
IvanCodes3 小时前
RAG 实战教程(一):RAG 工作原理与完整流程——分片、索引、召回、重排和生成
人工智能·后端·agent
CodeSheep4 小时前
稚晖君公司人事大变动,来了!
前端·后端·程序员
小满zs4 小时前
Go语言第八章(函数)
后端·go
eric-sjq4 小时前
10 分钟实战:用 0.6B 本地模型生成中文网页(WanlyFrontend + 编译器全流程)
javascript·机器学习·自然语言处理·html
To_OC5 小时前
LC 438 找到所有字母异位词:暴力超时后,我靠滑动窗口一招搞定
javascript·算法·leetcode
kyriewen6 小时前
面试官问"这段逻辑为什么这样写"——我才发现,这半年我写的代码,我自己都解释不了
前端·javascript·面试
stark张宇6 小时前
实战Go高级特性:Context超时控制、defer资源回收与Channel通信的关键避坑点
后端·go
一壶浊酒..6 小时前
Scrape Webpack练习
开发语言·javascript·webpack