# AI 审批辅助环节 — 接口约定 v1

> 适用：DTMP 流程引擎 · 表单审批节点 · AI 校验辅助
> 目标：把「模型输出一段中文」改为「规则引擎 + 模型语义判断，输出结构化结果」
> 前提：AI 仅给建议，人工拍板（不改变这一点，但要让人工拍板真正可执行）

---

## 一、当前实现的三个问题

| # | 现象 | 根因 | 后果 |
|---|---|---|---|
| 1 | 页面可读性差、无法按问题定位 | 模型输出 markdown 文本，无字段标识 | 前端无可渲染的结构，改样式无法解决 |
| 2 | 结论不可复现 | 20+ 条一次性进单次调用，长上下文注意力衰减 | 同一份数据两次跑可能不同；审批需留痕，不可接受 |
| 3 | 确定性校验被误判 | 非空/格式/阈值/枚举/跨字段比较交给了模型 | 实测「学历=大专」在「须本科及以上」下被标为通过 |

**结论：至少 17/22 项不属于模型能力范围，应由规则引擎判定。**

---

## 二、职责边界

### 2.1 走规则引擎（确定性，不调模型）

| 类型 | 校验内容 | 本表单中的例子 |
|---|---|---|
| 非空 | 必填、空值占位词库（无/暂无/—/N/A/一） | 工作单位及职务、家庭情况、评议说明 |
| 格式 | 正则、文件扩展名、枚举值 | 联系电话 `^1[3-9]\d{9}$`、附件不含 .xlsx |
| 阈值 | 数值/日期上下限、数量下限 | 出生日期 ≤ 1999-12-31、附件 ≥ 3 份 |
| 枚举 | 字典值、字典序比较 | 学历 ≥ 本科（字典序）、评议等级 ≠ 不合格 |
| 跨字段 | 字段间比较、与流程上下文比较 | 转正时间 > 入党时间；姓名 = 流程发起人 |
| 子表行级 | 对明细表逐行校验并回传行号 | 民主评议 3 行，第 2 行等级、第 3 行说明 |

**特征：** 同一输入永远同一输出、可追溯规则编号、直接决定 `blocking`。

### 2.2 走模型（语义判断，单项独立调用）

| 校验内容 | 为什么模型才能做 |
|---|---|
| 所属党支部是否隶属指定单位 | 组织机构库未收录时需名称语义推断 |
| 主要社会关系是否逐项列明 | 需判断内容完整性，非格式问题 |
| 奖惩情况「额」是否有效内容 | 需判断是否误触输入 |

**硬约束：**
1. **单项一次调用**，禁止把 N 项拼进一个 prompt。
2. 输出 `confidence`，一律落在 `severity=doubt`，**不得产生 blocking**。
3. `temperature=0`，固定 prompt 版本号，便于回归。

### 2.3 既不判也不猜：规则本身有歧义

规则文本无法解析为确定边界时（例：入党时间「须在 17 年之前」，可解释为 ≤2016-12-31 或 ≤2017-12-31），
返回 `severity=rule_ambiguous`，不给结论，前端提供「修订规则」入口回指规则中心。

---

## 三、输出数据结构

```json
{
  "instanceId": "2081996088068726786",
  "formCode": "party_member_register",
  "engineVersion": "rule-1.4.0",
  "aiPromptVersion": "semantic-v3",
  "checkedAt": "2026-07-28T15:24:00+08:00",
  "summary": {
    "total": 22,
    "blocking": 12,
    "doubt": 5,
    "passed": 5,
    "suggestion": "reject",
    "fixableBySubmitter": 9
  },
  "items": [
    {
      "seq": 1,
      "fieldId": "f_transfer_date",
      "fieldPath": "party_info.transfer_date",
      "fieldLabel": "转正时间",
      "groupLabel": "党籍信息",
      "rowIndex": null,
      "severity": "blocking",
      "source": "rule",
      "ruleId": "R-07",
      "ruleText": "转正时间须晚于入党时间",
      "actual": "2016-06-07",
      "expected": "> 2017-03-01",
      "deviation": "早 268 天",
      "relatedFields": ["f_join_date"],
      "reason": "转正时间早于入党时间",
      "confidence": null,
      "fixableBySubmitter": true
    },
    {
      "seq": 11,
      "fieldId": "f_branch",
      "fieldPath": "party_info.branch",
      "fieldLabel": "所属党支部",
      "groupLabel": "党籍信息",
      "rowIndex": null,
      "severity": "doubt",
      "source": "ai",
      "ruleId": "R-11",
      "ruleText": "所属党支部须隶属厦门大学",
      "actual": "国际合作与交流处党支部",
      "expected": "名称含「厦门大学」或在机构库中可溯源",
      "deviation": null,
      "relatedFields": [],
      "reason": "名称中不含「厦门大学」，疑为其下属处室建制；组织机构库未收录该支部",
      "confidence": 0.82,
      "fixableBySubmitter": false
    },
    {
      "seq": 6,
      "fieldId": "f_join_date",
      "fieldLabel": "入党时间",
      "severity": "rule_ambiguous",
      "source": "rule",
      "ruleId": "R-06",
      "ruleText": "入党时间须在 17 年之前",
      "actual": "2017-03-01",
      "expected": null,
      "reason": "规则边界表述有歧义：可解释为 ≤2016-12-31 或 ≤2017-12-31，两种口径结论相反",
      "confidence": null,
      "action": "revise_rule"
    }
  ]
}
```

