stcc-mcp
by devhc123
README.md
# stcc-mcp — 电话分诊分级:规则确定性算,0.6B 只核对判据
一段问诊记录进去,出 **L1–L5 档位 + 处置措辞 + 逐条引文 + 还缺哪几条判据**。
分级由**确定性规则引擎**算出(225 份 STCC 协议 / 849 分支 / 4,168 条判据,随包分发),
**Qwen3-0.6B 只回答一件事**:给定记录和**一条**判据,`yes` / `no` / `unknown`。
模型不选分支、不定档位、不生成处置措辞、**不做 tool call**。
> ⚠️ 不是急诊分诊系统,不做诊断,不能替代医生。L1–L5 是**处置阶梯**
> (叫救护车 / 立即急诊 / 今天就医 / 近两日门诊 / 居家观察)。紧急情况直接拨 120。
## 三步跑起来
```bash
pip install stcc-mcp # 或 uvx stcc-mcp …
ollama pull hf.co/chenhaodev/stcc-checker-0.6b-GGUF:Q8_0 # 640MB 核对器
stcc-mcp doctor # 自检:索引 / Ollama / 模型
stcc-mcp triage --protocol Chest_Pain.md "$(cat 记录.txt)"
```
零常驻进程、零 Docker、不联网。引擎是**纯标准库**,无第三方依赖。
### 第一次运行大概率会看到 `tier: L1` + `certain: false` —— 这是对的
拿一段普通问诊记录进去,很可能得到最紧急档加一串 `unresolved`。**不是判错**:
引擎只有在某支**全部条件都为 `no`** 时才敢排除它,而一次真实问诊不会把一支的判据问完。
例如轻症发热的记录里护士问了意识/呼吸/颈硬/皮疹,却**从没问过脱水体征**
(排尿减少、眼窝凹陷、皮肤张力差、口渴),于是该支排除不掉,输出停在它的档位上界。
**`unresolved` 就是"再问这几条就能收紧"的清单。** 把答案补进记录重跑,
`unresolved` 会单调变短;**但档位只有在某支被完全排除时才会降**——
实测同一份发热记录补齐脱水体征后 `unresolved` 8→2,档位仍停在 L1,
因为剩下的主干(`老年人或免疫低下者…脱水表现:` 这种半截话)核对器给的是 `unknown`。
这时候该动的是下面那个阈值旋钮,不是继续问。详见边界 ①②。
接进 agent(Claude Code / 任何 MCP client):
```bash
pip install "stcc-mcp[mcp]" && stcc-mcp serve # stdio MCP
```
> **Ollama 不支持 MCP,而这对本形态不是问题**:小模型从不 tool call,
> 编排层分别去调 Ollama(HTTP)和规则引擎(进程内),两者互不相识。
> 0.6B 自主 tool-calling 是已知重灾区,这个架构从设计上绕开了它。
## 输出
| 字段 | 含义 |
|---|---|
| `tier` | `L1`–`L5`,**安全上界**:永不比真实答案更轻 |
| `disposition` | 该分支的处置措辞(来自规则表,不是模型生成) |
| `certain` | `true` = 证据已足以定档;`false` = 这是 worst_case 上界 |
| `citations` | 判定依据的判据 + 行号,可审计 |
| `unresolved` | **还缺哪几条判据** —— 补问它们就能收紧档位 |
档位到行动的映射由**你的编排层**定;本包只给档位与处置措辞。
## 判 `no` 的门槛是一个旋钮
`--no-threshold`(默认 0.63):`P(no) ≥ τ ⇒ no`,否则在 `yes/unknown` 里取大者。
同一个模型在发布分布(n=3225)上的整条取舍曲线:
| τ | acc | `false_no`🔴 | `no_recall` |
|---|---|---|---|
| 0.63(默认) | 0.9426 | **0.0000** | 0.9027 |
| 0.50 | 0.9457 | 0.0000 | 0.9189 |
| **0.40** | **0.9495** | 0.0024 | **0.9378** |
| 0.30 | 0.9498 | 0.0084 | 0.9432 |
| 0.10 | 0.9516 | 0.0120 | 0.9635 |
同一段发热问诊记录,只改 τ:
```console
$ stcc-mcp triage --protocol Fever_Adult.md --no-threshold 0.63 "$(cat 记录.txt)"
tier L1 · 拨打救护车 · unresolved 2
$ stcc-mcp triage --protocol Fever_Adult.md --no-threshold 0.40 "$(cat 记录.txt)"
tier L3 · 2小时内接受医疗护理 · unresolved 5
```
τ 调低 ⇒ 核对器更敢把"问过且被否认"的判据判成 `no` ⇒ 分支被排除 ⇒ 档位下降。
**这不是让模型更准,是在同一条取舍曲线上换工作点**——换来的是漏诊风险上升。
**`false_no`(该成立却判成 `no`)是硬门**——假 `no` 会把真值分支从安全收敛里排除掉。
少判 `no` 只造成过分诊,是成本旋钮。
⚠️ **默认 τ=0.63 是经验工作点,不是统计上"控制住了"的阈值。**
按 Neyman-Pearson 范式(Tong et al., *Sci Adv* 2018, arXiv:1608.03109),
要以 95% 置信把 `false_no` 控制在 0.0036 以下,dev 需 **≥831 条正例**;我们只有 **495**——
即便最保守的阈值,真实 `false_no` 超标概率仍有 **16.8%**,495 条只能可靠控制到 α≈0.006。
文献还点名了我们用的那种选法(直接限制经验 type I error ≤ α)**不构成控制**。
⇒ **当工作点用,别当保证用**;曲线一起给出,按自己的代价矩阵挑点。
`python -m scripts.threshold_sweep --pick-tau` 会自己报可行性。
复现:`python -m scripts.threshold_sweep --model <m> --split <s> --collect --report`。
## 🔴 已知边界(先读这段再决定用不用)
**① 「该用哪份协议」这一步本包不提供。**
`--protocol` 是必填参数,需要调用方给——完整链路里这一环是空的:
```
一段问诊记录 ──[?? 谁来选协议 ??]──► protocol_id ──► 本包(引擎 + 核对器)──► L1–L5
↑ 缺这一环
```
**推荐形态:编排层用一个大模型看中文标题菜单来选**(`stcc-mcp protocols` 可打印,225 份)。
上一轮(run1,221 份协议的菜单,34 条真实自述)实测:大模型菜单选择
**@1 = 0.85(原文直选)/ 0.94(先抽一句主诉再选)· @2 = 1.00**;
而用判据文本做向量检索定协议只有 **@1 = 0.18** —— 同一句判据(如"恶心或呕吐")
跨几十份协议出现,**判据级索引天然定不了协议**,这条路已证伪,别再走。
选错协议时引擎的 `referral` / `indeterminate` 状态是自纠网,但**那是网,不是替代**。
**② 输入必须是「已按协议问过一轮」的记录,不是原始自述。**
短主诉 `unknown` 率 94%;真实富对话(IMCS-21,748 字 / 40 轮)仍有 88%。
原因是 STCC 前置分支筛的是急症红旗(噎着、发紫、无反应),
而自然产生的语料**按定义不含这些情形、医生也不会去问**——这是**选择效应**,换更富的语料无效。
**③ 一次真实问诊不会把一支的判据全问完**,于是档位停在该支上界。
这是 `worst_case` 在正确工作,但**上界松紧完全由问诊完备性决定**。
**④ `worst_case` 的安全性以「不产生假 `no`」为前提。**
输出是**在现有证据下排除不掉的最紧急一档**(某支只有全部条件为 `no` 才算排除),
因此**欠分诊恒为 0**、证据每多一个 `no` 就单调收紧(两条不变量由 `tests/` 守着)。
信息不足时它会退化成"人人叫救护车"——这是设计取向(欠分诊 10·d² vs 过分诊 d),不是 bug。
**⑤ 分级本身是 silver**:分支级参照由强模型定稿 + 人工复核,**终局结论仍缺一个真人护士 gold**。
## `unknown` 为什么是第一等状态
STCC 分支语义是「本支任一条件为**是** ⇒ 命中;全部为**否** ⇒ 转下一支」。
**"没提到" ≠ "说了没有"**:前者必须触发追问,后者才能转分支。
把 `unknown` 压成 `no` 等于**凭空伪造阴性**,会让引擎走到错误的分支。
## 判据文本
索引里的判据是**逐条独立改写版**(5,214/5,214)。纯阈值与单个医学术语
(「咳嗽」「体温>100.4°F」)按事实保留。改写过三道闸:
数值/否定/长度/雷同的形式校验、**oracle 回放与原版逐位一致**、下游 checker 指标不掉。
## 冒烟:用**发布物**验,不是用仓库源码验
```bash
pip install --no-cache-dir stcc-mcp
cd /tmp && python <本仓>/scripts/smoke_published.py # 命令行契约
python <本仓>/scripts/smoke_published.py --skip-cli --links README.md # 文档里每个 URL
```
断言的是**文档承诺过的东西**:协议菜单 225 条、协议名写错→退出码 2 + 可执行提示、
输入过短→`rejected` 且不给档位、依赖不可达→退出码 3 + 不吐 traceback、README 里每个链接可达。
CI 见 `.github/workflows/smoke.yml`(`repo` 测源码、`published` **从 PyPI 装**再测、
`e2e` 装 Ollama 拉发布模型跑三分类与端到端)。
> 为什么单独搞这一层:本项目发布后靠人肉抓到 **7 个**"传上去了但用不了"的东西
>(死链、没发布的命令、返回空串、废弃的 CLI、账号名写错、默认模型名只在开发机成立、
> 依赖没起时抛裸 traceback)。它们**都不会让上传失败,也都不会被单测抓到**——
> 单测跑仓库源码,用户跑发布物。
## 开发
```bash
uv sync
uv run pytest -q # 27 条回归
uv run python -m scripts.mcp_selfcheck # oracle 回放 4,168 条判据,<1 秒
```
`scripts/mcp_selfcheck.py` 是引擎的**忠实度自检**:逐条判据置 yes、其余置 no,
核对引擎走到的分支与处置。这不是模型评测——对不上就是编译或求值有洞。
## 模型
[`chenhaodev/stcc-checker-0.6b-GGUF`](https://huggingface.co/chenhaodev/stcc-checker-0.6b-GGUF)
(Q8_0 / Q4_K_M)。判据级 `false_no` 0.0024、延迟中位 ~250ms、端到端 1.25s/条。
Apache-2.0.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues