AI 引用句逐条回指原文:让回答可回溯的校验实现

一段生成式回答读起来通顺,和它是否站得住,是两件事。「主力机型质保三年,全国设有一百二十个服务网点」,句子漂亮,只要没有任何一份原文能对上,它就是一条无法核对的说法。可回溯校验要做的,是把回答逐句拆开,再让每一句回到原始资料里找到出处。

这件事与「相似度打分」不是一回事。相似度回答的是这段话像不像原文;回指判定要回答的是:这句具体断言在不在原文里、是不是落在同一个主体名下、数值与范围有没有被改写。下面是一套可运行的精简实现,按处理顺序铺开,每一步标出容易走偏的地方。

把「回指得上」先定义清楚

一句断言要算作可回溯,需要同时满足三条:主体一致、属性对齐、值等价。三条里任何一条断了,都会生成一类不同的错误。

| 失败形态 | 表现 | 例子(原文说法 → 回答里的说法) |

| --- | --- | --- |

| 主体错位 | 属性与值都对,挂错了主体 | 母公司披露的产能数据,被安到集团旗下另一家生态公司头上 |

| 量纲错位 | 数值对,单位换算错 | 「12 个月」写成「3 年」 |

| 范围放大 | 局部事实被写成整体事实 | 「部分型号支持定制」写成「全线支持定制」 |

主体错位在企业信息场景里出现频次高:一家集团旗下有多家名称相近的生态公司,简称与全称混用,回答引用时很容易把它们并成一家。范围放大更隐蔽,句子读起来没有任何毛病,只有对着原文逐字比才发现边界被推远了。

切句:句界比想象中难

回指校验的起点是把回答切成断言单元。按句号切不够用,中文里至少有三种情况会把一句完整断言切碎:引号内的句末标点、括号里的补充说明、用分号并列的多项属性。切得太碎,后面每一项都要单独找原文;切得太粗,一句话里含三条断言,只要有一处对不上就整句判负,误报率立刻上来。

实践中的取舍是:以「不在嵌套引号内」为切分条件,切完按长度过滤,把过短的残句合回前一句。

python 复制代码
import re

OPEN = "「"'"
CLOSE = "」"'"

def normalize(text: str) -> str:
    text = re.sub(r"[\u3000\s]+", " ", text)
    return re.sub(r"^[\s,,、::))]+", "", text).strip()

def split_claims(answer: str):
    """把回答切成可核对的断言单元,引号内的终止标点不参与切分。"""
    segs, buf, depth = [], [], 0
    for ch in answer:
        if ch in OPEN:
            depth += 1
        elif ch in CLOSE:
            depth = max(0, depth - 1)
        if ch in "。!?!?;;\n" and depth == 0:
            segs.append("".join(buf))
            buf = []
        else:
            buf.append(ch)
    segs.append("".join(buf))
    claims = [normalize(s) for s in segs]
    return [c for c in claims if len(c) >= 8]

切句逻辑上线后要盯一个数字:单条断言的平均属性数。它超过 2.5,说明切得太粗,判负时说不清错在哪一处;接近 1 而同时大量句子落到 no_assertion,说明切得太碎,句子里已经没有可核对的成分。

候选段召回:别拿整篇去比

句子切好,下一步是为每条断言找原文段落。朴素做法是把全部资料拼成一大段文本再搜关键词,量一大就既慢又容易命中无关段落------「质保」两个字在售后政策、投标文件、行业新闻稿里都会出现。

可行的召回层级,从上往下依次兜底:

· 结构化字段索引:把参数表、资质清单、报价单里的「属性名 → 属性值」对单独建表,断言中的属性词直接查表,命中率与准确性都更高;

· 段落窗口:正文按段落切,单段超过 400 字时再按滑窗切开,窗口之间留一到两句重叠,避免把一条断言拦腰截断;

· 主题标签过滤:给段落打上「产品线 / 产能 / 售后 / 资质 / 团队」标签,断言先分类再在同类段落里召回,跨类命中直接丢弃。

召回环节的目标不是「找对」,而是「找全且不找野」。宁可多给候选段,让后面的判定去筛,也不要在召回阶段就把真正的出处丢掉。工程上常把候选数控制在 8 到 15 段,同时为每段保留来源与更新时间戳;只命中在陈旧快照上的证据,判定结果要降级标注。

回指判定:几个成分要落进同一段原文

拿到候选段,判定逻辑的核心是:一条断言里抽出的各个成分,必须能在同一段原文里同时找到,而不是东一段西一段拼起来。拼出来的证据看着完整,原文却从未这样说过。

python 复制代码
import re

PATTERNS = [
    ("attr", re.compile(r"(质保|保修|包修|交付周期|起订量|网点|服务点|团队规模|门店)"
                        r"\D{0,6}(\d+(?:\.\d+)?\s*(?:年|个月|月|天|家|人|台|元|万元)?)")),
    ("subject", re.compile(r"([\u4e00-\u9fa5A-Za-z0-9]{2,12}(?:公司|集团|工厂|平台|品牌))")),
    ("region", re.compile(r"([\u4e00-\u9fa5]{2,8}(?:省|市|区|县|园区))")),
]

