docs: 架构定稿——四段流水线、两个库与待定问题

负责人 2026-10-16 定:数据流为「医院原始资料->中间文件->给专家的资料库 & 标准答案库」;
两个库和中间文件都用文件夹,SQLite 只用于调模型账本;调模型用 PolyGateway;CEUS 直接逐个
迁移;数据格式和口径以 40 例交付包为准。存储与迁移方案先由两个中立子 Agent 独立评审。

- design 0002:八条决定及负责人原话(化名与打乱顺序一条为 Agent 提出)
- explanation/结构.md 对照 ClinDB 的结构文档重写:切分原则、完整范围、12 个模块与依赖、
  边界、两个库与 CEUS 中间文件的内容、目录与检查、规模、推进顺序
- research-wiki/待定问题/:绿框、参数冻结、排除标准、跨表不一致四个问题;定了就删
- AGENTS.md:事实来源加「交付资料的格式和口径以最新交付包为准」
- .gitignore 与 pre-commit 拦截 library/、answers/、work/

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 00:44:42 -04:00
parent 22cf532dc4
commit 9b2f842559
12 changed files with 369 additions and 80 deletions
+3
View File
@@ -1,6 +1,9 @@
# 本地材料:不进 git(AGENTS.md 硬性约束 1、3),pre-commit 另有一道拦截。 # 本地材料:不进 git(AGENTS.md 硬性约束 1、3),pre-commit 另有一道拦截。
# 末尾不加斜杠,这样同名的符号链接也能匹配。 # 末尾不加斜杠,这样同名的符号链接也能匹配。
/data /data
/library
/answers
/work
/runs /runs
/reference /reference
/reference-pdf /reference-pdf
+2 -2
View File
@@ -7,10 +7,10 @@ repos:
hooks: hooks:
# AGENTS.md 硬性约束 1、3:即使 git add -f 也不能提交本地材料。 # AGENTS.md 硬性约束 1、3:即使 git add -f 也不能提交本地材料。
- id: local-only-paths - id: local-only-paths
name: data/、runs/、reference/、models/ 和 .env 不进提交 name: 本地材料(data、library、answers、work、runs、reference、models)和 .env 不进提交
language: fail language: fail
entry: "禁止提交本地材料:" entry: "禁止提交本地材料:"
files: '^(data|runs|reference|reference-pdf|models)(/|$)|(^|/)\.env(\.[^/]+)?$' files: '^(data|library|answers|work|runs|reference|reference-pdf|models)(/|$)|(^|/)\.env(\.[^/]+)?$'
exclude: '^\.env\.example$' exclude: '^\.env\.example$'
types_or: [file, symlink] types_or: [file, symlink]
# 使用 uv.lock 中的同一版本,不另装一份 ruff。 # 使用 uv.lock 中的同一版本,不另装一份 ruff。
+3 -1
View File
@@ -27,9 +27,11 @@ CADx-4 研究胰腺囊性肿瘤的鉴别诊断:给同一个病人的三路资
| 当前进度、下一步 | `WORK_LOG.md` | | 当前进度、下一步 | `WORK_LOG.md` |
| 代码分哪些模块、谁能依赖谁、各模块预期规模 | `research-wiki/explanation/结构.md` | | 代码分哪些模块、谁能依赖谁、各模块预期规模 | `research-wiki/explanation/结构.md` |
| 各环节在做什么、输入输出 | `research-wiki/explanation/` 的其他文档 | | 各环节在做什么、输入输出 | `research-wiki/explanation/` 的其他文档 |
| 还没定的问题 | `research-wiki/待定问题/`(定了就删,结论进 `design/`) |
| 某个决策为什么这样定 | `research-wiki/design/NNNN-*.md`(写完冻结) | | 某个决策为什么这样定 | `research-wiki/design/NNNN-*.md`(写完冻结) |
| 命令怎么敲、环境怎么配 | `research-wiki/guides/` | | 命令怎么敲、环境怎么配 | `research-wiki/guides/` |
| 医学词、参数的原始口径、临床方给的字段定义 | `research-wiki/reference/`(每条自带出处) | | 医学词、参数的原始口径、临床方给的字段定义 | `research-wiki/reference/`(每条自带出处) |
| 交付资料的格式和口径(列名、编码、时间的含义) | 最新的交付包,目前是 `data/交付包_20260908_40例/`。旧样本(如 `data/1.2-TEST/`)和旧程序的口径只当线索。来源:负责人 2026-10-16「数据改过很多版,所以你们需要对齐“40例交付包”那个」 |
| 配置值、字段定义 | 代码里的定义与校验;文档只解释含义,不抄数值 | | 配置值、字段定义 | 代码里的定义与校验;文档只解释含义,不抄数值 |
| 临床口径、双方约定、待交付清单 | Linear(临床方维护的那几份 document 是权威,仓库里的抄本以它为准) | | 临床口径、双方约定、待交付清单 | Linear(临床方维护的那几份 document 是权威,仓库里的抄本以它为准) |
| 旧项目做了什么、踩过什么坑 | `reference/CADx-4-CAD/`(只当线索,见下文) | | 旧项目做了什么、踩过什么坑 | `reference/CADx-4-CAD/`(只当线索,见下文) |
@@ -48,7 +50,7 @@ CADx-4 研究胰腺囊性肿瘤的鉴别诊断:给同一个病人的三路资
## 硬性约束 ## 硬性约束
1. **答案不进 AI 专家能看到的任何东西。** 病理金标准、工作簿里的诊断列、按规则算出诊断倾向的公式列、医生的结构化判读列(**标注工作簿**是临床方另交的一份表,医生在里面逐例标了病灶位置、大小、囊壁、增强模式等三十来列,它只用于评价专家抽取得准不准)、报告的「印象 / 结论」段、真实病例号、Excel 行序,一律不能进入提示词、工具返回或专家可读的文件。**行序也算**:原始表格按诊断分组录入,行号本身就带着答案。这一条错了不会报错,只会让所有「系统诊断得准不准」的结论变成「系统能不能读到答案」。任何会削弱这条的改动,先停下来问。来源:旧项目 CLAUDE.md §1.2、旧 design 0026。 1. **答案不进 AI 专家能看到的任何东西。** 病理金标准、工作簿里的诊断列、按规则算出诊断倾向的公式列、医生的结构化判读列(**标注工作簿**是临床方另交的一份表,医生在里面逐例标了病灶位置、大小、囊壁、增强模式等三十来列,它只用于评价专家抽取得准不准)、报告的「印象 / 结论」段、真实病例号、Excel 行序,一律不能进入提示词、工具返回或专家可读的文件。**行序也算**:原始表格按诊断分组录入,行号本身就带着答案。这一条错了不会报错,只会让所有「系统诊断得准不准」的结论变成「系统能不能读到答案」。任何会削弱这条的改动,先停下来问。来源:旧项目 CLAUDE.md §1.2、旧 design 0026。
2. **每个 AI 专家只看自己那一路资料。** 性别和年龄只给临床那一路;超声和 MR 两路看不到,即使医院把这几列也复制进了报告工作簿。各路能看见什么见 `research-wiki/explanation/` 里的会诊文档(迁移完成前看旧项目 `research-wiki/explanation/agent-system.md` §2)。来源:同上。 2. **每个 AI 专家只看自己那一路资料。** 性别和年龄只给临床那一路;超声和 MR 两路看不到,即使医院把这几列也复制进了报告工作簿。各路能看见什么见 `research-wiki/explanation/结构.md` 第四部分。来源:同上。
3. **患者资料不进 git、文档和提交说明。** 原始资料只在 `data/`,运行产物只在 `runs/`,两者都不进 git。文档里可以写结构和规模,不写报告原文、姓名、病例号和诊断。把病例资料发给外部模型服务之前,先确认负责人已同意该服务。超声帧上烧录的信息栏(拼音姓名、检查号、性别年龄、医院与设备参数)不做遮蔽:临床方判定拼音和不唯一的检查号不构成身份泄露(出自旧 design 0041)。 3. **患者资料不进 git、文档和提交说明。** 原始资料只在 `data/`,运行产物只在 `runs/`,两者都不进 git。文档里可以写结构和规模,不写报告原文、姓名、病例号和诊断。把病例资料发给外部模型服务之前,先确认负责人已同意该服务。超声帧上烧录的信息栏(拼音姓名、检查号、性别年龄、医院与设备参数)不做遮蔽:临床方判定拼音和不唯一的检查号不构成身份泄露(出自旧 design 0041)。
4. **密钥不进 git、文档、日志和运行快照。** 只放环境变量或本地 `.env`;仓库只提交不含真实值的 `.env.example`。 4. **密钥不进 git、文档、日志和运行快照。** 只放环境变量或本地 `.env`;仓库只提交不含真实值的 `.env.example`。
5. **写进论文和结论的数字能追到一次真跑的原始记录**(`runs/` 下的运行目录)。不把预期、估算、旧项目报告或别人论文里的数字当成本仓库的结果;没有就写「没有」。文献里的数字核到 `reference-pdf/` 的原文。 5. **写进论文和结论的数字能追到一次真跑的原始记录**(`runs/` 下的运行目录)。不把预期、估算、旧项目报告或别人论文里的数字当成本仓库的结果;没有就写「没有」。文献里的数字核到 `reference-pdf/` 的原文。
+8 -3
View File
@@ -4,7 +4,7 @@
## 当前目标 ## 当前目标
重构第一步:搭好协作环境和项目骨架(`research-wiki/explanation/结构.md` 第六节第 1 步)。下一步是 CEUS 定量的迁移。 骨架已完成;整体架构已和负责人讨论定稿(`research-wiki/design/0002`、`explanation/结构.md`)。下一步是写第 2 步「资料整理(不含 CEUS)」或第 3 步「CEUS 定量」的任务书。
## 已完成 ## 已完成
@@ -13,9 +13,11 @@
- 2026-10-15 `data/` 同步完成:`交付包_20260908_40例`(124 个文件)、`1.2-TEST`(1460 个文件),文件数与文件总字节数(4,034,244,118)和远程一致。 - 2026-10-15 `data/` 同步完成:`交付包_20260908_40例`(124 个文件)、`1.2-TEST`(1460 个文件),文件数与文件总字节数(4,034,244,118)和远程一致。
- 2026-10-15 骨架(三份新文档过了一轮「硕士生阅读」并按意见修订):`AGENTS.md`、`CLAUDE.md`、`research-wiki/` 目录与 `README.md`、`explanation/结构.md`、`design/0001`、`guides/开发环境.md`;uv 工具链、`make sync/fmt/test/check`、pre-commit。`make check` 通过(1 项测试、1 条导入契约)。提交 5eb38df(工具链)、1f7a6a3(文档)。 - 2026-10-15 骨架(三份新文档过了一轮「硕士生阅读」并按意见修订):`AGENTS.md`、`CLAUDE.md`、`research-wiki/` 目录与 `README.md`、`explanation/结构.md`、`design/0001`、`guides/开发环境.md`;uv 工具链、`make sync/fmt/test/check`、pre-commit。`make check` 通过(1 项测试、1 条导入契约)。提交 5eb38df(工具链)、1f7a6a3(文档)。
- 2026-10-15/16 深读旧项目三条线(CEUS 算法、资料与答案隔离、会诊与评价),两位中立评审独立评了存储与迁移方案;结论落成 design 0002 与重写的 `结构.md`。待定的研究问题记在 `research-wiki/待定问题/`。
## 未完成 ## 未完成
- 无。 - `结构.md` 按负责人要求对照 ClinDB 的 `结构.md` 重写后,重写版还没再过一轮「硕士生阅读」(design 0002 与上一版结构文档过过)。
## 关键决定 ## 关键决定
@@ -23,7 +25,10 @@
- 负责人 2026-10-15 选定:检查强度「ClinDB 级」(不要提交审查闸);注释和 docstring 用中文;本机为主(「目前以本地为主,以后会根据具体情况调整」)。 - 负责人 2026-10-15 选定:检查强度「ClinDB 级」(不要提交审查闸);注释和 docstring 用中文;本机为主(「目前以本地为主,以后会根据具体情况调整」)。
- 负责人 2026-10-15 同意:MedSAM2 与 torch 装在同一个 uv 环境,靠导入边界隔离,不单独开环境(design 0001 第 7 条)。 - 负责人 2026-10-15 同意:MedSAM2 与 torch 装在同一个 uv 环境,靠导入边界隔离,不单独开环境(design 0001 第 7 条)。
- 负责人 2026-10-16:调模型用 PolyGateway;两个库和中间文件都用文件夹(SQLite 只用于调模型账本);CEUS「直接一个个迁移」,不在本机重跑旧代码作基线;数据格式和口径以 40 例交付包为准,「推注开始时间s」是视频第 0 秒时已推注的秒数(design 0002)。
## 下一步 ## 下一步
- 写任务书 02:CEUS 定量迁移(`cadx.ceus`、`cadx.segment`、`cadx.runs` 和脚本),验收为本机复现旧项目 40 例 T1 结果与 `1.2-TEST` 的 Lognormal 回放。动手前先和负责人讲旧代码的结构与打算。 - 和负责人定先做第 2 步还是第 3 步,再写对应任务书;动手前先讲旧代码的结构与打算。
- 把 `待定问题/04`(6 例跨表不一致)转给医生。
- 等临床方交付 40 例的医生标注工作簿(含造影剂到达时间和注射时间)。 - 等临床方交付 40 例的医生标注工作簿(含造影剂到达时间和注射时间)。
+2 -1
View File
@@ -8,6 +8,7 @@
| `design/` | 每次重要决策的来历,文件名 `NNNN-简短主题.md` | 写完冻结;决策变了新写一份并注明取代关系 | | `design/` | 每次重要决策的来历,文件名 `NNNN-简短主题.md` | 写完冻结;决策变了新写一份并注明取代关系 |
| `guides/` | 怎么做某件事:建环境、跑某个环节 | 常青,随实际情况更新 | | `guides/` | 怎么做某件事:建环境、跑某个环节 | 常青,随实际情况更新 |
| `reference/` | 查得到的外部事实:术语表、算法参数的原始口径、临床方给的字段定义。每条自带出处;抄自 Linear 的写明抄自哪份、哪天同步 | 常青,随出处更新 | | `reference/` | 查得到的外部事实:术语表、算法参数的原始口径、临床方给的字段定义。每条自带出处;抄自 Linear 的写明抄自哪份、哪天同步 | 常青,随出处更新 |
| `待定问题/` | 还没定、迟早要定的问题,一个问题一份文件:问题、为什么要紧、选项、倾向、等谁定 | 定了就删掉,结论写进 `design/`,同一个提交 |
| `任务书/` | 每个模块动手前的任务书,文件名 `NN-模块名.md`:要做什么、依据哪些文档、怎么验收 | 负责人确认后交执行者;要改就在开头写「这一版改了什么」 | | `任务书/` | 每个模块动手前的任务书,文件名 `NN-模块名.md`:要做什么、依据哪些文档、怎么验收 | 负责人确认后交执行者;要改就在开头写「这一版改了什么」 |
| `survey/` | 文献调研:同类工作怎么做、对我们意味着什么,引文带 PDF 页码(PDF 在根目录 `reference-pdf/`,不进 git) | 写完冻结;有新调研另写一份 | | `survey/` | 文献调研:同类工作怎么做、对我们意味着什么,引文带 PDF 页码(PDF 在根目录 `reference-pdf/`,不进 git) | 写完冻结;有新调研另写一份 |
| `results/` | 实验结果报告,每个数字标明来自哪次运行(`runs/` 下的目录名) | 写完冻结 | | `results/` | 实验结果报告,每个数字标明来自哪次运行(`runs/` 下的目录名) | 写完冻结 |
@@ -19,4 +20,4 @@
- **design**:`# NNNN 标题` → `状态:负责人 YYYY-MM-DD 认可(「原话」) | 日期:…` → 取代了谁就写一行 `> 取代 NNNN 的第 X 条` → `## 先认识几个词` → `## 背景` → `## 决定`(编号条目,写明哪条是负责人定的、哪条是 Agent 提的) → 需要时加 `## 不做的事`。 - **design**:`# NNNN 标题` → `状态:负责人 YYYY-MM-DD 认可(「原话」) | 日期:…` → 取代了谁就写一行 `> 取代 NNNN 的第 X 条` → `## 先认识几个词` → `## 背景` → `## 决定`(编号条目,写明哪条是负责人定的、哪条是 Agent 提的) → 需要时加 `## 不做的事`。
- **任务书**:标题 → 一段目的 → `来源:`(负责人原话) → `## 先认识几个词` → `## 开工前先读` → 分阶段的工作内容(每阶段写收尾点) → `## 测试` → `## 规模` → `## 审阅` → `## 报告`(分开写已验证的事实、推断和未验证的部分,附运行目录、提交号、花费)。 - **任务书**:标题 → 一段目的 → `来源:`(负责人原话) → `## 先认识几个词` → `## 开工前先读` → 分阶段的工作内容(每阶段写收尾点) → `## 测试` → `## 规模` → `## 审阅` → `## 报告`(分开写已验证的事实、推断和未验证的部分,附运行目录、提交号、花费)。
- **results**:`日期 | 对应结构.md 第六节第 N 步 | 原始记录:runs/…` → `## 先认识几个词` → `## 一 跑了什么` → 结果,每个数字带运行目录。 - **results**:`日期 | 对应结构.md 第七部分第 N 步 | 原始记录:runs/…` → `## 先认识几个词` → `## 一 跑了什么` → 结果,每个数字带运行目录。
@@ -0,0 +1,39 @@
# 0002 四段流水线与两个库
状态:决定 1–6、8 负责人 2026-10-16 认可(原话见各条);决定 7 是 Agent 提出、负责人未单独表态 | 日期:2026-10-16
## 先认识几个词
- **给专家的资料库**:AI 专家能看到的一切都在这里,专家也只读这里。
- **标准答案库**:病理金标准、医生标注表等,只给评价用。
- **中间文件**:从原始资料到两个库之间的工作产物,主要是 CEUS 的分割结果和曲线,可以删掉重算。
- **CEUS、TIC**:超声造影视频;时间-强度曲线,即视频里病灶平均亮度随时间的变化。
- **关键帧**:从上千帧的 CEUS 视频里挑出的 7 张图,交给超声专家看。B0(刚开始变亮前)、D1(开始变亮)、D2(洗入拐点)、D3(最亮)、D4(洗出拐点)从 TIC 的拟合曲线上算;F1、F2 是推注后第 20、60 秒,按推注时间取。
- **AT**(到达时间):医生判断的「造影剂流到病灶、病灶开始变亮」的时刻,写在医生标注工作簿里。
- **账本**:每次调用模型时把请求、回复、花费按顺序记下来的 SQLite 文件,只追加、不修改。
- **中立评审**:2026-10-16 派出的两个互相独立的子 Agent,拿到同样的事实、不带讨论倾向,各自读旧代码和参考项目后给出建议。原始报告未存档,下文引用的证据都可在 `reference/` 和参考项目里核对。
## 背景
`design/0001` 定下按研究原型重写之后,2026-10-15 到 16 日深读了旧项目的三条线(CEUS 算法、资料与答案隔离、会诊与评价),据此提出四段流水线。负责人对整体形状的理解是:「医院原始资料->中间文件(可以有可以没有)->「给专家的资料库」(给agent的东西都在里面)&「标准答案库」(我们自己用来评估的金标准的标准答案等)」,存储介质等不确定的项交中立评审分析。
## 决定
1. **四段流水线,段与段之间只通过磁盘上的文件交接。** 四段是:CEUS 定量(视频 → 中间文件)、装配两个库(原始资料 + CEUS 结果 → 两个库)、会诊(只读给专家的资料库)、评价(读会诊结果和标准答案库)。四段的成本差别很大:CEUS 定量要用显卡,装配只用 CPU,会诊调模型要花钱,评价几乎免费,分开之后改评价指标不必重新调模型,改拟合参数不必重跑 GPU。另有一个总入口按顺序调用四段。负责人认可了这个形状(见「背景」里的原话),两位评审也都这样建议。
2. **两个库和中间文件都用文件夹,不用数据库。** 负责人原话:「两个库和缓存都用文件夹」。评审给出的理由:专家看到的就是文字和图片,文件夹能直接打开检查专家到底看到了什么;三路各一个子目录,天然互相隔开;旧项目把隔离做在数据库里,结果不得不专门写程序扫描「读取代码会不会顺着引用摸到答案表」(旧 `tests/leakage_scan.py`)。
3. **调模型用实验室的 PolyGateway**(`https://gitea.iomgaa.online/iomgaa/PolyGateway`),每次会诊运行一份 SQLite 只追加账本,请求和回复全文按内容哈希另存为文件。负责人原话:「调用模型直接用https://gitea.iomgaa.online/iomgaa/PolyGateway」。用 SQLite 的理由来自两位评审:测重复运行的一致性时,同一病例、同一专家要重复调用多次,每一次(第 1 次、第 2 次……)都必须恰好记一次,中断后续跑时不漏也不重,这靠数据库的事务和唯一键最省事;ClinDB-ReviewBench 有一份约 270 行的现成实现(`src/clindb/ledger/`)可以借鉴。两位评审也都指出,按几千次调用的规模,JSONL 文件同样撑得住,选 SQLite 是为了数据正确,不是为了性能。PolyGateway 怎么安装(直接从 Gitea 装,还是像 ClinDB 那样放一份 wheel 在 `vendor/`)在写会诊任务书时定。
4. **CEUS 代码直接逐个模块迁移,不在本机重跑旧代码作对比基线。** 负责人原话:「“本机重跑旧代码作基线”不需要,直接一个个迁移就好」。迁移是否正确靠两样东西保证:随模块带过来的旧算法单测(旧项目约 274 个,不依赖数据库),以及每个模块迁移后对照旧代码逐段核对行为。迁移时按旧 debug 脚本(实际跑出 40 例结果的那几个脚本)的行为为参照,以下几处已知问题在对应模块的任务书里逐条决定改不改,改了就单独记录:
- 程序要把左半边造影画面上的绿框换算到右半边画面上,再交给 MedSAM2 当「病灶在这里」的提示。换算用的映射锚点按 1856 宽的素材写死成像素坐标(旧 `config/ceus_roi_default.yaml` 的 `mapping` 段、旧 `tests/unit/test_roi_contract.py:51`),而 40 例的视频有 5 种分辨率,提示会被平移约 −195 到 +165 像素,分割就从偏了的位置开始(两位评审各自用旧代码算出,未目视确认)。这个偏差在旧结果里不显眼,因为最终病灶轮廓有 75% 直接取自绿框本身(见 `待定问题/01`);
- 旧 debug 路径不裁画面黑边、识别分屏只用 25 帧抽样、整段视频读进内存,旧 `service.py`(旧产品的编排代码)则相反,两者结果可能不同;
- 造影画面在右半边的病例找不到绿框。
5. **数据的格式和口径以 40 例交付包为准。** 负责人原话:「因为数据改过很多版,所以你们需要对齐“40例交付包”那个。我知道程序开发的时候用的是1.2-TEST那个,但那其实也是老版程序过期的一个地方」。所以:
- 超声报告工作簿的「推注开始时间s」是**视频第 0 秒时已经推注了多少秒**(负责人 2026-10-16 说明)。F1、F2 分别取视频第 20 − X 秒和 60 − X 秒(X 为该值)。旧项目的 design 0046(`reference/CADx-4-CAD/research-wiki/design/`)把它读成「推注发生在视频第 X 秒」,那是按 `1.2-TEST` 定的口径,不沿用。
- `1.2-TEST` 只作参考样本保留,主要用处是看医生标注工作簿模板长什么样;它的列名、编码、时间口径都不作数。
6. **AT 目前没有来源,B0、D1 由程序从 TIC 曲线上算。** 40 例交付包里没有 AT,40 例的医生标注工作簿尚未交付。将来标注工作簿到了,其中的 AT 进标准答案库,用来检验程序算出的时刻准不准,不用来挑关键帧:标注工作簿属于答案那一侧,用它挑出的图交给专家,等于医生的判断渗进了专家资料,而且「程序算的时刻和医生标的差多少」这项检验也就失去了意义。旧回放脚本用医生 AT 当 D1(旧 `app/domain/ceus_roi/anchor_frames.py:199-206`),新代码不复现这条路。
7. **给专家的病例用化名,并打乱顺序;化名与真实病例号的对照只放在标准答案库。** 交付文件名里就带着病例号(如 `U001_roi2.mp4`);旧项目的资料曾按诊断分组录入,顺序本身就会暗示答案。这条是评审建议、Agent 提出,负责人未单独表态,写在这里备查。
8. **还没定的研究问题(目前是 01–03 三个研究问题,另有 04 待医生回答)记在 `research-wiki/待定问题/`,一个问题一份文件,定了就删,结论写进 `design/`。** 负责人原话:「研究层面的三个问题晚点再定,你可以在research-wiki中建立一个文件夹专门记录这种问题,处理完一个删除一个」。
## 不做的事
- 不把旧项目在远程跑出的 40 例结果拉回本机做逐例对比(随决定 4 放弃)。
- 不为中间文件的每一个阶段都做跨运行复用。中间文件全部写进 `work/`,但评审建议只有最贵的 GPU 分割结果按参数复用,其余阶段每次重算;其他阶段要不要复用,等实测耗时之后再说。
+207 -73
View File
@@ -1,113 +1,247 @@
# 结构:代码分哪些模块、谁能依赖谁 # 代码结构
这份文件说明数据从医院原始资料到评价结果经过哪几段、代码分成哪些模块、每个模块藏住什么决定、模块之间怎么依赖、各模块预期多大,以及按什么顺序把它们写出来。各模块对外的函数和类型,写到哪个模块时再补进第八部分。
来源: 来源:
- 负责人 2026-10-15:「我们后面发现其其实不需要(至少目前不需要)是一个工程产品,所以我觉得现在需要在本项目中重构一下」。
- 负责人 2026-10-15 选定检查强度「ClinDB 级」、注释用中文、本机为主(「目前以本地为主,以后会根据具体情况调整」)。
- 旧项目的研究内容:`reference/CADx-4-CAD/research-wiki/explanation/agent-system.md`、`ceus-roi-pipeline.md`、`data-and-inputs.md`。
本文是常青文档:模块划分变了就重写对应段落。为什么从产品改成研究原型、继承旧项目哪些东西,见 `../design/0001-从产品重构为研究原型.md`。 - 负责人 2026-10-15 定:旧项目不再做成工程产品,在本仓库按研究原型重写。原话:「我们后面发现其其实不需要(至少目前不需要)是一个工程产品」(design 0001)。
- 负责人 2026-10-15 定:协作环境和文档体系参考 ClinDB-ReviewBench 与 dissect3;检查强度取 ClinDB 那一档;一个 uv 环境,MedSAM2 与 torch 装在里面,靠导入边界隔离(design 0001 第 4、7 条)。
- 负责人 2026-10-16 定:数据流是「医院原始资料->中间文件(可以有可以没有)->「给专家的资料库」(给agent的东西都在里面)&「标准答案库」(我们自己用来评估的金标准的标准答案等)」;两个库和中间文件都用文件夹,原话:「两个库和缓存都用文件夹」;调模型用实验室的 PolyGateway;CEUS 代码「直接一个个迁移」;数据格式和口径以 40 例交付包为准(design 0002)。
- 其余内容(模块名单、边界、依赖、规模、推进顺序)是 Agent 2026-10-16 的提议;规模在对应模块动手前由负责人逐个确认。
## 一、从一个病例走一遍 ---
先看数据怎么流过整个系统,再看代码怎么切。 ## 一、切分原则
医院按批次交付资料,病种的全称见 `AGENTS.md`「项目是什么」(目前是 `data/交付包_20260908_40例/`)。每个病例有三路资料: 不按流水线的步骤切,也不按技术层(接口、数据库、服务)切,按「以后可能会改的设计决定」切,一个模块藏住一个决定。换掉某个决定时,只改这一个模块。例如「用哪个分割模型」是一个可能会改的决定:把它藏进 segment,CEUS 的其余算法只接收「每帧一张病灶掩码」,不知道背后是 MedSAM2 还是别的模型;以后换模型只改 segment。这个原则来自 Parnas 1972 年的信息隐藏论文,ClinDB-ReviewBench 用的也是这个分法。
- **临床**:一张工作簿里的一行,性别、年龄、症状、既往史、抽血化验值。 旧项目按技术层切(`app/api`、`app/persistence`、`app/workers`、`app/providers`、`app/domain`)。按层切的问题是同一个决定散在几层里:
- **超声**:一段 CEUS 视频(`U0xx_roi2.mp4`)、一张医生截的关键图、一份超声报告。报告分「所见」(描述看到了什么)和「印象 / 结论」(医生的诊断倾向)两段。
- **MR**(磁共振):一份 MR 报告文本。目前没有 MR 和 CT(计算机断层扫描)图像。
答案(病理结论)也在这些工作簿的某一列里,必须在读取的那一刻就分出去。 - 「专家能看见什么」散在四处:Excel 列白名单在 `app/providers/clinical.py`、`modalities.py`,输入包的形状在 `app/domain/input_packages.py`,按病例拼装在 `app/domain/input_readers.py`,入库分区在 `app/domain/ingestion*.py`。
- CEUS 处理有两条互相分叉的路径:产品编排 `app/domain/ceus_roi/service.py`,以及实际跑出 40 例结果的 `debugs/` 脚本。两者在裁黑边、识别分屏用的帧、关键帧在哪一步算这几处行为不同(design 0002 决定 4)。
处理分四步: 判断一刀切得对不对,问这 7 个问题:
1. **CEUS 视频 → 定量结果。** 超声机录造影时屏幕左右分成两半,一半是造影画面、一半是普通灰阶画面,先识别分屏、取出造影那半;医生在病灶上画了一个绿框,用它告诉程序病灶在哪;再用 MedSAM2(一个医学图像分割模型)把病灶轮廓逐帧追踪下去;病灶里的囊液不进造影剂、在造影画面上发黑,这部分(暗区)要扣掉,剩下的实性部分的平均亮度随时间的曲线就是 TIC。再用 Lognormal(对数正态)曲线拟合,取关键时刻的帧,算出 11 个灌注指标,如峰值强度、达峰时间、洗入速率(仿照商用造影定量软件 VueBOX 的口径;为什么选这套口径和 Lognormal 模型,见旧项目 design 0042、0043、0047,迁移时重写进本仓库)。这一步不涉及模型 API,只用本机显卡。 1. 这个模块藏住的是哪一个决定?说不出来,或者说出来的是「第几步」,就是切错了。
2. **整理三份输入。** 每个病例分出三份互相看不见的输入:临床那份、超声那份(含第 1 步的定量结果、关键图和报告的「所见」段;关键帧图像给不给、怎么给,第 4 步定)、MR 那份(报告的「所见」段)。答案和医生的结构化判读列只进评价侧。 2. 换掉这个决定,要改几个模块?应该只改 1 个。
3. **会诊。** 三个 AI 专家各读自己那份输入。按 2026-08-02 和临床方的会议纪要(双方共同认下的结论;纪要只写了顺序和「主要看超声与 MR 两路能否一致」,没写为什么这样排),流程是单轮顺序:超声专家先给影像信息和判断,MR 专家再给,临床专家补充临床信息;主要看超声和 MR 两路能否一致。 3. 对外露出的函数和类型,是否比内部实现简单得多?
4. **评价。** 拿会诊结论对病理金标准算准确率等指标;拿专家抽出的字段对医生的判读列算抽取准确率。 4. 依赖有没有绕成圈?下层能不能离开上层单独测试?
5. 纯计算的模块里,有没有读写文件、网络或模型?
6. 每样外部的东西(交付包的 Excel、MedSAM2、模型服务、账本的 SQLite),是不是只有一个模块直接碰它?
7. 同一份知识(哪一路能看见哪些列、诊断编码、关键帧的时刻定义)是不是只在一处定义?
## 二、切分原则 ---
模块按「以后可能会改的设计决定」切,每个模块把一个决定藏在里面,决定变了只改这一个模块(Parnas 1972 的信息隐藏)。不按上面的四步切,因为步骤会调整,而「交付包长什么样」「用哪个分割模型」这类决定和步骤无关。 ## 二、项目的完整范围
另一条原则来自硬性约束 1:**答案在哪、谁能读,由导入边界机器检查**,不靠自觉。所以读金标准单独成一个模块,只有评价模块能导入它。 项目做胰腺囊性肿瘤的鉴别诊断:三个 AI 专家各看一路资料,模拟多学科会诊,判断病灶属于 IPMN、SPN、MCN、SCN 四类中的哪一类(全称见 `AGENTS.md`),结论对病理金标准评价;同时评价专家从资料里抽取字段的可靠性。模块要覆盖全部四段,不只是旧项目已经做的部分。
| 段 | 做什么 | 输入 → 输出 | 旧项目状态 |
| --- | --- | --- | --- |
| CEUS 定量 | 从超声造影视频里追踪病灶、画出亮度曲线、拟合、挑关键帧、算灌注指标 | `data/` 的视频 → `work/` | 算法约 7.3k 行,40 例跑通 34 例(旧项目远程结果清单);两条路径行为不一致,映射锚点按 1856 宽素材写死(design 0002 决定 4) |
| 装配两个库 | 读交付包,按白名单把每路允许的内容写进给专家的资料库,把答案写进标准答案库 | `data/` + `work/` → `library/`、`answers/` | 列白名单和编号规范化有,但按旧样本 `1.2-TEST` 写死了工作表名和列名,读不了 40 例交付包 |
| 会诊 | 三个专家各读自己那一路,按会诊流程给出判断 | `library/` → `runs/` | 没有,只有三版设计文档 |
| 评价 | 会诊结论对金标准;专家抽取的字段对医生标注 | `runs/` + `answers/` → `runs/<运行号>/eval/` | 没有;写过一套评估骨架,因缺陷和无人使用而删除 |
几个贯穿全文的词:
- **给专家的资料库**(`library/`):AI 专家能看到的一切。每次装配是一个新目录,叫一次**构建**。
- **标准答案库**(`answers/`):病理诊断、化名与真实病例号的对照、跨表一致性报告,将来还有医生标注工作簿。只给评价用。
- **中间文件**(`work/`):CEUS 定量的工作产物,可以删掉重算。
- **运行**:一次会诊或评价,产物放在 `runs/` 下一个新目录里,只追加不删。
- **账本**:一次会诊运行里每次调用模型的记录(请求、回复、用量、结局),存为 SQLite,只追加。
- **关键帧**:从 CEUS 视频里挑出交给超声专家的 7 张图,定义见第四部分。
---
## 三、模块 ## 三、模块
| 层 | 模块 | 藏住的决定 | 旧项目对应 | 预期规模 | | 层 | 模块 | 藏住的决定 | 旧项目里的对应内容 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- |
| 入口 | `scripts/`(不在包内) | 一次研究步骤怎么组合:参数从哪来、产物写到哪个运行目录 | `debugs/*.py` | 每个 ≤ 300 行 | | 入口 | `scripts/`(不在包内) | 人怎么操作:每段一个脚本(`ceus_quant`、`build`、`consult`、`evaluate`),另有一个按顺序调用四段的总入口;读配置文件,把模块装配起来 | `debugs/t1_local_debug.py`、`debugs/tic_lognormal_replay.py` |
| 组织 | `cadx.consult` | 会诊流程:几个专家、谁先说、看到别人的什么 | 无(从未实现) | 600 | | 组织 | build | 两个库怎么装配:每一路放哪些内容(可见性的唯一决定处)、化名怎么取、病例顺序怎么打乱、清单记什么。全项目只有它同时碰得到原始资料和答案 | `app/domain/input_readers.py`、`ingestion*.py` 的装配部分 |
| 组织 | `cadx.evaluate` | 指标口径、统计方法 | 无(已删除) | 400 | | 组织 | consult | 会诊怎么进行:几个专家、谁先说、后发言的看得到前面的什么、什么时候结束;「调一次模型」的统一入口(先存请求、记开始、调用、存回复、记结束)也在这里 | 没有 |
| 资料 | `cadx.cases` | 交付包的磁盘格式、列名、病例号形状、「结论」段怎么剔除、每路看得见哪些列 | `app/providers/clinical.py` 等、`input_readers.py` | 600 | | 组织 | evaluate | 评价口径:指标、统计方法、字段级对照怎么计分 | 没有(`core/eval` 已删) |
| 资料 | `cadx.gold` | 金标准在哪一列、数字编码对应哪一类(编码由临床方定,可能会改;取值只写在这个模块的代码里) | `config/runtime.yaml` 的 `diagnosis_coding` | 100 | | 格式 | library | 给专家的资料库的目录布局、文件格式和读取;按路分别读出,一路一个对象 | `app/domain/input_packages.py` 的数据形状 |
| 外部 | `cadx.llm` | 用哪家模型服务、怎么调用、怎么记录请求和回复 | `model_gateway_config.py`(只借思路) | 300 | | 格式 | answers | 标准答案库的目录布局、文件格式和读取 | 旧库里的金标准表 |
| 外部 | `cadx.segment` | 用哪个分割模型(目前 MedSAM2)、权重在哪 | `debugs/medsam2_local_track.py` | 300 | | 格式 | verdicts | 会诊产物(各路意见、会诊结论)的文件格式和读取;consult 写、evaluate 读 | 没有 |
| 外部 | `cadx.runs` | 运行目录的命名、快照里记什么、单进程写锁 | 无 | 200 | | 外部 | delivery | 交付包长什么样:目录和文件名、工作表与列名、病例编号的几种写法和规范化、诊断编码、报告「印象 / 结论」段怎么认、跨表一致性怎么核对。只读,不决定谁能看见什么 | `app/providers/clinical.py`、`modalities.py`、`workbook.py`、`delivery.py` |
| 纯计算 | `cadx.ceus` | ROI 与 TIC 的算法和参数:分屏、绿框、暗区、曲线、拟合、关键帧、灌注指标 | `app/domain/ceus_roi/`(除 `service.py`)、`app/providers/video_decoder.py` | 6000 | | 外部 | segment | 用哪个分割模型、权重在哪、怎么在显卡上把提示传播成逐帧掩码 | `debugs/medsam2_local_track.py`、`roi_inference/` 的后端 |
| 外部 | llm | 经 PolyGateway(实验室的模型调用库,管外部错误的重试和超时)调模型;交出服务商返回的精确版本号、响应编号、用量、结束原因。只管调用,不写账本 | `app/providers/model_gateway_config.py`(只借思路);ClinDB 的 `llm/` |
| 外部 | ledger | 账本怎么存:SQLite 只追加、大块内容按哈希存成文件、同一目录同时只许一个进程写、找出开始了没结束的调用 | 没有;ClinDB 的 `ledger/` |
| 外部 | runs | 运行目录怎么命名、运行快照记什么(git 提交号、配置、所用构建、提示词哈希、模型版本、随机种子) | 没有 |
| 纯计算 | ceus | CEUS 定量的全部算法和参数:分屏、绿框、坐标映射、多边形、暗区、TIC 采样、Lognormal 拟合、关键帧、灌注指标、出图;视频解码也在这里 | `app/domain/ceus_roi/`(除 `service.py`)、`app/providers/video_decoder.py` |
规模是粗估,按旧项目同类代码的行数推算,每个模块的任务书动手前由负责人确认。`cadx.ceus` 偏大,是因为旧项目 `app/domain/ceus_roi/` 共 8770 行,去掉编排和数据库实体(`service.py` 1440 行)后约 7.3k 行,迁移时再删掉只为产品层服务的部分(2026-10-15 用 `wc -l` 量)。 暂不设共用词汇模块。一样东西要被两个互相不能引用的模块用到,才另立一处放它;目前跨模块的类型各有归属(资料库格式归 library、答案格式归 answers、会诊产物格式归 verdicts)。配置的字段定义和校验归使用它的模块:CEUS 参数归 ceus(旧 `research_config.py`),会诊的实验配置归 consult;每个字段必填,不补默认值(`AGENTS.md`「不设默认值」)。
这里没有旧项目的数据库、HTTP 接口、任务队列、对象存储、迁移脚本。运行产物直接写成文件,放在 `runs/` 下的运行目录里。 依赖只能往下,同一行的模块之间不互相依赖:
## 四、谁能依赖谁
上层可以导入下层,同一层的模块互不导入(例外:`gold` 只给组织层的 `evaluate` 用):
``` ```
入口 scripts/ 入口 scripts/
组织 consult evaluate 组织 build consult evaluate
资料 cases gold(只给 evaluate) 格式 library answers verdicts
外部 llm segment runs 外部 delivery segment llm ledger runs
纯计算 ceus 纯计算 ceus
``` ```
具体边界: 谁具体能依赖谁:
- **只有 `cadx.evaluate` 能导入 `cadx.gold`。** 这是硬性约束 1 的机器检查。会诊那一侧拿不到读金标准的函数,也就不可能把答案递给专家。 | 模块 | 可以依赖 |
- `cadx.consult` 只经 `cadx.cases` 的公开接口拿输入包,不自己读工作簿。 | --- | --- |
- 只有 `cadx.segment` 导入 `torch`、`sam2`。torch 和 MedSAM2 将来装在同一个 uv 环境里(CEUS 迁移时加入,见 design 0001 第 7 条),但会诊和评价那一半的代码碰不到它们。 | ceus、delivery、segment、llm、ledger、runs | 不依赖其他 `cadx` 模块 |
- 只有 `cadx.llm` 导入模型服务的 SDK。 | library、answers、verdicts | 不依赖其他 `cadx` 模块 |
- 只有 `cadx.cases` 和 `cadx.gold` 导入 `openpyxl`(读 Excel)。 | build | delivery、ceus(读 CEUS 结果的类型)、library、answers、runs |
- `cadx.ceus` 不导入 `cadx` 的其他模块。它只做计算和读视频,分割结果由调用方传进来。 | consult | library、verdicts、llm、ledger、runs |
| evaluate | answers、verdicts、runs |
| scripts/ | 任何模块(不在导入检查范围内,见第五部分) |
这些边界写成 `pyproject.toml` 里的 import-linter 契约,**随模块落地逐条加入**:import-linter 对还不存在的模块无从检查。目前只有一条:`cadx` 整体不导入 `torch`、`sam2`;`cadx.segment` 落地时改成「除 `cadx.segment` 外」。 ### 几条边界
**契约里没有的保护不要当成机器守住了。** 比如 `scripts/` 不在包内,import-linter 管不到它;一个脚本完全可以同时导入 `gold` 和 `consult`。脚本放在包外是有意的:「跑会诊再评分」这种组合天然要同时碰两边,放进包里也得给它开例外。所以脚本里的隔离靠审查,以及第 3 步的泄露扫描测试(直接检查发给模型的输入里有没有答案)。 - **专家能看见什么,只在 build 一处决定。** build 按白名单把每路允许的内容写进 `library/`,此外任何模块都不挑选给专家的材料。consult 只拿到某一次构建的路径,经 library 按路读出;它不依赖 delivery、answers、build,读原始资料和答案的代码对它来说根本不存在。报告的「印象 / 结论」段(医生写的诊断倾向)由 delivery 认出、由 build 剔除,`library/` 里只有「所见」描述。
- **三路互相隔开。** 每个专家的提示词只用自己那一路的对象拼。consult 里拼某一路提示词的函数只接收那一路的对象,不接收整个病例。这是研究设计本身:模拟各科医生各看各的检查结果,再在会诊时交换意见。会诊时一路能看到另一路的什么,由 consult 的会诊流程决定,在会诊的任务书里定。
- **我们自己调模型只有一个入口:consult 的「调一次模型」。** 它按固定顺序做五步:把完整请求存进账本的大块存储、写「调用开始」、经 llm 调用、把完整回复存进大块存储、写「调用结束」。llm 只管调用,不写账本,所以任何一次模型调用都绕不开登记。中断后重启时,从账本找出开始了没结束的调用,记为「状态未知」,不重做已有结果的调用。做重复运行(同一病例、同一专家调多次,测一致性)时,每一次都恰好登记一次。
- **CEUS 算法不知道分割模型是谁,也不管中间文件放哪。** ceus 接收视频路径、参数和分割结果,交出曲线、拟合、关键帧和指标;中间文件写在哪、按什么复用,由 `scripts/ceus_quant.py` 决定(目前只有它一个使用者,出现第二个使用者再收进模块)。segment 只管「给提示、出逐帧掩码」,不认识分屏和绿框。
- **医生的判断不进 CEUS 定量。** 医生标注工作簿里的时刻(如造影剂到达时间 AT)属于标准答案库,ceus 和 build 都不读它;关键帧全部由程序自己算或按推注时间取(design 0002 决定 6)。交付视频里医生画的绿框是 CEUS 定量的输入,它能否作为输入、在最终病灶轮廓里占多大分量,记在 `../待定问题/01-医生画的绿框能否作为输入.md`。
- **评价只读文件,不重新调模型。** evaluate 读 `runs/` 里的会诊产物(经 verdicts)和 `answers/`,改指标或统计方法不需要重跑会诊。
每个模块是一个子包,内部文件加 `_` 前缀,对外只经 `__init__.py` 的 `__all__` 暴露。这样别的模块能用什么一眼可见,模块内部怎么改不牵连外面。 ---
## 五、目录 ## 四、两个库与 CEUS 中间文件的内容
### 给专家的资料库(`library/<构建号>/`)
```
library/<构建号>/
manifest.json 构建清单:每个文件的 SHA-256、构建时的 git 提交、配置哈希、所用 CEUS 结果的来源
cases/<化名>/
clinical/clinical.json 临床那一路
ultrasound/ 超声那一路:report.txt、key.png、keyframes/*.png、tic.json
mr/report.txt MR 那一路
```
每一路能看见什么:
| 内容 | 临床 | 超声 | MR |
| --- | --- | --- | --- |
| 性别、年龄 | 看得见 | 看不见 | 看不见 |
| 5 个症状、5 项既往史、5 项化验(原值与是否异常) | 看得见 | 看不见 | 看不见 |
| 超声报告「所见」段、医生截的关键图 | 看不见 | 看得见 | 看不见 |
| CEUS 关键帧、TIC 灌注指标 | 看不见 | 看得见(给哪些指标、几张图由会诊任务书定) | 看不见 |
| MR 报告「所见」段 | 看不见 | 看不见 | 看得见 |
| 病理诊断、报告「印象 / 结论」段、医生标注工作簿、真实病例号、Excel 行序、姓名 | 看不见 | 看不见 | 看不见 |
性别、年龄只给临床一路:医院在两份报告工作簿里也复制了这两列,但影像两路先看到它们会被人口学先验带偏(旧项目 design 0026 决定三)。
病例用化名,目录顺序打乱:交付文件名里带病例号(如 `U001_roi2.mp4`),行序可能和诊断有关。图片写入时去掉文件元数据。构建完不再修改;要改就做一次新构建。
### 标准答案库(`answers/<构建号>/`)
与同一次构建的资料库一一对应,放:病理诊断(按 delivery 认出的诊断编码解码)、化名与真实病例号的对照、跨表一致性报告(各表之间性别、年龄、诊断对不对得上;40 例中有 6 例对不上,见 `../待定问题/04-交付数据的跨表不一致.md`),将来加医生标注工作簿。
### CEUS 中间文件(`work/`)
CEUS 定量按下面的阶段进行(旧项目的阶段划分,迁移时逐模块核对):
| 阶段 | 做什么 | 跨运行复用 |
| --- | --- | --- |
| 1 解码与分屏 | 读视频;超声机录造影时屏幕左右分两半,一半造影画面、一半普通灰阶画面,找出分界 | 否 |
| 2 提示帧 | 在造影画面上找医生画的绿框,挑出绿框位置有变化的帧,换算到另一半画面上 | 否 |
| 3 分割 | segment 用 MedSAM2 把提示传播成逐帧病灶掩码;需要显卡,最贵 | **是**,按「视频内容哈希 + 阶段 1–3 的参数 + 模型权重哈希」存 |
| 4 多边形 | 掩码转成平滑轮廓,与绿框融合 | 否 |
| 5 暗区 | 找出病灶里不进造影剂、发黑的囊液部分 | 否 |
| 6 TIC 采样 | 逐帧取病灶(去不去暗区两种)平均亮度,得到 TIC(时间-强度曲线) | 否 |
| 7 拟合 | 用 Lognormal(对数正态)曲线拟合 TIC,过质量门 | 否 |
| 8 关键帧 | 按下面的定义挑 7 张 | 否 |
| 9 灌注指标 | 从拟合曲线算 11 个指标(峰值强度、达峰时间、洗入速率等,口径仿照商用造影定量软件 VueBOX) | 否 |
只有阶段 3 跨运行复用,其余每次重算:阶段 3 要显卡,最贵;其余阶段在 CPU 上算。改了后段参数不必重跑显卡。
关键帧:
| 帧 | 含义 | 怎么取 |
| --- | --- | --- |
| B0 | 刚开始变亮之前 | 从拟合曲线算出到达时刻,取其前 |
| D1 | 开始变亮 | 拟合曲线上的到达时刻 |
| D2 | 洗入拐点(变亮最快处) | 拟合曲线 |
| D3 | 最亮 | 拟合曲线 |
| D4 | 洗出拐点(变暗最快处) | 拟合曲线 |
| F1 | 推注后第 20 秒 | 视频第 20 − X 秒 |
| F2 | 推注后第 60 秒 | 视频第 60 − X 秒 |
X 是超声报告工作簿的「推注开始时间s」,含义是视频第 0 秒时已经推注了多少秒(design 0002 决定 5)。某个时刻落在视频之外,或附近没有可用帧,该帧记为未定位并写明原因,不拿别的帧补。各时刻的容差窗口和出处(哪些来自和临床方约定的口径,哪些是旧项目工程侧定的)在迁移关键帧模块时逐个核对,写进 ceus 的参数文件注释。
---
## 五、目录与检查
| 位置 | 内容 | 进 git | | 位置 | 内容 | 进 git |
| --- | --- | --- | | --- | --- | --- |
| `src/cadx/` | 上面的模块 | 是 | | `src/cadx/` | 一个 Python 包,每个模块一个子包;别的模块只能经子包的 `__init__.py` 引用它,内部文件加 `_` 前缀 | 是 |
| `scripts/` | 研究步骤的入口脚本 | 是 | | `scripts/` | 入口脚本 | 是 |
| `tests/` | 测试;需要显卡的标 `gpu`,花钱的标 `live` | 是 | | `tests/` | 不花钱、不要显卡的测试;需要显卡的标 `gpu`,花钱的标 `live`,两者不进 `make check` | 是 |
| `config/` | 算法参数、模型服务清单(只写环境变量名) | 是 | | `config/` | CEUS 参数、会诊的实验配置、模型用途清单(密钥只写环境变量名) | 是 |
| `prompts/` | 版本化提示词 | 是 | | `prompts/` | 版本化的提示词文件;每次运行记下所用提示词的哈希 | 是 |
| `research-wiki/` | 文档,见其 `README.md` | 是 |
| `data/` | 医院交付的原始资料 | 否 | | `data/` | 医院交付的原始资料 | 否 |
| `runs/` | 每次运行一个目录,含快照和全部产物 | 否 | | `library/`、`answers/` | 两个库,每次构建一个目录 | 否 |
| `models/` | 模型权重(MedSAM2) | 否 | | `work/` | CEUS 中间文件 | 否 |
| `reference/` | 旧项目副本、外部代码仓库 | 否 | | `runs/` | 会诊与评价的运行,只追加不删 | 否 |
| `reference-pdf/` | 文献原文 | 否 | | `models/` | 模型权重 | 否 |
| `reference/`、`reference-pdf/` | 旧项目副本、外部代码、文献原文 | 否 |
不进 git 的目录由 `.gitignore` 和 pre-commit(提交前自动运行的检查)里的 `local-only-paths` 一项两道拦截,`git add -f` 也提交不了。 不进 git 的目录由 `.gitignore` 和 pre-commit 的 `local-only-paths` 两道拦截,`git add -f` 也提交不了。
## 六、推进顺序 文件格式:数组用 npz(读取时 `allow_pickle=False`,pickle 读取时会执行任意代码),每例的标量和摘要用 json,跨病例的表用 csv,图用 png。缺失值写 null,不写 NaN 或 0:0 是合法的测量值,NaN 在 json 里不合法。
导入边界用 import-linter 检查,写进 `make check`。契约在 `pyproject.toml`,检查范围是 `src/cadx`,**不含 `scripts/` 和 `tests/`**。下面只列契约实际强制的,没有的保护不写:
- `cadx` 不引用 `torch`、`sam2`。
第三部分的依赖表和下面几条外部库独占,随模块落地逐条写成契约(import-linter 只能检查已经存在的模块):只有 delivery 引用 `openpyxl`;只有 segment 引用 `torch`、`sam2`;只有 llm 引用 `polygateway`;只有 ledger 引用 `sqlite3`。
契约管不到的,不要当成机器守住了:
- `scripts/` 不在检查范围内,一个脚本可以同时读 `answers/` 和调会诊。consult 在启动时核对:传进来的路径必须在 `library/` 下的某次构建里,否则报错。会诊落地时另写一条泄露扫描测试,检查发给模型的请求全文里没有答案和真实病例号。
- 三路互相隔开靠 consult 的函数签名(拼某一路提示词的函数只接收那一路的对象),import-linter 管不到同一模块内部,靠审查和上面的泄露扫描。
- ceus 是纯计算,只有「不引用其他 `cadx` 模块」能写成契约;它要读视频文件,`open()` 之类内置函数拦不住。
Python 环境用 uv;安装与检查命令见 `../guides/开发环境.md`。
---
## 六、规模
下表是粗估,参考了旧项目对应部分的行数。**计数口径**:一个模块的行数是 `cat src/cadx/<模块>/*.py | wc -l`,不含测试和提示词文件;旧项目的数按同样口径数(2026-10-16)。动手写某个模块之前,由负责人逐个确认;写的过程中超过预期一倍,就停下来检查是需求本身复杂,还是专门路径在变多。
| 模块 | 预期行数 | 实际行数 | 旧项目对应部分的行数 |
| --- | --- | --- | --- |
| ceus | 6000–7500(基本照搬旧算法,去掉只为产品层服务的部分) | — | `ceus_roi/` 除 `service.py` 7330,`video_decoder.py` 135 |
| segment | 150–300 | — | `medsam2_local_track.py` 240 |
| delivery | 400–700 | — | `clinical.py` 266、`modalities.py` 522、`workbook.py` 99、`delivery.py` 134,共 1021,其中入库与数据库部分不迁 |
| library | 150–300 | — | `input_packages.py` 104 |
| answers | 100–200 | — | — |
| verdicts | 100–200 | — | — |
| build | 300–500 | — | `input_readers.py` 329 的装配部分 |
| llm | 200–450 | — | ClinDB `llm/` 415 |
| ledger | 200–300 | — | ClinDB `ledger/` 274 |
| runs | 100–250 | — | — |
| consult | 500–1000(会诊口径定后重估) | — | — |
| evaluate | 300–600(评价口径定后重估) | — | 已删的 `core/eval` 622 |
| scripts/ 每个 | ≤ 300 | — | `t1_local_debug.py` 762、`tic_lognormal_replay.py` 627 |
---
## 七、推进顺序
1. **骨架**(已完成):协作规则、文档体系、工具链。 1. **骨架**(已完成):协作规则、文档体系、工具链。
2. **CEUS 定量**:迁移 `cadx.ceus`、`cadx.segment`、`cadx.runs` 和对应脚本。验收标准是在本机复现旧项目的 40 例 ROI 追踪与 TIC 结果和 `1.2-TEST`(只有 1 例,但它是唯一带造影剂到达时间的病例)的 Lognormal 回放,逐例对比旧产物。旧产物还在远程,对比要用的摘要和轨迹文件在那份任务书里按需拉取,视频不拉。 2. **装配两个库(先不含 CEUS 结果):delivery → library、answers → runs 的快照部分 → build。** 先做它,是因为它最轻,做完就能用文字资料试会诊;而且隔离(专家看不到答案)是整个研究成立的前提,越早有代码和测试守住越好。顺带产出跨表一致性报告,给 `待定问题/04` 提供证据。
3. **资料与答案隔离**:`cadx.cases`、`cadx.gold`,加上泄露扫描测试(输入包里不出现金标准、结论段、病例号)。 3. **CEUS 定量:ceus 按旧模块逐个迁移(带上旧的算法单测)→ segment → `scripts/ceus_quant.py` → build 加入 CEUS 结果。** 旧算法单测约 274 个,不依赖数据库,随模块带过来,作为迁移正确性的主要依据(design 0002 决定 4)。已知问题(映射锚点写死、两条旧路径的行为差别、造影画面在右半边)在任务书里逐条定。
4. **会诊**:`cadx.llm`、`cadx.consult`。动手前要先定模型服务。 4. **会诊:ledger → llm → consult 的「调一次模型」→ verdicts → consult 的会诊流程。** 动手前先专门讨论定下会诊口径:后发言的看得到什么、怎样算两路一致、超声一路给哪些指标和图、模型与重复次数等(旧项目留下的未裁决问题)。
5. **评价**:`cadx.evaluate`。 5. **评价:evaluate。** 动手前先定指标、统计方法,以及 `待定问题/02`(参数何时冻结、开发集与测试集怎么分)、`待定问题/03`(病例排除标准)。
第 2 步和第 3 步互不依赖,可以换顺序。 第 2 步和第 3 步互不依赖,可以换顺序;第 3 步完成后,build 要重新构建一次才把 CEUS 结果装进资料库。
## 七、还没定的事 ---
- **用哪家模型服务。** 旧项目经实验室的模型调用库 PolyGateway 调通义千问;更早的讨论选过 Claude Agent SDK。第 4 步任务书里定。 ## 八、各模块对外的函数与类型
- **专家先抽事实再判断,还是一步出判断。** 旧项目设计过两段式(先抽出可追溯的医学事实,再据此判断四分类),抽出的事实还能拿来对医生判读列算抽取准确率;但一行代码都没写。第 4 步讨论。
- **医生标注工作簿**(见 `AGENTS.md` 硬性约束 1)。40 例的标注工作簿还没交付,Lognormal 回放需要的造影剂到达时间(AT)和注射时间在里面。到之前只有 `1.2-TEST` 的 1 例能做完整回放。 写到哪个模块时,再把它对外的函数和类型补在这里。
- **分屏里造影画面在左还是在右。** 旧项目 40 例跑通 34 例;失败的 6 例里 3 例(013、029、036)是左半边找不到绿框,013 已确认造影画面在右半边,配置却写死取左边;另 3 例(007、011、033)是分屏中缝对比度不够、识别失败(出自旧项目远程 `data/交付包_20260908_40例_result/结果清单_20261016.md`,2026-10-15 读过)。是否自动判别左右未定。
@@ -0,0 +1,27 @@
# 01 医生画的绿框能否作为输入
提出:2026-10-16 中立评审(架构评审 B),负责人同意留待以后定。
## 问题
交付的 CEUS 视频里,医生已经在病灶上画了绿框。旧项目的 ROI(病灶区域)主要来自这个绿框:非提示帧上最终轮廓 = MedSAM2 结果 × 0.25 + 绿框先验 × 0.75,提示帧上 100% 是绿框(旧代码 `reference/CADx-4-CAD/app/domain/ceus_roi/polygons.py:531-541`,配置 `config/ceus_roi_default.yaml` 的 `roi_shape` 段)。
## 为什么要紧
- 决定论文能不能说「AI 自动分割病灶」。如果 ROI 实际上是医生画的,论文要么如实写「医生给定病灶位置」,要么补一组「只用绿框 vs 融合 MedSAM2」的对比。
- 决定 CEUS 迁移时融合权重怎么定。
- 负责人 2026-06-09 曾说「生产环境不预给 ROI」(记忆),而现在的交付视频都带绿框,两者需要对齐。
## 选项
1. 绿框是合法输入(相当于医生框选病灶,系统负责后续定量),论文如实写明。
2. 绿框只作第一帧种子,其余交给分割模型,需要重新定融合权重并验证。
3. 不用绿框,系统自己找病灶(难度和工作量都大得多)。
## 倾向
暂无。
## 等谁定
负责人,可能要问临床方:今后真实使用时医生是否愿意先框一下。
@@ -0,0 +1,25 @@
# 02 CEUS 参数何时冻结,开发集和测试集怎么分
提出:2026-10-16 两位中立评审都提到,负责人同意留待以后定。
## 问题
CEUS 定量有很多参数(拟合质量门、暗区阈值等),旧项目的拟合参数标注为「工程验收占位,代表性开发集标定后一次性锁定」(旧 `config/ceus_roi_default.yaml` 的 `fitting` 段)。会诊的提示词和流程也要调。如果一边在 40 例上调这些参数、一边看同一批 40 例的诊断准确率,就是隐性泄露:结果会偏乐观,审稿人会质疑。
## 为什么要紧
决定论文里的准确率能不能信。必须在看诊断结果之前定。
## 选项
1. CEUS 参数只按它自身的质量指标调(拟合是否通过、与医生 AT 的误差),不看诊断结果;40 例整体当开发集,等后续病例(约 200 例)再报正式结果。
2. 从现有病例中留出一部分作测试集,锁定后不再碰。
3. 不切分,只如实记录在这批病例上试过多少种配置(旧项目当时的做法,旧 `research-wiki/explanation/evaluation.md` 第 7 节自认「理由只够一半」)。
## 倾向
评审 B 倾向选项 1。负责人未定。
## 等谁定
负责人,可能要和临床方商量(旧项目里这件事挂在 Linear《研究规则》上)。
@@ -0,0 +1,27 @@
# 03 病例排除标准
提出:2026-10-16 中立评审(架构评审 B),负责人同意留待以后定。
## 问题
40 例里有几类特殊病例,怎么处理需要事先写定:
- 旧代码 CEUS 追踪失败 6 例:3 例分屏识别失败,3 例在左半边找不到绿框(其中 1 例已确认造影画面在右半边)。来源:远程 `data/交付包_20260908_40例_result/结果清单_20261016.md`,2026-10-15 读过。
- 交付说明标明 3 例用肝实质替代正常胰腺作参照、1 例没有参照(`data/交付包_20260908_40例/交付说明_40例_20260908.md`)。
## 为什么要紧
如果看了诊断结果之后再决定排除哪些病例,就是选择偏倚。标准必须在看结果之前写定。
## 选项
- 技术失败的病例:修好算法再纳入 / 记为「超声不可评价」但仍参与会诊 / 排除。
- 参照组织特殊的病例:纳入并分层分析 / 排除依赖参照的指标 / 整例排除。
## 倾向
暂无。
## 等谁定
负责人,可能要和临床方商量。
@@ -0,0 +1,19 @@
# 04 交付数据的跨表不一致
提出:2026-10-15 深读旧项目时发现(「资料与答案隔离」那条线),负责人尚未看过。
## 问题
40 例交付包里,有 6 例在临床工作簿和超声报告工作簿之间对不上:年龄 6 例不一致,诊断 2 例不一致,性别 4 例不一致(性别按临床表 1=男、2=女 推断,编码未经确认)。超声与 MR 两份报告表之间诊断 40/40 一致,所以对不上的只在临床表这一侧。核对脚本只输出计数,2026-10-15 运行。
## 为什么要紧
- 可能是临床表的编号和报告表不是同一个病人。如果是,临床专家看到的是别人的资料,诊断金标准也可能取错。
## 选项
问医生:这 6 例临床表的编号是否对应同一个病人;以哪份表的诊断为准。
## 等谁定
临床方回答,负责人转达。
+7
View File
@@ -0,0 +1,7 @@
# 待定问题
还没定、但迟早要定的问题,一个问题一份文件,文件名 `NN-简短主题.md`。
- 每份写清:问题是什么、为什么要紧(不定会影响什么)、有哪些选项、目前的倾向(注明是谁的倾向)、证据在哪、等谁来定。
- **定了就删掉这份文件**:结论写进 `design/` 的新决策文档(或负责人认可的其他位置),删除和写入放在同一个提交里,提交说明写明去向。
- 编号只增不复用。