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>

下游代码只需要提取 &lt;answer&gt; 里的内容:

const answer = output.match(/<answer>([\s\S]*?)<\/answer>/)?.[1]?.trim()

推理过程(&lt;thinking&gt;)可以丢弃,也可以记录到日志里供后续分析。

方式三: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 给用户

这种方式的好处:

  1. 推理过程对用户不可见——不需要用 XML 标签分隔
  2. 推理不占输出 Token——有独立的预算
  3. 更自然——模型不需要被要求「用某种格式输出推理」

代价是需要模型支持 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 做成可复用的工程组件。