什么是 Self-Consistency
我们在前面学过 Chain-of-Thought(CoT),它让 AI 一步步推理。但有个问题:同一个问题,AI 每次推理的路径可能不同,答案也可能不同。
Self-Consistency 的核心思想很简单:让 AI 推理多次,然后对结果投票,选出最一致的答案。
就像考试时你不确定一道题的答案,于是用三种不同方法解题,如果三种方法中有两种得出相同答案,那个答案大概率是对的。
为什么 CoT 还不够
单次 CoT 推理有一个根本性问题:推理路径的随机性。
问题:一个商店有 23 个苹果,卖掉了一些后还剩 7 个,又进货了 12 个,现在有多少个?
路径 A(正确):23 - 16 + 12 = 19 ✓
路径 B(错误):23 - 7 = 16, 16 + 12 = 28 ✗(误解题意)
路径 C(正确):剩 7 个 + 进货 12 个 = 19 ✓
如果我们只采样一次,有可能恰好走到路径 B。但如果采样三次,投票结果是 19(2票)vs 28(1票),正确答案胜出。
Self-Consistency 的工作流程
整个流程分为三步:
第一步:多次采样
对同一个问题,使用较高的 temperature 生成多条推理路径:
采样 1 → 推理路径 A → 答案: 19
采样 2 → 推理路径 B → 答案: 28
采样 3 → 推理路径 C → 答案: 19
采样 4 → 推理路径 D → 答案: 19
采样 5 → 推理路径 E → 答案: 28
第二步:提取答案
从每条推理路径中提取最终答案:
| 采样编号 | 推理路径 | 最终答案 |
|---|---|---|
| 1 | 先算卖掉的数量… | 19 |
| 2 | 用总数减去剩余… | 28 |
| 3 | 从剩余开始算… | 19 |
| 4 | 列方程求解… | 19 |
| 5 | 分步计算… | 28 |
第三步:多数投票
答案 19:3 票 ✓(胜出)
答案 28:2 票
最终输出答案 19。
API 实现:temperature 和 top_p
Self-Consistency 的关键在于采样多样性。我们需要调整 API 参数让每次生成的推理路径不同。
基础实现
import anthropic
from collections import Counter
client = anthropic.Anthropic()
def self_consistency(question: str, num_samples: int = 5) -> str:
"""使用 Self-Consistency 方法回答问题"""
prompt = f"""请一步步思考以下问题,展示完整推理过程,最后用 "最终答案:" 开头给出答案。
问题:{question}"""
answers = []
for i in range(num_samples):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
temperature=0.7, # 关键:较高的 temperature 增加多样性
messages=[{"role": "user", "content": prompt}]
)
text = response.content[0].text
# 提取最终答案
answer = extract_answer(text)
answers.append(answer)
print(f"采样 {i+1}: {answer}")
# 多数投票
counter = Counter(answers)
best_answer = counter.most_common(1)[0][0]
confidence = counter[best_answer] / len(answers)
print(f"\n投票结果: {dict(counter)}")
print(f"最终答案: {best_answer} (置信度: {confidence:.0%})")
return best_answer
def extract_answer(text: str) -> str:
"""从推理文本中提取最终答案"""
for line in text.split('\n'):
if '最终答案' in line or '答案是' in line or '答案:' in line:
return line.split(':')[-1].strip().split('。')[0].strip()
# 如果没找到标记,返回最后一行
return text.strip().split('\n')[-1].strip()
temperature vs top_p
这两个参数都影响采样多样性,但方式不同:
| 参数 | 作用 | Self-Consistency 推荐值 |
|---|---|---|
| temperature | 控制输出的随机性,值越高越随机 | 0.5 - 0.8 |
| top_p | 控制候选 token 的范围 | 0.9 - 0.95 |
# 方式一:调整 temperature(推荐)
response = client.messages.create(
model="claude-sonnet-4-20250514",
temperature=0.7,
# ...
)
# 方式二:调整 top_p
response = client.messages.create(
model="claude-sonnet-4-20250514",
top_p=0.9,
# ...
)
一般来说,调整 temperature 就够了。temperature 太低(< 0.3)会导致每次采样结果几乎相同,失去 Self-Consistency 的意义;太高(> 1.0)会导致推理质量下降。
进阶:加权投票
简单的多数投票把每条路径视为等权。但实际上,有些推理路径更可靠。我们可以根据推理质量加权:
def weighted_self_consistency(question: str, num_samples: int = 7) -> str:
"""加权 Self-Consistency"""
prompt = f"""请一步步思考以下问题。
要求:
1. 展示完整推理过程
2. 每一步都要验证
3. 最后用 "最终答案:" 给出答案
4. 用 "置信度:" 给出你对答案的信心(高/中/低)
问题:{question}"""
results = []
for i in range(num_samples):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
temperature=0.7,
messages=[{"role": "user", "content": prompt}]
)
text = response.content[0].text
answer = extract_answer(text)
confidence = extract_confidence(text)
# 置信度映射为权重
weight_map = {"高": 3, "中": 2, "低": 1}
weight = weight_map.get(confidence, 1)
results.append({"answer": answer, "weight": weight})
# 加权投票
weighted_votes = {}
for r in results:
weighted_votes[r["answer"]] = weighted_votes.get(r["answer"], 0) + r["weight"]
best_answer = max(weighted_votes, key=weighted_votes.get)
return best_answer
实战案例:代码逻辑判断
Self-Consistency 在代码相关的推理中特别有用。来看一个例子:
question = """
以下 Python 代码的输出是什么?
x = [1, 2, 3, 4, 5]
y = x[1:4]
y[0] = 99
print(x)
print(y)
"""
# 采样 5 次的结果可能是:
# 采样 1: x=[1,2,3,4,5], y=[99,3,4] → 正确(切片创建新列表)
# 采样 2: x=[1,99,3,4,5], y=[99,3,4] → 错误(误以为是引用)
# 采样 3: x=[1,2,3,4,5], y=[99,3,4] → 正确
# 采样 4: x=[1,2,3,4,5], y=[99,3,4] → 正确
# 采样 5: x=[1,2,3,4,5], y=[99,3,4] → 正确
# 投票:正确答案 4:1 胜出
什么时候 Self-Consistency 特别有效
| 场景 | 效果 | 原因 |
|---|---|---|
| 数学推理 | 很好 | 答案唯一,投票容易收敛 |
| 逻辑推理 | 很好 | 多条路径可以交叉验证 |
| 代码输出预测 | 好 | 答案明确,容易比较 |
| 常识推理 | 一般 | 答案可能有多种合理表述 |
| 开放式问题 | 不适用 | 没有唯一正确答案 |
成本与准确性的权衡
Self-Consistency 的代价是成本线性增长。采样 N 次意味着 N 倍的 API 调用费用。
采样次数 vs 准确率提升
根据论文和实践经验,准确率提升呈递减趋势:
采样次数 准确率提升(相对单次 CoT)
1 基线
3 +5% ~ +10%
5 +8% ~ +15%
10 +10% ~ +18%
20 +12% ~ +20%
40 +13% ~ +21%(边际收益很小)
实用建议
def smart_self_consistency(question: str, budget: str = "medium") -> str:
"""根据预算选择采样次数"""
sample_config = {
"low": 3, # 低预算:3 次采样,成本 3x
"medium": 5, # 中预算:5 次采样,成本 5x(推荐)
"high": 10, # 高预算:10 次采样,成本 10x
}
num_samples = sample_config[budget]
return self_consistency(question, num_samples)
经验法则:5 次采样是性价比最高的选择。3 次太少(2:1 的投票不够稳定),10 次以上边际收益递减。
与其他技巧的组合
Self-Consistency + Few-Shot CoT
prompt = """以下是一些推理示例:
问题:小明有 10 元,买了 3 元的笔,还剩多少?
推理:10 - 3 = 7
答案:7 元
问题:{actual_question}
推理:"""
先用 Few-Shot 提高单次推理质量,再用 Self-Consistency 提高整体准确性。这是目前效果最好的组合之一。
Self-Consistency + 角色扮演
roles = [
"你是一个数学教授",
"你是一个工程师",
"你是一个逻辑学家",
]
# 每次采样使用不同角色,增加推理路径的多样性
for role in roles:
prompt = f"{role},请一步步解决以下问题:\n{question}"
# ...
不同角色会倾向于使用不同的推理方法,天然增加了路径多样性。
常见问题
Q:所有采样结果都一样怎么办?
说明 temperature 太低,或者问题太简单不需要 Self-Consistency。把 temperature 调到 0.7 以上试试。
Q:投票结果是平票怎么办?
两种处理方式:
- 增加采样次数(用奇数次采样可以避免)
- 选择推理步骤更详细的那个答案
Q:对于非数值答案怎么投票?
需要做答案归一化。比如 “19个”、“19”、“十九” 应该视为同一个答案:
import re
def normalize_answer(answer: str) -> str:
"""归一化答案用于比较"""
# 去除单位和标点
answer = re.sub(r'[个元只条件份]', '', answer)
answer = answer.strip('。,、')
# 中文数字转阿拉伯数字
cn_num = {'零':0, '一':1, '二':2, '三':3, '四':4,
'五':5, '六':6, '七':7, '八':8, '九':9, '十':10}
for cn, num in cn_num.items():
answer = answer.replace(cn, str(num))
return answer.strip()
总结
| 维度 | 说明 |
|---|---|
| 核心思想 | 多次采样 + 多数投票 |
| 适用场景 | 有唯一正确答案的推理问题 |
| 推荐采样次数 | 5 次(性价比最高) |
| temperature | 0.5 - 0.8 |
| 成本 | N 倍于单次调用 |
| 与 CoT 的关系 | 是 CoT 的增强版,不是替代品 |
Self-Consistency 是目前提升 LLM 推理准确性最简单有效的方法之一。它不需要修改 Prompt,不需要微调模型,只需要多调几次 API 然后投票。
一个人可能会犯错,但一群人同时犯同样的错,概率就小得多。Self-Consistency 就是让 AI 成为”一群人”。
相关文章
评论
加载中...
评论
加载中...