Skip to content
🎯 场景实战第 7 / 16 节

引用核查:拦住幻觉引文 ​

💡 一句话总结:用 Jev 建立 AI 引用核查线 —— 字符串匹配拦住编造的引文、一个 Choice 问题判断上下文支不支持论断、低置信判定自动转人工。

LLM 回答问题时爱附引文:每条论断配一段源文档,外加它依据的那句引文。可其中有些是错的,甚至是编的:引文可能压根不在文档里;也可能一字不差地躺在文档里,而它的上下文说的完全是另一回事。

人工核对一条引文有多磨人,官方 cookbook 列了三步:找到文档、在文档里找到引文、再读够上下文判断它撑不撑得住论断。一条尚且如此,8 条、80 条、800 条呢?

这本官方 cookbook(Double-checking citations)的自动化方案是两道闸:先用普通字符串匹配找「原文里没有」的引文——这种是编造(fabricated),根本轮不到模型出场;活下来的引文再用一道 Choice 问题读上下文,判断它和论断的关系。

上一课 LLM 护栏拦「不该说的话」,这一课拦「不该引的文」——同款「概率 + 阈值 + 代码分流」的思路,这次守的是「每句论断有没有出处」。本课约 12 分钟,带你用官方数据包完整跑一遍:源文档是 RFC 7519(JSON Web Token,2015 年 5 月),8 条引文是一个 LLM 对着它写的——4 条准确、4 条被官方动过手脚,全部对着官方发布的数字核对(jev-1.12,2026-08-16 采样)。

text
一条引文(claim + quote + section)
        │
        ▼
第 1 道闸:字符串匹配——引文在源文档里吗?
        │                    │
     找不到              找到(记下所在章节)/ 只指了章节
        │                    │
        ▼                    ▼
    fabricated        第 2 道闸:一道 Choice 问题
   (编造,零请求)      上下文和论断什么关系?
                         supports      → verified
                         contradicts   → contradicted
                         says_nothing  → unsupported
                              │
                              ▼
                    confidence ≥ 0.8 → 判定生效
                    confidence < 0.8 → 人工确认

学完你能做什么 ​

  • 把 AI 输出里的每条引文自动判成四种之一:verified 放行、fabricated / contradicted 打回、unsupported 转人工
  • 用零 API 成本的字符串匹配先拦住「原文里根本没有」的幻觉引文
  • 用一道 Choice 问题读引文上下文,判断它 supports / contradicts / says_nothing 论断
  • 按 confidence 阈值自动分流:高置信判定直接生效,低置信进人工队列(官方起点 0.8)

为什么用 Jev,而不是人工逐条核对? ​

能力人工逐条核对Jev 引用核查
一条引文耗时❌ 找文档、找引文、读够上下文,一条一条来✅ 官方缓存实测:单次判定 0.08–0.25 秒
判定口径❌ 因人因时而异,「原文没提」和「原文说反了」常混成一档✅ 三种关系定义在 criteria 里,折叠成四种固定判定
编造的引文❌ 得先翻到原文对应位置才能发现✅ 字符串匹配先拦,一次请求都不用发
把握怎么表达❌ 「看着没问题」——没法进流程✅ 校准概率 + confidence,0.8 以下自动转人工
批量与审计❌ 核查记录留在脑子里、聊天记录里✅ 每条判定带概率、置信度、token 数,可存档可回放

你现在的困境 ​

  • 「AI 写的报告引文一条比一条像真的,总不能每条都翻原文对吧——可上次就是漏了一条编的」
  • 「更阴的是那种引文:一字不差真在原文里,上下文说的却是反话,肉眼扫一遍根本看不出来」
  • 「想批量核查,又不敢再让一个 LLM 当裁判——它判错的样子,和引文幻觉的样子一模一样」

什么时候用这一招 ​

当你需要:

  • 给 RAG 问答、AI 写作助手、文献综述这类「带引文的 AI 输出」做批量质检
  • 把每条引文判成四种之一,各自走各自的处理:放行、打回、转人工
  • 编造的引文零成本拦下,低置信的判定有人兜底

而且不想:

  • 一条条人工翻原文找引文、读上下文
  • 核查结果又是一段没法进流程的自由文本
  • 引真文说假话的引文混过质检、进到正式稿里

