开发过程报告 · 外滩大会 2026 AI Coding 黑客松

第二时钟:一个不肯猜的药箱

药盒上印的有效期,前提是一直没拆封。开封之后重新起算的那个期限,没有任何地方显示它。 这份报告记的不是功能怎么做出来的,而是它在什么地方差点给出错的答案,以及那些答案是怎么被拦下来的。

作品 2C.KLINIK.REN 出品人 INOICHI 构建于 CLAUDE CODE 审计 CODEX CLI

01交付状态

1832药品与家庭医疗用品条目↑ 起点 703
1744条附带储存安全提示
1684商品名与俗名别名
46/46生产环境测试通过
3轮独立审计
0前端持有的密钥

单文件网页,药库内联,断网可查。拍照识别走阿里云百炼 qwen-vl-plus, 交叉核对走智谱 glm-4.6v,联网兜底走智谱 web_search; 三条链路都经服务端反代,浏览器不持有任何密钥。 部署在已经跑着另一个生产站的同一台 nginx 上,域名分流,互不影响。

02产品的核心主张:三道防猜错

这个作品真正花力气的地方不是查得快,是拿不准的时候不猜。 原因是一条不对称性:药名认错,用户一眼看得出来,药盒就在他手上; 日期认错,他看不出来——而日期恰恰是他找工具替他记的那个东西。 所以自动化的边界不能一刀切,得按「错了以后还能不能被发现」来分层。

防线拦的是什么拿不准时的行为
两家 AI 独立交叉验证 单个视觉模型的幻觉 药名归一化后不完全相同即判分歧,一个字都不代填
证据必须可被代码核对 模型编造的日期 包装语种与证据文字矛盾、或声称的年份不在证据原文里,丢弃该日期
同名多剂型请用户选 查表先到先得给出的错答案 不预填,就地列出剂型按钮,交回给能看见药盒的人
界面上的措辞停在「结果一致」而不是「已验证」。一致只降低了单个模型独有错误漏过的概率, 两家仍可能被同一处模糊的包装误导而一起读错——这句差别写进了产品文案,因为它是真的。

03缺陷档案

下面七条按「会不会静默给出错答案」排序。红色的三条共同点是: 用户不会收到任何提示,而拿到的数字是别的药的。

静默错答

一个商品名可以是好几种药

现象
建索引时写的是「这个键还没有就存进去」——先到先得。而「希刻劳」既是头孢克洛干混悬剂(冲配后冷藏、14 天内用完),也是头孢克洛胶囊(按印刷效期、常温)。用户拿到的是数组里排在前面的那条。
规模
全库 27 个商品名。「泰诺林」「可乐必妥」「兰美抒」「胃复安」都在里面。
修法
索引值改成收全部命中条目;命中多条时先比对它们的预填答案,一致才算等价,不一致就不预填,并就地升起一排剂型按钮。「剂型」平时是选填字段,只在这一刻变必填。
之所以做成让用户点剂型、而不是原先那句「请补全完整通用名」:剂型就印在药盒上,照抄一眼的事; 而完整通用名他要是知道,一开始就不会去打商品名了。
静默错答

「诺和盈」和「诺和泰」不是同一个产品

现象
降糖版司美格鲁肽的别名里挂着减重版的商品名。两者开封后可用天数分别是 42 天28 天
同类
共九组。30R 胰岛素挂着「诺和灵50R」、口服补液盐 III 挂着 ORS I 与 II(三个不同配方)。另有三个过泛别名更麻烦:活菌制剂胰岛素瓶装滴剂型活菌——这种词会把用户输入的任意同类药抓到一张错卡上。
连带
修复时新建的「口服补液盐散(I/II)」又踩一次坑:搜索前的归一化会去掉括号与斜杠,(I/II) 归一化之后正好等于 III,两条撞成同一个键。改名成「(I型或II型)」才躲开。

归一化是为了容错,但它同时也在制造碰撞,而碰撞落在哪两条身上,不跑一遍全库是想不出来的。

我自己造成的

用正则批量改医疗数据,19 条里错了 12 条

