Technical Design · 救援进程报告(续报)

救援进程报告(续报)技术设计

模块 projects/supplement_report/ · 独立自包含子项目 · 业务产物《关于XX事故受伤人员救治情况的报告》

数字确定性 LLM 仅润色导语+基本情况 SSE 流式 DashScope · qwen-plus 冻结集回归通过 · L2 链路有效(非泛化 KPI) 零依赖 yingji

救援进程报告(续报)技术设计

模块:projects/supplement_report/(独立自包含子项目)
业务产物:《关于XX事故受伤人员救治情况的报告》
文档定位:Markdown 技术母版;架构图使用 Mermaid 编码。

1. 设计要点

续报把同一事件在某一时点的 HIS 救治状态冻结成可追溯快照,按正式公文模板生成报告。核心原则:数字确定性、不可编造;大模型只做语言润色,不创造事实。

  1. 三、下一步工作留空——不生成任何内容,仅保留段标题。
  2. 只有「开头导语 + 一、基本情况」走大模型润色;标题、二、伤员收治明细、落款全部用 Python 模板确定性拼接,不过大模型。
  3. 所有写进报告的数字(收治总数、死亡、存活、伤情分级、科室数)由 facts.py 确定性计算后注入 LLM prompt,LLM 只做语言组织、不增删数字。
  4. 统计走 HIS 院内口径:收治按 hisPatientId 去重,死亡按出院状态判定,分级由预检分诊级别映射。
  5. 模块自包含:LLM 客户端、统计、生成、测试集、评测全部在 projects/supplement_report/ 内,零外部项目依赖。

1.1 产品需求主流程(业务视角)

flowchart TD
    A[事件信息 + HIS 伤患列表] --> B[facts.compute_facts 确定性统计]
    B --> C{数字 + 客观事实}
    C --> D[LLM 润色: 开头导语 + 一、基本情况]
    C --> E[模板拼接: 标题]
    C --> F[模板拼接: 二、伤员收治明细]
    C --> G[模板拼接: 落款]
    D --> H[组装 markdown]
    E --> H
    F --> H
    G --> H
    H --> I[三、下一步工作 留空]
    I --> J[完整续报]

1.2 一句话定位

把 HIS 院内救治状态冻结成可追溯快照,确定性渲染全部数字与明细,仅让大模型润色两段叙述性文字(导语、基本情况),下一步工作不生成。

2. 需求与源材料

2.1 依据

序号 依据 位置
1 PRD 数据源总览 refs/HXdKwkWpxiwvODkqKcGcsXCRnSA.md:79
2 续报 PRD(进程续报) 同上 :140-150
3 续报模板 refs/.../突发事件紧急医学救援信息报告.docx
4 续报映射说明 refs/.../续报映射说明.xlsx
5 续报金标准示例 refs/.../续报示例-通州坍塌事故续报.docx
6 事件信息示例 refs/.../应急指挥平台-事件信息数据示例.xlsx(55 条)
7 紫云伤患示例 refs/.../紫云接口-伤患数据示例.json(30 事件)

2.2 源材料已确认的事实

2.3 口径隔离

紫云伤患是院前/现场口径(检伤/转运/初步处置),缺续报所需的 HIS 院内字段(科室、入院时间、出院状态)。续报走 HIS 院内口径。测试集构建时,紫云伤患经诊断关键词归科 + 事件死亡数分摊,合成为 HIS 院内伤患(见 §4)。生产环境应直接消费 BS310/BS313/预检分诊消息,不经此合成步骤。

3. 正式报告结构

关于{市+区}{事件名}受伤人员救治情况的报告

{开头导语 —— LLM 润色}

一、基本情况
{截至时间 + 收治科室 + 收治总数 + 死亡/存活 + 伤情分级 —— LLM 润色,数字注入}

二、伤员收治情况
(一){科室}:收治N名(危重症X人、重症X人、轻症X人)
1.{姓名},{性别},{年龄}岁,{分级},伤情诊断:{诊断}。
2.……
(二)……
(N)死亡伤员:M名
1.{姓名},{性别},{年龄}岁,死亡,伤情诊断:{诊断}。

三、下一步工作
{留空}

{市+区}卫健委
{生成时间}

3.1 各段生成方式

段落 生成方式 数据来源
标题 模板拼接 事件 districtsName + eventName
开头导语 LLM 润色 事件时间 + 地点 + 事件名
一、基本情况 LLM 润色 facts 确定性数字
二、伤员收治情况 模板拼接 facts 科室聚合 + 逐人明细
三、下一步工作 留空 —
落款 模板拼接 districtsName + 生成时间

4. 测试集构建

testset/build_cases.py 从数据源总览合成 6 个 case:

