推理模型,让模型先想再答

outlier 15 分钟 5859 字 0 0
系列教程Agent从入门到放弃 第 13 / 19 章
  1. 01Agent 从入门到放弃:教程大纲
  2. 02①大模型、Chatbot、Agent,一次把名字理清楚
  3. 03②Agent 的最小定义,一个 while 循环
  4. 04③Agent 简史,从 ReAct 到 Claude Code
  5. 05④Workflow 还是 Agent?大部分场景你不需要 Agent
  6. 06Agent 能力地图,为什么编码 Agent 最先成熟
  7. 07Token,模型眼里的世界
  8. 08Next-token prediction,一个字一个字地猜
  9. 09采样,temperature、top-p 与"同题不同答"
  10. 10Transformer 与 Attention,不推公式的直觉版
  11. 11Context Window,模型的内存条
  12. 12训练三阶段,能力是怎么被"装"进去的
  13. 13推理模型,让模型先想再答
  14. 14用原理解释怪行为,原理篇结业考
  15. 15从 Prompt 工程到 Context 工程
  16. 16System Prompt 写作,角色、约束与示例
  17. 17Context Rot,为什么长对话越聊越笨
  18. 18项目记忆,CLAUDE.md 与 AGENTS.md
  19. 19检索,RAG 还是 Agentic Search
查看教程目录

开场:问它 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(测试时计算,也有人叫推理时计算)。

flowchart TB subgraph T["训练时花算力"] T1["更大的模型"] --> T3["能力更强
但每次回答的成本也跟着涨"] 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;
  • 用验证器挑:如果答案能自动验证(代码能跑测试),那就更简单了,生成几个,哪个通过了测试就用哪个。
flowchart LR Q["一道难题"] --> L["想得更久
一条很长的思考链"] 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 篇讲了三个基本的训练阶段,推理模型在此之上多了关键的一步:用有标准答案的任务做强化学习。

flowchart LR Q["一道有标准答案的题
数学题、编程题"] --> 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.Thinking

3.4 交错思考:每一圈都在想

把这条规定和 Agent 的循环放在一起看,你会发现推理模型在 Agent 里的工作方式,和在聊天里不太一样。

聊天里,它想一次,答一次。Agent 里,每一圈都会先想一想,再决定调用什么工具:第 1 圈想"先跑测试看看",调用 run_bash;拿到结果后,第 2 圈想"报错说折扣算错了,去看 order.go",调用 read_file;再拿到结果,第 3 圈想"公式写反了",调用 edit_file……思考和工具调用交替出现,这种模式常被叫做交错思考(interleaved thinking)。

sequenceDiagram participant M as 模型 participant H as Harness Note over M: 思考:先跑测试 M->>H: run_bash H-->>M: FAIL Note over M: 思考:报错指向折扣计算 M->>H: read_file(order.go) H-->>M: 代码 Note over M: 思考:公式写反了 M->>H: edit_file

这其实就是第 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 的每一圈,难度是不一样的:

  • 刚拿到任务、需要理解问题和制定计划的那一圈,值得多想;
  • 中间"读一个文件""跑一下测试"这种机械的步骤,不需要想太多;
  • 测试失败、需要分析原因的那一圈,又值得多想。
flowchart LR A["理解任务、定计划
多想"] --> 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 后,表格的每一行都有意义了。重点看:

  1. 关掉思考(off)时答对了吗? 这道题需要枚举和排除,不思考的话很容易数错;
  2. low、high、max 之间:耗时和输出 token 差了多少倍?对错有没有区别?这道题对 high 来说可能已经足够,那 max 多花的钱就是浪费;
  3. 加上 -show:程序会把每一档思考的前 200 个字打印出来,看看它是怎么拆解问题的,有没有出现"等一下""我再检查一下"这种自我纠正。

把结果记下来,你就有了一张"这道题值得开到哪一档"的实测表。换一道你自己工作中的难题再跑一次,表可能完全不一样。

动手作业

  1. 换一道简单的题,比如"把 hello world 翻译成中文",用 -efforts low,max 跑一次,看看 max 档会不会"想多了";
  2. 换一道更难的题(比如再加两个约束条件,自己先写个穷举程序算出标准答案),看看 low 档会不会开始答错;
  3. 用 -model deepseek-v4-pro 跑一次,对比一下更强的模型在同一档位下的表现和价格。
  4. 对照 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 里还会占上下文,强度要和任务难度匹配。

flowchart LR Q["问题"] --> T["思考
草稿纸:拆解、尝试、检查、回溯"] T --> A["正式回答"] T -.->|"按输出计费
要花时间"| C["代价"] style T fill:#fff3e0,stroke:#e65100 style C fill:#ffcdd2,stroke:#b71c1c

评论

还没有评论,来说点什么吧。

发表评论

Ctrl + Enter 快速提交
Enter 搜索Esc 关闭