
这篇并不是又一个“用框架搭 AI 应用”的教程而是回到一个最简单的起点LLM 到底是怎么工作的如果之前被各种概念绕晕过那么这篇文章直接帮你把“Token”和“Next Token Prediction”这两个词吃透。项目标题“The Next Token – LLMs, from the Beginning”是一个很有代表性的观点它把大语言模型从参数、注意力、微调这些复杂名词中拎出来最终落回一句话LLM 本质上只是在不断预测下一个 Token。这次我们不做教学空谈会直接结合 Python 代码、Hugging Face 生态和 OpenAI 兼容 API 的调用示例把 Token 是什么、生成文本的循环长什么样、训练和推理怎么对接、显存和 token 计费怎么估算全部串起来。读完这篇文章你至少能看懂一段 LLM 生成日志、估算一次 API 调用的费用也能知道“为什么长文本容易爆显存”“temperature 调大为什么容易胡说八道”这些高频问题背后的原因。1. 核心概念速览能力维度说明研究对象大语言模型LLM的底层生成机制核心单位Token模型实际处理的文本最小单元生成方式自回归每次只预测下一个 Token训练阶段预训练、监督微调SFT、偏好对齐RLHF/DPO关键参数模型参数量、上下文窗口、temperature、top-k、top-p推理资源权重显存 KV Cache 显存具体以模型和上下文为准接口能力主流平台均提供 token 计数、chat completion、批量请求适合读者刚接触 LLM 的开发者、想深入理解生成逻辑的算法工程师不适合场景想一键部署模型的零代码用户或需要行业级安全认证的生产系统这里先定一个基调理解 LLM不需要一开始把所有技术细节都背下来。只需要抓住“Token”和“下一个 Token”这条主线后面的注意力机制、位置编码、RLHF 都可以慢慢补。本文所有代码示例以学习验证为主不依赖特定硬件普通开发机加少量 CPU/GPU 就可以跑通小模型实验。2. Token理解 LLM 的最小单位2.1 为什么是 Token 而不是字很多初学者会问LLM 是不是按“字”来理解文本的实际上不是。中文里“字”的粒度太小英文里“单词”的粒度又太粗而且不同语言混合使用的时候很难统一。Token 就是模型自己规定的一种“文本切分单元”它可能是一个完整单词、半个单词、一个汉字、一个标点符号甚至是一个字节。举个例子英文单词“unbelievable”可能被切分成“un”、“believ”、“able”这样几个 token中文“今天天气不错”可能被切分成“今天”、“天气”、“不错”。具体怎么切由分词器Tokenizer决定。模型在做生成的时候并不直接看到原始字符串而是看到一串 token 编号。这个编号到 token 的映射表就是模型的词表Vocabulary。这种切分方式带来的直接好处是模型不需要为每个词汇单独学一个表示而是可以通过更小的子词单元组合出大量词汇。对中文场景来说一个常用汉字通常是一个 token但某些生僻字或表情符号可能会被拆成多个 token这也直接影响后续讨论的 token 计费。2.2 一个 Token 大约是多少字符这是实际开发里最常遇到的量化问题。不同模型的 Tokenizer 词表不同但大体遵循一个经验值一个英文 token 大约对应 3 到 4 个字符一个中文 token 大约对应 1 到 2 个汉字。也就是说1000 个英文字符大约对应 250 到 340 个 token1000 个汉字大约对应 600 到 1000 个 token。这个数字为什么重要因为几乎所有大模型 API 都按 token 计费而发一条请求时用户输入和模型输出都会被计费。你不知道一段文本到底消耗多少 token就没办法估算成本。另一层影响是上下文窗口容量模型说“128K 上下文”并不是说它能处理 128K 个汉字而是 128K 个 token。所以在实际测试长文本能力时需要先明确你的文本量换成 token 是多少。2.3 用代码观察 Token安装依赖pip install transformers接下来用 Hugging Face 的 AutoTokenizer 加载一个小模型分词器直接观察中英文的 token 切分结果from transformers import AutoTokenizer # 这里加载的是一个很小的分词器仅用于观察 token 行为 tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) text 今天天气不错适合学习 LLM。The next token is important. tokens tokenizer.tokenize(text) ids tokenizer.convert_tokens_to_ids(tokens) print(原始文本:, text) print(token 数量:, len(tokens)) print(token 列表:, tokens) print(token ids:, ids) # 直接编码 encoded tokenizer.encode(text) print(encode 后的 ids:, encoded) print(decode 还原:, tokenizer.decode(encoded))运行后会看到中文被按字或词切成多个 token英文和标点也有各自的编号。这里需要说明BERT 和 GPT 的分词规则不完全一样所以同一个句子在不同模型里的 token 数可能不同。真正做生成任务时应该用你将要部署或调用的那个模型自带的分词器而不是随意替换。3. 生成原理一切都在预测下一个 Token3.1 自回归生成逻辑LLM 的生成不是一次给出整段回答而是一个循环把当前已经生成的所有 token 作为输入让模型计算下一个 token 的概率分布然后按某种策略选出一个 token拼到序列末尾再送入模型继续预测。这个过程叫自回归生成Autoregressive Generation。假设输入是“今天天气”模型先预测下一个 token 是“不”的概率最高于是文本变成“今天天气不”。接着再预测下一个 token可能得到“错”然后序列变成“今天天气不错”。每一步模型都在用自己的输出作为下一步的输入直到遇到结束符EOS或达到最大生成长度。有一个很容易被忽略的点模型每一步都在“并行走一次前向计算”。也就是说生成 100 个 token 就需要执行 100 次前向传播而不是一次输出 100 个。这也是 LLM 生成速度相对较慢、且对算力要求高的核心原因。虽然现在有投机采样Speculative Decoding、批处理、KV Cache 等加速手段但底层的自回归逻辑没有变。3.2 最小生成循环用 Hugging Face Transformers 跑一个最小的自回归生成示例。这里只需要 CPU 即可模型很小用来理解流程from transformers import AutoTokenizer, AutoModelForCausalLM # 使用一个小型中文模型仅用于验证生成流程 model_name uer/gpt2-chinese-cluecorpussmall tokenizer AutoTokenizer.from_pretrained(model_name) # 部分模型没有 padding token这里简单处理 tokenizer.pad_token tokenizer.eos_token model AutoModelForCausalLM.from_pretrained(model_name) prompt 人工智能的下一步是 inputs tokenizer(prompt, return_tensorspt) # 直接生成 output_ids model.generate( inputs.input_ids, max_new_tokens30, do_sampleTrue, top_k50, top_p0.95, temperature0.8, ) output_text tokenizer.decode(output_ids[0], skip_special_tokensTrue) print(输入:, prompt) print(输出:, output_text)这段代码已经包含 do_sample、top_k、top_p、temperature 等采样参数它们会在后面的章节详细解释。此时只需要理解一件事模型接收 input_ids然后返回新的 id 列表最后通过 decode 还原成文字。如果你想观察“每一步选择哪个 token”可以手动实现一个循环。这会更加直观import torch device cuda if torch.cuda.is_available() else cpu model.to(device) model.eval() input_ids tokenizer(prompt, return_tensorspt).input_ids.to(device) with torch.no_grad(): for _ in range(20): logits model(input_ids).logits next_token_logits logits[0, -1, :] # 用 temperature 缩放 scaled_logits next_token_logits / 0.8 probs torch.softmax(scaled_logits, dim-1) # 概率最高的 token next_token_id torch.argmax(probs).unsqueeze(0).unsqueeze(0) input_ids torch.cat([input_ids, next_token_id], dim-1) print(tokenizer.decode(next_token_id[0], skip_special_tokensTrue), end) print() print(tokenizer.decode(input_ids[0], skip_special_tokensTrue))这个手写循环展示了最朴素的“贪心解码”每次都取概率最高的 token。实际产品里不会只用贪心因为贪心容易让回复单调、重复。于是就有了下一节要讨论的采样策略。4. 训练流程预训练、指令微调与对齐理解生成原理后再看训练就不难了。LLM 训练的第一阶段是预训练Pretraining目标非常简单粗暴给模型喂海量文本让它不断预测下一个 token。损失函数就是预测 token 和真实 token 之间的交叉熵。这个阶段让模型学会语言规律、知识体系和基本推理能力。预训练结束后模型已经能“接话”但它不一定懂得如何回答问题也可能输出无意义内容。于是有了第二阶段监督微调Supervised Fine-Tuning, SFT。人工标注一批“用户输入-期望输出”对比如“请介绍 LLM 的 token 概念”对应一段标准回答再继续训练模型。这个阶段会让模型学会指令跟随的基本格式。第三个阶段是对齐常见方法包括 RLHF基于人类反馈的强化学习和 DPO直接偏好优化。目标是让模型输出符合人类价值观和偏好减少有害内容、提高回答质量。通俗讲预训练让模型“能说”SFT 让模型“会回答”对齐让模型“说得像正常人”。从工程角度这里给一个清晰结论普通开发者不需要从零训练大模型。绝大多数场景是加载一个开源底座模型用自己的领域数据做 SFT 或 DPO或者干脆直接调用 API。开源的 7B 到 14B 模型在消费级显卡上可以用 LoRA 等参数高效微调技术跑起来但这是另一个话题。理解训练流程的目的是为了知道模型能力上限在哪里以及为什么模型会“一本正经地胡说八道”。5. 推理采样temperature、top-k、top-p5.1 参数含义生成时模型输出的不是最终文本而是下一个 token 的概率分布。如何从这个概率分布里挑 token直接决定了回答质量。temperature 控制概率分布的平滑程度。temperature 越低高概率 token 的优势越明显输出更确定temperature 越高低概率 token 也有机会被选中输出更多样但也更容易乱说。实际经验是代码生成、数学推理用较低温度0.1 到 0.3创意写作、头脑风暴用较高温度0.7 到 1.0。top-k 只保留概率最高的 k 个 token 作为候选其余全部过滤。top-p 则按累计概率过滤从概率最高的 token 开始累加直到累计概率超过 p然后在这个候选集合里重新归一化采样。top-k 和 top-p 可以一起用也可以单独用。下面是一个简单的参数对比表参数作用调低效果调高效果适用场景temperature整体概率分布平滑度更保守、更确定更多样、更发散低代码/数学高创意top-k候选 token 数量候选变少更稳定候选变多更多样配合 top-p 使用top-p候选累计概率范围候选变少更稳定候选变多更多样控制输出随机性5.2 接口调用示例如果你调用的是 OpenAI 兼容接口一般会暴露 temperature、top_p、max_tokens 等参数。下面是一个通用请求模板实际地址、模型名和密钥需要替换成你自己的服务import requests url http://127.0.0.1:8000/v1/chat/completions payload { model: your-model-name, messages: [ {role: user, content: 请用一句话解释什么是 Token} ], temperature: 0.3, top_p: 0.9, max_tokens: 200 } headers { Authorization: Bearer YOUR_API_KEY, Content-Type: application/json } response requests.post(url, jsonpayload, headersheaders, timeout60) data response.json() print(data[choices][0][message][content]) print(usage:, data.get(usage))返回结果里的 usage 字段通常会包含 prompt_tokens、completion_tokens 和 total_tokens这是做 token 计费和生产监控的重要数据。如果你自己部署模型也可以参照这个结构封装一个简单的服务。6. 上下文、KV Cache 与资源占用6.1 上下文窗口上下文窗口Context Window是模型一次最多能处理的 token 数量包括输入和输出。常见的有 4K、8K、32K、128K、200K 等。超过这个限制模型就会表现异常早期输入可能被截断或者明确报错。实际使用中建议保留一定余量不要把窗口完全占满。为什么长上下文会消耗大量资源因为 Transformer 模型在推理时需要缓存已生成 token 的 Key 和 Value 矩阵这个缓存叫 KV Cache。生成时每新增一个 tokenKV Cache 就会变大一点。所以上下文越长显存占用越高。这也是很多本地部署用户发现“单条短对话显存够长对话突然 OOM”的原因。6.2 显存估算显存占用主要由三部分构成模型权重、KV Cache、中间激活值。模型权重部分可以快速估算一个 7B 模型用 FP16 加载权重约占 14GB7B 个参数每个参数 2 字节。用 INT8 量化后约 7GB用 INT4 量化后约 3.5GB。KV Cache 大小则取决于模型层数、注意力头数、隐藏层维度和当前上下文长度不能简单按一个数字估算。实操建议是先用默认配置跑通再逐步增加上下文长度用 nvidia-smi 实时观察显存曲线。下面给出一段常用的观察命令nvidia-smi --query-gpumemory.total,memory.used,memory.free --formatcsv watch -n 1 nvidia-smi6.3 生成速度观察除了显存生成速度也需要关注。LLM 推理的加载阶段是并行计算速度较快生成阶段是逐 token 自回归速度较慢。可以用下面这段小逻辑统计每秒生成 token 数import time start time.time() output_ids model.generate( inputs.input_ids, max_new_tokens50, do_sampleTrue, top_p0.95, temperature0.8, ) elapsed time.time() - start generated_tokens output_ids.shape[1] - inputs.input_ids.shape[1] print(f耗时 {elapsed:.2f} 秒生成 {generated_tokens} token速度 {generated_tokens / elapsed:.1f} token/s)如果你在接入生产环境记录生成速度和 token 数非常关键这能帮你预估高峰期的并发能力和成本。7. 接口 API、Token 计费与批量任务7.1 Token 计费几乎所有商业大模型 API 都按 token 计费而不是按字符数。计费通常分输入和输出两个价格输入便宜、输出更贵。原因很简单输出端是逐 token 自回归生成的计算量远大于输入端。开发者在设计应用时要考虑“一次请求花多少钱”。比如每 1000 个输入 token 花费 0.002 元、每 1000 个输出 token 花费 0.006 元数字仅示例实际价格以平台公布为准。那么一次输入 2000 token、输出 500 token 的请求就是2 * 0.002 0.5 * 0.006 0.007元。积少成多批量任务尤其要提前估算成本。7.2 批量任务与重试策略批量任务的典型场景是“给 10000 条文本做摘要”或“给一批文档做知识问答”。此时不能简单写一个 for 循环逐个请求因为可能遇到限流、超时和单点失败。工程上建议把任务拆成文件或数据库中的一批记录每条记录记录状态。每条请求独立设置超时时间失败后自动重试 2 到 3 次。增加并发数时分阶梯往上涨观察接口返回的限流状态。处理长文本时分批或分页发送避免超过上下文上限。记录每次请求的 token 消耗生成费用报表。示例一个简单的批量处理框架。import time import requests tasks [task001, task002, task003] results {} def call_llm(text): # 这里替换成真实接口 url http://127.0.0.1:8000/v1/chat/completions payload { messages: [{role: user, content: text}], max_tokens: 200, temperature: 0.3 } for attempt in range(3): try: resp requests.post(url, jsonpayload, timeout30) resp.raise_for_status() return resp.json() except Exception as e: if attempt 2: raise e time.sleep(2 ** attempt) for task in tasks: content f这是任务 {task} 的输入文本 try: result call_llm(content) results[task] result[choices][0][message][content] print(task, 成功) except Exception as e: results[task] f失败: {e} print(task, 失败)这个模板没有绑定具体平台可以直接改造成自己的批量任务模块。关键点在于状态要可追踪失败要重试并发要受限。7.3 常见 API 错误错误类型现象排查方向上下文超限输入 token 超过模型最大限制截断、摘要或换更大窗口模型限流出现 429 Too Many Requests降低并发、增加退避时间鉴权失败403 / 401检查 API Key、权限和地区支持超时长时间无返回增加 timeout或减小 max_tokens8. 常见问题与排查方法问题现象可能原因排查方式解决方案生成结果重复温度过低或缺省 top_p检查采样参数适当上调 temperature 到 0.7或启用 top-k生成内容偏离主题温度过高查看输出概率分布降低 temperature或设更小 top-p输入过长报错token 超过上下文窗口打印 usage 或 token 数量截断、摘要、分块处理显存不足模型权重 KV Cache 超限nvidia-smi 观察峰值显存量化模型、减小上下文、换小模型生成速度太慢自回归 token 数多测量 token/s减少 max_tokens、用批量推理、升级硬件接口偶发失败网络波动或限流查看状态码加重试、退避、错误日志token 数量统计不一致用了错误的分词器检查 tokenizer 是否匹配模型使用模型自带 tokenizer排查原则是先看日志和状态码再复现最小用例最后考虑参数调整。不要一上来就换模型很多问题其实出在参数或输入处理上。9. 最佳实践与后续学习路线把“The Next Token”这条主线吃透之后建议做三件事。第一用一个本地小模型把生成循环和采样参数跑通。不需要高配显卡CPU 也能跑。重点观察温度变化对输出的影响以及 token 数量如何变化。这一步能建立直觉。第二接一次真实 API。无论你用哪个平台先打印一次请求的 usage 字段记录输入和输出 token 数自己算一下费用。然后尝试写一个小批量任务加上重试和日志。这样你会对“token 计费”“上下文窗口”“限流”有真实感知。第三再回头看更高级的概念。注意力机制解决的是“模型如何关注前文中的关键 token”位置编码解决的是“token 顺序怎么表达”KV Cache 解决的是“生成加速”。有了主线这些概念就不再是孤立的点而是围绕同一个目标展开更好地预测下一个 token。下一步还可以学习这些方向用 Transformers 库加载一个 7B 模型尝试本地推理。用 LoRA 做领域微调观察训练数据如何影响生成。用 vLLM 或 FastChat 部署一个兼容 API 的服务测试并发。跑一遍向量检索和 RAG 流程理解“外接知识”如何影响 token 上下文。最后给一个实用建议笔记里优先记 token 数量、显存占用、生成速度和失败重试策略而不是只记概念。因为这些数字会直接决定你的本地部署方案和 API 成本。把这套实践跑下来再回去看模型源码和论文会顺畅很多。