case 事件 患者 死亡 科室
event09 宜昌客车坠桥 22 8 2
event21 郑州地铁水淹 31 14 2
event25 吉林化工厂爆炸 82 8 2
event27 衡阳火灾 26 20 2
event29 泉州酒店坍塌 53 29 2
tongzhou_collapse 通州建筑坍塌 20 2 3

选案原则:有死亡 + 有紫云伤患 + 灾种多样(坠桥/水淹/爆炸/火灾/坍塌)。tongzhou_collapse 有 docx 金标准可对照。

每个 case 产出:

5. 统计口径(facts.py)

5.1 统计公式

admission_total = len(按 hisPatientId 去重后的伤员)
death_total     = 伤员中 dischargeStatus == "死亡" 的数量
survivor_total  = admission_total - death_total
tier_counts     = 存活伤员按预检分诊映射计数(危重/重症/轻症)
department_list = 存活伤员的 department 去重排序

5.1.1 冲销与隔离预处理(PRD:144 / PRD:147)

去重后、统计前,按业务含义对上游已解析的标记做三步预处理:

(0) BS313 退院(cancel)冲销:dischargeCancel == True 的就诊登记被撤销 → 不计入收治/死亡/分级
(a) BS321 出院召回冲销:recallStatus == "recalled" 的患者出院效果撤销
       → dischargeStatus 强制改回 "在院"、清 dischargeTime / dischargeDisposition
       → 重新参与科室聚合与存活分级,死亡数 -1
       → 记入 recall_corrected,missing_fields 备注「出院经BS321召回,已按在院校正」
(b) 未匹配伤员隔离:isMatched == False 的患者 → 不计死亡统计、从存活分级/科室剔除
       → 单独收集到 unmatched_patients,明细 tier / 诊断 / 科室标「待核实」
       → missing_fields 备注「未匹配,已标待核实,不计入院内死亡统计」

预处理后:admission_total = len(已匹配且去重的伤员),未匹配伤员单列不进可回溯总数,保证数字闭合可追溯。

5.2 预检分诊映射

预检分诊级别 续报伤情档
I / 危重 危重 → 危重症
II / 重症 重症 → 重症
III、IV / 轻症、轻伤 轻症 → 轻症

死亡伤员优先标记为「死亡」,不重复计入存活分级。

5.3 科室分组

存活伤员按 department 分组,每科室产出小计文本 + 逐人明细行。死亡伤员从科室明细剔除,单列「死亡伤员」组。

5.4 出院去向标注(PRD:121 BS313 出院状态/病人去向)

存活伤员按 BS313 出院状态标注去向,附在明细行末尾:

dischargeStatus dischargeDisposition 标注
在院 / 空 — 不标
出院 含「转…院/医」 (已转院)
出院 其它(居家休养/治愈等) (已出院)
死亡 — 归死亡组,不标

示例:1.郑X,男,55岁,危重症,伤情诊断:重度烟雾吸入性损伤、气道烧伤。(已转院)

6. 端到端技术架构

6.1 哪些环节调用大模型

flowchart LR
    subgraph 确定性["确定性(Python)"]
        IN[事件信息 + HIS 伤患列表]
        R[(0) BS313 退院冲销
(a) BS321 召回冲销
(b) 未匹配隔离] F[facts.compute_facts 确定性统计] T[标题/明细/落款模板] end subgraph LLM["大模型(仅润色)"] P[polish: 导语+基本情况] end IN --> R --> F F -->|数字+事实注入| P F --> T P --> G[generator.generate 组装] T --> G G --> OUT[完整续报 markdown] G --> V[store.save_version 版本快照]

只有 1 次大模型调用:润色导语 + 基本情况。其余全确定性。facts 前置冲销/隔离预处理,确保数字可回溯。

6.2 LLM 边界(system prompt 铁律)

  1. 只能使用给定事实,不得增删、修改、编造任何数字。
  2. 不得加入未提供的事件细节(伤员姓名、诊断、医院名)。
  3. 不得生成二段、三段、标题、落款。
  4. 严格按 JSON 输出(lead / basic 两 key)。
  5. 数字逐字采用,语言润色。

6.3 失败回退

LLM 返回无法解析(非 JSON / 缺字段)时,回退到确定性拼接文案(_fallback_lead / _fallback_basic),保证报告可生成、数字仍正确。评测验证过这条回退路径。

6.4 组件职责

