
自我一致性Self-Consistency是提示词工程里少数几个“不需要改模型、不需要训练、加几行采样代码就能见效”的方法。它的做法并不复杂同一个提示词配上思维链让模型用非零温度生成多条推理路径再对最终答案做多数投票。听起来像是“多问几遍取众数”但真正落地时会撞上一堆细节——温度设多少、采样几条、答案怎么归一化、投票什么时候停、一致性分数能不能当置信度用。这些问题在概念介绍里基本看不到却是决定方案能否交付的地方。下面这些内容来自我把这套方法塞进若干实际任务后的记录覆盖方案选型、参数推断、代码实现和翻车现场适合已经会用提示词、想把准确率再往上顶一个档位的同学也适合刚接触思维链、想搞清楚“为什么多采样几次就更准”的入门读者。1. 自我一致性到底解决了什么问题1.1 一次贪心解码的隐患模型把错误走成了唯一的路先说清楚它在对抗什么。常规调用大模型时多数人用的是贪心解码或极低温度采样也就是每一步都挑概率最高的那个词。这个策略的问题是一旦模型在某一步选错了方向后面所有的推理都会沿着这条错路继续往下走而且走得非常自信。算术题里抄错一个数字、阅读理解里认错一个主体、分类任务里被一个干扰词带偏都属于这一类。关键在于单次解码只暴露了模型的一条思路而这条思路可能只是概率分布里排名第一的那条不代表它是正确的。用生活里的类比你问一个同事要方案他张嘴就给你一个你没法判断他是深思熟虑还是随口一说但如果你问十个人八个人给出同一个答案你对这个答案的信心就完全不同了。自我一致性就是把“问一次”改成“问多次并统计”用分布代替单点。这里有个容易被忽略的前提贪心解码的“确定性”只是假象。同一个提示词在不同批次、不同上下文长度下输出可能微妙漂移你以为的稳定复现其实并不牢靠。与其依赖一条不可控的路径不如主动引入多样性再收敛把不可控变成可统计。1.2 把推理路径当成隐变量投票为什么在数学上站得住自我一致性的理论解释很干净模型生成答案的过程可以拆成两步先隐式地选一条推理路径再由这条路径得出答案。单次解码相当于只采样了一条路径而正确答案往往能通过多条不同的合法路径到达错误答案则更依赖于某条特定的错路。举个具体例子。一道应用题问“三天共卖出多少件”正确解法可以先算每天平均、再乘三天也可以先算总量再核对这些路径写法不同但终点都是同一个数字。而如果模型在第一天就抄错了原始数字它有可能错成 120、也有可能是 150错误方向并不统一。于是正确答案的“票源”是分散但集中的错误答案的票源是分散且稀疏的多数投票自然把正确的那一簇选出来。这个解释同时给出了适用边界错误必须是相对独立的投票才有增益。如果模型对某类题目存在系统性误解比如把“同比”理解成“环比”那它十次采样里有十次都会错投票只会把错误答案的置信度从 0.6 顶到 1.0反而更危险。我在早期做财务报表问答时就踩过这个坑投票一致性极高的那批样本人工抽检错误率反而最高——因为系统性偏差被投票放大了。1.3 三类不适合上自我一致性的任务不是所有任务都值得这么折腾我筛下来有三类建议直接别用。第一类是答案不可判定或答案空间开放的生成任务。写摘要、写文案、写邮件十条输出十个样子没有“同一个答案”可以投票多数投票直接失效。这类任务要么做通用自我一致性让模型自己挑最一致的那条见 5.1 节要么老老实实换别的评估手段。第二类是单次准确率已经接近随机猜测的任务。投票是放大已有信号的前提是模型本身对这类问题有一定辨别力。如果单次准确率只有 30%说明模型的内部表征里根本没有抓住关键特征采一百次也只是把噪声投得更均匀。这种情况应该先回去改提示词、补示例、拆任务别急着加采样。第三类是延迟极其敏感的交互场景。自我一致性的成本是线性的一次请求要跑 n 次推理端到端延迟至少翻几倍。如果产品要求 2 秒内出结果你只能采 2 到 3 条增益有限。我的做法是把自我一致性放在异步链路或离线批次里同步链路只做轻量校验。注意判断“能不能用”最快的办法是拿 200 条标注样本固定提示词分别跑 n1 和 n10看准确率曲线有没有明显分离。如果两条曲线几乎重合说明这套方法对这个任务是无效的别再往下投入。2. 方案设计与关键取舍2.1 提示词模板先把思维链写稳再谈投票自我一致性依赖一个前提——模型输出的答案是“从推理路径里推导出来的”而不是直接拍脑袋。没有思维链多采样得到的只是同一个直觉的不同表述投票没有意义。所以提示词的第一个任务是强制模型把中间步骤写出来并且把最终答案放在一个固定、可解析的位置。我习惯的模板结构是四段任务说明、约束条件、推理要求、答案格式。推理要求里不要写“请仔细思考”这种空话要写具体的动作比如“先列出题目中的已知量再写出计算式最后给出结果”。约束条件要明确单位和精度不然同一道题会同时出现 3.5 和 3.50投票时被当成两个答案。答案格式是整套方案的基础。我的标准做法是用一个显式标记把最终答案包起来比如要求模型在最后一行输出“答案X”或者用\boxed{}包裹。标记要足够独特避免正文里出现同款字符导致解析歧义。同时要求模型“只输出一个最终答案不要给区间或备选”否则你在聚合阶段会收到一堆“约为 120 或 130”这种没法投票的字符串。提示不要在提示词里写“如果不太确定可以说可能”这种话会污染答案格式。明确要求模型必须给唯一确定的答案哪怕它是错的你才能在后面的环节用投票筛掉它。模板还有一个细节值得强调示例few-shot要克制。示例太多会让模型的输出高度同质化十条采样里九条的推理路径一模一样温度再高也没用。做自我一致性时我通常只用 1 到 2 个示例把多样性留给采样本身。2.2 采样条数与温度的账怎么算温度和采样条数是这套方法里唯一两个必须调的超参而且它们互相耦合。温度太低比如 0.1多条采样会高度趋同路径重复率可能超过 90%你花了 n 倍的钱拿到了 1.2 倍的信息量。温度太高比如 1.5 以上模型开始胡言乱语推理链断裂答案格式崩坏归一化阶段一片狼藉。我的经验区间是0.5 到 1.0top_p 配 0.9 到 0.95。如果任务的推理链很长、步骤很多往 0.6 靠如果是短分类、短问答可以到 0.9。采样条数 n 的取值一般落在 5、10、20 三档。下面这张表是我在自己几个任务上跑出来的粗略感受不是权威数据但能帮你建立成本直觉。采样条数 n相对单次调用的 token 开销典型准确率增益我实际用在什么场景11 倍基线格式转换、简单抽取33 倍2 到 4 个点同步链路延迟硬约束55 倍4 到 6 个点线上主力配置1010 倍6 到 9 个点效果与成本的平衡点2020 倍8 到 12 个点离线批量、高价值单据4040 倍边际增益常低于 1 个点抽样审计、基准校准增益随 n 增长是明显递减的因为投票的方差按 n 的平方根下降而成本是线性上涨。我通常把 10 作为默认值只有当投票份额低于阈值时才升级到 20 条这个“按需升级”的策略能把平均成本压到固定 20 条的一半左右。成本估算要算全。假设单条采样的提示词 600 token、输出 400 tokenn10那么一次用户请求消耗约 10000 token。日请求量 5000 次就是 5000 万 token 的日消耗。这个数在预算表上很扎眼所以必须配合去重、早停和缓存具体做法在 4.3 节展开。另外别忘了那个最容易漏的项——开发阶段的试错成本调温度、调 n、调提示词每一轮都是全量重跑我在第一个项目上光评估就烧掉了正式流量的三成。2.3 聚合规则的三档选择多数投票、加权投票、聚类投票投票规则决定最终输出它不是只有“数票数”一种写法。最基础的是严格多数投票把归一化后的答案分组计数票最多的获胜。平票时要有确定的裁决规则我一般按“取第一条出现的”或者“取数值居中的”必须写死不能随机否则线上结果不可复现。第二档是加权投票每个样本带一个权重再求和。权重可以来自模型返回的 token 对数概率虽然很多接口不直接给但可以用长度归一化的困惑度近似也可以来自一次额外的自校验调用——让模型对每条推理链打分再按分数加权。加权投票在长推理链任务上效果明显因为长链里那些“一眼错”的样本会被低分压下去。第三档是聚类投票适用于答案不能精确匹配的场景。比如答案是一段短句字面不同但语义相同这时先用相似度把答案聚成簇再在簇之间投票。相似度可以用字符级编辑距离也可以用一次轻量的语义比较调用。聚类投票的代价是复杂度上去了我一般只在答案空间是自然语言短句时才用。还有一个容易被跳过的判定——投票份额本身就是信息。份额 0.95 说明模型高度自洽可以直接放行份额 0.4 说明众说纷纭这时候正确答案往往不在票数第一的组里。我的表结构里固定存一个vote_share字段后面对它的利用率比投票结果本身还高具体用法见 3.4 节。3. 实操落地从零搭一个可复用的自一致性流水线3.1 第一步把任务改造成可判定的答案形式动手写代码之前先改造任务。判断标准很简单两个正确答案放在一起能不能用一行代码判断它们相等。能就适合投票不能就先想办法让它能。改造手段有三招。第一招是限制输出空间多选题只允许输出选项字母数值题只允许输出纯数字并规定小数位分类任务只允许输出预设标签之一。第二招是附加标准化说明比如要求“结果保留两位小数不要带单位不要写过程”。第三招是在代码侧做归一化把常见变体映射到统一形式比如“1/2”映射成“0.5”“三点五”映射成“3.5”。下面这个归一化函数是我在数值类任务里用了很久的版本逻辑很直白但覆盖了大部分坑import re def normalize_answer(raw: str) - str: if not raw: return s str(raw).strip() # 优先取 \boxed{} 包裹的内容避免正文里的数字干扰 m re.search(r\\boxed\{([^{}]*)\}, s) if m: s m.group(1) # 统一大小写、去掉千分位与空白 s s.lower() s re.sub(r[,\s], , s) # 去掉句末标点避免 120. 与 120 被当成两个答案 s re.sub(r[。.!?;:\“”‘’], , s) # 分数转小数 m re.fullmatch(r(-?\d)/(-?\d), s) if m and int(m.group(2)) ! 0: v int(m.group(1)) / int(m.group(2)) s f{v:.6f}.rstrip(0).rstrip(.) # 百分号转小数 if s.endswith(%): try: s f{float(s[:-1]) / 100:.6f}.rstrip(0).rstrip(.) except ValueError: pass return s这段代码有几个刻意为之的地方。先取 boxed 内容是因为模型的正文里经常出现中间计算结果直接抓最后一个数字容易抓错。去标点必须在分数转换之前否则“3/4。”这种带句号的输入会匹配失败。百分号转换放在最后是因为它的输入形态依赖前面的清洗结果。注意归一化函数一定要配套一份单元测试。我见过最隐蔽的 bug 是归一化把负数处理错了导致投票时正负答案被合并准确率看曲线还挺正常实际是在稳定输出错误结果。3.2 第二步采样循环、并发与早停采样层要做三件事并发跑 n 次、去重相同路径、够票就停。串行跑 10 次推理端到端延迟是单次的十倍以上线上根本没法接受所以并发是必须的。并发实现上我用线程池调用同步接口或者用异步客户端配信号量限流。关键点是每个请求单独设置随机种子或温度参数别指望框架帮你保证多样性。有些接口在同一个连接里连续调用会复用某些内部状态导致输出趋同实测下来加个微小的时间戳扰动或者显式传不同的 seed 更保险。早停规则可以做得非常确定。假设计划采样 n 条已经采了 k 条当前第一名 c1 票、第二名 c2 票剩余名额 r n - k。只要c1 - c2 r也就是剩下的票全给第二名也翻不了盘就可以立刻收工。这个判断是精确的不会牺牲任何准确率。from collections import Counter def should_stop(counts: Counter, sampled: int, planned: int) - bool: if sampled planned or not counts: return sampled planned ranked sorted(counts.values(), reverseTrue) c1 ranked[0] c2 ranked[1] if len(ranked) 1 else 0 remaining planned - sampled return (c1 - c2) remaining实测下来这套早停在答案集中度高的问题上能省掉 40% 到 60% 的调用量尤其是在 n20 的配置下效果最明显。答案分散的难题反而省不下来但那种题本来就需要更多票。采样循环里还要加两个保护。一是路径去重把归一化后的完整推理链哈希一下如果重复率超过 90%说明温度太低或模型输出空间太窄直接在日志里打警告必要时自动提高温度重跑一轮。二是格式失败重试解析不出答案的样本不要直接丢给它一次带格式纠正的重试机会成功率能提回来一半以上。3.3 第三步投票、置信度与兜底分流投票本身只有几行难的是围绕它的分流逻辑。我的做法是把结果分成三档高置信直接采纳、中置信二次校验、低置信拒答或转人工。def aggregate(answers, weightsNone): cleaned [normalize_answer(a) for a in answers] cleaned [a for a in cleaned if a] if not cleaned: return None, 0.0, {} cnt Counter(cleaned) if weights: score Counter() for a, w in zip(cleaned, weights): score[a] w top score.most_common(1)[0][0] share score[top] / sum(score.values()) else: top cnt.most_common(1)[0][0] share cnt[top] / len(cleaned) return top, share, dict(cnt)阈值的设定不能凭感觉。我的方法是拿验证集跑一遍把样本按 vote_share 分桶统计每个桶的准确率。典型的分布长这样份额 0.9 以上的桶准确率能到 97% 左右0.6 到 0.8 的桶掉到 80% 出头0.5 以下的桶常常只有五成多。这说明份额是有区分度的可以直接当置信度用。于是分流规则就出来了。份额大于 0.8 的直接输出0.5 到 0.8 的触发一次“升级采样”把 n 从 10 加到 20 再投一次或者走一次自校验小于 0.5 的直接进人工队列宁可慢也不要错。这套分流把自动通过率控制在七成左右人工量降到了原来的三分之一是我们那条链路最早见效的优化。还有一条经验值得单独说平票不要用随机。平票时我优先取数值型答案里的中位数非数值型取第一次出现的那个。原因很现实随机会让同一个输入在不同时间给出不同输出用户投诉“你们系统前后不一致”时你很难解释。3.4 第四步离线评估与灰度上线评估环节最容易被做糊。我的标准动作是固定三份集合一份 500 条以上的标注验证集、一份 200 条的困难集专门收集历史错例、一份 100 条的回归集每次改提示词都跑。三份都要缺一份就会出现“整体涨了但老问题复发”。评估要看的不只是最终准确率还有三条曲线。第一条是准确率随 n 的变化用来定 n 的上限第二条是vote_share 与正确率的关系用来定分流阈值第三条是成本与延迟曲线用来回复老板的“为什么这么慢”。这三条线上跑一次大概花掉半天时间但能省掉后面两周的反复。灰度的做法是把自我一致性做成一个开关按流量比例放量。第一天 5%看准确率指标和人工复核通过率第三天 20%看成本是否超出预算第七天 50%观察有没有长尾输入导致的成本尖峰。我印象很深的一次是放量到 30% 时突然发现某个格式异常的输入会触发无限重试直接把配额烧穿了幸好只在灰度阶段影响面很小。指标上除了准确率我会额外埋两个平均采样条数反映早停的有效性和低置信占比反映模型对当前输入分布的适应度。后一个指标掉得快往往意味着上游的数据分布变了提示词该更新了。4. 常见问题与排查实录4.1 采样越多反而越差三种典型翻车现场第一种是系统性偏差被放大。前面提过模型如果对某类问题有稳定的误解投票会把错误答案的置信度顶满你的分流规则一看份额 0.95 就自动放行错得又快又自信。排查方法是拿错误样本看 vote_share 分布如果错例的份额普遍偏高那问题不在投票而在提示词得回去修任务描述。第二种是温度过高导致推理链断裂。采样到链接的中后段开始跑偏答案格式崩坏解析失败率上升归一化后剩不下几条有效票。表现在指标上是“n 从 5 加到 20准确率反而掉了两个点”。排查办法很简单统计解析失败率超过 10% 就说明温度该降。第三种是提示词里的示例太少或太多导致的多样性失衡。示例太多、太相似路径重复率飙升投票等于同一个答案投了十遍示例太少模型对输出格式的理解不稳定每条采样都换一个格式。我的经验是把示例控制在一到两个且格式必须严格遵守内容可以笼统一点。下面这张表是我整理的排查速查表遇到指标不对时按顺序过一遍通常能定位到八成的问题。现象最可能原因快速验证方法处理动作加采样后准确率不升反降温度过高链断了统计解析失败率温度降到 0.6 以下重跑准确率不动成本猛涨路径重复率过高统计推理链去重后的数量提高温度或减少示例高份额样本错误率高系统性偏差抽检 vote_share0.9 的错例改提示词别指望投票平票比例异常高答案空间过于离散看分组计数的分布加强归一化或限制输出延迟翻了好几倍早停没生效看平均采样条数检查早停阈值逻辑4.2 答案抽取环节的静默失败这是最阴的一类问题因为它不报错。解析器把“答案约 120 件”解析成了“约120件”和“120”不是一个字符串投票时被分成两组两个组各拿一半票最后平票。或者更糟解析器抓了正文里的某个中间数字把正确答案覆盖掉了。我的做法是给解析器加三道关卡。第一道是格式校验如果模型没按约定的标记输出答案直接标记为解析失败而不是猜。第二道是类型校验数值题的结果必须能被转成 float转不了就是失败。第三道是范围校验比如订单金额不可能超过某个上限超了就是解析错了。三道关卡都过才进投票池其余走重试。实操心得解析失败率这个指标一定要单独监控。它掉下去的时候准确率往往不动因为失败的样本被静默丢弃了投票池变小看起来还挺正常。我在一个项目里就是靠这个指标发现了模型升级后输出格式的微妙变化。4.3 成本与延迟压不住怎么办成本问题有四个抓手按性价比排序。第一是早停前面算过答案集中的任务能省一半调用量几乎零成本就能上线。第二是缓存把提示词的哈希加上输入内容做键命中直接返回。自我一致性的输入往往是重复的同一个用户反复问、同一批单据格式相同缓存命中率能到 20% 以上。第三是分流先用一次低成本调用做难度判断简单题直接走 n3难题才走 n20。第四是模型分层采样用便宜的小模型最终校验用大模型这个组合在很多任务上比全程用大模型更划算。延迟问题基本靠并发解决但并发有上限接口侧的 QPS 限制是硬约束。我的做法是把 n 次采样拆成两批第一批 5 条先返回一个初步结果如果份额够高就直接给用户剩下 5 条在后台补跑用于统计和后续校准。用户感知的延迟只有一半代价是在少数情况下后台结果和前台结果不一致需要在日志里做好标记。5. 几个值得关注的变体与延伸玩法5.1 通用自我一致性答案自由文本时怎么投标准自我一致性要求答案可精确比较遇到写解释、写建议这类自由文本就没法用了。通用自我一致性的思路是让模型自己当裁判先采 n 条候选输出再把这些候选一起塞回提示词让模型判断哪一条和其余的“最一致”。这个方法的妙处在于它不需要外部评分器成本只多一次调用。缺点是引入了裁判偏差——模型倾向于选最长最像样的那条而不是最正确的那条。我的用法是把它当粗筛先砍掉明显跑偏的候选剩下的再交给人工或规则校验。实测在开放式问答上它能过滤掉大部分格式混乱和答非所问的输出但对细微的事实错误无能为力。5.2 跨模型投票让错误互相抵消同模型多次采样错误的独立性是有限的毕竟参数一样、偏置一样。跨模型投票是把同一个提示词发给两三个不同的模型再把所有答案汇总投票。不同模型的训练数据、对齐策略、词表都不同错误的耦合度低得多我在几个事实性问答任务上看到准确率比单模型内采样高出三到五个点。代价很明显要维护多套接口、要处理各家的格式差异、成本还要乘以模型数量。我的建议是只在错误代价极高的场景用比如合同条款抽取、金额核算这类错了要赔钱的地方。日常任务用单模型采样加早停就够了。5.3 用一致性分数做置信度与数据筛选一致性分数最被低估的用途不是提升单次准确率而是当一个免费的置信度信号。模型对某个输入很“确定”时多次采样的答案会高度集中很“犹豫”时答案会散开。这个信号不需要标注、不需要额外模型几乎是白送的。我用它做两件事。一是主动学习把份额最低的那批样本挑出来优先标注标注效率比随机抽样高好几倍因为模型最不确定的地方往往就是数据分布的边界。二是伪标签筛选在无标注数据上跑自我一致性只保留份额高于 0.9 的样本当训练数据噪声率明显低于直接取单次输出。我们有一次用这个方法扩充了一批训练集模型微调后的效果比用三倍规模但未筛选的数据还好。还有一个衍生用法是做回归检测。把固定测试集上的平均一致性分数记下来当基线每次改提示词或换模型版本就重跑一次。分数突然掉了说明模型对原来那批输入的确定性变差了即使准确率还没掉也是个早期预警信号。最后分享一个我个人踩过好几次坑之后才想明白的点自我一致性不是“精度旋钮”拧大一点就变准。它更像是一次统计校正前提是模型的错误是随机分布的。如果模型的错误是成体系的采样再多也只是把偏见固化。所以每次加完采样、看到准确率涨了我都会额外抽一批高份额样本人工看一眼确认这批“高置信”的答案是真的对而不是被投票包装过的错。这个习惯救过我两次都是在上线前的最后检查里发现的不然线上表现会是“用户越信任的答案错得越离谱”。另外如果你手头还没有标注集别急着上自我一致性先把那 200 条标注做出来。没有它你调温度、定阈值、选 n 全是盲调最后只能靠感觉判断“好像变好了”。数据先行这件事在这个方法上体现得格外明显。