负责人 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>
22 KiB
代码结构
这份文件说明数据从医院原始资料到评价结果经过哪几段、代码分成哪些模块、每个模块藏住什么决定、模块之间怎么依赖、各模块预期多大,以及按什么顺序把它们写出来。各模块对外的函数和类型,写到哪个模块时再补进第八部分。
来源:
- 负责人 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 的提议;规模在对应模块动手前由负责人逐个确认。
一、切分原则
不按流水线的步骤切,也不按技术层(接口、数据库、服务)切,按「以后可能会改的设计决定」切,一个模块藏住一个决定。换掉某个决定时,只改这一个模块。例如「用哪个分割模型」是一个可能会改的决定:把它藏进 segment,CEUS 的其余算法只接收「每帧一张病灶掩码」,不知道背后是 MedSAM2 还是别的模型;以后换模型只改 segment。这个原则来自 Parnas 1972 年的信息隐藏论文,ClinDB-ReviewBench 用的也是这个分法。
旧项目按技术层切(app/api、app/persistence、app/workers、app/providers、app/domain)。按层切的问题是同一个决定散在几层里:
- 「专家能看见什么」散在四处: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 个。
- 对外露出的函数和类型,是否比内部实现简单得多?
- 依赖有没有绕成圈?下层能不能离开上层单独测试?
- 纯计算的模块里,有没有读写文件、网络或模型?
- 每样外部的东西(交付包的 Excel、MedSAM2、模型服务、账本的 SQLite),是不是只有一个模块直接碰它?
- 同一份知识(哪一路能看见哪些列、诊断编码、关键帧的时刻定义)是不是只在一处定义?
二、项目的完整范围
项目做胰腺囊性肿瘤的鉴别诊断:三个 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/(不在包内) |
人怎么操作:每段一个脚本(ceus_quant、build、consult、evaluate),另有一个按顺序调用四段的总入口;读配置文件,把模块装配起来 |
debugs/t1_local_debug.py、debugs/tic_lognormal_replay.py |
| 组织 | build | 两个库怎么装配:每一路放哪些内容(可见性的唯一决定处)、化名怎么取、病例顺序怎么打乱、清单记什么。全项目只有它同时碰得到原始资料和答案 | app/domain/input_readers.py、ingestion*.py 的装配部分 |
| 组织 | consult | 会诊怎么进行:几个专家、谁先说、后发言的看得到前面的什么、什么时候结束;「调一次模型」的统一入口(先存请求、记开始、调用、存回复、记结束)也在这里 | 没有 |
| 组织 | evaluate | 评价口径:指标、统计方法、字段级对照怎么计分 | 没有(core/eval 已删) |
| 格式 | library | 给专家的资料库的目录布局、文件格式和读取;按路分别读出,一路一个对象 | app/domain/input_packages.py 的数据形状 |
| 格式 | answers | 标准答案库的目录布局、文件格式和读取 | 旧库里的金标准表 |
| 格式 | verdicts | 会诊产物(各路意见、会诊结论)的文件格式和读取;consult 写、evaluate 读 | 没有 |
| 外部 | delivery | 交付包长什么样:目录和文件名、工作表与列名、病例编号的几种写法和规范化、诊断编码、报告「印象 / 结论」段怎么认、跨表一致性怎么核对。只读,不决定谁能看见什么 | app/providers/clinical.py、modalities.py、workbook.py、delivery.py |
| 外部 | 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 |
暂不设共用词汇模块。一样东西要被两个互相不能引用的模块用到,才另立一处放它;目前跨模块的类型各有归属(资料库格式归 library、答案格式归 answers、会诊产物格式归 verdicts)。配置的字段定义和校验归使用它的模块:CEUS 参数归 ceus(旧 research_config.py),会诊的实验配置归 consult;每个字段必填,不补默认值(AGENTS.md「不设默认值」)。
依赖只能往下,同一行的模块之间不互相依赖:
入口 scripts/
组织 build consult evaluate
格式 library answers verdicts
外部 delivery segment llm ledger runs
纯计算 ceus
谁具体能依赖谁:
| 模块 | 可以依赖 |
|---|---|
| ceus、delivery、segment、llm、ledger、runs | 不依赖其他 cadx 模块 |
| library、answers、verdicts | 不依赖其他 cadx 模块 |
| build | delivery、ceus(读 CEUS 结果的类型)、library、answers、runs |
| consult | library、verdicts、llm、ledger、runs |
| evaluate | answers、verdicts、runs |
| scripts/ | 任何模块(不在导入检查范围内,见第五部分) |
几条边界
- 专家能看见什么,只在 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/,改指标或统计方法不需要重跑会诊。
四、两个库与 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 |
|---|---|---|
src/cadx/ |
一个 Python 包,每个模块一个子包;别的模块只能经子包的 __init__.py 引用它,内部文件加 _ 前缀 |
是 |
scripts/ |
入口脚本 | 是 |
tests/ |
不花钱、不要显卡的测试;需要显卡的标 gpu,花钱的标 live,两者不进 make check |
是 |
config/ |
CEUS 参数、会诊的实验配置、模型用途清单(密钥只写环境变量名) | 是 |
prompts/ |
版本化的提示词文件;每次运行记下所用提示词的哈希 | 是 |
data/ |
医院交付的原始资料 | 否 |
library/、answers/ |
两个库,每次构建一个目录 | 否 |
work/ |
CEUS 中间文件 | 否 |
runs/ |
会诊与评价的运行,只追加不删 | 否 |
models/ |
模型权重 | 否 |
reference/、reference-pdf/ |
旧项目副本、外部代码、文献原文 | 否 |
不进 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 |
七、推进顺序
- 骨架(已完成):协作规则、文档体系、工具链。
- 装配两个库(先不含 CEUS 结果):delivery → library、answers → runs 的快照部分 → build。 先做它,是因为它最轻,做完就能用文字资料试会诊;而且隔离(专家看不到答案)是整个研究成立的前提,越早有代码和测试守住越好。顺带产出跨表一致性报告,给
待定问题/04提供证据。 - CEUS 定量:ceus 按旧模块逐个迁移(带上旧的算法单测)→ segment →
scripts/ceus_quant.py→ build 加入 CEUS 结果。 旧算法单测约 274 个,不依赖数据库,随模块带过来,作为迁移正确性的主要依据(design 0002 决定 4)。已知问题(映射锚点写死、两条旧路径的行为差别、造影画面在右半边)在任务书里逐条定。 - 会诊:ledger → llm → consult 的「调一次模型」→ verdicts → consult 的会诊流程。 动手前先专门讨论定下会诊口径:后发言的看得到什么、怎样算两路一致、超声一路给哪些指标和图、模型与重复次数等(旧项目留下的未裁决问题)。
- 评价:evaluate。 动手前先定指标、统计方法,以及
待定问题/02(参数何时冻结、开发集与测试集怎么分)、待定问题/03(病例排除标准)。
第 2 步和第 3 步互不依赖,可以换顺序;第 3 步完成后,build 要重新构建一次才把 CEUS 结果装进资料库。
八、各模块对外的函数与类型
写到哪个模块时,再把它对外的函数和类型补在这里。