🎒 开始前的准备 ​

  • 完成了 Choice 选择题:知道 criteria 是选项 map、答案带 probabilities 和 confidence——本页整条判定线就靠一道 Choice
  • (可选但推荐)读过 Confidence 置信度:三档用法里「低置信转人工」就是本页 AUTO_ACCEPT 阈值的思路
  • 跑通过安装与第一个请求:.venv 里有 typesafe-sdk,TYPESAFE_API_KEY 已在环境变量里
  • 知道官方数据包带结果缓存:重跑回放已发布数字、不发请求;删掉缓存才走真实调用(7 次——第 8 条引文在字符串匹配那步就出局了)。按输入 token 计费、输出免费,费率见模型与价格

跟我做 ​

第 1 步:拿到官方数据包(RFC 7519 + 8 条引文 + 结果缓存) ​

为什么:核查引文要有真文档、真引文。官方 cookbook 把三样东西都备好了:rfc7519.txt(RFC 7519 原文,从 rfc-editor.org 抓取)、citations.json(一个 LLM 对着 RFC 写的 8 条引文——4 条准确,4 条被官方动过手脚、注定不过关)、json_cache.json(官方跑出的结果缓存)。都放在 typesafe-public-examples 仓库的 cookbooks/citation_check 目录里。

在你跑通安装课的目录里(.venv 所在处)下载三个文件,再装官方 notebook 用的两个包:

bash
curl -sfLO https://raw.githubusercontent.com/typesafe-ai/typesafe-public-examples/main/cookbooks/citation_check/rfc7519.txt
curl -sfLO https://raw.githubusercontent.com/typesafe-ai/typesafe-public-examples/main/cookbooks/citation_check/citations.json
curl -sfLO https://raw.githubusercontent.com/typesafe-ai/typesafe-public-examples/main/cookbooks/citation_check/json_cache.json
.venv/bin/pip install ipython 'cooksafe>=0.2.0,<0.3.0'

然后新建本课主脚本 citation_check.py,先写骨架(客户端与缓存逐字来自官方 cookbook):

python
# citation_check.py —— 引用核查:拦住幻觉引文
# 依赖:.venv 里已有 typesafe-sdk(安装课装过),本课另装 ipython 和 cooksafe
import json
import os
import re
from pathlib import Path
from time import perf_counter

from cooksafe import JsonCache
from typesafe_sdk import Choice, TypeSafeClient

# 官方 cookbook 锁定的版本:本页核对的所有数字出自 jev-1.12(2026-08-16 采样)。
# 阈值是对着具体版本调的——jev-latest 别名会随新版本漂移,调过阈值的系统要锁版本 ID。
TYPESAFE_MODEL = "jev-1.12"
AUTO_ACCEPT = 0.8  # 官方注释:起步设高,多转人工;对模型的信任建立起来再往下调

client = TypeSafeClient(
    api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),
    base_url=os.environ.get("TYPESAFE_ENDPOINT"),
    timeout=120.0,
)
json_cache = JsonCache(Path("json_cache.json"))

检查文件都到位了:

bash
.venv/bin/python -c "from pathlib import Path; import json; print(Path('rfc7519.txt').stat().st_size, 'bytes'); print(len(json.loads(Path('citations.json').read_text())), 'citations')"

你应该看到:

text
63039 bytes
8 citations

一个贴心的细节:官方把 api_key 的缺省值写成 "cache-only"——只要 json_cache.json 在,重跑全程回放官方数字、一次请求都不发,所以暂时没有 API Key 也能完整跑完本课;删掉缓存文件跑真实调用时,才必须有 Key。

  • 三个文件下载成功,上面的命令打印出 63039 bytes 和 8 citations

第 2 步:加载源文档并切成编号章节 ​

为什么:模型不该读整篇 58k 字符的 RFC。官方做法:load_source() 先剥掉每页的页眉页脚(不然会把段落切碎),split_sections() 再按编号章节标题切成 45 节;后面每条引文只把它所在的那一节送进 state。「大 state 满是无关细节」是 Jev 的已知坑——这一步就是提前把无关细节挡在门外。

接着 citation_check.py 往下写(解析函数逐字来自官方 cookbook):

