logo

Chain-of-Thought、ReAct 与 Tree-of-Thought 对比

Published on

Chain-of-Thought、ReAct 与 Tree-of-Thought 对比

复杂问题往往不是模型“不会回答”,而是它需要先做几步中间工作。比如一道数学题要先列条件,再计算;一个故障排查要先看日志,再查文档;一个旅行计划要不断比较时间、预算和交通。让模型一次性把最终答案吐出来,等于要求它在脑中完成所有步骤,却不给系统任何观察和校验的机会。

于是我们看到三种经常被放在一起讨论的方法:Chain-of-Thought,简称 CoT;ReAct;Tree-of-Thought,简称 ToT。它们都试图让模型“多走几步”,但并不是同一个东西。

我早期做技术选型时也会被名称带偏:看到任务复杂,就想上 ToT;看到需要工具,就想上 ReAct;看到数学推理,就把 Prompt 改成“请一步一步思考”。后来复盘真实轨迹才发现,很多问题只需要把输出结构写清楚,很多问题需要的是工具和观察,只有一小部分任务真的值得维护搜索树。

理解三者的关键,不是背定义,而是问四个问题:中间状态在哪里?下一步由谁决定?有没有外部事实?错误路径能不能回退?

一、先看一张统一的地图

Chain-of-Thought:一条线
问题 → 步骤 1 → 步骤 2 → 步骤 3 → 答案

ReAct:一条会观察的线
问题 → 思考 → 行动 → 外部观察 → 思考 → 答案

Tree-of-Thought:多条可比较的线
问题 → 状态
      ├─ 路径 A → A1 / A2
      ├─ 路径 B → B1 / B2
      └─ 路径 C → C1
              评估、剪枝、选择

CoT 主要解决“如何把复杂答案拆成连续步骤”;ReAct 解决“如何让步骤与外部工具和环境反馈交替发生”;ToT 解决“如果第一步有多个可能方向,如何保留、评估并搜索多条思路”。

它们不是互相排斥的标签。一个 ReAct Agent 内部可以使用 CoT 风格的计划;一个 ToT 节点也可以调用工具获得观察;一个生产系统还可以用代码工作流包住其中一段模型推理。真正的区别在于状态管理和搜索控制。

二、Chain-of-Thought:先把一条路走清楚

CoT 的基本想法是让模型生成中间推理步骤,而不是直接给结论。最简单的提示可能是“请逐步分析”,更稳定的做法是定义步骤格式、示例和最终答案字段。

问题:仓库有 12 箱,每箱 8 件,发出 15 件后还剩多少?

步骤 1:计算总件数:12 × 8 = 96
步骤 2:减去发出数量:96 - 15 = 81
结论:还剩 81 件

它的最大优点是简单。一次模型调用就可以完成,延迟、成本和调试门槛都比较低。对于数学、逻辑、代码解释和内容规划等任务,清晰的中间结构往往已经能带来明显改善。

但 CoT 仍然是一条单路径生成。第一步错了,后面很可能沿着错误继续;它没有外部事实来源,也没有天然的回退机制。更长的文字还不等于更正确,有时只是把一个错误解释得更自信。

工程上不要把 CoT 文字直接当作事实或审计记录。更好的做法是要求结构化中间结果,并让代码验证:

type StepResult = {
  operation: string
  inputs: number[]
  result: number
  verified: boolean
}

function verifyStep(step: StepResult) {
  return Number.isFinite(step.result) && step.verified
}

CoT 适合单路径、无外部依赖、可以接受偶尔重试的任务。如果步骤稳定且能用代码写死,就不要为了“像推理”而把它交给模型。

三、ReAct:让思考遇到真实世界

ReAct 把推理和行动交替起来。模型先判断需要什么信息,再调用搜索、数据库或计算工具,获得观察结果后重新决策。

