Jev 置信度与兜底策略实战
Jev 的每个响应都带 confidence 分数——这一个字段,把模型从”尝鲜”变成”生产组件”。分数告诉你模型哪里有把握、哪里在猜,你就可以自动化有把握的、分流剩下的。这篇讲阈值怎么定、三条兜底路径怎么设计、没有标注数据时怎么冷启动、批量场景怎么处理,以及整个回路怎么监控才不会悄悄烂掉。
TL;DR: 阈值从 0.8 起步:以上自动化,以下分流。低置信度条目三选一——换更锋利的问法重试、升级人工、或给通用 LLM 做一次完整判断。持续盯 confidence 分布:整体往低漂移,是输入或问题措辞变化最早的警报。
confidence 在响应里是什么
每种题型的 Jev 答案都带同一形态。打分题示例——示例数据(example fixture),正式字段名以官方文档为准:
{
"answer": 8,
"scale": [1, 10],
"confidence": 0.86,
"rationale": "The answer addresses the core question but omits the setup step."
}
判断题(是/否)时 answer 是 "yes"、"no" 或 "unclear"。选择题时 answer 是所选选项的标签。所有题型下 confidence 都是 0.0 到 1.0 的浮点数,rationale 是模型的自然语言说明。
把它当路由信号读,不要当真理。有用的心智模型:confidence 把条目按”能否安全自动化”排序。分布的顶部给自动化,底部给人审,具体的切线由你的错误代价决定。
有一件事 confidence 不告诉你:答案是否客观正确。高置信度的错误答案完全可能存在——当问题措辞清晰但源文档本身有误导性时就会发生。confidence 衡量的是模型自身的确信程度,这与准确率相关但不等同。这正是为什么在自动化任何有实际后果的决策之前,必须先用标注数据验证阈值。
阈值怎么定:0.8 起步,用数据调
| 场景 | 建议起步阈值 | 理由 |
|---|---|---|
| 垃圾信息 / 审核标记 | 0.80 | 误伤惹恼用户;复核队列很便宜 |
| 工单分流 | 0.85 | 分错要花人工交接时间 |
| 简历 / 线索筛选 | 0.85–0.90 | 风险高;只自动化清晰段 |
| 发票审批闸口 | 0.90+ | 财务错误代价大;几乎不自动化 |
| 内部排序 / 重排 | 0.70 | 错误便宜、量大 |
这些数字是起点,不是法律。真正要紧的调参回路:
- 先跑一周,把每个答案连同 confidence 记日志,暂不对低置信度做动作。
- 人工标注低置信度样本的一部分。
- 算出错误实际从哪里开始——安全切线经常比 0.8 高或低。
- 把阈值定在那里,每季度回看。
没有标注数据时的冷启动
多数团队想在有标注数据之前就把阈值定好。一个可靠的冷启动方案:
- 影子模式跑五到七天。 在生产里正常调用,但不执行兜底逻辑——只把 confidence 分数连同输入和答案一起记日志。
- 抽分层样本。 从 confidence 的每个十分位各取约 50 条。你需要覆盖整个分布,而不只是尾部。
- 人工标注。 对每条样本,按自己的判断标记答案是否正确。
- 按分段画错误率折线图。 把样本分成 0.0–0.69、0.70–0.79、0.80–0.89、0.90–1.0 四段,看各段的错误率。错误率越过你可接受水平的那一段,就是你的阈值所在。
- 第一次上线时定得保守一点。 把 20% 的条目送去人审、事后发现太谨慎——这种情况很容易修正;自动化过多就不一样了。
如果影子模式做不到(这个任务还没上生产),用合成保留集代替:人工准备 200–500 条有代表性的样本,全部手工打分,用这些标注定阈值。比猜要准得多,虽然不如真实流量精准。
三条兜底路径
confidence 低于阈值时,应该发生且只发生以下三件事之一:
import json, os, requests
def judge_with_fallback(prompt: str, threshold: float = 0.8) -> dict:
# Confirm the exact model slug on the OpenRouter model page
resp = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json={
"model": "typesafe/jev-1.13",
"messages": [{"role": "user", "content": prompt}],
},
timeout=30,
)
result = json.loads(resp.json()["choices"][0]["message"]["content"])
if result["confidence"] >= threshold:
return result # 自动化
if result["answer"] == "unclear" or result["confidence"] < threshold / 2:
return {"route": "human"} # 深度不确定:绝不重试循环
return {"route": "llm_review", "jev": result} # 中间段:完整 LLM 判断
封装函数在置信时返回 fixture 形态的答案,否则返回路由决策;这里展示的形态都是示例数据(example fixture),正式字段名以官方文档为准。三条路按顺序:
- 换更锋利的问法重试——给中间段。经常是问题的问题,不是模型的问题:按《问题设计》那篇把边界写明再问一次。只重试一次,绝不循环。
- 升级人工——给深度不确定段,以及一切涉及钱或法律后果的条目。
unclear答案就是为这个存在的。 - 通用 LLM 完整判断——给”人不必看但一次调用能解决”的中间段。聊天模型写完整的论证;Jev 的低置信度答案作为上下文一并带过去。
正确实现重试路径
重试不是把同一个 prompt 再发一遍。目的是给模型更多信息,让问题能以更高确信度得到回答。常见的问题锐化方式:
- 加上它可能缺少的判断标准。 如果你问”这是高级工程师吗?“但没给定义,就补上:“高级工程师指有至少五年专业软件开发经验、并主导过至少一个项目从头到尾交付的人。”
- 缩小输出量表。 如果 1–10 量表返回低置信度,就改成对某个具体阈值的是/否判断:“根据这份简历,这位候选人在技术深度上能达到 7 分及以上吗?”
- 加一个正确答案的具体示例。 问题里加一个简短的工作样例,能大幅降低边界案例的歧义。
一定要在代码里硬性限制只重试一次。第二次仍然低置信度时再触发重试的逻辑,只会白烧成本、延迟条目处理。一次重试,然后路由到人工或 LLM:
def judge_with_one_retry(original_prompt: str, sharpened_prompt: str, threshold: float = 0.8) -> dict:
first = call_jev(original_prompt)
if first["confidence"] >= threshold:
return first
second = call_jev(sharpened_prompt)
if second["confidence"] >= threshold:
return second
# 两次都低于阈值——路由,不再重试
return {"route": "human", "attempts": [first, second]}
把 Jev 上下文传给 LLM 兜底
路由到通用 LLM 时,把 Jev 的低置信度答案作为先验上下文一并发过去,而不是让 LLM 从头开始。这样 LLM 能聚焦在解决不确定性上,而不是重做完整评估:
def llm_review_with_jev_context(item: str, jev_result: dict) -> str:
system = (
"You are reviewing a judgment made by a specialized AI model. "
"The model returned a low-confidence answer. Your task is to confirm, "
"correct, or override it with a full rationale."
)
user = (
f"Item: {item}\n\n"
f"Model answer: {jev_result['answer']}\n"
f"Model confidence: {jev_result['confidence']}\n"
f"Model rationale: {jev_result.get('rationale', '')}\n\n"
"Please provide your final judgment and reasoning."
)
# 在这里调用你的通用 LLM —— GPT-4o、Claude 等
...
批量处理时的 confidence 管理
单条调用的处理很直接。批量评估带来两个额外问题:需要逐条追踪 confidence,同时又不想因为批次里有几条低置信度的就对整批触发 LLM 兜底。
把批量循环设计成先分离高置信度(直接执行)和低置信度(路由到兜底),分别处理后按原始顺序合并结果,再交给下游消费者:
from concurrent.futures import ThreadPoolExecutor, as_completed
def process_batch(items: list[str], threshold: float = 0.8) -> list[dict]:
results = [None] * len(items)
needs_fallback = []
# 第一轮:对所有条目调用 Jev
for i, item in enumerate(items):
r = call_jev(item)
if r["confidence"] >= threshold:
results[i] = {"source": "jev", "result": r}
else:
needs_fallback.append((i, item, r))
# 第二轮:只对低置信度子集走兜底
with ThreadPoolExecutor(max_workers=5) as ex:
futures = {
ex.submit(llm_review_with_jev_context, item, jev_r): idx
for idx, item, jev_r in needs_fallback
}
for future in as_completed(futures):
idx = futures[future]
results[idx] = {"source": "llm_fallback", "result": future.result()}
return results
给 source 字段打上标记("jev" 对 "llm_fallback"),这样你就能按时间追踪每批次的兜底率。某类条目的兜底率突然上升,是问题需要改写的信号。
常见反模式
循环重试直到 confidence 够高。 这是最常见的错误。如果模型在同一输入上两次返回低置信度,第三次大概率还是低置信度。循环无法解决结构性的不确定性,只会烧钱加延迟。设一次重试上限,之后路由出去。
对所有题型用同一个阈值。 是/否判断题的 0.8 阈值,和 1–10 打分题的 0.8 阈值不是一回事。打分题在量表中段本身就有更多歧义,平均 confidence 分布会偏低。按题型分别校准,不要按部署整体校准。
丢掉 rationale 字段。 rationale 字段记录起来很便宜,丢了很贵。当一个高置信度答案事后证明是错的,rationale 通常是最快理解”模型为什么对错误答案这么确信”的地方。
把 unclear 和低置信度混为一谈。 unclear 是结构性拒绝——问题无法从提供的上下文中得到回答。低置信度是不确定的回答。把两者路由到同一个兜底路径,会把”需要改问题”的条目白白浪费成人工复核时间。
阈值定一次就不管了。 输入分布会漂移。上游数据源会改格式。新的边界案例会出现。六个月前正确的阈值今天可能已经错了。下面讲到的平均 confidence 指标,就是专门用来捕捉这种情况的。
监控回路
三个指标能在用户之前抓住大多数故障:
| 指标 | 能发现什么 | 告警条件 |
|---|---|---|
| 平均 confidence | 问题或输入漂移 | 连续数天下滑 |
| 升级率 | 阈值失配或流量变难 | 没有发版却突然跳升 |
| 兜底路径占比 | 形成重试循环 | 重试占比逐周上升 |
每个答案都要把走的路由(automate、retry、human、llm_review)一起记录下来。每周汇总兜底路径占比能捕捉重试循环——如果重试占比逐周上升,说明你的问题措辞已经漂移,锐化后的问题其实并没有更锋利。
升级率是触达人工的条目比例。如果没有发版却突然跳升,通常是三件事之一:上游数据变了、涌进来了你的问题没有覆盖到的新类型输入、或者读取 confidence 的代码有 bug 导致路由错误。按这个顺序排查。
LLM 兜底的成本预算,参见《成本与延迟》。经验法则:平均 confidence 在 0.85 以上,LLM 兜底影响的调用不到 15%,相对于准确率的提升,增量成本通常可以接受。
发票审批闸口的 case 是保守阈值的典型样本——几乎全审、几乎不自动化;工单分流场景则是高流量下相反的平衡。当”报错”其实是响应形态读错了而不是置信度低,《Jev 常见报错排查》有对应的修复。
本文适用版本 Jev 1.13。