主题
FDS-15 · B6 出处校验
抽取(B5)给每条结论都附了"这句话出自原文哪里"和一个位置。但模型自报的引文和位置不可信——它会编出一句看起来很像、其实原文没有的话。B6 这一关用纯程序逐条核对:引文是否真能在精修文本里对上;对得上才放行进事实库,对不上就丢弃或转人工。这一步绝不让模型自己判断,确定性、可复算。
1. 业务说明
- 是什么:夹在"抽取(B5)产出之后、写入事实库(B8)之前"的一道程序闸门。它把 B5 每条结论自带的"出处引文 + 位置"拿来,和上一步精修(B3)产出的文本逐字核对——引文要么在原文里真的能找到,要么找不到;能找到就钉死它在原文里的精确字符区间,找不到就拦下。
- 解决什么:模型抽取时很会"脑补"——它给你一条决策"上线时间定在 6 月底",还煞有介事地附一句引文"我们决定 6 月底上线",但精修文本里压根没人说过这句,或者说的是"6 月底争取,但要看测试"。这种看似合理实则编造的引文,人眼一条条核对核不过来,下游(待办、看板、复盘)又全靠事实库,一条假出处会污染整条链。B6 就是把这件"人核不动、又绝不能错"的事交给程序。
- 为什么必须是程序、不能交给模型:让模型自己核对自己的引文,等于让它给自己的作业打分——它判断"这句和原文意思差不多"时,正是它最会自圆其说的地方。程序只认一件事:这段字符到底在不在那段文本里,它不懂"意思差不多",也正因为不懂,它不会被"听起来很对"骗。这是确定性(同样输入永远同样结果、可重算)与"看起来对"的根本区别。
- 不做会怎样:就是现状——出处全靠模型一句话自报,没人验、不可追溯。点开一条结论想看出处,跳过去发现原文根本没这句;或者三个会的结论互相打架,谁也说不清哪条是真的。事实库"唯一真相"的地基就塌了。
2. 用户操作与界面(交互)
用户不直接操作 B6——它在流水线里自动跑、对用户透明。用户感知到的是结果:
- 会议结果页每条结论旁有一个"看出处"的链接,点开能跳到精修文本里高亮的那一段(就是 B6 钉死的精确区间)。能跳得准、且高亮的就是结论说的那句——这是 B6 干对了。
- 某条结论的引文没对上、被 B6 拦下:它不会出现在结果页的"已确认"里,而是落到 B7 的站内确认任务(见 FDS-16),由会议负责人在工作台里看到"这条 AI 抽的没找到原文出处,请确认"。
- 整场会跑完,结果页顶部一行小结:"本场抽取 N 条,出处核实 M 条,转人工确认 K 条"——让人对这场的可信度一眼有数。
3. 数据与接口(字段级)
B6 是代码模块,不是技能,没有 SKILL.md。它在 acdm-backend 流水线里作为一个节点函数存在,输入输出都是结构化数据。
输入(来自 B5 抽取,每条一个对象):
| 字段 | 类型 | 说明 |
|---|---|---|
| extract_id | str | 这条抽取项的临时 id(本场内唯一,贯穿到 B7/B8) |
| kind | enum | decision/risk/action/milestone/fact(类型决定"未命中"是丢弃还是转人工) |
| text | text | 结论的标准化内容(B5 措辞后的,不参与比对,只随条目走) |
| claimed_quote | text | 模型自报的"原文引文"——B6 要核的就是它 |
| claimed_span | obj null | 模型自报的位置 {char_start, char_end}(可能为空、可能不准,仅作提示加速,不作依据) |
| refined_file_id | uuid | 指向 B3 产出的精修文本(meeting_files.kind=refined),比对的基准 |
| content_hash | text | B5 抽取时所依据的精修文本指纹(防止比对时文本已被改过,见第 4 节第 0 步) |
输出(每条一个判定结果):
| 字段 | 类型 | 说明 |
|---|---|---|
| extract_id | str | 原样带回 |
| verdict | enum | hit_exact / hit_substr / hit_fuzzy / to_human / dropped |
| source_ref | jsonb null | 命中时钉死的精确出处:{file_id, char_start, char_end, speaker, ts, match_type, similarity};未命中为 null |
| similarity | float | 模糊对齐时的相似度分(0~1);精确/子串命中记 1.0 |
| reason | str | 判定说明(人能读:"全文未找到该引文,最高相似 0.41,判定编造") |
source_ref的字段形状与 FDS-17project_facts.source_ref完全一致——B6 放行的条目,其source_ref就是 B8 入库时直接落进事实库的那个出处,字符区间由 B6 钉死,不再由模型说了算。speaker/ts从精修文本对应区间所属的那一段(B3 的segments,见 FDS-12)反查带出,不取模型自报。
接口(acdm-backend 内部,流水线编排调用,不暴露给技能):
verify_citations(refined_file_id, content_hash, items[]) -> results[]:批量核验一场会的全部抽取项,返回每条判定。同步、确定性、无副作用(不写库;写库是 B8 的事)。- 读精修文本经事实库侧的只读取文接口(按
refined_file_id取文本与segments),B6 不直连数据库表(与 FDS-04 §5 一致:正确性模块也走接口,边界清晰)。
4. 技术实现(分步)
核心是一句话:把引文和精修文本用同一把尺子洗干净,然后看引文这段字符到底在不在文本里;在,就钉死它的精确区间;不在,就拦下。 全程没有任何一步问模型"你觉得对不对"。
第 0 步 · 防呆:确认比对基准没被换过。 取精修文本后先算它当前的指纹,和输入带来的 content_hash 比。不一致说明 B5 抽取后精修文本又被改过(比如人改了纪要触发重精修)——此时位置全失效,整场退回重抽,不在错的基准上做无意义的核对。
第 1 步 · 文本规范化(关键:两边用同一把尺子)。 模型抄引文时常顺手改了标点、空格、全半角、大小写,字面比对会因为这些无意义差异全军覆没。所以比对前,把"精修文本"和"引文"都过同一套规范化(规范化 = 统一成一种标准写法,消除不影响语义的表层差异):
- 统一空白:连续空格/换行/制表压成一个,首尾去空。
- 统一标点:中英文标点归一(", 。 ! ?" 与 ", . ! ?" 等价),成对引号/括号归一。
- 全角半角、英文大小写归一。
- 逐字符记录映射:规范化会改变字符串长度,必须同时建一张"规范化后第 i 个字符 ↔ 原文第 j 个字符"的对照表。最终要写回事实库的区间是原文坐标(用户点出处要跳回没被洗过的原文),不能是规范化后的坐标。这张表是后面所有"钉死区间"能落回原文的前提。
第 2 步 · 先按自报位置精确比对(快路径)。 若模型给了 claimed_span,就在规范化文本里取那一段,和规范化后的引文逐字比。一致 → hit_exact,经映射表换算回原文坐标,钉死区间,放行。这是最省事的路径,但自报位置只是加速提示:对不上绝不据此判错,继续走下一步(模型给的位置经常偏)。
第 3 步 · 全文找子串(忽略位置)。 在整篇规范化文本里搜规范化引文这段子串:
- 恰好一处:
hit_substr,换算回原文坐标钉死,放行。 - 多处都能对上(第 4 步 · 命中多处):取离
claimed_span最近的那一处(模型位置虽不精确,但通常落在大致范围,够用来区分重复句)。若没有claimed_span、或多处距离都差不多无法区分 → 不猜,转 B7 人工(让人指认是哪一处)。宁可慢一条,不可钉错一条。 - 一处都没有:进第 5 步模糊对齐。
第 5 步 · 模糊对齐 + 相似度阈值(容错最后一关)。 找不到完全一致的子串,可能是模型抄时漏字/多字/小改个别词(还算如实转述),也可能是整句编的。用一个确定性的近似匹配:在规范化文本里用滑动窗口(窗口长度 ≈ 引文长度上下浮动)找与引文最相近的片段,算两者编辑距离(把一个字符串改成另一个最少要增删改几个字),归一成 0~1 的相似度分(1 = 完全一样)。这个算法同输入永远同输出,可复算、可审计。按两道阈值判:
- 相似度 ≥ 高阈值(如 0.92):认定是如实转述只是抄写有微小出入 →
hit_fuzzy,把最相近片段在原文里的区间钉死(注意:钉的是原文里真实存在的那段,不是模型那句),放行。 - 中阈值 ≤ 相似度 < 高阈值(如 0.75~0.92):像、但不敢替模型背书 →
to_human,转 B7 由负责人裁定(见 FDS-16)。 - 相似度 < 中阈值(如 < 0.75):原文根本没有相近的话 → 判定编造。按
kind分流:决策/风险/待办这类一旦为真影响大的,即便编造嫌疑也走to_human(让人最后看一眼,避免误杀真信息);一般fact类直接dropped。无论丢弃还是转人工,都写一条审计日志(extract_id、引文、最高相似分、判定理由),可追溯为什么拦它。
第 6 步 · 阈值的确定与冻结。 高/中阈值不是拍脑袋——用一批人工标注好的样例(哪些引文是真转述、哪些是编造)跑一遍,选能把"真的别误杀、假的别放过"分得最开的值,写进代码常量并记录依据。一旦定下,生产期不随意改;要调必须重跑样例、记录新依据(和事实库版本失效同思路:判定规则也得可追溯)。
第 7 步 · 批量与边界。
- 一场会几十上百条,批量核;单条互不影响,任一条异常(如引文为空)单独记
dropped+ 原因,不拖垮整批。 - 引文过短(如只有几个字"同意""推进")子串匹配会满篇命中且无意义——设最小引文长度,过短的直接
to_human(太短的"出处"本就不足以追溯,交人确认)。 - 全程不写任何库:B6 只产出判定;
hit_*的条目连同钉死的source_ref交 B8 入库,to_human的交 B7,dropped的只留审计日志。职责单一。
5. 技能 or 代码
全部代码(acdm-backend),不是技能,一行都不进 SKILL.md。
依 FDS-04 §2 的判据——"错了会算错数据/出假结论的 → 代码"——B6 是这条判据最典型的样板:它的全部价值就在于不被"听起来很对"骗,而模型恰恰最擅长生成"听起来很对"。把核验写成技能让模型自律,等于撤掉这道闸门。规范化、子串/模糊匹配、阈值、区间换算全是确定性算法,同输入永远同输出、可复算、可审计——这正是"防瞎编"必须具备的性质,模型判断不具备。技能(B3 精修、B5 抽取)只负责"生成";"验真"永远在代码侧,两者职责不混。
6. 二开归类
🔴 从头自建(acdm-backend),放在流水线"抽取(B5)之后、入库(B8)之前"的固定关卡——这是单场会议流水线里一个写死的代码节点,不可被技能替代、不可被跳过。
- 为什么不复用 deer-flow 的东西:deer-flow 没有"核对模型自报引文是否真在原文"这类能力——它面向"生成与检索",不面向"对生成结果做确定性验真"。这是本模块特有的正确性需求,upstream 无对应扩展点,只能自建。与 FDS-04 §6 三条不让步一致:防瞎编是代码红线,不外包给模型机制。
- 复用什么:复用项目已有的取文接口(读 B3 精修文本与
segments)、复用 B7 站内确认通道(转人工不自造一套)、复用 B8 入库接口(放行条目交它写,B6 自己不碰库)。新建的只有"规范化 + 匹配 + 判定"这一块确定性核验逻辑。 - 自建边界:只做"判定 + 钉死区间",不做生成(那是 B5)、不做写库(那是 B8)、不做派人(那是 B7)。一个纯函数式的关卡。
7. 与人机协同模型关系
B6 是任务流里一个纯机器、无 AI 参与的内层关卡,体现"AI 与人共写"里机器把关那一半:
- AI(B5 技能)负责"提出"——抽出结论并主张出处;代码(B6)负责"验真"——不接受单凭主张,必须经程序核对;人(B7)负责"裁定"——程序拿不准的(像但不敢断、命中多处无法区分)派回负责人。三方各管一段,谁都不替别人拍板。
- 对应工作台模型:B6 放行的事实进项目级账本(事实库);B6 拦下的变成任务卡(B7 确认任务)出现在负责人工作台。它是"AI 产出"与"可信事实"之间那道不可绕过的质检,确保进账本的每条都有真出处。
8. 验收(可证伪)
- 能挡编造:构造一批"原文里根本没有的引文"喂进去,全部被判
to_human或dropped,无一条被当作命中放行进事实库。 - 不误杀如实转述:构造一批"原文确有、仅标点/空格/全半角/个别字小改"的引文,全部被判
hit_*(精确/子串/模糊)放行,且钉死的区间能在原文里高亮出对应那句。 - 区间钉得准:随机抽 20 条放行项,按其
source_ref跳回原文,高亮段落与结论所指一致(配合 FDS-17 §8 的同项验收)。 - 重复句不钉错:精修文本中存在多处相同句子时,命中能落到与上下文相符的那一处;无法区分的转人工而非随便钉一处。
- 可复算:同一场同一批输入跑两次,每条
verdict、source_ref、similarity完全一致(确定性,无随机)。 - 基准防呆:故意在抽取后改精修文本(改 content_hash),B6 拒绝在旧基准上核对并触发整场重抽,而非给出错误区间。
9. 现状
没有。当前出处全靠模型自报——抽取时模型顺带说"这条出自某句、大概在某位置",没有任何程序核对:引文可能是编的、位置可能是错的、点开"看出处"可能跳到不相干处甚至跳不过去,也无法追溯某条结论凭什么进了库。下游(待办/看板/复盘)又全部依赖这些未经核验的出处,假出处会顺着链路污染一片。本章从零补上这道"抽取之后、入库之前"的确定性闸门,把"出处可信、可追溯"这件事从"靠模型自觉"变成"靠程序保证"。
10. 风险与取舍
- 风险:阈值定太松→放编造进库;定太紧→误杀如实转述、把人淹没在确认任务里。取舍:用人工标注样例校准并冻结阈值(第 6 步),宁可偏紧(误杀转人工成本可控,放假进库污染地基不可控);拿不准一律转 B7,不让程序在灰区替人拍板。
- 风险:模糊匹配在长引文上算量大,一场会几百条可能慢。取舍:先精确/子串快路径拦掉绝大多数(模型多数会照抄),只有少数落到模糊;模糊只在引文长度邻域开窗,不全文两两比;批量异步跑、不阻塞用户提交。必要时对超长引文截断取代表片段比对。
- 风险:精修文本本身有错(B3 漏了原话)→ 真引文反被判编造。取舍:这类落
to_human而非直接dropped,由人发现"是精修丢了"并回退重跑 B3(与 FDS-12 §10"宁可保留也不臆造删改"呼应)——B6 只对"引文 vs 精修文本"负责,精修质量由 B3 验收兜底,职责不越界。 - 风险:规范化过度→把本不同的两句洗成一样,误判命中。取舍:规范化只动确定无语义的表层差异(空白/标点/全半角/大小写),不做近义替换、不做去停用词;凡可能改变语义的归一一律不做,误判方向永远偏向"判不命中转人工",不偏向"误命中放行"。
- 风险:模型学会"抄一段原文真句子当引文,但配一条原文没有的结论"——引文真、结论假,B6 只验引文存在性会放过。取舍:B6 的边界明确只验"引文是否真实存在于原文",不验"结论是否由该引文支撑"(那是语义判断,确定性程序做不了)。"引文真但结论歪"由 B5 抽取技能的 eval 样例约束 + B7 人工抽检兜底,不让 B6 越界去做它做不可靠的语义裁决——一个关卡只做它能做对的那件事。