开场:问它 1 + 1,它想了八秒
第一次用推理模型的时候,我先问了个简单的:"1 + 1 等于几?"
界面上出现了一行灰色的小字"正在思考……",然后等了大概八秒。展开它的思考过程一看,洋洋洒洒好几段:先确认这是十进制的加法,再考虑有没有可能是二进制的陷阱题,又想了想用户是不是在测试它……最后得出结论:2。
我有点哭笑不得。紧接着我给了它一道我自己都要拿草稿纸算的逻辑题:五个人排队拍照,有四个约束条件,问一共有多少种站法。它又"思考"了二十多秒,然后给出了正确答案,思考过程里还能看到它中途发现自己数漏了一种情况,回头改了过来。
同一个"先想再答",用在简单题上是浪费,用在难题上是救命。 这一篇讲推理模型:它是什么,怎么训练出来的,在 API 里长什么样,以及最实际的问题:什么时候该让它多想,什么时候该让它闭嘴直接答。读完本篇,你应该能回答这几个问题:
- 什么是 test-time compute?"想得久"为什么能让答案更好?
- 推理模型是怎么训练出来的?为什么在数学和代码上效果最明显?
- 思考在 API 里长什么样?DeepSeek 的思考模式怎么开、怎么关、怎么调强度?
- 思考的代价有哪些?
- 写 Agent 时,什么时候该开思考、开多深?
一、Test-time compute:答题时多花点力气
1.1 回到"没有草稿纸"
第 07 篇讲过一个关键结论:模型没有草稿纸,它写出来的就是它的思考过程。 每个 token 能花的计算量是固定的,要让模型"多想一会儿",唯一的办法就是让它多写一些 token,把中间步骤写出来。
那一篇我们用 prompt 来做这件事:"先一步一步写出计算过程,最后一行写答案。"推理模型做的事情本质上一样,只是把这个习惯训练进了模型里:在给出正式回答之前,它会自动生成一段"思考",这段思考是它的草稿纸。
1.2 两种花算力的方式
以前提升模型能力,主要靠训练时多花算力:更大的模型、更多的数据、更长的训练。推理模型开辟了另一条路:在回答问题时多花算力,也就是多生成一些思考 token。这叫 test-time compute(测试时计算,也有人叫推理时计算)。
但每次回答的成本也跟着涨"] T2["更多的数据"] --> T3 end subgraph I["回答时花算力"] I1["简单题:少想"] --> I3["按需花钱
难题多想,简单题少想"] I2["难题:多想"] --> I3 end style T3 fill:#e3f2fd,stroke:#1565c0 style I3 fill:#fff3e0,stroke:#e65100
后一种方式的好处是灵活:同一个模型,遇到简单问题少想几句,遇到难题多想一会儿。理想情况下,花在每道题上的算力和题目的难度相匹配。
1.3 思考里都在干什么
如果你看过推理模型的思考过程,会发现它和人在草稿纸上做题很像:
- 拆解问题:"这道题有四个约束,先看哪个最强";
- 尝试和枚举:"假设 A 在第二位,那么……";
- 自我检查:"等一下,我好像漏了一种情况";
- 回溯:"这条路走不通,换个思路";
- 最后整理:"综上,一共 5 种"。
这几种动作,和你自己在草稿纸上解一道难题时几乎一模一样。最后两条特别关键。第 07 篇说过,已经写出来的 token 收不回,"先答后想"的模型只能顺着错误往下编。但推理模型的思考是一块可以写错的草稿纸:写错了没关系,它可以在后面写"不对,重新算",最后交给你的正式回答,是整理过的结论。
1.4 另一种花算力的方式:多想几遍
"想得更久"不是回答时多花算力的唯一办法。还有一种更朴素的:同一个问题,独立地想好几遍,再从几个答案里挑。
- 投票:让模型独立解同一道题五次,取出现最多的那个答案。第 04 篇的并行投票就是这个思路,学术上叫 self-consistency;
- 挑最好的:生成好几个候选,再用另一个模型或者一段程序来打分,选分最高的那个,叫 best-of-N;
- 用验证器挑:如果答案能自动验证(代码能跑测试),那就更简单了,生成几个,哪个通过了测试就用哪个。
一条很长的思考链"] Q --> W["想好几遍
几条独立的思考链"] W --> V["投票 / 打分 / 跑测试
挑一个"] style L fill:#fff3e0,stroke:#e65100 style W fill:#e3f2fd,stroke:#1565c0
两种方式可以叠加。它们各有所长:想得更久,擅长那种"一步错了要回头改"的题;想好几遍,擅长那种"每次都有一定概率做对,但不知道哪次对"的题。第 08 篇的 pass@k 说的就是后者:只要你能认出哪次是对的,多试几次本身就是一种能力。
在 Agent 里,"想好几遍"的成本要高得多,因为每一遍都是一整个多轮的任务。但它确实有人在用:第 34 篇会讲用 git worktree 并行跑几个 Agent 做同一个任务,最后挑一个最好的结果。
二、推理模型是怎么训练出来的
2.1 用可验证的奖励做强化学习
第 11 篇讲了三个基本的训练阶段,推理模型在此之上多了关键的一步:用有标准答案的任务做强化学习。
数学题、编程题"] --> M["模型写思考 + 答案"] M --> V{"自动判分
答案对吗?测试过了吗?"} V -->|"对"| R1["奖励:
这种思考方式多来点"] V -->|"错"| R2["惩罚:
这种思考方式少来点"] R1 & R2 --> M style V fill:#fff9c4 style R1 fill:#e8f5e9,stroke:#2e7d32 style R2 fill:#ffcdd2,stroke:#b71c1c
数学题可以直接对答案,编程题可以跑测试,这些都能自动地、大规模地判分,不需要人来看。让模型做海量这样的题,做对了就强化它那次的"想法",做错了就弱化。
神奇的是,不需要人去教它"要自我检查""要回溯",这些行为会在训练中自己冒出来,因为它们能提高答对的概率。2025 年初 DeepSeek 发布 R1 时,论文里就描述了这样的现象:只用强化学习,模型的思考过程会越来越长,还会自发地出现反思和重新检查的行为。
2.2 为什么数学和代码进步最快
答案就在"可验证"三个字上。数学和代码有客观的对错标准,奖励信号清晰、便宜、可以无限量地生成。而"写一篇好文章""给出一个好建议",什么是好?很难自动判断,奖励信号就模糊得多。
这和第 05 篇的结论是一回事:结果可验证的地方,Agent 最先成熟;结果可验证的地方,模型也进步得最快。 两件事背后是同一个原因。
三、在 API 里长什么样
3.1 思考是一种单独的输出
推理模型的输出分成两部分:思考,和正式回答。不同厂商的表示方式不一样:
- 有的用单独的
thinking内容块,有的只返回思考的摘要,有的干脆不返回; - DeepSeek 用的是一个和
content并列的字段:reasoning_content,里面是完整的思考过程。
配套仓库统一用 thinking 内容块来表示。DeepSeek 客户端在 internal/llm/deepseek_format.go 里做了翻译:
// fromDeepSeek 把一条 DeepSeek 助手消息翻译回内容块:思考在前,文字其次,工具调用最后。
func fromDeepSeek(msg dsMessage) []ContentBlock {
var blocks []ContentBlock
if msg.ReasoningContent != "" {
blocks = append(blocks, ContentBlock{Type: "thinking", Thinking: msg.ReasoningContent})
}
// ……文字、工具调用
}3.2 DeepSeek 的思考模式
DeepSeek 的做法(截至写作时,以官方文档为准):
- 思考默认是开着的。不想让它思考,要显式地在请求里写
"thinking": {"type": "disabled"}; - 思考的强度用
reasoning_effort控制。文档里列了很多档位名,但实际只有三档:low、high、max。minimal会被当成low,medium和xhigh会被当成high。默认是high; - 思考模式下
temperature不起作用,第 08 篇讲过; - 思考模式下不支持强制调用某个工具,
tool_choice设成required或指定函数会报错; max_tokens的默认值会变:不思考时默认 8K,思考时默认 64K,max档位默认 128K。思考的 token 也算在输出里,所以要给足空间。
配套代码把这些翻译成了请求字段:
// 思考:DeepSeek 默认开着。thinking.type=disabled 关掉;effort 对应 reasoning_effort(low / high / max)。
if req.Thinking != nil {
t := "enabled"
if req.Thinking.Type == "disabled" {
t = "disabled"
}
out.Thinking = &Thinking{Type: t}
}
if req.OutputConfig != nil && req.OutputConfig.Effort != "" && (req.Thinking == nil || req.Thinking.Type != "disabled") {
out.ReasoningEffort = req.OutputConfig.Effort
}3.3 一个对 Agent 很重要的规定
DeepSeek 还有一条规定,写 Agent 时一定会遇到:在带工具的多轮对话里,之前每一轮的 reasoning_content 都必须原样传回去,否则直接返回 400 错误。不带工具的普通对话,传不传都行,传了它也会忽略。
为什么?可以这样理解:Agent 的每一步工具调用,都是在上一步思考的基础上做出的决定。如果把思考删掉,模型看到的就是一串"没头没尾"的工具调用,和它训练时见过的对话格式对不上(第 11 篇说过,格式要和训练时一致)。
所以配套仓库在把助手消息翻译回 DeepSeek 格式时,会把 thinking 块原样放回 reasoning_content:
case "thinking":
msg.ReasoningContent += b.Thinking3.4 交错思考:每一圈都在想
把这条规定和 Agent 的循环放在一起看,你会发现推理模型在 Agent 里的工作方式,和在聊天里不太一样。
聊天里,它想一次,答一次。Agent 里,每一圈都会先想一想,再决定调用什么工具:第 1 圈想"先跑测试看看",调用 run_bash;拿到结果后,第 2 圈想"报错说折扣算错了,去看 order.go",调用 read_file;再拿到结果,第 3 圈想"公式写反了",调用 edit_file……思考和工具调用交替出现,这种模式常被叫做交错思考(interleaved thinking)。
这其实就是第 03 篇 ReAct 的 Thought、Action、Observation,只不过 Thought 从 prompt 里约定的一行文字,变成了模型训练出来的思考块。也正因为每一步的决定都建立在前面的思考上,前面的思考才必须原样传回去。
这条规定有一个副作用:思考内容会留在历史里,跟着每一圈一起发送。 推理模型的思考可能很长,这会让 Agent 的上下文涨得更快。第 28 篇做上下文管理时,这是需要考虑的一个因素。
四、思考的代价
4.1 三笔账
思考不是免费的:
| 代价 | 说明 |
|---|---|
| 钱 | 思考 token 按输出计费,而输出是最贵的。DeepSeek deepseek-flash 高峰价每百万输出 token $1.2 |
| 时间 | 思考要一个 token 一个 token 地生成(第 07 篇),而且要等它想完,你才能看到正式回答 |
| 上下文 | 在 Agent 里,思考内容会留在历史中,占用上下文窗口 |
算一笔小账:如果一次调用思考了 2000 个 token,按 deepseek-flash 的高峰价是 0.0024 美元。看起来很少。但一个 Agent 任务转 50 圈,每圈都想 2000 个 token,就是 10 万个输出 token;如果是 deepseek-v4-pro,单价是 flash 的三倍多。再乘以每天跑的任务数,就不是小数目了。
4.2 想得多不一定想得对
更重要的是:多想不保证更对。
- 简单的问题,想多了反而会出错:开场那个"1 + 1"还好,有时候它会把一个简单的问题想复杂,给出一个"考虑周全"但偏离了原意的回答;
- 思考也可能跑偏:它可能在一个错误的假设上越想越深,最后很有信心地给出一个错误的答案;
- 思考过程不一定忠实:第 07 篇说过,模型写出来的解释不一定是它得出答案的真实原因,思考过程也是一样。别把它当成"模型真实想法"的录像。
4.3 开了思考,prompt 还要写"一步一步想"吗
一个常见的疑问:既然模型自己会想,prompt 里那句"请一步一步思考"还有没有用?
大多数情况下,没什么用了,甚至可能有点碍事:
- 模型已经被训练成会自己拆解问题,你再要求一遍,它可能会在正式回答里又把推理过程写一遍,输出变长,信息却没变多;
- 你在 prompt 里规定了一套很死的思考步骤("第一步分析需求,第二步列出方案,第三步……"),反而可能限制它,让它没法按这道题本身的特点去想。
更有用的做法是,告诉它想什么,而不是怎么想:
- 给它判断标准:"这个方案要同时满足性能和可维护性,冲突时优先可维护性";
- 给它需要考虑的边界:"注意空输入、并发调用和时区";
- 给它自检的办法:"写完之后,用这三个例子在思考里验算一遍"。
这些内容会进入它的思考,影响它想的方向,而不是替它想。第 15 篇写 system prompt 时,你会看到同一个原则:给原则和标准,别给死板的步骤。
五、什么时候该开、开多深
5.1 一张对照表
| 任务 | 建议 |
|---|---|
| 简单问答、改写、翻译、格式转换 | 关掉思考,或者用最低档 |
| 分类、抽取信息 | 关掉或低档 |
| 多步数学、逻辑推理、约束求解 | 开,高档 |
| 写复杂的代码、定位疑难 bug | 开,高档或最高档 |
| 制定计划、拆解一个大任务 | 开 |
| 按格式调用一个工具、读一个文件 | 低档就够 |
一条原则:思考的强度应该和任务的难度相匹配。 简单的任务开高档,只是又慢又贵;难的任务不开,可能根本做不对。
5.2 在 Agent 里怎么用
Agent 的每一圈,难度是不一样的:
- 刚拿到任务、需要理解问题和制定计划的那一圈,值得多想;
- 中间"读一个文件""跑一下测试"这种机械的步骤,不需要想太多;
- 测试失败、需要分析原因的那一圈,又值得多想。
多想"] --> B["读文件、跑命令
少想"] B --> C{"测试过了吗?"} C -->|"没过:分析原因
多想"| B C -->|"过了"| D["收尾
少想"] style A fill:#fff3e0,stroke:#e65100 style C fill:#fff3e0,stroke:#e65100 style B fill:#e8f5e9,stroke:#2e7d32 style D fill:#e8f5e9,stroke:#2e7d32
现在很多模型的"自适应思考"就是这个思路:让模型自己判断这一步要不要多想。另一个思路是模型路由:规划和拍板交给更强的模型,打杂的子任务交给便宜的模型。比如用 deepseek-v4-pro 做主 Agent,deepseek-flash 做读文件的子 Agent。第 37 篇会在 mini-agent 里实现这个功能。
mini-agent 自己在请求里没有设 effort,用的是 DeepSeek 的默认值 high。这是一个偷懒但合理的默认:编码任务里难的那几圈确实需要想,而 DeepSeek 的模型在简单的步骤上,本来也不会想太久。如果你发现账单里输出 token 占比特别高,再考虑两个方向:一是用 -sub-model deepseek-flash 把读文件、搜代码这种杂活交给子 Agent;二是自己改代码,在"刚拿到任务"和"测试失败"这两种时刻用高档,其余时刻用低档。改完之后用第 36 篇的评测集跑一跑,看通过率有没有掉。
六、动手:同一道题,不同的思考强度
实验:五个人排队拍照
配套代码 examples/ch12-effort/main.go 准备了一道逻辑题:
A、B、C、D、E 五个人排成一排拍照,满足:
1. A 不站在两端;
2. B 紧挨在 C 的左边;
3. D 站在 E 的右边(不一定相邻);
4. C 不站在正中间。
一共有多少种站法?最后一行只写「答案:<数字>」。这道题人来做,认真一点也要几分钟:要么一个个枚举,要么先把 B、C 绑在一起再排除。标准答案是 5。这个答案不是我拍脑袋写的,puzzle_test.go 里用穷举把 120 种排列全部检查了一遍:
go test ./examples/ch12-effort标准答案本身也要验证。 这个习惯在第 35、36 篇做评测时会救你很多次。
然后在不同的 effort 下各问一次,记录耗时、输出 token、费用和对错:
go run ./examples/ch12-effort -off -show不设 Key 时用的是离线演示模型,它会按 effort 写出长短不同的"思考过程",输出 token 和费用会随之增长,但它不会真的解题,所以"结果"一栏全是错的(也可以在 Playground 的第 12 篇里跑):
模型 demo-1
effort 耗时 输出token 费用($) 结果
off 0.0s 28 0.0005 ✗ 错
low 0.0s 80 0.0010 ✗ 错
high 0.0s 148 0.0017 ✗ 错
max 0.0s 222 0.0024 ✗ 错能看出来的只有一件事:effort 越高,输出 token 越多,费用越高。这一点在真实模型上同样成立,而且差距会大得多。
这个实验要接真实模型才有意义,在线 Playground 演示不了:先export DEEPSEEK_API_KEY=sk-...再跑。默认跑 low、high、max 三档,加上-off一共 4 次调用。
export DEEPSEEK_API_KEY=sk-...
go run ./examples/ch12-effort -off接上 DeepSeek 后,表格的每一行都有意义了。重点看:
- 关掉思考(off)时答对了吗? 这道题需要枚举和排除,不思考的话很容易数错;
- low、high、max 之间:耗时和输出 token 差了多少倍?对错有没有区别?这道题对 high 来说可能已经足够,那 max 多花的钱就是浪费;
- 加上
-show:程序会把每一档思考的前 200 个字打印出来,看看它是怎么拆解问题的,有没有出现"等一下""我再检查一下"这种自我纠正。
把结果记下来,你就有了一张"这道题值得开到哪一档"的实测表。换一道你自己工作中的难题再跑一次,表可能完全不一样。
动手作业
- 换一道简单的题,比如"把 hello world 翻译成中文",用
-efforts low,max跑一次,看看 max 档会不会"想多了"; - 换一道更难的题(比如再加两个约束条件,自己先写个穷举程序算出标准答案),看看 low 档会不会开始答错;
- 用
-model deepseek-v4-pro跑一次,对比一下更强的模型在同一档位下的表现和价格。 - 对照 1.4 节,用
-efforts low,low,low,low,low让低档连跑五次,再用-efforts high跑一次,看看"低档想五遍、取多数"和"高档想一遍"相比,哪个更准、哪个更便宜。五次的答案如果各不相同,本身就说明这道题对低档来说太难了。
七、踩坑记录:推理模型的七个坑
坑 1:所有任务都开最高档。
思考是按输出计费的,最贵的那种 token。简单任务开高档,钱花了,时间花了,效果还可能更差。先用低档试,不够再往上加。
坑 2:max_tokens 给少了,思考把答案挤掉了。
思考的 token 也算在 max_tokens 里。给了 1024 个 token 的上限,模型想了 1000 个 token,正式回答只剩 24 个,甚至一个字都没有,stop_reason 是 max_tokens。我在第 10 篇的大海捞针实验里就踩过这个坑,后来干脆在那个实验里关掉了思考。开思考时,max_tokens 要给足。
坑 3:在思考模式下调 temperature。
DeepSeek 的思考模式下,temperature 不起作用,也不报错,你会以为它生效了。第 08 篇的实验专门关掉了思考,就是这个原因。
坑 4:带工具的对话里把思考内容删了。
有人觉得思考内容太长,回传之前把它删掉,省点 token。DeepSeek 会直接返回 400。即使不报错的模型,删掉思考也可能让它的后续决策变差。要省上下文,就用第 28 篇的压缩,整段处理。
坑 5:把思考过程当成"真实想法"展示给用户。
思考是草稿纸,里面有试错、有绕路、有被推翻的假设,也不一定忠实地反映它最终的决策依据。给用户看可以,但要标明它是"思考过程",不是"结论",更不是"证据"。
坑 6:只看一次结果就下结论。
"开了 max 档就答对了,low 档就错了",这可能只是运气。第 08 篇说过,模型的输出有随机性。要比较不同档位的效果,同一道题每档至少跑几次,看正确率。第 36 篇讲评测时会把这件事做得更系统。
坑 7:在 prompt 里规定思考的每一步。
4.3 节说过,推理模型自己会拆解问题。你写一套"第一步……第二步……"的死板流程,它要么照着走、错过这道题真正的难点,要么在正式回答里把这些步骤又复述一遍。告诉它标准、边界和自检的办法,然后让它自己想。
一句话总结
推理模型把"先想再答"训练进了模型:它在正式回答之前先在草稿纸上思考,用回答时的算力换答案的质量;这种能力是靠可自动验证的任务强化学习练出来的,所以在数学和代码上最强;思考按输出计费、要花时间,在 Agent 里还会占上下文,强度要和任务难度匹配。
草稿纸:拆解、尝试、检查、回溯"] T --> A["正式回答"] T -.->|"按输出计费
要花时间"| C["代价"] style T fill:#fff3e0,stroke:#e65100 style C fill:#ffcdd2,stroke:#b71c1c
评论
还没有评论,来说点什么吧。