python
def load_source() -> str:
    """RFC 7519 verbatim, minus the page headers and footers that interrupt its paragraphs."""
    lines = []
    for line in Path("rfc7519.txt").read_text().splitlines():
        bare = line.lstrip("\f")
        if re.match(r"Jones, et al\.\s.*\[Page \d+\]$", bare):
            continue
        if re.match(r"RFC 7519\s+JSON Web Token \(JWT\)\s+May 2015$", bare):
            continue
        lines.append(bare)
    return re.sub(r"\n{3,}", "\n\n", "\n".join(lines))


def split_sections(source: str) -> dict[str, str]:
    """Map each numbered section ("4.1.3") to its text, split on the RFC's header lines."""
    boundary = re.compile(r"(?m)^(?:(\d+(?:\.\d+)*)\.  .+|Appendix [A-Z]\..*)$")
    marks = list(boundary.finditer(source))
    sections = {}
    for mark, nxt in zip(marks, marks[1:] + [None]):
        if mark.group(1) is None:  # an appendix header only terminates the section before it
            continue
        sections[mark.group(1)] = source[mark.start() : nxt.start() if nxt else len(source)].strip()
    return sections


SOURCE = load_source()
SECTIONS = split_sections(SOURCE)
CITATIONS = json.loads(Path("citations.json").read_text())

看看加载结果和引文长什么样:

bash
.venv/bin/python -c "from citation_check import SOURCE, SECTIONS, CITATIONS; print(f'{len(SOURCE):,} characters, {len(SECTIONS)} numbered sections, {len(CITATIONS)} citations')"

你应该看到:

text
58,365 characters, 45 numbered sections, 8 citations

再看两条引文的样子(一条带引文、一条只指了章节):

bash
.venv/bin/python -c "import json; from citation_check import CITATIONS; print(json.dumps(CITATIONS[1], indent=2)); print(json.dumps(next(c for c in CITATIONS if c['quote'] is None), indent=2))"

你应该看到:

text
{
  "id": "aud_reject",
  "claim": "If a validator does not find itself in a token's audience list, it has to reject the token.",
  "quote": "If the principal processing the claim does not identify itself with a value in the \"aud\" claim when this claim is present, then the JWT MUST be rejected.",
  "section": "4.1.3"
}
{
  "id": "iat_future",
  "claim": "The \"iat\" claim requires validators to reject tokens whose issue time is in the future.",
  "quote": null,
  "section": "4.1.6"
}

每条引文四个字段:id(你起的标识)、claim(AI 输出里的那句论断)、quote(它引用的原文片段)、section(章节号)。quote 为 null 表示「只指了章节、没给引文」——没有可匹配的字符串,直接把指名的章节送模型。

  • 两条打印都对上了:58,365 characters, 45 numbered sections, 8 citations,以及 aud_reject(带 quote)和 iat_future(quote 为 null)

第 3 步:字符串匹配先拦「原文里没有」的引文 ​

为什么:引文要是不在文档里,那就是编的——这种不需要模型,普通字符串匹配就够,一次 API 都不发。匹配前先归一化:RFC 文本每行约 72 字符折行,引文原样带换行就匹配不上;把空白折叠成单个空格、弯引号折成直引号,跨行的引文也能对上。

接着往下写(逐字来自官方 cookbook):

python
def normalize(text: str) -> str:
    """Collapse whitespace and fold curly quotes, so a quote matches across line wraps."""
    table = str.maketrans({"“": '"', "”": '"', "‘": "'", "’": "'"})
    return re.sub(r"\s+", " ", text.translate(table)).strip()


def find_quote(sections: dict[str, str], quote: str) -> str | None:
    """The number of the section that contains the quote verbatim, or None."""
    needle = normalize(quote)
    for number in sorted(sections, key=lambda n: [int(p) for p in n.split(".")]):
        if needle in normalize(sections[number]):
            return number
    return None


def locate(sections: dict[str, str], citation: dict) -> tuple[str, str | None]:
    """Step 1 for one citation: a status, plus the section step 2 will read."""
    if citation["quote"] is None:
        return "section-only", sections[citation["section"]]
    number = find_quote(sections, citation["quote"])
    if number is None:
        return "missing", None
    return "found", sections[number]

locate() 返回两样东西:一个状态(found 找到 / missing 没找到 / section-only 只指了章节),以及下一步要送给模型读的那一节文本。新建 locate_report.py 看看 8 条引文的匹配结果:

python
# locate_report.py —— 第 3 步检查:8 条引文的字符串匹配结果
from citation_check import CITATIONS, SECTIONS, locate

