MIT 开源项目 Bedrock 从 DOI 列表出发,沿最多两跳引用关系查找撤稿论文,并展示引用链、撤稿时间和原因。文章介绍 FalkorDB 实现经验及智能体计数错误,AI 仅解释结果和建议修改,不能直接执行。
本文最初发表于 bedrock-audit.ibnweb3lab.workers.dev/blog,为 Graph Hacks 黑客松(WeMakeDevs × FalkorDB)的 Track 01 赛道而作。
想象一下,你花了一整年做文献综述。参考文献列表里的每个来源看起来都没问题,因为文献管理工具没有标出任何异常。但沿着引用关系往回追两步,其中一个来源依赖的论文早在几年前就被撤稿了,而你使用的工具,没有一个会告诉你这件事。
Bedrock 就是为填补这个空白而做的。粘贴一组 DOI,它就会从你的论文出发,沿每条引用路径向后追溯,最多两步,查找是否关联到被撤稿的研究。对于每项发现,它都会展示完整的引用链、撤稿日期以及记录在案的撤稿原因。AI Agent 负责解释结果、提出修改建议,但它永远不能自行执行修改。
这篇文章讲的是我如何在三天里把它做出来:哪些部分进展顺利,哪部分做砸了,以及我自己的 Agent 是怎么把数量数错的。
代码(MIT):GitHub 仓库 ibnweb3/bedrock
在线体验:bedrock-audit.ibnweb3lab.workers.dev
标记撤稿论文的工具通常只检查你引用的论文,不会进一步追问这些论文又引用了什么。后一个问题属于图查询:从一篇论文出发,沿着 CITES 边遍历,遇到已撤稿的节点就停下,并返回这条路径。
下面是语料库中的一条真实路径:
your reference ──cites──▶ not flagged ──cites──▶ RETRACTED 2014-10-01
10.1016/j.neubiorev 10.1111/ejn.13603 10.1016/j.jpsychires
.2019.09.040 .2014.06.018
中间那篇论文没有被撤稿,这恰恰就是你收不到任何提醒的原因。
整个项目始终遵循两条规则。第一,Bedrock 只陈述固定语料库中的事实,不做猜测。对于显示“未发现关联”的论文,绝不称其为安全,只说明在两跳范围内没有找到相关路径。第二,决定权在人手里。Agent 提出建议,由人批准。
撤稿记录来自 Crossref 分发的 Retraction Watch 数据库,引用关系来自采用 CC0 协议的 OpenAlex。我拉取了 213 本期刊、共计 251,645 篇文章的数据,并将它们与 61,299 篇已撤稿文献关联起来。黑客松规则要求只能使用公开数据,因此在任何数据写入磁盘之前,我就在内存中使用秘密 salt 对作者信息进行哈希处理,并删除姓名、电子邮件和 ORCID。我发布的图完全不包含作者数据。
Retraction Watch ─┐
├─▶ FalkorDB (Cypher) ─▶ audit queries ─▶ read-only agent ─▶ you approve
OpenAlex (CC0) ───┘
每一项发现都由图查询产生。模型只负责撰写总结,任何修改都由人批准。
数据模型刻意保持简单:Paper 节点携带 doi、retracted_at 和 retraction_reasons 属性,另有 Venue 节点,以及 CITES、PUBLISHED_IN 两种关系。撤稿被建模为属性,而不是独立节点。这样既能控制图的规模,也让“截至某个日期”的筛选变得非常简单。两跳问题只需要一条查询:
UNWIND $ids AS i
MATCH (p:Paper {id: i})
WITH p
MATCH (p)-[:CITES]->(x:Paper)
WHERE x.retracted_at IS NULL
WITH p, x
MATCH (x)-[:CITES]->(r:Paper)
WHERE r.retracted_at IS NOT NULL AND NOT (p)-[:CITES]->(r)
RETURN p.id, x.id, x.doi, r.id, r.doi, r.retracted_at, r.retraction_reasons
LIMIT $limit
用人话读一遍:这篇论文引用了 x,x 没有被撤稿,x 又引用了已撤稿的 r,并且原论文没有直接引用 r。Bedrock 展示的所有内容,包括直接命中的撤稿论文、两跳路径,以及用于寻找替代文献的“与它一同被引用的论文”建议,都是由 FalkorDB 执行这种规模的查询得到的。
第一次审查九篇论文花了 59 秒。数据量不大,问题也简单,显然哪里不对。原因是:FalkorDB 的查询规划器从 61,299 篇已撤稿论文出发,反向查找我的输入,而不是从我指定的那几篇论文开始。解决办法就是上面那条不起眼的 WITH p。它迫使遍历从输入论文开始,再沿着它们数量不多的参考文献展开。
修复后重新计时:九篇论文需要 0.05 到 0.4 秒,150 篇需要 0.15 到 0.4 秒。
我从中学到的是:在图数据库里,查询的结构比数据量更重要。如果可以通过索引查找确定遍历的起点,就要在查询中明确表达出来。这决定了它究竟只能做个 demo,还是能成为产品。
Bedrock 最初的想法很有野心:利用图信号来预测哪些论文会被撤稿,例如一篇论文引用了谁、与哪些论文有共同作者,以及它属于哪个社群。在查看任何结果之前,我先写下了通过标准:图方法必须优于表现最好的简单基线,而且移除规模最大的期刊后,这个优势仍然必须成立。随后,我在有撤稿历史的期刊中选取了 134,798 篇文章进行测试,其中 140 篇在随后两年内被撤稿,截止日期为 2020-12-31。
结论是:没有可靠的提升。最佳组合相对于期刊基线的优势,小于不确定性范围。这 140 次撤稿集中在 20 到 27 本期刊中,因此研究的统计功效较低,而且多跳信号也没有优于对应的一跳信号。唯一突出的信号是“这篇论文已经引用了被撤稿的论文”:根据截止日期的不同,具有这一特征的论文后来被撤稿的可能性,是平均水平的 5 到 12 倍。但这属于事实查询,不是预测。
我本可以照样发布一个“撤稿风险评分”。它看上去会很厉害,却没有足够的证据支撑。于是,我改变了产品方向:Bedrock 不再预测,而是提供证据。它展示精确的来源链路,并把失败的测试放在首页。相比任何一条查询,这个决定才是我愿意在任何人面前坚持的。
Agent 使用 Groq 免费套餐上的 openai/gpt-oss-120b。它有三个工具:audit_bibliography、explain_finding 和 find_dois,而且全部只读。它没有编辑工具,还有一项测试确保这一点始终成立。这些护栏都来自我亲眼见过的失误:
依据约束。 回答中的每个 DOI 都必须来自你输入的文本或工具返回的结果。模型曾在重新输入 DOI 时把其中一些写错,因此现在只要出现凭空生成的 DOI,就会显示明确的警告。
完整性。 它曾经漏掉一篇被标记的论文。如果总结遗漏了某篇,Agent 会获得一次重试机会;重试后仍有遗漏,就用直白的文字把缺失内容补上。
数量。 在同一次运行中,审查明明找到了两篇撤稿论文,它开头却写“有一篇撤稿论文”。模型会数错,因此开场那句“已检查 9 个 DOI:2 篇已撤稿,2 篇直接引用了已撤稿文献……”始终由代码根据审查结果生成,绝不交给模型。
降级处理。 发现结果的计算独立于模型。即使缺少 key,或者 Groq 触发限流,你仍然能拿到完整的审查结果。
接下来是批准环节:服务器会自行重新执行审查,而且只应用它自己生成的 proposal id。浏览器不能直接交来一份结果,让服务器照单全收。
研究用图包含作者,以及为回测构建的两种衍生边:169 万条 COAUTHOR 和 268 万条 BIB_COUPLED,它们都在 FalkorDB 内部通过 Cypher 构建。整张图大约有 250 万个节点,太大了,不适合直接交给别人使用。但关联路径只能沿着我所持有论文之间的 CITES 关系前进,因此对审查来说,其中大部分数据都是负担。
于是,我构建了一张精简图,只保留论文、期刊,以及已收录论文之间的引用关系:318,942 个节点,530,894 条边。然后我没有停留在口头宣称,而是实际验证了它。一个脚本在两张图上分别审查了 2,501 个 DOI,并比较完整结果:状态、路径、原因、覆盖情况和替代文献。差异为零。(这项测试还抓到了一个真实 bug:37 篇论文因为自引用,参考文献计数恰好多了 1。)
精简图以一个 17 MB 的 DUMP 快照随仓库分发。在限制为 512 MB 内存和十分之一个 CPU 的容器中,大约一分钟就能完成恢复,稳定运行时占用约 300 MB 内存。评委可以用下面的命令运行整个项目:
git clone https://github.com/ibnweb3/bedrock && cd bedrock
docker compose --profile demo up --build app
# open http://127.0.0.1:8765
我从一个全新 clone、没有配置任何 key 的环境验证过:不使用缓存的构建耗时略超过两分钟,应用启动五秒后就能响应。
它只了解自己的语料库:213 本期刊、251,645 篇文章及其引用的论文,使用的是 2026 年 10 月 15 日的 Retraction Watch 快照。
它向后追溯两跳。“未发现关联”只代表在这个范围内没有找到,并不意味着论文可靠。
撤稿原因和日期来自 Retraction Watch。标记只是提醒人去阅读,绝不是对作者作出的判断。
在线 demo 运行在我自己的机器上,因此,上面的一条命令启动方式才是可靠的使用途径。
运行测试前,先写下通过标准。正是这一步,才让失败成为一个结果,而不是一场争论。
在图数据库里,要明确遍历的起点。查询结构比数据规模更重要。
让代码陈述事实,让模型组织文字。即便如此,也要核对模型写出的数字。
证明你的简化是成立的。“精简图与原图等价”这个说法,只有在完成 2,501 个 DOI 的测试之后,我才有底气说出口。
我独自用了三天完成这个项目,Claude Code 根据我的指令编写了大部分代码和文档。我选择了这个想法、制定规则、批准实验规模、决定从预测转向来源追溯、为 Agent 选用 Groq,并最终批准了每一次发布和部署。决策日志位于仓库的 docs/DECISIONS.md。代码采用 MIT 许可证;数据则保留各自的使用条款:通过 Crossref 获取的 Retraction Watch 数据,以及采用 CC0 的 OpenAlex 数据。
照片:死亡谷 Zabriskie Point 的沉积岩层,摄影者 Brigitte Werner,CC0,来自 Wikimedia Commons。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。