Thought:我需要确认这个 API 的最新限制。
Action:search_docs({ query: "rate limit" })
Observation:文档显示每分钟最多 60 次请求。
Thought:还需要结合当前租户的配额。
Action:get_quota({ tenantId: "..." })
Observation:当前租户剩余 12 次。
Final:本次任务最多还可以调用 12 次。

ReAct 的关键不是把“Thought”这个词写进 Prompt,而是观察结果真的会影响下一步。工具返回空结果时,模型应该换查询或请求用户澄清;工具超时时,系统应该决定重试、降级或停止。没有真实反馈的“行动”,只是另一段文本生成。

一个最小状态可以这样写:

type ReactState = {
  goal: string
  observations: Array<{ tool: string; result: unknown }>
  actions: Array<{ tool: string; input: unknown; status: string }>
  steps: number
  maxSteps: number
}

async function runReact(state: ReactState) {
  while (state.steps < state.maxSteps) {
    const decision = await model.decide(state)
    state.steps += 1

    if (decision.kind === 'final') return validate(decision, state)
    if (!allowedTools.has(decision.tool)) throw new Error('工具不在白名单')

    const result = await executeTool(decision.tool, decision.input)
    state.actions.push({
      tool: decision.tool,
      input: decision.input,
      status: result.ok ? 'succeeded' : 'failed',
    })
    state.observations.push({ tool: decision.tool, result })
  }

  return { status: 'needs_review', reason: '超过最大步骤' }
}

ReAct 适合信息会变化、需要工具、步骤不能完全预先写死的任务。代价是每一次行动都会增加延迟和失败点,工具权限、重试、幂等和停止条件都必须进入设计。

四、Tree-of-Thought:为什么要保留多个方向

CoT 和 ReAct 的基本形态仍然是一条路径。可是有些问题第一步就有多个合理选择,走错之后很难回来。例如拼图、组合规划、复杂代码重构和需要反事实比较的方案设计。ToT 的思路是把中间思考表示成节点,保留多个候选状态,再通过评估函数选择扩展、剪枝或回退。

type ThoughtNode = {
  id: string
  parentId?: string
  state: string
  score?: number
  status: 'open' | 'expanded' | 'pruned' | 'solved'
}

async function searchThoughtTree(root: ThoughtNode, maxNodes: number) {
  const open: ThoughtNode[] = [root]
  const visited = new Set<string>()

  while (open.length > 0 && visited.size < maxNodes) {
    const node = selectBestOpenNode(open)
    if (!node) break
    visited.add(node.id)

    const evaluation = await evaluateState(node.state)
    if (evaluation.solved) return node
    if (evaluation.score < 0) {
      node.status = 'pruned'
      continue
    }

    const children = await expandThought(node)
    open.push(...children)
    node.status = 'expanded'
  }

  return null
}

ToT 真正难的地方是评估函数。它要判断一个中间状态是否值得继续,而不是只看文字是否流畅。对于数学和游戏任务,可以有明确规则;对于开放式设计,评估往往不稳定,搜索出来的只是模型偏好的方案。

搜索树还会迅速膨胀。深度为 d、每个节点分支数为 b 时,最坏节点数量接近 b^d。所以必须限制宽度、深度、总 Token 和时间,并使用缓存、去重和剪枝。没有预算的 ToT,不是高级推理,而是一台成本不可控的文本生成器。

五、三种方法的核心差异

维度Chain-of-ThoughtReActTree-of-Thought
路径形态单一路径单一路径加观察多路径搜索
外部世界通常不依赖依赖工具或环境可依赖,也可不依赖
状态文本步骤动作与观察节点、边与评估分数
主要收益拆解复杂问题根据事实调整行动避免过早锁定错误路径
主要成本多一些输出 Token工具延迟和失败搜索空间与评估成本
适合任务数学、解释、规划草稿检索、操作、调查组合搜索、方案比较、复杂规划
主要风险错误一路传递工具误用或循环分支爆炸与错误评估