for citation in CITATIONS:
    status, section = locate(SECTIONS, citation)
    where = f"section of {len(section):,} chars" if section else "not in the source"
    print(f"{citation['id']:<18}{status:<14}{where}")
bash
.venv/bin/python locate_report.py

你应该看到(官方发布的数字):

text
epoch_seconds     found         section of 3,122 chars
aud_reject        found         section of 761 chars
sig_reporting     missing       not in the source
clock_skew        found         section of 529 chars
exp_required      found         section of 529 chars
pii_encryption    found         section of 1,653 chars
iat_future        section-only  section of 270 chars
duplicate_names   found         section of 918 chars

sig_reporting 在这里就出局了:它的「引文」在 RFC 里根本不存在——官方的数据执照(DATA_LICENSE.md)也写明,这条 quote 是他们自己写的,不是 RFC 里的原文。后面不会再为它花任何请求。iat_future 是另一类:只指了 4.1.6 节没给引文,没有可匹配的字符串,直接取那一节往下走。

  • 8 行状态里有一行 missing(sig_reporting)、一行 section-only(iat_future),其余 6 行都是 found

第 4 步:用一个 Choice 问题判断上下文支不支持论断 ​

为什么:走到这一步的引文都逐字在原文里——但这不够:引文准确,建在它上面的论断照样可能错。判断这件事要读引文的上下文(第 3 步找到的那一节)。官方用一道 Choice 问题覆盖章节和论断的三种关系;最高概率的选项就是判定,confidence ≥ 0.8 判定直接生效,低于 0.8 转人工确认。

接着往下写(问题定义与判定折叠逐字来自官方 cookbook):

python
QUESTIONS = {
    "relation": Choice(
        instructions="How does the section relate to the claim?",
        criteria={
            "supports": "The section states the claim or directly implies that it is true",
            "contradicts": "The section states the opposite of the claim or implies it is false",
            "says_nothing": "The section does not address what the claim asserts, either way",
        },
    ),
}

RELATION_TO_VERDICT = {
    "supports": "verified",
    "contradicts": "contradicted",
    "says_nothing": "unsupported",
}


@json_cache
def ask(claim: str, section: str) -> dict:
    started = perf_counter()
    response = client.system_one(
        state={"claim": claim, "section": section},
        questions=QUESTIONS,
        model=TYPESAFE_MODEL,
    )
    answer = response.answers["relation"]
    return {
        "choice": answer.choice,
        "probabilities": answer.probabilities,
        "confidence": answer.confidence,
        "seconds": round(perf_counter() - started, 2),
        "input_tokens": response.usage.input_tokens or 0,
        "output_tokens": response.usage.output_tokens or 0,
    }


def verdict(status: str, answer: dict | None) -> dict:
    """Fold step 1 and step 2 into one of the four labels, plus an auto-or-review flag."""
    if status == "missing":
        # confidence None: no model was called, so there is no model confidence to report
        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(sections: dict[str, str], citation: dict) -> dict:
    status, section = locate(sections, citation)
    answer = ask(citation["claim"], section) if section is not None else None
    return {"id": citation["id"], "status": status, "answer": answer, **verdict(status, answer)}

三处设计值得盯住:

  • state 只有两样东西:claim 和 section。「读哪一段」这个活第 3 步已经干完了,模型拿到的是干净的判断材料。
  • @json_cache 装饰器:缓存命中直接回放官方数字、不调 API;缓存里没有的才真发请求。
  • 四种判定怎么折叠:missing → fabricated(confidence 记 None——没调模型,没有模型置信度可报);其余按 RELATION_TO_VERDICT 映射成 verified / contradicted / unsupported,auto 标记由 AUTO_ACCEPT 决定。

💡 这就是 Jev 的独有价值

「引文逐字在原文里」不等于「论断成立」。exp_required 的引文一字不差摘自 4.1.4 节,论断说「每个 JWT 必须带 exp,没有 exp 的 token 无效」——而同一节写着 "Use of this claim is OPTIONAL"。概率分布里 contradicts=1.0、confidence 0.99,一次拦下这种「引真文说反话」。判定是结构化字段 + 校准概率,代码直接分流;换成让生成模型写一段「核查意见」,你还得解析文本,更别说它自己也会幻觉。

💡 中文内容注意

