Files
zyt/docs/plans/prescription-ai-deployment.md
2026-09-10 15:19:17 +08:00

281 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 手工处方双模型分析:实施与部署
本地实现日期: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% 仅表示可比较但无相同项。煎服与疗程差异另外展示。
此指标反映处方一致程度,不能称为医生准确率。医生统计只接受有证据的独立基线;普通“已查看/未采纳/已复核”不是独立专家判定,不能计为专家合格率。
## 部署顺序
1. 备份数据库,使用现有迁移方式执行 `server/database/migrations/2026_09_09_prescription_ai_analysis.sql`。默认表前缀是 `zyt_`,其他前缀需一致替换。脚本可重复执行,新增表和权限不修改现有报告表。
2. 核实角色权限:`tcm.prescriptionAi/statuses``reports``detail``regenerate``retry``review``statistics`。迁移继承现有患者 AI 报告的读取/生成权限;既有角色未获得相应权限时,应由管理员按实际职责配置。
3. 各 Web 与消费者节点配置同一个至少 32 字符的随机 `PRESCRIPTION_ANALYSIS.ENCRYPTION_KEY`。不配置时单节点会在非公开 `server/runtime/prescription_ai_private/snapshot.key` 建立密钥。多节点必须显式配置并备份相同密钥;丢失密钥将无法解密历史报告,不得把密钥提交代码库。
4.`PRESCRIPTION_ANALYSIS.START_AT` 设为实际启用时刻的 Unix 秒时间戳,用于有界补偿扫描。保留既有 `prescription_ai` 双模型供应商配置,核验两个端点的实际图片、文件和 JSON 输出能力。
5. 先在测试环境配置 `PRESCRIPTION_ANALYSIS.ENABLED=true`,分别运行一次三个命令,确认任务与报告正常。上线启用需完成同样的真实供应商验证。
ThinkPHP `.env` 示例(随机密钥自行配置,不使用示例文字作密钥):
```ini
[PRESCRIPTION_ANALYSIS]
ENABLED = false
START_AT = 0
```
`server` 目录运行三个独立进程,并交给现有进程守护器管理:
```sh
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` 控制等待、租约、并发、补偿范围与任务预算。长请求超时应小于任务租约。调整后需重启常驻消费者。
## 历史补生成与回退
历史记录不会一次全量运行。先明确日期范围和条数进行预览:
```sh
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 实例,创建随机前缀测试库,结束后删除该测试库,绝不加载应用生产数据库配置。
```sh
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 后运行:
```sh
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 的[官方文件校验实现](https://github.com/langgenius/dify/blob/main/api/factories/file_factory/validation.py)也分别检查文件类型、扩展名与传输方式,部署版本具体行为仍需在该环境确认。
本轮最终状态为 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 秒读取最新状态,等待重试时冻结上次执行耗时。
本地数据库迁移及消费者更新已完成;测试和限制详见 [进度验证记录](prescription-ai-progress-2026-09-10.md)。线上部署须同步本次代码、执行追加迁移并重启对应消费者,本轮未改动远程服务器。
## 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 都在 46005700 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 味不在机构药材字典中(写"生山楂",字典没有;另一例写"泽泻",字典里是"生泽泻/麸泽泻")。按方案不能自动替换或剔除,一味不认识整张方就不可比。
据此新增两项:
1. **药名回问。** 候选方生成后,服务端逐味核对机构药材名清单;有不在清单中的药名,就把这些药名回给模型,要求改用清单内逐字一致的名称,或在临床上确无合适药材时删除该味并说明,其余药味与剂量保持原判断。默认最多回问 2 次,服务端绝不代替模型换药。仍未纠正时,把这些药名写进候选方的风险提示,交给医生核对,不隐藏也不硬算分数。
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_model` 1 → 2)。该值只有在同一通道真的运行了这么多消费者进程时才生效,因此本地按 `prepare×1、qwen×2、openai×2` 启动。此前列表里出现"已用时 1 小时"多半是排队等待,不是单份报告真的跑了一小时。
线上如需同样吞吐,需在进程守护器中为每个模型通道配置两个实例,并确认上游应用与账号的并发额度。
## 2026-09-10 报告窗口重新设计
原窗口把免责声明、批次信息、四段式流程、批次进度、两套模型面板(各自四个页签、复核下拉、意见框、两个按钮)纵向堆在一起,字号权重相同,医生打开后没有落点。重排为六层,信息一条没删,只是各归其位:
1. **标题卡**:处方/诊单/版本一行加粗,右侧是版本有效性、对照类型的状态药丸,再右是历史批次选择与刷新/重新分析。第二行小字放状态、失败原因、资料截止与批次建立时间。
2. **状态行**:读取提示与错误信息。
3. **流程条**:四阶段流程只在批次仍在处理或属于历史批次时显示,完成后不再占位。批次级进度行在两个模型各自报告阶段时隐藏,避免重复。
4. **两张结果卡(视觉重点)**:模型名 + 状态药丸 + **大号一致度数字** + "药味与剂量一致度"说明,下面是覆盖情况与原因、当前阶段、进度条与耗时。数字用中性墨色,不用红绿——高低不是评分。没有可比结果时显示灰色"—"。
5. **共用页签**:综合分析 / 候选用药 / 逐味对照 / 来源与缺口 由八个页签合并为四个,页内左右分栏同时显示两个模型的同一节,真正可以横向对照。
6. **复核条**:两个模型的复核状态、意见、保存与重试压缩到一行,原来纵向占用的约 200px 让给报告正文。免责声明降为脚注。
改动只在展示层:接口、轮询、权限、进度语义和落库数据都没变;`model_views` 的既有键全部保留,新增 `score``status_chip``card``model_label`。97 项桌面回归与 Ruff 通过,示意图见 `app/artifacts/issued-prescription-ai/redesign-running-20260910.png``redesign-finished-20260910.png`(合成数据,无网络)。