Metadata-Version: 2.5
Name: stcc-mcp
Version: 0.1.1
Summary: STCC 电话分诊分级：确定性规则引擎 + MCP，配 0.6B 判据核对器
Project-URL: Homepage, https://github.com/devhc123/stcc-mcp
Project-URL: Model, https://huggingface.co/chenhaodev/stcc-checker-0.6b-GGUF
Author: chenhaodev
License: Apache-2.0
License-File: LICENSE
Keywords: mcp,medical,ollama,rule-engine,triage
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Healthcare Industry
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Description-Content-Type: text/markdown

# 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` 只造成过分诊，是成本旋钮。
默认值在 dev 上按 `false_no ≤ 0.0036` 选出（该 dev 仅 495 条正例、分辨率 0.0020，
系统性偏保守）。**所以没有硬钉"最优值"，曲线一起给出**，按自己的代价矩阵挑点。
复现：`python -m scripts.threshold_sweep --model <m> --split <s> --collect --report`。

## 🔴 已知边界（先读这段再决定用不用）

**① 输入必须是「已按协议问过一轮」的记录，不是原始自述。**
短主诉 `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
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.
