第 20 章

第 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 None

0.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 / 原语 / 置信度 (中文镜像站有对应页)。此处只提醒实验会反复用到的三点:

  1. 请求模型:同一个 state + 一组问题 → 类型化答案;问题彼此独立、按 ID 返回;
  2. 三种原语:Choice 选命名选项;Score 返回概率加权期望分(可为小数);Noul 返回“是”的概率(无 confidence);
  3. 代码掌控制权:阈值、路由、合并答案都在你的代码里;模型只回答狭窄、原子的判断。

💡 问题 ID 不会发给模型——完整语义写在 instructions / criteria 里。


1. 核对引用(Double-checking citations)

先用字符串匹配确认引文是否出现在源文档中;缺失则直接标 fabricated。 幸存引用再用一道 Choice 判断小节与论断的关系,并用 confidence ≥ 0.8 决定是否转人工。

流水线

  1. 引文不在源文档 → fabricated(不调模型);
  2. 否则 Choice:supports / contradicts / says_nothing → verified / contradicted / unsupported;
  3. 置信度门控:< 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 是同一思路用在输出合规上。

📑 Jev Cookbook:System One 判断模型实战教程

1 第 1 章:认识 Jev:模型、上手与场景 2 第 2 章:核心概念总览 3 第 3 章:System One:判断的核心心智模型 4 第 4 章:状态:让判断连续可追溯 5 第 5 章:原语:Choice、Score 与 Noul 6 第 6 章:置信度:让概率可信 7 第 7 章:应用构建:从原语到完整系统 8 第 8 章:架构模式 9 第 9 章:实战指南总览 10 第 10 章:自一致性 · Noul 11 第 11 章:自一致性 · Choice 12 第 12 章:并行提问 13 第 13 章:重排序 14 第 14 章:逐行语义搜索 15 第 15 章:结构恢复 16 第 16 章:函数调用 17 第 17 章:技能推荐 18 第 18 章:实体对齐 19 第 19 章:RAG 段落分类 20 第 20 章:引用核查 21 第 21 章:LLM 防护栏 22 第 22 章:SDE 级联 23 第 23 章:日期抽取 24 第 24 章:预解析值抽取 25 第 25 章:层级分类 26 第 26 章:自动研究特征发现 27 第 27 章:基于置信度的分类 28 第 28 章:智能家居实验 29 第 29 章:模型评测总览 30 第 30 章:模型评测实验 31 第 31 章:Laya vs Jev 对比基准 32 第 32 章:JevBench:LLM 评测体系 33 第 33 章:智能家居应用实战 34 第 34 章:Jev-Mem 研究总览 35 第 35 章:Jev-Mem 缩放实验 36 第 36 章:Jev-Mem 完整走查 37 第 37 章:Agent 集成总览 38 第 38 章:Pi 集成实验 39 第 39 章:DSH 决策协作 40 第 40 章:本地模型总览 41 第 41 章:本地模型介绍与对比 42 第 42 章:中文数据集构建方案 43 第 43 章:微调指南 44 第 44 章:RLCD 原理与实验优化 45 第 45 章:中文数据集构建实验 46 第 46 章:中文 Head 微调实验 47 第 47 章:全量 v2 微调实验 48 第 48 章:知识库
← 返回本书大纲