# 分配 Agent · 改造规划

> 把现有助手改造到 [assignment-agent-flow.md](./assignment-agent-flow.md) 定义的行为，
> 并按 [agent-doctrine.md](./agent-doctrine.md) 的教条整理代码组织。

| | |
|---|---|
| **状态** | 待开工 |
| **起点** | 现有实现 —— 服务端 5 文件约 1600 行，前端 3 文件约 3100 行 |
| **原则** | 能用的保留；乱的整理；错的重写。⛔ 不为了整齐而重写已经正确的东西 |

---

## 一、不动的部分

这几处已符合教条，**重写等于倒退**：

| 保留 | 依据 |
|---|---|
| `mcp-auth.service.ts` 整条鉴权链路 | D1–D3 全中，无服务账号，身份随每次调用传递 |
| MCP 工具的 scope 下推 + `resolveClinicId` | D3 |
| 侧信道机制本身 | E1 |
| 条件注册 + 能力分桶缓存 | D4 |
| `assertRevokeActuallyHappened` 前端对账 | C2 在线埋点 |
| `streamText` 自主循环（含 8 步上限） | B1 |
| 代码里那些带日期的踩坑注释 | 它们是**证据**，⛔ 不许在重构中丢掉 |

---

## 二、分阶段

### P0 · 独立小改（不依赖架构改动，可立刻做）

| # | 改什么 | 落点 | 收益 |
|---|---|---|---|
| 1 | 工具返回值里的取值码 → 中文 | `mcp-server.factory.ts` + `plan.service` 返回映射 | **直接删掉提示词里两页「不许说出取值码」** |
| 2 | 长描述 → 返回值 `_guide` | `get_assignment_detail`（35 行）、`list_assignment_batches` | F10，工具清单瘦身，常驻成本归零 |
| 3 | `stopWhen(8)` 到顶时 log + 告知前端 | `assistant.service.ts` | 现在是静默截断 |
| 4 | `SYSTEM_PROMPT` 改掉「为牙科诊所的客服人员提供帮助」 | `assistant.service.ts` | 主管也在用，与 `DISPATCHER_EXTRA` 打架 |

### P1 · 引导节点（核心新增）

| # | 改什么 | 落点 |
|---|---|---|
| 5 | `Signal` 类型 + 6 个判定器 + 需处置/提示分层 | **新建** `plan/assignment-signals.ts` |
| 6 | **基数改造**：每人每日 15 × 在岗 × D，⛔ 不再沿用上次 | `assignment-proposal.service.ts` |
| 7 | `AssignmentProposal` 加 `signals[]` | `packages/types` |
| 8 | 画像亮点基线（同治疗项池子的分布） | 复用 `cohort-attributes.service` |
| 9 | **intent 契约**：节点选项 → intent；`edit_assignment_sheet` 拆成意图级工具，与按钮共用同一组 intent | **新建** `plan/assignment-intents.ts` |
| 10 | 确认单卡片渲染节点区 + 选项按钮 | `assignment-confirm-sheet.tsx` |

> ⚠️ 第 10 项有现实障碍：该文件已 **1540 行**，加节点区之前必须先按 §三 拆分。

### P2 · 事实与措辞分离

| # | 改什么 |
|---|---|
| 11 | `selectionNote` / `basisNote` / `pendingNote` 成品句子 → **结构化事实对象**（两段） |
| 12 | 删掉 `propose_assignment` 返回值里那 25 行指令 |
| 13 | 散落各工具描述里的措辞禁令 → **一处写作规范** |
| 14 | 删掉 `###` 骨架规则 —— 结构由 `signals` 决定，不再靠模型自觉 |

**这一步一次性消掉三类问题的根**：背成品句子、复述卡片数字、改写措辞。

### P3 · 上下文与历史

| # | 改什么 |
|---|---|
| 15 | `toApiMessage` **分层回传**工具调用（名字 + 关键参数 + 结果摘要） |
| 16 | 小数据给原文：候选 ≤50 人时明细进上下文，⛔ 不必一律走侧信道 |

> ⚠️ 15 是本轮唯一会显著改变模型行为的改动，建议单独上、单独观察。

### P4 · 草稿状态机（可与 P1 并行）

| # | 改什么 |
|---|---|
| 17 | `active / superseded / confirmed / cancelled`，**新版生成即作废旧版** |
| 18 | 作废卡片显示「已作废」+ 确认按钮禁用（⛔ 不许静默变灰、⛔ 不许消失） |
| 19 | edit 作用于唯一 `active`，删掉「倒着扫消息找最后一张」 |

### P5 · 工具补齐

| # | 改什么 | 依据 |
|---|---|---|
| 20 | `explain(planId)` —— 「为什么这个人给了张悦」 | F5，现在模型只能编 |
| 21 | `validate(draft)` + read-back | F5 |
| 22 | 兜底 `query_cohort(枚举化 filters)` | F8 |
| 23 | 各 list 工具补 count 模式 | F9 |
| 24 | 测 `get_persona`/`get_facts`/`get_recall_plan` 使用率 → 可能折成 `overview(sections)` | 同质化 |

### P6 · 减法与评测（**必须最后**）

| # | 改什么 |
|---|---|
| 25 | 提示词减法 —— `DISPATCHER_EXTRA` 231 行大幅缩减 |
| 26 | golden set：断言工具调用序列 + 节点该命中没命中 |

> ⚠️ 25 放最后的原因：前面每做一步就有一批规则被结构消化掉。
> **先删提示词等于删掉还在起作用的护栏。**