组件 文件 职责
LLM 客户端 src/supplement_report/llm.py DashScope/OpenAI 兼容,关 thinking,读 .env
统计 src/supplement_report/facts.py 确定性统计 + 冲销/隔离预处理,模板真源
生成 src/supplement_report/generator.py 组装三段,润色+模板+留空,待核实/待补标注
版本快照 src/supplement_report/store.py 本地 JSON 持久化,fcntl 锁,历史不可覆盖
服务 src/supplement_report/app.py FastAPI + SSE,含版本路由
CLI src/supplement_report/cli.py 命令行入口
测试集 testset/build_cases.py 从数据源合成 case + 金标准
评测 eval/evaluate.py 冻结集回归 + mutation;eval/llm_judge.py 另设测量可靠性门槛
版本验证 eval/verify_versions.py 版本链/历史不覆盖/diff 端到端验证

7. LLM 配置

8. 服务接口(SSE 流式)

src/supplement_report/app.py 提供独立 FastAPI 服务,端口 8003。

8.1 接口

方法 路径 说明
POST /api/supplement_report/generate SSE 流式生成:步骤事件 + 最终报告(含 versionId/versionNo/prevVersionId)
GET /api/supplement_report/presets 预设 case 列表(8 个,含完整 payload)
GET /api/supplement_report/health 健康检查
GET /api/supplement_report/versions/{eventId} 列出该事件所有历史版本摘要
GET /api/supplement_report/versions/{eventId}/latest 取最新版本完整报告
GET /api/supplement_report/versions/{eventId}/{versionId} 取指定版本完整报告
GET /api/supplement_report/diff/{eventId}?from=v1&to=v2 两版本数字演进对比
GET /docs /report /ppt 技术设计 / 工作汇报 / 一页 PPT 页面

8.2 SSE 事件序列

sequenceDiagram
    participant C as 前端
    participant S as 8003 服务
    C->>S: POST /generate (eventInfo+patients)
    S-->>C: event:step 1 running 解析事件与伤患
    S-->>C: event:step 1 done + facts(确定性统计)
    S-->>C: event:step 2 running 大模型润色
    S->>LLM: polish(导语+基本情况, 数字注入)
    LLM-->>S: {lead, basic}
    S-->>C: event:step 2 done + source(llm/fallback)
    S-->>C: event:step 3 running 模板拼接
    S-->>C: event:step 3 done
    S-->>C: event:report + 完整 markdown
    S-->>C: event:done + elapsedMs

LLM 调用用 asyncio.to_thread 包装同步客户端,不阻塞事件循环;解析或守门失败自动回退确定性文案,source="fallback",具体原因写入 reason。

8.3 启动

source .venv/bin/activate
PYTHONPATH=projects/supplement_report/src \
  python -m uvicorn supplement_report.app:app --host 0.0.0.0 --port 8003

demo/run.sh 已集成 8003 启动段(SUPPLEMENT_V2_ENABLED=0 可跳过)。

9. Demo 体验页

demo/static/supplement_report.html + demo/server.py 的 /supplement_report 路由。

9.1 一键启动

bash demo/run.sh   # emergency:8000 + duty:8001 + supplement_report:8003 + demo:8080
# 浏览器打开 http://<本机IP>:8080 → 续报 v2 演示

10. 评测

10.1 评测维度(13 维纯检查器 + GLM-5.2 独立语言质量 judge + mutation)

eval/evaluate.py 为纯检查器(check_result 接收既有 result,不内部调生成器,可对 mutation 样本独立评分)。118 例冻结集当前回归通过;这只是发布门禁,不是未知分布泛化率。维度按 oracle 等级拆分(同口径 derived / 独立 independent),见 experiments/REPORT_supplement_report.md §2 全表。语言质量不在硬指标表——自动规则只能叫"语言合规性"(无占位符/含"报告如下""收治"等词法检查),真正的语言质量由 eval/llm_judge.py 盲审,并先经过裁判输出可靠性门槛。

维度 检查项 oracle
数字可回溯(同口径回归) facts 收治/死亡/存活/分级/科室数 == 同口径金标准 derived
数字可回溯(独立·文本标签) 最终报告文本"死亡N/存活N/分级N/科室N"标签级精确匹配 independent
代数闭合 收治=死亡+存活;存活=三级合计;tier_pending≤轻症 independent
结构完整性 含标题/导语/一/二/三段/落款 derived
段落注入防御 lead/basic 禁含换行/Markdown标题/二·三段标题 independent
行政区划去重 标题/落款不得出现"省省/市市/区区"重复 independent
下一步留空 三段内容为空 derived
LLM 未越界 润色段无越界大数字;标签数字∈facts;0值不编造/非0必含待核实 independent
全明细评分 科室列表/小计/逐人行序号+行数/死亡组/未匹配组/出院去向 independent
语言合规性 无占位符X、含"报告如下"/"收治"(仅词法,非质量) derived
异常处理 召回校正/退院撤销/未匹配隔离/出院去向标注 independent
polish 可观测 记录 source(llm/fallback) 与 reason,报告 fallback 率 independent

