This commit is contained in:
Your Name
2026-09-10 15:19:17 +08:00
parent 27fbef9321
commit 36975c6c1b
487 changed files with 15696 additions and 78 deletions
@@ -0,0 +1,465 @@
# 手工处方触发双模型患者综合分析方案
日期:2026-09-09
状态:用户已确认实施;本地核心流程实现和回归验收已完成。线上数据库、配置、后台进程和真实模型调用尚未执行;实际支持范围与未接入能力见 [实施与部署说明](prescription-ai-deployment.md)。
本轮补充:按用户追加需求,加入处方列表中的双模型对比百分比与医生维度统计,详见第 14 节;用于百分比比较的候选方改为先隐藏本次人工方生成并冻结,再进行对照。部署和实际支持范围见配套实施说明。
## 1. 目标与产品边界
当已开处方中的系统空白处方第一次保存为手工处方,或医生直接新建手工处方后,服务端自动安排后台分析。系统汇总患者在授权范围内的完整临床资料,固定一份资料快照,同时交给现有千问和 OpenAI 两个模型。每个模型独立生成一份综合分析报告,并在允许的专业辅助范围内提供中医辨证与候选用药方案,供医生对照本次人工处方复核。
这一功能属于开方后的辅助分析,不能作为开方前风险已被检查的证明。处方保存后即可继续操作;模型失败不会撤销已经保存的处方。AI 输出保存在报告中,不自动覆盖人工处方,不自动提交审核、签名、创建业务订单或下发药房。
“AI 开设的中医处方”需要在产品上明确边界。《互联网诊疗监管细则(试行)》要求接诊医师本人开具处方,并禁止使用人工智能自动生成处方。2025 年相关实施意见支持临床辅助决策,但不能据此推定自动开方限制已解除。因此本方案将这一部分设计为面向授权医师的辨证和候选用药建议;具体到药味、剂量的展示及任何导入正式处方功能,上线前需由医疗机构结合实际服务场景确认适用边界,不能认为改称“草稿”或增加签名就当然合规。第一期不接自动导入、自动采纳链路。依据:[国家卫健委《互联网诊疗监管细则(试行)》](https://www.nhc.gov.cn/yzygj/c100068/202203/2072f0e8988249e59d942e1b2a933916.shtml)、[《关于促进和规范“人工智能+医疗卫生”应用发展的实施意见》](https://www.nhc.gov.cn/guihuaxxs/c100133/202511/d1a42ae835c743b9b3e83ac0253c3e9f.shtml)。
## 2. 当前代码核实结果
本次是本地代码静态调研,不代表已验证生产数据库、模型账号权限或 Dify 已发布工作流。历史 research 文档部分内容已过时,以下以当前业务代码为准。
| 项目 | 现状 | 本次需要补充 |
| --- | --- | --- |
| 截图界面 | `app` 的 Python 桌面医生工作台 | 优先改已开处方页及共享患者详情 |
| 来源识别 | `is_system_auto=1` 对应系统空方,编辑保存会置为 0 | 在服务端识别首次转手工,不能仅看页面显示文本 |
| 患者关联 | 系统空方创建时可写 `patient_id=0`,编辑不会补齐;预约的 `patient_id` 实际指诊单 ID | 通过处方关联诊单取得稳定患者 ID,校验冲突与缺失 |
| 双模型 | 配置键为 `qwen``openai`;代码配置名为 `qwen3.6-35b``gpt-5.6-sol` | 复用配置,核验实际服务能力;Dify 路径的实际模型由应用配置决定,不能仅凭本地名称确认 |
| 患者资料 | 已聚合病历、备注、日常记录、处方、IM/企微归档、视频转写 | 抽取共用构建器,完善范围、时间、附件与来源证据 |
| 报告生成 | 患者报告是同步单模型;诊单双报告是循环顺序调用;接诊台部分路径先千问成功再调用 OpenAI | 改为两个独立后台任务,共享一次冻结的输入 |
| 附件 | 默认最多附带 3 个,可被运行配置覆盖;超出转地址清单;拒收可能降级纯文字 | 分批处理并记录每个模型实际收到的附件和失败项 |
| 文档支持 | 当前 OpenAI-compatible 适配只内联图片,非图片进入缺口清单 | 报告 PDF/扫描件必须解析或转换为确实支持的输入 |
| 报告内容 | 患者报告只有分析、风险、治疗建议,提示词明确不直接开方;另有独立 AI 处方草稿能力 | 新增专用报告结构和专业辅助输出规则,不能只换标题 |
| 保存历史 | 患者报告 INSERT 留历史,诊单报告按模型覆盖 | 本功能使用独立、不可变的批次结果,关联具体处方版本 |
| 后台基础 | 未发现 AI 专用队列;已有数据库任务领取、租约、重试和常驻命令模式 | 借鉴现有任务机制,新建 AI 专属表及消费者 |
项目根 `AGENTS.md` 所引用的 `.trellis/workflow.md``.trellis/spec/` 当前不在工作区;未创建或假设其中规则。本方案存放在 `docs/plans/`
## 3. 自动触发规则
推荐在“保存成功、进入待审核”时启动,不等待审核通过,符合用户描述的手工开方完成时点。触发在服务端完成,使桌面端及其他调用相同保存接口的入口行为一致。
| 事件 | 推荐行为 |
| --- | --- |
| 只创建系统空白处方 | 不调用模型,显示“尚未开方” |
| 空白处方首次保存为含有效药味的手工处方 | 创建一次分析批次,双模型自动生成 |
| 直接新增含有效药味的手工处方 | 创建一次分析批次,双模型自动生成 |
| 同一保存请求重试、重复点击、并发提交同一版本 | 返回已有任务,不重复生成 |
| 药味、剂量、剂型、疗程、服法、临床诊断等实质变化 | 旧报告标记“处方已变更”;合并短时间连续修改后生成新版本 |
| 内容未变、仅审核状态变更 | 更新关联处方状态,不重新调用模型 |
| 仅姓名、电话、内部备注等非临床内容修正 | 不重新调用模型;性别、年龄等临床属性改变按资料更新处理 |
| 作废、删除、驳回且作废 | 停止待执行任务;在途结果不得成为当前有效报告;保留依法允许保留的历史审计 |
| 作废后重新编辑恢复为有效手工处方 | 按新的处方状态版本评估,旧取消批次不能直接复用 |
| 打开列表、查看报告、刷新页面 | 只读状态与报告,不触发生成 |
| 医生点“重新分析” | 新批次并记录原因;同版本已有任务运行时优先返回原任务 |
| AI 生成的候选建议 | 仅报告数据,不创建正式处方,因此不触发循环 |
新增/编辑写入轻量持久化事件,不在保存请求内读全量聊天、解析附件或请求模型。推荐将事件与处方变更放在同一数据库事务。模型或消费者故障不影响已经提交的处方;若事件自身无法持久化,保存接口不能谎报“已安排分析”,应按事务失败明确返回。这一点与模型失败分开处理。
为兼容异常部署或已有数据,增加低频补偿扫描:发现启用时间之后的合格手工处方版本缺少事件时补写。扫描遵守相同唯一键、权限和预算,不替代正常事件链路。功能关闭时保留恢复标记,重新启用可补齐启用期间应生成的数据。
## 4. 统一患者资料包
“完整”定义为:在本次资料截止时间前,服务端已保存、已归档、具备合法访问权限的全部相关临床资料。未上传照片、未保存表单、未同步聊天、丢失的录音不能假装已读取。各类缺失、未同步、无权限、无法解析分别计数。
| 资料 | 纳入内容 | 处理要求 |
| --- | --- | --- |
| 患者资料及历次病历 | 年龄、性别、身高体重、主诉、现病史、既往史、过敏、家族史、特殊人群、现用中西药、肝肾相关资料 | 保留记录日期及来源,区分最新陈述、既往记录和冲突;不把未填视为“无” |
| 医生备注与医助跟踪 | 正文、时间、角色、关联资料 | 医生意见与患者自述分开;不能只取图片数量 |
| 舌象 | 全部已上传舌象、拍摄/上传日期、医生描述、来源 | 去重、按时间排序、分批视觉输入;质量不佳或非舌象不强行下结论 |
| 检验/检查报告 | 原文件、可提取正文、检查日期、项目、数值、单位、参考区间、异常标记 | 原文提取优先,扫描件 OCR;保留页码,OCR 疑点可核对原件;不得补造数值或单位 |
| 日常记录 | 血糖、血压、用药/胰岛素、饮食、运动、相关图片和备注 | 原明细全部在资料范围内;额外提供近期趋势,区分空腹/餐后和测量时点 |
| 历史处方 | 药味、剂量、剂型、服法、疗程、处方病历、审核/作废状态 | 待审、作废、历史有效处方分别标记;已开方不等于已服药 |
| 本次人工处方 | 本次提交的处方版本及完整内容 | 服务端保留,在独立候选方冻结后才用于对照;不提前泄漏给生成候选方的模型,也不视为既往疗效证据 |
| 聊天记录 | 已归档腾讯 IM、企微消息,角色、时间、正文及相关附件 | 按消息标识去重;显示各通道同步到什么时间;语音需有已完成转写才算文字证据 |
| 每次视频问诊 | 历次通话日期、医生/患者角色、完整转写段、时间戳及转写状态 | 完整、部分、失败、未开始分开;默认使用转写,不重复外发全部视频 |
| 疗效、依从性与不良反应 | 已有随访中实际服药、停药原因、症状变化、不良反应;必要的配药/配送事实 | 作为补充资料;付款或配送不能证明服药,也不能证明疗效 |
患者绑定从 `prescription.diagnosis_id → tcm_diagnosis.patient_id` 解析,与其他稳定标识核对。没有诊单绑定或标识冲突时,显示“需完善患者关联”,不按姓名、手机号自动猜测匹配。当前预约字段存在同名不同义问题,实施时须集中封装解析。
权限不是简单地“能查看一张处方就能看患者所有聊天”。现有患者并集查询会扩大到未挂诊单的记录,必须逐数据源定义患者级及诊单级访问规则,并与医生、医助、部门范围取交集。后台任务不能以超级管理员身份绕过这些限制。
旧 AI 报告默认不作为临床事实再输入。确有医生确认的结论时,以医生确认记录及来源重新纳入,避免模型推断被反复引用。
## 5. 相同输入与附件完整性
一个批次只冻结一次资料包,包括来源记录版本、附件内容哈希、资料截止时间、提取器版本、脱敏规则版本、模型及提示词配置版本。两个模型引用同一个 `snapshot_id`,不能分别执行当前单模型生成接口来抓两份不同时间的数据。
快照冻结时间通常晚于处方保存时间,应同时显示“处方保存时间”和“资料截止时间”。原处方版本在触发时固定;资料按冻结时一致性读取。冻结前若处方再次修改,旧待执行批次合并/失效;冻结后的新资料进入后续版本,不修改原快照。
上述时点规则用于当前临床辅助报告。第 14 节用于医生独立比较的基线必须另外固定在开方决策时点,不得用后台执行时才出现的新检查或疗效评价早先处方。基线评价和最新资料辅助报告共用数据访问能力,但资料时点、快照及统计资格分别保存;无法重建开方时资料的记录不强行补出基线分数。
大资料按病历、消息、通话和文件页等语义边界分片,保留来源编号。按照模型实际上下文限制预算 token,不用固定字节数假设一定能放下。不默认只取近 N 条。处理总量超过配置预算时分阶段排队或明确提示范围限制,不静默截断并标为完整。
附件处理流程:
1. 在授权存储范围内收集并去重全部附件,记录文件版本、类型、大小和关联来源;内容变化即使 URL 不变也产生新版本。
2. 数字报告提取原文,扫描报告 OCR,保留图片/页码映射;任何机器提取都标记为派生证据,不能等同医生确认。
3. 舌象和需要视觉阅读的报告页按各模型附件上限分批,两个模型各自读取相同原图集合,保留各自识别结果。不能把千问的舌诊结论当作共同原始事实再交给 OpenAI。
4. 文本分片和附件识别完成后,各模型在自己的分支中综合,输出最终报告。超长场景会有多次子调用,不是无论资料多少都只调用两次。
5. 每份结果记录附件传输和处理清单:已送达、解析完成、不可读、不支持、缺失、受权限限制等。传输成功不能证明模型正确理解,医学结论仍需核对来源。
6. 如果某模型不支持图片,需明确显示该模型的能力缺口。共享 OCR 能帮助文档读取,但不能替代舌象原图分析;本次要求的双模型完整视觉能力未满足时不得标记为完整成功。
新分析链路应采用严格附件策略:不能带文件时不偷偷改为纯文字完整报告。可以保留“资料不全的初步报告”,但以独立覆盖状态说明限制。按 2026-09-10 追加需求,一般图片、转写或历史版本缺口不再单独禁止基于已读证据的候选方与一致度比较;影响用药安全的关键信息缺失时仍不强行给出具体药味剂量。完整性来自清单及处理结果,不能继续用固定 `snapshot_complete=true` 表示所有资料都已被模型阅读。
## 6. 视频转写、聊天与晚到资料
以服务端成功保存的转写最终状态为准,不能以视频窗口关闭、HTTP 请求成功或客户端泛化完成信号判断完整转写。
推荐规则:
- 本次诊单/预约关联的通话仍在进行或转写未归档时,批次显示“等待本次问诊转写”。
- 服务端首次归档完整转写后,以通话、session、转写内容版本形成幂等事件,唤醒相关批次。
- 默认允许最多等待 5 分钟(待确认的运行参数)。超时或实际为 `partial` 时,可生成明确标注缺口的初步分析;关键问诊资料缺失时候选方案为空,列出待补问题。
- 晚到转写、转写纠错、报告解析完成后,自动为本次关联处方生成补充版本。保留旧版,说明“本次新增/修正了什么资料”。
- 通过诊单、预约、通话的服务端关联定位相关批次,不能按最近时间或患者姓名猜配到另一场问诊。
- 同一问诊连续上传多张图片、补写备注、保存转写分段,合并到约 60 秒的静默窗口;设置最长合并等待与重算频率上限,避免长期不出结果。
- 默认对当前问诊和最近相关处方自动补充;较早历史处方只显示资料已更新,不因患者今后每条饮食/聊天记录重算全部历史处方。新的手工处方会自然读取此前全病程。
- 聊天读取已有归档与同步水位,可在后台执行已有的授权只读同步;同步失败保留缺口和水位,不无限等待。
原始开方时分析与之后的补充评估分开标注;不能让晚到资料倒灌成“开方当时医生已经知道”的证据。
## 7. 每个模型输出什么
两模型使用相同任务定义和基础资料,独立完成分析,不读取对方报告。不要求两者同时完成;先完成的一份立即可查看。按用户新增的百分比对比需求,第一期默认先隐藏本次人工方及其重复副本,让两个模型各自生成候选方并冻结,再由服务端与人工方比较。完整病历中的既往有效处方仍保留,但本次处方药味/剂量、复制到备注或聊天中的本次方案、现有 AI 对其作出的评论等不能提前进入候选生成上下文。无法排除泄漏或资料时点不可比时标记为辅助复核,不进入医生独立比较统计。
药味和剂量百分比由固定程序计算,无需额外模型调用。需要对照理由时,在候选方冻结之后,由各自模型另做复核说明,允许其看到本次人工方,但不能回写或重新选择计分候选方。这一步单独记录阶段、输入哈希、耗时和费用;大资料或双模型完整复核说明会增加调用次数。两个模型在每个相同阶段使用同一基础输入范围,候选阶段和复核阶段的输入哈希分别保存。
| 报告部分 | 内容 |
| --- | --- |
| 概要 | 本次主要问题、需要医生优先确认的结论 |
| 病程与资料依据 | 当前及历史变化,引用具体病历、记录、报告页或问诊时间点 |
| 中医辨证分析 | 症状、舌象及已有脉象支持的辨证意见,支持证据、矛盾证据、待鉴别项;不得从照片推断未提供的脉象 |
| 风险与资料缺口 | 过敏、特殊人群、现用药、肝肾相关风险、检查/转写缺失等;事实与推断分开 |
| 中医候选用药建议 | 在允许范围与资料充分前提下给出治法、方义、药味与剂量建议、单位、剂型、用法、疗程、依据及复核点 |
| 本次人工处方复核 | 与本次人工方的药味、剂量、疗程、剂型、用法及风险差异,说明差异依据,不自动判断医生对错 |
| 随访建议 | 需要补问的问题、需要医生评估的检查及观察项目;不自动调整既有中西药 |
| 报告来源说明 | 资料截止时间、模型配置、版本、来源计数、附件缺口及复核状态 |
专业候选建议使用结构化字段,至少包含药名、规范药材映射结果、剂量及单位、剂量基准(每剂/每日)、炮制或特殊煎服说明、主辅方、服次、疗程、方义和证据引用。药材 ID 由服务端映射,不能信任模型生成的 ID;同名异物、重复药味、未知药名、无单位、小数或总量异常进入复核状态。
饮片、颗粒、浓缩水丸等不能直接按相同克数互换。模型应说明建议剂型;没有机构确认的换算规则时不得自动换算。缺失剂量或疗程时返回缺失原因,不自动填“7 剂、每日 2 次”等通用默认值。
候选输出允许 `insufficient_data``withheld_for_risk``available_for_review`。2026-09-10 用户确认本功能用于医学研究对照后,默认改为“必须开方”:无论资料是否完整,两个模型都要先各自独立开出候选方,再由服务端比较,缺口与所作假设写进候选方说明和风险提示(见第 15 节)。原“用药安全依据不足即暂缓候选方”的策略保留为可配置项,默认关闭。应将机构维护的药材字典、规则与医生/药师复核结合使用;数值大于零、JSON 合法、两个模型一致都不能证明临床安全。高风险用药规则需要由机构维护,不能让通用模型自建禁忌数据库。
两模型比较页提供“共同意见、不同意见、人工处方差异、资料覆盖差异”四部分。药名、剂量、疗程的结构化差异由程序比较;辨证语义差异原文并列展示。第一期不加第三个模型裁判,不合并出一张所谓最优处方,也不把任一模型的自报置信度当成可靠概率。
## 8. 页面与医生操作流程
### 8.1 已开处方列表
在现有“来源”附近增加紧凑的“AI 分析”状态列,行操作增加“AI 报告”。原有处方来源和审核状态保持各自含义,不能用 AI 是否完成替代审核状态。
状态示例:尚未开方、待分析、等待转写、分析中 0/2、已完成 1/2、已完成 2/2、需重试、资料不全、处方已变更、需完善患者关联。两模型完成数量与资料是否完整分别显示,避免把“两个请求成功”误读为“资料完整”。
可筛选“已完成、生成中、失败/部分完成、资料缺失、待医生查看”。列表批量返回状态摘要,避免每行各发请求;仅针对可见的进行中任务进行状态轮询,页面隐藏/关闭即停止轮询,后台分析仍继续。查看详情、翻页、刷新不得重复计费。
按用户追加需求,列表再增加“与 AI 一致度”列,单元格分别显示“千问 80% / OpenAI 75%”等对比结果(此处数字仅为展示示例)。默认口径为药味与剂量一致度,细节及不适用情况见第 14 节;不标为医生准确率。用法/疗程差异和需复核风险另有标识,不能由高百分比分数遮蔽。支持按单个模型一致度排序、筛选以及进入医生维度汇总。
### 8.2 AI 报告窗口
顶部固定显示:关联处方、处方版本、资料截止时间、生成时间、当前/历史/已失效状态、待复核提示。
窗口内容划分为概览、千问报告、OpenAI 报告、差异对照、资料来源、历史版本。宽屏可左右对照,窄屏用标签切换;药味列表使用可比较的表格,不把所有内容塞成长段落。
每个模型独立显示排队、读取资料、分析、校验、完成/失败。只提供真实阶段与计数,不编造精确百分比或完成倒计时。失败时可“仅重试此模型”,并说明原资料版本;需要纳入最新资料时使用“按最新资料重新分析”,两者不混用。
来源可追溯到某天医生备注、某张舌象、报告页、处方版本、聊天时间或通话转写时间点;点击仍校验来源权限。缺失文件不可显示成“已经分析”。
医生可标记已查看、需要补资料、不采纳及原因,添加独立复核备注;不修改模型原始报告。严重风险进入系统内负责医生/复核人员的待办或提醒,不自动向患者发送模型结果,不以异步提醒替代原有诊疗处置流程。
### 8.3 患者详情与接诊台
共享患者详情增加“AI 报告”页签,与截图中的病历、医生备注、日常记录、处方、视频、聊天并列。按处方和分析时间显示报告历史,可进入同一个共享报告窗口。
从接诊页已有患者级报告弹窗提取可复用展示组件,保留原患者纵向报告和新处方关联分析的类型标识。原接诊台自动生成路径不能又为同一次处方分析重复调用两个模型;通过报告类型及批次识别实现互通,不全盘替换无关的旧 AI 功能。
本次第一交付范围是 PHP 服务端 + 截图所示 Python 桌面端。Vue 管理端若需要同样的查看入口,可复用接口另行补齐;无需为实现截图需求先改小程序、患者端或重做整个后台。
## 9. 后台任务与数据设计
```mermaid
flowchart TD
A[医生保存手工处方] --> B[同一事务保存处方版本与分析事件]
B --> C[保存成功返回]
B --> D[后台校验关联及权限]
D --> E[等待必要转写与资料归档]
E --> F[固定同一份资料快照及附件清单]
F --> G[千问任务:隐藏本次人工方,生成独立候选及报告]
F --> H[OpenAI任务:隐藏本次人工方,生成独立候选及报告]
G --> I[结构与证据校验,独立保存]
H --> J[结构与证据校验,独立保存]
I --> M[冻结候选后计算与人工方的一致度]
J --> M
M --> K[列表百分比、报告与差异对照]
K --> L[医生查看与记录复核意见]
```
### 9.1 推荐的数据实体
以下为拟新增的逻辑实体,最终表名按项目规范确定,不是已实施数据库变更。
| 实体 | 主要字段和用途 |
| --- | --- |
| 分析事件/outbox | 事件唯一键、处方 ID、处方修订号、触发类型、触发人、诊单及稳定患者 ID、创建/消费时间 |
| 分析批次 | 关联事件、原处方版本、快照 ID、父批次、生成原因、权限范围指纹、配置版本、资料等待截止、当前有效性 |
| 资料快照及来源清单 | 资料截止时间、受保护的临床正文、来源版本/别名映射、临床内容哈希、附件内容哈希、提取版本、缺失项、同步水位 |
| 模型任务 | 批次 ID、模型键、执行状态、尝试次数、下次重试时间、租约 token/到期时间、心跳、实际耗时、内部错误码 |
| 模型结果 | 报告 JSON、候选建议 JSON、人工处方对照、引用校验结果、覆盖状态、模型/工作流版本、生成时间、使用量(上游可提供时) |
| 处方对比结果 | 人工处方修订号、冻结候选结果 ID、比较算法版本、药材字典/单位换算版本、阶段输入哈希、一致度/药味重合度、逐味差异、可比性状态、统计资格及原因 |
| 复核记录 | 结果 ID、查看/复核人、意见、状态、时间;与不可变模型结果分开 |
附件解析和模型分片结果可以有子任务/缓存,键中必须包含附件内容版本、解析器或模型版本、提示词版本和权限隔离范围。重用文件提取结果可以节省费用,但不能在不同患者或不同权限之间泄露数据。
推荐新建处方分析专属结果表,复用现有患者快照构建与展示能力;不要复用会覆盖旧版的诊单报告表。患者总历史可通过统一查询展示两种报告,不混用写入契约。至少建立事件唯一索引、`batch_id + model_key` 唯一索引,以及任务状态/下次执行时间、处方/患者/生成时间查询索引。
### 9.2 状态不是一个混合枚举
- 任务执行状态:等待资料、排队、运行、重试等待、成功、失败、取消。
- 批次汇总状态:进行中、两个成功、一个成功、全部失败、取消。
- 资料覆盖状态:完整覆盖、部分覆盖、关键资料缺失。
- 结果有效性:当前、资料已更新、处方已修改、处方已作废/删除。
- 医生复核状态:未查看、已查看、待补资料、已记录意见。
这些维度分开存储,例如“两个模型成功,但一份关键报告不可读”不能被压缩为一个绿色成功标识。
### 9.3 并发、去重和恢复
使用项目已有的数据库任务 + 常驻 CLI 消费模式,新建专用 AI worker。至少两个独立执行单元,建议按模型分配处理通道,使千问故障或积压不会阻止 OpenAI 开始。并行指提交及执行不依赖对方结果,不承诺供应商在同一毫秒开始计算。
事件按处方、单调递增修订号和触发类型去重;模型任务按批次和模型去重;重复客户端保存应有服务端识别的请求幂等键。内容哈希用于识别有无临床变化和审计,但不能把 A→B→A 的不同修订错误当成同一个业务事件。生成时间、临时签名 URL、查看次数等不进入临床内容哈希。
新增触发需与现有药房订单锁保持一致的锁顺序,并锁住处方的旧状态再判断首次转手工;唯一索引负责最终兜底。锁只覆盖本地短事务,绝不持有处方/订单锁等待模型响应。
快照构建使用一致性读取或可验证的来源版本水位。大资料分页抓取时核验记录版本,避免第一页与最后一页来自不一致状态;需要重试时重建整个快照,不能让两个模型各取一半旧、一半新。数据库读取结束后再执行附件和模型网络请求。
任务通过短事务原子领取,携带租约及所有者 token;长任务续租;保存结果时同时验证 token、当前处方版本、来源权限和批次有效性,避免旧 worker 覆盖新结果。中断后可恢复已完成的附件/分片步骤;无效旧版本结果即使返回也不得成为当前报告。
建议可重试错误为超时、临时连接故障、429、可恢复 5xx;按供应商 Retry-After 或依次约 30 秒、2 分钟退避,含首次调用总尝试最多 3 次,以上是待压测校准的默认参数。认证、权限、配置、明确不支持的附件等错误暂停并说明原因。格式不符最多一次受控修复,计入总次数和预算。
HTTP 超时不代表供应商没有完成或没有计费。提供商支持幂等键/任务查询时使用;不支持时记录调用结果不确定并限制重试。本方案保证本地任务与结果幂等,不承诺跨外部模型的绝对只计费一次。
成功模型不因另一模型失败再次生成;只有输入版本变化才新建双模型批次。重试旧批次始终用原快照,界面明确这是旧资料版本。
### 9.4 接口契约建议
采用独立处方分析接口,具体路由命名在实施时与项目约定对齐:
- 状态批量查询:只返回当前可访问处方的批次、两模型状态、覆盖和有效性摘要。
- 报告详情/历史查询:按处方、批次和模型分页读取已保存结果。
- 重新分析:创建新批次,返回任务标识及排队状态;不等待模型完成。
- 单模型重试:只针对可重试的失败任务,使用原快照。
- 复核意见:写入独立医生意见,保留模型原文。
- 来源查看:按内部来源引用查原记录,每次重新鉴权,不返回任意文件地址。
客户端不能提交模型地址、密钥、患者来源正文或替换服务端患者绑定。状态查询本身不触发任何模型调用。手工处方写入和分析权限独立:无 AI 权限不影响合法开方,但不能借自动任务扩大其数据访问范围。
## 10. 隐私、权限与可追溯性
后台运行保留发起者及授权范围;执行、重试、查看时均校验当前有效权限。需要以机构服务身份执行时也必须显式配置允许的业务范围,不能默认使用 root。跨部门转交或撤权后,旧报告包含超出新权限的资料时限制访问,必要时在新范围下重新生成。
发送给模型的是临床所需信息与去标识化来源别名,不需要手机号、身份证、住址、内部账号和签名。病历自由文本、附件内的身份信息也需处理;不能只删 JSON 的 name/phone 字段就认为已脱敏。应保留药名、剂量、检查值等临床信息。来源别名映射只在服务端保存,用于报告引用回查。
患者照片、报告、聊天仍是敏感医疗资料。启用范围应与机构既有患者告知、授权、供应商处理约定和实际数据地域匹配;不能把模型 API 已配置视作已获得全患者资料外发授权。模型是否用于训练、保存期限、删除机制、Dify 到供应商的真实传输路径须在上线检查中核实。
附件使用私有对象存储或服务端授权上传,临时链接有效期覆盖实际队列等待与读取时间;链接过期可更新访问凭证但不得更换快照对应的文件内容。文件抓取限定批准存储域,校验大小、类型和重定向,避免让病历中的任意地址变成后台抓取目标。
快照与结果采取访问控制、传输保护及适当的静态加密;原始病历、模型输入快照、临时 OCR 文件、运行日志分别设保留策略,按机构适用要求制定期限,不在本方案凭空指定统一天数。删除/撤回涉及原始医疗记录保留义务时走机构规则,不盲目级联删除医疗历史。
日志仅记任务 ID、模型键、阶段、耗时、用量和脱敏错误码,不记录完整提示词、病历、聊天、附件地址或密钥。报告中的建议要绑定来源别名并校验该引用存在;来源存在也不等于结论正确,仍保留医生复核。
病历、聊天、报告/OCR 内的文字均为待分析资料,不能当作系统指令执行。模型生成的内容同样按不可信输出处理:结构白名单、长度与数值校验、安全呈现,不自动执行其中链接或操作指令。
## 11. 历史处方、费用与上线策略
### 11.1 历史手工处方补生成
支持筛选日期、医生、部门、患者、是否已有同类型结果,先显示符合条件的数量及预算估计,再创建可暂停的补生成批次。历史补生成的优先级低于新开方任务,遵守同一去重机制,排除空白、作废、删除、缺关联或无权限记录。
推荐默认:新功能启用后的合格手工处方自动生成;存量暂不全量自动跑,可选择“近 30 天”或指定日期范围补齐。这是待用户确认的产品默认值,不意味着用户要求中的存量处方被忽略。历史范围确认后才执行补生成。
历史补分析默认使用生成时可获得的授权资料,标记“回顾性分析、资料截至本次生成”,不能伪装成当年的实时报告。若要求还原开方当时的判断环境,需先核实所有来源是否保存了历史版本;单靠 create_time/update_time 不能还原后来被修改的内容。
### 11.2 成本与耗时
费用包括两个模型的输入/输出、分片综合、各自视觉读取、文档 OCR 和有限重试。数据越多,调用次数越多;单份报告不会保证固定价格或固定秒数。本次没有调用真实模型,没有可据以承诺的生产耗时或价格。
提供每天/每部门/每患者的批次和用量预算、每模型并发上限、历史任务低优先级、超预算暂停及管理员查看。达到预算不静默丢资料,应显示等待预算或需要调整处理范围。
上线前以授权的代表性样本测试少量、长病程、多附件、大量聊天四类患者,记录 P50/P95、文件解析成功率和实际费用,再确定并发及时间预算。建议服务端事件登记额外耗时目标为 P95 小于 200ms、空闲队列下就绪任务约 10 秒内被领取;这些是待验证目标,不是现有系统能力或模型完成时限。
### 11.3 灰度与回退
先增加表与接口,部署 worker 并验证健康,再对少量医生/部门启用,最后逐步扩大。功能开关分别控制首次开方分析、临床修改后分析、晚到资料补分析、历史补生成、候选建议展示。关闭开关停止新调度并保留已有报告和可恢复任务;不要回滚删除历史结果表。
通过开方保存成功率、触发遗漏率、排队时长、单模型成功率、资料覆盖、重复调用次数、版本失效和医生复核反馈判断是否扩大。上线前核验实际双模型是否具备图片/文档能力,不用本地配置名称代替验证。
## 12. 实施顺序与验收
这是一项跨后台任务、资料处理和桌面展示的完整功能,不适合作为在保存按钮后追加两次 HTTP 请求的小改动。
| 阶段 | 交付内容 | 进入下一阶段的条件 |
| --- | --- | --- |
| A:契约及数据层 | 患者权威绑定、事件/批次/快照/任务/结果、模型及权限契约 | 假模型可演示完整事件流和版本关联 |
| B:资料和模型执行 | 全来源清单、附件解析分批、转写等待、两模型并行、独立重试和恢复 | 大资料不静默遗漏,两模型共享同一快照 |
| C:医生界面 | 列表状态及双模型百分比、共享报告窗口、患者详情入口、逐味差异、医生统计、来源、历史及复核 | 保存不等 AI、查看不重新生成、单模型先成功可看、百分比可复算且不误称准确率 |
| D:联调与灰度 | 授权样本验证、费用/时延校准、权限回归、告警与回退、选定历史补生成 | 临床与工程验收通过后扩大启用 |
首期必须完成 A–D 的核心闭环,转写晚到、附件完整性、权限和失败恢复不能作为上线后才补的基础缺口。医生把候选建议导入正式处方、第三模型裁决、直接面向患者发布报告、全院无限历史重算均不纳入默认首期。
验收至少覆盖以下场景:
1. 空白处方不触发;首次转手工和直接手工各自动产生一个双模型批次。
2. 系统空方 `patient_id=0` 能通过关联诊单正确定位患者;未关联和标识冲突有明确状态,无串患者。
3. 重复保存、并发编辑、接口重试只产生应有批次;有临床变化才产生新修订。
4. 保存不等模型;关闭客户端或服务端 worker 重启后,任务仍可恢复。
5. 两模型确实并发执行,千问失败不阻止 OpenAI;失败重试不会重新生成成功结果。
6. 同一批次两模型的输入快照哈希一致;生成中补资料不会混入其中一份。
7. 至少 4 张舌象、多页 PDF、扫描报告及不支持/损坏附件可验证分批覆盖;不存在“只发前三个却显示完整”。
8. 已归档聊天、未同步聊天、完整/部分/失败转写均准确表现;本次转写晚到只产生应有补充版本。
9. 药名与剂量单位、剂型、疗程、历史处方状态及资料引用都可核对;未知药名和缺数据不自动补默认值。
10. 药味、剂量或用法更改后旧报告明确过期;作废、删除、撤权发生在任务运行中时不展示为当前结果。
11. 不同医生/部门的患者、未挂诊单记录、来源文件及旧报告均通过真实行级权限测试。
12. 模型返回越权字段、伪造药材 ID、非法 JSON、超长文本、指令注入、错误来源引用时能拒绝或标明校验失败。
13. 候选建议不创建正式处方,不改变审核,不产生业务订单或药房任务,不引发自循环。
14. 页面加载、翻页、查看和刷新只查缓存/状态,不发模型请求;大量处方列表没有逐行查询放大。
15. 模型超时、429、认证失败、worker 崩溃、租约过期、事件登记失败和结果保存失败均有可恢复或明确终止路径。
16. 历史批量补生成可预览、暂停、恢复、去重,预算耗尽状态清楚,历史报告不冒充开方时实时分析。
17. 候选生成上下文不含本次人工方及其在备注/聊天/截图中的重复副本;无法保证独立性或存在未来资料时不进入医生独立比较统计。
18. 一致度由服务端固定算法复算:药味剂量完全相同为 100%,可比但完全无共同药味为 0%;空方、未知药名、缺剂量、不可换算剂型为不适用,不冒充 0%。
19. 重试、主动重新生成、临床修改、补充资料和算法升级不能重复增加统计样本,也不能自动选择对医生最有利的模型结果。
20. 两个模型分别显示百分比;单模型失败、资料不全和历史失效明确标注;高一致度时仍展示用法差异和需复核风险。
21. 医生汇总显示符合范围总数、有效比较数、不可比/缺失数及模型版本;无专家复核数据时不能产生“准确率”或复核合格率数值。
## 13. 需要用户确认的默认方案
| 决策 | 推荐默认值 |
| --- | --- |
| 自动生成时点 | 手工处方保存成功后即排队,待审核也生成 |
| 临床内容再次修改 | 自动生成新版本;未改变临床内容不重复调用 |
| 本次转写未完成 | 最多等待 5 分钟,之后出有缺口标识的初步分析;晚到后补充新版本 |
| 附件范围 | 所有授权临床附件分批处理,不默认只取近期几张;资源超限显式排队/提示 |
| 原始视频 | 使用每次归档转写,默认不把全部视频外发给两模型 |
| 候选用药方案 | 研究对照默认必须开方:资料不全也要各自给出候选方并注明假设与缺口;仍只进医生报告,不自动写正式处方 |
| 列表对比百分比 | 分别展示与千问、OpenAI 的药味与剂量一致度,点击看逐味明细;不称医生准确率 |
| 候选生成与计分 | 先隐藏本次人工方生成冻结候选,再由固定算法比较;不由 AI 自报百分比 |
| 医生统计 | 独立模型一致度、有效样本及覆盖率;有独立人工复核后才显示复核合格率 |
| 历史补生成 | 先启用新增;存量可选近 30 天或指定日期,默认不立即全量执行 |
| 页面范围 | 先服务端和截图桌面端;患者详情共用报告入口 |
| 通知 | 系统内完成/失败/需要复核提醒,不自动向患者发报告 |
用户确认此方案及需要调整的默认值后,才进入业务代码、迁移、测试和部署准备;本文件本身不授权访问生产患者数据或执行历史模型批量任务。
## 14. 新增:列表对比百分比与医生维度统计
### 14.1 指标名称与含义
响应用户“比较 AI 方与医生方,把百分比展示在列表上”的需求,新增双模型一致度。列表列名为“与 AI 一致度”,完整说明为“药味与剂量一致度”。它衡量医生方与指定模型候选方的结构接近程度,是本项目定义的可复算指标,尚未经临床效度验证,不能命名为“医生准确率”“诊断准确率”或“安全率”。
AI 本身可能产生错误和不完整的医学内容,也可能使使用者过度依赖模型判断;因此采用独立比较、明确指标边界及人工复核的设计。参考:[WHO 关于医疗多模态模型的指导说明](https://www.who.int/news/item/18-01-2024-who-releases-ai-ethics-and-governance-guidance-for-large-multi-modal-models)。一致度高可能只是双方相同,一致度低也可能来自不同但可接受的治疗思路;是否合理须依据临床资料和独立复核判断。
### 14.2 先独立生成,后对照
固定顺序为“保存人工处方及评价时点 → 两模型读取同一份不含本次人工方的临床资料 → 各自生成并冻结一个主候选方 → 程序计算百分比 → 展示并可追加对照解释”。不能先把医生处方交给模型,再把模型与医生的高度相同解释为医生水平高。
需要排除的不仅是本次处方表记录,还包括本次旧修订、草稿、复制到备注/聊天中的药味剂量、问诊转写中口述的拟用方、附件中的处方图片、旧 AI 评论以及包含这些信息的缓存摘要。屏蔽的是本次治疗方案,真正此前的历史用药保留;保存屏蔽清单、规则版本和检测结果。不能保证已屏蔽时标记“非独立对照”,不进入独立比较汇总。
模型调用会话与缓存按阶段隔离。千问已进入看到人工方的复核阶段时,不得污染尚在生成的 OpenAI 上下文;复核也不能修改已经用于计分的候选。技术重试采用事先约定的首个有效冻结候选,不从多候选、多次生成或两个模型里选择最高分。
### 14.3 时间公平与版本规则
用于医生汇总的基线取该次开方的首次有效人工提交版本,并记录是否已有 AI 建议展示/导入。已受 AI 辅助的处方单独标记,不能当作未经 AI 辅助的医生独立表现。统计单位为一个手工处方首次有效提交事件;后续修订与补充分析不增加该单位的样本数,真正新的问诊开方事件可以单独计数,并同时展示患者数及重复就诊情况。
基线只使用开方决策时已经存在且医生当时可获取的临床证据,记录事实发生时间、系统归档时间与来源内容版本。实现上在各来源落库时维护版本或可重建快照,开方事务只登记轻量版本水位,后台按水位重建,不能在开方业务事务里执行全量资料解析。尚无历史版本的来源标记为无法重建,不通过当前值猜测过去内容。
开方后的检查结果、服药效果、新症状、补写结论只能进入“最新资料补充评估”。晚到转写若能证明忠实记录了开方前的既有信息,可另建“开方时资料重建”评价,并排除口述本次拟用方;仍与原始基线分层展示,不能覆盖已有基线或重复计数。只凭记录创建时间不足以证明内容在当时已存在。
列表默认展示当前处方版本对应的最新可比结果,并附“独立基线/最新资料对照/AI 辅助后修订/非独立对照”类型和版本;医生统计默认只用符合基线条件的样本。用户打开最新资料报告看到的分数与基线不同,应可回查两者原因。旧处方版本分数不可挂在当前行而不标过期。
### 14.4 固定算法,不让 AI 自己打分
第一期采用药项等权的药味与剂量一致度,不把辨证文字相似、方名相似或模型自报置信度混入。这样每一分都能还原到具体药项,避免任意设置“辨证 40%、药味 30%”等未经验证的临床权重。
设医生与某个模型的规范药项集合分别为 D、A,药项数为 nD、nA。共同药项 i 的剂量已经换算为相同的、明确的剂型及剂量基准,分别为 di、ai。每个共同药项的匹配贡献为:
`ri = min(di, ai) / max(di, ai)`
列表百分比为:
`S = 100 × 2 × Σri / (nD + nA)`,其中只对 D、A 共同药项求和。
纯药味重合度作为详情中的辅助指标:
`H = 100 × 2 × 共同药项数 / (nD + nA)`
例:双方各有 10 个药项,其中 8 个相同且剂量一致,S 为 80%。如果这 8 个共同药项中有一个剂量相差一倍,该项贡献为 0.5,S 为 75%。这些数值仅演示公式,不代表任何医生的实际数据或临床正确概率。
规范化与边界要求:
- 药项键包含规范药材身份、炮制信息及主辅方角色,必要时增加给药路径/分组;别名由服务端药材字典映射,模型给出的 ID 不作为事实。
- 不擅自把生品/炮制品、同名异物或替代药判为相同。主辅方按固定语义匹配,不尝试寻找最高分排列;角色无法明确时标记不可比。
- 仅对同药项、同剂量基准且煎服语义一致的重复行合并,保存合并记录,不能通过拆行或重复列药改变分数。
- 比较时固定相同剂型和每剂/每日基准;单位换算必须使用机构确认规则。不能把饮片与颗粒、浓缩水丸按表面克数比较。更改基准或换算表必须产生算法配置新版本。
- 两边均有有效非空方案且完全没有共同药项,S 为 0%;全部药项、剂量一致为 100%。100% 仅表示本指标一致,不代表疗程、服法、风险或疗效一致。
- 空白处方、模型失败/弃答、风险暂缓、未知药名、缺剂量/单位、零/负数/非有限数值、剂型不可换算时 S 为 null,显示“—”及原因,不能显示 0%。双方都空也不能给 100%。明确建议暂不使用药物时单列“用药决策差异”,不当作模型生成失败或空方硬算。
- 不能剔除未知或不可比药项后冒充完整分数。仅药名可比时可显示明确标注的 H,但不得顶替列表 S 或混入 S 的医生汇总。
- 保留原始规范化结果、每项贡献、分母、人工/AI 版本、算法与字典版本;展示可四舍五入为整数,详情可看一位小数,汇总使用未舍入值。
服法、给药途径、疗程、主辅方使用安排和配伍/过敏等风险独立列出。各药项等权只是工程比较口径,不能反映某一味药可能具有的重大临床影响;即使 S 很高,有需复核风险也必须保持独立提醒,不用绿色“优秀/合格”覆盖风险。
### 14.5 列表与对比详情示例
下表为界面示例,非真实处方数据。
| 处方 | 千问一致度 | OpenAI 一致度 | 比较说明 | 操作 |
| --- | --- | --- | --- | --- |
| 示例处方 A | 80% | 75% | 独立基线;有疗程差异 | 查看逐味对照 |
| 示例处方 B | 62% | — | OpenAI 未完成 | 查看已完成对照 |
| 示例处方 C | — | — | 剂型不可直接换算 | 查看原因 |
紧凑列表中可将两模型放在同一个单元格上下两行,保留模型名。鼠标悬停解释分数范围、比较类型、资料截止时间和可比性。两个模型之间也可计算相同口径的一致度,在详情呈现模型意见分歧,不以两模型平均或最大值冒充统一医生评分。
点击百分比进入逐味表格:规范药名/炮制、医生剂量、模型剂量、单位和基准、增减药项、匹配贡献;服法、疗程和风险另列。资料不全但仍有可计算结构时,只作为明确标注的辅助对照,默认不进入基线汇总。
列表排序和筛选使用服务端已保存的未舍入数值,按指定模型操作;null 不当成最低分,过期结果不混入当前排序。仅因“分数低于某数”不直接标记医疗错误;可用于安排人工复核,阈值作为机构工作流配置而非医学正确性界线。
### 14.6 医生统计和实际处方质量评价
新增有权限控制的医生维度统计,可按医生、部门、开方日期、病种/初复诊、剂型和模型/算法版本查看:
- 范围内合格开方事件总数 N、涉及患者数、每模型有效基线比较数 n,以及覆盖率 n/N。
- 分别对千问和 OpenAI 计算一致度均值、中位数及分布;同一病例两模型配对比较时只使用两者均有效的共同样本,并给出共同样本数。
- 单模型失败、资料不足、不可换算、非独立对照、未来信息、AI 辅助后改方等排除原因和数量。排除会影响代表性,不能隐藏后只展示成功样本。
- 单独展示修改前后、最新资料补充和开方时资料重建视图,不混入首次独立基线。
不同病例构成、重复患者及模型版本会影响统计,默认按相近条件分组;小样本显示样本不足,不给医生贴“准确/不准确”标签或默认做绩效排名。更换模型、提示词或算法后分层展示,不能静默覆盖旧分数。医助代操作时归属开方医师,操作人另外记录,不能把录入人员当成开方医生。
若用户要进一步了解医生处方质量,增加独立的“专家复核合格率”,与 AI 一致度并列:
`专家复核合格率 = 合格处方数 / 已完成且可评价的复核处方数 × 100%`
由机构先固定复核标准、随机/分层抽样规则和争议裁决流程;复核者先看当时临床资料及人工方,尽可能隐藏医生身份、AI 候选方和 AI 分数。复核结论为合格、需修改、不合格、不可评价;可评价分母包含前三类,只有合格进入分子,不可评价及未完成数量单列。不能只挑低分或高分病例抽样后当作全体合格率;风险定向复核与代表性抽样结果分开展示。
汇总同时显示复核数、抽样覆盖、置信区间及争议状态;重复患者的相关性要纳入统计方法。尚无独立复核数据时显示“未建立复核样本”,不把 AI 一致度填入合格率。随访疗效和不良反应可作为另一个结果维度,但受病情、依从性和其他治疗影响,不能直接换算成医生准确率。
第一期交付列表百分比、逐味差异和医生一致度汇总;专家复核提供结构化记录与统计入口,实际复核标准、人员及样本需由机构落实,系统不会自行生成专家结论。
## 附:实施定位索引
以下行号对应本次调研工作区,用于后续实施定位。
- [手工处方新增](/D:/web/zyt/server/app/adminapi/logic/tcm/PrescriptionLogic.php:251)、[编辑入口](/D:/web/zyt/server/app/adminapi/logic/tcm/PrescriptionLogic.php:397)、[来源转手工](/D:/web/zyt/server/app/adminapi/logic/tcm/PrescriptionLogic.php:529)、[系统空方患者字段](/D:/web/zyt/server/app/adminapi/logic/tcm/PrescriptionLogic.php:1141)。
- [患者上下文入口](/D:/web/zyt/server/app/adminapi/logic/tcm/PatientAiReportLogic.php:284)、[全病程聚合](/D:/web/zyt/server/app/adminapi/logic/tcm/PatientAiReportLogic.php:417)、[患者并集范围](/D:/web/zyt/server/app/adminapi/logic/tcm/PatientAiReportLogic.php:500)、[现有报告契约](/D:/web/zyt/server/app/adminapi/logic/tcm/PatientAiReportLogic.php:1087)、[历史授权](/D:/web/zyt/server/app/adminapi/logic/tcm/PatientAiReportLogic.php:1354)。
- [现有诊单双报告顺序调用](/D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:865)、[草稿解析](/D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:594)、[草稿提示词](/D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php:1525)。
- [附件数量配置](/D:/web/zyt/server/config/prescription_ai.php:26)、[图片适配](/D:/web/zyt/server/app/common/service/DifyChatService.php:397)、[附件截断](/D:/web/zyt/server/app/common/service/DifyChatService.php:440)、[无附件回退](/D:/web/zyt/server/app/common/service/DifyChatService.php:487)。
- [服务端转写收尾](/D:/web/zyt/server/app/adminapi/logic/tcm/DiagnosisLogic.php:1996)、[视频客户端归档调用](/D:/web/zyt/app/src/doctor_workstation/video/lifecycle.py:636)。
- [已开处方页](/D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:999)、[保存流程](/D:/web/zyt/app/src/doctor_workstation/ui/pages/prescriptions.py:1757)、[共享患者详情](/D:/web/zyt/app/src/doctor_workstation/ui/dialogs/diagnosis.py:381)、[患者报告弹窗](/D:/web/zyt/app/src/doctor_workstation/ui/pages/reception.py:3114)。
- [可借鉴的任务领取方式](/D:/web/zyt/server/app/common/service/qywx/QywxPromotionAutomationStore.php:103)、[常驻命令模式](/D:/web/zyt/server/app/command/QywxWorkPromotionAutomation.php:13)。
## 15. 2026-09-10 变更:研究对照下必须开出候选方
用户确认本功能用于医学研究对照,要求即使资料不全也必须让两个模型各自开出处方,再与人工方比较。据此调整:
- 服务端不再因缺少年龄、性别、过敏史、当前用药、妊娠哺乳等关键事实而把候选方改写为空方案。这些缺口继续逐项显示在资料缺口和候选方风险提示中,覆盖状态仍为不完整。
- 提示词改为研究对照口径:必须输出 `available_for_review` 候选方,未知信息按最保守假设处理并逐条写明假设、缺口与复核点;不得编造患者事实、检查数值或用药依据。
- 模型仍拒绝开方时,携带其拒绝理由重新追问,默认最多 2 次;仍拒绝则整项任务以 `CANDIDATE_WITHHELD_BY_MODEL` 失败并可重试,拒绝结果不进缓存,不显示为“已完成”。
- 原“关键安全信息缺失即暂缓候选方”策略保留为配置项 `prescription_ai.manual_analysis.require_candidate=false`,默认不启用。
- 未改变的边界:候选方仍不写回正式处方、不签名、不提交审核、不产生订单或药房任务;一致度仍由服务端固定算法计算,不由模型自报;医生统计口径不变,被迫在关键事实缺失下生成的候选方所对应的比较结果仍按原有排除规则处理。
+280
View File
@@ -0,0 +1,280 @@
# 手工处方双模型分析:实施与部署
本地实现日期: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`(合成数据,无网络)。
@@ -0,0 +1,59 @@
# 处方双模型实现审查
审查日期:2026-09-09。范围:Store、Worker、Logic、Controller、Request、PrescriptionLogic 保存钩子、迁移及其与生成器/比较器的接口。审查时主实现仍在修改;以下行号对应本轮读取,修复后应按方法名复查。未修改业务文件,未访问数据库、生产服务或 `.env`
## 已修复问题(保留审查记录)
以下六项均已在本轮合入修复:Worker 解码原始 JSON 后比较;资料时钟退出变化指纹;冻结来源权限清单单独加密,并在读取、模型分片调用和发布前重验;有效基线不再写入排除原因;生成器统一覆盖状态;比较器识别 `instructions` 和明确无额外炮制,并拒绝未知炮制与煎服语义冲突的重复行。
字典已改为在准备批次时冻结完整有效目录与全局内容哈希,两模型共用。`PrescriptionAiPipelineTest.php` 覆盖原始数据库 JSON 到 100% 的完整链路、两分支之间目录变化、权限中途撤销;`PrescriptionAiPolicyTest.php` 覆盖炮制与重复煎服差异;队列测试覆盖真实保存回滚和有效统计输入。以下内容记录修复前的发现,不表示这些问题仍然存在。
1. **[P1] 原始数据库药味 JSON 直接送入比较器,全部医生方不可比。**
- 位置:`server/app/common/service/prescriptionai/PrescriptionAiWorker.php:74``:88``PrescriptionAiStore.php:101`
- 冻结处方来自 `Db::name(...)->find()``herbs` 是 JSON 字符串。Worker 仅在收集药名时临时调用 `Policy::decode`,实际传给 `compare()``$doctor['herbs']` 仍为字符串;比较器要求数组,返回 `empty_prescription`
- 修复:构造专用比较输入,先解码 `herbs``aux_usage` 等 JSON 字段;保留冻结原文。增加“原始数据库行 → Worker 输入 → 100%”集成测试。
2. **[P1] 资料指纹包含读取时间,无资料变化也会自动重生成。**
- 位置:`server/app/common/service/prescriptionai/PrescriptionAiContext.php:296``:299``PrescriptionAiWorker.php:188`
- `source_hash` 对包含 `cutoff_at=time()` 的整个 source 哈希。`refreshSources()` 每次重建都会看到新指纹,产生新批次并失效旧报告,直至日预算耗尽。
- 修复:将稳定资料内容/版本/可用性指纹与冻结快照哈希分开;变化检测排除扫描时间,快照仍保存截止时间。验证同内容不同扫描时间不入队,新增或修改资料只入队一次。
3. **[P1] 读取报告只核验诊单范围,漏掉生成时的聊天/通话员工权限。**
- 位置:`server/app/adminapi/logic/tcm/PrescriptionAiLogic.php:335``server/app/common/service/prescriptionai/PrescriptionAiContext.php:131`
- Context 除诊单可见外还通过 `sourceStaffAllowed()` 限制聊天和通话归属;`visibleBatch()` 仅核验处方、诊单 ID。具体场景:医生生成包含本人聊天/通话的报告,同诊单医助拥有处方和诊单访问权,可读取该报告,但按 Context 的员工规则,该医助自己构建资料时不能读取医生的这些原始来源。
- 修复:冻结逐来源权限清单,报告/列表摘要/统计入口对当前读者重新核验全部来源范围。短期可保守限制为原生成者且权限范围未变,或具有覆盖全部冻结来源的明确授权;不能用“同诊单”替代员工归属核验。需增加“同诊单但不同员工消息范围”的拒绝访问测试。
4. **[P1] 合格基线也写入排除原因,统计有效数永远为零。**
- 位置:`server/app/adminapi/logic/tcm/PrescriptionAiLogic.php:228`
- 当前表达式在 `$eligible=true`、结果成功且无其他排除原因时仍填 `incomplete_coverage`。Statistics 对任何非空排除原因都拒绝纳入,正确分数也无法成为有效样本。
- 修复:合格时 `exclusion_reason=''`;不合格时区分资料不足、失败、不可比与基线条件。向统计输入传递 `comparison_reason_code`,避免把“剂型不可换算”等原因一概归为覆盖不足。
5. **[P2] 覆盖字段契约不一致,所有结果被持久化为 partial。**
- 位置:`server/app/common/service/prescriptionai/PrescriptionAiStore.php:318``PrescriptionAiGenerator.php:174`
- Generator 输出 `coverage.complete``coverage.source_complete` 布尔值,没有 `coverage.status`Store 读取后者并默认 partial,完整结果也无法显示完整或进入完整覆盖基线。
- 修复:统一严格覆盖契约,由已有完成布尔值、逐来源与附件清单确定持久化状态;未知值保持 partial。添加完整/部分覆盖输出到数据库字段的离线集成测试。
6. **[P1] 生成器与比较器的炮制/煎服字段不一致,既可误报 0% 也可误报 100%。**
- 位置:`server/app/common/service/prescriptionai/PrescriptionAiGenerator.php:307``:405``PrescriptionAiComparison.php:37``:196``:247`
- Generator 要求 `processing` 非空,提示模型填写“明确无”;医生旧数据及当前字典查询不带 processing,比较器归一化为 `''`。同一药名同剂量、模型写 `processing='无'` 时两个药项键不同,结果变成 0%。
- Generator 的特殊煎服字段为 `instructions`,比较器只读取 `usage_instruction/decoction_instruction/special_usage/...`,会忽略它。将一味药拆成“先煎 4g + 后下 6g”,对照 10g,现返回 100% 且无用法差异。
- 修复:统一端到端字段;将 `instructions` 纳入严格重复行合并语义及用法差异。明确定义“无额外炮制”的规范值,并与服务端药材字典衔接;“未知”不得当作“无”,生/炙等真实差异不得抹平。
## 已知字典版本问题及修法
`PrescriptionAiStore.php:332` 当前对整份病例 normalization 哈希作为 dictionary_version,导致每个病例独立分层。主实现者已知并在修复。**只替换为 `normalization.dictionary_hash` 仍不足**:Worker 当前仅查询本病例出现药名的子集,同一版机构字典下不同病例的子集哈希仍不同。
建议使用机构发布的完整药材字典修订号;若暂无版本表,对完整有效目录的规范字段(id/name/aliases/processing/status,以及未来批准的换算规则)稳定排序后计算全局内容哈希并缓存。首次冻结批次时固定字典版本及对应映射,两模型复用;病例匹配子集哈希另留审计,不作医生统计分层键。不要把库存、价格或查询时间等与药材身份无关的字段纳入版本。
## 离线验证与边界
通过 standalone PHP 纯函数复现,无框架初始化:
| 输入 | 当前结果 |
| --- | --- |
| 医生 herbs 为原始 JSON 字符串 | `empty_prescription` |
| 同药同剂量,医生无 processing,候选 processing 为“无” | `score=0` |
| 同药拆成不同 instructions 的 4g/6g,对照 10g | `score=100``usage_differences=[]` |
| baseline_eligible=true,但 exclusion_reason=incomplete_coverage | `valid_count=0` |
静态检查确认新增保存及作废钩子运行于现有事务保护内,Request 的预占/完成键位于新增处方事务,task/result 唯一键及租约 token 可防止一般重复落库。本轮未执行数据库事务、并发领取、迁移实跑,因此不把这些静态观察当作运行验证。现有 Context 明确因历史来源版本不可重建排除独立基线,零合格样本应如实展示;不能为了让统计出现数值而将该条件改成 true。
@@ -0,0 +1,30 @@
# 处方 AI 处理进度
## 目的与显示口径
已开处方列表和双模型报告窗口展示后台实际步骤,帮助医生区分资料等待、模型响应较慢、已完成和失败。千问与 OpenAI 独立展示进度,单个模型失败不掩盖另一模型仍在处理。
流程包括:准备资料、等待问诊资料归档、排队、文字资料分析、附件处理、资料汇总、生成报告、校验结果、处方对比、完成。附件组数表示已结束处理的组数,其中可能包含明确记录为不支持或无法读取的附件;不等同于已读完全部附件。
已耗时来自服务端时间,界面用单调时钟补充秒级变化;模型响应耗时与最终完成时间不能可靠预估,因此不提供虚假的总进度百分比或预计完成时间。分组计数只属于当前步骤。转写等待和自动重试仅在后台提供明确截止时间时展示倒计时。
## 数据与权限
进度摘要仅包含固定阶段、计数、时间及系统说明,不包含患者正文、附件地址、模型回答、提示词或密钥。列表查询使用小型摘要,避免每次轮询解密完整模型缓存。
读取沿用处方与资料权限。完成状态以报告及对比结果实际落库为准。模型返回不等于整项任务完成;重试、失效及旧报告都不能继续显示为实时生成中。断网时显示最近收到的进度,并明确提示刷新失败。
## 验证与本地更新
覆盖分组进度、模型等待、倒计时、双模型独立状态、失败与完成状态、旧接口兼容、断网恢复、窗口关闭、敏感字段过滤及结果提交前的状态。队列与迁移测试使用项目内独立空 MySQL 数据目录,禁止连接业务库进行测试。
本地升级先确认新增进度字段可用,再在消费者空闲时加载新代码。正在执行的旧消费者保留到任务结束;旧任务没有保存的分组明细不补造。客户端源码及本地启动包完成验证后更新,用户当前打开的窗口保留到自行重启。
## 2026-09-10 本地验证记录
- 后端:38 项进度检查、Generator 回归、252 项比较器检查和 20 项策略检查通过。独立 MySQL 上新版结构 Queue 68 / Pipeline 80、旧版结构 Queue 66 / Pipeline 75 检查通过,包括重复迁移及旧库回退。
- 桌面:122 项测试通过,Ruff 通过。使用合成资料生成的 `app/artifacts/issued-prescription-ai/progress-report-20260910.png` 已目视检查。追加验证失败最后步骤说明、重试等待耗时冻结、真实尝试次数和报告阅读位置保留。
- 本地应用数据库已执行 `server/database/migrations/2026_09_10_prescription_ai_progress.sql`。迁移只增加可空摘要列,执行前后的任务状态、尝试次数和结果数量一致;记录见 `artifacts/prescription-ai-runtime/progress-migration-20260910.json`
- 已等全部活动任务自然结束,再于 10:27 重启三个本地消费者。记录见 `artifacts/prescription-ai-runtime/progress-worker-reload-20260910-102729.json`。真实权限接口只读校验通过,状态接口返回进度,未生成新的模型请求或重写历史报告。
- 当前旧批次的进度摘要为空,显示已保存的结束状态和耗时;新执行任务才记录完整分组阶段。本次验证保留实际千问格式校验失败、OpenAI 超时状态,不将其标记成功。
- 进度版 Windows 1.4.2 包已通过冻结模块、真实计数/重试计时、媒体组件及入口检查,于 10:31 切换到 `app/dist/DoctorWorkstation`。EXE SHA-256 为 `410cf8ecacba379f8c292856d2c636c54c9f6e6e814808ce7ea59f4a92fcf945`;旧包保留在 `app/dist/DoctorWorkstation-before-progress-20260910-103105`。详情见 `app/artifacts/issued-prescription-ai/local-package-build-progress-20260910.md``local-package-promotion-progress-20260910.json`。当前客户端会话未关闭,需自行重启以加载界面更新。