这张表不能代替实际实验,但能帮助你先排除明显不合适的方案。不要因为任务名字里有“Agent”,就默认需要 ToT;也不要因为使用了搜索工具,就以为已经实现了完整 ReAct。

六、选择方法的四个问题

问题一:任务是否需要外部事实

如果答案只依赖输入内容和模型已有能力,先试 CoT。需要最新文档、数据库或环境状态,就考虑 ReAct。ToT 也能使用工具,但工具只是节点的一部分,不是它存在的主要理由。

问题二:第一步选错后能否继续修正

如果错误可以通过最终验证发现,再重试一次也不贵,CoT 足够。若每一步都要根据外部观察更新,使用 ReAct;若存在多个相互竞争的计划,且回退比多次采样更有价值,再考虑 ToT。

问题三:有没有可靠的评估函数

没有评估函数,就无法判断哪条思路更好。数学答案、代码测试和约束满足容易评估;开放式战略和创意设计难很多。评估不可靠时,树越大,错误选择越多,不如使用少量候选加人工或裁判复核。

问题四:预算和延迟是否允许

先计算最坏情况:模型调用次数、输入输出 Token、工具调用、验证器运行时间和并发占用。如果任务价值无法覆盖成本,就不要追求理论上的最优路径。用户通常更愿意得到一个及时、诚实、可继续的答案,而不是等待很久的一篇过度推理。

七、不要把三种方法当成 Prompt 技巧

“请一步一步思考”只是行为提示;“先思考再行动”也不能自动授予工具权限;“生成三条思路并选择最好”更不等于实现了搜索。真正的工程实现需要状态、执行器、验证器和预算控制。

例如 ReAct 的工具调用必须由服务端校验,ToT 的节点必须可持久化和去重,CoT 的中间结果应该在适合的地方被程序重新计算。模型输出的是候选计划,系统状态才是事实。

此外,用户界面也不一定要展示完整思维链。对用户提供简短的处理进度、工具来源和最终依据通常已经足够;内部保留结构化轨迹,方便调试和评测。透明不等于把所有内部生成文本原样倾倒给用户。

八、用实验而不是感觉做选择

准备一组包含简单、复杂、无答案和对抗样本的任务,分别使用 CoT、ReAct 和 ToT,控制模型版本、提示词和最大预算。比较的不只是准确率,还包括:

  • 单任务平均和 P95 延迟。
  • 输入输出 Token 与总成本。
  • 工具调用次数、重复率和失败率。
  • 找到正确答案前的平均步骤数。
  • 没有足够信息时的拒答或追问质量。
  • 结果能否通过确定性验证。
type MethodReport = {
  method: 'cot' | 'react' | 'tot'
  successRate: number
  p95LatencyMs: number
  avgTokens: number
  toolErrorRate: number
  verificationRate: number
}

很多时候实验结果会很朴素:CoT 在大多数简单任务上性价比最高,ReAct 在需要实时信息的任务上明显胜出,ToT 只在少数高价值、可评估、确实需要回溯的问题上值得使用。这不是方法有高低之分,而是复杂度要和问题匹配。

九、我的总结:多走几步不等于走得更对

Chain-of-Thought 让模型把一条思路展开,ReAct 让思路能够和外部世界交换信息,Tree-of-Thought 让系统保留多条思路并进行搜索。三者的共同目标是减少一次性猜答案的压力,但它们付出的代价不同。

我现在的选型原则很简单:能用代码写死的步骤,不交给模型;单路径推理足够的任务,先用 CoT;需要事实和工具反馈的任务,用 ReAct;只有当多个候选路径的比较有明确价值、评估函数可靠、预算也承担得起时,才使用 ToT。

真正成熟的推理系统,不会因为画出了更复杂的流程图就更聪明。它知道什么时候应该多想,什么时候应该去查资料,什么时候应该保留备用方案,也知道什么时候继续计算已经没有收益。把方法选对,把状态记清,把结果验证好,模型才是在解决问题,而不是在用更多文字表演解决问题。

🤪 您也可以编辑此页: