思维链 CoT 怎么用:让模型推理更准的工程实践
Chain-of-Thought(CoT) 让模型在给出最终答案前先输出推理步骤,对数学计算、多步逻辑、代码分析等任务准确率提升显著。工程上的关键决策:何时开、怎么开、成本怎么控。
你八成也踩过这个坑:给模型扔一道两位数乘法混合应用题,它嗖地吐出一个答案,看着挺像那么回事,你拿计算器一验——差了十万八千里。这不是模型笨,是它在自回归生成时一步到位直接蹦出最终答案,中间该做的计算全靠”直觉”,没有真实执行过程。CoT 干的事其实很朴素:把原本要一步压缩完成的推理拆成多个 token 位置分别产出,等于给模型多要了几次”计算时间”——每多生成一个 token,模型就多一次基于上文做注意力计算的机会,相当于把单步的计算量摊薄到多步里。这也是为什么 CoT 对纯知识检索型任务(一次前向传播就能查到答案的那种)帮助有限,但对需要多步演算、链式推理的任务效果拔群——推理越依赖”过程”,CoT 拆出来的中间 token 就越有实际计算价值。
CoT 的两种触发方式
Zero-shot CoT
最简单,在提示末尾加一句:
请一步一步思考,然后给出最终答案。
或英文环境常用的经典咒语:
Let's think step by step.
适用场景:没有现成示例、任务类型多变、快速原型。
这句”Let’s think step by step”不是拍脑袋想出来的——最早出自 2022 年 Kojima 等人那篇论文,作者测过一堆提示语变体,最终这句在 GSM8K 数学题集上把原本几乎不会推理的模型准确率拉高了几十个百分点,效果好到离谱,之后才成了业界公认的”咒语”。但你实测时容易踩个坑:不是所有模型对这句都买账,一些经过强 RLHF 对齐、本身就倾向直接给结论的模型,加了这句反而输出变得啰嗦但推理没深入,答案该错还错。判断标准很简单:拿你自己的典型任务跑一组 A/B 对比,输出准确率没提升就别为了”显得严谨”硬加这句,白搭 token。
Few-shot CoT
在示例中展示完整的推理链,模型会模仿这个推理风格:
FEW_SHOT_COT = """
Q: 仓库有 120 件商品,今天卖出 35 件,进货 50 件,现在还有多少件?
A:
思考过程:
- 初始库存:120
- 卖出后:120 - 35 = 85
- 进货后:85 + 50 = 135
最终答案:135 件
Q: {question}
A:
"""
Few-shot CoT 在任务格式固定时效果更稳,推荐用于生产环境。
这里有个容易被忽略的细节:示例里的推理步骤格式要跟你线上任务的输出格式严格对齐,包括换行、加粗、项目符号这些排版细节——模型的模仿能力强到近乎”抄格式”,你示例里用”-“列点,它大概率也会用”-“列点;你示例只给两步推理,真实任务需要五步的它也可能只写两步就收尾。示例数量上,2-3 个够用,超过 5 个边际收益很小,还白占 prompt 的 token 预算和上下文窗口。另外别偷懒直接抄网上现成的 few-shot 模板——你的业务字段名、单位、进位规则跟示例不一致,模型会照着示例的习惯犯错,这类 bug 排查起来比没上 CoT 时更麻烦,因为你得先辨认清楚是提示词的问题还是模型本身的问题。
CoT 与普通提示的效果对比
| 任务类型 | 普通提示 | CoT | 提升幅度 |
|---|---|---|---|
| 数学应用题 | 中等 | 显著提升 | +20-40% |
| 多跳逻辑推理 | 差 | 明显改善 | +30-50% |
| 代码 bug 分析 | 中等 | 略有提升 | +10-20% |
| 简单问答/分类 | 好 | 无明显提升 | ≈0% |
| 情感分类 | 好 | 可能下降 | -5% |
简单任务用 CoT 反而增加幻觉风险,因为模型会在推理步骤里”编”出中间结论。
结构化 CoT:分隔推理与结果
让模型把”过程”和”答案”分开,方便后处理只取结果:
SYSTEM = """
请按以下格式回答:
<thinking>
(在这里写推理步骤,用户不会看到)
</thinking>
<answer>
(在这里写最终答案,简洁明了)
</answer>
"""
解析时用正则提取 <answer> 标签内容:
import re
def extract_answer(response: str) -> str:
match = re.search(r"<answer>(.*?)</answer>", response, re.DOTALL)
return match.group(1).strip() if match else response
这个模式也是 Anthropic Claude 模型内置 extended thinking 的思路原型。
这个模式在生产里有个真实翻车案例你一定要提防:如果你把模型输出直接透传给前端做流式展示(SSE 或 WebSocket 推流),<thinking> 标签里的内容会跟着一起流出去,用户会在聊天窗口里看到一堆”思考过程”裸奔在外面——轻则显得啰嗦,重则暴露你不想让用户看到的中间判断(比如”用户似乎想绕过某规则,需要委婉拒绝”这种敏感推理)。正确做法是流式接收时做增量缓冲,检测到进入 <thinking> 标签就把这部分 token 暂存不下发,只有匹配到 <answer> 标签内的内容才推给前端;如果模型支持原生 thinking 字段(跟正文分开返回,而不是拼在同一个文本流里),优先用原生字段,解析成本低也不会有漏标签的风险。
成本控制:CoT 的 token 代价
CoT 会大幅增加输出 token:
普通回答:~50 tokens
CoT 推理过程:200-800 tokens(视任务复杂度)
控制策略:
- 按任务路由:在请求前判断任务复杂度,简单任务走 zero-shot,复杂任务才启用 CoT
- 隐藏 thinking:支持 thinking 模式的模型(如 Claude 3.7 Sonnet)可设置 budget_tokens 控制推理深度
- 缓存推理链:对相同或相似问题,缓存已有的 CoT 输出复用
def should_use_cot(task_type: str, complexity_score: float) -> bool:
COT_TASKS = {"math", "logic", "code_debug", "multi_hop_qa"}
return task_type in COT_TASKS and complexity_score > 0.6
提示词 CoT 和模型内置推理是两回事
做接入选型时容易把这两者混为一谈,其实完全不是一码事。你在 prompt 里手写”请一步一步思考”,本质是让模型在输出的 token 序列里”显式”展示推理过程,模型本身的参数和训练方式没变,纯粹是推理阶段的提示技巧。而部分模型内置的 extended thinking / reasoning 模式(比如 Claude 的 thinking 模式),是在训练阶段就针对”先想后答”这个模式做过专门优化,模型会自己决定要想多深、要不要回头检查,你在 API 层面通常只需要设置一个 budget_tokens 之类的参数,不需要自己写”请一步步思考”这句提示——加了反而可能是多余的,甚至跟模型内部已有的思考流程冲突,输出变得又臭又长。
选型上给你个粗判断:任务是数学、代码调试、多跳推理这类”越想越准”的场景,且预算允许,优先选带内置推理能力的模型,省心也更稳;如果任务简单,或者对延迟极敏感(比如实时客服的首句响应),提示词 CoT 配合前面的按任务路由更划算,别为了”用最强推理能力”把简单任务的成本和延迟都拉上去。具体某个模型是否支持、参数怎么传,以对应服务商的官方文档为准,接口和参数名经常会调整。
Self-Consistency:多路 CoT 投票
对高精度要求的任务,生成多条推理链再多数投票:
import asyncio
from collections import Counter
async def self_consistency(question: str, n: int = 5) -> str:
tasks = [call_llm_cot(question) for _ in range(n)]
answers = await asyncio.gather(*tasks)
# 只取 <answer> 部分
extracted = [extract_answer(a) for a in answers]
# 多数投票
return Counter(extracted).most_common(1)[0][0]
成本是单次调用的 n 倍,但准确率可再提升 5-15%,适合高价值低频任务。
实现时注意两个工程细节:一是 asyncio.gather 默认没有做限流,n 一大很容易撞到接口的并发速率限制,报错通常长这样——RateLimitError: Rate limit reached for requests,或者 HTTP 状态码 429。解决办法不是傻等重试,而是给每个任务包一层带退避的重试逻辑(指数退避,比如首次等 1 秒,失败再等 2 秒、4 秒,一般封顶在 30-60 秒并设置最大重试次数),同时用信号量(asyncio.Semaphore)把并发度卡在你账号配额允许的范围内,别一次性把 n 条请求全甩出去。二是多数投票在答案是自由文本而非固定选项时会失灵——“135 件”和”135”两个字符串不相等,投票会各自为政投不出多数。稳妥做法是在 extract_answer 之后再加一层归一化(去空格、去单位、统一大小写),或者用规则把答案标准化到统一格式后再进 Counter。
常见问题
模型推理链写得”看起来正确”,但最终答案错了怎么办? 这是 CoT 的已知局限,称为”unfaithful reasoning”。可以在推理链末尾加”请验证你的答案”步骤,或用 self-consistency 多数投票减少单次失误率。
中文提示里加”Let’s think step by step”还是中文版? 对大多数中英文双语模型(如 DeepSeek、Qwen),中文提示用”请一步一步思考”效果相当,不必强行混入英文。但少数模型在英文指令下推理更稳定,可以 A/B 测试确认。
CoT 对 function calling / structured output 有帮助吗? 有,但要把推理步骤放在 tool_choice 决策之前的”planning”阶段,而不是插入 JSON 输出里(否则会破坏格式)。
CoT 会不会拖慢首字返回时间(TTFB)?
会,而且很明显。因为模型要先把整段推理过程生成完(或至少生成到 <answer> 标签前),用户等待”看到东西”的时间会变长。如果你的场景对首字延迟敏感,两个思路:一是用流式接口把 thinking 部分实时映射成一个”正在思考…”的 loading 态展示(不展示具体内容,只展示状态),等 <answer> 标签出现再切换成正式内容流;二是干脆用支持内置推理的模型,并把 reasoning 单独做成一个可折叠的”思考过程”UI 组件,这两种体验都比让用户对着一段无响应的空白干等要好。
用了 CoT 之后偶尔输出会被截断,<answer> 标签都没出现就没了,是什么原因?
八成是 max_tokens(或对应参数名)设得不够。CoT 的推理部分本身可能吃掉几百 token,如果你按普通回答的预算设了一个偏小的值,模型可能还在”思考”的中途就被截断,</thinking> 和 <answer> 都没机会输出。排查时先把 max_tokens 放宽到 1000 以上试一次,如果问题消失就是预算问题;同时前面 extract_answer 里 else response 那个分支其实就是兜底逻辑,但生产环境建议在这里补一条日志或告警,而不是静默把原始整段文本直接返回给下游,不然截断问题会被隐藏很久才被人发现。
延伸阅读:大模型应用开发模式 · 应用模式 Hub · few-shot 示例怎么给 · 让模型稳定输出 JSON