官方说明:英语是 Jev 的主要训练语言,包括中文在内的其他语言能用但准确度较低。拿中文文档、中文论断做引用核查,或把问题翻译成中文之前,先用自己的真实数据跑一批,盯住概率分布和 confidence 再定阈值。

你应该看到:这一步没有输出,代码写完先放着——下一步全量跑。

第 5 步:全量跑 AI 引用核查,看四种判定各拦到谁 ​

为什么:单条看逻辑,全量看效果。官方把 8 条引文全过了一遍:4 条准确的全部 verified(confidence 0.93 以上),4 条动过手脚的全部被拦——1 条 fabricated、1 条 contradicted、2 条 unsupported 转人工。

给 citation_check.py 收尾,加上主程序:

python
if __name__ == "__main__":
    print(f"{'citation':<18}{'quote':<14}{'relation':<14}{'conf':>6}  {'verdict':<13}{'action':>7}")
    for citation in CITATIONS:
        result = check_citation(SECTIONS, citation)
        answer = result["answer"]
        relation = answer["choice"] if answer else "-"
        conf = f"{answer['confidence']:.2f}" if answer else "-"
        action = "auto" if result["auto"] else "review"
        print(
            f"{result['id']:<18}{result['status']:<14}{relation:<14}{conf:>6}"
            f"  {result['verdict']:<13}{action:>7}"
        )

运行(缓存文件在,回放官方数字、不发请求;删掉缓存才是 7 次真实调用):

bash
.venv/bin/python citation_check.py

你应该看到(官方发布的数字,jev-1.12、2026-08-16 采样;缓存文件删除后为真实调用):

text
citation          quote         relation        conf  verdict       action
epoch_seconds     found         supports        0.93  verified        auto
aud_reject        found         supports        0.95  verified        auto
sig_reporting     missing       -                  -  fabricated      auto
clock_skew        found         supports        0.99  verified        auto
exp_required      found         contradicts     0.99  contradicted    auto
pii_encryption    found         says_nothing    0.27  unsupported   review
iat_future        section-only  says_nothing    0.56  unsupported   review
duplicate_names   found         supports        0.99  verified        auto

逐行读这张表,四个细节是官方特意安排的:

  • 4 条准确引文全部 verified:epoch_seconds、aud_reject、clock_skew、duplicate_names,confidence 0.93 以上,远高于 0.8 的生效线。
  • sig_reporting 一次请求都没花:quote 不在 RFC 里,字符串匹配那步就判了 fabricated,连模型都没到。
  • exp_required 是「引真文说反话」:引文逐字摘自 4.1.4,同节却说 "Use of this claim is OPTIONAL"——contradicts 1.0,confidence 0.99。
  • pii_encryption 和 iat_future 进了人工队列:says_nothing 且 confidence 只有 0.27 和 0.56,都低于 0.8。pii_encryption 正是「光有字符串匹配不够」的官方例证——引文逐字在原文里,它所在的第 3 节却对「必须加密」这个论断不置可否。

再看一眼成本:缓存里记录的 7 次模型调用,单次 0.08–0.25 秒,合计 4,420 输入 token、281 输出 token——输出免费;输入按当前价目 $0.042/Mtok(见模型与价格)折算,这一轮全量核查约 $0.0002。

  • 我跑出的表里 verified / fabricated / contradicted / unsupported 四种判定都出现了
  • 两条 unsupported 都带着 review 标记,其余都是 auto

📋 魔法 Prompt ​

本页的「Prompt」是给 API 的问题与判定模板——抄走、替换【】占位符(带 ? 的是选填)就能用。

📝 引文关系 Choice 问题(三选项判定) ​

预期效果:把「这段上下文和论断什么关系」变成一道固定三选项的 Choice 题,判定口径写死在 criteria 里。

python
from typesafe_sdk import Choice

QUESTIONS = {
    "relation": Choice(
        instructions="【一句话问上下文与论断的关系,官方原文:How does the section relate to the claim?】",
        criteria={
            "supports": "【什么算支持:章节陈述了论断,或直接暗示它为真】",
            "contradicts": "【什么算相抵触:章节说的是论断的反面,或暗示它为假】",
            "says_nothing": "【什么算没提到:章节对论断的断言不置可否】",
        },
    ),
}
# 必填项:三个选项的方向要正——supports = 引文真的撑得住论断
# 选填项:需要更多判定档位(如「部分支持」)就加选项;选项数变了 confidence 的读法也跟着变?

