LLM 基础:Token、上下文、采样与幻觉
第一次把大语言模型接进产品时,很多问题都来自对它工作方式的误解:
- 以为模型“记得”上一轮对话,结果每次请求它都像第一次见面;
- 对话聊久了,模型开始忘记最开始交代的规则;
- 把温度设成 0,以为输出就会完全固定,结果还是偶尔不一样;
- 让模型统计一段话里有多少个字,它很自信地给出了一个错误的数字;
- 模型给出的“参考文献”看起来很专业,查了才发现根本不存在。
这篇从模型生成内容的方式讲起,把这些问题背后的原因说清楚。后面的 Prompt、RAG、Agent 文章,都建立在这些基础之上。
读完这篇你会
- 理解“预测下一个 Token”意味着什么
- 正确管理对话历史和上下文长度
- 知道哪些任务不能直接交给模型,以及该怎么补救
1. 原理:一个超级强大的输入法联想
手机输入法会根据你已经打的字,推荐下一个最可能的词。大语言模型做的事情本质上一样,只是强大得多:根据前面所有的内容,计算下一个 Token 的概率分布,从中选一个接上,然后把新内容加到前面,再预测下一个……
text
输入:"今天天气"
→ 计算下一个 Token 的概率:真(0.6) 很(0.2) 不(0.1) ……
→ 选中:“真”
输入:"今天天气真"
→ 计算:好(0.7) 不错(0.2) ……
→ 选中:“好”
……直到生成结束标记这个“逐个接龙”的机制,直接决定了模型的几个特性:
- 它生成的是“最可能的下一段文字”,不是“查证过的事实”。训练数据里学到的知识可能过时、可能不完整,模型本身也无法告诉你某个说法的来源;
- 输出带有随机性,同样的输入可能得到不同的结果;
- 精确的计算、计数、查询,不是它的强项,这些应该交给程序。
模型本身也不会上网、不会读你的数据库。产品里的“联网搜索”“读取文件”,都是外层程序检索到资料后放进输入里,再交给模型的。
2. 第一次调用模型
现在大多数模型服务都提供兼容 OpenAI 格式的“对话补全”接口。一次最小的调用是这样的:
js
const response = await fetch(`${process.env.LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.LLM_API_KEY}` // 密钥只放在服务端环境变量中
},
body: JSON.stringify({
model: process.env.LLM_MODEL,
messages: [
{ role: 'system', content: '你是一名简洁的技术助手,回答不超过三句话。' },
{ role: 'user', content: '什么是闭包?' }
],
temperature: 0.3
})
})
const data = await response.json()
console.log(data.choices[0].message.content) // 模型的回答
console.log(data.usage) // 本次消耗:输入 Token、输出 Token、合计三种角色:system 是你给模型的总体规则,user 是用户说的话,assistant 是模型之前的回答。
3. 坑一:以为模型会记住上一轮对话
现象:用户先说“我叫小林”,下一轮问“我叫什么”,模型回答“我不知道”。
原因:模型本身是无状态的。每一次请求都是独立的,它只知道这次请求里 messages 包含的内容。所谓“记忆”,其实是应用每次都把之前的对话重新发送一遍。
js
const history = [
{ role: 'system', content: '你是一名友好的助手。' },
{ role: 'user', content: '我叫小林。' },
{ role: 'assistant', content: '你好,小林!' },
{ role: 'user', content: '我叫什么?' } // 前面的内容都要带上,模型才“记得”
]由此带来的影响:
- 对话越长,每次请求发送的内容越多,费用和延迟都会随之增长;
- 历史记录由你的应用保存和管理,包括什么时候截断、什么时候总结;
- 用户之间的历史必须严格隔离,不能把 A 的对话发给 B 的请求。
4. Token 和上下文窗口
Token 是模型处理文本的单位。一个 Token 可能是一个汉字、半个汉字、一个英文单词或者单词的一部分,具体怎么切由模型的分词器决定。计费、长度限制都按 Token 计算。
上下文窗口是模型一次请求能处理的 Token 上限,它包含了所有内容:系统指令、历史对话、检索到的资料、工具定义,以及模型要生成的回答。
可以把上下文窗口想成一张工作台:所有材料都要摊在台面上,模型才能看到。台面大小是固定的,放不下的东西就只能拿走。
坑二:用字数估算长度,结果请求超限
现象:按“一个汉字约等于一个 Token”估算,以为还有很多余量,请求却因为超出上下文长度被拒绝;或者账单比预估高了不少。
原因:Token 和字数的换算比例因模型、语言、内容类型而异。代码、特殊符号、混合语言的文本,比例差别很大。
修复:
- 以接口返回的
usage字段为准,记录每次请求的实际消耗; - 需要预先估算时,使用对应模型提供的分词工具;
- 发送前预留足够的空间给模型的回答(输出也占用上下文)。
坑三:对话一长,模型就忘了最开始的规则
现象:系统指令里要求“只用中文回答”,聊了几十轮后,模型开始用英文回答,或者忽略了其他规则。
原因:
- 历史太长,超出了窗口,应用为了塞下新内容,把最早的消息(可能包括关键规则)截掉了;
- 即使没有超出窗口,内容太多时,模型对中间部分信息的关注度也会下降。
修复:
- 截断历史时,始终保留系统指令,只截掉中间较早的对话;
- 较早的对话可以让模型总结成一段摘要,代替原文;
- 只把和当前问题相关的资料放进上下文(这正是 RAG 要做的事),而不是把所有东西都塞进去。
5. 采样:温度到底控制什么
模型算出下一个 Token 的概率分布后,要从中选一个。温度(temperature)控制选择的“冒险程度”:
- 温度低:更倾向于选概率最高的那个,输出更集中、更保守;
- 温度高:概率较低的选项也有更多机会被选中,输出更多样、更有创意。
可以想成抽签:温度低时,热门选项的签被做得特别多;温度高时,各个选项的签数更接近。
| 任务 | 温度建议 |
|---|---|
| 信息提取、分类、格式转换 | 低 |
| 一般问答 | 中等 |
| 头脑风暴、创意写作 | 较高 |
各家模型支持的参数和取值范围不同,部分推理模型甚至不支持调整温度,接入新模型时要先查看它的文档。
坑四:温度设成 0,输出还是偶尔不一样
现象:为了让结果稳定,把温度设成 0,但同样的输入,偶尔还是会得到不同的输出。
原因:温度为 0 能大幅降低随机性,但服务端的实现细节(并行计算、硬件差异、模型版本更新)仍可能导致结果出现差异。
修复:不要把模型输出当成确定的接口返回值。需要结构化数据时,用 schema 校验输出;需要精确结果时,交给程序计算;需要事实正确时,核对来源。
6. 幻觉:一本正经地说错话
幻觉指模型生成了看起来合理、但没有依据或者与事实不符的内容。它不是“故意撒谎”,而是“预测下一个最可能的 Token”这个机制的自然结果:当模型缺乏相关知识时,它仍然会生成一段“看起来像答案”的文字,而且语气可能很自信。
| 容易出问题的场景 | 原因 |
|---|---|
| 具体的数字、日期、版本号 | 训练数据可能过时,也无法确认来源 |
| 引用文献、链接、法规条款 | 没有真实资料时,可能生成格式正确但不存在的引用 |
| 精确计数、复杂计算 | 逐 Token 生成不等于真正的计算 |
| 关于自身的描述(版本、能力) | 缺少可靠的运行时信息 |
坑五:让模型做精确的计算和统计
现象:让模型统计一份名单里有多少人,或者计算订单总金额,结果经常差一点。
修复:精确的事情交给程序做。可以让模型调用工具(比如一个计算函数、一次数据库查询),再根据工具返回的结果组织回答,详见 Agent 架构与工具调用。
坑六:相信模型给出的引用
现象:模型的回答里附带了几篇“参考文献”和链接,格式完全正确,点开后发现有的不存在,有的内容对不上。
修复:
- 需要依据资料回答时,先由程序检索到真实的资料,再让模型基于这些资料回答,并标明引用的是哪一段(RAG);
- 程序校验模型给出的引用是否确实存在于检索到的资料中;
- 产品上明确展示来源,让用户可以自己核对。
7. 把这些基础串起来
| 模型的局限 | 应用层的补救 | 详见 |
|---|---|---|
| 不知道你的任务要求 | 写清楚任务、约束、示例和输出格式 | Prompt 工程实践 |
| 不知道你的私有资料、最新信息 | 先检索,再把相关资料放进上下文 | RAG 检索增强生成 |
| 不擅长计算、查询、执行操作 | 让模型调用工具,由程序执行 | Agent 架构与工具调用 |
| 输出不稳定,可能出错 | 校验、评测、加护栏 | LLM 评测与 Guardrails |
排查思路
- 记录完整的请求:每次调用都记录发送的
messages、模型名、参数和返回的usage。大部分“模型怎么这样回答”的问题,看一眼它实际收到了什么就明白了。 - 用同样的输入复现:把记录下来的请求原样重发几次,区分是偶发的随机性,还是稳定出现的问题。
- 怀疑上下文问题:检查发送的内容有没有被截断、关键规则是否还在、有没有混入不相关的资料。
- 事实性错误:确认这个信息是否应该来自检索或工具,而不是模型自己的“记忆”。
检查清单
- [ ] 应用自己保存和管理对话历史,不同用户严格隔离
- [ ] 以
usage为准统计 Token,为输出预留空间 - [ ] 截断历史时保留系统指令,较早的对话可以总结
- [ ] 根据任务类型设置温度,不把输出当作确定的返回值
- [ ] 精确计算、计数和查询交给程序或工具
- [ ] 事实和引用来自检索到的真实资料,并经过校验
- [ ] 每次调用都记录请求内容和用量,便于排查
延伸阅读
- Prompt 工程实践:把任务说清楚
- RAG 检索增强生成:让模型依据资料回答
- AI Agent 开发实战:从最小工具到完整的业务链路