# emr-parse-normalize **Repository Path**: medical-light-years/emr-parse-normalize ## Basic Information - **Project Name**: emr-parse-normalize - **Description**: 把同一住院 visit 的入院记录、病程、护理、会诊、检查、出院小结等多份自由文本文书,按两步抽取与跨文书反思归一为带来源与置信度的病案首页草稿、跨文书时间线与质控缺项清单;走 draft→pending_review→committed 三态状态机与逐字段复核签字后才允许回写,每次运行按 audit_id 落七子目录全流程留痕,模型自动签字被守卫拦截;CLI/HTTP/Docker 全栈,1500+ 测试与 120 份规模回归覆盖。 - **Primary Language**: Python - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-23 - **Last Updated**: 2026-09-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 电子病历多源解析归一系统 > 医生提交同一住院 visit 的入院记录 / 病程 / 护理 / 会诊 / 检查 / 出院小结后,一键得到一份可逐字段复核确认的病案首页草稿、跨文书时间线与质控缺项清单;医生显式复核签字后才允许回写或导出归档。 把同一患者在不同时间、由不同人写的多份自由文本文书,统一归一为结构化、可核对、可审计的病案首页草稿 —— 模型只做"自由文本→首页字段"这一步,剩下的签字、回写、状态推进由人完成。 ## 适用场景 / 目标岗位 | 岗位 | 什么时候用 | 得到什么 | |------|------------|----------| | 临床医生 | 出院前 1 小时,提交本次 visit 的所有文书 | 一份带来源、出处片段、置信度的病案首页草稿 + 时间线 + 缺项清单;逐字段确认 / 修正后才签字 | | 病案室编码员 | 终末质控 / 出院归档批量核对 | 按 JSONL 名单批量跑同一批 visit,输出首页草稿 + 跨文书冲突 / 缺项 / 时间序差异报告,按字段打回或确认 | | 信息科工程师 | 接入 HIS / EMR 接口规范与回写审计 | 正式 REST / HL7 接口规范、ModelGateway 调用审计、按 `audit_id` 的全流程留痕目录、可回放的运行日志 | > **表格纪律**:本表只写岗位口语 + 业务时刻 + 可核对产出;HL7 / ASTM / REST 路径、状态枚举、字段名一律放到「命令 / API / 配置说明」。 ## 能力要点 - **多源解析归一**:按 doc_type(入院记录 / 病程 / 护理 / 会诊 / 检查 / 出院小结)路由字段抽取,每个字段带 `value` / `sources`(多份文书证据) / `confidence` 三元组。 - **两步法抽取**:首轮按文书类型抽主字段,二轮用首轮字段名当关键词回其他文书回填补全;冲突不静默覆盖,写入反思队列。 - **跨文书反思循环**:节点链 `FirstExtract → Reflection → Summary → ReflectionSummary`,自动识别同字段异值(冲突)、缺项、时间序异常(病程早于入院、出院晚于入院 N 天等)。 - **鲁棒解析与确定性 baseline**:模型返回含 `` 标签或坏 JSON 时多策略修复(清思考标签 / 平衡括号 / json_repair / LLM 辅助);修复失败或模型不可达时回退正则 + 关键词 + 模板填空 baseline,标灰字段并禁止静默回写。 - **病案首页模板装订**:按 Markdown 模板 + 字段组优先级把归一结果装订为最终首页草稿,必填字段缺失落入 `qc_gaps`(severity=error)。 - **三态状态机与医生复核**:`draft → pending_review → committed`;撤回走 `committed → pending_review` 留 `retry-id`;模型不得自动签字,不得跳过复核进入 committed。 - **EMR 回写与审计**:正式 REST 接口规范 + Bearer Token 占位;未复核不得回写;每次运行落 `audit//` 目录(文书摘要、prompt 摘要、响应哈希、降级原因、复核人与时间戳)。 ## 快速开始 ```bash # 1. 安装依赖(Python 3.11) cd emr-parse-normalize pip install -e . # 2. 复制配置:模型与 EMR 接口(不修改 .env.example;本地凭据填到 .env) cp .env.example .env # 编辑 .env,至少设置 MODEL_BASE_URL / MODEL_API_KEY / MODEL_NAME # 3. 跑端到端 demo(Phase 4 后续 task 补齐后可用) bash scripts/demo.sh # 4. 验证 pytest -q ``` ### Docker 部署(可选) ```bash docker compose up -d # 启动 api 服务(端口 8000) docker compose --profile debug up # 含 stub 联调服务(端口 9001) docker compose logs -f emr-parse-normalize # 查看日志 ``` 不强依赖容器:单机 / 离线 / 信创环境可直接 `pip install -e .` 跑 CLI 或 `python -m emr_parse_normalize api` 起 HTTP 服务。详见 `Dockerfile` + `docker-compose.yml`(默认挂卷 `/data/audit` + `/data/db`)。 ## 命令 / API / 配置说明 ### CLI(typer 入口) | 子命令 | 用途 | |--------|------| | `python -m emr_parse_normalize ingest --visit-id ` | 按 visit_id 拉取该次住院的全部文书,落 audit_id | | `python -m emr_parse_normalize parse --visit-id ` | 跑 ingest → 多源抽取 → 反思 → 装订,输出首页草稿预览 | | `python -m emr_parse_normalize review --audit-id --field --action confirm|reject --reviewer <工号>` | 医生逐字段确认 / 打回 | | `python -m emr_parse_normalize commit --audit-id ` | 经复核后正式回写或导出 | | `python -m emr_parse_normalize audit --show ` | 查审计摘要与文件清单 | | `python -m emr_parse_normalize batch ` | 病案室批量离线处理(Phase 4 后续 task 补齐) | | `python -m emr_parse_normalize api` | 启动 fastapi 简化 HTTP 服务(Phase 4 后续 task 补齐) | ### HTTP API(fastapi,简化版) | 路径 | 用途 | |------|------| | `POST /parse` | 触发首页归一,请求体为该 visit 文书流,返回 `audit_id` 与首页草稿预览 | | `POST /review` | 医生逐字段确认 / 修正,返回更新后的状态 | | `GET /audit/` | 查审计摘要与文件清单 | ### 关键配置(`.env`) | 变量 | 含义 | |------|------| | `MODEL_BASE_URL` | OpenAI-compatible 协议 base_url(如医院本地模型网关) | | `MODEL_API_KEY` | 模型 API key(缺值时启动报可读错误,不堆栈) | | `MODEL_NAME` | 模型名 | | `MODEL_TIMEOUT` | 单次调用超时(秒) | | `EMR_BASE_URL` | EMR 主索引 base_url | | `EMR_TOKEN` | EMR 访问 token | | `DB_PATH` | SQLite 本地持久化路径 | | `AUDIT_DIR` | 审计目录(默认 `./data/output/audit/`) | > **协议选型**:本系统统一只走 OpenAI-compatible 协议一种;业务层只调用 `ModelGateway`,不直接依赖任何厂商 SDK。 ## 典型场景 ### 场景一 · 临床医生出院前 1 小时一键触发 **谁在用**:科室临床医生。 **触发时刻**:本次 visit 全部文书(入院记录 / 病程 / 护理 / 会诊 / 检查 / 出院小结)都已提交,准备出院前 1 小时内。 **怎么做**: ```bash # 1. 拉取文书并跑归一(一行命令) python -m emr_parse_normalize parse --visit-id # 输出:front_page_draft 预览(每字段含 value/sources/confidence)+ timeline + qc_gaps + audit_id ``` **得到什么**:医生工作站上看到一份**可逐字段复核确认**的病案首页草稿;时间线把入院 / 病程 / 护理 / 会诊 / 出院小结按时序排好;冲突 / 缺项 / 时间序异常被显式列出。医生按字段 `confirm` 或 `reject`,全部确认后 `sign` → `commit`,由 EMR 回写接口正式落档。所有动作落 `audit//`,可事后回放。 ### 场景二 · 病案室终末质控批量核对 **谁在用**:病案室编码员 / 终末质控员。 **触发时刻**:每日出院归档前,按 visit 名单批量核对首页完整性与一致性。 **怎么做**: ```bash # 1. 把当日待核对 visit 列表存为 JSONL(每行一个 visit_id) python -m emr_parse_normalize batch visits_to_review.jsonl # 输出:每份 visit 的首页草稿 + 差异报告路径 + 汇总(total / succeeded / failed) ``` **得到什么**:每份 visit 输出首页草稿 + 跨文书冲突 / 缺项 / 时间序**差异报告**(含建议动作:复核 / 退回 / 提示人工);病案室按字段打回(走 `review --action reject`)或确认(`review --action confirm`)。批处理不阻塞单条 visit 失败,单条失败仍打印 ERROR 行便于人工干预。 ### 场景三 · 补录文书后同 visit 重跑差异对比 **谁在用**:临床医生 / 病案室(共享)。 **触发时刻**:visit 已 `committed` 归档后,又补录了一份新文书(如补充会诊意见 / 修正手术记录)。 **怎么做**: ```bash # 1. 补录文书后再次 parse → 触发差异报告而非覆盖 python -m emr_parse_normalize parse --visit-id # 已 committed → 不覆盖;输出 compare_for_replay 的 DiffReport # 2. 走 retry 流程:撤回 committed → 重新走复核 python -m emr_parse_normalize review --audit-id --action retry --reason --reviewer # 撤回后 pending_review → 重新逐字段确认 → sign → commit ``` **得到什么**:原 `committed` 版本不被静默覆盖,新草稿与旧版本以字段级 Diff 报告呈现(同值 / 异值 / 仅新 / 仅旧 / 来源差异);临床医生按字段决定是否采纳差异;所有撤回与重试动作写 `audit//retry_.json` 与 `diff_.md` 留痕。 ### 场景四 · 信息科接入 HIS / EMR 接口规范与回写审计 **谁在用**:信息科工程师。 **触发时刻**:院内 HIS / EMR 接入本工具的解析归一能力,按 `spec.md` 落地正式 REST / HL7 适配层。 **怎么做**: 1. 参考 `tests/fixtures/emr_adapter_stub.py` 实现正式适配层(`EMR_BASE_URL` / `EMR_TOKEN` 配置化)。 2. 配置 `.env` 中的模型网关与 EMR 凭据。 3. 每次回写落 `audit//writeback_.json`(含 attempt / status / error + 重试队列)。 **得到什么**:正式接口规范 + 按 `audit_id` 的全流程留痕目录 + 可回放的运行日志 + 飞检 / 内审语境自洽的留痕结构(不写完整可识别患者字段,仅写摘要 + 短哈希)。 ## 输出样例 ### 首页草稿(脱敏片段) ```json { "front_page_draft": { "chief_complaint": { "value": "", "sources": [{"doc_id": "doc_", "snippet": "..."}], "confidence": 0.86 }, "admission_date": { "value": "2026-08-10", "sources": [{"doc_id": "doc_"}], "confidence": 0.99 } }, "timeline": [ {"ts": "2026-08-10T08:32:00", "event": "入院记录", "source_doc_id": "doc_"}, {"ts": "2026-08-15T16:05:00", "event": "出院小结", "source_doc_id": "doc_"} ], "qc_gaps": [ {"field": "discharge_diagnosis", "reason": "missing", "severity": "error"}, {"field": "operation_name", "reason": "未在出院小结中明确", "severity": "warn"} ], "state": "pending_review", "audit_id": "" } ``` ### 审计目录样例 ``` data/output/audit// ├── manifest.json # 审计摘要(event_count / node_set / 25 类 event_type 计数) ├── events/ # 业务事件流(按 ts 顺序 + seq 序号) │ ├── 2026-08-16T22-30-01_ingest_started_001.json │ ├── 2026-08-16T22-30-04_ingest_completed_002.json │ ├── 2026-08-16T22-30-09_extract_broad_completed_003.json │ ├── 2026-08-16T22-30-15_reflection_completed_004.json │ ├── 2026-08-16T22-30-20_review_field_confirmed_005.json │ ├── 2026-08-16T22-30-22_review_submitted_006.json │ ├── 2026-08-16T22-30-25_review_signed_007.json │ └── 2026-08-16T22-30-30_writeback_success_008.json ├── calls/ # 每次 ModelGateway 调用的元数据 │ ├── 2026-08-16T22-30-08_call_001_extract_broad_chief_complaint.json │ └── 2026-08-16T22-30-14_call_002_extract_deep_present_illness.json ├── traces/ # 每个字段经过的反思节点 trace │ └── 2026-08-16T22-30-16_node_trace.json ├── parse/ # RobustJSONParser 命中的策略记录 │ └── 2026-08-16T22-30-18_parse_report.json ├── diffs/ # 重跑 / retry 触发的字段级 Diff 报告 │ └── diff_001_.md ├── degradation/ # baseline 兜底 / 模型不可达等降级事件 │ └── 2026-08-16T22-30-18_degradation.json └── writeback/ # EMR 回写尝试与回执 ├── 2026-08-16T22-30-28_writeback_attempt_001.json └── 2026-08-16T22-30-30_writeback_receipt.json ``` ### 差异报告(脱敏片段 · 同值 / 异值 / 仅新 / 仅旧) ```markdown ## 字段级 Diff(同 visit 重跑) | 字段 | 状态 | 旧值 | 新值 | 旧来源 | 新来源 | |------|------|------|------|--------|--------| | chief_complaint | changed | | | doc_ | doc_ | | admission_date | same | 2026-08-10 | 2026-08-10 | doc_ | doc_ | | operation_name | only_in_new | — | | — | doc_ | | discharge_diagnosis | same | | | doc_ | doc_ | **结论**:原 committed 版本不被静默覆盖;建议动作 = **复核** changed / only_in_new 字段。 ``` ## 架构与数据流 ### ASCII 概览 ``` EMR 主索引 (REST / HL7) │ ▼ ingest / preflight ──► 缺文书早退 / 鉴权失败 │ ▼ extract (broad_field → deep_field) │ 首轮按 doc_type 抽主字段 │ 二轮按字段名跨文书回填补全 ▼ reflect (节点链 FirstExtract → Reflection → Summary → ReflectionSummary) │ 冲突 / 缺项 / 时间序三类检测 ▼ robust (json_parser → baseline) │ 多策略修复 → 失败回退正则 + 模板填空 ▼ templates (template_parser → composer) │ 必填校验 → qc_gaps → front_page_draft ▼ review (state_machine → reviewer) │ draft → pending_review → committed ▼ audit + persistence (SQLite + audit//) │ ▼ EMR 回写(仅 committed 状态) ``` ### Mermaid 数据流 ```mermaid flowchart TD EMR[EMR 主索引 REST/HL7] -->|按 visit_id 拉取| Preflight[ingest / preflight
连接自检 + 必需 doc_type 校验] Preflight -->|缺文书 / 鉴权失败| Exit[早退 + 可读错误] Preflight -->|通过| Broad[extract / broad_field
按 doc_type 抽主字段] Broad --> Deep[extract / deep_field
二轮跨文书回填补全] Deep --> Auth[extract / authority
文书权威级别裁决冲突] Auth --> Reflect[reflect 节点链
FirstExtract → Reflection → Summary → ReflectionSummary] Reflect -->|冲突/缺项/时间序| QC{qc_gaps
severity=error?} QC -->|是| Draft1[draft 状态
装订 front_page_draft
含 error gap] QC -->|否| Draft2[draft 状态
装订 front_page_draft] Draft1 --> Robust Draft2 --> Robust[robust / json_parser
多策略修复 → baseline 兜底] Robust -->|降级触发| Baseline[robust / baseline
正则 + 关键词 + 模板填空
灰字段标 needs_human=true] Robust -->|解析成功| Compose[templates / composer
必填校验 → IR 装订] Baseline --> Compose Compose --> Review[review / state_machine
draft → pending_review → committed] Review -->|医生逐字段 confirm / sign| Commit[EMR 回写
仅 committed 状态] Review -->|补录文书重跑| Diff[review / diff_report
字段级 Diff 报告] Commit --> Audit[audit / store
events/ + calls/ + traces/
+ diffs/ + writeback/] Diff --> Audit Baseline -.->|降级事件| Audit Review -.->|状态迁移 / 签字| Audit Audit --> SQLite[(SQLite
visits / drafts / reviews
writeback_queue / audit_index)] Audit --> Files[(audit/<id>/ 目录
manifest + 7 子目录)] ``` ### 关键交互点 - **`ingest / preflight`**:运行前连接自检 + 必需 `doc_type` 校验;缺文书早退,不进入抽取阶段。 - **`extract`**:两步法抽取(broad_field → deep_field)保证首轮按文书类型、二轮跨文书回填补全;`authority` 按文书权威级别(出院小结 > 入院记录 > 病程 > 护理 > 会诊 > 检查报告)裁决冲突。 - **`reflect`**:节点链最多迭代 `max_iterations`;连续两轮无新冲突即收敛;时间序校验识别"病程早于入院""出院晚于入院 + N 天异常"等边界异常。 - **`robust`**:thinking_pattern → 括号平衡 → json_repair → LLM 辅助 4 策略逐级修复;连续 3 次失败切换 baseline;**baseline 路径下强制不能进入 committed**。 - **`templates`**:Markdown 模板含 front-matter 必填标记;缺必填字段产生 `qc_gaps[severity=error]` 但仍可装订;装订后 `qc_gaps` 含 error 项时不允许直接 committed。 - **`review`**:三态状态机(`draft → pending_review → committed`);`committed → pending_review` 走 `retry-id` 留痕;模型自动签字路径在 `Reviewer` 内被守卫拦截。 - **`audit`**:每个关键节点落审计事件;按 `audit_id` 建目录,文件名 `__.json`;敏感字段(姓名 / 身份证 / 电话)落盘前自动哈希或 `[REDACTED]`。 ## 安全与合规边界 - **模型只做草稿归一**:模型输出仅为首页字段草稿(`front_page_draft`),**不得**直接推进临床状态;诊断 / 治疗建议一律由医务人员出具,本系统**不产出**任何诊断或治疗建议。 - **不替代医生签字**:进入 `committed` 状态必须经过 `review` 阶段的人工**逐字段**确认 + `sign`;`Reviewer.sign()` 在业务层守卫,**模型自动签字路径被拦截**(`ModelNotAllowed` 异常),仅临床医生工号可触发。 - **审计脱敏**:审计目录中只写文书摘要、prompt 摘要、响应哈希;**不写**完整可识别患者字段(姓名 / 身份证 / 电话 / 具体住院号等)。`_SENSITIVE_KEYS` 名单中的字段自动替换为 `sha256:`+16hex 短哈希;`_REDACT_KEYS` 名单中的字段替换为 `[REDACTED]`;`patient_id` / `visit_id` / `doc_id` / `audit_id` 等业务标识保留但属内部占位。 - **降级禁止静默回写**:模型不可达 / 超时 / 解析失败回退 baseline 时,**强制不能进入 committed**(`StateMachine` 在 `robust/orchestrator` 触发 baseline 后拒绝 `transition(committed)`);灰字段在 IR 中标 `needs_human=true`,须人工补全。 - **接口替身 ≠ 最终对接**:`tests/fixtures/emr_adapter_stub.py` 仅用于验证正式接口规范,不是最终对接方式;正式回写须走 `EMR_BASE_URL` / `EMR_TOKEN` 真实 EMR 网关,替身文件顶部明确标注「替身仅用于验证正式接口规范,不是最终对接方式」。 - **审计可回放**:每次运行落 `audit//` 完整目录(events + calls + traces + parse + diffs + degradation + writeback 七子目录 + manifest.json);飞检 / 内审场景下可按 `audit_id` 追溯整次运行的输入 / 抽取 / 反思 / 复核 / 回写全过程。 - **不写诊断 / 不代签**:本系统**不**产出 ICD-10/ICD-11 编码自动赋值(仅抽取医生在文书中已写的诊断文本);**不**生成电子签名;**不**触发自动归档。临床决策与签字一律走既有 EMR 工作流。 ## 项目结构 ``` emr-parse-normalize/ ├── emr_parse_normalize/ # Python 包(CLI / API / runtime 业务编排层) │ ├── __main__.py # python -m emr_parse_normalize 入口 │ ├── cli.py # typer CLI(ingest / parse / review / commit / audit / batch / api) │ ├── api.py # fastapi 简化 HTTP(POST /parse / POST /review / GET /audit/) │ ├── runtime.py # CLI 与 FastAPI 共用的业务编排层(含替身注入 + 错误分级) │ ├── gateway/ # ModelGateway(OpenAI-compatible;含超时、重试、调用元数据审计) │ ├── ingest/ # EMR 文书源适配层 + preflight + 按 doc_type 连接器注册 │ ├── extract/ # 多源字段抽取(broad_field / deep_field / authority) │ ├── reflect/ # 跨文书反思节点链(FirstExtract → Reflection → Summary) │ ├── robust/ # 鲁棒解析(json_parser)+ baseline 兜底 + 降级编排 │ ├── templates/ # 病案首页模板解析(template_parser)+ composer 装订 │ ├── review/ # 三态状态机 + 医生复核 + 回写 + 差异报告 │ ├── prompts/ # prompts 注册中心(按字段名 / node 检索) │ ├── audit/ # 审计事件(AuditEvent)+ AuditStore 落盘 + 敏感字段过滤 │ ├── persistence/ # SQLite 迁移(schema_version)+ 5 张表 DAO │ └── utils/ # 配置 / 日志等 ├── tests/ # pytest(接入替身与脱敏样例) │ ├── fixtures/ │ │ ├── emr_adapter_stub.py # EMR 接口替身(多 visit / 多 doc_type / 鉴权失败 / 超时) │ │ ├── visits/ # 脱敏 visit 样例(含边界用例 + 120 份规模回归集) │ │ └── visit_generator.py # 大批量脱敏 visit 生成器 │ └── test_*.py # 1500+ 测试(覆盖数据流 + 状态机 + baseline + e2e + 规模回归) ├── scripts/ │ └── demo.sh # 端到端 demo(ingest → parse → review → commit → audit --show) ├── data/ │ ├── samples/ # 脱敏样例(visit_sample.json 等) │ ├── output/ # 运行时审计与 SQLite(不进 git) │ └── README.md # 脱敏与替身边界声明 ├── templates/ │ └── front_page.md # 病案首页 Markdown 模板(含必填字段 front-matter) ├── docs/ │ ├── scale_report.md # 120 份规模回归度量(耗时 / 命中率 / 降级率) │ └── scale_report.json # 同上结构化版本(CI 抓取用) ├── logs/ # 运行时日志(不进 git;round 根 logs/taskN-*.log 存构建/测试输出) ├── Dockerfile # 容器化(python:3.11-slim + 非 root + ENTRYPOINT python -m emr_parse_normalize) ├── .dockerignore # 排除 .git / .venv / tests/fixtures/visits/ 大样例等 ├── docker-compose.yml # 主服务 + 可选 stub 服务(profile=debug)+ 持久卷 + 健康检查 ├── .env.example # 配置样例 ├── .gitignore ├── LICENSE ├── README.md └── qrcode.jpg ``` > **目录约定**:所有源代码、依赖、配置、模板、测试都放在项目子目录 `emr-parse-normalize/` 内;round 根目录仅放 `task.json` / `progress.txt` / `score.json` / `ideas.md` / `research.md` / `spec.md` / `plan.md` 等 Phase 元数据,**不进**项目子目录发布。 ## License MIT --- ## 关注我们 欢迎扫码关注公众号,获取项目更新与交流加群: ![关注我们](qrcode.jpg)