65 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(合成数据,无网络)。
2026-09-10 三栏对照工作区与医生原方快照
上一版把两个模型的分数做成了重点,但医生仍要靠记忆比对自己开的方。本轮把"医生原方"接进窗口,改成一次看三列。
服务端:报告详情新增 doctor_snapshot。 由批次入队时冻结的处方密文投影而来(PrescriptionAiDoctorSnapshot::project()),因此模型还没开始就能显示,且选择历史批次时始终是当时那一版——线上处方后来被改动也不会回写。投影只保留展示所需的标量字段:患者姓名/性别/年龄、clinical_diagnosis、剂型与用法、逐味药名剂量单位炮制与煎服说明、独立的辅方用法;明确排除电话、签名、case_record、药材 ID 等。权限沿用 loadBatch(当前处方 + 历史来源双重校验),列表与状态摘要不带该字段。源数据没有的单位或中西医诊断拆分一律不补造。
桌面端:PrescriptionReviewWorkspace 三栏工作区。
- 左栏"医生原方":患者、医生诊断、逐味药方与用法,按保存顺序原样展示(含重复行)。
- 中栏"诊断对照 + 逐味剂量差异":两个模型的辨证结论并排,下面是逐味剂量点线图(医生原方 / 千问 / OpenAI 三点同轴),支持药名搜索与"仅看剂量差异"过滤,未保存值显示"—"。
- 右栏"复核笔记":待确认差异、已保存对比计数、复核状态与意见表单。
- 顶部导航收敛为"对比总览 / 完整报告 / 资料记录"三个主入口,方义与用法、逐味明细、处理进度、重新分析移入"更多内容"菜单;上方"本次关注"一行给出本批次最该看的一句话。
展示层之外没有改动:轮询、权限、进度语义、比较算法与落库数据均不变。
本轮验证:桌面 182 项(报告窗口 / 对照组件 / 工作区三个套件)通过,Ruff 通过(顺手修了本轮编辑引入的一处 import 排序);后端 12 个离线套件通过,独立 MySQL 上 Queue 78 / Pipeline 90,旧结构 Queue 76 / Pipeline 85 通过,其中新增了原方快照的隐私、权限、不可变与"不补造字段"检查。界面渲染脚本 app/scripts/render_issued_prescription_ai.py 用合成数据离线产出 1024/1280/1440 三种宽度及部分完成、历史、等待中状态的截图。
2026-09-10 并发生成:多张处方同时分析
此前每个模型最多只跑 1~2 个任务,多张处方只能排队等待,界面上就表现为"一次只能生成一个"。本轮把并发做成真正可调的能力:
prescription_analysis.max_parallel_per_model默认提到 4,并改为环境变量prescription_analysis.MAX_PARALLEL_PER_MODEL可调(最小 1)。该值只是上限,每一个并发任务都需要一个消费者进程:本地按prepare×2、qwen×4、openai×4启动,启动脚本artifacts/prescription-ai-runtime/start_workers.ps1支持-Mode Add(保留在跑的进程,补齐缺的实例)与-Mode Restart(全部重启,仅在没有任务运行时使用)。daily_model_tasks、daily_patient_batches同样改为环境变量可调;单张处方每日分析上限由 10 提到 20——每次修复后重新分析是正常复核方式,这是预算护栏而不是正确性限制。超限时接口仍明确返回"今日分析次数已达预算"。- 资料扫描不再挡住新处方。 准备通道原来在同一轮里既领批次又跑来源/处方扫描,而扫描会重建完整上下文;现在扫描只在准备通道无事可做时执行,刚保存的处方永远优先被领取。
实测:同时对 3 张处方发起重新分析,加上原有 1 个在跑的任务,7 个模型任务并行执行(千问 3 + OpenAI 4),队列为空,无失败。批次 13(处方 7556,今天反复失败的那张)两个模型都完成:千问 18.88%、OpenAI 16.73%,均为可比。批次 14/15/16 的千问侧一致度分别为 19.29%、42.97%、17.36%,OpenAI 侧三个任务同时在跑。
线上同步部署时,需要在进程守护器里为每个模型通道配置与 max_parallel_per_model 相同数量的实例,并确认上游应用与账号的并发额度;数据库连接数也要留出余量(每个消费者一条长连接)。
PrescriptionAiWorkerResilienceTest 增加 4 项契约检查(并发下限、三个预算/并发值可由环境变量覆盖、扫描仅在空闲时执行),共 11 项。
2026-09-10 科技蓝指标条与图表
报告窗口补上"一眼看懂"的一层:在三栏工作区之上增加科技蓝指标条(issued_prescription_ai_metrics.py),全部取自已保存的不可变报告,不新增任何接口调用。
- 两张模型卡:模型名 + 状态药丸 + 覆盖情况;大号一致度数字与刻度条(千问 #1769E8、OpenAI #0E9384);下面是药味构成堆叠条与图例——医生独有 / 共同 / 该模型独有,并标出药味重合度。没有可比结果时数字为灰色"—"、刻度条留空,绝不显示 0%。
- 历史一致度折线:来自历史批次列表(按时间从左到右),不可比的批次断线不补点;这是真正能看出"这次比上次接近还是更远"的图。
- 资料覆盖卡:附件已读 / 受限不支持堆叠条、缺口总数与其中关键项数、资料构成(病历、医生备注、历史处方、问诊通话、监测记录、聊天记录、记录合计)。
- 自适应高度:窗口高度小于 780 时指标条收起副图(折线、构成条、阶段行),只留数字与刻度条,把首屏让给处方工作区;
MAX_HEIGHT/COMPACT_HEIGHT有回归测试守住。
同时修掉一个真实显示缺陷:逐味剂量差异表的列宽按"尚未布局完成的视口宽度"按比例计算,结果所有列被压到二三十像素,药名一个字一行、剂量显示成省略号。改为药名列与模型列有最小宽度、图表列吸收剩余宽度,窗口过窄时优先保证药名和剂量可读,并在 showEvent 里再测一次。
新增 app/tests/test_issued_prescription_ai_metrics.py 19 项检查(空值不变 0、0% 是真实测量、共同药味不超过任一侧、覆盖与缺口计数、历史折线时间顺序、紧凑模式取舍、百分比格式边界)。四个报告窗口相关套件共 201 项通过,Ruff 通过。
需要注意:本地客户端连接的是线上 admin.zhenyangtang.com.cn,而医生原方快照(doctor_snapshot)尚未部署到线上,所以左栏会显示"原方快照未保存",图表只能回退到对比结果里记录的医生剂量。要看到完整左栏,需要把服务端这轮代码部署上线。
2026-09-11 报告窗口改版落地(第一批)
按确认的设计稿开始落地,配色全部取自工作站的 reception_style.TECH_BLUE(主色 #1769E8、深色 #124EA9、画布 #F3F7FD、线条 #DBE5F2),OpenAI 侧继续用 #0E9384 区分。设计稿 app/artifacts/ui-mockups/v2/ 已同步换色。
① 蓝色渐变头部条。 面包屑、标题、版本与对照类型药丸、批次选择与刷新/重新分析/保存复核、六列信息带(处方·诊单 / 患者 / 临床诊断 / 剂型·剂数 / 资料截止 / 比较口径)以及主导航全部收进同一条渐变带,下方白色区域完整留给处方内容。信息带取自冻结快照,线上未部署 doctor_snapshot 时逐项显示"—",不猜测。窗口高度小于 780 时信息带与副标题自动收起,保证处方工作区仍占首屏一半以上(有回归测试守住)。
② 模型失败可就地重试。 失败原因从隐藏的进度页移到该模型卡片上:中文原因 + 「重试 千问 / 重试 OpenAI」按钮同行显示,只重试该模型并使用原资料快照;手动重试额度用尽时按钮隐藏,改为提示改用"重新分析"。无权限、批次过期、进度暂停时按钮不可用。
③ 新图表进入程序。 新增 issued_prescription_ai_charts.py(QPainter 自绘):
VennChart三方用药交集——由两个模型的候选药味与对比行中的医生药味算出七个区域,无候选方时不画空图;WaffleCoverage附件华夫图——一格一个附件,蓝=模型已读、橙=受限或不支持;DivergingDoses剂量差异发散条——已实现待接入逐味页。
回归:报告窗口相关四个套件 208 项通过,Ruff 通过。后续按设计稿推进:逐味页发散条与贡献列、资料与缺口页、处理进度页、历史与趋势页、统计页。
2026-09-11 报告窗口改版落地(第二批,全部页面完成)
第一批只完成了头部条、失败重试与三个图表控件;这一批把设计稿里剩下的页面全部做进程序,并顺手修掉改版过程中暴露出来的几处旧问题。
① 导航按目的地命名,不再按下标。 新增页面后原来写死的 setCurrentIndex(2/4/6) 全部错位("资料记录"实际跳到了原文页,"处理进度"出现了两个同名标签)。改成 tab_pages 字典 + _goto(key):主导航是 对比总览 / 逐味明细 / 资料与缺口 / 历史与趋势,更多内容 菜单收 方义与用法 / 综合分析 / 处理进度 / 原方记录 / 逐味原文 / 来源原文。工作区内的"查看依据"链接现在指向新的组合页而不是原始文本页。回归测试断言标签名不重复、每个链接落到正确的页面。
② 逐味明细页。 发散条形图(相对医生原方,只画两侧都有剂量的药味)+ 合并表:一行一味药,列出医生 / 千问 / OpenAI 三方剂量与两个模型的贡献值,说明列区分"仅医生使用""模型新增""剂量差 N g"。支持搜索药名与"仅看剂量差异"过滤,脚注写明贡献的算式。
③ 资料与缺口页。 左栏资料构成(病历 / 问诊通话 / 记录合计等,按行数自适应高度)与读取范围(资料截止、比较算法、提示词版本、覆盖状态、对照类型);右栏附件华夫图 + 一句话说明"读取 N 个、受限 N 个、未送达 N 个",下方缺口清单按类型合并并标注关键 / 一般。
④ 处理进度页(与旧进度页合并)。 批次流程与实时进度条保留在上方,下方是新的每模型阶段卡与调用记录表(模型 / 阶段 / 耗时 / 附件数 / 输出 tokens / 结果)。阶段键 text:0、files:1、final 一律翻成中文(文字资料 1、附件读取 2、开方与对比),错误码走词表。
⑤ 历史与趋势页。 一致度趋势柱(按时间从左到右,不可比的批次留空)+ 批次列表(批次号、时间、状态、两个模型的分数、算法版本、对照类型)。分数同时兼容列表接口的扁平 score 与详情接口的 comparison.score,并且只有 comparable 才给分——之前趋势图与列表全是"—"就是漏了这一层。排序也从"把时间戳当数字"改成按时间字符串排。
⑥ 统计窗口改版。 四张 KPI 卡(合格开方事件 / 两个模型的有效比较数与均值 / 专家复核合格率)、样本口径漏斗、排除原因合计、按医生列表,下方保留原有的分层明细 HTML。统计窗口现在也套用同一套样式表,卡片有边框有底色。脚注继续写明"一致度不是医生准确率",没有复核样本时合格率显示"—"而不是拿一致度顶替。
⑦ 顺带修掉的问题。
资料截止:一行因为写错表达式一直是空的,现在正常显示时间,没有就显示"—"。覆盖状态:partial直接吐英文;覆盖语境下译为"部分资料缺失"(沿用 STATE_LABELS 的"部分完成"会误导成进度)。- 表头默认居中,在被拉宽的最后一列里飘到中间;统一改为左对齐。
- 指标条在压缩模式下高度不够,华夫图与文字叠在一起;压缩时只留计数文字。
- 打开自带图表的页面(逐味 / 资料 / 历史 / 进度)时指标条自动收起,把高度让给正文。
QFrame#AiMetricCard此前根本没有样式,卡片是"白底白字";补上边框与圆角。- 短表格(资料构成、样本口径、排除原因)按行数固定高度,不再出现四行内容配一个滚动条。
回归:
- 桌面端报告窗口五个套件 226 项通过,Ruff 通过。
- 服务端 12 个离线套件全部通过(对比 266 项、统计 64 项、进度 38 项、Worker 韧性 11 项等)。
- 服务端两个落库套件在独立 MySQL(127.0.0.1:13379)上通过:
PrescriptionAiQueueTest78 项、PrescriptionAiPipelineTest90 项。 scripts/render_issued_prescription_ai.py现在额外产出per-herb / sources / history / pipeline / failed / statistics六张截图用于核对。
已知与本次无关的失败,均已用"把本次改动 stash 掉再跑"验证过是既有问题:
tests/test_busy_overlay.py::test_shell_construction_never_shows_orphan_business_controls;tests/test_diagnosis_drawer_visual.py整个模块跑到第 8 项时进程级崩溃(access violation / 0xc0000374),stash 后同样在同一位置崩溃。
2026-09-11 报告窗口按设计稿重做(第三批)
上一批做出来的界面和确认过的设计稿 app/artifacts/ui-mockups/v2/ 并不是一回事——指标条、图表形态、页面骨架都是旧的。这一批按设计稿逐页重做。
导航。 六个目的地全部平铺在蓝色带里,和设计稿一致:对比总览 / 完整报告 / 候选与逐味 / 资料与缺口 / 处理进度 / 历史与趋势。「更多内容」下拉去掉;「重新分析」从菜单里挪到band 上,和「刷新」「保存复核」并排。原始文本页(逐味原文、来源原文、原方记录)不再占导航位,由页面内的链接进入。跳转全部改成按名字寻址(tab_pages + _goto),不再用会随页面增减而错位的下标。
① 对比总览(设计稿 01)。 整页重写:
- KPI 行:两张模型卡(左侧色条 + 环形一致度 + 大号分数 + 药味重合/共同药味/候选药味三列),加一张「本次关注」卡(标题句 + 关键缺口/附件受限/共识药味三个药丸)。原来横在标题下的指标条取消,KPI 行现在属于总览页本身,其它页面不再被它占掉高度。
- 左卡:三方用药交集韦恩图(七个区域各自的药味数,三个集合各自标注「医生 N / 千问 N / OpenAI N」)+ 资料覆盖华夫图与图例。
- 中卡:剂量差异分布——一行一味药,左侧药名与原方剂量,中间以「与原方相同」为中轴的双色发散条,右侧两个模型各自的差值;某个模型没收录就画「OpenAI 未收录」的虚线徽标,两边都一致则画绿色「两模型一致 6 g」,模型新增的药味用浅色条画出全量。底部一条共用刻度尺。
- 右卡:复核清单——按严重度排序的圆点列表(模型风险 > 模型缺口 > 系统缺口),每条注明是哪个模型提出的;下方是复核意见表单。
② 候选与逐味(设计稿 03)。 顶部三张处方卡(医生原方 / 千问候选 / OpenAI 候选),各自列出药味与剂量、味数与剂型、服法与风险提示;中间是逐味对照表,贡献列改为「小条 + 数值」,说明列区分三方共有/仅医生使用/某模型未收录/剂量差 N g;筛选从一个复选框换成设计稿的四个计数筹码(全部 / 仅共同 / 仅差异 / 仅未收录)加搜索;底部四格用法对照(服法、疗程、剂型、辅方,医生与模型分行)。
③ 资料与缺口(设计稿 04)。 改成三栏:资料构成用按类型的比例条(问诊通话、监测记录、医生备注、历史处方、病历、聊天归档),读取范围列出资料截止、比较算法、提示词、药材字典与覆盖状态;中栏是附件华夫图、读取说明与「模型各自读取到的量」表;右栏是按严重度排序的缺口清单,配一段说明缺口含义的提示块。
④ 处理进度(设计稿 05)。 顶部四格统计(批次总耗时 / 模型调用 / 格式修复·追问 / 失败调用);两张模型泳道卡,阶段列表由保存的调用记录归并得出(文字资料分析、附件读取、生成候选与报告、格式修复、药名回问……),没有调用记录就不编造阶段;调用记录表增加「耗时分布」条,按本批次最慢的一次调用取比例。
⑤ 历史与趋势(设计稿 06)。 左右两栏:左边是趋势柱与一段「分层说明」——相邻两个批次之间只要比较算法或提示词版本变了,就写清楚 v1.0.1 → v1.1.0、v3 → v4 并注明两侧分数不能直接相减;右边是批次列表(批次号、时间、状态/原因、两个模型的分数、算法版本),失败批次直接显示模型给出的原因。
⑥ 统计窗口(设计稿 07)。 四张 KPI 卡之后是一致度分布直方图(按 20% 分箱,两个模型并排)与「均值 / 中位 · 配对样本」三格小结;右侧样本口径与排除原因改成条形(标签写在条内),下面是那段说明「一致度不是医生准确率」的琥珀色提示块;按医生列表保留在最下方。
为此在服务端把已经算好但没往外传的分布补上:PrescriptionAiLogic::statistics() 的每个模型多返回一个 distribution(PrescriptionAiStatistics::summarize() 早就在算 [0,20) … [80,100] 五个分箱,只是之前没进接口)。PrescriptionAiQueueTest 增加一条断言,保证分箱确实随统计接口返回。
⑦ 完整报告(设计稿 02)。 每一栏加上模型名 + 状态药丸 + 生成时间的抬头。设计稿左侧的章节目录(TOC)没有做——报告正文是服务端结构化字段渲染的,没有可跳转的锚点,硬做会是个假目录。
顺带修掉的问题。 比较口径 改为从对比行真实的单位与剂量基准推导(全部一致才显示,否则回退到冻结处方的剂数单位);QFrame#AiMetricCard 之前完全没有样式;统计窗口现在也套用同一套样式表。指标条里已经用不到的堆叠条、迷你折线、覆盖卡与交集卡整体删除,不留死代码。
回归:报告窗口五个套件 243 项通过,Ruff 通过;服务端 12 个离线套件全部通过,落库套件 PrescriptionAiQueueTest 79 项、PrescriptionAiPipelineTest 90 项通过。scripts/render_issued_prescription_ai.py 的示例数据换成三份真正不同的处方(医生 8 味 / 千问 7 味 / OpenAI 6 味)与 5 个历史批次,六张页面截图逐张核对过。
2026-09-11 报告窗口改造为深色分析控制台(ai-redesign-v2)
按 C:/Users/pc/WorkBuddy/2026-09-11-16-46-03/ai-redesign-v2.html 与同目录 overview.md 重做。注意:动手前发现这些文件正在被另一个进程同时修改(白色顶栏、可展开交集卡、"保存千问复核"都不是本会话写的),与用户确认另一会话已停止后才接手,本轮是在那个状态上继续改的。
① 主题令牌独立成模块。 新增 issued_prescription_ai_theme.py:海军蓝底 #080D18、卡片 #0F1726/#131D2E、线 #22304A;科技蓝 #2E7BF6 只做界面色(按钮、激活态、焦点),千问改天青 #17BFDD、OpenAI 改紫 #9B7BFF,风险保留琥珀 #F5B942 与玫红 #F4697A。窗口内六个模块统一从这里取色,不再各自写死浅色值;原先散落的 30 多个浅色硬编码全部换成令牌。
② 顶栏。 面包屑「甄养医生工作站 / AI 分析报告」+ 标题 + 版本药丸;右侧四组事实(处方/诊单、剂量口径、资料截止、批次)、版本选择、刷新、重新分析、登录医生。患者 / 主诉 / 临床诊断 / 剂型收在标题下一行——设计稿顶栏没有这几项,但这是医疗窗口,冻结快照的身份信息不能丢。
③ 一致率对比条(新控件 issued_prescription_ai_console.py)。 两侧各自:模型点 + 状态 + 资料覆盖 + 用时,大号一致率,0/25/50/75/100 刻度条,以及对手所在位置的灰线标注「OpenAI 在此」。中间给差值 +7.2pt 与领先方,并列出两侧药味重合。下面一行写明一致率的定义与可比条件。模型失败的原因与「重试 X」按钮也移到这里——那是整个窗口里唯一只属于单个模型的位置。
④ 左侧编号步骤导航。 01–06 六个目的地,各带数量徽标(剂量差异项数、报告数、药味数、缺口数、调用次数、批次数),下面是「本次关注」四项计数与「保存本次复核」。原来的横向标签条与顶部 KPI 卡整块删除,MetricsPanel 及其 21 项测试一并移除。
⑤ 对比总览重排为设计稿的两行。
- 本批结论:四条按规则生成的编号结论(候选方味数 / 一致率与差值 / 剂量偏差最大的三味 / 资料缺口分布),配「结论由规则生成,不代表医生判断」的说明条。
- 药味归属分布:三方共识 / 医方独有 / 千问独有 / OpenAI 独有,各自条形 + 直接列出药名(韦恩图读不出结论,换成这个)。四组不含"医方与单一模型共用"的药味,标题会把这部分单独说明,避免四个数加不回总数。
- 复核清单:关键 / 一般徽标 + 归并后的条数 + 来源模型,复核意见与状态表单折进同一张卡。
- 剂量差异分解:以「与医方相同」为轴,右=加量、左=减量,刻度 −N/−N/2/0/+N/2/+N,右侧两列分别是千问 Δ 与 OpenAI Δ。
- 高风险提示:偏差 ≥10 克或幅度 ≥50% 自动进清单,标高/中并写明触发原因;阈值只做排序,不替代医生判断。
- 底部四个快捷入口。
⑥ 其余五页。 完整报告、候选与逐味、资料与缺口、处理进度、历史与趋势跟随新色板;四页包在滚动容器里,窗口不够高时滚动而不是把控件压在一起。图表的模型色统一换成天青/紫。
图标。 窗口不带图片资源,新增 issued_prescription_ai_glyphs.py,所有图标(含两个模型徽标、窗口标记)都用 QPainter 画线稿,颜色由调用方给。
设计稿里剩下的四项也已补齐:
- 深 / 浅主题切换(◐):
issued_prescription_ai_theme.py里给出DARK/LIGHT两套令牌,use_theme()就地改写CONSOLE并回调所有派生值(图表色、卡片样式表、窗口样式表)。窗口构建整段抽成_build_ui(),切换时原地重建:当前页面、选中批次与尚未保存的复核意见都会带过去。 - 顺带查出一个真 bug:
QPushButton#AiStep:checked QLabel#AiStepName这种"父级伪状态 + 后代"选择器在 Qt 里是按子控件的状态判定的,于是六个步骤的文字全部取了选中态的白色 —— 深色底看不出来,一换浅色就成了白底白字。改为在set_current()里按选中与否直接给标签上色。 - 附件缩略图网格:资料与缺口页把华夫图换成一格一个附件的磁贴,标注类型与编号,蓝=已读、琥珀=受限/不支持,悬停给出完整编号、状态、原因与版本核验情况。为此服务端
PrescriptionAiGenerator在覆盖清单的四条写入路径上都补了type字段(原来只有file_id),并在PrescriptionAiGeneratorTest里加了断言守住。 - 图表悬停说明:贡献条给出算式、来源构成条说明归一口径、调用耗时条给出占最慢一次的比例、趋势柱与分布柱逐批次/逐分箱列出数值、归属分布的条形与药名列出完整药味。
- 完整报告页章节目录:报告按字段切成带锚点的章节,左栏列出章节可跳转(两栏同时滚动),并提供「并排对照 / 仅千问 / 仅 OpenAI / 只看不同段落」四种显示方式;"只看不同段落"按章节渲染结果是否逐字相同来判定,规则可解释。
- 另外把左栏「附件受限」的口径改成模型确实没读到的附件数(原先取的是缺口记录条数,与页面上的附件状态对不上)。
回归:
- 报告窗口四个套件(
test_issued_prescription_ai{,_pages,_workspace,_comparison}.py)231 项通过,Ruff 通过;服务端PrescriptionAiGeneratorTest通过。 - 全量桌面套件(排除早已进程级崩溃的
test_diagnosis_drawer_visual.py)有 18 项失败,全部落在本轮没有碰过的模块里:busy_overlay、list_detail_geometry、patients_visual_blue、patient_orders_visual_blue、patient_progress_visual_blue、reception_completion_button_ui、reception_parity_ui、silent_list_loading、diagnosis_order_video_visual、patient_ai_report_desktop、diagnosis_index_visual。 - 与改造前的一次完整跑(同一工作区、深色化之前)逐条比对:失败集合完全一致,只多出
diagnosis_index_visual一项,而该项单独跑通过 —— 是整轮跑到后段(进程占用 4.6 GB)时的顺序/内存相关抖动。 - 另外验证过:把
DEBUG_MODE临时改回True后这些用例仍然失败,所以与工作区里那处版本/调试开关的改动无关;git status显示本轮只动了issued_prescription_ai*这一组文件。
逐项对齐设计稿 ai-redesign-v2.html(字号 / 间距 / 描边)
上一轮把结构搭到位,但字号与间距是估的,医生反馈"好多细节都不匹配",并指出两处硬伤:一致率刻度的百分比标签被裁掉、逐味对照矩阵的列错位。这轮按设计稿的 CSS 逐条换算(html{font-size:15px},所以 .68rem = 10.2px、.82rem = 12.3px、.9rem = 13.5px、1.8rem = 27px),把两边的数值对齐。
取值方式。 设计稿是本地 HTML,直接读它的 CSS 而不是量截图:--radius:10px、--radius-sm:7px、卡片描边用 --line-soft 而不是 --line、.card-h{padding:15px 18px 0}、.card-body{padding:14px 18px 18px}、.grid{gap:14px}。浅色令牌与 rgba() 色调(--blue-dim 等)按叠在卡片底色上的合成值写进调色板,Qt 这边没有 alpha 混合也能落到同一颜色。
改到的地方:
- 卡片头。设计稿里
.card-h只有标题 + 小字说明 + 右侧图例,没有图标;_panel_head()去掉了 glyph 参数,标题 13.5px/600、说明 10.65px--text-faint。说明文字换成HintLabel:宽度不够时省略号收尾并留 tooltip,绝不再把卡片顶宽(上一版为此把它设成Ignored,结果整行说明直接消失)。 - 左侧步骤导航。
.stepnav的 26px|1fr|auto 栅格:序号片 22×22/圆角 6/10.2px,名称 12.6px,数量徽标做成 20px 起宽的胶囊(9.9px、圆角 8、上下 17px 固定高)——之前徽标会被拉满整行高度,六个连成一条竖条,看着像滚动条。选中态按设计稿画左侧 3px 圆角竖条(QSS的border-left会把内容顶偏,改在_StepButton.paintEvent里画),徽标按严重度分色:待决项有关键项时取玫瑰,资料缺口取琥珀。 - 一致率对比条。百分号按
.val sup处理成小一号、上标、--text-dim,新增ScoreLabel自绘,text()仍返回 "56.1%" 这一个字符串,调用方与测试不受影响。刻度条填充改成设计稿的rgba(hue,.45) → hue渐变,四分位分隔线换--grid-line,对手模型的标记改成 2px 圆角竖条。卡片左缘补上蓝→紫渐变竖条(按卡片圆角裁剪),中间差值列的分隔线改虚线。两侧模型块顶部对齐,失败的一侧不再把整列往下推。 - 刻度标签。设计稿里
.axis的bottom:-1px让 0%/25%/… 压在轨道上、被填充盖掉一半 —— 这正是医生截图里"看不清"的那处。这里不照抄:标签仍排在轨道下方 31px 处,可读优先。 - 逐味对照矩阵。列固定为 药味 / 千问(克/剂 · 贡献度)/ OpenAI(克/剂 · 贡献度)/ 医方原方 / 结论,后四列定宽、首列拉伸,每个模型一格自绘(剂量 + 贡献条 + 数值),不再靠自动列宽挤在一起。右上补回「千问贡献度 / OpenAI 贡献度」图例;「仅医方使用」按设计稿不套胶囊,用弱化文字。
- 列表行。本批结论、复核清单按
.finding li/.rv-item加 1px--line-soft分隔线与 10/11px 的上下留白;结论正文 12.3px/160%,复核标题 12.3px、副题 10.5px、计数用等宽字 13px。 - 表格与统计卡。所有表头 10px/
--text-faint/padding:10px 16px,单元格 12px/padding:8px 16px,分隔线--line-soft;处理进度页四个数字改 26px 等宽字,标签 10.65px 带字距。 - 完整报告正文。文档样式表原先写死了浅色(
#EFF4F9表头、#244C79标题),深色主题下表格是一块白。改成按当前调色板生成:正文 13px/195%、章节标题 11px--blue-text、表头 10px。Qt 的富文本引擎会自己决定<h3>的字号、无视样式表,所以章节标题改用<p class=sec>。 - 趋势图。每根柱子按设计稿在顶部标出自己的数值(用该模型的颜色),两根柱子拉开到能容下各自标签的间距。
- 说明条(
ReadingNote)。虚线描边、9/12 内边距、10.8px、圆角 7,并在resizeEvent里按换行后的高度设最小高——否则 QLabel 只申报一行的高度,第二行被卡片裁掉。
窄窗口跟着设计稿的断点走。 设计稿在 @media (max-width:1240px) 把卡片栅格收成一列、把左栏改成横向;照着这个思路补了三处,都是这轮出图时才暴露的:
- 对比总览的两行卡片改用
QGridLayout,工作区窄于 960px 时收成一列。原先三张卡各占 1/3,940 宽时「本批结论」只剩两个字一行。 - 左栏放进自己的滚动区。窗口不够高时它原来会被压到最小高度以下,「本次关注」四行直接塌成四条空条。
- 顶栏在窗口窄于 1400px 时把资料摘要(处方 / 诊单、剂量口径、资料截止、批次)换行到第二排;窄于 1240px 再收起翻页箭头、面包屑与读取状态。原先 1280 宽时标题会被裁成「诊断与药...」。
原生标题栏跟着主题走。 深色控制台原先只画到窗口边框为止,Windows 自己画的标题栏还是浅色,接缝很突兀。apply_window_chrome() 用 DwmSetWindowAttribute 把标题栏底色、文字色、边框色设成当前调色板(Windows 11 22000+ 生效;其他系统调用失败即保持原样,不报错),窗口显示时与主题切换时各调一次;统计窗口同样处理。
候选卡片不再自己截断。 三张处方卡原来写死 setMaximumHeight(250),方义长一点(真实数据里常有五六行)就把药味表和「另有 N 味」一起切掉。去掉上限,药味表按内容申报高度,方义换成 WrappedLabel —— 普通 QLabel 开了自动换行也只申报一行的高度,布局照给一行,剩下的被裁;这个类在 resizeEvent 里按实际宽度回问一次所需高度并设为最小高度。ReadingNote 里原来那段同样的代码合并到这个类上。
表格直接往下铺,不再挤在剩余空间里。 调用记录原来带 stretch 塞在页面底部,实际只剩六十来像素,表头被切、一次只看得到一行。_fit_rows() 统一成按实际行高量一次(原先那版假定每行 31px),关掉表格自己的纵向滚动条,把高度设成表头 + 所有行;逐味对照矩阵、缺口清单同样处理。页面长了就由页面自己滚——这也是设计稿的做法,矩阵在稿子里就是十二行一次铺完。
顺带修掉两处连带问题:
- 处理进度页是唯一没进滚动容器的页,表格铺开后整页被压到 464px,两个模型的阶段行直接塌成零高。改成和其余五页一样走
_add_scrolled_tab()。 - 顶栏换行阈值原来写死 1400px,但批次翻页箭头出现时横向要 1482px,1440 下资料摘要又被切了。改成按
top_row.sizeHint()与顶栏可用宽度实测比较,换行后把收起的宽度加回去再比,不会来回抖。
回归:
- 报告窗口四个套件 234 项通过(新增一个跳过分隔线取结论文本的辅助函数),Ruff 通过。
- 六个页面在 1440 / 1280 / 1024 / 940 四档宽度重新出图,深浅两套主题各出一张,逐张与设计稿截图对照。
- 全量桌面套件(排除早已进程级崩溃的
test_diagnosis_drawer_visual.py)17 项失败,全部落在本轮没碰过的模块里,与改造前基线的 18 项相比只少了diagnosis_index_visual那条——该条单独跑通过,是整轮跑到后段的顺序/内存抖动,此前已记录。全仓只有那四个 AI 测试文件 import 本轮改动的 dialog 模块。