### 依赖关系

```
   P0  ──────────────────────────────▶ 完全独立，随时做
   P4  ──────────────────────────────▶ 独立，可与 P1 并行
   P1 ──▶ P2 ──▶ P6
   P3  ──────────────────────────────▶ 独立，单独观察
   P5  ──────────────────────────────▶ 独立，按需加
```

**建议起步**：P0 + P4（都独立、都不碰模型行为、都能立刻验证），然后 P1 → P2 连着做。
**`render_artifact` 整个后置** —— 实验性质，不阻塞主线。

---

## 三、代码组织

### 现状问题

| | |
|---|---|
| **职责混杂** | `assistant.service.ts` 457 行里混了三件事：MCP 工具适配、本地工具定义、streamText 循环。其中约一半是内联的工具描述字符串 |
| **描述内联拼接** | `tool({ description: '...' + '...' + ... })` 几十行拼接，改一个字要在里面找 |
| **同类内容三处散落** | 成品句子在 proposal service、解读规范在 tool description、措辞禁令在 prompt |
| **前端超大文件** | `assignment-confirm-sheet.tsx` 1540 行、`assistant-chat.tsx` 1007 行 |
| **类型分散** | `Block` 在 hook 里、`AssignmentProposal`/`SheetEditOp` 在 types |

### 目标结构 · 服务端

```
modules/assistant/
  assistant.service.ts        只剩：装配工具 + 跑循环
  assistant.controller.ts     只剩：SSE 转发
  tools/
    index.ts                  工具注册表（唯一清单）
    propose-assignment.tool.ts
    draft-intents.tool.ts     P1 拆分后的意图级工具
    render-artifact.tool.ts
  prompts/
    system.ts                 通用人设
    writing-style.ts          ⭐ P2 新增：唯一的写作规范
    dispatcher.ts             角色约束（P6 后大幅缩减）
    staff.ts

modules/mcp/
  model-facing.ts             ✅ P0 已落：取值码 / 桶名 → 中文（只翻译，不判断）
  guides.ts                   ✅ P0 已落：随返回值下发的解读规范（`_guide`）
                              ⚠️ 放这里而不是 assistant/ —— 与发出它们的工具同址

modules/plan/
  assignment-proposal.service.ts   只出提案
  assignment-signals.ts            ⭐ P1：引导节点判定
  assignment-facts.ts              ⭐ P2：两段结构化事实
  assignment-intents.ts            ⭐ P1：intent 契约与执行
```

### 目标结构 · 前端

```
components/assistant/
  assistant-widget.tsx
  assistant-chat.tsx          只剩消息流编排
  chat/
    markdown.tsx              从 assistant-chat 拆出
    tool-step.tsx
    artifact.tsx
  confirm-sheet/
    index.tsx                 从 1540 行拆
    signals.tsx               ⭐ P1：引导节点区
    summary-bar.tsx
    agent-group.tsx
    patient-row.tsx
```

### 规范

1. **一个工具一个文件**，导出 `{ name, description, inputSchema, execute }`；描述用顶部常量，⛔ 不内联拼接。
2. **工具描述里只写「怎么用这个工具」**；「怎么解读结果」走 `_guide`（F10）。
3. **⛔ 工具返回值里不许写给模型的指令。** 已经栽过一次：`get_assignment_detail` 的 `note` 字段混着内部指令，模型照抄贴给了主管。P2 之后返回值只有事实。
4. **跨端共享的形状一律定义在 `packages/types`**；前端只保留纯 UI 概念（如 `Block`）。
5. **文件行数软上限**：服务端 300 行 / 前端组件 250 行。超了先问「是不是混了两件事」，再拆。
6. **踩坑注释保留并统一格式**：
   ```ts
   // 🔴 2026-08-06 实测：<现象> → <结论>
   ```
   带日期、现象、结论三要素。**这些是证据不是啰嗦**，重构时随代码迁移，⛔ 不许顺手删。
7. **每条护栏标注归属**（C3）：`// [弥补模型]` 或 `// [业务要求]`。前者随模型变强要复查，后者永久保留。

### 术语表（代码与文档统一）

| 中文 | 代码标识 | 说明 |
|---|---|---|
| 引导节点 | `Signal` | ⛔ 不叫「灯」 |
| 确认单 | `sheet` | UI 侧的叫法 |
| 草稿 | `draft` + `state` | 数据侧的叫法，含 `active/superseded/confirmed/cancelled` |
| 动作契约 | `AssignmentIntent` | ⭐ 按钮与模型工具**共用同一个** |
| 事实两段 | `facts.selection` / `facts.dispatch` | 怎么选的 / 怎么派的 |
| 待分配 | `pending` | |
| 解读规范 | `_guide` | 随工具返回值下发 |

---

## 四、验收

每个阶段完成时至少要能回答：

| | |
|---|---|
| **P0** | 提示词里「不许说出取值码」那两页删干净了吗？ |
| **P1** | 每个引导节点都能构造出触发用例吗？默认全部是 no-op 吗？ |
| **P2** | `propose_assignment` 的返回值里还有指令性文字吗？（应为零） |
| **P3** | 模型能在第 2 轮引用第 1 轮的工具结果吗？上下文有没有变胖？ |
| **P4** | 出第二版后，第一版卡片显示「已作废」且按钮禁用了吗？ |
| **P5** | 每个写工具都能回答「模型怎么知道它做对了」吗？ |
| **P6** | golden set 跑得起来吗？删提示词前后它的通过率变了吗？ |
