同一份权重,换个用法能差出十几分,也能差出十倍成本。推理时策略(test-time 策略)就是这样一类东西:不改模型,只改"怎么问、问几次、要不要它自己检查"。麻烦在于三种策略的成本结构完全不同——Chain-of-Thought 加长输出,Self-Consistency 乘以调用数,Self-Refine 乘轮次,三者叠起来是乘积关系。先把这笔账算清楚,再决定开哪一档。
1. 推理时算力花在哪三个地方
| 维度 | 手段 | 成本增长方式 | 收益上限 |
|---|---|---|---|
| 输入侧 | 示例、上下文、检索片段 | 线性(输入 token) | 任务定义清晰度决定 |
| 输出侧 | Chain-of-Thought、结构化草稿 | 线性到数倍(输出 token) | 有推理链可分解的任务 |
| 采样侧 | Self-Consistency 多票 | 乘以调用数 | 有唯一正确答案的任务 |
| 轮次侧 | Self-Refine 迭代 | 乘以轮次 | 有可判定反馈的任务 |
四条加起来,最坏情况是 输入 × 调用数 × 轮次。这个乘积是所有"推理成本失控"事故的共同形状。
2. Chain-of-Thought:它买到的是"可中断的中间状态"
CoT 有效的机制解释:让模型把中间量写进上下文,后续 token 就能以这些中间量为条件,而不是在单步里同时完成"回忆 + 计算 + 组织答案"。它换来的是可分解性,不是"更聪明"。
COT_TMPL = """按下面四步作答,每步一行,最后一行只输出结论:
1) 已知条件:从题面摘出全部给定值与单位
2) 目标:要求的是什么,单位是什么
3) 推导:写出用到的关系式,逐步代入
4) 结论:只写最终值,带单位
题面:{q}"""
| 任务特征 | 开 CoT | 理由 |
|---|---|---|
| 多步算术、单位换算 | 是 | 中间量必须落盘 |
| 规则条文的多条件判断 | 是 | 条件逐条对齐 |
| 单跳事实问答 | 否 | 只增加 token 与幻觉面 |
| 格式转换(JSON/表格) | 否 | 结构约束已足够 |
| 主观写作、摘要 | 视风格 | 推理链通常无害但无用 |
一个反直觉的实测倾向:CoT 会放大"题目本身有歧义"的影响。模型会把歧义沿着推理链一路放大,最后给出一个看起来很严谨的错误答案。歧义题的正确处理是先追问澄清,不是加推理链。
3. Self-Consistency:票数与温度是同一个旋钮的两半
Self-Consistency 的做法:同一问题采样 N 条推理链,对最终答案取多数。它成立的前提有两个——答案可归一化(能判等)、错误分布不是系统性同向。
from collections import Counter
def self_consistency(q, llm, n=5, temperature=0.7):
answers = []
for _ in range(n):
chain = llm(COT_TMPL.format(q=q), temperature=temperature)
answers.append(normalize(chain.strip().splitlines()[-1]))
top, cnt = Counter(answers).most_common(1)[0]
return top, cnt / n # 一致率是重要信号,不只是副产品
票数 N |
相对成本 | 典型收益 | 备注 |
|---|---|---|---|
| 1 | 1× | 基线 | 等同单次 CoT |
| 3 | 3× | 多数场景的主要收益在此 | 性价比拐点 |
| 5 | 5× | 少量提升 | 一致率低于 0.6 时别再加 |
| 9+ | 9× 以上 | 通常饱和 | 收益被生成方差吞掉 |
两个必须注意的点:
- 温度太低时多票没有意义。
temperature=0采样出的链几乎相同,等于用 5 倍成本做一次调用。 - 一致率要当指标看。一致率低于 0.6 说明题目本身歧义或模型没有该能力,加票只是把错误答案投票得更自信。
import re
UNITS = ("元", "美元", "kg", "公里")
def normalize(ans):
a = ans.strip().replace(",", "").replace(",", "")
a = re.sub(r"\s+", "", a)
for u in UNITS:
a = a.replace(u, "")
# 数值型统一到同一精度,文本型转小写去标点
try:
return f"{float(a):.4g}"
except ValueError:
return re.sub(r"[。..!!??]", "", a).lower()
答案归一化是最容易漏的一步:1,200、1200元、1200 是同一个答案,不归一化就变成三票分散。
4. Self-Refine:收益全在"反馈是否可判定"
Self-Refine 是"生成 → 自我批评 → 修订"的循环。它能起作用的唯一条件是批评环节引入了新信息:要么来自外部校验器(编译、单测、schema 校验),要么来自明确的评分标准。
def self_refine(task, llm, check, max_rounds=3):
draft = llm(task)
for r in range(max_rounds):
ok, report = check(draft) # 可判定反馈:单测 / schema / 规则
if ok:
return draft, r
new = llm(f"{task}\n\n上一版问题:{report}\n只修这些问题,不要重写:\n{draft}")
if new.strip() == draft.strip(): # 修订无效,早停
return draft, r
draft = new
return draft, max_rounds
| 反馈来源 | 可判定性 | 典型轮次收益 | 风险 |
|---|---|---|---|
| 单测 / 编译错误 | 强 | 第 1 轮最大,第 2 轮递减 | 修一处坏一处 |
| JSON schema 校验 | 强 | 通常 1 轮解决 | 语义正确但结构对 |
| 规则清单打分 | 中 | 2–3 轮 | 评分器自身偏差被放大 |
| 模型自我批评 | 弱 | 常为 0 甚至负 | 越改越偏离 |
弱反馈下的 Self-Refine 是典型的"看起来在工作":每轮都有改动,质量不动甚至下降。判断方法很简单——把轮次从 3 降到 1,指标如果没有明显变化,那两轮就是白花钱。
5. Thought-Action-Observation 与 CoT 的分界
CoT 是"想",Thought-Action-Observation 是"想一步、做一步、看结果"。区别在于信息是否来自环境。
| 维度 | Chain-of-Thought | Thought-Action-Observation |
|---|---|---|
| 信息来源 | 模型内部知识 | 工具返回值、检索结果 |
| 可验证性 | 事后才能验 | 每步都有环境返回值可验 |
| 失败形态 | 推理链自洽但错误 | 动作失败、参数错、工具超时 |
| 停止条件 | 输出结论 | 结论 + 动作预算耗尽 |
| 成本 | 输出 token | 输出 token + 工具调用次数 |
判据:需要环境里的事实就走向 Observation,纯靠已知条件推就走 CoT。用 CoT 猜数据库里的值,是最常见的误用。
6. 成本账:三种策略的乘法关系
# 粗略成本估算:in/out 单价按供应商单价表核对,此处只示范结构
def cost(in_tok, out_tok, calls, price_in, price_out):
return calls * (in_tok * price_in + out_tok * price_out)
base = dict(in_tok=800, out_tok=200, calls=1)
cot = dict(in_tok=800, out_tok=900, calls=1) # CoT 拉长输出
sc = dict(in_tok=800, out_tok=900, calls=5) # 5 票
both = dict(in_tok=800, out_tok=900, calls=5) # 5 票 + 2 轮修订
print(cost(**base, price_in=1e-6, price_out=4e-6))
print(cost(**both, price_in=1e-6, price_out=4e-6) * 2)
| 组合 | 相对成本 | 适合 |
|---|---|---|
| 单次回答 | 1× | 高频、可容错 |
| CoT | 约 2–4× | 多步推导 |
| CoT + 5 票 | 约 10–20× | 离线批处理、准确率优先 |
| CoT + 5 票 + 2 轮 Refine | 约 20–40× | 只在有强校验器时值得 |
表中的倍数是结构推演的量级,具体倍数按实际 token 分布与单价核对。能承受的只有离线任务与低 QPS 场景;在线高并发链路上开满三件套,账单会先于质量出事。
7. 路由:不是所有请求都配得上重推理
| 请求特征 | 策略档位 | 判据 |
|---|---|---|
| 短问答、FAQ 命中 | 直答 + 检索 | 有现成答案,推理只添噪声 |
| 需要一步计算的业务问题 | CoT 单次 | 中间量必须落盘 |
| 有唯一标准答案、可离线 | CoT + Self-Consistency | 答案可归一化比较 |
| 有编译/单测/校验器 | Self-Refine + 强反馈 | 反馈可判定 |
| 需要外部事实 | Thought-Action-Observation | 事实不在权重里 |
路由实现上,先做一个便宜的分类器(规则 + 小模型),把请求分到档位,再按档位设不同的超时与预算上限。没有预算上限的推理策略一定会被长尾请求打穿。
TIERS = { # 每档:票数、轮次、输出上限
"direct": dict(n=1, rounds=1, max_out=300),
"cot": dict(n=1, rounds=1, max_out=1200),
"vote": dict(n=5, rounds=1, max_out=1200),
"refine": dict(n=1, rounds=3, max_out=2000),
}
def route(req):
if req.hit_cache: return "direct"
if req.has_verifier and req.is_code: return "refine"
if req.answer_is_unique: return "vote"
return "cot" if req.needs_calc else "direct"
8. 排错:六种"策略开了但没用"
| 现象 | 原因 | 处理 |
|---|---|---|
| 开 CoT 后准确率下降 | 题目有歧义,推理链放大偏差 | 先澄清,再推理 |
| 多票结果与单票相同 | 温度过低,采样退化 | 提高温度,或直接省掉多票 |
| 一致率始终低于 0.6 | 答案无唯一形式或超出能力 | 做答案归一化,或降级为"给候选" |
| Refine 越改越差 | 反馈来自模型自评 | 换强校验器,或把轮次设为 1 |
| 成本涨了 10 倍,指标涨 1 点 | 档位路由缺失 | 按请求特征分档,设预算上限 |
| 推理链中突然跳到结论 | 输出长度上限截断 | 提高上限或用外置草稿(文件/变量) |
9. 边界:推理时策略改不了的东西
| 问题 | 该动的地方 | 为什么加推理无用 |
|---|---|---|
| 知识缺失 | 检索、微调 | 推理不能凭空造出事实 |
| 工具不可用 | 工具实现与权限 | 动作失败与推理深度无关 |
| 任务定义模糊 | 需求澄清、评测集 | 策略只放大已有定义 |
| 上下文超窗 | 分块、压缩、外置记忆 | 是被截断,不是没想清楚 |
| 延迟硬约束 | 模型路由、缓存 | 重推理天然增加延迟 |
10. 自测
- Chain-of-Thought 与 Self-Consistency 的成本分别按什么方式增长?两者叠加是什么关系?
temperature=0时做 5 票 Self-Consistency,问题出在哪?- 一致率 0.55 说明什么?继续加票能解决吗?
- Self-Refine 在什么条件下才有正收益?如何用一次实验判断那两轮是否白花钱?
- 什么时候必须用 Thought-Action-Observation 而不能用 CoT?
- 给在线高并发链路设计推理档位路由时,除了任务特征还必须限制什么?