Skip to content

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

排查思路 ​

  1. 记录完整的请求:每次调用都记录发送的 messages、模型名、参数和返回的 usage。大部分“模型怎么这样回答”的问题,看一眼它实际收到了什么就明白了。
  2. 用同样的输入复现:把记录下来的请求原样重发几次,区分是偶发的随机性,还是稳定出现的问题。
  3. 怀疑上下文问题:检查发送的内容有没有被截断、关键规则是否还在、有没有混入不相关的资料。
  4. 事实性错误:确认这个信息是否应该来自检索或工具,而不是模型自己的“记忆”。

检查清单 ​

  • [ ] 应用自己保存和管理对话历史,不同用户严格隔离
  • [ ] 以 usage 为准统计 Token,为输出预留空间
  • [ ] 截断历史时保留系统指令,较早的对话可以总结
  • [ ] 根据任务类型设置温度,不把输出当作确定的返回值
  • [ ] 精确计算、计数和查询交给程序或工具
  • [ ] 事实和引用来自检索到的真实资料,并经过校验
  • [ ] 每次调用都记录请求内容和用量,便于排查

延伸阅读 ​

为复用而记录,为理解而整理。