33 KiB
手工处方双模型分析:实施与部署
本地实现日期:2026-09-09。功能默认关闭;本次没有迁移线上数据库、启动线上消费者、调用真实模型或批量分析历史患者。
本地运行包更新(2026-09-10)
用户反馈本地启动后看不到功能。核验发现:项目“一键运行”优先使用 app/dist/DoctorWorkstation/DoctorWorkstation.exe,原 EXE 尚未包含 doctor_workstation.ui.dialogs.issued_prescription_ai;桌面快捷方式另指向 C:/Program Files/ZYT/DoctorWorkstation/DoctorWorkstation.exe,该安装版为 1.3.0,同样没有此模块。
已为处方列表增加独立可见的 AI 状态说明,区分服务未启用、暂不可用、缺少查看权限和空列表;仍按真实权限显示报告入口,未启用/不可用时不展示虚构分数。服务恢复后清除错误说明。相关 65 项桌面回归、Ruff 和 diff 检查通过。
已将项目“一键运行”使用的目录更新为包含本功能的 1.4.2 运行包,旧目录备份为 app/dist/DoctorWorkstation-before-prescription-ai-20260910-091537。新 EXE SHA-256:d97625e84b3e418aef471e24cfd8830cd07dda85a7762757a5b323d84ce10300。归档模块、Qt WebEngine/Multimedia、字体、OpenSSL 同源与两项隔离启动检查通过,一键运行入口验证通过。完整证据见 app/artifacts/issued-prescription-ai/local-package-build-20260910.md 和 local-package-promotion-20260910.json。
桌面安装版没有更新。使用项目 app/一键运行_医生工作站.bat 可打开新包;现有客户端偏好连接线上后端 https://admin.zhenyangtang.com.cn/adminapi,本地程序更新不会自动部署线上代码、执行数据库迁移或启用分析任务。本轮未执行这些线上变更。
Linux 宝塔进程预部署记录(2026-09-09)
已通过 SSH 在用户指定的 Linux 宝塔服务器完成进程配置。服务器尚未同步本功能代码,因此仅启动 Supervisor 管理服务,三个消费者均为 STOPPED / Not started,配置均为 autostart=false。未同步业务代码、修改应用 .env、迁移数据库或执行分析任务。
- 实际项目目录:
/www/wwwroot/zyt/server;站点运行目录为其public子目录。 - PHP CLI:
/www/server/php/82/bin/php,现场版本 8.2.28;运行用户www。 - 进程名称:
prescription-ai-prepare、prescription-ai-qwen、prescription-ai-openai;各一个实例,命令使用 PHP 和think的绝对路径。 - 已补齐宝塔官方 Supervisor 管理界面,复用服务器已安装的 Supervisor 4.2.1,未升级宝塔 Python 依赖。
- 主配置:
/etc/supervisor/supervisord.conf;进程配置:/www/server/panel/plugin/supervisor/profile/prescription-ai-*.ini。 - 日志:
/www/server/panel/plugin/supervisor/log/,每个进程分别记录 stdout/stderr,每文件 10 MB、保留 5 个轮转备份。 - 服务:
/etc/systemd/system/supervisord.service,管理服务已设开机启动;三个消费者仍不会自动启动。 - 已配置异常退出重启、TERM 停止及进程组清理。prepare 停止等待为 600 秒,模型消费者为 14,400 秒;当前 worker 只在任务外层响应退出,单任务包含多次模型调用,调整上游超时/调用预算后应重新核对等待时间。
现场验证通过:Supervisor 配置解析、systemd 单元校验、PHP CLI 必需扩展、www 用户的信号函数与 runtime 写权限;宝塔 GetPorcessList 返回三个 STOPPED 进程,系统进程表中无处方 AI 消费者。配置校验没有执行业务命令,不能代替上线后模型联调。
代码同步后,先按下方部署顺序完成数据库、密钥、启用时间、权限和模型配置检查,再启动消费者。确认上线后将三个配置的 autostart 改为 true 并重新加载 Supervisor,才能在服务器重启后自动恢复消费者。保留现有快照密钥;如本地已对同一数据库登记任务,必须使用登记时的相同密钥。
已接入的业务流程
- 手工新增处方、空白处方第一次编辑为手工、临床内容修改:与处方保存处于同一数据库事务的分析事件。
- 新客户端对同一新增请求复用
request_key;服务端验证请求内容,网络响应丢失后返回原处方 ID。旧客户端按处方版本去重。 - 基于诊单的患者 ID 绑定,空白处方编辑时补齐患者 ID;绑定不足显示明确状态。
- 独立的资料准备、千问、OpenAI 三个消费者;一次冻结资料,两模型独立执行与失败重试。
- 分批读取授权资料和附件,保存模型读取覆盖与缺口;等待本次视频转写最多 5 分钟,晚到资料通过扫描安排新版本。
- 处方临床版本变化、作废、删除后取消旧任务。结果不可覆盖,旧报告保留过期标识。
- 桌面已开处方列表显示模型状态及“药味与剂量一致度”;点击/右键可看报告、候选方案、逐味差异、资料缺口和历史。患者详情也可进入。
- 提供单模型失败重试、重新分析和人工复核意见;AI 结果不会自动写回正式处方、审核、订单或药房。
- 快照、模型中间进度、报告正文和复核意见加密存储。服务端返回报告前重新验证处方和每个来源的授权。
百分比的含义
对两张单位、剂量基准、剂型等可比较的处方,服务端依据本地药材身份映射计算:
一致度 = 100 × 2 × Σ(共同药味 min(医生剂量,AI剂量)/max(医生剂量,AI剂量)) ÷ (医生药味数+AI药味数)。
药味别名、炮制和主辅方需要明确;单位/每剂或每日基准不明、剂型不可比较、关键资料不足、候选方案未生成时显示“—”及原因,不记为 0%。0% 仅表示可比较但无相同项。煎服与疗程差异另外展示。
此指标反映处方一致程度,不能称为医生准确率。医生统计只接受有证据的独立基线;普通“已查看/未采纳/已复核”不是独立专家判定,不能计为专家合格率。
部署顺序
- 备份数据库,使用现有迁移方式执行
server/database/migrations/2026_09_09_prescription_ai_analysis.sql。默认表前缀是zyt_,其他前缀需一致替换。脚本可重复执行,新增表和权限不修改现有报告表。 - 核实角色权限:
tcm.prescriptionAi/statuses、reports、detail、regenerate、retry、review、statistics。迁移继承现有患者 AI 报告的读取/生成权限;既有角色未获得相应权限时,应由管理员按实际职责配置。 - 各 Web 与消费者节点配置同一个至少 32 字符的随机
PRESCRIPTION_ANALYSIS.ENCRYPTION_KEY。不配置时单节点会在非公开server/runtime/prescription_ai_private/snapshot.key建立密钥。多节点必须显式配置并备份相同密钥;丢失密钥将无法解密历史报告,不得把密钥提交代码库。 - 将
PRESCRIPTION_ANALYSIS.START_AT设为实际启用时刻的 Unix 秒时间戳,用于有界补偿扫描。保留既有prescription_ai双模型供应商配置,核验两个端点的实际图片、文件和 JSON 输出能力。 - 先在测试环境配置
PRESCRIPTION_ANALYSIS.ENABLED=true,分别运行一次三个命令,确认任务与报告正常。上线启用需完成同样的真实供应商验证。
ThinkPHP .env 示例(随机密钥自行配置,不使用示例文字作密钥):
[PRESCRIPTION_ANALYSIS]
ENABLED = false
START_AT = 0
在 server 目录运行三个独立进程,并交给现有进程守护器管理:
php think prescription-ai:work --lane=prepare
php think prescription-ai:work --lane=qwen
php think prescription-ai:work --lane=openai
--once 只处理一轮,适合诊断或已有定时执行方式。只启动 prepare 不会调用模型。默认每模型并发 1,每次任务最多自动尝试 3 次、手动重新尝试 2 轮,每模型每日最多领取 200 次任务。一次模型任务可能包含多次分片调用;实际调用次数及供应商可用的 token 使用量保存在加密进度和结果中,不能把任务数当成 token 数或费用金额。
prescription_ai.manual_analysis 控制输入预算和每模型任务最大调用次数;prescription_analysis 控制等待、租约、并发、补偿范围与任务预算。长请求超时应小于任务租约。调整后需重启常驻消费者。
历史补生成与回退
历史记录不会一次全量运行。先明确日期范围和条数进行预览:
php think prescription-ai:backfill --from=2026-09-01 --to=2026-09-09 --limit=50
确认预览范围后同命令追加 --apply 才登记任务;用返回的 last_id 继续传入 --after-id 翻页。日期筛选使用处方 update_time,不是处方笺日期。命令本身不调用模型,消费者随后执行。既有同版本任务不会重复创建;已失败的单模型从报告窗口重试。
回退时先停止三个消费者,再关闭功能开关并重启 Web 服务;保留新增表和密钥,旧处方流程继续工作。再次启用后从既定起始时间补偿缺失事件。不要删除历史报告表或密钥来暂停任务。
当前数据与供应商能力限制
- 旧来源缺少可重建历史版本,当前批次不能获得独立基线资格;列表仍可展示具备比较条件的辅助一致度,医生统计有效样本可能为 0。后补资料不会被冒充为医生开方时已经掌握的证据。
- 远程附件 URL 尚不能证明字节不可变;系统会分批提交并保留该缺口,不标记完整覆盖。默认仅使用已配置存储域名内授权上传路径。
- 当前 OpenAI-compatible 路径支持图片;PDF/不支持的文件保留能力缺口,由模型基于实际已读证据评估候选方案。一般附件缺口本身不再一律禁止候选方;关键用药安全信息仍缺失时继续暂缓。Dify 严格文件路径不允许偷偷丢弃附件并声称完整成功。通用本地 PDF 页面渲染/OCR 和附件内容寻址存储尚未接入,不能把链接清单当成全文已读。
- 聊天归档缺少完整同步水位时显示缺口;未能证明患者关联和授权的记录不会凭患者 ID 扩大读取。
- 自 2026-09-10 起默认研究对照模式:缺少用药、过敏等关键事实不再抑制候选方案,改为在假设下开方并逐项标注缺口与复核点。空白仍不等于“无”;男性/女性实际编码以及明确否认值会保留。
- 源资料自动更新扫描默认覆盖最近 7 天的分析批次。更久的处方可手动重新分析;历史全量更新需明确范围与预算。
- 暂无已验证的独立专家评审数据,不显示虚构的“医生准确率”。AI 百分比排序/全库筛选与专家抽样评审工作流不在当前接口内。
以上为实际支持边界,不代表已完成线上模型联调。
本地验证
新增 PHP 独立测试覆盖比较公式、统计排除与版本分层、资料隔离、文件分批、JSON 契约、恢复与加密;数据库测试只使用本地临时 MySQL 实例,创建随机前缀测试库,结束后删除该测试库,绝不加载应用生产数据库配置。
php server/tests/PrescriptionAiPolicyTest.php
php server/tests/PrescriptionAiComparisonTest.php
php server/tests/PrescriptionAiStatisticsTest.php
php server/tests/PrescriptionAiContextTest.php
php server/tests/PrescriptionAiGeneratorTest.php
将 ZYT_AI_TEST_MYSQL_PORT 指向独立的本地空密码测试 MySQL 后运行:
php server/tests/PrescriptionAiQueueTest.php
php server/tests/PrescriptionAiPipelineTest.php
桌面无网络测试使用项目 Python 环境,主要入口:app/tests/test_issued_prescription_ai.py;截图位于 app/artifacts/issued-prescription-ai/。
本轮最终验证:17 个后端测试脚本通过,包括 54 项数据库队列/真实保存检查、63 项带模型边界桩的完整链路检查、252 项比较器检查、64 项统计检查和 20 项策略/加密检查(检查数有重复覆盖,不相加冒充独立病例数)。桌面五份相关测试文件全部通过;PHP 语法检查及 git diff --check 通过。真实供应商输出质量、图片/PDF能力和线上进程配置仍需部署环境联调。
2026-09-10 本地运行排障
“等待转写/资料”有固定 300 秒截止时间。本次实际观察到等待截止后约 3 秒进入双模型分析,准备进程正常。部署时需区分桌面 Debug 配置、本地 API 域名映射与线上 API,不能仅因线上 Supervisor 未启动就认定本地任务没有消费者。
本次修复了真实供应商调用中暴露的四类问题:
- 接受完整单个 JSON 代码围栏,对合法来源引用列表去重;仍拒绝夹杂解释文字、遗漏或未知来源、错误字段和未通过临床校验的候选方案。
- 最终压缩按完整提示词计算预算,包括全部来源覆盖、附件状态和资料缺口;固定说明本身已超限时明确失败,不丢弃缺口或扩大调用预算。
- 严格附件传输保留全部逻辑附件;分析器将相同 URL 的不同来源分到不同请求,避免上游模型合并后遗漏来源。普通聊天保留原去重行为。
- 严格解析失败后持久化清除该步骤的无效返回缓存,保留其他成功步骤、累计调用记录及原重试上限,避免重试反复复用错误结果。调用记录新增经过格式限制的错误码,不记录新明文正文或凭据。
离线回归覆盖 Generator、UpstreamContract、StreamContract 和 Policy;本地模型进程在确认空闲后重启,失败任务通过原权限与预算检查恢复。OpenAI 已实际生成一份报告,但该应用的附件请求返回 HTTP 400 / invalid_param,故保留附件缺口,不能声称图片已被该模型读取。
随后通过只读 /parameters 核实:千问应用的 allowed_file_types 包含 image/document/audio/video;OpenAI 应用只有 custom,虽列出图片扩展名,但没有 image 类型。这是需要在 Dify 应用侧核对的实际配置差异;当前请求拒绝不能靠把图片标记为已处理解决。Dify 的官方文件校验实现也分别检查文件类型、扩展名与传输方式,部署版本具体行为仍需在该环境确认。
本轮最终状态为 OpenAI 报告成功、千问附件结构校验失败。千问两次手动重试额度已耗尽,未重置额度或创建新批次绕过限制;错误缓存清理已生效。两条模型消费者均已加载最新修复并继续运行,prepare 保留原正常进程。继续排障需要 Dify 管理后台的附件及输出配置;本次未修改远程业务代码或线上开关。
2026-09-10 中文展示与资料不全时的候选方案
按用户追加要求,界面的状态、资料缺口、来源编号和统计排除原因提供中文说明,兼容旧报告中嵌入的技术代码。原始存储数据、来源绑定和统计含义不因显示翻译而改变;医学缩写与模型品牌保留原文。
候选方案采用 manual-prescription-available-evidence-v2 提示词策略:图片未读、部分转写未完成、历史版本和归档同步无法验证等覆盖限制,不再单独阻止模型根据已有临床证据提出候选方。缺口仍逐项展示,候选方案注明资料不完整和医生复核要求;药味、剂量、单位等具备可比条件时,正常进行药味与剂量一致度比较。
若附件确已送达但一组返回结构不合格,清除该组无效缓存,丢弃全部识别内容,并逐项标记“模型未能正确解析这组附件”,继续其他资料及报告。该组附件不能被最终报告引用为已读证据。附件送达数量不符、文字证据结构错误或最终报告校验失败仍严格报错;重试保留已耗调用量。
年龄、性别、过敏史、当前用药或适用人群妊娠哺乳等关键安全事实缺失,未知的关键风险条件,或模型判断现有证据不足以支持具体用药时,仍暂缓具体候选用药,不能为了分数编造药味剂量。这不是无条件自动开具正式处方。医生统计保持首次独立基线口径,辅助对照结果不冒充医生准确率。
旧报告保持原分析结论;重新分析产生新批次并使用新策略。恢复遇到旧提示词版本时不复用旧输出,但保留累计调用预算,不因升级重置成本限制。此次变更不执行历史全量重跑。
本次验证:72 项桌面相关测试、Generator 回归、252 项比较器检查、PHP 语法与差异格式检查通过。Windows 1.4.2 中文包通过冻结入口、媒体组件和中文标签验证,已切换至 app/dist/DoctorWorkstation,旧包保留在 app/dist/DoctorWorkstation-before-chinese-20260910-100701;切换记录位于 app/artifacts/issued-prescription-ai/local-package-promotion-chinese-20260910.json。本地两条模型消费者在空闲时重启并加载新策略,prepare 保持正常运行。未关闭当前 Debug 客户端;重启当前本地客户端后使用新版显示,旧分析需要通过“重新分析”生成新批次。本轮没有生成新的真实患者报告。
2026-09-10 处理进度
追加 2026_09_10_prescription_ai_progress.sql 迁移,给任务表增加小型公开进度摘要;已有密文缓存与历史报告不变。列表和双模型报告窗口显示当前阶段、本阶段组数、等待说明、尝试次数及耗时,完成状态以结果保存成功为准。旧库尚未迁移时继续运行原流程并返回缺少分段进度的提示;迁移后需在空闲时重启三个消费者。界面每 5 秒读取最新状态,等待重试时冻结上次执行耗时。
本地数据库迁移及消费者更新已完成;测试和限制详见 进度验证记录。线上部署须同步本次代码、执行追加迁移并重启对应消费者,本轮未改动远程服务器。
2026-09-10 研究对照:必须开出候选方
按用户确认的医学研究用途,默认要求两个模型在看不到本次人工方的前提下各自开出候选处方,再由服务端比较。
- 新配置
prescription_ai.manual_analysis.require_candidate(默认true)与candidate_insist_rounds(默认 2)。设为false可恢复原“关键安全信息缺失即暂缓候选方”的策略。 - 环境变量:
prescription_ai.MANUAL_REQUIRE_CANDIDATE、prescription_ai.MANUAL_CANDIDATE_INSIST_ROUNDS。 - 提示词版本升至
manual-prescription-required-candidate-v3。恢复中的旧任务不复用旧提示词的输出,但保留已耗调用预算;已保存的历史报告不变,需要新策略时用“重新分析”生成新批次。 - 模型连续拒绝开方时任务以
CANDIDATE_WITHHELD_BY_MODEL失败并可重试,拒绝的回答不进缓存,界面不显示为完成。追问会额外消耗调用预算,长期拒绝的模型请核对上游应用的内容策略配置。 - 未变:候选方不写回正式处方、审核、订单或药房;一致度仍由服务端固定算法计算;医生统计的独立基线口径不变。
2026-09-10 排障:准备资料过久与模型直接报错
现场记录(处方 7556 / 诊单 1391)显示三个独立问题,均已在本地修复。
1. 每个批次固定卡 300 秒。 该诊单有 25 条通话记录,全部是已结束但从未转写的旧通话(transcription_status 为空、无会话号、无分段)。原判断把"空转写状态"一律视为"仍可能到达",于是每次都等满整个转写窗口;实际资料组装只需 3~4 秒(实测批次 1~4 的准备耗时均为 303~304 秒)。现在只有三种情况才等待:通话仍在进行、转写任务为 pending/running、或通话刚结束且在等待窗口之内。其余旧通话照常作为 TRANSCRIPT_NOT_VERIFIED_COMPLETE 缺口展示,不再阻塞。
2. OpenAI 每次都在 90 秒超时。 后台分阶段分析原本复用同步页面的 prescription_ai.timeout=90,而最终汇总一次调用就超过该时长,三次尝试全部 UPSTREAM_TIMEOUT。新增 prescription_ai.manual_analysis.request_timeout(默认 240 秒,上限 300,环境变量 prescription_ai.MANUAL_REQUEST_TIMEOUT),只作用于后台分析;同步页面仍用原超时。该值必须明显小于任务租约 lease_seconds(默认 600 秒),任务每一步都会续租。
3. 千问每次因格式校验失败整支报废。 结构不符原本直接结束整个模型分支,一次格式抖动就要走完整轮重试。现按方案第 9.3 节实现"最多一次受控格式修复":任一阶段(文字、附件、压缩、最终报告、追问)解析失败时,立即在同一步骤内追问一次,只重申结构要求,不放宽校验、不接受夹带解释文字、不改动来源编号。修复失败才报错,且 INVALID_EVIDENCE_OUTPUT、INVALID_REPORT_OUTPUT 改为可重试。两次无效回答都不进缓存,调用次数照常计入预算,进度中记录 format_rejects 阶段标记(不含正文)。
其他:界面上"资料截止"等未设置的时间戳(服务端返回 0)显示为"—",不再显示 0 或 1970 年。
本地验证:13 个后端离线测试通过(含新增的转写等待、格式修复、拒绝不入缓存与调用计数用例),桌面测试通过。三个本地消费者已在空闲时重启加载本次修复。线上未做任何改动。
2026-09-10 让一致度真正能算出来
排障中用真实数据复算发现:即使模型正常出方,百分比仍然恒为"—"。原因有三,都在本地修好了。
- 医生方没有逐味单位。 工作站把用量单位存在处方级
dosage_unit(用量单位,实测 9389 张为g),剂量基准由dose_unit=剂表达,逐味行只有药名、剂量、主辅方和药材 ID。比较器原来只在剂型为"饮片"时才补g/每剂,而本机构 9507 张处方是"浓缩水丸",于是每一味都判为"剂量单位缺失"。现在改为读取医生自己填写的dosage_unit,并在dose_unit为剂/付时确定每剂基准;两者都没有时仍然不可比,不猜单位。所有套用都逐行记录在defaults中。 - 两边剂型和基准对不上。 快照中新增"调配约定"(剂型、每味用量单位、每剂/每日基准),来自处方的调配字段,不含任何药味或剂量,交给两个模型,要求候选方按同一剂型、同一单位、按饮片原药材克数/每剂表达。比较器另外只做剂型写法归一(中药配方颗粒→颗粒等),不做任何剂量或提取比换算。
- 模型写的药名不在机构药材字典里。 实测千问给出的 9 味中有 5 味无法唯一映射:机构字典里是"生麦冬""生五味子""麸炒白术""生地骨皮""麸炒苍术",模型写的是"麦冬""五味子""炒白术""地骨皮""苍术"。按方案要求不能把生品与炮制品自动判为同一味,因此改为把机构药材名清单(仅药名,无库存、价格或患者信息)随候选阶段一起下发,要求逐字选用清单内名称;清单超过提示词预算四分之一时整体不下发,不做截断。
提示词版本升至 manual-prescription-required-candidate-v4。旧报告保持原结论,需要新口径请用"重新分析"。本轮新增 8 项比较器检查和 2 项候选阶段检查,后端 11 个离线测试与桌面相关测试全部通过。
2026-09-10 消费者被数据库断线卡死
把后台单次请求超时提到 240 秒后,OpenAI 那条消费者从领到任务起就再也没有恢复:日志连续输出 storage_or_configuration_error,任务只能等租约到期被判 LEASE_EXPIRED。
原因:本机 MySQL 的 wait_timeout 与 interactive_timeout 都是 120 秒,而一次模型调用期间数据库连接是空闲的。超时 90 秒时连接刚好活得下来,提到 240 秒后连接被服务端关闭,之后每一次查询都抛 PDOException,连 fail() 都写不进去,消费者就永久空转。
修复(只作用于消费者进程,不改 Web 配置):
- 命令启动时对默认数据库连接打开
break_reconnect,断线自动重连。 - 每轮异常后主动关闭可能已失效的连接,下一轮重新建立。
- 失败输出改为"异常类名@文件:行号 + SQLSTATE",不打印异常消息本身(消息可能带 SQL 值或病历文本)。原来那句
storage_or_configuration_error查不出任何信息。
部署注意:数据库的 wait_timeout 应大于 prescription_ai.manual_analysis.request_timeout,两者与任务租约 lease_seconds 的关系是 请求超时 < wait_timeout 且 请求超时 << 租约。新增 server/tests/PrescriptionAiWorkerResilienceTest.php 固定这些约束。
2026-09-10 千问格式失败:不是输出被截断
先前根据"每次 completion 都在 4600~5700 tokens"怀疑上游最大输出长度截断。用合成数据实测否定了这一点:同一个千问应用被要求输出 200 条数组时返回了 13204 tokens 的完整合法 JSON(probe_output_limit.php)。再用合成病例跑完整候选阶段(probe_final_schema.php),千问 9.3 秒返回合法 v4 报告与 6 味候选方,零次格式修复。
因此失败来自真实病例的内容,而不是长度上限。最可能的是来源编号引用:真实病例有几十个来源编号和附件编号,模型引用了未读到或不存在的编号,整份报告即被拒。为此在最终提示词中显式给出 ALLOWED_EVIDENCE_IDS(本分支已读来源 + 实际解析成功的附件),并要求引用只能逐字取自该清单——不放宽校验,只消除歧义。未读成功的附件编号不会出现在清单里。
同时校验失败现在记录具体规则(json_syntax、top_level、report_lists、candidate_herbs 等)与返回内容长度,修复追问会附上该规则对应的中文提示。下一次真实失败即可直接读出原因,不再靠猜。
两个探针脚本只使用合成数据,不读取任何患者记录。
2026-09-10 首批真实对照结果与两处计分缺陷
数据库断线修复后,OpenAI 连续三次成功(批次 6、7、8),千问在批次 6 成功,四份结果全部 comparable、零不可比问题。也就是说"AI 独立开方 → 程序比较 → 列表出百分比"这条链路在真实数据上跑通了。
用真实结果逐味核对,发现两处会压低分数的缺陷:
- 炮制标签把同一味药拆成两项。 机构药材字典把炮制写在药名里(醋五味子、麸炒白术、生麦冬),字典本身没有 processing 字段。模型按接口要求填了
processing:"醋制",比较器就把[19,"","主方"]和[19,"醋制","主方"]当成两味不同的药。现在规则收窄为:只有当处方写的炮制标签的每个字都已出现在药名中时,才视为对药名的重复描述,不参与身份键(仍作为"炮制标注差异"展示);药名没有承载的炮制(如"黄芪"+"蜜炙")依旧是不同药项。字典自己声明了 processing 时行为不变。比较算法版本升至prescription-soft-dice-v1.1.0。 - 主辅方语义不一致。 本系统的"辅方"指与主方分开调配的另一张处方,医生用它装另包的煅龙骨、煅牡蛎等;模型把它理解成君臣佐使里的佐使药,于是茯苓、干石斛被标成"辅方",和医生主方里的同一味药匹配不上。已在提示词中明确定义,并要求除非确有单独的辅助处方,所有药味一律填"主方"。
修正身份键后,批次 6 千问由 14.35% 升到 21.76%(共同药味 2→3)。主辅方定义要新批次才生效,历史结果保持原值不改写。
真实分数目前普遍偏低(0%~22%):医生方 16~19 味,模型方 9~11 味,共同 0~3 味。这既有临床思路差异,也因为模型现在只能读到部分证据(附件仍读不到、聊天与转写有缺口)。这个数字是本指标的如实测量,不代表医生或模型谁对谁错。
2026-09-10 千问失败的真实原因与药名不在字典的处理
用只读探针把处方 9575 的冻结快照按当前代码重跑(不写任何数据),拿到了此前只能猜的答案:
final阶段被拒的规则是candidate_fields——候选方对象的键或类型不合规(多出字段、times_per_day/usage_days写成字符串、evidence_references为空等),不是被截断,也不是来源编号问题。一次受控格式修复即通过。files阶段被拒的规则是files_shape——返回的附件条数或编号与清单不一致,同样一次修复即通过。- 修复通过后仍然
not_comparable,原因是unknown_herb_name:模型 12 味里有 1 味不在机构药材字典中(写"生山楂",字典没有;另一例写"泽泻",字典里是"生泽泻/麸泽泻")。按方案不能自动替换或剔除,一味不认识整张方就不可比。
据此新增两项:
- 药名回问。 候选方生成后,服务端逐味核对机构药材名清单;有不在清单中的药名,就把这些药名回给模型,要求改用清单内逐字一致的名称,或在临床上确无合适药材时删除该味并说明,其余药味与剂量保持原判断。默认最多回问 2 次,服务端绝不代替模型换药。仍未纠正时,把这些药名写进候选方的风险提示,交给医生核对,不隐藏也不硬算分数。
- 提示词按实际失败点收紧。 候选方键集必须完全一致、数值字段必须是 JSON 数字、引用不得为空数组;附件阶段数组长度与编号必须和清单完全一致。
处方 9575 的完整重跑(生成 + 比较,只读)结果:comparable,9 次调用,其中 1 次附件格式修复、1 次候选字段修复、2 次药名回问,最终药味与剂量一致度 5.13%、药味重合 7.69%,零不可比问题。
2026-09-10 附件能力已放开与吞吐调整
只读核对两个应用的 /parameters(不提交任何患者数据):
| 应用 | allowed_file_types | number_limits |
|---|---|---|
| 千问 | image / document / audio / video | 3 |
| OpenAI | document / image / audio / video | 10 |
也就是说 OpenAI 应用先前"只有 custom、不含 image"的限制已经不在了,两个应用现在都接受图片。据此调整:
- 每个模型按自己的上限分批附件。 新增
prescription_ai.models.<模型>.max_files(千问 3、OpenAI 10,环境变量prescription_ai.QWEN_MAX_FILES、prescription_ai.OPENAI_MAX_FILES),全局max_files作为兜底。22 个附件在 OpenAI 侧由 8 次请求降到 3 次,直接缩短一份报告的时间。上限必须与应用配置一致:超限时 Dify 会用 400 invalid_param 拒绝整单。 - 每个模型允许 2 个任务并行(
prescription_analysis.max_parallel_per_model1 → 2)。该值只有在同一通道真的运行了这么多消费者进程时才生效,因此本地按prepare×1、qwen×2、openai×2启动。此前列表里出现"已用时 1 小时"多半是排队等待,不是单份报告真的跑了一小时。
线上如需同样吞吐,需在进程守护器中为每个模型通道配置两个实例,并确认上游应用与账号的并发额度。
2026-09-10 报告窗口重新设计
原窗口把免责声明、批次信息、四段式流程、批次进度、两套模型面板(各自四个页签、复核下拉、意见框、两个按钮)纵向堆在一起,字号权重相同,医生打开后没有落点。重排为六层,信息一条没删,只是各归其位:
- 标题卡:处方/诊单/版本一行加粗,右侧是版本有效性、对照类型的状态药丸,再右是历史批次选择与刷新/重新分析。第二行小字放状态、失败原因、资料截止与批次建立时间。
- 状态行:读取提示与错误信息。
- 流程条:四阶段流程只在批次仍在处理或属于历史批次时显示,完成后不再占位。批次级进度行在两个模型各自报告阶段时隐藏,避免重复。
- 两张结果卡(视觉重点):模型名 + 状态药丸 + 大号一致度数字 + "药味与剂量一致度"说明,下面是覆盖情况与原因、当前阶段、进度条与耗时。数字用中性墨色,不用红绿——高低不是评分。没有可比结果时显示灰色"—"。
- 共用页签:综合分析 / 候选用药 / 逐味对照 / 来源与缺口 由八个页签合并为四个,页内左右分栏同时显示两个模型的同一节,真正可以横向对照。
- 复核条:两个模型的复核状态、意见、保存与重试压缩到一行,原来纵向占用的约 200px 让给报告正文。免责声明降为脚注。
改动只在展示层:接口、轮询、权限、进度语义和落库数据都没变;model_views 的既有键全部保留,新增 score、status_chip、card、model_label。97 项桌面回归与 Ruff 通过,示意图见 app/artifacts/issued-prescription-ai/redesign-running-20260910.png 与 redesign-finished-20260910.png(合成数据,无网络)。