拒绝阅读的工具 — 在诊疗流程里把判断放到外面

6 分钟
目标2026-04 我们刊了一篇医生做诊疗流程工具的文章。那篇只用文字写了这个工具不做什么。这次我们把它画进了代码 — 这个服务器没有产出判断的工具,而且一旦要吐出听起来像判定的词就会抛异常。

先说明两件事。

第一,2026 年 4 月,本刊登过一篇某位医生做诊疗流程工具的文章。这个现场不是某家特定的诊所。 我们把它实际运转的形状照实构成,也不点名是哪里。以下是按那个形状自己做的,流程与数值都是编的。

第二,这篇不是医疗建议,这里做的东西也不是医疗器械。 这个系列没有做临床验证,也不在能做的位置上。它处理的是软件的一条边界 —把判断放在工具之外,在代码里长什么样。

用文字画的线会被抹掉

原文里也有那条线。「诊断留给人,只把诊疗的流程整理到画面上。」

说得对。问题是它只作为一句话存在。

作为一句话存在的线会被抹掉。而且是以这种方式被抹掉:几个月后有人说「要是有一行摘要就方便了」,加了一个字段。那个字段里放进 大体在正常范围内 这样的字符串。没有人注意到那一刻。 那正是工具开始下诊断的那一刻。

所以这次我们把它画进了代码。画了两次。

第一条线 — 没有那样的工具

tools offered: flow.state, flow.done, readings.list

只有三个。展示流程、把某一步标记为完成、把测量值原样返回。没有做判定、分类、解读的工具。

验证从工具的名字开始看。

WORDS = ["diagnos", "assess", "judg", "risk", "abnormal", "recommend"]
for n in s.tool_names():
    assert not any(w in n.lower() for w in WORDS), n

这是先前一篇里确认过的原理的应用。工具清单就是表面,而不在清单上的能力,人去按也好模型去选也好都够不着。

第二条线 — 听起来像判定就抛异常

光有清单不够,因为判定可能混进 flow.state 返回的字符串里。

/// Words that would turn a report into a verdict. Anything this server is
/// about to say is checked against them.
///
/// A list of words is a crude guard and it is meant to be. It cannot stop a
/// determined author, but it does stop the ordinary way this line gets
/// crossed: someone adds a helpful-sounding summary field months later and
/// nobody notices that the tool started diagnosing.
static const forbidden = [
  'diagnos', 'likely', 'suggests', 'consistent with', 'probable',
  'abnormal', 'normal', 'healthy', 'concerning', 'severe', 'mild',
  'recommend', 'should take', 'prescribe',
];

static String _guard(String s) {
  final lower = s.toLowerCase();
  for (final w in forbidden) {
    if (lower.contains(w)) {
      throw StateError('clinic_server tried to emit a judgement word: "$w"');
    }
  }
  return s;
}

这个服务器吐出的每一个字符串都要经过这个函数。 请注意 normal 在清单上 — 它是看起来最无害的词,也是最常越线的词。

这是个粗糙的守卫。铁了心的人挡不住。可这条线实际被抹掉的方式不是铁了心,而是疏忽,而疏忽靠这点东西就挡住了。

而且验证会扫描整次运行。

no judgement vocabulary in anything the server returned

这个工具实际做的事

线画了两条,来看剩下的是什么。

按顺序展示流程。 就是医生写的那个顺序。

routine: intake* -> vitals* -> review -> exam -> plan -> note

* 是已完成的。画面最上面显示的是接下来要做的,而这也不是建议。

// "Next" is position in a list the clinician wrote. It is not advice.
'next': _guard(next.label),

它只是清单里的一个位置。 这个区分看着微不足道,但「接下来请做这个」和「在你写下的顺序里下一格是这个」,责任的位置是不同的。

今天的流程 —— 每一步都写着在打勾之前需要什么。真实渲染截图

把一步标记为完成,只有位置在动。

完成一步之后 —— NEXT IN THE LIST 往下走。“下一步”是临床医生所写清单里的位置,不是建议

测量值原样给出,但把参考范围挂在旁边。

readings: Blood pressure 148/92mmHg (clinic uses <130/80)
        | Heart rate 78bpm (clinic uses 60-100)
        | Temperature 36.8C (clinic uses 36.1-37.2)

148/92 旁边有 <130/80。但它不说「偏高」。 那个判断不属于这个工具。

挂上参考范围本身是有必要的。只有一个数字,读的人会自己把判定补上。 而那个判定不会被记录在任何地方。范围在旁边,至少「它是跟什么比的」留在了画面上。

验证也看这一条。

readings = s.call("readings.list")["readings"]
assert len(readings) == 3 and all(r["usual"] for r in readings)

读数 —— 每个数字旁边都是它的常规范围,任何地方都没有判定

这个样例的范围

没有这位医生。 而且上面的流程和参考值都是我编的。像 clinic uses <130/80 这样的值是这个样例的数字,不是任何诊疗指南。

这篇验证的只到软件的一条性质。 就是「这个工具不产出判断」。那并不意味着它是个好的诊疗工具。 它在临床上是否有用、是否安全、是否满足监管要求,全都在这个系列之外,而我们没有做那些判断所需的验证。

医疗器械监管也没有涉及。 这类软件从哪一点起进入监管范围,各辖区不同,这篇没有去探讨那条线。

把「不做什么」写进代码

原文那句话是对的。诊断留给人。

做出来之后再加一行。那句话必须在代码里的某处,以可执行的形式存在。 只写在文档里,六个月后没人记得,而没人记得的原则不是原则。

在这个服务器里,那个形式是三样东西。

  • 不存在的工具 — 清单就是表面
  • 会抛异常的守卫 — 听起来像判定就抛
  • 会失败的验证 — 上面两条一旦垮掉就不通过

三样都不是了不起的技术。只是三样都是代码,不是文字。 这个差别会在六个月后留下来。

练习任务

从设备示例中选一个回答,指出它引用的记录。如果没有匹配的记录,工具应该怎么做?

相关文章The Tool That Refuses to Read — Keeping Judgment Outside a Clinic Flow