### 字段说明

| 字段 | 必填 | 说明 |
|---|---|---|
| `fieldId` | 是 | 表单引擎中的字段唯一标识，**前端据此定位/高亮控件** |
| `fieldPath` | 是 | 含子表路径，便于嵌套定位 |
| `rowIndex` | 子表必填 | 明细表行号，从 1 开始；主表字段为 `null` |
| `severity` | 是 | `blocking` / `doubt` / `rule_ambiguous` / `passed` |
| `source` | 是 | `rule` / `ai` — 前端据此区分是否可信为确定结论 |
| `ruleId` | 是 | 规则编号，可跳转规则中心查看原文与变更历史 |
| `actual` / `expected` | 是 | 原样回传，前端不再从中文里抽取 |
| `deviation` | 否 | 偏差量（早 268 天、缺 1 份），减少审批人心算 |
| `confidence` | AI 项必填 | 0–1；`source=rule` 时为 `null` |
| `fixableBySubmitter` | 是 | 决定退回清单如何分发 |

> `seq` 仅用于展示，**不承担定位职责**。定位一律用 `fieldId` + `rowIndex`。

---

## 四、执行流程

```
提交表单
  │
  ├─ 1. 规则引擎全量执行（同步，无模型调用）
  │      → blocking / passed / rule_ambiguous
  │
  ├─ 2. 仅对「规则未覆盖的语义项」逐项调模型（并发，单项独立）
  │      → doubt + confidence
  │      失败/超时：该项标 ai_unavailable，不阻断整体结果
  │
  ├─ 3. 汇总 summary
  │      blocking > 0 → suggestion = reject
  │      blocking = 0 且 doubt > 0 → suggestion = review
  │      全 passed → suggestion = approve
  │
  └─ 4. 返回前端渲染
```

**要点：**
- 规则引擎不依赖模型，模型不可用时校验仍完整（当前实现是模型挂了整个环节失效）。
- 模型调用并发但单项独立，20 项约 3–5 个并发批次，延迟可控。
- `suggestion` 由规则结果推导，**不由模型生成**。

---

## 五、人工拍板的留痕要求

当前「AI 建议 + 人拍板」的实际风险：审批人只看末尾结论，等于替 AI 的错误签名。

要求：
1. `doubt` 项必须逐条「认可 / 驳回」后方可提交，未处理项阻止提交动作。
2. 人工决策写入审批记录：`itemSeq / fieldId / aiSuggestion / humanDecision / operator / timestamp`。
3. 与 AI 建议相反的决策单独标记，作为规则或 prompt 的改进输入。
4. `blocking` 项不允许人工「认可通过」——若确需放行，走例外审批流并记录理由。

---

## 六、本表单暴露的两个产品侧问题（非 AI 问题）

**1. 表单引擎缺字段类型约束**
「培训经历」字段类型为文本，规则却要求「不少于 3 次」。文本存数字，此项永远只能靠模型猜测。
→ 字段类型改为数字后，该项自动转为确定性校验。
→ 通用问题：规则若含数值比较，应在配置时校验目标字段类型是否兼容，不兼容直接阻止保存规则。

**2. 规则配置界面未对边界做结构化约束**
「须在 17 年之前」这类自然语言能存进系统，歧义是必然结果。
→ 日期/数值类规则应强制结构化录入（运算符 + 具体值），不允许自由文本描述边界。

---

## 七、验收用例（建议纳入固定回归集）

| 用例 | 输入 | 期望 |
|---|---|---|
| 字典序比较 | 学历 = 大专，规则「≥ 本科」 | `blocking`，不得为 passed（当前实现失败） |
| 跨字段日期 | 转正 2016-06-07，入党 2017-03-01 | `blocking`，deviation 含天数 |
| 子表行级 | 民主评议 3 行，第 2 行等级=不合格 | `rowIndex=2`，前端可定位到该行 |
| 空值占位 | 工作单位 = 「无」 | `blocking`，命中占位词库 |
| 结论一致性 | 同一份数据连续跑 3 次 | 3 次 `blocking` 集合完全一致 |
| 模型不可用 | 断开模型服务 | 规则校验结果完整返回，语义项标 `ai_unavailable` |
| 规则歧义 | 规则文本「17 年之前」 | `rule_ambiguous`，不给通过/不通过结论 |

---

_v1 · 2026-07-28 · 供研发对照实现_
