文献核验实践:让模型解析,让规则裁决
CiteWise 查询多个学术数据源,按标题、作者、年份和 DOI 等字段核验引用。文章讨论模型参与处理杂乱引用时的职责边界,并复盘首版中意外越过该边界的问题。
AI 深度解读
CiteWise 用 LLM 辅助核查学术引用,却不让模型决定引用是否真实。作者踩过的坑很具体:模型补出一个合理标题,系统找到对应论文,再拿这个标题验证论文,最终把模型自己的猜测判成了「已验证」。
这篇值得程序员读的地方,是它拆出了两条容易混淆的边界:搜索线索不能充当核验依据,服务没有响应也不能算作搜索无结果。即使最后一步完全由规则执行,上游数据用错,结论仍然会错。
规则评分没变,为什么还能误判
CiteWise 要检查的是:一条学术引用是否指向真实论文,以及引用中的字段是否与论文记录一致。
它先用规则解析器提取标题、作者、年份、期刊、DOI,再并行查询 Crossref、DataCite、OpenAlex、Semantic Scholar、PubMed 和 arXiv。对返回的候选论文,系统逐字段计算标题相似度、作者重合度、年份和 DOI 是否一致,并对矛盾信息扣分,最后给出 verified、ambiguous、conflicting metadata、not found 等状态。
评分是确定性的。同一条引用、同一批来源响应,重复执行会得到相同分数。
但确定性只能保证规则执行一致,不能保证比较对象正确。
有些引用从 PDF 复制出来后,标点丢失,作者、标题、期刊混成一串。规则解析不完整,搜索也就可能找不到。作者因此增加了 LLM 补字段步骤,但只在两个条件同时满足时触发:没有 DOI,且首轮结果为 not found 或 insufficient evidence。
问题出在重试之后。旧实现看到新分数不低于首轮,就执行 candidate = enriched,把模型补全的版本替换成待核验对象。
这个变量既参与检索,也充当评分基准。替换之后,系统实际回答的问题变成了:找到的论文是否符合模型补出的描述。
规则引擎可以稳定地算出一个错误结论,因为它核对的已经不是用户输入。
旧代码里的 >= 又放大了问题:即使重试没有提高分数,模型版本也会在平局时胜出。
模型可以帮忙找,但不能改掉待核验内容
作者的修复不是换模型,而是把模型输出限制为检索提示。它可以提供更多搜索词,帮助找到候选论文;候选论文返回后,仍须与用户原始输入解析出的字段比较。
下面这段代码把两份数据的用途直接写进了调用关系:
// 补全结果只提供检索提示。
// 它绝不能成为关于用户输入的证据。
ResolutionResult resolveWithHints(Candidate original, Candidate hints) {
return resolve(original, retrieval.retrieve(hints));
}
要看的是最后一行:hints 只进入 retrieval.retrieve(hints),评分接收的始终是 original。
重试结果的选择也改成了严格比较:
var selected = retry.score() > first.score() ? retry : first;
这里的 > 保证平分时保留首轮结果。只有新找到的论文与原始字段更匹配,第二轮才会替换第一轮。
不过,严格大于只是配套修复。真正切断错误链路的,是原始输入不再被模型补全结果替换。否则,哪怕分数确实提高,也可能只是模型生成的内容更容易与某篇真实论文匹配。
作者还撤掉了一个过度推断的状态:POSSIBLE_HALLUCINATION。旧系统只要经过 LLM 处理却仍然没找到,就可能给出这个标签。但「没有检索到」不足以证明引用是幻觉。新结果不再产生该状态,历史记录则显示为「按旧规则未找到」。
这也划清了产品措辞的边界:系统应报告观察到的结果,不能替缺失的证据补上解释。
「没有结果」之前,先确认搜索真的完成了
第二个问题与模型无关,却同样会让系统给出过强结论。
旧代码通过 diagnostics.isEmpty() || diagnostics.stream().anyMatch(d -> d.ok()) 判断搜索是否完成。它把诊断列表为空,也算成了搜索完成。
于是,如果请求在任何来源回报之前就出了问题,系统仍可能告诉用户「未找到」。实际上,它连一次可用的核查都没做完。
修复后,每次检查都会记录哪些来源真正响应,并单独携带完整度字段:COMPLETE、PARTIAL 或 INCOMPLETE。
带 DOI 的引用要求 Crossref 和 DataCite 都响应;不带 DOI 的引用,要求三个通用来源中至少两个响应。原文没有明确指出这里的三个通用来源分别是哪三个,不宜自行补齐。达不到要求时,系统说明检查不完整并建议重试,而不是直接判为 not found。
核查结论与核查完整度,需要分开表达。
这个区分还进入了计费逻辑:完整执行但没有找到论文,仍消耗一次检查;来源宕机导致检查没能完成,则不收费。计费跟随实际完成的工作,而不是简单跟随一次请求。
我的判断
我赞同作者把 LLM 留在检索一侧。相比直接让模型回答「这篇论文真实吗」,这套流程能交代匹配了哪些字段、存在什么矛盾,也更方便排查错误。它保留了模型处理混乱文本的价值,同时让最终判断有可追溯的依据。
但这仍是一份工程经验记录。作者试过直接询问 LLM 的早期版本,也给出了旧实现的问题和修复代码,却没有提供误判率、召回率、评分阈值或修改前后的对照数据。因此,不能据此宣称规则方案已经达到某种准确率。
原始输入也不天然可靠。规则解析器可能提错字段,来源记录可能缺失,作者姓名和标题还可能存在不同写法。把模型输出移出评分基准,解决的是一次明确的数据混用,不能包办所有核验问题。
同样,两个来源响应是一条产品规则,不等于覆盖了全部学术文献。团队采用类似设计时,还得解释什么叫「响应成功」、什么叫「证据足够」。原文没有展开这些细节,结果页就更需要保留不确定性。
能拿来做什么
- 在日常 AI 编程中,让模型根据报错补搜索词、寻找文档或代码位置时,保留原始报错、用户需求和测试结果,验收仍围绕这些材料展开。
- 在 Node 服务里,把原始输入、检索提示和来源记录拆成用途明确的数据结构,避免同一个
candidate变量同时承担搜索条件与评分基准。 - 在前端结果页分别展示核查状态和检查完整度,让用户知道「完整查过但未找到」与「检查尚未完成」需要采取不同操作。
- 审查带模型重试的分支时,检查评分对象是否改变,以及
>=是否让模型结果无条件赢得平局;只有比较基准一致,分数高低才有意义。