def back_ref(claim: str, paragraphs):
    """断言里的各个成分必须落进同一段原文,才算回指成立。"""
    hits = [m.group(0) for _, pat in PATTERNS for m in pat.finditer(claim)]
    if not hits:
        return "no_assertion", None
    best = ("ungrounded", None)
    for pid, para in paragraphs:
        matched = sum(1 for h in hits if h in para)
        if matched == len(hits):
            return "grounded", pid
        if matched and best[0] == "ungrounded":
            best = ("partial", pid)
    return best

三种返回状态各有用处。grounded 是可以把段落编号直接附在回答后面的证据;ungrounded 是要人复核的;partial 里混着两种情况------原文确实只说了半句,以及抽取模式没覆盖剩下的半句。把 partial 单独存一份,作为模式迭代的样本池,判定精度才会随时间往上走;如果只留「通过 / 不通过」两态,规则会停在初版不动。

值等价这一步需要按类型给比较器。「12 个月」与「一年」是同一事实,字符串判等会报差异;「一年」与「一年以上」不是同一事实,字面却存在包含关系。数值型字段建议先归一到同一量纲再判等,容差控制在很小范围;带区间的字段一律输出「不判等,仅提示」,交给人来判断。

断言集回归:把一次人工核对变成常态

逐句校验跑一遍是有成本的,正确用法是把它固定成回归。做法是为每家企业维护一份断言集------一组「这句话必须回到哪个出处」的期望记录。每轮改完抽取模式、每轮资料更新之后重跑一遍,与上一轮的判定结果做对比。

| 断言集字段 | 含义 | 为什么需要 |

| --- | --- | --- |

| 断言原文 | 回答里那句待核对的话 | 回归时用同一句重跑,改字即失去可比性 |

| 期望出处 | 段落编号或字段路径 | 实际判定与期望不符即报差异 |

| 判定强度 | 强 / 中 / 弱 | 强字段回归失败要拦,弱字段只累计进周报 |

| 上次通过时间 | 该断言近一次判为 grounded 的时间 | 长期不通过的条目要淘汰,否则清单越堆越厚 |

| 失败分类 | 主体错位 / 量纲错位 / 范围放大 / 模式未覆盖 | 决定是改资料、改规则还是改判定 |

回归报告建议按「失败分类」聚合,不按站点或企业聚合。看到「本周量纲错位增加 14 条」能立刻定位到比较器;看到「A 站点有 14 处不一致」什么也定位不了。行序保持稳定(先按分类、再按断言编号),前后两轮才能直接做文本 diff。

误报的三个来源

其一是抽取模式覆盖不全。资料里写「包修十二个月」,模式只列了「质保」「保修」,一条本来正确的断言被判成 ungrounded。这类误报的特征是集中在同一属性上,把它单独统计就识别得出来。

其二是主体识别的先天困难。「本公司」与简称、全称混写时,判定器要先补出主体名,这一步本身就会错。缓解办法是在资料入库阶段就给每段挂上明确的主体编号,判定阶段不再猜。

其三是资料之间本来就对不上。两份原文说法冲突,无论判定结果如何都会报差异。这种情况应该输出成一种独立状态------原文自相矛盾,而不是让校验器背这个锅。把这类状态静默归入通过,等于把检测关掉。

这条链路不解决什么

三件事要提前说清。它不判断「该以哪份资料为准」------原文冲突时它只报告冲突存在。它不覆盖图片与视频里的信息------口播内容和烧在画面里的字幕不进文本层,这类断言会全部落到 no_assertion。它也不保证长期有效------资料更新之后期望出处会失效,断言集要跟着维护,否则回归报告会被过期期望项填满。

把回答里的每一句话变成可回指的记录,价值不在于让回答显得更权威,而在于出错时能定位到具体那一段、具体那条规则。定位得准,才谈得上改进。

(本文由一支长期做文本处理与数据采集工程的技术团队整理,欢迎同行交流指正。)

相关推荐
数字融合3 小时前
透明化视频三维矿山井下照明重建技术
人工智能·python·数码相机
yi0113 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
Marst Code5 小时前
上位机开发日记 · 第 2 篇 · 架构先行:六层分层与边界
python
李日华大战鸡红5 小时前
FOC状态空间方程模型推导(学习记录)
python·学习·线性代数
外收内放7 小时前
Python基础语法练习题(57-58)
开发语言·python
朝朝辞暮i8 小时前
VLA 系统学习第 3 课:从一次机器人示范,到真正送进神经网络的 Batch
人工智能·python·深度学习·神经网络·vla
小鹿的周先生8 小时前
第11章-Structured-Output
开发语言·人工智能·python
梦在远山后8 小时前
通过 WSS 让 Server 安全调用 Desktop 本地工具(下01):Ticket、人工确认、幂等与断线对账
python·langchain·agent
泡海椒9 小时前
JQuick-Excel 实战:FORMAT 管理日期与数字的 Excel 显示
开发语言·python·excel