核心原理

Self-Consistency (SC) 的核心思想极其简洁:对同一个问题生成多条推理路径,通过投票选出最一致的答案

传统 Chain-of-Thought (CoT) 只采样一次推理路径,若该路径在某一步出错,最终答案就会错。SC 利用了一个关键观察——正确答案往往比错误答案更容易被多种推理路径到达

# 单次 CoT:一条路径
问题 → [推理路径 A] → 答案 A

# Self-Consistency:多条路径 + 投票
问题 → [推理路径 A] → 答案 A ─┐
     → [推理路径 B] → 答案 B ─┼→ 多数投票 → 最终答案
     → [推理路径 C] → 答案 A ─┘
     → [推理路径 D] → 答案 A ─┘

采样策略详解

温度参数 (Temperature)

温度控制采样的随机性,是 SC 效果的关键变量:

温度范围效果适用场景
0.0-0.3路径高度相似,多样性不足简单问题
0.5-0.7多样性与质量的最佳平衡推荐默认
0.8-1.0路径差异大,但质量下降复杂推理需高多样性

Top-p 采样

Top-p (nucleus sampling) 限制了候选 token 的概率质量:

import openai

def self_consistency_query(prompt, n=10, temperature=0.7, top_p=0.95):
    """Self-Consistency 多次采样"""
    responses = []
    for i in range(n):
        resp = openai.chat.completions.create(
            model="gpt-4",
            messages=[{"role": "user", "content": prompt}],
            temperature=temperature,
            top_p=top_p,
            max_tokens=1024,
        )
        responses.append(resp.choices[0].message.content)
    return responses

采样数量与收益曲线

采样数 NGSM8K 准确率相对成本边际收益
1 (CoT)56.2%1x基线
568.4%5x+12.2%
1072.8%10x+4.4%
2075.2%20x+2.4%
4076.1%40x+0.9%

收益递减明显:N=10 是性价比最优的选择,之后每增加 10 次采样仅提升 ~1%。

投票策略

简单多数投票

from collections import Counter

def majority_vote(answers):
    """简单多数投票"""
    counter = Counter(answers)
    return counter.most_common(1)[0][0]

加权投票

不是所有路径都同等可靠。可以根据路径长度、置信度等加权:

import re
from collections import defaultdict

def weighted_vote(responses_and_answers):
    """
    加权投票:推理路径越短,权重越高(奥卡姆剃刀)
    responses_and_answers: [(reasoning_text, answer), ...]
    """
    scores = defaultdict(float)
    for reasoning, answer in responses_and_answers:
        # 路径越短权重越高
        length = len(reasoning.split())
        weight = 1.0 / (1.0 + length / 100)
        scores[answer] += weight
    return max(scores, key=scores.get)

def extract_answer(response_text):
    """从推理文本中提取最终答案"""
    # 匹配 "答案是 X" 或 "The answer is X"
    match = re.search(r'(?:答案|answer)\s*[::]??\s*([^\n。]+)', 
                      response_text, re.IGNORECASE)
    return match.group(1).strip() if match else response_text.strip()

log-probability 加权

更精确的方法是使用每条路径的 log-probability 作为权重:

import math

def logprob_weighted_vote(paths_with_logprobs):
    """
    paths_with_logprobs: [(answer, avg_logprob), ...]
    avg_logprob: 该路径 token 的平均 log probability
    """
    scores = defaultdict(float)
    for answer, logprob in paths_with_logprobs:
        # 转换为概率权重
        scores[answer] += math.exp(logprob)
    return max(scores, key=scores.get)

SC + CoT 完整实现

import openai
import re
from collections import Counter

class SelfConsistency:
    def __init__(self, model="gpt-4", n_samples=10, 
                 temperature=0.7, top_p=0.95):
        self.model = model
        self.n_samples = n_samples
        self.temperature = temperature
        self.top_p = top_p
    
    def _build_prompt(self, question):
        return f"""请逐步推理以下问题,并在最后给出答案。

问题:{question}

请按以下格式回答:
推理过程:...
答案:[最终答案]

解题步骤:"""
    
    def _sample(self, prompt):
        resp = openai.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=self.temperature,
            top_p=self.top_p,
            max_tokens=1024,
        )
        return resp.choices[0].message.content
    
    def _extract_answer(self, text):
        match = re.search(r'答案[::]\s*(.+?)(?:\n|$)', text)
        return match.group(1).strip() if match else text.strip()
    
    def solve(self, question):
        prompt = self._build_prompt(question)
        
        # 并行采样(实际生产环境用 asyncio)
        responses = [self._sample(prompt) for _ in range(self.n_samples)]
        
        # 提取答案
        answers = [self._extract_answer(r) for r in responses]
        
        # 多数投票
        counter = Counter(answers)
        best_answer, vote_count = counter.most_common(1)[0]
        confidence = vote_count / len(answers)
        
        return {
            "answer": best_answer,
            "confidence": confidence,
            "all_answers": answers,
            "responses": responses,
        }


# 使用示例
sc = SelfConsistency(n_samples=10)
result = sc.solve("一个水池有两个进水管A和B,A单独20小时注满,B单独30小时注满。同时开3小时后关闭A,B还需几小时注满?")
print(f"答案: {result['answer']}")
print(f"置信度: {result['confidence']:.0%}")

成本优化策略

自适应采样

不需要对所有问题都采样 10 次——简单问题 3 次就够了:

def adaptive_self_consistency(question, solver, 
                                initial_n=3, max_n=10, 
                                confidence_threshold=0.8):
    """自适应采样:达到置信阈值即停止"""
    responses = []
    for i in range(max_n):
        resp = solver._sample(solver._build_prompt(question))
        responses.append(resp)
        
        if i + 1 >= initial_n:
            answers = [solver._extract_answer(r) for r in responses]
            counter = Counter(answers)
            top_answer, votes = counter.most_common(1)[0]
            confidence = votes / len(answers)
            
            if confidence >= confidence_threshold:
                return {
                    "answer": top_answer,
                    "confidence": confidence,
                    "samples_used": i + 1,
                }
    
    return {
        "answer": top_answer,
        "confidence": confidence,
        "samples_used": max_n,
    }

成本对比表

策略平均采样数GSM8K 准确率相对成本
固定 N=1 (CoT)156.2%1x
固定 N=101072.8%10x
自适应 (阈值0.8)4.371.5%4.3x

自适应策略以 4.3x 成本达到接近 10x 的效果,节省 57% 的 API 调用

实战建议

  1. 数学/逻辑问题首选 SC:这类问题有唯一正确答案,投票效果最佳
  2. 开放生成问题慎用:创意写作等没有唯一答案的场景,SC 可能导致趋同
  3. 注意答案归一化"3""3.0""三" 需要归一化后才能投票
  4. 并行化采样:用 asyncio 或批量 API 并行发送,避免串行等待
  5. 监控 token 消耗:SC 成本随 N 线性增长,务必设置 budget 上限

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。