Technical Design · 救援进程报告(续报)
救援进程报告(续报)技术设计
模块 projects/supplement_report/ · 独立自包含子项目 · 业务产物《关于XX事故受伤人员救治情况的报告》
救援进程报告(续报)技术设计
模块:
projects/supplement_report/(独立自包含子项目)
业务产物:《关于XX事故受伤人员救治情况的报告》
文档定位:Markdown 技术母版;架构图使用 Mermaid 编码。
1. 设计要点
续报把同一事件在某一时点的 HIS 救治状态冻结成可追溯快照,按正式公文模板生成报告。核心原则:数字确定性、不可编造;大模型只做语言润色,不创造事实。
- 三、下一步工作留空——不生成任何内容,仅保留段标题。
- 只有「开头导语 + 一、基本情况」走大模型润色;标题、二、伤员收治明细、落款全部用 Python 模板确定性拼接,不过大模型。
- 所有写进报告的数字(收治总数、死亡、存活、伤情分级、科室数)由
facts.py确定性计算后注入 LLM prompt,LLM 只做语言组织、不增删数字。 - 统计走 HIS 院内口径:收治按
hisPatientId去重,死亡按出院状态判定,分级由预检分诊级别映射。 - 模块自包含: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 源材料已确认的事实
- 续报模板固定三段:一、基本情况 / 二、伤员收治情况 / 三、下一步工作(略)。
- 伤员明细字段顺序:序号 → 姓名 → 性别 → 年龄 → 伤情分级 → 伤情诊断。
- 统计字段来源:收治总数(BS310 患者ID去重)、死亡(BS313 出院状态=死亡)、分级(预检分诊 I-IV)。
- 金标准示例(通州坍塌)按科室分组列明细,死亡伤员单列一组。
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 产出:
cases/<id>.json:续报输入(eventInfo + patients + asOfTime)golden/<id>.json:期望 facts(收治总数/死亡/存活/分级/科室聚合/死亡明细数)
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 铁律)
- 只能使用给定事实,不得增删、修改、编造任何数字。
- 不得加入未提供的事件细节(伤员姓名、诊断、医院名)。
- 不得生成二段、三段、标题、落款。
- 严格按 JSON 输出(lead / basic 两 key)。
- 数字逐字采用,语言润色。
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 配置
- Provider:DashScope(阿里百炼)OpenAI 兼容端点
- Base URL:
https://dashscope.aliyuncs.com/compatible-mode/v1 - 默认模型:
qwen-plus(可由SUPPLEMENT_LLM_MODEL覆盖) - 强制
enable_thinking=False(Qwen 思考模式会污染正文且变慢) - API Key:从仓库根
.env的DASHSCOPE_API_KEY读取 - temperature=0(置 0 后同 case 连跑 5 次逐字一致,消除采样抖动),max_tokens=1200,timeout=60s
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 路由。
- 访问的是服务接口:前端 fetch 直连 8003 的
/api/supplement_report/generate(SSE),不走 8080 代理。 - 左栏流式渲染三步进度(统计→润色→拼接)+ 确定性统计 chips;右栏渲染最终续报全文。
- 预设下拉从 8003
/presets加载 6 个 case,可编辑 JSON 后重新生成。 - 首页
demo/static/index.html已加 MODULE 04 入口卡片。
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):
- L1 报告冻结集通过情况、模型接受率和回退率,不压成“泛化满分”。
- L2 当前
measurementQuality.scoreReportable=true,但0.9841只作内部冻结集自动评审参考;人工盲评校准完成前不作外部泛化或质量 KPI。
诚实边界:当前组合探针和冻结集说明已知错误受控;真正泛化仍需修复过程未见的人工外部留出集。
性能(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 存储与并发
- 路径:
projects/supplement_report/snapshots/{eventId}/{version_id}.json(.gitignore已忽略snapshots/) fcntl.flock文件锁:同事件并发生成时串行化 version_no 分配- 历史不可覆盖:version_no 递增不冲突,目标文件存在则
FileExistsError兜底防御
12.3 版本链规则
- 每次 generate 自动
save_version,响应回versionId / versionNo / prevVersionId prevVersionId指向上一版(首版为空),形成不可变链- 最新版本用于当前展示,历史版本只读可回看
diff_versions给出两版本数字演进 delta(收治/死亡/存活/分级/科室/待核实)
12.4 验证
eval/verify_versions.py 端到端覆盖 17 项:v1→v2→v3 版本链、历史不覆盖、列表升序、latest、diff delta(同输入 delta=0、跨 case delta≠0)。
13. 后续可演进
- 接入真实 HIS 消息流:当前测试集是紫云院前数据合成院内伤患;生产应直接消费 BS310/BS313/预检分诊消息(替换
build_cases.synthesize_patients)。 - 下一步工作按需开启:若产品后续要求生成,可受计划事实约束,确定性渲染或受控润色。
- 版本快照持久化后端:当前本地 JSON,生产可换 SQLite/对象存储,接口与版本链规则不变。
- HTML 渲染:可补一份带 Mermaid 图的 HTML 版本。
- 守门从关键词黑名单重构为事实白名单(codex seg6 已知限制,见
docs/known_limitations.md):当前_sanitize_basic多处用关键词匹配("截至"/"均"/"全部"+死亡/科室名至少一个/_has_stray_number只拦>100),存在 7 个可复现绕过。后续把守门改为解析事实并精确绑定(时间值校验、全死亡排除任何存活陈述、科室名全集合隶属、任意未知数字白名单),并补对应 mutation 探针,目标收敛 codex PASS。优先堵 P0-7(低位编造数字,影响最直接)。