文本蕴含(Textual Entailment)判断是自然语言处理中的经典任务:给定一个前提句和一个假设句,模型需要判断前提是否能够推出假设。传统的做法是让模型直接回答蕴含、矛盾或者中立,但在实际使用推理模型时,很多人发现模型经常给出犹犹豫豫的答案,比如“这句话在一定程度上可以被推出,但也存在歧义”这类表述。这种模糊输出既无法直接用于下游业务,也让自动化流程难以继续。问题的根源在于我们强迫模型在一个本质上存在不确定性的任务上输出确定性结论,更好的做法是让模型输出概率分布,再由我们根据业务需求自行设定阈值做最终决策。

一、为什么推理模型在蕴含判断上会犹豫
文本蕴含判断的本质是评估两个文本之间的逻辑推导关系,而这个关系并非总是非黑即白。比如前提是“小明昨天去了北京”,假设是“小明昨天离开了北京”,这两个句子之间到底算蕴含还是矛盾,不同标注者的判断可能都不一致。模型在训练时见过大量这类边界模糊的样本,学到的自然是一种“软性”的判断倾向,而不是硬性的结论。
另一个原因是解码方式的限制。当我们让模型生成“蕴含”或“矛盾”这样的文字时,模型实际上是在做一次采样。假设“蕴含”这个词的生成概率是0.51,“矛盾”是0.49,最终采样结果可能随机落在任意一边。同一组输入跑两次得到不同结论,这正是很多使用者遇到的“模型不稳定”现象。但这并不是模型能力不行,而是我们没有利用好模型内部本来就存在的概率信息。
此外,提示词设计不当也会加剧犹豫。如果提示词中要求模型“仔细分析后给出唯一答案”,模型在边界样本上会倾向于在推理文本中反复权衡,最后给出的结论反而更随意。与其压制不确定性,不如显式地要求模型把不确定性表达出来,这就是概率分布输出思路的出发点。
二、让模型输出概率分布的提示词设计
核心思路是把三分类任务改写为打分任务。我们要求模型对蕴含、矛盾、中立三个标签分别给出一个0到100之间的整数分数,三个分数之和必须是100。用整数而不是小数,是为了避免模型输出格式失控;限定总和为100,则是为了让输出可以直接归一化为概率分布。
一个经过验证的提示词模板如下:
PROMPT_TEMPLATE = """你是一个专业的文本蕴含判断器。
前提:{premise}
假设:{hypothesis}
请对以下三个标签分别打分,分数为0到100的整数,三个分数之和必须等于100:
- entailment(蕴含):前提可以推出假设
- contradiction(矛盾):前提与假设矛盾
- neutral(中立):前提无法确定假设真假
只输出JSON,格式如下,不要输出其他任何内容:
{{"entailment": 分数, "contradiction": 分数, "neutral": 分数}}
"""
def build_prompt(premise: str, hypothesis: str) -> str:
return PROMPT_TEMPLATE.format(premise=premise, hypothesis=hypothesis)
这里的几个细节值得注意。第一,明确要求只输出JSON,可以配合模型的JSON模式或者结构化输出参数一起使用,能极大降低解析失败率。第二,标签用英文单词加中文说明的方式呈现,模型对英文标签的输出一致性通常更好,中文说明则保证了语义理解的准确性。第三,不要在提示词里加入“请谨慎判断”之类的模糊指令,这类指令反而会诱导模型把分数往中间靠拢,三个标签各打三十多分,输出就失去了区分度。
还有一个实用技巧是温度设置。在调用API时把temperature设为0或接近0,可以让概率打分更稳定。有条件的话,同一个输入跑三次取平均分数,方差会明显下降,这一点在后面讲集成策略时还会展开。
三、概率解析与阈值决策的实现
拿到模型输出的JSON后,第一步是做合法性校验和归一化,第二步才是阈值决策。下面给出一段完整可用的Python实现,包含了容错处理:
import json
def parse_scores(raw_output: str) -> dict:
"""解析模型输出并归一化为概率分布"""
try:
# 兼容模型输出可能带有的markdown代码块包裹
text = raw_output.strip()
if text.startswith("```"):
text = text.strip("`")
if text.lower().startswith("json"):
text = text[4:]
data = json.loads(text)
scores = {
"entailment": float(data.get("entailment", 0)),
"contradiction": float(data.get("contradiction", 0)),
"neutral": float(data.get("neutral", 0)),
}
except (json.JSONDecodeError, ValueError, AttributeError):
return {}
total = sum(scores.values())
if total <= 0:
return {}
return {k: v / total for k, v in scores.items()}
def make_decision(probs: dict,
accept_threshold: float = 0.75,
reject_margin: float = 0.15) -> str:
"""基于阈值输出最终决策"""
if not probs:
return "parse_error"
top_label = max(probs, key=probs.get)
top_score = probs[top_label]
second_score = sorted(probs.values(), reverse=True)[1]
# 最高分未达到接受阈值,判定为不确定,转人工审核
if top_score < accept_threshold:
return "uncertain"
# 最高分与次高分差距太小,同样视为不确定
if top_score - second_score < reject_margin:
return "uncertain"
return top_label
这段代码体现了阈值决策的两个关键参数。accept_threshold是接受阈值,只有最高标签的概率超过它,才输出确定结论;reject_margin是边际阈值,要求最高分和次高分之间有足够的差距,防止出现两个标签各占一半的骑墙输出。任何不满足条件的样本都会落入uncertain类别,进入人工审核或者更昂贵的二次推理流程。这种设计把模型的模糊地带显式地暴露出来,而不是强行给一个可能错误的答案。
阈值的取值需要结合业务场景调整。在事实核查类应用中,错误的蕴含判断代价很高,接受阈值应该设得严格一些,比如0.8以上;在召回优先的信息筛选场景中,可以把阈值降到0.6,让更多样本被自动处理。一个确定阈值是否合适的简单方法是:拿一批人工标注好的样本跑一遍,统计不同阈值下的自动处理率和错误率,找到两者的平衡点。下面是一个简单的阈值评估示意:
def evaluate_thresholds(samples, human_labels, thresholds):
"""遍历阈值,统计自动处理率与准确率"""
results = []
for t in thresholds:
auto, correct = 0, 0
for probs, label in zip(samples, human_labels):
decision = make_decision(probs, accept_threshold=t)
if decision != "uncertain":
auto += 1
if decision == label:
correct += 1
results.append({
"threshold": t,
"auto_rate": auto / len(samples),
"accuracy": correct / auto if auto else 0,
})
return results
四、进阶优化:多次采样与标签平滑校准
单一一次推理的概率输出仍然可能带有噪声,特别是对于边界样本。一个成本可控的改进方案是多次采样取平均:同样的输入用temperature为0.7的设置推理三到五次,把每次的概率分布做平均。这样做的好处是,如果模型对某个判断真正有信心,多次采样的分布会高度一致;如果模型本身就在犹豫,平均后的分布会明显趋向中立,这本身就是有价值的信号。
另一个常见问题是打分分布的整体偏移。有的模型习惯把最高分打在70左右,有的则敢打95,这导致同一套阈值在不同模型上表现差异很大。解决思路是做简单的校准:收集一批带标注的样本,统计模型在各标签上被打分的实际分布,然后做线性缩放,把不同模型的分数映射到统一尺度上。经过校准后,同一套阈值可以跨模型复用,维护成本大幅降低。
最后,如果业务对准确率要求极高,可以引入多模型投票。让两个不同家族的模型分别输出概率分布,取加权平均后再过阈值。当两个模型意见严重分歧时,样本自动进入uncertain通道。实践表明,这种双模型方案通常能把自动判断的准确率提升三到五个百分点,同时把人工审核量控制在可接受的范围内,是准确率与成本之间一个不错的折中点。
总结来说,让推理模型输出概率分布再由阈值决策,本质上是把判断权从模型交还给工程侧。模型负责表达它对三种关系的置信程度,我们则根据业务的容错能力决定哪些判断可以直接采纳、哪些需要人工介入。这套方法实现成本低,效果稳定,非常适合用在信息抽取、事实核查、知识库问答等依赖蕴含判断的系统中。