动机
一批「未开封需冷藏」的药,说明里其实写着开封后可以转室温,而界面只显示一个储存状态。于是写了条规则:凡 storage 是冷藏、且说明里出现「室温」或「常温」的,统一追加一句「开封启用后可转室温存放」。
结果
审计报回 7 条是反的,我自己核完是 12 条。阿莫西林干混悬剂的说明是「干粉常温保存;配制后冷藏」——正则匹配到「常温」二字,于是追加了一句意思正好相反的话,等于告诉用户冲好的抗生素混悬液可以放室温。另有五条是「未开封时允许短期室温外放」的宽限条款,被写成了「开封后可转室温」,主语也错了。
处置
全部撤回,逐条核实后只给确实成立的 7 条重新加;反向的那几条另写「未拆封的干粉按常温存放,加水冲配之后才需要冷藏——顺序别弄反」。
正则读得到字符,读不到那句话在讲哪个阶段。而这批数据里,「常温」出现在句子的前半段还是后半段, 决定的是完全相反的两件事。

这是整个项目里最危险的一个错误,而它恰好印证了作品自己的主张: 批量生成的东西必须过一道独立的核对,包括我自己批量生成的东西。

判断分歧

「开封」指的是拆盒还是撕袋

审计意见
单剂量小袋装的颗粒剂,说明写着「拆袋后当次冲服完」,而开封天数是空的(按印刷效期)。审计判为自相矛盾,建议改成 1 天。照做了。
为什么错
用户填的「开封日期」,填的是他拆盒那天。一盒二十袋,拆盒后剩下十九袋仍是独立密封的,好到印刷效期为止。标 1 天的话这张卡第二天就变红说过期,用户会扔掉一整盒好药。
结论
两个说法都对,只是主语不同:一个说的是盒,一个说的是袋。撤回改动,保留「按印刷效期」,把两件事在文案里拆开讲。顺着这条又发现剂型兜底规则里「颗粒剂」与「口服液」的默认值都写着 1 天——意味着任何一个没被收录的同类产品都会被填成「明天过期」,一并改掉。
审计指出的矛盾是真的,但它给的修法不一定对。「哪个数字对」的前提是「这个字段到底在描述谁」, 而后者不在代码里,得回到用户的动作上去问。
安全边界

模型不只会错,还会替自己背书

实测
对着一板没有外盒的胶囊,模型报出完全不相干的药名;对着一盒克罗地亚语包装,它把注册批准号旁边的日期当成有效期,还编出一段中文证据「有效期至」——那五个汉字不可能出现在那个盒面上。而这些情况下它都自称 confidence=high,3/3 稳定复现。
提示词加固
能减少,杜绝不了。写明反例之后仍会复发。
改法
不再跟模型讲道理,改成要它交出可被代码核对的东西:逐字证据原文 + 包装语种。代码检查证据语种是否与包装语种矛盾、它声称的年份是否真的出现在证据原文里;核不上就丢弃该日期。
守卫失效

闸都在,多点一下就绕过去了

之一
低置信度药名的拦截装在识别结果那一屏。用户点「修改信息」转到手动表单后,药名框失焦会再次无条件触发药库预填——换个入口就绕过去了。守卫下沉进预填函数本身才算数。
之二
置信度判断写的是 confidence === "low"。审计指出字段缺失或取值非法时会直接绕过——这是我当初怕误拦而刻意写松的,判断错了。改成归一化后再比。
之三
联网查询的结果在 await 返回后直接写回表单,而查询期间用户完全可以把药名改成别的。拍照那条路早有批次令牌,手动表单这条一直没有。
已处置

数据存在,不等于功能生效

现象
药库里有上千条储存与使用注意事项,但渲染条件写错,它们几乎从不显示。「84 消毒液不可与洁厕灵混用」这类提醒全都躺在数据里睡觉。
为什么它最坏
不是因为少了一个功能,是因为它让维护者以为这一面已经防住了
修法
单独渲染,并且只在药名精确命中时出现——模糊命中的药名本身带着不确定性,不该用确定的口吻转述一条安全提醒。

04把交叉验证用在开发过程上

产品里让两家模型互相核对,开发时也照同一条原则做:代码由 Claude Code 编写, 全程用 Codex CLI 作只读独立审计,只产审计文件、不改代码。写的和查的不能是同一个模型。 三轮审计各换一个视角——代码正确性、提交文案、以及最后一轮换成「评委兼第一次用的普通用户」去读界面上所有能看到的字。

审计确实拦下了自查发现不了的东西,上一节红色的那几条大半出自它。但同样重要的是 它说错的时候要顶回去——审计意见是断言,不是判决。这一轮我明确不采纳两条:

