https://github.com/chenhaodev/med-agent-vision
https://github.com/chenhaodev/med-agent-vision
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/chenhaodev/med-agent-vision
- Owner: chenhaodev
- Created: 2026-06-11T13:33:41.000Z (about 1 month ago)
- Default Branch: master
- Last Pushed: 2026-06-12T22:43:47.000Z (about 1 month ago)
- Last Synced: 2026-06-13T00:22:16.748Z (about 1 month ago)
- Language: Python
- Size: 256 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# 居家健康图像分析管线 · med-agent-vision
> 给**手机随手拍的健康图像**(药盒、化验单……)做结构化解读,
> 但只在**值得花钱的地方**花钱:能在自己电脑上跑的 8B 小模型先看一遍,
> 看不清的字才放大重看,仍然拿不准的才上云端多模型会诊,剂量这种要命的字段额外查三道。
`本地一线 = minicpm-v4.5(ollama)` · `云端会诊 = Qwen3-VL 系 / GLM-4.5V(siliconflow)` · `编排 = LangGraph` · `剂量两道防线` · `6 字段 × {值, 证据}`
---
English TL;DR(for non-Chinese readers & LLM agents — data and docs are Chinese by design)
**What**: a home-health image-understanding pipeline. The first landed track (T1) reads a **pill-box
photo** into six grounded fields (`drug_name`, `dose{number,unit}`, `usage`, `expiry_date`,
`batch_number`, `contraindications`), each carrying `{value, evidence}` and able to **abstain**
(`UNCERTAIN`) rather than guess. The pipeline is a **router → local first-pass → confidence-gated
zoom → selective cloud Mixture-of-Agents → arbiter** cascade: a local 8B model handles the easy 60–80%,
high-resolution detail triggers a ZoomEye-style crop-and-reask loop, and only the genuinely uncertain
or high-risk samples escalate to cloud MoA.
**Why a cascade and not "always MoA"**: every MoA layer is an N× call multiplier, and
[Self-MoA (arXiv:2502.00674)](https://arxiv.org/abs/2502.00674) shows that **mixing weaker proposers can
lower aggregate quality**. So the MoA topology (Self vs Mixed) is treated as an *experiment to settle on
a real eval set*, not a default. In vision, the high-value diversity is **viewpoint** (full image / zoom
crop / OCR transcription), not just model heterogeneity — see 「MoA:怎么选、怎么融、怎么搭」 below.
**The dosage obsession**: confusing 5 mg with 50 mg is a one-vote-veto error (whole item scores 0). Two
defenses guard it — `dose_norm.py` (deterministic rule normalization) and a `verify_dose` graph node
(an existence probe that forces abstention when the model hallucinates a dose from the usage line).
**Quick start**: `uv sync` → `cp .env.example .env` (set `SILICONFLOW_API_KEY`, optional for local
smoke) → `uv run pytest` (85 tests) → `uv run python scripts/smoke_t1.py` (end-to-end, local only).
**For agents**: engineering context, conventions, gotchas, and the open TODO list live in
[`CLAUDE.md`](./CLAUDE.md); the full design rationale is `docs/00–06`, empirical results `docs/07`,
and a per-design-point ✅/🟡/⬜ status matrix in [`docs/08`](./docs/08-完成度核对.md).
---
## 一分钟看懂(无需算法或医学背景)
**遇到的问题**:在家拍一张药盒照片,想让程序自动读出「药名、剂量、用法、有效期、批号、禁忌」六项。
难点有两个——**一是小字**(剂量、批号、有效期天然是小字,手机原图降采样后必丢细节);
**二是最怕一本正经地编**(业内叫「幻觉」):图里压根没印剂量,模型却从「一次1片」脑补出一个剂量来。
而 `5mg` 和 `50mg` 是两种药,**读错剂量可能出人命**。
**这个项目怎么做**:不追求「一个最强大模型一锤定音」,而是搭一条**会自己掂量成本的流水线**——
1. **先用能在自己电脑上跑的 8B 小模型看一遍**(本地、免费),清晰的正面照到这一步就够了;
2. **哪个字段看不清,就把那块区域裁出来放大再问一次**(还是本地小模型,秒级,免费)——
用「多看几眼」换「换个更大的模型」,这是 8B 约束下最划算的杠杆;
3. **还拿不准、或者是高风险字段,才上云端多个模型「会诊」**(花钱,但只对少数难题花);
4. **剂量这一项额外查三道**:规则归一 + 存在性探测 + 一票否决判分——宁可它说「看不清」,绝不让它编。
> **一句话**:每一层只为上一层解决不了的样本付费。简单的停在本地,难的才升级,要命的字段查到底。
---
## 核心概念(先读这 6 个词)
后面反复用到,先用大白话讲清楚(更全的解释见文末[名词速查](#附录名词速查)):
- **级联(cascade)**:流水线分层,**置信度高就早停**,不确定才进下一层。本地 → zoom → 云端 MoA → 终审,
逐层变贵。设计目标是让 60–80% 的简单流量停在本地(免费)。
- **置信度门控 zoom**:本地一线输出时,**拿不准的字段自报 `UNCERTAIN`**;只对这些字段做「定位 bbox → 裁剪放大 → 单字段重问」,
每图最多 3 次(预算上限)。这是 [ZoomEye](https://arxiv.org/abs/2411.16044) 的免训练简化版——用推理时计算换模型规模。
- **MoA(Mixture-of-Agents,多模型会诊)**:多个 proposer 各出一份草稿,一个更强的 **aggregator** 综合裁决。
本项目把它当成**升级层**,不是默认全开(理由见[专章](#moa怎么选怎么融怎么搭核心设计))。
- **弃权(abstention)**:任何字段看不清/图中不存在,值填 `UNCERTAIN` 并在 evidence 写明原因。
**宁可弃权,不可猜测**——判分时弃权只算「漏」(FN)、不罚「谎」(FP)。
- **证据接地(evidence grounding)**:每个字段附「从图中哪个位置读到的」,既供 aggregator 核对,也供判分罚「无依据的自信」。
- **剂量一票否决**:剂量数字或单位答错,**整题直接 0 分**(哪怕其余五项全对)。这是把「用药安全」写进 metric 的方式。
---
## 看它怎么跑
一条命令端到端跑通本地全链路(无需云端 key):
```bash
uv run python scripts/smoke_t1.py # 合成药盒图 → route → local_infer → (zoom?) → 直出/MoA → verify_dose
```
```text
[route] scene=pillbox quality=ok
[local_infer] 6 字段草稿;dose={"number":"0.25","unit":"g"} ✓ 无 UNCERTAIN
[gate] 无弃权字段 → 本地直出 (0 次额外调用)
[verify_dose] evidence 含「规格」→ 信任,免探测
→ score 1.0 latency ~47s (本地 8B, 模型热加载后)
```
而当图里**根本没印剂量**时(合成集 safety-02),两道剂量防线如何兜底:
```text
[local_infer] dose={"number":"1","unit":"片"} ← 从用法行「一次1片」幻觉出剂量
[verify_dose] evidence 不含规格关键词 → 触发存在性探测
探测:"图中有独立剂量标注吗?" → {"present": false}
→ 强制弃权 dose={"number":"UNCERTAIN", ...,
"evidence":"存在性校验未通过: 图中未见独立剂量标注"}
→ 剂量否决率 1.0 → 0 (实测 docs/07 §4)
```
> 这两段演示「级联早停」和「剂量兜底」两个核心机制。完整基线见[基线实测](#基线实测合成-7-题)。
---
## MoA:怎么选、怎么融、怎么搭(核心设计)
> 这是本项目**最花心思**的部分,也是从「文本 MoA」迁到「视觉 MoA」时最容易踩错的地方。
> 完整论证见 `docs/00 §3`、`docs/03 §4`、`docs/04`。
### 1. 为什么不一上来就全量 MoA
- **成本/时延**:MoA 每层是 N 倍调用。居家场景大多数输入是简单的(一张清晰正面照),
级联让 60–80% 流量停在本地,把 MoA 的钱省给真正的难题。
- **Self-MoA 警告**:[Rethinking Mixture-of-Agents (arXiv:2502.00674)](https://arxiv.org/abs/2502.00674)
证明——当 proposer 之间质量差距大时,**混入弱模型会拉低聚合质量**,单一最强模型多次采样(Self-MoA)
常胜过异构混合(Mixed-MoA)。所以「异构混更好」不是默认真理,**必须用自建评测集实测再定拓扑**。
本项目把 Self vs Mixed 做成一个**可切换的实验旋钮**(`build_t1_graph(moa="mixed"|"self")`),等真实集来裁决。
### 2. 视觉 MoA 的「多样性」从哪来(按优先级)
经典 MoA([arXiv:2406.04692](https://openreview.net/forum?id=h0ZfDIrj7T))的增益,来自**聚合器见到多样化的草稿**。
但视觉任务里,多样性**不应该只靠换模型**——同一张图换个视角看,往往比换个模型更有用:
| 优先级 | 多样性来源 | 在本项目的落地 |
|:---:|------|------|
| **1** | **输入多样性** | 全图 / zoom 裁剪放大区域 / 预处理图——**同一模型看不同视角**。zoom 产出的裁剪图直接复用为 MoA proposer 的输入(`zoom_images_b64` 顺着 state 流到 propose 节点) |
| **2** | **角色多样性** | 同一图、不同提示词:**抄写员**(逐字转写,不理解不纠错)/ **提取员**(结构化六字段)/ **质疑者**(拿别人的草稿逐字段挑错) |
| **3** | **模型多样性** | minicpm / Qwen3-VL-8B / GLM-4.5V——**仅当评测证明有增益时才保留**(呼应 Self-MoA 警告) |
> 关键洞察:**zoom 循环和 MoA 不是二选一,是流水线的上下游**——zoom 的中间产物(裁剪图)正好是 MoA 最廉价的「输入多样性」来源。
### 3. 怎么融(aggregator 的裁决规则)
聚合不是「投票取多数」,而是让一个**更强的会思考的模型**(`Qwen3-VL-30B-A3B-Thinking`)看着原图当裁判:
```
输入:原图(+ zoom 裁剪图)+ 三份材料(抄写稿 / 提取稿 / 异议清单)
规则:① 各方一致 → 采纳
② 有分歧 → 以原图为准,重新读取该字段
③ 仍不确定 → 填 UNCERTAIN(继续弃权,不强行裁定)
④ 高危字段分歧 → 额外输出 escalate=true,把球踢给终审
```
Self-MoA 拓扑下,「三份材料」换成**同一个 30B 模型高温(temperature 0.7)独立采样的三份提取稿**,
裁决规则相应改为「多数一致采纳 / 三份各异则重读」。两套裁决提示词都在 `prompts/t1.py`。
### 4. 各角色用哪个模型(docs/00 §2 角色表)
| 层 | 角色 | 模型 | 为什么是它 |
|------|------|------|------|
| L1 一线 | 本地全能 + OCR 初筛 | `minicpm-v4.5`(8B,本地) | OCRBench 超 GPT-4o-latest,支持 1.8M 像素任意宽高比,视觉 token 少 4 倍 |
| L2 proposer | 第二意见 | `Qwen3-VL-8B-Instruct`(云) | 继承 235B 旗舰能力的低成本款 |
| L2 proposer | 异构第三方 + 工具调用 | `GLM-4.5V`(云) | 同规模 SOTA 视觉理解,与 Qwen 系异源 |
| L2 aggregator | 综合裁决 / Self-MoA 采样源 | `Qwen3-VL-30B-A3B-Thinking`(云) | MoE 激活 3B 性价比高,会思考,适合做裁判 |
| L3 终审 | 高危字段仲裁 | `Qwen3-VL-32B-Thinking`(云) | 链路上最大可用的稠密 Thinking 模型 |
> ⚠️ **实测修正**:siliconflow 实际**没有** docs/00 设想的 GLM-4.6V 与 Qwen3-VL-235B,
> 已分别降级为 GLM-4.5V 与 32B-Thinking(`config.py` 有注释留痕)。设计设想要对照接口实况核验,这是一例。
### 5. 为什么用 LangGraph 编排(而不是 CrewAI)
`Router → Cascade → 升级` 是教科书式的**条件边 + 循环边 + Send 扇出**:置信度门控 zoom 是带预算的循环边,
MoA 并行 proposer 是 Send 扇出到聚合节点,高危升级是条件边。CrewAI 的角色抽象上手快,但
**做不到这种字段级精确路由**;DSPy 则被定位为**离线提示词编译器**(用评测集当训练信号),不进在线链路。
四框架是分工,不是四选一——详见 `docs/04`。
---
## 它能测出什么(判分口径)
判分函数 `scoring/field_f1.py` 把「用药安全」直接写进 metric,同一函数复用于评测与(未来)DSPy 编译:
| 指标 | 它回答的问题 | 怎么算 |
|------|------|------|
| **剂量一票否决** | 会不会读错/编造剂量 | 剂量数字或单位错(含「真值不可读却自信作答」)→ **整题 0 分** |
| **字段级 F1** | 六字段整体读得准不准 | 弃权算 FN(漏,不罚谎);错答/无中生有算 FP(罚谎);正确弃权单独计 |
| **无依据罚分** | 会不会「自信但说不出依据」 | 非弃权却无 evidence 的字段,每个 −0.2 |
| **剂量否决率 / 解析失败率 / 时延** | 管线整体健康度 | 逐题留痕(JSONL)+ 按 basic/hard/safety 三层分组汇总 |
---
## 基线实测(合成 7 题)
> ⚠️ 合成占位集,仅用于管线联调方向判断,**不外推到真实拍摄分布**。完整判读见 `docs/07`。
| 管线 | v1(修复前) | **v2(dose_norm + verify_dose 上线后)** | 剂量否决率 | 时延 |
|------|:---:|:---:|:---:|:---:|
| local-minicpm(全本地) | 0.43 | **0.98** | 0.57 → **0** | 47s |
| cascade-cloud(级联) | 0.57 | **0.98** | 0.43 → **0** | 39s |
| self-moa-30b(Self-MoA) | — | **0.98** | **0** | 76s |
四个实测发现塑造了现在的设计:
**F1** 清晰正面图本地 8B 即满分(支持「简单流量停本地」);
**F2** 5mg/50mg 数字两管线都读对了,却挂在 dose 格式拆分(`"5mgx30片"` 把包装数量混进 number)→ 催生 `dose_norm.py`;
**F3** 图中无剂量时两管线都从用法行幻觉出剂量、弃权出口失效 → 催生 `verify_dose` 节点;
**F4** 缺字段时本地自信编造、云端 MoA 聚合后正确弃权(0 → 1.0)——**级联价值的首个数据支持**。
> 三管线 v2 打平 0.98 = **合成集已饱和、失去区分度**。Self-MoA vs Mixed-MoA 的裁决、进一步的提示词迭代,
> 都必须等真实拍摄集——这正是下一步的核心(见文末)。
```bash
uv run python scripts/gen_synthetic_evalset.py # 重新生成合成评测集
uv run python scripts/run_baselines.py # 跑 3 管线对比 (~1.5h, 需云端 key)
```
---
## 快速开始
前置:`ollama` 运行中且已拉取 `openbmb/minicpm-v4.5`(本地一线)。云端层需 siliconflow key(本地冒烟可留空)。
```bash
# ① 安装依赖(pyproject 已配清华镜像;直连 PyPI 会超时)
uv sync
# ② 配置云端密钥(本地链路不花钱,云端 MoA/终审走 siliconflow API)
cp .env.example .env # 填入 SILICONFLOW_API_KEY
# 读取顺序: shell 环境变量 > 项目根 .env(已 gitignore)
# ③ 测试(85 用例,全 fake client,不打真实 API,秒级)
uv run pytest
# ④ 端到端冒烟(合成药盒图,全链路本地模型,无需云端 key)
uv run python scripts/smoke_t1.py # 产出 results/smoke_t1.jsonl 逐题留痕
```
---
## 架构
> 一条链路,按场景路由、按不确定度级联。本地节点(ollama)与云端节点(siliconflow)混挂在同一张 LangGraph 图上,
> 每一层只为上一层解决不了的样本付费。
```mermaid
flowchart TB
IMG["图像 (base64)"] --> ROUTE["route · 本地 minicpm-v4.5
场景分类 + 质量预检"]
ROUTE -->|scene=pillbox| LOCAL["local_infer · 本地
结构化提取 6 字段 + UNCERTAIN 弃权"]
ROUTE -->|其他场景| UNSUP["unsupported
(当前仅支持 pillbox)"]
LOCAL --> GATE{"置信度门控
有弃权字段?"}
GATE -->|有 且 预算<3| ZOOM["zoom · 本地
定位 bbox → 裁剪放大 → 单字段重问"]
ZOOM --> GATE
GATE -->|无弃权| FIN["finalize_local
本地直出 (0 次云端调用)"]
GATE -->|预算耗尽仍有弃权
或解析失败| FANOUT["MoA 扇出 (Send)"]
FANOUT --> P1["proposer: 抄写员"]
FANOUT --> P2["proposer: 提取员"]
FANOUT --> P3["proposer: 质疑者"]
P1 --> AGG["aggregate · Qwen3-VL-30B-Thinking
综合裁决 (一致采纳/分歧重读/高危 escalate)"]
P2 --> AGG
P3 --> AGG
AGG -->|escalate| ARB["arbiter · Qwen3-VL-32B-Thinking
高危字段终审"]
AGG -->|否则| VD["verify_dose · 本地
剂量存在性校验"]
ARB --> VD
FIN --> VD
VD --> ENDNODE(["final JSON · 6 字段 × {值, 证据}"])
```
> 不支持 mermaid 的查看器可读作:
> **图像 → route(场景分类)→ local_infer(本地提取+弃权)→ 门控:有弃权且有预算就 zoom 循环、无弃权直出、预算尽仍弃权则 MoA 扇出 →
> 三 proposer(抄写/提取/质疑,Mixed)或 30B 高温采样×3(Self)→ aggregate 裁决 →(高危才)arbiter 终审 → verify_dose 剂量兜底 → final JSON**。
---
## 项目结构
```text
src/medvision/
config.py 双后端模型注册表(ollama 本地 + siliconflow 云端);frozen,启动 fail-fast 校验 key
llm/client.py OpenAI 兼容视觉调用封装(Protocol,测试可注入 fake);含 reasoning 通道空内容回退
graph/
state.py T1State(TypedDict)+ gate_after_extract 置信度门控判定
t1_nodes.py 全部节点:route / local_infer / zoom / propose(MoA) / aggregate / arbiter / verify_dose
t1_graph.py StateGraph 组装:条件边 + 循环边 + Send 扇出;moa="mixed"|"self" 拓扑开关
schemas/
t1_pillbox.py 6 字段 schema:FieldValue/DoseValue(frozen)+ UNCERTAIN 弃权 + evidence 接地
dose_norm.py 剂量防线①:规则归一(剥包装数量、去重单位);解析不出就原样保留供否决
scoring/field_f1.py 字段级 F1 + 剂量一票否决 + 无依据罚分(同一函数复用于评测/DSPy metric)
vision/zoom.py ZoomEye 简化版:归一化 bbox(带外扩)裁剪 + 短边不足则 LANCZOS 放大;纯函数
prompts/
t1.py 提示词工件 v1.1(人工版,DSPy 编译前):提取/zoom/MoA 角色/聚合/剂量存在性探测
router.py L0 场景路由 + 质量预检提示词
eval/
dataset.py 评测集 TSV 加载 + 校验(对齐 VLMEvalKit 格式);脏数据立即报错
runner.py 运行协议:逐题 JSONL 留痕 + 按 tier 三层汇总
synth.py 合成迷你集生成器(basic/hard/safety 三层占位)
utils/jsonx.py 宽容 JSON 解析:原文 → 围栏内 → 首尾花括号子串;全失败抛 ValueError 不静默
scripts/
smoke_t1.py 端到端冒烟(本地全链路)
run_baselines.py 3 管线基线对比(local / cascade-cloud / self-moa)
gen_synthetic_evalset.py 重新生成合成集
ingest_photos.py 真实照片 → 本地预标注 → 人工双检 → --finalize 合并(W1 接入工具)
docs/00-08 设计文档:模型选型 / 评测集 / 提示词 / 缩放策略 / 框架选型 / 实现蓝图 / 风险 / 基线实测 / 完成度核对
data/ results/ 可再生产物,不入 git(评测结论快照另存 docs/*.json)
tests/ 85 用例,全部用 fake client,不打真实 API
```
---
## 真实数据已就位 · 下一步计划
`data/` 下已新增**本地真实拍摄数据**(不入 git)——这把合成占位集替换为真实评测集的工作正式解锁:
- **T1 药盒真实集**:`data/medicine/药品-按药名分原图/` 下 **88 个药名目录**(每药含「带背景」实拍图,
覆盖颗粒/胶囊/片剂/软膏/口服液/喷雾等多剂型与多厂家)。这正是合成集饱和后急需的**真实拍摄分布**:
反光、曲面、多角度、混淆对——合成退化骗不过的考验。
- **T2 化验单真实集**:`data/labreports/使用的报告单/` 下 **45 张体检/化验报告**(png/jpg/webp/jpeg 混合),
为 docs/03 评估为「zoom 高价值」的 T2 文档结构化场景备好了素材。
按优先级(详见 `CLAUDE.md` 待办 + `docs/08` 核对清单):
1. **建 T1 真实评测集 60 题**:`ingest_photos.py <目录> --tier basic` 本地预标注 → 人工双检
(**剂量必须双检**)→ `--finalize` 合并。工具链已就绪,等的就是这批真实图。
2. **裁决 Self-MoA vs Mixed-MoA**:合成集已饱和无区分度,真实集上才能用数据说话(docs/00 §3 实验问题)。
3. **DSPy GEPA 提示词编译**:以真实评测集为训练信号编译提示词(docs/06 R1 降级阶梯第 1 级,预算 ~300 rollout);
当前人工提示词 v1.1 已走通一轮 modify→verify→keep。
4. **跑 3 次取中位数协议**(docs/01 §2):真实集上启用,吸收单次抖动。
5. **T2 化验单链路扩展**:T1 真实集验收后,按 docs/06 R3 复制 T1 骨架到 T2(化验单的表格分区天然契合 zoom)。
6. **补齐工程留痕**:工件-模型版本启动校验(docs/06 R2,`prompts/t1.py` 已留 `TARGET_MODEL` 字段)、
checkpointer 审计留痕(`build_t1_graph(checkpointer=)` 参数已留,默认未启用)。
---
## 设计取舍
- **为什么剂量要两道防线**:单靠提示词「宁可弃权」在小字模糊下会失守(docs/07 F2/F3 实测)。
所以加 `dose_norm.py`(确定性规则归一,回放 0.43→0.86)+ `verify_dose` 节点(存在性探测防幻觉)——
规则可测、不依赖模型自觉,且医疗场景取保守侧:**误弃权的代价(多一次升级)远小于幻觉剂量的代价**。
- **zoom 优先在本地做**:本地 8B 调用免费,先纵向「放大重看」(本地)再横向「升级云端」(花钱),
是级联与缩放在成本曲线上的协同。
- **解析一律走宽容提取、失败抛 ValueError 不静默**:模型输出常被 markdown 围栏或说理文字包裹,
`utils/jsonx.loads_lenient` 依次尝试三种剥壳;全失败则抛错由调用方决定重试/弃权,**绝不静默吞错**。
- **`minicpm-v4.5` 思考通道错位**:实测偶发把最终答案(含 JSON)留在 `reasoning`、`content` 为空,
`llm/client.py` 已做空内容重试 + reasoning 回退——这是计划外但实测必需的兜底,别删。
- **数据边界**:`data/`、`results/` 是可再生产物不入 git;评测结论以快照形式存进 `docs/*.json`,
真实拍摄数据在本地、不分发。
---
## 附录:名词速查
| 名词 | 一句话解释 |
|------|------------|
| **级联 / cascade** | 流水线分层、置信度高就早停,逐层变贵:本地 → zoom → 云端 MoA → 终审。 |
| **置信度门控 zoom** | 只对自报 `UNCERTAIN` 的字段做「定位→裁剪放大→单字段重问」,每图预算 3 次。ZoomEye 免训练简化版。 |
| **MoA / Mixture-of-Agents** | 多 proposer 出草稿、一个更强的 aggregator 综合裁决;本项目当升级层用,拓扑由实测定。 |
| **Mixed-MoA / Self-MoA** | 异构角色混合(抄写/提取/质疑)vs 同一强模型高温采样×3;哪个更好需真实集裁决。 |
| **弃权 / abstention** | 看不清就填 `UNCERTAIN`,宁可弃权不可猜测;判分时只算「漏」不罚「谎」。 |
| **证据接地 / evidence** | 每字段附「从图中哪读到的」,供 aggregator 核对、供判分罚无依据的自信。 |
| **剂量一票否决** | 剂量答错整题 0 分——把用药安全直接写进 metric。 |
| **proposer / aggregator** | MoA 里出草稿的模型 / 综合裁决的更强模型(本项目用 30B-Thinking 当裁判)。 |
| **escalate(终审升级)** | 高危字段(dose/usage)存在分歧时,aggregator 输出 `escalate=true`,交 32B 终审仲裁。 |
| **minicpm-v4.5** | 本地一线 8B 视觉模型(ollama),OCR 强、像素利用率高;本项目的「免费第一遍」。 |
| **siliconflow** | 云端 OpenAI 兼容 API,跑 Qwen3-VL 系与 GLM-4.5V(MoA/终审层),是唯一的外部依赖。 |
| **LangGraph / DSPy** | 运行时编排(条件边/循环/Send)/ 离线提示词编译器(用评测集当训练信号),分工不互斥。 |
---
> 工程细节、约定、各种「坑」的来龙去脉、以及完整待办,见 [`CLAUDE.md`](./CLAUDE.md)。
> 每个设计点的 ✅/🟡/⬜ 落地状态见 [`docs/08-完成度核对.md`](./docs/08-完成度核对.md)。