08-Chain of Thought使用边界
要点
- Chain of Thought(CoT)是让模型在输出最终答案之前,先输出一段推理过程
- CoT 能显著提升复杂推理任务的准确率,但也会增加 Token 消耗和延迟
- CoT 不是万能的——简单任务用 CoT 反而可能引入错误
- 两种实现方式:Prompt 里要求「逐步思考」和 API 层面的 thinking/reasoning 参数
- CoT 的输出通常不展示给用户——需要分离推理过程和最终答案
- 什么时候用 CoT、什么时候不用,需要按任务类型判断
1. 什么是 Chain of Thought
先看一个对比。任务:判断一段代码是否有 bug。
没有 CoT(直接回答)
输入:以下函数有没有问题?
function add(a, b) { return a + b }
输出:这段代码没有问题。
模型直接给出判断。如果代码很简单,这通常是对的。但如果代码有隐蔽的 bug,模型可能跳过思考直接回答,漏掉问题。
有 CoT(先推理再回答)
输入:以下函数有没有问题?请逐步分析。
function getUserDiscount(user) {
if (user.level = 'vip') {
return 0.2
}
return 0
}
输出:
让我逐步分析这段代码:
1. 函数目的:根据用户等级返回折扣
2. 第 2 行:`user.level = 'vip'` 使用了赋值运算符 `=`,而不是比较运算符 `===`
这意味着每次调用都会把 user.level 改成 'vip',而且条件永远为 true
3. 这是一个 bug
结论:代码有 bug,第 2 行应该用 `===` 而不是 `=`。
CoT 让模型先把思考过程写出来,再给出结论。模型在「写」推理的过程中,会更容易发现隐蔽的问题。
2. CoT 为什么有效
这个问题背后的解释比较复杂,简化来说有两点:
第一,CoT 把复杂问题拆成了步骤。 模型一次性回答「有没有 bug」需要同时检查语法、逻辑、类型、边界等多个维度。拆成步骤之后,每一步只需要关注一个维度,准确率更高。
第二,CoT 增加了计算的「时间」。 模型生成每个 token 时都在做计算。如果直接输出结论,模型只生成了几个 token 的计算量。如果输出推理过程,模型可能生成几百个 token,每一步都在做计算,相当于给问题分配了更多的计算资源。
但这两点也暗示了 CoT 的局限:如果问题本身不需要多步推理,CoT 只是在浪费 Token。
3. 什么时候用 CoT
CoT 不是所有任务都适合。按任务类型分:
| 任务类型 | 是否用 CoT | 原因 |
|---|---|---|
| 数学计算 | ✅ 适合 | 多步计算,容易出错 |
| 逻辑推理 | ✅ 适合 | 需要逐步推导 |
| 代码调试 | ✅ 适合 | 需要逐行分析 |
| 复杂决策 | ✅ 适合 | 需要权衡多个因素 |
| 简单分类 | ❌ 不需要 | 一步能判断的事,拆步骤反而引入噪声 |
| 格式转换 | ❌ 不需要 | 机械转换,不需要推理 |
| 信息提取 | ❌ 不需要 | 从文本中提取字段,不涉及推理 |
| 创意写作 | ❌ 不需要 | 创作过程不适合结构化推理 |
一个判断标准
问自己:如果让一个工程师做这个任务,他需要「想一想」吗?
- 如果答案是「需要想一想」→ 用 CoT
- 如果答案是「一眼就能看出来」→ 不用 CoT
4. CoT 的两种实现方式
方式一:Prompt 里要求逐步思考
const prompt = `
检查以下代码是否有 bug。
请先逐步分析每一行代码,列出可能的问题,最后给出结论。
分析过程:
`最简单的方式。模型会在输出里包含推理过程和最终结论。
问题:推理过程和结论混在一起,下游代码需要从中提取结论。
// 模型输出
const output = `
让我逐步分析:
1. 第 1 行:函数定义,没问题
2. 第 2 行:user.level = 'vip' 使用了赋值运算符,应该用 ===
...
结论:代码有 bug,第 2 行应改为 ===
`
// 需要从输出中提取结论
const conclusion = output.match(/结论:(.+)/)?.[1]方式二:分离推理和结论
在 Prompt 里要求模型用特定格式输出推理和结论:
const prompt = `
检查以下代码是否有 bug。
按以下格式输出:
<thinking>
在这里写出你的逐步分析过程。
</thinking>
<answer>
在这里给出最终结论。如果有 bug,说明是哪一行、什么问题、怎么修改。
如果没有 bug,输出「代码无问题」。
</answer>
`模型输出:
<thinking>
1. 第 1 行:函数定义,参数 user 无类型标注,但不算 bug
2. 第 2 行:user.level = 'vip' 使用了赋值运算符 =,而不是比较运算符 ===
这会导致两个问题:
a. 条件永远为 true(赋值表达式的返回值是被赋予的值,非空字符串为 truthy)
b. 每次调用都会修改 user.level
</thinking>
<answer>
代码有 bug。
第 2 行:`user.level = 'vip'` 应改为 `user.level === 'vip'`
</answer>下游代码只需要提取 <answer> 里的内容:
const answer = output.match(/<answer>([\s\S]*?)<\/answer>/)?.[1]?.trim()推理过程(<thinking>)可以丢弃,也可以记录到日志里供后续分析。
方式三:API 层面的 thinking 参数
Anthropic 的 Claude 和部分 OpenAI 模型支持 API 层面的 thinking/reasoning 参数。推理过程在 API 内部完成,不占用户可见的输出:
const response = await callModelAPI({
model: 'claude-sonnet-4-5',
messages: [
{ role: 'user', content: '检查这段代码是否有 bug...' },
],
thinking: {
type: 'enabled',
budget_tokens: 2000, // 给模型分配 2000 token 的思考预算
},
})
// response 里有 thinking 字段(推理过程)和 content 字段(最终回答)
// 可以只展示 content 给用户这种方式的好处:
- 推理过程对用户不可见——不需要用 XML 标签分隔
- 推理不占输出 Token——有独立的预算
- 更自然——模型不需要被要求「用某种格式输出推理」
代价是需要模型支持 thinking 参数,而且 thinking 的 Token 也要计费。
5. CoT 的成本控制
CoT 会增加 Token 消耗。推理过程越长,消耗越大。
成本对比
// 没有 CoT
const inputTokens = 500
const outputTokens = 50 // 直接给结论
const totalCost = calculateCost(inputTokens, outputTokens)
// 有 CoT
const inputTokens = 500
const outputTokens = 500 // 推理过程 450 + 结论 50
const totalCost = calculateCost(inputTokens, outputTokens) // 贵了 10 倍控制策略
策略一:限制推理长度
const prompt = `
请简要分析这段代码,用 3-5 句话说明分析过程和结论。
`策略二:只对复杂任务启用 CoT
// src/services/prompt/cot-router.ts
function shouldUseCoT(task: string, input: string): boolean {
// 简单任务不用 CoT
if (input.length < 50) return false
// 分类、提取、格式化不用 CoT
const simpleTasks = ['classify', 'extract', 'format', 'translate']
if (simpleTasks.includes(task)) return false
// 调试、推理、决策用 CoT
const complexTasks = ['debug', 'analyze', 'reason', 'decide']
if (complexTasks.includes(task)) return true
return false
}策略三:用小模型做初筛,大模型做推理
// 第一步:用小模型快速判断是否需要深入分析
const quickCheck = await callModelAPI({
model: 'gpt-4o-mini', // 小模型,便宜
messages: [
{ role: 'system', content: '判断以下代码是否可能包含 bug。只需回答「可能」或「不太可能」。' },
{ role: 'user', content: code },
],
temperature: 0,
max_tokens: 10,
})
// 第二步:如果小模型认为可能有问题,用大模型做详细分析
if (quickCheck.choices[0].message.content.includes('可能')) {
const detailed = await callModelAPI({
model: 'gpt-4o', // 大模型,贵
messages: [
{ role: 'system', content: '详细分析这段代码的 bug,逐步推理。' },
{ role: 'user', content: code },
],
temperature: 0,
})
}6. CoT 的常见陷阱
陷阱一:简单任务也用 CoT
// ❌ 简单的分类任务不需要 CoT
const prompt = `
判断以下邮件是「工作」还是「个人」。请逐步分析邮件的主题、发件人、内容后给出分类。
`
// ✅ 简单任务直接回答
const prompt = `
判断以下邮件是「工作」还是「个人」。只输出分类结果。
`简单任务用 CoT,模型可能在推理过程中「想太多」,反而给出不稳定的答案。
陷阱二:CoT 的推理过程有错但结论对了
<thinking>
1. 第 2 行使用了 let,但没有被重新赋值,应该用 const
2. 第 5 行的循环条件 i < arr.length 是正确的
3. 第 8 行有一个拼写错误
</thinking>
<answer>
代码有两个问题:第 2 行应该用 const,第 8 行有拼写错误。
</answer>
推理过程中有些判断是错的(比如第 5 行其实没问题),但结论碰巧是对的。这种情况很难发现——因为结论是对的,你可能不会去检查推理过程。
如果推理过程对用户可见,错误的推理可能会误导用户。
陷阱三:CoT 和 Structured Output 冲突
// ❌ CoT 和 JSON Schema 同时要求
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{ role: 'system', content: '逐步分析后给出结论。' },
{ role: 'user', content: '...' },
],
response_format: { type: 'json_schema', schema: { /* ... */ } },
})Structured Output 要求输出严格符合 JSON Schema,但 CoT 要求输出推理过程——两者冲突。模型要么输出不符合 Schema 的推理文本,要么跳过推理直接输出 JSON。
解决方案:用 thinking 参数(如果模型支持),或者不用 Structured Output,改用正则提取结论。
7. 一个完整的 CoT 示例
// src/services/prompt/bug-analyzer.ts
const BUG_ANALYZER_PROMPT = `你是一个 TypeScript 代码调试助手。
## 任务
分析用户提交的代码,找出其中的 bug。
## 分析步骤
1. 逐行阅读代码,理解每行的意图
2. 检查每行是否有语法错误、类型错误、逻辑错误
3. 检查函数之间的数据流是否有问题
4. 总结发现的问题
## 输出格式
<thinking>
在这里写出你的逐步分析。
</thinking>
<answer>
{
"hasBug": true/false,
"bugs": [
{
"line": number,
"type": "syntax" | "type" | "logic",
"description": string,
"fix": string
}
]
}
</answer>
`
export async function analyzeBugs(code: string) {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{ role: 'system', content: BUG_ANALYZER_PROMPT },
{ role: 'user', content: code },
],
temperature: 0,
})
const output = response.choices[0].message.content
// 提取 thinking 和 answer
const thinking = output.match(/<thinking>([\s\S]*?)<\/thinking>/)?.[1]?.trim() ?? ''
const answerRaw = output.match(/<answer>([\s\S]*?)<\/answer>/)?.[1]?.trim() ?? '{}'
let answer
try {
answer = JSON.parse(answerRaw)
} catch {
answer = { hasBug: false, bugs: [] }
}
return {
thinking, // 可以记录到日志,也可以丢弃
...answer,
}
}总结
回顾这一节的要点:
- CoT 让模型先推理再回答,能提升复杂任务的准确率
- CoT 适合多步推理、逻辑推导、代码调试;不适合简单分类、格式转换、信息提取
- 三种实现方式:Prompt 要求、XML 标签分隔推理和结论、API 层面的 thinking 参数
- CoT 会增加 Token 消耗和延迟,需要对简单任务关闭
- 常见陷阱:简单任务也用 CoT、推理过程有错但结论碰巧对了、和 Structured Output 冲突
- 判断标准:如果工程师做这个任务需要「想一想」,就用 CoT
下一篇讲结构化 Prompt 模板——怎么把 Prompt 做成可复用的工程组件。