审计意见处置理由
交叉验证冲突时应禁用提交入口 不采纳 冲突时本来就一个字段都不预填,用户自己核对完药盒再提交是正常流程。禁掉等于告诉他「你确认过也不许记」。
手册章节顺序应调整(把上手放到功能前面) 不采纳 它把「编号对不上」诊断成了「顺序错了」。真正错的只是子标题的数字,改成 2.1–2.8 即可;顺序是作者定过的。
把「不预填天数、不带出储存条件」改写成更强的「不预填任何字段」 不采纳 只核实过前者。照改等于把一句已验证的话换成一句没验证的话——而那正是审计自己反对的做法。

05当仪器坏掉的时候

药库扩充后有 270 个新别名从未经过任何外部核实,只过了生成者的记忆。 派了七个子代理去联网核,结果全部在第一次查询就撞上配额耗尽。

七个全部停批,交了空结果,没有一个凭印象判定。 这是对的:它们「知道」福善美是阿仑膦酸钠,但那是记忆不是核实,写进库里等于伪造核实结果—— 而这批数据出问题的根源,恰恰就是「让模型凭印象判断」。

换了通道重跑:驱动浏览器搜索,把结果页正文取回来,在脚本里检查该通用名有没有出现。 判据仍然是机器可核的那一套,和日期证据同一个思路——没有任何一步交给模型下判断。 网页原文一个字都没进模型上下文,因此这一轮几乎不花 token。

145药品商品名送核
142查到证据,保留
2始终无证据,删除
125器械俗称,判定无需送核

那 125 个之所以不送核:它们是「额温枪」「验孕棒」「暖宝宝」这类口语俗称, 风险性质完全不同——不会把用户导向另一种药。 删掉的两个(科索普克龄蒙)很可能是对的,只是复核时搜索引擎已经限流。 按既定判据照删,因为代价不对称:删错了用户改打通用名,留错了用户拿到另一种药的储存条件。

06几条留得下来的

被普遍不执行的条款,危害不在它没被执行,在于它让人以为那一面有防护。 数据也一样:写了却不显示的安全提示,比没写更糟。

其余四条,按这一轮踩到的顺序:

守卫要装进被调用的那个函数里,不是装在调用处

装在调用处的闸,换个入口就绕过去了。这条在本项目里被验证了两次——一次是识别结果转手动表单,一次是我自己新加的剂型监听打开了一条旧路径。

fail-closed 的默认值要写死,不能靠「一般不会缺」

confidence === "low" 这种写法,在字段缺失或取值非法时会静默放行。当初为了避免误拦而写松,是把两种错误的代价估反了。

让用户回答他答得上来的那个问题

「请补全完整通用名」他答不上来,「照药盒上的剂型点一个」他照抄就行。同一个消歧需求,问法决定了它是一道墙还是一个台阶。

一致不等于正确

两个模型的作用主要是发现分歧。「两家独立编出同一个错答案的概率更低」这个推断需要错误相互独立,而它们并不独立——共同的误读会让两家一起错。这句话原本写进了手册,后来删掉了。

07边界

这个作品不做什么

不判断药品质量、疗效或是否变质,也不构成用药建议。它只按用户记录的日期提醒—— 日期没到,不代表那盒药一定还能用;受潮、变色、有异味都该停用,跟日历无关。

所有开封期限均为参考值,依据《中国药典》2020 年版制剂通则、卫生行业标准与药师科普通用口径整理; 无法核到产品说明书原文的条目一律标注「参考值」,并提示以手中说明书为准。

记录只保存在本机浏览器,无需注册登录。仅在用户主动点击时才会联网: 拍照识别会把照片分别发送给阿里云百炼与智谱 AI,联网查询会把产品名称发送给智谱, 提交反馈会把产品名称与补充说明发送至本站服务器。不点就不会。

已知仍然存在的不确定性

器械类的 125 个俗称未经联网核实,判定依据是「风险性质不同」而非「已核实」。 新增条目中相当一部分的开封期限取自剂型通用值而非说明书原文,均已标注「参考值」。 混合包装(同一品种既有铝塑板又有瓶装)的条目按更保守的瓶装口径给值,说明里写明了两种情况—— 这是一个字段表达两套规则时的折中,不是精确答案。