第 20 章:引用核查
第 20 章:引用核查
本文整理自 Datawhale 开源项目 datawhalechina/jev-cookbook(CC BY-NC-SA 4.0),源文件:main/04_实战指南/11_引用核查.ipynb。
针对官方文档 实战指南 · Double-checking citations 的可运行实验笔记, 用真实 TypeSafe API(Jev 模型)复刻核心流程并中文化。中文翻译版见 datawhalechina.github.io/jev-cookbook。
笔记本结构
| 章节 | 内容 | 实验 |
|---|---|---|
| 0. 准备 | 安装库、配置客户端、连通性测试、离线回退 | — |
| 📖 理论速览 | System One / Choice / 置信度门控(精简) | — |
| 1. 核对引用 | 字符串匹配 → Choice → confidence 门控 | 短中文源文档 · 5 条引用 |
每个主题按固定节奏展开:原理 → 理论根基 → 定义数据 → 定义问题 → 调用 → 解读结果, 每个单元格只做一件事,可直接顺着跑完(约 约 4 次 API 调用(捏造引用跳过模型))。
运行要求
- Python ≥ 3.10(官方 SDK 要求;macOS 系统自带 python3 是 3.9,装不上 SDK)
- 一个 TypeSafe API Key(console.typesafe.ai/keys 获取)
推荐:一键创建本地环境(在本 notebooks 目录下)
./setup_env.sh # 创建 .venv:Python 3.12 + 全部依赖
export TYPESAFE_API_KEY=你的key
.venv/bin/jupyter lab <本文件>.ipynb或者手动创建:python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt
🔑 API Key 安全提示:本笔记从环境变量
TYPESAFE_API_KEY读取密钥, 不要把 Key 硬编码进笔记本(尤其打算提交到公开仓库时)。🈶 关于语言:实验全部使用中文
state与中文提示词。三种原语的选项 key (如billing、verified)属于代码标识符,保持英文以便代码分支判断; 它们的描述文字(criteria 值)均为中文,模型据此理解语义。
0. 准备
0.1 安装所需的库
如果已经用 ./setup_env.sh 创建过环境,本节通常显示“依赖已满足”;在其他环境里首次运行时会自动安装。
%pip install -q -U typesafe-sdk # 本笔记本必需(要求 Python ≥ 3.10)
# %pip install -q -U jupyterlab # 如本机还没有 Jupyter,取消注释运行一次
# %pip install -q -U nbformat nbclient # 仅在需要重新生成/批量执行笔记本时安装0.2 导入库并创建客户端
Choice/Score/Noul:三种问题原语的构造器(对应官方文档原语一章);TypeSafeClient:同步客户端,model="jev-latest"表示使用官方旗舰模型的最新别名;- Key 从环境变量
TYPESAFE_API_KEY读取;临时调试也可以直接赋值给API_KEY(不要提交)。
import os
import time
from typesafe_sdk import (
Choice, # 选择题:从命名选项中选一个,返回 choice + probabilities + confidence
Score, # 打分题:按有序量表打分,返回期望分 + probabilities + confidence
Noul, # 是非题:返回"是"的概率(0~1),本身就是概率所以没有 confidence 字段
TypeSafeClient,
TypeSafeAuthenticationError, # 401 鉴权失败时抛出
)
API_KEY = os.environ.get("TYPESAFE_API_KEY", "")
# API_KEY = "apikey_..." # ← 仅在临时调试时使用,注意不要提交到公开仓库
# Key 为空时不构造客户端:SDK 在无 Key 时抛 TypeSafeError(非 401 的
# TypeSafeAuthenticationError),不会被下面的回退捕获,会让整本笔记本中断。
client = TypeSafeClient(api_key=API_KEY, model="jev-latest") if API_KEY else None0.3 离线回退用的两个替身类
真实 API 不可用时,我们需要一个与官方 SDK 响应对象同构的替身,让后续分析代码不用改。官方 SystemOneResponse 的访问方式是:
| 访问方式 | 返回 |
|---|---|
resp.answers["名称"] |
全部答案(按问题名) |
resp.choices["名称"] |
选择题答案:.choice .confidence .probabilities |
resp.scores["名称"] |
打分题答案:.score .confidence .legend .probabilities |
resp.nouls["名称"] |
是非题答案:.noul(本身就是概率,无 confidence) |
resp.usage.input_tokens |
本次请求计费的 input token 数 |
下面的替身类暴露完全相同的属性,仅用于离线模式。
class _FakeAnswer:
"""单个答案的替身:按需挂属性(choice/score/noul/confidence/...)。"""
def __init__(self, type_, **kw):
self.type = type_
for k, v in kw.items():
setattr(self, k, v)
class _FakeResponse:
"""整个响应的替身:与 SystemOneResponse 同构(answers/nouls/choices/scores/usage)。"""
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)0.4 调用助手 ts.call()
统一入口:优先请求真实 API;只有当鉴权失败(401)时,才回退到各实验预置的离线示例数据,并在第一次回退时给出显著警告。这样拿到无效 Key 也能跑通全流程,而有效 Key 下全程真实。
class TS:
real_calls = 0 # 成功的真实调用计数
offline = False # 一旦回退过就置 True,后续单元格据此跳过真实计时等逻辑
_warned = False # 完整警告只打印一次,避免刷屏
def call(self, state, questions, offline_answers=None):
if client is None: # 未配置 Key:直接走离线示例,不触碰任何客户端
TS.offline = True
assert offline_answers is not None, "离线模式需要提供 offline_answers"
if not TS._warned:
TS._warned = True
print("⚠️ 离线示例模式:未设置 TYPESAFE_API_KEY,以下为内置示例数据而非真实模型结果;"
"设置有效的 TYPESAFE_API_KEY 后重跑本笔记本即可得到真实输出。")
else:
print("⚠️ (本次为离线示例数据,非真实 API 输出)")
return _FakeResponse(offline_answers)
try:
resp = client.system_one(state, questions)
TS.real_calls += 1
return resp
except TypeSafeAuthenticationError:
TS.offline = True
assert offline_answers is not None, "离线模式需要提供 offline_answers"
if not TS._warned:
TS._warned = True
print("⚠️ 离线示例模式:API Key 无效(401),以下输出为内置示例数据而非真实模型结果;"
"设置有效的 TYPESAFE_API_KEY 后重跑本笔记本即可得到真实输出。")
else:
print("⚠️ (本次为离线示例数据,非真实 API 输出)")
return _FakeResponse(offline_answers)
ts = TS()0.5 连通性测试
用一条最简单的是非题试连官方 API:Key 有效则提示通过;若返回 401,后面的实验会自动切换到离线示例模式(见下一节说明),流程照样能走通。
if client is None:
TS.offline = True
print("⚠️ 未设置 TYPESAFE_API_KEY,以下实验以【离线示例模式】运行:")
print(" 代码路径与真实调用完全一致,仅数据换成本笔记内置的示例值;")
print(" 在有效 Key 下重跑本笔记本即可得到真实模型输出。")
else:
try:
ping = client.system_one(
"你好",
{"is_greeting": Noul(instructions="这段文字是在打招呼吗?")},
)
print("✅ API 连通正常,Key 有效。将进行真实实验。")
except TypeSafeAuthenticationError:
TS.offline = True
print("⚠️ API Key 无效(401)。以下实验将以【离线示例模式】运行:")
print(" 代码路径与真实调用完全一致,仅数据换成本笔记内置的示例值;")
print(" 在有效 Key 下重跑本笔记本即可得到真实模型输出。")0.6 本章离线示例数据
下面是各引用(找到引文后)的 Choice 示例答案,仅在 Key 无效时才会被用到。捏造引用不会调用模型,因此不在此表。
# 实验:按 citation id 预置 relation Choice
CITATION_OFFLINE = {
"verified_ok": _FakeAnswer(
"choice", choice="supports", confidence=0.93,
probabilities={"supports": 0.93, "contradicts": 0.04, "says_nothing": 0.03},
),
"contradicted_ok": _FakeAnswer(
"choice", choice="contradicts", confidence=0.96,
probabilities={"supports": 0.02, "contradicts": 0.96, "says_nothing": 0.02},
),
"unsupported_ok": _FakeAnswer(
"choice", choice="says_nothing", confidence=0.88,
probabilities={"supports": 0.06, "contradicts": 0.06, "says_nothing": 0.88},
),
"low_conf": _FakeAnswer(
"choice", choice="says_nothing", confidence=0.42,
probabilities={"supports": 0.28, "contradicts": 0.30, "says_nothing": 0.42},
),
}📖 理论速览(精简)
本笔记本默认你已读过 System One / 原语 / 置信度 (中文镜像站有对应页)。此处只提醒实验会反复用到的三点:
- 请求模型:同一个
state+ 一组问题 → 类型化答案;问题彼此独立、按 ID 返回; - 三种原语:
Choice选命名选项;Score返回概率加权期望分(可为小数);Noul返回“是”的概率(无 confidence); - 代码掌控制权:阈值、路由、合并答案都在你的代码里;模型只回答狭窄、原子的判断。
💡 问题 ID 不会发给模型——完整语义写在
instructions/criteria里。
1. 核对引用(Double-checking citations)
先用字符串匹配确认引文是否出现在源文档中;缺失则直接标
fabricated。 幸存引用再用一道Choice判断小节与论断的关系,并用confidence ≥ 0.8决定是否转人工。
流水线
- 引文不在源文档 →
fabricated(不调模型); - 否则 Choice:
supports/contradicts/says_nothing→verified/contradicted/unsupported; - 置信度门控:
< 0.8→ human review,否则自动采纳。
官方原文与中文镜像: citation_check · 中文版。
📖 理论根基
- 确定性工作留给代码:是否逐字出现在源文档,是字符串问题,不该花模型钱。
- Choice 回答关系,不回答“真假”结算:结算标签写在
RELATION_TO_VERDICT里。 - 置信度是第二决策轴:低置信度表示分布平坦——即使有获胜选项,也不该自动放行。
- 引文存在 ≠ 支持论断:
unsupported常见于“引文真实,但小节压根没谈这件事”。
1.1 定义短中文源文档与 5 条引用
覆盖:verified、fabricated、contradicted、unsupported、低置信度需人工。
SOURCE = """产品账号安全说明(节选)
重置密码:用户可在登录页点击「忘记密码」,通过已绑定手机号收取短信验证码后设置新密码。
未绑定邮箱时,同样使用短信通道,不要求邮箱。
密码规则:新密码长度至少 8 位,需同时包含字母与数字。系统不会强制要求特殊符号。
会话时长:默认登录会话为 30 天;用户可在安全设置中提前退出所有设备。
两步验证:开启后,登录除密码外还需短信或认证器验证码。本节不描述密码重置步骤。
"""
CITATIONS = [
{
"id": "verified_ok",
"claim": "未绑定邮箱的用户可以通过手机短信重置密码。",
"quote": "未绑定邮箱时,同样使用短信通道,不要求邮箱。",
},
{
"id": "fabricated",
"claim": "官方建议把初始密码设为 admin123。",
"quote": "官方建议把初始密码设为 admin123。",
},
{
"id": "contradicted_ok",
"claim": "系统强制要求密码必须包含特殊符号。",
"quote": "系统不会强制要求特殊符号。",
},
{
"id": "unsupported_ok",
"claim": "会话时长默认是 7 天。",
"quote": "开启后,登录除密码外还需短信或认证器验证码。",
},
{
"id": "low_conf",
"claim": "两步验证与密码重置是同一套流程。",
"quote": "本节不描述密码重置步骤。",
},
]
print(f"源文档字符数: {len(SOURCE)}")
print(f"引用条数: {len(CITATIONS)}")
for c in CITATIONS:
print(f" {c['id']:<18} {c['claim']}")1.2 字符串匹配:先抓捏造引文
def normalize(text: str) -> str:
"""折叠空白,便于跨行匹配。"""
import re
return re.sub(r"\s+", " ", text).strip()
def locate(source: str, citation: dict) -> tuple[str, str | None]:
"""返回 (status, section_text)。status 为 found / missing。"""
needle = normalize(citation["quote"])
if needle and needle in normalize(source):
return "found", source
return "missing", None
for c in CITATIONS:
status, _ = locate(SOURCE, c)
print(f"{c['id']:<18} {status}")1.3 定义 Choice 问题与判定映射
AUTO_ACCEPT = 0.8
QUESTIONS = {
"relation": Choice(
instructions="该小节与论断的关系如何?",
criteria={
"supports": "小节陈述了该论断,或直接蕴含它为真",
"contradicts": "小节陈述了与论断相反的内容,或蕴含它为假",
"says_nothing": "小节无论从哪一方都未涉及论断所断言的内容",
},
),
}
RELATION_TO_VERDICT = {
"supports": "verified",
"contradicts": "contradicted",
"says_nothing": "unsupported",
}1.4 定义 check_citation()
把字符串匹配与 Choice、置信度门控收成一个函数。
def verdict_from(status: str, answer) -> dict:
if status == "missing":
return {"verdict": "fabricated", "confidence": None, "auto": True}
return {
"verdict": RELATION_TO_VERDICT[answer.choice],
"confidence": answer.confidence,
"auto": answer.confidence >= AUTO_ACCEPT,
}
def check_citation(source: str, citation: dict) -> dict:
status, section = locate(source, citation)
if section is None:
return {"id": citation["id"], "status": status, "relation": None, **verdict_from(status, None)}
off = CITATION_OFFLINE[citation["id"]]
resp = ts.call(
{"claim": citation["claim"], "section": section},
QUESTIONS,
offline_answers={"relation": off},
)
ans = resp.answers["relation"]
return {
"id": citation["id"],
"status": status,
"relation": ans.choice,
**verdict_from(status, ans),
}1.5 运行全部引用检查
print(f"{'id':<18}{'quote':<10}{'relation':<14}{'conf':>6} {'verdict':<13}{'action':>8}")
for c in CITATIONS:
r = check_citation(SOURCE, c)
rel = r["relation"] or "-"
conf = f"{r['confidence']:.2f}" if r["confidence"] is not None else "-"
action = "auto" if r["auto"] else "review"
print(f"{r['id']:<18}{r['status']:<10}{rel:<14}{conf:>6} {r['verdict']:<13}{action:>8}")观察要点
fabricated:引文不在源文档,字符串匹配即可结案,不调模型;contradicted_ok:引文真实,但小节内容否定论断;unsupported_ok:引文真实,但小节与论断无关;low_conf:即便有获胜选项,confidence < 0.8 →review。
小结
| 步骤 | 结果标签 |
|---|---|
| 引文缺失 | fabricated |
| supports / contradicts / says_nothing | verified / contradicted / unsupported |
| confidence < 0.8 | human review |
延伸阅读
⚠️ 若本笔记在离线示例模式下运行:输出中的数值是内置示例; 设置有效的
TYPESAFE_API_KEY后 Restart & Run All 即可得到真实结果。
知识补充
- 分层核查:先字符串匹配抓"捏造引文"(确定性、零成本),语义判定只留给"引文存在但意思被扭曲"的难例——代码能查的绝不问模型,这是贯穿官方文档的纪律。
- 结构化审计:每条引用输出 supports/partially/contradicts/fabricated + 概率,可直接进合规报表;"矛盾"类自动升级人工。
- 同类:
12_LLM防护栏.ipynb是同一思路用在输出合规上。