今天要解决的问题
做 Agent 不需要会训练模型,但你得知道模型是怎么「想」的,不然很多现象解释不了:为什么同一个问题问两遍答案不一样;为什么让它算 37 × 89 偶尔会错;为什么给它一份 200 页的文档它会漏掉中间的内容;为什么中文提示词比英文贵。
今天结束时你应该能:
- 说清楚「下一个 token 预测」和「自回归生成」是什么意思,并用它解释幻觉。
- 知道 token 是什么,能用代码数出一段中文和一段英文各是多少 token。
- 知道上下文窗口是什么,以及「窗口有 1M」不等于「1M 都能用好」。
- 知道 temperature 和 top_p 调的是什么,Agent 场景下一般怎么设。
- 知道「推理模型 / 思考模式」和普通模式的区别,以及什么时候该开。
预计用时:3 到 4 小时,其中视频大约 1.5 小时。前置:Day 0 的环境已经跑通。
模型只做一件事:猜下一个 token
把大模型想成一个巨大的函数。输入是一串 token,输出是「下一个 token 是词表里每一个词的概率」。比如输入「北京是中国的」,它输出的概率分布里,「首」可能是 0.92,「政」0.03,「一」0.01……一共十几万个候选,每个都有一个概率。
然后程序从这个分布里挑一个 token 出来(怎么挑后面讲),比如挑了「首」。把「首」接到输入后面,变成「北京是中国的首」,再喂进去,得到下一个分布,挑出「都」。如此循环,直到挑出一个特殊的「结束」token,或者达到你设的最大长度。
这就叫自回归生成:每一步都把自己上一步的输出当作下一步的输入。ChatGPT 打字一个一个往外蹦,不是故意做的动画效果,它就是这么一个一个算出来的。
用这件事解释幻觉
模型在训练时学到的是「什么样的文字在什么样的上下文后面出现得多」。它没有一个「事实数据库」可以查,也没有「我不确定」这个内置开关。你问它「云簿 CloudLedger 公司的退款政策是什么」,这家公司是我们虚构的,它从没见过。但「XX 公司的退款政策是」后面接「自购买之日起 30 天内可申请全额退款」,在训练数据里是非常常见的文字模式。于是它就很流畅地编了一段。
这就是幻觉的本质:生成流畅的文字和说出真实的事实是两件事,模型只被训练成擅长前一件。后来的指令微调和 RLHF 让它学会了在一些情况下说「我不知道」,但这个能力不可靠。
对 Agent 工程师来说,结论很直接:凡是需要事实的地方,都不要指望模型自己知道,要通过工具或检索把事实送到它眼前(W2 和 W3 的全部内容)。然后还要让它引用出处,方便检查。
为什么它算术会错
37 × 89 = 3293。模型算这个不是在做乘法,是在「猜下一个 token」。如果「3293」在它的词表里被切成「329」「3」两个 token,它要先猜对「329」再猜对「3」。常见的小乘法它见得多,猜得准;大一点的数就开始不靠谱。现在的模型在这方面已经好了很多,特别是开了思考模式之后,但原理没变。
所以 Agent 里遇到计算,正确做法是给它一个计算器工具,或者让它写代码去算。Day 14 的小项目里就有一个计算器工具。
Token:模型看到的不是字,是 token
模型不直接处理汉字或字母,处理的是 token。一个 token 可能是一个英文单词、半个单词、一个汉字、半个汉字,甚至几个汉字的组合,取决于这个模型的分词器(tokenizer)。
分词器是怎么来的?最常见的方法叫 BPE(Byte Pair Encoding)。简单说:一开始把所有文字拆成最小的字节,然后统计训练语料里哪两个相邻片段一起出现得最多,就把它们合并成一个新 token,加进词表。重复几万到十几万次,词表就建好了。常见的组合(比如英文的 “ing”、” the”,中文的「的」「我们」)会变成单个 token,罕见的组合就被拆成好几个。
这带来几个直接影响 Agent 工程的后果。
中文通常比英文贵。 DeepSeek 官方文档给的经验比例是:1 个英文字符约 0.3 个 token,1 个中文字符约 0.6 个 token。同样的意思,英文一个单词平均五六个字母,大约 1.5 到 2 个 token;中文表达同样的意思可能要三四个字,2 到 2.5 个 token。不同模型差别很大,国产模型的分词器对中文优化得更好,有些海外模型的中文会切得更碎。计费按 token 算,所以这直接影响成本。
数字和代码会被切得很碎。 订单号 INV-2026-004417 可能变成七八个 token。这也是模型偶尔会把长编号抄错一位的原因之一。Agent 里涉及编号的地方,尽量让工具返回原样,不要让模型「转述」。
字母级的任务对模型很别扭。 经典的例子是问「strawberry 里有几个 r」。模型看到的可能是 “str” “aw” “berry” 三个 token,它看不到单个字母。
上下文窗口:模型能看到的全部世界
模型每次调用能接收的 token 总数是有上限的,叫上下文窗口(context window)。输入加输出都要算在里面。2026 年主流模型的窗口普遍很大:DeepSeek 的 deepseek-flash 官方写的是 1M(一百万)token,Claude 的当前模型也是 1M。
这里有两个重要事实,比数字本身更重要。
第一,模型没有记忆,窗口就是全部。 API 是无状态的。你和它聊了二十轮,第二十一轮它之所以「记得」前面的内容,是因为你的程序把前二十轮对话全部重新发了一遍。窗口外的东西,对模型来说就是不存在。Day 4 会写代码亲手管理这件事。
第二,窗口大不等于都能用好。 大量测试发现,塞进去的内容越多,模型对中间部分的注意力越容易下降,业内叫「lost in the middle」,最近也有人叫「上下文腐烂」(context rot)。而且塞得越多,每次调用越贵、越慢。一个 Agent 跑了 30 步,每步都把前面所有工具结果带上,窗口很快就被垃圾填满。所以上下文窗口要当作稀缺资源来管理,这就是 Day 20 要讲的「上下文工程」。
算一笔账感受一下:一个客服 Agent 每处理一张工单要调用模型 8 次,平均每次输入 6,000 token。用 deepseek-flash 高峰价(输入 2 元 / 百万 token),一张工单输入部分是 8 × 6,000 × 2 ÷ 1,000,000 ≈ 0.1 元。一天一万张工单就是一千元。如果你偷懒把整个知识库都塞进去,每次 10 万 token,那就是十几倍。
采样:temperature 和 top_p
模型给的是概率分布,最后输出哪个 token 由「采样」决定。
temperature(温度) 控制分布的「尖锐程度」。temperature 低,高概率的 token 更占优势,输出更确定、更保守;temperature 高,分布被拉平,冷门 token 被选中的机会变大,输出更多样,也更容易跑偏。temperature = 0 时(或接近 0)基本就是每次都选概率最高的那个。
top_p 是另一种截断方式:只在累计概率达到 p 的那些候选里采样。比如 top_p = 0.9,就是把概率从高到低排,加到 90% 为止,剩下的长尾候选直接不考虑。
Agent 场景下的经验:需要稳定、可复现的步骤(分类、抽取字段、决定调哪个工具),temperature 设低,0 到 0.3;需要创意的(起草营销文案)可以高一点。一般只调 temperature,不同时调 top_p。另外要知道:temperature = 0 也不保证每次输出完全一样,服务端的批处理、浮点运算顺序都可能带来微小差异。不要把系统正确性建立在「每次输出一字不差」上。
还有一点:开了思考模式的推理模型,很多厂商会限制或忽略 temperature 参数。用之前看文档。
模型是怎么训出来的(一句话版)
你只需要知道三个阶段的大意,面试偶尔会问:
- 预训练:用海量互联网文本训练「猜下一个 token」。得到的是一个「文本续写器」,你给它一个问题,它可能接着写出更多问题,因为网上很多文本就是问题列表。
- 指令微调(SFT):用大量「指令 - 好回答」的对话样本继续训练,让它学会「你问我答」的格式。
- 人类反馈强化学习(RLHF)及其变体:让人(或别的模型)对回答打分,训练它倾向于给出被打高分的回答。拒绝有害请求、承认不知道,都是这个阶段加强的。
后来又多了一类:用强化学习专门训练模型在回答前先「思考」,就是推理模型。
推理模型和思考模式
2025 年以后,主流模型基本都有「思考」能力:回答之前先生成一段很长的推理过程,再给最终答案。DeepSeek 2026 年的模型是默认开启思考模式的,思考内容在返回的 reasoning_content 字段里,和最终回答 content 分开;Claude 当前的模型叫自适应思考,由 effort 参数控制想多少。
对 Agent 工程师,思考模式是个成本和效果的权衡:
- 开:复杂多步任务、需要规划、需要算数和推理的场景,准确率明显更高。
- 关:简单分类、抽取、闲聊,开了只是更慢更贵(思考的 token 也按输出计费)。
这门课 W1 的练习默认关掉思考模式,让你更快看到结果、更省钱。W2 开始做 Agent 时会再讨论什么时候打开。顺便提前说一个坑:DeepSeek 文档要求,带了 tools 参数的对话里,历史轮次的 reasoning_content 必须完整回传,否则会报 400。Day 8 会遇到。
Transformer、注意力和 MoE,面试够用版
前面一直说「模型猜下一个 token」,没说里面是什么。做 Agent 不需要会推公式,但国内面试几乎必问「讲讲 Transformer」「注意力是干什么的」「MoE 为什么快」,而且这几件事能直接解释你后面会碰到的成本和延迟现象。这里只讲直觉,够答题、够做工程判断就行。
现在主流的大模型,几乎都是 2017 年论文《Attention Is All You Need》提出的 Transformer 的一个变体,叫 decoder-only Transformer。它的结构说白了就是同一种「层」叠几十次。每一层做两件事:先让每个 token 去看前面的 token(注意力),再让每个 token 自己过一个大的前馈网络(FFN)消化一下。输入的 token 先变成向量,一层层往上传,最后一层的输出再变成「下一个 token 的概率分布」,就是上一节那张表。
注意力:每个 token 决定该看谁
读这句话:「客户说发票开错了,它的金额少了 20 元」。人一看就知道「它」指的是发票。模型怎么知道?靠注意力。处理到「它」这个位置时,模型会给前面每个 token 打一个相关度分数,然后按分数加权把那些 token 的信息拿过来。「发票」的分数高,「客户」「说」的分数低,所以「它」的表示里主要混进了「发票」的信息。
几个面试常追问的细节,每个都对应一个工程现象:
- 只能往前看。 生成模型的注意力加了「因果遮罩」,每个位置只能看到它前面的 token,看不到后面的。所以它必须一个一个往外吐,这就是自回归。
- 多头注意力。 一层里有几十个注意力「头」并行,各自学不同的关系,有的盯指代,有的盯语法,有的盯位置。不用记细节,知道「多个角度同时看」就够了。
- 位置信息要另外加。 注意力本身不分先后,模型靠位置编码知道谁在前谁在后,现在多数模型用的是 RoPE(旋转位置编码)。上下文能不能从 128K 拉到 1M,很大程度上就是在这上面做文章。
- 长度越长越贵。 每个新 token 都要和前面所有 token 算一次相关度,所以处理一段长输入的计算量大约随长度的平方增长。这就是为什么长 prompt 的首 token 延迟明显变长。
- KV cache。 注意力里每个 token 会算出一对「键(K)」和「值(V)」向量,前面 token 的 K、V 在生成后面的 token 时不会变,所以推理服务把它们存在显存里复用,这叫 KV cache。它解释了两件事:一是上下文越长占显存越多,同一张卡能同时服务的请求就越少;二是 prompt caching 能便宜,本质就是把相同前缀的 KV cache 存下来下次直接用,Day 38 会讲怎么利用它。
还有一个容易被问的:注意力负责「token 之间交换信息」,那知识存在哪?一般的说法是大部分事实性知识存在每层的前馈网络(FFN)参数里,注意力更像是「在上下文里找相关的东西」。这也是 RAG 能起作用的直觉:把资料放进上下文,让注意力去找,而不是指望 FFN 里记住了。
MoE:参数很多,每次只用一小部分
前面的本地模型表里出现过好几次 MoE(Mixture of Experts,混合专家)。它改的是每层里的 FFN:把一个大 FFN 换成几十到几百个小 FFN(叫「专家」),再加一个很小的路由器。每个 token 进来,路由器打分,只挑分数最高的几个专家(常见是 2 到 8 个)去算,其余专家这一步完全不动。
所以 MoE 模型有两个参数量要分清:总参数和激活参数。DeepSeek-V3 总参数 671B,每个 token 激活约 37B;Qwen3-30B-A3B 名字里就写了,总 30B,激活 3B。对你的影响是:
- 速度和单次计算成本看激活参数。 每个 token 只算 37B 的量,所以它比同样总参数的稠密模型快得多,API 也能卖得便宜。
- 显存看总参数。 所有专家都要加载进内存,路由器随时可能选到任何一个。所以自己部署 DeepSeek 完整版仍然要多卡服务器,这是很多人第一次算错的地方。
- 效果更接近总参数那一档。 不同专家各自学到不同领域的东西,总的知识容量大。
一句话版本:MoE 用更多显存换更快的速度和更低的单 token 成本。面试时能把「总参数 / 激活参数 / 显存 / 速度」这四个词的关系讲清楚,就比背定义强。
想看动画讲解,3Blue1Brown 深度学习系列第 5、6 章(B 站官方号有双语版)讲 Transformer 和注意力讲得最直观,学习资料里有链接。
补充:哪些模型能本地部署?自己训一个要做什么?
能下载到自己机器上跑的叫「开放权重」(open-weight)模型。截至 2026 年年中的主流系列:
| 系列 | 厂商 | 特点 |
|---|---|---|
| Qwen(通义千问) | 阿里 | 尺寸最全(零点几 B 到几百 B),中文强,社区最常用 |
| DeepSeek | 深度求索 | 旗舰是几百 B 的 MoE,效果接近闭源一线,完整版要多卡服务器 |
| GLM | 智谱 | 中文好,有 Agent / 代码方向的版本 |
| Kimi K2 | 月之暗面 | 万亿参数级 MoE,擅长 Agent 和工具调用 |
| Llama | Meta | 开源生态的老大哥,英文为主 |
| Gemma | 小尺寸做得好,适合笔记本和手机 | |
| Mistral | Mistral AI | 中小尺寸,许可宽松 |
| gpt-oss | OpenAI | 2025 年发布的开放权重模型,20B / 120B 两档 |
| Phi | 微软 | 小参数做推理 |
用什么跑: 个人电脑用 Ollama(一条命令下载并运行)、LM Studio(图形界面)、llama.cpp(底层引擎,CPU 也能跑)、MLX(Apple Silicon 专用);服务器用 vLLM、SGLang(生产主流,吞吐高)。
要多大内存(粗算): 4-bit 量化 ≈ 参数量(B)× 0.6 GB,7B 约 4–5 GB、32B 约 20 GB;不量化(FP16)≈ 参数量 × 2 GB;另加 KV cache,上下文越长越多。没有独立显卡的 Intel 笔记本只能用 CPU 跑 1–4B 的小模型,每秒几个 token;16 GB 内存的 M 芯片 Mac 能流畅跑 7–8B。
举例:32 GB 内存的 Mac mini 能跑多大? macOS 默认只让 GPU 用约 2/3–3/4 的统一内存,32 GB 机器大约 21–24 GB 可用。4-bit 量化下 7–8B(约 5 GB)很轻松;14B(约 9 GB)最舒服;32B(约 19–20 GB)能跑但紧,上下文不能开太长;70B(40 GB 以上)跑不了。速度上限 ≈ 内存带宽 ÷ 每个 token 要读的模型大小:普通 M4(约 120 GB/s)跑 32B 理论上每秒约 6 个 token,M4 Pro(约 273 GB/s)约 14 个。最划算的是 30B 左右的 MoE 模型(如 Qwen3-30B-A3B、gpt-oss-20b):总参数大,但每个 token 只激活约 3B,速度接近 3B 模型,效果接近 30B。所以同样 32 GB,选机器要看内存带宽,它决定生成速度。
本地跑 14B 能赚钱吗? 基本不能。14B 的效果比 DeepSeek 这类 API 差,API 又便宜到几分钱处理几十万字,一台 32 GB Mac mini 的钱够调用很多年;单机算力也太小,租不出去。本地模型真正的用处是:①数据不能出公司的场景(代码、客户资料、合同);②大批量简单任务(分类、打标签、抽字段)跑一晚上不花钱;③学本地部署、LoRA 微调、大小模型级联,做进作品集。它的价值是帮你找到更好的工作,不是直接赚钱。要用 AI 赚钱,靠的是用模型做出来的产品或服务,而那多半直接调 API 就够了。
开发板能跑吗? 国内常见的是 RK3588 开发板(香橙派 5 系列等):芯片自带 6 TOPS 的 NPU,用瑞芯微官方的 RKLLM 工具链把 Qwen 这类小模型转换后放到 NPU 上跑。1.5B–3B 比较流畅,7B 能跑但慢(社区实测大致 1.5B 每秒十几个 token、7B 每秒几个)。瓶颈是内存带宽而不是 NPU 算力:每生成一个 token 都要把整个模型从内存读一遍。适合离线语音助手、机器人这类边缘场景,或者学嵌入式部署。ESP32 这类单片机只有几 MB 内存,只能跑几十万参数的玩具模型,实际用法是收音、联网后转发给云端模型。
本地跑和用厂商服务有什么区别? 能力上,个人电脑一般只能跑 7–32B,比厂商的旗舰模型弱一档,更容易胡编;隐私上,数据不出本机,适合公司代码、合同这类敏感内容;成本上,硬件一次投入、之后只有电费,不按 token 付费;网络上,断网也能用,不受限流和宕机影响;维护上,下载、升级、调参都得自己来。
本地跑是不是没有限制? 不是。厂商服务的限制大致三层:①模型自身的训练,后训练阶段学会的拒绝行为写在权重里,下载到本地照样拒绝,国内模型训练时按国内内容要求做的对齐也一样还在;②服务端审核,厂商在模型前后加的内容过滤和系统提示词,本地没有了;③账号和条款,违规会被封号、请求会被记录,本地也没有了。所以本地跑只是少了外面两层。社区里有去掉拒绝行为的改版模型,但质量通常下降。另外开源许可证本身有使用限制(比如 Llama 的可接受使用政策),用模型做了什么,法律责任始终在使用者自己。对 Agent 工程师来说,本地模型的价值主要是隐私和省钱,不是「没限制」。
买板子是不是比买电脑性价比高? 只看「多少钱跑多大模型」板子便宜,但对学 Agent 来说性价比最低:板子 14B 基本跑不动(带宽瓶颈,7B 已经很慢);RKLLM 要转换模型、只支持部分模型,时间花在调环境上;学到的是嵌入式部署,和 Agent 岗位不对口。更划算的顺序:课程直接用 API(每月几块到几十块)→ 想体验本地模型就在现有电脑上装 Ollama 跑 1.5B → 学微调时按小时租云端 GPU(AutoDL 这类平台,一张卡每小时几块钱)→ 本来就要换电脑时再选 32 GB 内存。板子适合把做硬件当爱好的人,比如做离线语音助手。
自己从零训一个大模型,就是把上面「三个阶段」真做一遍,再加前后两头:
- 数据:抓海量文本(网页、书、代码、论文),清洗、去重、过滤,定好各类数据配比。最费人力,也最决定效果。
- 分词器:用自己的数据训 BPE 词表。
- 结构:基本都是 decoder-only Transformer,定层数、宽度、注意力头数、要不要 MoE、上下文多长。
- 预训练:几千张 GPU 训几周到几个月。工程难点是分布式并行(数据 / 张量 / 流水线)、断点恢复、loss 突刺。
- 后训练:SFT → 偏好对齐(RLHF / DPO)→ 可验证奖励的强化学习(数学、代码,推理模型靠这一步)。
- 评测:公开基准 + 自建评测集 + 安全红队。
- 推理部署:量化、推理引擎、上线。
成本粗算: 训练算力 ≈ 6 × 参数量 × 训练 token 数。7B 模型训 2T token ≈ 8×10²² FLOPs,按 H100 跑到峰值 40% 左右算约 6 万 GPU 小时,光算力十几万美元,还不算人力和失败重来。一线水平的大模型是几千万到上亿美元。
个人可行的两条路: 想学懂原理,跑 Karpathy 的 nanoGPT / nanochat,租几小时 GPU,把上面 1–6 步走一遍迷你版;想做出有用的东西,在 Qwen 这类开源模型上做 LoRA 微调(Unsloth、LLaMA-Factory),几千条领域数据、一张卡几小时就够。对 Agent 工程师来说,从零预训练基本用不上;会本地部署、会 LoRA 微调是加分项,Day 101 会把微调小模型接进网关做级联路由。
动手练习
今天写三个小脚本,都放在 agent-60-lab/w1/ 下。
练习 1:数 token。 不同模型分词器不同,最可靠的办法是直接看 API 返回的 usage。
# agent-60-lab/w1/count_tokens.py
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"])
MODEL = os.environ["LLM_MODEL"]
SAMPLES = {
"中文": "我的发票被重复扣款了,请帮我查一下是怎么回事,谢谢。",
"英文": "I was charged twice for my invoice. Could you check what happened? Thanks.",
"编号": "INV-2026-004417 INV-2026-004418 INV-2026-004419",
}
def prompt_tokens(text: str) -> int:
resp = client.chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": text}],
max_tokens=1, # 只关心输入算了多少 token,输出 1 个就停
extra_body={"thinking": {"type": "disabled"}}, # 非 DeepSeek 删掉这行
)
assert resp.usage is not None # SDK 把 usage 标成「可能为空」,先断言一下,编辑器就不报红线了
return resp.usage.prompt_tokens
baseline = prompt_tokens("a") # 消息格式本身也占 token,先测一个空壳的开销
for name, text in SAMPLES.items():
n = prompt_tokens(text) - baseline + 1
print(f"{name}: {len(text)} 个字符 ≈ {n} 个 token,每字符 {n / len(text):.2f}")
load_dotenv()和 Day 0 一样,把.env读进环境变量。client = OpenAI(整个脚本共用这一个客户端,下面每次调用都复用它。MODEL = os.environ["LLM_MODEL"]实测值是deepseek-flash。全大写的变量名是 Python 的约定,表示「常量,别改它」,语言本身不会拦。SAMPLES = {名字 → 文本 的 dict。len()数的是字符数,实测:中文那句 26 个字符(全角逗号句号也各算 1 个),英文 74 个,编号 47 个。def prompt_tokens(text: str) -> int:给一段文本,返回 API 算出来的输入 token 数。messages=[{"role": "user", "content": text}],每次只放一条用户消息,里面就是要测的文本。max_tokens=1,只让模型输出 1 个 token。实测每次completion_tokens=1,finish_reason是"length",意思是被长度上限截断了;模型实际吐出来的是'Hi'、'您好'、'I'这种半句话。我们本来就不看输出,能省则省。assert resp.usage is not None下面正文讲了:用来消除编辑器的红线。实跑时 usage 从来没有空过。return resp.usage.prompt_tokens只取输入部分的 token 数。baseline = prompt_tokens("a")实测baseline = 5。字母a本身占 1 个 token,另外 4 个是消息格式带的开销(角色标记、分隔符之类),每次请求都会有。for name, text in SAMPLES.items():.items()每次给出一个(名字, 文本)元组,for把它解包成两个变量。第一轮是name="中文",text="我的发票被重复扣款了,…"。n = prompt_tokens(text) - baseline + 1减掉 baseline 就去掉了格式开销,但 baseline 里还含着a的那 1 个 token,多减了,所以要+ 1加回来。实测三轮:
中文:API 返回 20 →20 - 5 + 1 = 16
英文:API 返回 20 →16
编号:API 返回 26 →22print(f"{name}: {len(text)} 个字符{n / len(text):.2f}是除完保留两位小数。实测输出:中文: 26 个字符 ≈ 16 个 token,每字符 0.62英文: 74 个字符 ≈ 16 个 token,每字符 0.22编号: 47 个字符 ≈ 22 个 token,每字符 0.47
中文 0.62 和文档说的 0.6 基本吻合;英文 0.22 比文档的 0.3 还少,因为这句全是常见短词,一个词常常就是一个 token;编号里的数字和连字符被切得很碎,47 个字符用了 22 个 token。同样长度,编号比英文贵一倍多。
cd agent-60-lab && uv run w1/count_tokens.py
如果你用 Zed、VS Code 这类带类型检查的编辑器,没有 assert 那行的话,prompt_tokens 下面会出现红色波浪线,提示 "prompt_tokens" is not a known attribute of "None"。原因是 openai SDK 把 usage 的类型写成了 CompletionUsage | None:协议允许服务端不返回用量,所以类型检查器要求你先排除 None。这只是静态检查的提示,不加 assert 照样能跑。加上它,一是消掉红线,二是万一真没返回用量,会在这一行直接报错,而不是在后面报一个看不懂的 AttributeError。后面的课凡是读 resp.usage 的地方,都是一样的道理。
输出是三行,每行给出字符数、估算的 token 数和比例。把结果和 DeepSeek 文档的 0.3 / 0.6 对照一下。如果你手上还有别家的 key,换一下 .env 再跑一遍,比较两家分词器的差别。
想直观看切分结果,可以打开 tiktokenizer,选一个模型,把上面的三段文字贴进去,每个 token 会用不同颜色标出来。注意它展示的是 OpenAI 系等模型的分词器,不是 DeepSeek 的,看个大意就行。
练习 2:temperature 实验。
# agent-60-lab/w1/temperature.py
import os
from collections import Counter
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"])
PROMPT = "给一家在线记账软件公司起一个中文名字,只输出名字本身,不要解释。"
for t in (0.0, 1.0, 1.5):
names = []
for _ in range(8):
resp = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=[{"role": "user", "content": PROMPT}],
temperature=t,
max_tokens=20,
extra_body={"thinking": {"type": "disabled"}},
)
names.append((resp.choices[0].message.content or "").strip())
print(f"temperature={t}: {len(set(names))} 种不同答案 -> {Counter(names).most_common(3)}")
from collections import CounterCounter是标准库里专门用来计数的 dict,Counter(["a", "b", "a"])得到{'a': 2, 'b': 1}。PROMPT = "给一家在线记账软件公司起一个中文名字要求「只输出名字」,这样每次的输出短而且好比较。for t in (0.0, 1.0, 1.5):外层循环:三档 temperature 各做一轮。names = []每一档开始前清空,只装这一档的 8 个答案。for _ in range(8):同一个问题问 8 次。循环变量用_是惯例写法,表示「这个变量我用不上」。temperature=t,这一档的温度。max_tokens=20,名字很短,20 个 token 足够,也防止模型不听话写一长段解释。names.append((resp.choices[0].message.content or "").strip()).strip()去掉前后的空格和换行,不然"轻账"和"轻账\n"会被当成两个不同的名字。前面的(… or "")和练习 1 里的assert是同一个道理:SDK 把content标成str | None,直接.strip()编辑器会画红线。or ""把 None 换成空字符串,红线消了,万一真返回 None 也不会崩。实跑 24 次 content 都有值。print(f"temperature={t}: {len(set(names))} 种不同答案set(names)去重后数个数就是「几种不同答案」;Counter(names).most_common(3)取出现次数最多的前三个,次数相同时按第一次出现的先后排。这次实跑:
t=0.0:8 次是轻账 ×5、简账 ×3→2 种不同答案 -> [('轻账', 5), ('简账', 3)]
t=1.0:5 种不同答案 -> [('轻账', 3), ('随手记', 2), ('简账通', 1)],另外还有「账轻松」「易账通」
t=1.5:6 种不同答案 -> [('随手记', 2), ('轻账', 2), ('云账通', 1)]
temperature=0 也出了两种名字,正好印证「0 也不保证完全一样」。
跑完你会看到:temperature 越高,8 次里出现的不同名字越多。temperature = 0 时多数情况下是同一个名字,偶尔会有一两个不同,这正好印证了上面说的「0 也不保证完全一样」。把观察记下来。
练习 3:制造一次幻觉。 问模型一个它不可能知道的问题,看它怎么答:
# agent-60-lab/w1/hallucination.py
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(base_url=os.environ["LLM_BASE_URL"], api_key=os.environ["LLM_API_KEY"])
QUESTIONS = [
"云簿 CloudLedger 公司的退款政策是什么?",
"云簿 CloudLedger 公司的退款政策是什么?如果你不确定,直接说不知道,不要猜。",
]
for q in QUESTIONS:
resp = client.chat.completions.create(
model=os.environ["LLM_MODEL"],
messages=[{"role": "user", "content": q}],
temperature=0,
extra_body={"thinking": {"type": "disabled"}},
)
print("问:", q)
print("答:", resp.choices[0].message.content, "\n")
QUESTIONS = [两个问题只差后半句。第二个多了「不确定就说不知道,不要猜」。for q in QUESTIONS:依次问两次,每次都是全新的一轮对话,第二问看不到第一问。temperature=0,尽量让输出稳定,两个问题的回答差异才能归到提示词上,而不是随机采样的运气。print("问:", q)print传多个参数时,中间自动用空格隔开。print("答:", resp.choices[0].message.content, "\n")最后的"\n"是为了在两组问答之间多空一行。这次实跑:
问题 1 的回答是「抱歉,我无法确认”云簿 CloudLedger”这家公司的退款政策。我的训练数据中不包含这家公司的可靠信息,因此不能凭空给出具体条款……」,后面还列了三条去官网、问客服的建议,一共 140 个输出 token。
问题 2 直接回答「我不知道。我的知识库里没有关于……的信息」,48 个 token。
可见这次的模型第一问就没有编。新模型在「明显查不到的公司」上越来越会拒答,但这靠的是训练,不是保证。换个更像真事的问题(比如问它某个真实小公司的退款天数),还是可能编出来。
比较两个回答。第一个可能编出一段像模像样的政策,或者给出「一般 SaaS 公司的做法」;也可能直接说查不到。我用 deepseek-flash 实跑时它直接拒答了,新模型对「明显查不到的公司」越来越谨慎,但这是训练出来的倾向,不是保证,换个更像真事的问题照样可能编。第二个加了一句「不确定就说不知道」,多数现代模型会老实承认。这说明提示词能缓解幻觉,但不能根治,后面我们会用 RAG 把真实政策送到它眼前。
验收标准: 三个脚本都跑通;能说出你用的模型下中文每字大约多少 token;能用自己的话解释练习 3 里第一个问题为什么有可能被「编」出来,以及第二个问题多的那句话起了什么作用。
名词解释
| 名词 | 意思 |
|---|---|
| Token | 模型处理文本的基本单位,可能是一个词、半个词、一个汉字或几个汉字 |
| 分词器(tokenizer) | 把文字切成 token、再把 token 转成编号的程序,每个模型有自己的 |
| BPE | 一种建词表的方法:反复合并语料中最常一起出现的相邻片段 |
| 下一个 token 预测 | 模型的唯一能力:给定前文,输出下一个 token 的概率分布 |
| 自回归生成 | 每生成一个 token 就接回输入,再预测下一个,循环往复 |
| 上下文窗口 | 一次调用里输入加输出能容纳的最大 token 数 |
| 幻觉(hallucination) | 模型生成流畅但不真实的内容 |
| temperature | 采样温度,越高输出越随机多样,越低越确定 |
| top_p | 只在累计概率前 p 的候选 token 中采样 |
| 预训练 / SFT / RLHF | 模型训练的三个阶段:学续写、学对话格式、学人类偏好 |
| 推理模型 / 思考模式 | 回答前先生成一段推理过程的模型或模式,思考 token 也计费 |
| reasoning_content | DeepSeek 返回思考过程的字段,与最终回答 content 分开 |
| Transformer | 现在大模型的基本结构,同一种层叠几十次,每层是注意力加前馈网络 |
| 注意力(attention) | 每个 token 给前面的 token 打相关度分,按分数把它们的信息加权取过来 |
| KV cache | 推理时把前面 token 的键、值向量存在显存里复用,上下文越长占得越多 |
| MoE(混合专家) | 每层有很多个「专家」小网络,路由器每次只选少数几个算;分总参数和激活参数 |
常见误区
以为模型「知道」事实。 它只知道什么样的文字常见。需要事实就给它工具或检索结果。
以为 temperature = 0 就是确定性的。 多数时候稳定,但不保证一字不差。测试和评测要允许合理的波动。
按字数估算成本。 计费按 token,中文、英文、代码、编号的比例都不一样。估算要用 usage 字段的真实数字。
以为窗口越大越好,什么都往里塞。 塞得越多越贵越慢,模型对中间内容的注意力还会下降。Agent 的上下文要精挑细选。
所有任务都开思考模式。 简单分类、抽取开了思考只会更慢更贵。按任务难度决定。
学习资料
文档与文章
- DeepSeek:Token 用量计算:中英文字符和 token 的换算口径。5 分钟。
- DeepSeek:思考模式:怎么开关思考、
reasoning_content字段、带工具时要回传思考内容的规则。必看,10 分钟。 - Claude 模型概览:看一下国外旗舰模型的上下文窗口、价格和思考模式说明,和 DeepSeek 对照。
- bbycroft.net/llm:一个 3D 可视化网页,能看到一个小 GPT 里数据怎么一层层流过去。不用看懂每个矩阵,感受一下「输入一串 token,输出下一个 token 的概率」这件事。20 分钟。
- Jay Alammar《The Illustrated GPT-2》:图解 GPT-2 怎么一个一个生成 token。只看前半部分。
B 站视频
- 卡帕西 3.5 小时深度讲解(上面正文里嵌的 BV16cNEeXEer):今天看前 1 小时。
- 【中英字幕】Andrej Karpathy 一小时大语言模型介绍(BV1kC4y1P7Lt):2023 年那场经典讲座的中字版,和 YouTube 上的原版是同一个内容,二选一看。
- 台大李宏毅:80 分钟快速了解大语言模型(BV1FM4m1Z7JZ,搬运合集):李宏毅讲课节奏慢、例子好,适合喜欢中文讲解的人。
- 3Blue1Brown 官方号《【官方双语】GPT是什么?直观解释Transformer | 深度学习第5章》(BV13z421U7cs,27 分钟):配合上面的「Transformer、注意力和 MoE」一节看,看完在官方号里接着看第 6 章讲注意力的那一集。
其他经典视频
- Andrej Karpathy《Let’s build the GPT Tokenizer》(YouTube ID zduSFxRajkE):2 小时手写 BPE 分词器,想彻底搞懂 token 的人看,不是必需。
书籍
- 《图解大模型:生成式 AI 原理与实战》(Jay Alammar、Maarten Grootendorst 著,人民邮电出版社,2025;英文原版《Hands-On Large Language Models》,O’Reilly,2024):读第 2 章(token 与嵌入)和第 3 章(Transformer 内部)的前半部分,图非常多,2 小时左右。
- 《从零构建大模型》(Sebastian Raschka 著,人民邮电出版社,2025;英文原版《Build a Large Language Model (From Scratch)》,Manning,2024):读第 2 章「处理文本数据」,讲分词和 BPE。想走得更深的人可以读,这门课不要求。
开源项目与代码
- karpathy/minbpe:最小的 BPE 实现,几百行,配合上面的 tokenizer 视频看。
- openai/tiktoken:OpenAI 的分词器库,本地数 token 用,W5 统计成本时可能用到。
- Tokenizer Playground:在线比较多种开源模型分词器的切分结果。
自测
- 用「下一个 token 预测」解释:为什么问模型一家它没听说过的公司的政策,它会编一段出来?
答案
模型的训练目标是生成在该上下文后面常见的文字。「某公司的退款政策是」后面接一段具体政策条款,在训练文本里非常常见,所以它会按这个模式流畅地续写,而不是去查证事实。它没有内置的事实数据库,也没有可靠的「不确定」机制。
- 你和模型聊了 10 轮,第 11 轮它还记得第 1 轮你说的名字。这是因为模型有记忆吗?
答案
不是。API 是无状态的,你的程序在第 11 次调用时把前 10 轮的全部消息重新发给了模型,名字就在这次的输入里。如果程序只发最后一轮,它就「忘了」。
- 一段工单文本 800 个汉字,按 DeepSeek 文档的经验比例大约多少 token?如果一天处理 2 万张这样的工单,只算这段文本的输入,用 deepseek-flash 高峰价多少钱?
答案
800 × 0.6 ≈ 480 token。2 万张 × 480 = 960 万 token。高峰输入价 2 元 / 百万 token,约 9.6 × 2 = 19.2 元。实际调用还有系统提示词、工具定义、检索结果,会比这个多很多。
- 做一个「判断工单属于哪个类别」的步骤,temperature 应该怎么设?为什么?
答案
设低,0 到 0.3。分类需要稳定可复现,同一张工单不应该今天分到「计费」明天分到「技术」。高 temperature 会增加随机性,没有任何好处。
- 面试题:面试官问「上下文窗口已经 1M token 了,为什么还需要 RAG?直接把整个知识库塞进去不行吗?」
答案
要点:一是成本和延迟,每次调用都带几十万 token,费用和响应时间成倍增加;二是效果,内容越多模型对中间部分的注意力越容易下降(lost in the middle / context rot),塞进去不等于用得好;三是规模和更新,真实知识库往往远超 1M,而且经常变;四是权限,不同用户能看的文档不同,检索时可以按权限过滤。所以更合理的做法是只把相关的少量内容检索出来放进上下文。可以补一句:知识库很小且固定时,直接塞进去配合 prompt caching 确实是可行的简化方案。
- 为什么
strawberry 里有几个 r这种问题对模型很难?Agent 里遇到类似的「字符级」任务该怎么办?
答案
模型看到的是 token 而不是字母,“strawberry” 可能被切成两三个 token,它没有直接看到每个字母。Agent 里遇到字符级、计算类任务,应该交给代码:给一个执行代码或字符串处理的工具,而不是让模型「看」出来。
- (面试题)DeepSeek-V3 总参数 671B、激活参数约 37B。为什么它的 API 能卖得便宜,你自己部署却仍然要多卡服务器?
答案
它是 MoE 模型,每个 token 只经过路由器选中的少数专家,计算量按约 37B 算,所以单 token 的算力成本和速度接近一个 37B 的稠密模型,厂商能便宜卖。但所有专家都可能被选到,671B 的权重必须全部放进显存(量化后也要几百 GB),所以部署门槛按总参数算。再加上长上下文的 KV cache 也占显存,单机跑完整版不现实。
明天预告
Day 2 · 写给后端工程师的 Python 速成:uv、类型标注、async、pydantic。用你熟悉的 Java、Go 或 JS 做对照,一天把写 Agent 需要的 Python 补上。不学全,只学够用的那一部分:项目怎么建、依赖怎么装、类型怎么标、异步怎么写、数据怎么校验、测试怎么跑。
评论