Citation-Packing-RAG

模型能说出数字,但无法指出原文最小片段,审计和纠错都做不了。根因是论断(claim)与原文片段(span)未对齐。本方法按 claim 装箱最小证据。它不提高召回,没有证据就应拒答而不是造引用。

本文属于 RAG 工程框架中的「3 重排与证据组装」环节,聚焦「Citation Packing(引用组装)」方法。

定位

维度 内容
角色 claim 与 span 的对齐装箱
输入 → 输出 草稿答案 + 证据 → 带 citation 的 claim 列表
默认组合 LlamaIndex CitationQueryEngine
何时不用 不对用户展示引用的内部助手,可只做 Faithfulness

核心机制

$$
\mathrm{CiteP}=\frac{|{c:\mathrm{supported}(c,\mathrm{span}(c))}|}{|C|}
$$

段末注释:引用精确率(Citation Precision,CiteP) = 已挂引用的 claim 中,能被对应 span 支持的比例。$C$ 为拆出的 claim 集合。

定义

这对词是引用装箱的两个对齐对象,不是检索阶段的 chunk。

claim span
中文 论断 原文片段
来自哪侧 生成侧:答案草稿 原文侧:证据句
粒度 一条只说一件事,真假可判定 最短、连续子串,能指回 doc_id + 起止位置
非法形态 整句里塞多个数字当一条 整段当 citation

草稿 GAPDH siRNA is 20 nM for 48 h. 拆成两条 claim:20 nM siRNA(剂量)、48 h(时程)。同一证据 Treat HEK293T with 20 nM siRNA targeting GAPDH for 48 h. 对齐为两个最小 span:20 nM siRNA targeting GAPDHfor 48 h

关系:supported(c,\mathrm{span}(c)) 成立才算引用有效。草稿写成 24 h 时该 claim 对不上任何 span,应删或拒答,禁止造 doc_id

图 1 claim 从草稿拆出,span 从原文圈出;对不上的 claim 丢弃,禁止造引用

实现路径与心智:先得到答案草稿,拆成 claim,每条对齐到原文最小 span,对不上的删或改写。底层心智:答案是一组可核验命题,引用不是装饰。本步不提高召回。

优缺点

  • 优点:可审计。
  • 缺点:对齐模型有额外时延。

契约与走通样例

输入

1
{"draft": "GAPDH siRNA is 20 nM for 48 h.", "evidence": ["Treat HEK293T with 20 nM siRNA targeting GAPDH for 48 h."]}

中间量

两条 claim 分别被同一证据句中的两个最小 span 支持,CiteP=$1$。若草稿写成 24 h,该 claim 无 span,应删或拒答。

输出

1
2
3
4
5
6
{
"claims": [
{"text": "20 nM siRNA", "span": "20 nM siRNA targeting GAPDH", "doc_id": "P-GAPDH-01-C1"},
{"text": "48 h", "span": "for 48 h", "doc_id": "P-GAPDH-01-C1"}
]
}

社区实现

LlamaIndex CitationQueryEngine。风险:整段当 citation,CiteP 虚高但审计无用。

工程落地

最小可运行示例

复制为 .py 后直接运行(仅标准库)。生产把字面对齐换成 NLI / LlamaIndex CitationQueryEngine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"""按 claim 装箱最小原文 span;对不上则丢弃,禁止造 doc_id。"""
from __future__ import annotations

import re
from dataclasses import dataclass


@dataclass
class PackedClaim:
"""已对齐的论断。span 必须是 evidence 的子串。"""

text: str
span: str
doc_id: str


def split_claims(draft: str) -> list[str]:
"""从草稿抽出带数字的原子命题。生产可用 LLM claim splitter。"""
return re.findall(r"\d+\s*(?:nM|h)", draft)


def min_span(claim: str, evidence: str) -> str | None:
"""在 evidence 中取含该数字的最短连续片段。"""
token = re.search(r"\d+\s*(?:nM|h)", claim)
if not token:
return None
m = re.search(rf".{{0,28}}{re.escape(token.group())}.{{0,28}}", evidence)
return m.group().strip() if m else None


def citation_pack(draft: str, evidence: str, doc_id: str) -> list[PackedClaim]:
"""输入草稿与一条证据;输出能对齐的 claim 列表。"""
packed = []
for claim in split_claims(draft):
span = min_span(claim, evidence)
if span:
packed.append(PackedClaim(claim, span, doc_id))
return packed


if __name__ == "__main__":
evidence = "Treat HEK293T with 20 nM siRNA targeting GAPDH for 48 h."
ok = citation_pack("GAPDH siRNA is 20 nM for 48 h.", evidence, "P-GAPDH-01-C1")
bad = citation_pack("GAPDH siRNA is 20 nM for 24 h.", evidence, "P-GAPDH-01-C1")
print([(c.text, c.span) for c in ok])
print([c.text for c in bad]) # 24 h 无 span,被丢弃

参数

参数 起点 影响
最小 span 含数字的最短子串 过长失去定位
无对齐策略 丢弃该 claim 禁止伪造 doc_id

失效—信号—螺丝

  • 乱挂引用:CiteP 低。螺丝:强制 NLI 通过才挂。
  • 覆盖高精确低:螺丝:拆细 claim。
  • 召回为空仍输出数字:螺丝:拒答。

规模(100 篇生物学 PDF)

可选 NLI GPU 4–8 GB。在线每问 +100–600 ms(随 claim 数)。

-------------本文结束感谢您的阅读-------------