# 输出规范:answer.choice → 最高概率选项;answer.probabilities → 每个选项的概率;answer.confidence → 0–1
# 约束条件:
# ✅ 选项定义写成可对照的情境,边界情形放进 criteria
# ❌ 不要只给 supports / not_supports 两档——「原文说了相反的话」和「原文没提」必须分开处理

✅ 四种判定折叠 + 转人工阈值 ​

预期效果:把字符串匹配结果和模型判定折叠成 verified / unsupported / contradicted / fabricated,低置信自动转人工。

python
AUTO_ACCEPT = 【0.8】  # 官方起点:先高后低,低于它就转人工

RELATION_TO_VERDICT = {
    "supports": "verified",
    "contradicts": "contradicted",
    "says_nothing": "unsupported",
}


def verdict(status: str, answer: dict | None) -> dict:
    """status: missing / found / section-only(字符串匹配那步的输出)"""
    if status == "missing":
        return {"verdict": "fabricated", "confidence": None, "auto": True}  # 没调模型,confidence 记 None
    return {
        "verdict": RELATION_TO_VERDICT[answer["choice"]],
        "confidence": answer["confidence"],
        "auto": answer["confidence"] >= AUTO_ACCEPT,
    }
# 必填项:AUTO_ACCEPT 用你自己文档上跑出的分布定,0.8 只是官方起点
# 选填项:想更严可以给 contradicted 单独设一条更低的转人工线?

# 输出规范:{"verdict": "verified|unsupported|contradicted|fabricated", "confidence": float|None, "auto": bool}
# 约束条件:
# ✅ auto=False 的一律进人工队列——官方就是这么处理 pii_encryption(0.27)和 iat_future(0.56)的
# ❌ 不要拿 confidence 改判定本身——它只决定「谁来看」,不决定「是什么」

🔧 citations.json:换成你自己的文档和引文 ​

预期效果:把核查指向你自己的 RAG / 写作产品:换源文档、换引文清单,判定管线一行不用改。

json
[
  {
    "id": "【引文 id,用你产品里的引文主键,判定结果好回写】",
    "claim": "【AI 输出里的那句论断,原文照抄】",
    "quote": "【它引用的原文片段;只指章节不给引文时填 null】",
    "section": "【章节号,如 4.1.4;quote 为 null 时必填,模型读这一节】"
  }
]
  • 必填项:claim + quote(或 quote: null 配上 section)
  • 选填项:id 用产品主键,方便把判定回写到原文档?
  • 输出规范:每条引文 → 四种判定之一 + confidence + auto 标记
  • 约束条件:
    • ✅ quote 逐字抄源文档——官方的匹配是归一化后的精确匹配
    • ❌ 别指望它容忍「差不多」的引文——截断或改写过的 quote 不算 found,会被判 fabricated;生产要容忍马虎引用得换模糊匹配(官方原话)

检查点 ✅ ​

  • citation_check.py 跑出 8 行判定表,verified / fabricated / contradicted / unsupported 四种判定都出现过
  • 我能说出 sig_reporting 为什么一次请求都没花就被判 fabricated——字符串匹配在 RFC 里找不到那条「引文」
  • 我知道 exp_required 的引文逐字在 4.1.4 里,判定为什么是 contradicted——同一节写着 "Use of this claim is OPTIONAL"
  • 我知道 pii_encryption(0.27)和 iat_future(0.56)为什么进人工队列——都低于 0.8 的 AUTO_ACCEPT
  • 我知道官方数字出自 jev-1.12(2026-08-16 采样):缓存文件在就回放这份,删掉才发真实请求

踩坑提醒 ​

❌ 以为「引文在原文里」就等于「论断成立」 ​

pii_encryption 是官方安排的反例:它的引文逐字在 RFC 第 3 节里,字符串匹配妥妥 found——但那一节讲的是 JWT 的结构(claims 编码进 JWS/JWE),对「带个人数据必须加密」这个论断不置可否,模型判 says_nothing、confidence 只有 0.27。字符串匹配只能拦「编造」,拦不住「引真文说假话」——所以才有第 4 步那道 Choice。

❌ 拿精确匹配当模糊匹配用 ​

