你们有没有遇到过这种情况:一个AI智能体,开发的时候测试集跑得漂漂亮亮,一上线就被真实数据打得措手不及。不是代码逻辑有bug,而是藏在链路里的某次LLM调用,这次结果是对的,下次结果是错的,毫无规律。前阵子我帮一个内部团队排查工单分拣Agent的问题,单看模型分类准确率能到90%上下,但每天几千条工单,哪怕漏几十条没有送到正确部门,客服体验就会肉眼可见地崩。后来我们把核心分类节点改成冗余执行,一次运行变多次采样、多样性投票,问题才算真正压下去。
“冗余执行”这个说法听着简单——多跑几次,少数服从多数——但真正落地过的人都知道,这中间全是细节:N设多少?什么时候投票?投票依据是什么?成本怎么兜住?为什么自我批判循环会收敛到同一个错误答案?这些点不搞清楚,冗余执行只会从“可靠性兜底”变成“烧钱机器”。这篇文章我把实际用过的方案、踩过的坑,以及一个可以直接套用的最小可靠执行模板都梳理一下。
1. 为什么单个 LLM 调用总是让你不放心
1.1 LLM 的随机性不是坏味道,是内建属性
很多做智能体工程的人,下意识会把LLM当成一个普通函数:输入固定,输出就应该固定。这个假设从一开始就不成立。大模型在推理时,token的生成本质是一个概率采样过程,即便输入完全一致,只要温度参数不为0,每次采样走的概率分布路径就不同,输出自然有差异。这跟传统软件里的线程调度不确定还不是一回事——后者可以用锁和原子操作抹平,而LLM的输出空间本身就是开放的,同一个“帮我总结这段文本”,两次结果可能意思接近,但措辞完全不同。
为了调试方便,你会把temperature调到0,这只能让核心逻辑相对稳定,但很多模型即便在温度归零后,采样层面依然存在不完全确定的部分。更现实的问题是,我们恰恰需要温度带来的多样性去探索更优解。这种随机性不是缺陷,它是模型能够产生创造性输出的根源,但在需要结果可复现、可审计的业务场景里,它就是实打实的工程风险。我自己的习惯是,不要试图消除随机性,而是设计一套能容纳随机性、并把随机性转化为可靠输出的执行框架。
1.2 智能体链路:一步错,步步错
智能体相比单轮对话,最大的特点是它会“行动”:调用工具、读取文件、改写代码、查询数据库,每个动作的结果又会作为下一轮推理的上下文。这个链路越复杂,出错点就越多。我见过一个文档自动化处理Agent,总共五步:解析附件→提取关键字段→生成校验脚本→执行校验→输出报告。其中第二步提取字段偶尔会漏掉一个值,等走到第五步,报告里相关段落直接变成空白。线下排查时发现,根因就是第二步的一次LLM调用,在某个样本上抽样到了错误的分支。
这种问题最讨厌的地方在于,你很难通过加“重试”解决。重试只是把同一段代码再执行一遍,如果第一步已经产出了错误的结构化字段,后面步骤再稳定,也只是在一个错误的地基上盖楼。所以智能体的可靠性,不能只靠某一层重试机制,而要在关键节点主动引入多路径、多视角、多次验证的结构性冗余。你说它是过度设计也好,说它浪费算力也好,但在真实业务里,这是用可控成本换取结果置信度最直接的办法。
1.3 模型的“自信”不值得押上全部
调用LLM时,响应里通常会带confidence或者logprobs之类的置信度信息。很多团队把这个当成可靠信号直接用:分数高就放行,分数低就重试。但实际测试下来,模型的自信度和真实正确率并不完全挂钩。尤其是在开放生成任务里,模型可能用非常流畅、非常肯定的语气输出一个完全虚构的内容——这就是大家常说的幻觉。
幻觉的本质是:模型在自回归生成时,倾向于补充语法上合理的信息,而不是事实上正确的信息。所以单次调用的置信度,很大程度反映的是模型对自己生成文本流畅度的估计,跟任务成功率之间是弱相关。这也是为什么冗余执行不能单纯靠置信度来触发——应该把它当成一种常态化的执行方式,而不是某个“感觉不对”时才启动的应急开关。如果你把置信度当开关,大概率会在最需要兜底的时候发现那个信号早就失效了。
2. 冗余执行的几种典型形态
2.1 核心思想:用多样性换取确定性
冗余执行的基本原则其实很朴素:单次判断不可靠,那就做多次判断;但多次判断之间还要有足够的差异,否则跟执行一次没区别。这里的“多样性”是灵魂。如果同一个Agent在完全相同的配置下跑三次,大概率给出相似度极高的结果,投票意义很弱。而一旦我们在采样参数、提示词视角、甚至底层模型上制造差异,三次结果之间就会出现真正有信息量的分歧,这时候的投票和聚合才有价值。
这一点跟分布式系统的思路很像。分布式存储里,一份数据放三个副本,如果三个副本都因为同一个bug而损坏,冗余就失效了。所以真正的冗余要求副本之间存在故障独立性。映射到智能体场景,就是多次执行之间不能共享同一个脆弱点。至少要在采样策略、上下文组织,或者多个环节上做差异化处理。不要以为“我多调了几次API”就是冗余,那只能叫重复,不叫高可靠设计。
2.2 多次采样 + 投票:Self-Consistency 范式
最常用的冗余执行形态,是Self-Consistency范式:让同一个模型对同一个任务独立采样N次,得到N个候选结果,再用某种聚合逻辑选出一个最终答案。这个方法最早在数学推理场景里被证明有效,后来被广泛迁移到分类、信息抽取、SQL生成等任务上。它的逻辑是:如果多次采样得出的结论高度一致,那这个结论是正确答案的概率就比单次采样高得多。
具体操作上,有三点需要控制好:
- 采样多样性:temperature不能设得太低,一般0.7到1.0区间比较合适。太低,N次采样差异太小,投票退化成装饰品;太高,结果过于发散,聚合难度上升。
- 结果格式:候选输出尽量采用结构化格式,比如JSON、固定枚举值。结构化输出可以直接做精确匹配投票,省去文本相似度判断的误差。
- 聚合策略:分类任务用少数服从多数即可;文本生成类任务可以基于编辑距离、向量余弦相似度聚类,取最大簇内的代表结果。
我在工单分拣这个场景里,就把模型输出约束成固定格式的JSON片段,字段是intent枚举值,然后让模型跑5次,5次里至少3次给出同一个枚举值,就采用这个值;小于3次一致,则进入后置规则校验。线上运行了几个月,结果一致性明显比单次调用高,而且这几个参数都是从业务侧实际调出来的,不是拍脑袋定的。
2.3 自己审自己:反思与批判循环
投票解决的是“多个结果选一个”的问题,但还有一种常见情形:单个结果本身有提升空间。这时可以用反思循环,也就是Reflexion范式:让Agent先生成一版答案,再让模型自己扮演批判者,从不同角度检查这版答案的漏洞,随后根据批判结果重新生成一版,如此往复。
我见过一个辅助写周报的Agent,初版内容经常啰嗦且遗漏关键数据。最初的设计是强制它写得精炼一点,效果一般。后来改成两阶段:第一阶段自由生成,第二阶段让另一个提示词的批判角色提取“哪些信息缺失、哪些表述冗余”,再基于批判结果重写。重启后的文本质量明显上了一个台阶。这种冗余的本质不是多次执行同一个任务,而是执行“生成-评估-修正”的差异化循环,最终输出的信息量远超第一次版本。
需要提醒的是,批判循环不适合无限迭代。开放任务中模型自我批判的边际收益递减很快,一般两三轮就到头了。我在实际中设置的最大轮数就是3轮,超过3轮强制采用当前版本,避免陷入打磨泥潭。
2.4 换个视角交叉验证:LLM Judge 与多智能体
比“自己审自己”更进一步的,是把评价环节交给另一个独立视角。这里的独立视角可以是:
- 用一个不同的模型,专门扮演评审角色;
- 用同一个模型,但切换完全不同的提示词角色;
- 用一个更小但更确定的规则模型做合法性校验;
- 用一个完全独立的智能体实例,并行解决同一问题,最后做合并。
我们把这种机制叫“交叉验证”。它比反思循环更抗片面性,因为判断双方的知识盲区和偏好不同,更容易暴露出单一视角下的错误。代价是需要额外模型调用成本,适合用在错误代价较高的环节,比如最终交付物生成前,或者涉及对外发布的内容。
实际项目里,我一般不会让两个Agent完全独立求解后互评,成本太高。更经济的组合是:主体Agent负责产出,校验Agent只做“找茬”,输出一段精简的修正意见,再回灌给主体Agent。这个组合在成本和效果之间平衡得很好,你们可以按这个思路搭自己的链路。
3. 工程落地:关键参数与代码骨架
3.1 N 到底设多少,投票阈值怎么定
冗余执行的N是所有人关心的第一个参数。设小了没效果,设大了成本爆炸。根据我的落地经验,大多数任务N在3到5之间是个甜点区间。3次采样足够暴露出单次执行的不确定性;5次时投票的置信度开始有区分度;超过5次,成本曲线直线上升,但可靠性提升进入一个很明显的平台期。
投票阈值的选择更多取决于业务对“漏判”和“误判”的容忍度。如果分错后果严重,阈值就设高点,比如5次中要有4次一致才放行,否则走人工审核;如果只是内部打分场景,3次中2次一致就可以收工。最忌讳的是阈值定得太低,比如3次中1次一致就拍板,这时候冗余执行形同虚设。
我自己定的规矩是:阈值低于60%的投票等于没有投票。因为低于这个线,说明采样结果之间分歧很大,这时候硬选一个答案,跟猜硬币没有本质区别。
3.2 聚合策略:不能只会少数服从多数
文本类结果不能直接进行字符串精确匹配,一般要用相似度聚类。我在工程上最常用的聚合方式有这几种:
| 结果类型 | 推荐聚合方式 | 说明 |
|---|---|---|
| 固定枚举/分类 | 精确匹配计数 | 计算快,无歧义 |
| 短文本/命名实体 | 编辑距离或字符相似度聚类 | 简单可靠,阈值得调 |
| 长文本/总结 | 向量嵌入余弦相似度聚类 | 语义更鲁棒,需接入向量模型 |
| 代码/结构化数据 | LLM作为裁判做两两比较再汇总 | 效果最好,成本最高 |
无论用哪种方式,记住一个原则:先聚类,再统计最大簇的占比,占比达到阈值才算通过。不要直接在N个原始结果上做任何加权平均——LLM的原始输出空间不是连续向量空间,强行平均只会得到一坨语义混乱的东西,这是我最初犯过的错误。
3.3 成本与延迟的分级控制
冗余执行最被诟病的问题就是贵。实际上完全可以通过分级策略摊薄成本。我的建议是不要对所有环节无差别冗余,而是把链路拆开,标注每个节点的“错误影响面积”:
- 影响下游推理的节点:必须冗余;
- 只影响展示格式的节点:不需要冗余;
- 中间结果可以在下游被外部工具校验的:可以用轻量校验替代冗余。
还可以设置动态冗余:先用单次低成本执行,如果结果能够通过规则校验(比如JSON格式合法、枚举值在允许列表内、数值在合理区间),就直接放行;只有校验失败才启动多次采样投票。这种方法在大多数正常请求上只花一份钱,只在异常请求上多花几份钱,比无脑全量冗余省得多。
延迟方面,如果N次采样比较耗时,可以把它们改成并行调用。多个LLM请求之间天然独立,用asyncio.gather或者多线程并发即可。但要注意大多数模型服务有并发限制,需要压测确认在没有触发限流的前提下并行度能开到多少。
3.4 一个可以直接用的执行器骨架
下面是我在项目里用过的一个简化版冗余执行器骨架,结构上看个大概,你们根据自身场景调整:
import asyncio from dataclasses import dataclass from difflib import SequenceMatcher from typing import Callable, Awaitable, List # TaskRunner 抽象一次LLM调用,真实场景里封装的是模型API TaskRunner = Callable[[str, float], Awaitable[str]] @dataclass class RedundantConfig: n: int = 5 # 采样次数 threshold: float = 0.6 # 最大簇占比阈值 temperature: float = 0.8 # 采样温度,保证结果多样性 similarity: float = 0.85 # 文本聚类相似度阈值 def text_similarity(a: str, b: str) -> float: """基于字符的相似度,适用短文本;长文本可以替换为向量余弦。""" return SequenceMatcher(None, a, b).ratio() def cluster_results(results: List[str], similarity_threshold: float) -> List[List[str]]: """把N个候选结果聚成若干簇,返回簇列表。""" clusters: List[List[str]] = [] for item in results: target = None for c in clusters: if text_similarity(item, c[0]) >= similarity_threshold: target = c break if target is None: clusters.append([item]) else: target.append(item) return clusters async def run_redundant( task: str, runner: TaskRunner, config: RedundantConfig = RedundantConfig() ) -> str: # 并行执行N次独立调用 results: List[str] = await asyncio.gather(*[ runner(task, config.temperature) for _ in range(config.n) ]) clusters = cluster_results(results, config.similarity) main_cluster = max(clusters, key=len) main_ratio = len(main_cluster) / config.n if main_ratio >= config.threshold: return main_cluster[0] # 低于阈值时,说明结果分歧较大,不要硬选一个 return await fallback(task, main_cluster) async def fallback(task: str, candidates: List[str]) -> str: """兜底策略:可以换提示词重跑,或者把分歧结果交给人工,也可以转给一个更稳定的慢模型。""" # 示例:直接选相似度最高的簇内结果,真实项目建议加上人工或规则审核 return candidates[0]这段代码逻辑的关键点在于:如果结果簇占比高于阈值,说明多样性采样收敛了;低于阈值说明任务对模型来说确实存在歧义,这时硬拼出一个结果风险很大,老老实实走后置规则或者人工兜底。另外注意,聚类阈值和占比阈值是两个不同的参数,前者决定两个文本算不算“同一个意思”,后者决定“支持率”够不够高,不要混用。
4. 我在实际项目中踩过的坑
4.1 冗余不是简单的重复调用
第一版冗余执行器,我偷懒写了最简单的逻辑:同一个prompt、同一个温度跑5遍,取出现最多的结果。上线后发现,投票偶尔会给出一个看起来很合理、但其实是多数派错误的结果。原因就是几次采样之间缺乏分化,陷入了同一个局部偏好。后来我调整了策略,模拟不同视角的prompt模板轮换使用,比如让模型分别扮演“客服专家”“规则审计员”“中性执行者”三个视角各跑几轮,再把结果聚合。结果才开始真正稳定。
所以这里特别想强调一句:如果你要让冗余有价值,请务必在N次执行之间制造足够的多样性。多样性可以是温度、可以是prompt视角、可以是few-shot示例的随机排列、可以是底层模型的差异,但绝对不能是什么都不改、只跑N遍。那种冗余跑完,你得到的只是同一个不确定性乘了N。
4.2 Vote 可能会让你错得更彻底
前面提到“多数派错误”,这是投票机制的内在风险。如果大多数采样都沿着一个错误的思路走,投票不仅不会纠错,反而会把那个错误包装成高置信度结论。我在一个数据清洗Agent里遇到过这种情况:5次采样里5次都把“NaN”字符串当成了有效的空值标记,投票全票通过,但业务上它应该是非法字段。这类问题靠内部投票永远无法暴露,因为它们的一致性来自训练数据里的偏见,而不是任务本身。
解决方案只有一个:引入外部事实来源。要么给Agent提供工具,在判断前先查一下配置表、数据库或文件头信息,要么用独立的规则模型对最终输出做硬校验。冗余执行解决的只是“同一知识范围内的一致性”,它解决不了“知识本身是错的”这一类问题。
4.3 反思循环收敛到同一个错误
反思循环也有坑。最典型的是:模型生成的批判意见本身就建立在错误的假设上,于是新一轮生成等于在错误基础上继续修补。比如让AI写配置文件,初版缺了一个必要字段,批判阶段模型没看出来,反而建议调整格式,重写之后字段依然缺失。我后来加了一个简单的收敛检测:连续两轮产生的结果相似度超过95%,就强制中断循环并触发规则检查,而不是让它继续空转。
这个防呆逻辑建议直接抄走——任何自我批判型循环都需要有一个外部终止条件和收敛检测。没有终止条件的循环,在LLM时代不是一个功能,而是一个事故现场。
4.4 结构化约束比冗余更便宜
还有个反直觉的经验:很多场景下,先把输出格式收窄、加上可校验的约束,比增加冗余次数划算得多。例如强制模型输出JSON并用JSON Schema校验,如果解析失败直接重试一次,可能比跑5次采样的费用低一个数量级。冗余的价值是兜住“格式正确但语义错误”的漏网之鱼,而不是替弱提示词擦屁股。
我的原则是:能用工程规则约束的,不交给模型自觉;能用一次性校验解决的,不动用冗余投票;只有在规则校验覆盖不到、且错误代价足够高的语义判断节点,才上冗余执行。这个排序能帮你省下大量预算,也会让你的系统在关键场景下真正可靠。
5. 我总结的最小可靠执行模板
说了这么多,最后给一个可以直接照抄的落地模板,适用于大多数“输入一段文本→判断/分类/抽取→输出结构化结果”的智能体场景。
第一层:输入规范约束。在prompt里明确要求输出JSON,并携带可校验的Schema。这层成本最低,先做。
第二层:单次执行后立即做规则校验。检查输出是否为合法JSON、枚举值是否在允许集合内、必填字段是否存在。如果通过,直接交付,不启动冗余。这一步拦截了绝大多数低级错误,成本几乎等于没增加。
第三层:规则校验不通过,或者模型本身给出低置信度标记时,触发冗余执行。用3到5次并行采样,结果聚类后看最大簇占比。达到阈值就采用,达不到就走第四层。
第四层:兜底逻辑。可以是换一个更加稳定的模型重新执行一次,也可以把结果连同N个分歧候选一起推给人工审核。至少你要在代码里留一个接口,让系统知道自己“不确定”,而不是假装确定。
这套模板在不同项目里我用过多次,整体可靠性和成本控制都比较符合预期。刚开始跑的时候建议先把每条链路的日志和追踪做好,多观察几周再微调N和阈值,不要一上来就追求最理想的参数组合。
6. 我的最后一点体会
冗余执行不是银弹,它只是在“LLM单次结果天然不可靠”这个底层事实之上,人为构造的一个安全垫。真正决定系统可靠性的,还是你对业务场景的理解:哪些节点能出错,出错之后会造成多大影响,以及用什么成本去换回确定性。
我个人在项目里反复验证的一个原则是——先让成本最低的工程约束承担主要拦截工作,再把冗余集中在少数几个真正关键、且单次结果确实不可靠的节点上。不要做那种所有环节都跑五遍的暴力冗余,那不是设计模式,是预算黑洞。
如果你正在为一个智能体系统的上线压测发愁,我建议你先花半天时间,把线上日志里出错的任务类型拉出来,看看真正的错误分布,再决定在哪几个节点上引入冗余。这比照着别人的配置抄一个N参数有用得多,因为每个业务的错误形态不一样,冗余设计自然也不该一样。