第 10 章
第 10 章:自一致性 · Noul
第 10 章:自一致性 · Noul
本文整理自 Datawhale 开源项目 datawhalechina/jev-cookbook(CC BY-NC-SA 4.0),源文件:main/04_实战指南/01_自一致性Noul.ipynb。
本 notebook 把中文镜像站中的 Cookbook 改写成可以逐格运行、修改输入并观察结果的最小实验。 重复 Noul 量表并显式处理不确定概率。
运行方式与 ../03_架构模式/01_架构模式.ipynb 一致:有有效的 TYPESAFE_API_KEY 时调用真实的
TypeSafe API;没有 Key 或返回 401 时使用内置的离线示例答案。后续代码不区分两种模式,便于先学习
控制流,再切换到真实模型观察概率和置信度。
学习提示:先顺序运行全部单元格,再回到“定义 state”或“定义问题”的单元格修改内容,重新运行后面的单元格。 API Key 只从环境变量读取,不能写进 notebook。
0. 准备
0.1 安装依赖
%pip install -q -U typesafe-sdk0.2 创建客户端
import os
import statistics
import time
from pprint import pprint
from typesafe_sdk import (
Choice,
Score,
Noul,
TypeSafeClient,
TypeSafeAuthenticationError,
)
API_KEY = os.environ.get("TYPESAFE_API_KEY", "")
client = TypeSafeClient(api_key=API_KEY, model="jev-latest") if API_KEY else None
print("客户端已创建:模型=jev-latest,Key=", "已配置" if API_KEY else "未配置(将使用离线示例)")0.3 离线响应与统一调用入口
class _FakeAnswer:
def __init__(self, type_, **values):
self.type = type_
for key, value in values.items():
setattr(self, key, value)
class _FakeResponse:
def __init__(self, answers):
self.answers = answers
self.nouls = {k: v for k, v in answers.items() if v.type == "noul"}
self.choices = {k: v for k, v in answers.items() if v.type == "choice"}
self.scores = {k: v for k, v in answers.items() if v.type == "score"}
self.model = "jev-latest(离线示例)"
self.usage = _FakeAnswer("usage", input_tokens=0, output_tokens=0)
class TS:
offline = False
_warned = False
@classmethod
def call(cls, state, questions, offline_answers):
if client is None:
cls.offline = True
if not cls._warned:
cls._warned = True
print("⚠️ 未设置有效 TYPESAFE_API_KEY,以下输出使用内置离线示例。")
return _FakeResponse(offline_answers)
try:
return client.system_one(state, questions)
except TypeSafeAuthenticationError:
cls.offline = True
if not cls._warned:
cls._warned = True
print("⚠️ 未设置有效 TYPESAFE_API_KEY,以下输出使用内置离线示例。")
return _FakeResponse(offline_answers)
def answer_line(name, answer):
if answer.type == "noul":
return f"{name}: noul={answer.noul:.2f}"
if answer.type == "choice":
return f"{name}: choice={answer.choice} confidence={answer.confidence:.2f}"
return f"{name}: score={answer.score:.2f} confidence={answer.confidence:.2f}"
print("模式:", "离线示例" if TS.offline else "真实 API(首次调用后确定)")0.4 连通性测试
if client is None:
TS.offline = True
print("⚠️ API Key 未设置,后续单元格使用离线示例。")
else:
try:
ping = client.system_one("你好", {"is_greeting": Noul(instructions="这段文字是在打招呼吗?")})
print("✅ API 连通正常,后续单元格会使用真实结果。")
except TypeSafeAuthenticationError:
TS.offline = True
print("⚠️ API Key 无效,后续单元格使用离线示例。")1. 自一致性:Noul
对同一份理赔状态重复提问,观察每个布尔判断的概率是否稳定;然后把中间概率标成 uncertain,
让业务代码把不确定结果交给人工审核,而不是强行变成 True / False。
1.1 定义理赔状态
CLAIM = {
"claim_type": "车险理赔",
"description": "车辆在雨天打滑撞上护栏,车门凹陷但仍可缓慢行驶。保单刚过等待期。",
"photos": "已提交车辆侧面和现场照片",
}
print("state 已定义:车险理赔,字段数=", len(CLAIM))1.2 定义 Noul 评分量表
QUESTIONS = {
"covered": Noul(instructions="这份理赔是否属于保单承保范围?"),
"repairable": Noul(instructions="车辆是否仍然可以维修,而不是必须报废?"),
"fraud_flag": Noul(instructions="这份理赔是否存在明显的欺诈信号?"),
"rental_eligible": Noul(instructions="客户是否符合租车替代服务的条件?"),
"manual_review": Noul(instructions="这份理赔是否应该交给人工审核?"),
}
print("questions 已定义:", len(QUESTIONS), "个 Noul 问题")1.3 重复调用并收集概率
OFFLINE_RUNS = [
{"covered": .84, "repairable": .93, "fraud_flag": .09, "rental_eligible": .47, "manual_review": .56},
{"covered": .82, "repairable": .92, "fraud_flag": .11, "rental_eligible": .51, "manual_review": .59},
{"covered": .85, "repairable": .94, "fraud_flag": .08, "rental_eligible": .49, "manual_review": .54},
{"covered": .83, "repairable": .91, "fraud_flag": .10, "rental_eligible": .53, "manual_review": .58},
{"covered": .84, "repairable": .92, "fraud_flag": .09, "rental_eligible": .50, "manual_review": .57},
]
N_RUNS = len(OFFLINE_RUNS)
runs = []
for index in range(N_RUNS):
offline = {name: _FakeAnswer("noul", noul=value) for name, value in OFFLINE_RUNS[index].items()}
response = TS.call(CLAIM, QUESTIONS, offline)
sample = {name: answer.noul for name, answer in response.nouls.items()}
runs.append(sample)
print(f"第 {index + 1} 次:", " | ".join(f"{k}={v:.2f}" for k, v in sample.items()))1.4 统计稳定性与不确定区间
def noul_decision(probability):
if 0.30 <= probability <= 0.70:
return "uncertain"
return "yes" if probability > 0.70 else "no"
for name in QUESTIONS:
values = [sample[name] for sample in runs]
deviation = statistics.stdev(values) if len(values) > 1 else 0.0
mean = statistics.mean(values)
print(f"{name:18} mean={mean:.2f} stdev={deviation:.3f} decision={noul_decision(mean)}")观察:noul 是“为真”的概率本身,没有额外的 confidence 字段。阈值是业务策略,不是模型返回的事实;把 0.30–0.70 留给人工审核,能保留模型的“不确定”信号。
知识补充
- 对付非确定性:同一命题重复问几次再取均值/多数,是官方给的自一致性方案。社区实测(JevBench)同一套题隔 16 分钟跑两遍只有 1.2% 的答案漂移——多数请求一次就稳,自一致性留给临界样本(概率在 0.4–0.6 之间的)更划算。
- 成本账:Jev 只按输入 token 计费、输出免费,重复 n 问 ≈ 成本 ×n,但延迟几乎不变(同一次调用里并排问多个同义问题即可,不用发 n 次请求)。
- 进阶阅读:分布形状与置信度门控见
../02_核心概念/04_置信度.ipynb;按置信度分流动作见../03_架构模式/01_架构模式.ipynb。
小结
这本 notebook 的边界很清楚:TypeSafe 只负责受限、可编程的判断;排序、阈值、分组、重建文本和 函数分派都由 Python 代码完成。修改输入或问题后重新运行,就能观察“模型答案 → 确定性代码”的变化。