自洽性:Noul
同一份材料问两遍,答案应该一样。理赔分流(直接赔付、拒赔、转人工)靠的是每个检查项的概率:如果概率在两次请求之间来回跳,恰好落在阈值附近的那一项就会一次赔、一次不赔。
这篇拿一份车险理赔单,配 14 个是非检查项(全部是 Noul),同一请求重复 15 次,看每个概率稳不稳。结论先放在这里:象信一号在 15 次重复里,每题概率的平均标准差为 0.0037,最大单题极差 0.03,14 题中没有一题跨过 0.5。 但"稳"不等于"对":在 11 道有明确参考答案的题里,象信一号按 0.5 阈值答错了 4 道,而且每次都错在同一个方向。后半篇讨论为什么这两件事要分开看,以及怎样用一个"待人工"区间承接不确定的判断。
LLM 对照组的数字待补
TypeSafe 原文把几种大模型(温度 0、默认温度、只答是/否)放在一起对比。本文的对照组代码已经写好(DeepSeek,OpenAI 兼容接口,llm_baseline.py),但本次运行时 LLM 密钥尚未就绪,下面所有表格只有象信一号的真实数字,LLM 一列暂缺,没有任何估算或编造的值。密钥就绪后重跑脚本即可补齐。
环境准备
pip install xiangxin-sdk openai
export XIANGXIN_API_KEY="sk-xx-..."
# 可选:LLM 对照组(OpenAI 兼容接口)
export LLM_BASE_URL="https://api.deepseek.com" LLM_API_KEY="..." LLM_MODEL="..."本次运行:xiangxin-latest(返回模型版本 xiangxin-1.0.0,30 次调用全部一致),采样时间 2026-09-25(UTC)。
数据:一份带争议点的车险理赔单
保单条款部分逐字摘录自《中国保险行业协会机动车商业保险示范条款(2020 版)》(中国保险行业协会 2020 年 9 月发布的公开示范条款,各财险公司官网均有全文,原文文本见仓库 data/示范条款2020_原文.txt)。投保信息、理赔申请和系统备注为本文构造,故意埋了几个需要判断的点:
- 驾驶人去赛车场参加"赛道开放日",而条款第九条把"竞赛、测试期间"列为责任免除;但事故发生在赛车场外的观众停车场,车辆熄火停放,被倒车的社会车辆撞了后保险杠,当天没有进赛道。
- 索赔明细里有一笔 1,300 元的代步车费用,但保单没有投保附加修理期间费用补偿险。
- 出险 2026-06-28 15:40,报案 2026-07-01 10:05,约 66 小时,超过第四十条要求的 48 小时。
- 没有交警事故认定书,车辆已在 4S 店先行拆检(第十五条要求修理前会同保险人检验)。
- 保单约定 10% 绝对免赔率,但"自动核赔"备注已经按 13,100 元全额核准赔付。
state 直接传 JSON 对象,不必先拼成字符串:
CLAIM = {
"保单": {
"保单号": "PDAA2026330100000417",
"被保险人": "陈某",
"保险期间": "2026-01-15 00:00 至 2027-01-14 24:00",
"承保险种": {
"机动车损失保险": {"投保": True, "保险金额": 186000.00},
"机动车第三者责任保险": {"投保": True, "每次事故责任限额": 2000000.00},
"附加绝对免赔率特约条款": {"投保": True, "绝对免赔率": "10%"},
"附加修理期间费用补偿险": {"投保": False},
},
"条款摘录(示范条款 2020 版原文)": {
"第六条": "保险期间内,被保险人或被保险机动车驾驶人(以下简称“驾驶人”)在使用被保险机动车过程中,因自然灾害、意外事故造成被保险机动车直接损失,且不属于免除保险人责任的范围,保险人依照本保险合同的约定负责赔偿。",
"第九条(三)3": "被保险机动车有下列情形之一者,不论任何原因造成被保险机动车的任何损失和费用,保险人均不负责赔偿:……竞赛、测试期间,在营业性场所维修、保养、改装期间。",
"第十五条": "因保险事故损坏的被保险机动车,修理前被保险人应当会同保险人检验,协商确定维修机构、修理项目、方式和费用。",
"第十七条": "因第三方对被保险机动车的损害而造成保险事故,被保险人向第三方索赔的,保险人应积极协助;被保险人也可以直接向本保险人索赔,保险人在保险金额内先行赔付被保险人,并在赔偿金额内代位行使被保险人对第三方请求赔偿的权利。",
"第四十条": "发生保险事故时,被保险人或驾驶人应当及时采取合理的、必要的施救和保护措施,防止或者减少损失,并在保险事故发生后 48 小时内通知保险人。",
"附加绝对免赔率特约条款": "主险实际赔款=按主险约定计算的赔款×(1-绝对免赔率)",
},
},
"理赔申请": {
"报案号": "RDAA2026330100088123",
"出险时间": "2026-06-28 15:40",
"报案时间": "2026-07-01 10:05",
"驾驶人": "陈某之子 陈某某(持 C1 驾照)",
"出险经过": "驾驶人报名参加某国际赛车场举办的“赛道开放日”活动。事故发生在赛车场外的观众停车场,"
"本车处于熄火停放状态,被另一辆倒车的社会车辆撞击后保险杠。本车当天未进入赛道。",
"索赔金额": 13100.00,
"费用明细": [
{"项目": "后保险杠总成更换", "金额": 6800.00},
{"项目": "后部钣金喷漆", "金额": 3200.00},
{"项目": "倒车雷达校准", "金额": 1800.00},
{"项目": "代步车租赁(5 天)", "金额": 1300.00},
],
"已提交材料": ["4S 店维修报价单(已在 4S 店拆检)", "现场照片 8 张", "对方车辆车牌照片"],
"未提交材料": ["交警事故认定书或双方协议书"],
},
"系统备注": [
{"来源": "自动核赔", "内容": "车损险有效,自动通过。按索赔金额 13,100 元全额赔付给被保险人,3–5 个工作日到账。"}
],
"历史理赔": {"近 12 个月出险次数": 2, "其中拒赔": 0},
}问题设计:14 个 Noul
每题都写成"答'是'表示被检查的那件事成立",这样每一行的概率含义一致,象信的 noul 和大模型给出的概率可以直接对比。
from xiangxin import Noul
QUESTIONS = {
"covered": "这次损失是否属于机动车损失保险的保险责任范围?",
"exclusion": "是否有责任免除条款适用于这次损失?",
"on_track": "事故是否发生在车辆于赛道上行驶的过程中?",
"deductible": "自动核赔给出的赔付金额是否已经正确扣减了 10% 绝对免赔率?",
"docs_sufficient": "已提交的材料是否足以直接核定这笔赔案?",
"within_limit": "索赔金额是否在机动车损失保险的保险金额以内?",
"within_period": "出险时间是否在保险期间内?",
"reported_timely": "是否在条款要求的时限内通知了保险人?",
"rental_eligible": "代步车租赁费用是否属于本保单可以赔付的范围?",
"fraud_flag": "是否存在需要转交反欺诈调查的迹象?",
"auto_approved": "这笔赔款是否在没有人工理赔员审核的情况下被自动核准?",
"manual_review": "这笔赔案在赔付前是否应当转人工或主管复核?",
"line_items_sum": "费用明细各项金额之和是否等于索赔金额?",
"subrogation": "是否存在可能负有责任、保险人可以向其代位追偿的第三方?",
}
questions = {key: Noul(instructions=text) for key, text in QUESTIONS.items()}怎么问
象信:一次 system_one 请求带上全部 14 题,每题的 noul 就是答案为"是"的概率。跑两个条件,各 15 次:
xiangxin_uid:每次在 state 里加一个随机的uid字段(和 TypeSafe 原文、以及下面 LLM 对照组的做法相同),理赔单和问题不变;xiangxin_same:请求体逐字节相同。TypeSafe 原文说它的设置"无法区分对无关字段的敏感和同一请求本身的波动",多跑这一组就能把两者分开看。
from secrets import token_hex
from xiangxin import XiangxinClient
client = XiangxinClient()
def ask_xiangxin(state):
resp = client.system_one(model="xiangxin-latest", state=state, questions=questions)
return {key: resp.answers[key].noul for key in QUESTIONS}, resp.usage.input_tokens
uid_runs = [ask_xiangxin({"uid": f"{i}:{token_hex(4)}", "claim": CLAIM}) for i in range(15)]
same_runs = [ask_xiangxin({"claim": CLAIM}) for _ in range(15)]LLM 对照组:一条提示词里放 json.dumps(CLAIM) 和全部 14 题,要求模型只输出一个 JSON 对象,键为题目键、值为 0–1 的概率。三个条件各 15 次:概率·温度 0、概率·默认温度、只答"是/否"·温度 0(映射为 1.0 / 0.0)。每次提示词开头同样带一个随机 uid。无法解析的回复计为解析失败,不参与统计。
def rubric_prompt(mode: str, sample_index: int) -> str:
if mode == "yesno":
fmt = "\n\n请逐题回答“是”或“否”。\n只输出一个 JSON 对象:键为题目的键,值为 \"是\" 或 \"否\",每题一项。"
else:
fmt = "\n\n请对每一题给出答案为“是”的概率。\n只输出一个 JSON 对象:键为题目的键,值为 0.00 到 1.00 之间的数字,每题一项。"
return (
f"uid: {sample_index}:{token_hex(4)}\n\n"
f"文档(一份车险理赔材料):\n{json.dumps(CLAIM, ensure_ascii=False, indent=2)}\n\n题目:\n"
+ "\n".join(f"- {k}: {q}" for k, q in QUESTIONS.items())
+ fmt
)完整的请求、解析和统计代码见文末仓库链接(main.py、llm_baseline.py、report.py)。
结果
稳定性总表
"平均每题标准差"是 14 题各自在 15 次里的标准差再取平均;"跨 0.5 的题数"是 15 次里最小值和最大值落在 0.5 两侧的题数,也就是用 0.5 做阈值时会出现一次"是"、一次"否"的题。
| 条件 | 调用次数 | 平均每题标准差 | 最大单题极差 | 跨 0.5 的题数 | 解析失败 |
|---|---|---|---|---|---|
象信 xiangxin_uid | 15 | 0.0037 | 0.03 | 0 | 0 |
象信 xiangxin_same | 15 | 0.0016 | 0.01 | 0 | 0 |
| LLM 概率 t=0 | 待补 | 待补 | 待补 | 待补 | 待补 |
| LLM 概率 默认温度 | 待补 | 待补 | 待补 | 待补 | 待补 |
| LLM 是/否 t=0 | 待补 | 待补 | 待补 | 待补 | 待补 |
两点值得注意:
- 同一请求重复 15 次,概率也不是逐位相同(
xiangxin_same的最大极差 0.01)。一个可能的原因是服务端做批量推理,批次组成不同会带来浮点层面的微小差异,反映到两位小数上就是偶尔差 0.01。 - 加入随机
uid后波动略大(最大极差 0.03,出现在within_period:0.68–0.71),并且within_period的中心也从 0.74 移到了 0.69 左右。一个无关字段能让个别题移动几个百分点,这是真实存在的敏感性,写 state 时应尽量只放和判断有关的内容。
每题的取值范围(15 次的最小–最大)
| 题目 | 象信 xiangxin_uid | 象信 xiangxin_same | LLM 各条件 |
|---|---|---|---|
covered | 0.81–0.83 | 0.81–0.81 | 待补 |
exclusion | 0.29–0.30 | 0.30–0.30 | 待补 |
on_track | 0.06–0.07 | 0.07–0.07 | 待补 |
deductible | 0.69–0.70 | 0.68–0.69 | 待补 |
docs_sufficient | 0.42–0.43 | 0.41–0.42 | 待补 |
within_limit | 0.95–0.95 | 0.95–0.95 | 待补 |
within_period | 0.68–0.71 | 0.74–0.74 | 待补 |
reported_timely | 0.80–0.81 | 0.79–0.79 | 待补 |
rental_eligible | 0.76–0.78 | 0.76–0.77 | 待补 |
fraud_flag | 0.22–0.23 | 0.23–0.23 | 待补 |
auto_approved | 0.96–0.96 | 0.95–0.95 | 待补 |
manual_review | 0.26–0.27 | 0.27–0.28 | 待补 |
line_items_sum | 0.94–0.95 | 0.94–0.94 | 待补 |
subrogation | 0.86–0.86 | 0.84–0.85 | 待补 |
稳定,但不一定对
为了说明"稳定"和"正确"是两回事,我们按条款和材料给 11 道题写了参考答案(common.py 里的 REFERENCE;covered、exclusion、fraud_flag 三题有争议,不设答案)。这是本文作者的判断,不是权威理赔结论。按 0.5 阈值看 15 次中多数的方向:
| 条件 | 有参考答案的题数 | 多数方向正确 | 答错的题 |
|---|---|---|---|
象信 xiangxin_uid | 11 | 7 | deductible、reported_timely、rental_eligible、manual_review |
象信 xiangxin_same | 11 | 7 | 同上 |
| LLM 各条件 | 待补 | 待补 | 待补 |
四道错题都需要一步"推算":deductible 要先算出 13,100 × 0.9 再和备注比;reported_timely 要算出 66 小时再和 48 小时比;rental_eligible 要把明细里的代步车和"附加修理期间费用补偿险:未投保"对上;manual_review 则要把前面几处疑点汇总起来。象信一号是一个 9B 的系统一模型,一次前向直接给概率,不做逐步计算,这类多跳推理正是它的弱项(见 象信一号 1.0 的能力边界)。在生产里,这几项更适合用代码直接算(日期差、金额比对、险种是否投保),而不是交给任何模型"判断"。
同样值得看的是它没答错的地方:on_track(0.07)正确区分了"去了赛车场"和"在赛道上行驶";line_items_sum(0.94)、subrogation(0.86)、auto_approved(0.96)都明确;docs_sufficient(0.42)方向正确但把握不大。
成本与速度
| 条件 | 平均输入 token | 每次调用成本 | 延迟(中位数 / 最快) |
|---|---|---|---|
象信 xiangxin_uid | 1,259 | ¥0.0000529 | 6.7 s / 3.1 s |
象信 xiangxin_same | 1,245 | ¥0.0000523 | 15.6 s / 2.3 s |
| LLM 各条件 | 待补 | 待补(按 usage × DeepSeek 官方价目估算) | 待补 |
象信成本按 ¥0.042 / 百万输入 token、输出免费计算。本次的延迟数字不代表正常水平:采样时同一套服务正在同时跑多篇实战手册的批量任务,两张 GPU 上排队明显,部分请求还遇到了 500 后重试。这里如实列出,但不拿它和任何基线比较速度。
允许"待人工",而不是强行二选一
用 0.5 做阈值时,0.49 和 0.51 会触发相反的动作,尽管二者表达的都是"拿不准"。更稳妥的做法是在应用层加一个区间:
- 低于 0.30 →
否; - 0.30 到 0.70(含两端)→
待人工; - 高于 0.70 →
是。
这只是对返回概率的一段应用逻辑:不需要新问题,也不需要第二次调用。区间边界只是示意,不是校准过的保证,生产中应当用有标注的样本、结合误判和人工复核的代价来定(见 置信度 与 置信度门控路由)。
def noul_decision_with_uncertainty(p: float) -> str:
if p < 0.30:
return "否"
if p > 0.70:
return "是"
return "待人工"把它套到 xiangxin_uid 的 15 次结果上(每题下面是 15 次的决策分布,逐次概率见仓库 results/tables.md):
| 题目 | 决策分布 |
|---|---|
covered | 是 × 15 |
exclusion | 否 × 4,待人工 × 11 |
on_track | 否 × 15 |
deductible | 待人工 × 15 |
docs_sufficient | 待人工 × 15 |
within_limit | 是 × 15 |
within_period | 待人工 × 14,是 × 1 |
reported_timely | 是 × 15 |
rental_eligible | 是 × 15 |
fraud_flag | 否 × 15 |
auto_approved | 是 × 15 |
manual_review | 否 × 15 |
line_items_sum | 是 × 15 |
subrogation | 是 × 15 |
这张表同时说明了区间的好处和它自身的边界:
- 好处:
deductible(0.69–0.70)在 0.5 阈值下是一个稳定的错误"是",放进区间后 15 次都变成了"待人工",错误被拦了下来;docs_sufficient也一样。 - 区间也有边:
exclusion的概率在 0.29 和 0.30 之间,正好骑在下边界上,于是 4 次判"否"、11 次判"待人工";within_period在 0.68–0.71 之间,骑在上边界上。模型的波动只有 0.01–0.03,但只要概率贴着某条边,决策照样会翻。区间把"是/否对翻"变成了"自动/人工之间翻",后果轻了很多,但并没有让模型更确定。 - 区间救不了自信的错误:
reported_timely(0.80)、rental_eligible(0.77)、manual_review(0.27)都落在区间外,而且方向是错的。这类错误要靠把可计算的检查交给代码、或者用标注数据发现并改写问题来解决,而不是靠调阈值。
讨论
为什么象信的概率这么稳。 象信一号不采样生成文本,一次前向直接读出每个选项的概率,同一输入的数值差异只在百分位上;大模型的"概率"则是它写出来的一段文字,会随采样、随提示里的无关字段变化。这也是 TypeSafe 原文的核心论点;LLM 一侧的实测数字待本文对照组重跑后补上。
稳定是下限,不是上限。 一个每次都给出同一个错误答案的模型,自洽性满分。这篇里 4 道错题就是例子。自洽性测试能告诉你"可以放心地用阈值做路由",但阈值定在哪里、哪些题根本不该交给模型,要靠有标注的样本来决定。
什么时候不该这样用。 日期差、金额加总、某个险种是否投保,这些能用一行代码确定的检查,就用代码。把模型留给真正需要"读懂材料"的判断:on_track 这种"去了赛车场但没上赛道"的区分,正是它做得好的地方。
完整代码
https://github.com/xiangxinai/xiangxin-cookbooks/tree/main/consistency_noul_cookbook
common.py:理赔单、14 个问题、参考答案main.py:象信两个条件各 15 次,结果写入results/xiangxin_runs.jsonllm_baseline.py:LLM 对照组(3 个条件 × 15 次),结果写入results/llm_runs.jsonreport.py:汇总生成results/summary.json与results/tables.md