官方原话:归一化之后的匹配是精确的——引文被截断、或轻轻改写过一个词,结果就是 missing → fabricated。你的系统若要容忍马虎引用,需要换模糊匹配。上线前先想清楚:你的产品里「引文」是怎么生成的,截断和改写有多常见。

❌ 只分「过 / 不过」两档 ​

四种判定各有各的去处:verified 直接放行;fabricated 和 contradicted 要打回重写——一个是编的,一个是原文说了反话;unsupported 看情况——可能要补出处,也可能论断本身要改。压成两档,下游就只能一刀切,exp_required(原文说反话)和 pii_encryption(原文没说)会被同样对待。

❌ 阈值设了就不回头看 ​

官方注释写得很清楚:AUTO_ACCEPT = 0.8 是「起步设高、多转人工」的起点,跑一阵、看看模型在你自己文档上的表现再往下调。另外官方把 TYPESAFE_MODEL 锁在 jev-1.12 不是随手写的——jev-latest 别名会随新版本漂移,对着旧版本调好的阈值,换了版本可能就是另一回事(见模型与价格)。

进阶技巧(可选) ​

浏览器里先试一把(免代码)。 官方 cookbook 用 cooksafe 的 make_playground_link() 把 exp_required 的 claim + section 和问题打成分享链接,点开就是在 Playground 里跑同一个请求:

打开 exp_required 的 playground 示例 →

生成链接的代码很短(官方 notebook 里跑,example 和 example_section 取自 exp_required 那条引文):

python
from cooksafe import make_playground_link
from IPython.display import Markdown, display

playground_link = make_playground_link(
    {"claim": example["claim"], "section": example_section}, QUESTIONS
)

指向你自己的数据。 官方收尾说得直白:换掉 rfc7519.txt 和 citations.json 就行——但 load_source() 和 split_sections() 是照 RFC 的版式写的(剥页眉页脚、按编号章节标题切),别的文档形状要写自己的解析。网页、PDF 转文本、Markdown 手册,切「节」的逻辑各不相同,但「每条引文只送它所在的那一段」这个原则不变。

缓存回放 vs 真实调用。 json_cache.json 在,重跑就回放官方数字(api_key 缺省 "cache-only",没有 Key 也能复现);删掉它,7 次真实调用按输入 token 计费、输出免费。想在你的项目里也这样「先回放再实跑」,cooksafe 的 JsonCache 装饰器就是干这个的。

本课小结 ​

✅ AI 引用核查两道闸:字符串匹配拦「原文里没有」的引文(fabricated,零 API 成本);一道 Choice 问题判断上下文支不支持论断。

✅ 四种判定各就各位:verified 放行、contradicted 打回(原文说了反话)、unsupported 转人工(原文没提)、fabricated 打回(引文是编的)——「引真文说假话」和「原文没说」分开处理。

✅ confidence 门控:≥ 0.8 判定生效,低于转人工——官方 8 条里正好 2 条(0.27、0.56)进了人工队列。

✅ 官方实测:4 条准确引文全部 verified(0.93 以上),4 条问题引文全部被拦;7 次模型调用合计约 4.4k 输入 token,按当前价目折算约 $0.0002。

关键配置字段回顾:

字段本页的值说明
TYPESAFE_MODELjev-1.12官方 cookbook 锁定版本;别名会漂移,调阈值要锁 ID
AUTO_ACCEPT0.8判定生效线,低于转人工;起步高、往下调
QUESTIONS["relation"]Choice 三选项supports / contradicts / says_nothing
判定映射supports→verified 等missing→fabricated 不经模型,confidence 记 None
state{"claim": …, "section": …}模型只读论断 + 所在章节,无关细节不进上下文
缓存json_cache.json回放官方数字;删除则真实调用

相关教程:

下一课预告 ​

引文核查守的是「引出来的话」,下一课守「塞进上下文的段落」:RAG 段落把关:只放相关内容进上下文。

你会学到:

  • 每个检索段落一次请求打相关分,代码决定哪些段落能进 answering model
  • 用 Noul 是非题 给每个段落打 0–1 概率分——和本页的 Choice 判定正好互补
  • 把不相关段落挡在上下文外,回答质量不再跟着检索质量漂
搞懂AI关于我们怎么写隐私说明

独立的第三方教程,与文中提到的任何产品及其公司没有隶属或合作关系;产品名称和商标归各自所有者。价格和政策以官方页面为准。