构建 TrialProof 时,我不再追逐准确率,开始清点没有丢掉的符合入组条件的患者:固定 13 案例金标准集上 0 对 3。
Clinical TrialsMachine LearningHealthcare

我做了个演示想复现一个著名的 AI 错误。我的基线拒绝犯这个错。

Ashutosh SinghalAshutosh Singhal2026年6月29日12 min

我无法复现的那个错误

我开始做这个演示时,本想复现一个有充分文档记载的特定失败。患者匹配 AI 把临床笔记当文本来读,于是会把医学里形似而义异的词搞混。经典例子很干净:一项 III 期抗凝试验排除有既往心导管检查,某患者的笔记写的是中心静脉导管置入,相似度匹配器却把两者看成两种心血管导管操作,给出接近的分数,于是把其实符合入组条件的患者排除了。已发表的评估证实真实模型会犯这个确切错误(Fierce Biotech,2025)。我希望我的演示先让它发生,再展示我的引擎如何抓住它。

于是我写了一个公平的基线来当反派。实体级 TF-IDF 余弦相似度,词与字符 3 到 5-gram,一种真正的向量相似度方法。我甚至给了它宽松的设置,并替它做了决策阈值的交叉验证(分层 3 折 ROC、Youden's J、种子 13、钳制于 t = 0.6932),因为稻草人证明不了什么。然后我跑了心导管案例,等待错误排除出现。

它没有出现。基线给这两个导管短语打出的分数稳稳低于其自身交叉验证得到的阈值,并返回了符合入组条件。我整场演示所围绕的那个错误,根本复现不出来。

我本想上演一场著名的失败,却发现我诚实的反派太弱,犯不下那个错。

原因后来证明很有启发,我想说得精确,因为这里很容易被夸大。一个稀疏词汇基线产生不了那种特定的错误排除。它需要稠密语义嵌入,才能把那两个短语拉得足够近从而触发。再加一个重量级嵌入模型会把演示撑成无法一条命令离线运行的东西,于是我做了决定:保持基线诚实且稀疏,不再假装它会犯它犯不了的罪。这个决定重组了我正在写的整篇文章。如果你想自己跑,它在 veriprajna.com/zh-Hans/demos/clinical-trial-recruitment-ai。

中心静脉导管到底是什么?

我仍然保留了心导管案例,因为它最终证明的东西比抓住一个错误更好。它证明的是为什么我的引擎给出的答案根本值得信任。这里的两个概念都有真实、可核对的 SNOMED-CT 标识符。中心静脉置管392230005心导管检查41976001。你可以把其中任何一个粘贴到任何公开的 SNOMED 浏览器里,确认它们位于层级的不同分支上。不存在从一者到另一者的is-a路径。中心静脉导管不是心导管检查,只有层级结构才知道这一点。

这就是整篇论题浓缩在图的一条边上。相似度分数无法表示「is-a」。它只能表示「这些字符串看起来相似」,而看起来相似并不等于意思相同。当我的引擎评估排除条件「无既往心导管检查」时,它并不打分。它问的是一个结构问题:患者的已验证事实是否被禁止概念所包容?它遍历本体,找不到包容路径,于是返回符合入组条件,并给出三步轨迹,点名两个概念 ID 以及它检查过的图边。

TrialProof 推理轨迹显示中心静脉置管 392230005 is-a 心导管检查 41976001 判定为 False,位于层级的不同分支,裁决为 ELIGIBLE
合成患者 P-074 上的 EXCL-CARDCATH 轨迹。第 2 步询问中心静脉置管(392230005)是否 is-a 心导管检查(41976001),答案为 False(层级的不同分支),并返回 ELIGIBLE。下方基线面板报告一个低于阈值的相似度分数,没有出处,也没有任何可复现的东西。

当我第一次看着那条轨迹渲染出来时,打动我的不是裁决本身,而是它下面的收据。同一屏幕上基线的框只显示一个相似度数字,没有出处,也没有任何可复现的东西。我的引擎的框点名了两个 SCTID 以及它问出的那个确切的is-a问题。其中一个是监管方可归档的材料。另一个只是一个带着耸肩的数字。那种对照——而不是抓住一个错误——才是心导管案例真正赢来的东西。

匹配器真正扔掉的那个患者

我仍然需要一个真正被丢掉的患者,于是去找诚实基线真正失败的地方,结果发现它就在一个词上:not。合成主角病历 P-074 带着笔记行「无糖尿病证据」。肿瘤方案的排除条件之一是「无 diabetes mellitus 诊断」。向量基线看到词元「diabetes」紧挨着标准里的「diabetes」,并以相似度 1.0匹配它们。完美分数。它没有否定模型,于是把一句将糖尿病排除在外的句子读成好像把糖尿病纳入其中,从而排除了一个本符合入组条件的患者。

这就是匹配器扔掉的那个患者,也是我原本指望心导管案例来承担的那个桥段。否定,正是稀疏基线在自身最优阈值下诚实崩坏之处,无需任何操纵。

TrialProof EXCL-DM 轨迹:基线以相似度 1.0 把 diabetes 匹配到 diabetes 并返回 EXCLUDED,而验证器剥掉被否定的提及,引擎返回 ELIGIBLE
P-074 上的 EXCL-DM 裁决。基线的最佳提及以相似度 1.0 把「Diabetes mellitus」匹配到「Diabetes mellitus」并返回 EXCLUDED。TrialProof 的验证器剥掉被否定的提及,于是没有任何已验证事实被禁止概念所包容,裁决为 ELIGIBLE。

我一直在想,这种失败有多安静。没有错误信息,没有低置信度标志,没有任何信号表明出了问题。分数是 1.0,可能的最高分,系统能有的最强把握。基线从未比它错得最厉害的那一刻更确定。审核这类队列的协调员无从得知,这个特别完美的匹配其实是一个本该入组的患者。把这放大到整个方案,你就明白为什么 80% 的试验错过入组时间表(行业共识,2025),以及为什么每次筛查失败平均大约花费 $1,200(Antidote.me,2025)。

基线从未比它错得最厉害的那一刻更自信。那不是你能调参消掉的 bug。那是一个范畴错误。

为什么裁决要活在模型之外?

我很早就做了一个架构决定,现在我认为那是唯一真正要紧的决定:让语言模型完全远离裁决。整条流水线里恰好只有一个概率步骤。一个可替换提供商的模型,仅作咨询,阅读凌乱的散文并提出候选事实,每个事实带着它读到该事实的原文片段,以及从一个小的封闭词表中抽取的候选概念 ID。那正是模型真正擅长的一件事:阅读。它在谁符合入组条件上没有投票权。

之后的一切都是我可以审计的确定性代码。在任何提议的事实进入决策之前,一个对抗式验证器对照字面笔记用三项检查挑战它:片段是否实际存在、是否被否定、以及主语是否是患者而非家属。「无糖尿病证据」这个事实未通过否定检查,永远到不了引擎。同一份病历上,「乳腺癌家族史」未通过主语检查,因为那段病史属于家属而非患者,于是被标记为拒绝,并点名失败的检查。

TrialProof Verify Facts 面板显示每个提议的事实在进入决策前都要接受片段存在、否定与主语的挑战
P-074 上的 Verify Facts 阶段。模型提出的每个事实在进入决策前都要接受挑战:片段存在、未被否定、主语是患者本人。这里中心静脉置管事实被 ACCEPTED,原因:片段存在、未被否定、患者主语。未通过检查的事实被标记为 REJECTED,并点名原因。

在整个金标准集上,这个验证器拒绝了7 个事实实例、3 个不同的坏事实(一个被否定的糖尿病提及、一个乳腺癌家族史归属,以及一个没有支持片段、人为植入的幻觉药物),分布在 13 次计分案例运行中的 4 次,全部发生在它们触及裁决之前。当有人问我「我怎么信任智能体从我的笔记里抽出的东西」时,这个面板就是全部答案。我不要求你信任它。我展示它提议了什么、扔掉了什么以及为什么。

然后裁决本身是坐在智能体框架之外的普通 Python:一个道义逻辑引擎,在本体和一些日期运算上评估禁止、时间例外与要求。模型无法推翻这道闸门,因为闸门运行时模型不在场。这也正是引擎可复现的原因。当逻辑是固定本体上的确定性代码时,重跑同一份病历会逐字节给出相同答案,每一次都是。

模型负责读。它不投票。那一道边界,正是让重跑逐字节相同的东西。

我的临床运营读者真正在意的唯一数字

我花了数周优化那些指标,最终自己承认买家夜里不会为它们失眠。决策准确率是排行榜数字。赞助方或 CRO 里负责可行性的人并不在比排行榜分数。他们看着入组时间表在滑落,而每一天的滑落都很贵。Tufts CSDD Impact Report(2024)把入组延误的成本大约定为每天损失处方销售额约 $800K,而在这个演示触及的治疗领域更高:肿瘤学约每天 $840K,心血管约每天 $1.4M。自 2005 年以来,试验程序中的方案复杂度攀升了 139%(IQVIA,2026),意味着更多标准、更多条款,以及文本匹配器更容易弄错一处的地方。

于是我不再以准确率开场,开始以真正映射到那种痛感的数字开场:你没有丢掉的符合入组条件的患者。在一个固定的、已标注的金标准集上——13 个案例,来自 7 名合成患者、跨越 2 个合成方案——我的引擎丢掉0 名符合入组条件的患者。公平基线丢掉 3 名。同一集合,同一阈值按对基线有利的方式交叉验证。

TrialProof 基准磁贴:决策准确率 100% 对基线 53.8%,丢掉的符合入组条件患者 0 对 3,可审计轨迹覆盖率 100% 对 0%,基于已标注金标准集
金标准集基准。在 13 个已标注案例上,TrialProof 决策准确率 100%,基线为 53.8%;丢掉 0 名符合入组条件的患者,基线丢掉 3 名;100% 的决策带有推理轨迹,基线为 0%。重跑全部 13 个案例得到逐字节相同的裁决与轨迹。

我想精确说明这些数字是什么、不是什么。它们是该固定 13 案例集上评测架自身的输出,不是开放世界的承诺。100% 是「在这个金标准集上的 100%」,绝不是「永远正确」。我不会告诉你 TrialProof 从不出错,因为我没有数据这么说,也不会相信任何这么说的人。我能说的更窄,也更有用,我认为:在这个集合上引擎丢掉零名符合入组条件的患者,每个决策都带有可复现轨迹,有两次决策在缺少所需实验室或生命体征时安全弃权并给出 NEEDS-REVIEW 而不是猜测,并且重跑整个集合逐字节相同,13 对 13。所有患者、笔记与方案都是合成夹具,任何地方都没有真实记录。你可以在 veriprajna.com/zh-Hans/demos/clinical-trial-recruitment-ai 观看每一次运行。

我在意的数字不是准确率。而是我没有扔掉的符合入组条件的患者。在这个集合上,相对基线丢掉的三名,那是零。

这里也有监管形态,我会小心地命名它。FDA 2026 年 1 月的临床决策支持指南,是这类人在环匹配辅助的相关框架。引擎发出的每个决策都可以导出为 CDISC SDTM IE 记录,每位患者与每条标准一行,携带裁决、推理轨迹、概念 ID 与道义操作。那不是准入许可,我也没有在主张获批。那是对齐与方向。但这意味着轨迹不是调试便利。它是一份可归档产物,并且从设计上就存在于每个决策上,而不是事后补上。

我不断回到的地方

我不断回到反派拒绝演它那一角的时刻,因为它改变了我在问的问题。三年来,这个领域一直在问如何让模型在决定谁符合入组条件上变得更好。更好的提示、更大的上下文、更多检索,全都瞄准让概率系统足够可信,好对患者入组做裁决。这段构建的前半程我也困在那个框架里,试图抓住模型的一个错误,好去修模型。

最终想通的是:那是错误的层。相似度分数无法表示「is-a」,无法表示「不」,也无法表示「除非治疗在随机化前十二个月以上已完成」。再多提示也加不进去这些,因为它们不是语言问题。它们是逻辑问题。所以高级做法不是让模型变得可信。而是让信任变得不必要。让模型做它擅长的那一件事:阅读散文,并提出带着它所读片段的事实。然后让验证器扔掉笔记不支持的东西,再让基于医学本体的普通、可审计代码计算裁决。

入组资格应当被计算,而不是被预测。出乎我意料的是,走进来时我没想到回报感觉根本不像基准。它感觉像一张收据。同一份病历每次给出相同答案,答案点名决定它的概念 ID 与图边,而可行性负责人真正为之失眠的那个数字——被扔掉的符合入组条件的患者——变成零。

如果你更想看它跑而不是听我描述,整套端到端运行就在这里。

所以这就是我一直翻来覆去的问题,我真心想知道你会怎么答。当赌注是一个真实的人参加试验的机会时,你希望信任落在哪里:落在一个你必须相信的模型里,还是落在你可以阅读的代码里?

相关研究

同步发布于

满怀信心地构建您的 AI。

与一支在打造新一代企业级 AI 方面拥有深厚经验的团队携手合作。让我们助您设计、构建并部署一套值得信赖的 AI 战略。

Veriprajna 深度科技咨询公司 专注于为医疗健康、金融和监管等领域构建安全攸关的 AI 系统。我们的架构均依据成熟的规范进行验证,并配有完善的合规文档。