普通 case 19+ 项;召回/未匹配/出院去向场景动态扩展(含异常处理与全明细子项)。mutation 测试对每例生成篡改变体(删明细行/换数字/注入段标题/全死亡造"等0个科室"/全死亡漏放死亡总数/科室名全错/as_of 编造时间),验证 evaluator 能检出(297/297 检出,N/A 无效变异 411 不计分母:全死亡/零收治 case 跑部分 mutation 属 no-op,非漏洞)。

10.2 结果

118 例冻结集回归通过(最近一次真实模型运行 110 / 安全回退 8)
mutation 检出 297 个有效篡改(N/A 411)
GLM judge:measurement valid(冻结集自动评审,未人工校准)
  冻结集自动评审 0.9841;无效轮次 0/1401;118/118 例完成
三方交叉验证 9/9 adv + 消费端哈希复算 9/9 一致

两套证据分层(详见 docs/evaluation_status.md):

诚实边界:当前组合探针和冻结集说明已知错误受控;真正泛化仍需修复过程未见的人工外部留出集。

性能(PRD:148 接口5min/2并发):实测 2并发各 2.8s、单发 2.5s(asyncio.to_thread 包装同步 LLM 客户端不阻塞事件循环);118 例并发 8 实测 ~52s。

版本快照另由 verify_versions.py 端到端验证(17 项全通过):版本链 / 历史不覆盖 / 列表 / diff / latest。

10.3 运行

source .venv/bin/activate
python projects/supplement_report/eval/evaluate.py projects/supplement_report/testset/cases/

11. 异常与缺数

按 PRD:147,字段缺失按「初步核实」语境或标注「待补」;未匹配伤员标「待核实」不计入死亡统计。四类异常:

异常类型 触发 处理 文案
患者列表为空 patients=[] 走确定性兜底 「经初步核实,暂无收治人员信息,待补。」
预检分诊缺失 triageLevel 空 归轻症保持数字闭合 + 计入 tier_pending 明细行 tier 标「待核实」;基本情况追加「另有 N 人分级待核实」
BS321 出院召回 recallStatus=recalled 出院效果撤销,按在院 missing_fields 备注「出院经BS321召回,已按在院校正」
BS313 退院撤销 dischargeCancel=true 不计收治 missing_fields 备注「就诊经BS313退院撤销,已不计入收治」
BS313 出院去向 dischargeStatus=出院 明细行末标注 (已出院)/(已转院);在院不标
患者未匹配 isMatched=false 不计死亡、单列待核实组 二段加「(N)待核实伤员:M 名(患者关联失败,不计入院内死亡统计)」

补充:事件名/地区缺失 → 标题/落款回退 XX市XX区 / XX市卫健委;LLM 调用/解析失败 → 回退确定性文案,报告仍可生成。所有缺数项进 facts.missing_fields,报告全文不含「暂无收治数据」旧措辞。

12. 版本快照

PRD:142「同一事件可多次续报,数字随数据更新演进,各版本保留快照」。

12.1 数据模型(store.py:ReportVersion)

version_id        # f"{eventId}_v{n}"
event_id
version_no        # 该事件第几次续报(1-based)
prev_version_id   # 上一版本id,首版为 ""
as_of_time / generated_at / title
facts             # 冻结的确定性统计快照(可回溯)
sections / signature / markdown
missing_fields

12.2 存储与并发

12.3 版本链规则

12.4 验证

eval/verify_versions.py 端到端覆盖 17 项:v1→v2→v3 版本链、历史不覆盖、列表升序、latest、diff delta(同输入 delta=0、跨 case delta≠0)。

13. 后续可演进

  1. 接入真实 HIS 消息流:当前测试集是紫云院前数据合成院内伤患;生产应直接消费 BS310/BS313/预检分诊消息(替换 build_cases.synthesize_patients)。
  2. 下一步工作按需开启:若产品后续要求生成,可受计划事实约束,确定性渲染或受控润色。
  3. 版本快照持久化后端:当前本地 JSON,生产可换 SQLite/对象存储,接口与版本链规则不变。
  4. HTML 渲染:可补一份带 Mermaid 图的 HTML 版本。
  5. 守门从关键词黑名单重构为事实白名单(codex seg6 已知限制,见 docs/known_limitations.md):当前 _sanitize_basic 多处用关键词匹配("截至"/"均"/"全部"+死亡/科室名至少一个/_has_stray_number 只拦>100),存在 7 个可复现绕过。后续把守门改为解析事实并精确绑定(时间值校验、全死亡排除任何存活陈述、科室名全集合隶属、任意未知数字白名单),并补对应 mutation 探针,目标收敛 codex PASS。优先堵 P0-7(低位编造数字,影响最直接)。