Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.0.archive.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

531 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术方案 - REQ-通用-004 敏感词检测
> **版本**: v1.0
> **日期**: 2026-07-28
> **REQ编号**: REQ-通用-004
> **关联PRD**: `01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.0.md`
> **状态**: v0.7.1 已上线(部分达标)
> **作者**: 宋献
> **关联原型**: `01-产品文档/00-产品规划/PRD-REQ-集成-002-管理后台-v1.0.html`"代答排除规则"页,含敏感词转移人工/财务敏感问题规则,可作为后续词库管理 UI 雏形)
---
## 目录
1. [设计概述](#1-设计概述)
2. [现状分析](#2-现状分析)
3. [架构设计](#3-架构设计)
4. [关键模块设计](#4-关键模块设计)
5. [数据模型](#5-数据模型)
6. [接口设计](#6-接口设计)
7. [部署与灰度](#7-部署与灰度)
8. [测试策略](#8-测试策略)
9. [风险与降级](#9-风险与降级)
10. [关联文档](#10-关联文档)
---
## 1. 设计概述
### 1.1 背景
坐席发送消息的**事前内容审核**机制,包含两个独立检测模块:
| 模块 | 库 | 检测目标 |
|------|-----|----------|
| **敏感词检测** | `wordfilter` 0.2.7(基于 DFA) | 服务用语不当(脏话/推诿/反问) |
| **隐私字段检测** | Python `re` 标准库 | 手机/身份证/银行卡/个人邮箱 |
v0.7.1 已上线运行,但**有 3 项已知不达标**(看板验真测试报告 ⑤节),本文档同步记录现状与改进方向。
### 1.2 设计目标
| 目标 | 描述 |
|------|------|
| **G1 覆盖完整** | 服务用语 + 隐私字段双覆盖 |
| **G2 低侵入** | 不阻塞主消息发送流程(异步/降级) |
| **G3 可扩展** | 词库/正则可运营(v1.1 计划) |
| **G4 可审计** | 关键命中可追溯(v1.1 计划) |
### 1.3 设计原则
| 原则 | 说明 |
|------|------|
| **P1 纵深防御** | 词库(DFA)+ 正则(pattern)双引擎,单一引擎失败不致命 |
| **P2 数字边界** | 所有数字相关正则必须用 `(?<!\d)/(?!\d)`**禁止**用 `\b`Python 3 re 对中文失效) |
| **P3 异步可降级** | 审核失败时降级为 PASS,不阻断发送 |
| **P4 集中配置** | 词库/正则集中在一个 service,修改影响范围可控 |
### 1.4 适用范围
| 端 | 是否审核 | 触发位置 |
|----|----------|----------|
| 坐席端发送消息 | ✅ 是 | 后端 `agent_send_message` 接口前 |
| 员工端(H5)消息 | ❌ 否 | 走企微原生反垃圾 + AI Wingman |
| AI 回复 | ❌ 否 | 由 Dify 自带的内容安全机制处理 |
| 系统消息 | ❌ 否 | 内部通知,无需审核 |
---
## 2. 现状分析
### 2.1 已有实现(v0.7.1 已上线)
```
src/backend/
├── app/services/
│ ├── content_moderation_service.py # 核心:审核 + 隐私检测
│ └── admin/admin_moderation_service.py # 仅有快速回复审核,**未做敏感词管理**
└── tests/
└── test_content_moderation.py # 13 用例,11 通过 2 失败
```
| 模块 | 实现 | 行数 |
|------|------|------|
| `ContentModerationService` | 单例服务,wordfilter + 自定义词库 | 331 行 |
| `ModerationAction` | Enum: pass/warn/block | 5 行 |
| `ModerationCategory` | Enum: profanity/politics/porn/ad/privacy/other | 8 行 |
| `ModerationResult` | dataclass 返回值 | 13 行 |
| `moderate(text)` | 主审核入口 | 50 行 |
| `check_privacy_leak(text)` | 隐私正则检测 | 33 行 |
### 2.2 看板验真测试报告(2026-07-07)— 3 项不达标
| # | 问题 | 根因 | 现状 | 计划 |
|---|------|------|------|------|
| 1 | 隐私正则 `\b1[3-9]\d{9}\b` 中文场景失效 | Python 3 `re` 将中文字符视为 `\w``\b` 边界失效 | ✅ **已修复**(用 `(?<!\d)/(?!\d)` 数字边界) | - |
| 2 | 命中动作固定 WARN,不 BLOCK | 业务决策(2026-07-08 评审) | 🟡 **维持 WARN** | v1.1 可配置 |
| 3 | 自定义词库写死 4 条,未接 system_config | 开发未完成 | 🔴 **未修复** | v1.1 入库 |
### 2.3 测试覆盖(11/13 通过)
| 用例类型 | 数量 | 通过 | 失败 | 说明 |
|----------|------|------|------|------|
| 敏感词命中 WARN | 4 | 4 | 0 | 4 条基础词均命中 |
| 正常文本 PASS | 2 | 2 | 0 | 包括空字符串 |
| 隐私字段检测 | 3 | 3 | 0 | 手机/身份证/正常 |
| 自定义词库断言 | 1 | 1 | 0 | 确认 4 条词存在 |
| **默认值 WARN** | 1 | 0 | 1 | test_default_action_is_warn_not_block2026-07-08 决策后视为通过 |
| 隐私正则边界 | 2 | 1 | 1 | 待补充中文+号码场景 |
---
## 3. 架构设计
### 3.1 总体流程
```
坐席端输入文本
[1] 后端 agent_send_message 接口接收
[2] 调用 ContentModerationService.moderate(text)
├──▶ [2a] wordfilter 检测(DFA 匹配)
│ │
│ ├── 命中 → matched_words 非空
│ └── 未命中 → matched_words 为空
├──▶ [2b] _classify(matched) → 分类(当前仅 profanity
├──▶ [2c] 决策 action = WARNv1.0 固定)
├──▶ [2d] _generate_suggestion(category) → 建议文案
[3] 返回 ModerationResult{action, category, matched_words, suggestion}
[4] 前端根据 action 渲染:
├── PASS → 直接发送
└── WARN → 黄色提示条 + 仍可发送
```
### 3.2 隐私检测独立流程
```
文本输入
check_privacy_leak(text)
├──▶ [A] 手机号正则 (?<!\d)1[3-9]\d{9}(?!\d)
│ └── 命中 → leaked.append("phone")
├──▶ [B] 身份证号正则 (?<!\d)\d{17}[\dXx](?!\d)
│ └── 命中 → leaked.append("id_card")
├──▶ [C] 银行卡号正则 (?<!\d)\d{16,19}(?!\d)
│ └── 命中 → leaked.append("bank_card")
├──▶ [D] 个人邮箱正则(排除公司域名)
│ └── 命中 → leaked.append("personal_email")
返回 List[str](如 ["phone", "id_card"]
```
> **注意**v1.0 `check_privacy_leak` 是**独立方法****未接入** `moderate` 主流程!前端需单独调用。
### 3.3 关键时序
```mermaid
sequenceDiagram
participant 坐席
participant 前端
participant 后端
participant Moderation as ContentModerationService
participant DB
坐席->>前端: 输入消息文本
前端->>后端: POST /api/agent/send {text}
后端->>Moderation: moderate(text)
Moderation->>Moderation: wordfilter.blacklisted(text)
alt 命中
Moderation->>Moderation: _extract_matched → 4 条基础词循环
Moderation->>Moderation: _classify → PROFANITY
Moderation->>Moderation: action = WARN (固定)
Moderation-->>后端: ModerationResult{WARN, profanity, [...]}
else 未命中
Moderation-->>后端: ModerationResult{PASS, None, []}
end
后端-->>前端: {action: WARN, suggestion: "..."}
alt WARN
前端->>坐席: 显示黄色提示条 + 仍可点"发送"
坐席->>前端: 点发送
前端->>后端: 确认发送
else PASS
前端->>后端: 直接发送
end
后端->>DB: 写 messages 表
后端-->>前端: 发送成功
```
---
## 4. 关键模块设计
### 4.1 敏感词检测
#### 4.1.1 核心实现
```python
# content_moderation_service.py
class ContentModerationService:
def __init__(self):
self.wf = Wordfilter() # wordfilter 0.2.7DFA 算法
self.custom_sensitive_words: List[str] = [
"投诉我", # 暗示员工投诉自己
"你爱找谁找谁", # 不当推诿
"自己不会百度吗", # 不当反问
"这点小事", # 轻视员工问题
]
if self.custom_sensitive_words:
self.wf.addWords(self.custom_sensitive_words)
```
#### 4.1.2 关键修复(v0.7.1 部署)
> **BUGFIX**: `\b` 和 `(?<!\w)` 对中文均失效(Python 3 `\w` 含中文),
> 改用 `(?<!\d) / (?!\d)` 检查数字边界——"电话13800138000" 可正确匹配。
```python
# ❌ 错误写法(Python 3 中文失效)
re.search(r"\b1[3-9]\d{9}\b", "我的电话13800138000") # 命中失败
# 因为 Python 3 re 中 \w 包含中文,"电话"和"138"之间没有 \b
# ✅ 正确写法(数字边界)
re.search(r"(?<!\d)1[3-9]\d{9}(?!\d)", "我的电话13800138000") # 命中成功
```
#### 4.1.3 词库加载策略(v1.1 计划)
```
服务启动 (FastAPI lifespan)
[1] 从 system_config 读取 sensitive_words 键值(JSON 数组)
├── 存在 → 解析 + 加载到 Wordfilter
└── 不存在 → 降级为写死 4 条
[2] 同样从 system_config 读取 privacy_patterns
├── 存在 → 加载到正则列表
└── 不存在 → 降级为内置 4 条正则
```
### 4.2 隐私字段检测
| 检测项 | 正则 | 说明 |
|--------|------|------|
| phone | `(?<!\d)1[3-9]\d{9}(?!\d)` | 11 位 1 开头手机号 |
| id_card | `(?<!\d)\d{17}[\dXx](?!\d)` | 18 位身份证(末位 X 兼容) |
| bank_card | `(?<!\d)\d{16,19}(?!\d)` | 16-19 位银行卡 |
| personal_email | `(?<!\w)[a-zA-Z0-9._%+-]+@(?!servyou-it\.com\|servyou\.com\.cn)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?!\w)` | 排除公司域名 |
**设计要点**
- 所有正则必须用 `(?<!\d) / (?!\d)` 数字边界,**禁止**用 `\b`
- 邮箱正则用 `(?<!\w) / (?!\w)`(邮箱前缀是 ASCII,前面不会是中文)
### 4.3 命中动作决策
```python
# v1.0 固定 WARN
action = ModerationAction.WARN
```
**未来扩展(v1.1**
```python
# 根据 category + severity 决策
SEVERITY_MAP = {
ModerationCategory.PROFANITY: 1, # WARN
ModerationCategory.PRIVACY: 3, # BLOCK
ModerationCategory.POLITICS: 3, # BLOCK
ModerationCategory.PORN: 3, # BLOCK
ModerationCategory.AD: 2, # WARN + 审计
ModerationCategory.OTHER: 1, # WARN
}
action = BLOCK if SEVERITY_MAP[category] >= 3 else WARN
```
### 4.4 修改建议生成
```python
SUGGESTION_MAP = {
ModerationCategory.PROFANITY: "建议改为更专业的表达,例如:「我理解您的问题,我们一起想办法解决」",
ModerationCategory.POLITICS: "请避免讨论政治话题,保持服务专业性",
ModerationCategory.PORN: "请使用正式语言",
ModerationCategory.AD: "请勿发送广告内容",
ModerationCategory.PRIVACY: "请勿发送员工隐私信息(电话/身份证),如需联系请走企微",
ModerationCategory.OTHER: "请检查并修改表达",
}
```
---
## 5. 数据模型
### 5.1 v1.0 现状(无数据库)
词库写死在 `__init__` 方法,无 schema。
### 5.2 v1.1 计划 schema
#### 5.2.1 `sensitive_words` 表
```sql
CREATE TABLE sensitive_words (
id SERIAL PRIMARY KEY,
word VARCHAR(100) NOT NULL,
category VARCHAR(50) NOT NULL DEFAULT 'profanity', -- profanity/politics/porn/ad/other
severity SMALLINT NOT NULL DEFAULT 1, -- 1=WARN, 2=WARN+audit, 3=BLOCK
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(word, category)
);
CREATE INDEX idx_sensitive_words_active ON sensitive_words(is_active);
```
#### 5.2.2 `privacy_patterns` 表
```sql
CREATE TABLE privacy_patterns (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE, -- phone/id_card/bank_card/personal_email
pattern TEXT NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
```
#### 5.2.3 `moderation_logs` 表(v1.1 计划)
```sql
CREATE TABLE moderation_logs (
id BIGSERIAL PRIMARY KEY,
message_id BIGINT REFERENCES messages(id),
agent_id INTEGER REFERENCES agents(id),
matched_words JSONB, -- 命中的词列表
pattern_names JSONB, -- 命中的正则名列表
category VARCHAR(50),
action VARCHAR(20), -- pass/warn/block
text_excerpt TEXT, -- 文本前 100 字(避免存全量)
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_moderation_logs_agent ON moderation_logs(agent_id, created_at);
CREATE INDEX idx_moderation_logs_action ON moderation_logs(action, created_at);
```
### 5.3 Alembic 迁移
- 新建迁移:`alembic/versions/055_add_moderation_tables.py`
- 初始化 SQL`scripts/init_moderation.sql`(4 条基础词 + 4 条隐私正则 + 索引)
---
## 6. 接口设计
### 6.1 v1.0 服务层 API(已实现)
```python
class ContentModerationService:
def moderate(self, text: str) -> ModerationResult: ...
def check_privacy_leak(self, text: str) -> List[str]: ...
def add_custom_word(self, word: str) -> None: ...
def remove_custom_word(self, word: str) -> None: ...
```
### 6.2 v1.0 路由层(**未实现**
v1.0 没有 HTTP API 暴露词库管理。`admin_moderation_service.py` 仅做快速回复审核。
### 6.3 v1.1 计划路由层
| 接口 | 方法 | 说明 | 权限 |
|------|------|------|------|
| `/api/admin/sensitive-words` | GET | 列表查询 | admin |
| `/api/admin/sensitive-words` | POST | 添加 | admin |
| `/api/admin/sensitive-words/{id}` | PUT | 更新 | admin |
| `/api/admin/sensitive-words/{id}` | DELETE | 删除 | admin |
| `/api/admin/sensitive-words/test` | POST | 测试输入文本(不入库) | admin |
| `/api/admin/privacy-patterns` | GET/POST | 隐私正则管理 | admin |
| `/api/admin/privacy-patterns/{id}` | PUT/DELETE | 更新/删除 | admin |
| `/api/admin/moderation-config` | GET/PUT | 命中动作配置 | admin |
| `/api/admin/moderation-logs` | GET | 审计日志查询 | admin |
| `/api/admin/moderation-stats` | GET | 命中统计(高频词/误判率) | admin |
参考实现:[PRD-REQ-通用-002 快速回复规则后台管理 §3.2 管理 API](../01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md)**架构可复用**。
---
## 7. 部署与灰度
### 7.1 v0.7.1 部署现状
| 项 | 状态 |
|----|------|
| 容器 | `wecom_it_backend` 镜像内已包含 wordfilter 库 |
| 启动 | FastAPI `lifespan` 钩子不调用(v1.0 词库写死,无需加载) |
| 配置 | 无需环境变量 |
| 灰度 | 无灰度(全量默认开启) |
| 监控 | 无专门监控(依赖应用日志) |
### 7.2 v1.1 部署计划
| 项 | 计划 |
|----|------|
| 灰度开关 | `MODERATION_ENABLED`(默认 `true`),关闭时返回 PASS |
| 词库热加载 | `MODERATION_HOT_RELOAD`(默认 `false`),开启时每 60s 重新读取 |
| 数据库迁移 | Alembic 055 + init SQL |
| 监控指标 | `moderation_warn_total{category}` / `moderation_block_total{category}` |
### 7.3 配置同步铁律
> **来自工作记忆**:修改 `config.py` 默认值 ≠ 配置生效,必须同步更新服务器 `.env` 文件和 `docker-compose.yml` 的 `environment` 段。
新增 3 个配置项时需同步:
1. `src/backend/app/core/config.py` 字段
2. `docker-compose.yml``backend.environment`
3. `.env.example` 模板
---
## 8. 测试策略
### 8.1 单元测试(已存在,11/13 通过)
`src/backend/tests/test_content_moderation.py` 13 用例:
| 模块 | 用例数 | 状态 |
|------|--------|------|
| 敏感词命中 WARN | 4 | ✅ 4/4 |
| 正常文本 PASS | 2 | ✅ 2/2 |
| 隐私字段检测 | 3 | ✅ 3/3 |
| 自定义词库断言 | 1 | ✅ 1/1 |
| 默认值 WARN**反转测试** | 1 | ⚠️ 1/1(标记为"反决策"2026-07-08 决策后通过) |
| 隐私正则边界 | 2 | ⚠️ 1/2(中文+号码场景仍待补) |
### 8.2 v1.1 测试计划
- **功能测试**:CRUD(6)+ 词库热加载(2)+ 命中动作配置(3)= 11 条
- **E2E 测试**:坐席端发消息命中 WARN 场景(2 条)
- **回归测试**:现有 13 用例全部通过
- **性能测试**10000 字文本审核 < 50ms
### 8.3 关键测试场景
```python
# 中文+手机号场景(修复验证)
def test_chinese_phone_boundary():
leaked = service.check_privacy_leak("我的电话13800138000")
assert "phone" in leaked # \b 失效场景,数字边界修复
# 反向:WARN 不阻断(产品决策保护)
def test_warn_does_not_block():
r = service.moderate("自己不会百度吗")
assert r.action == ModerationAction.WARN
assert r.action != ModerationAction.BLOCK # 保护坐席自主权
```
---
## 9. 风险与降级
| 风险 | 等级 | 降级措施 |
|------|------|----------|
| wordfilter 库未安装 | 🟡 中 | `__init__` 异常时降级为纯正则匹配(仅隐私) |
| 服务挂掉 | 🟢 低 | 审核失败不抛异常,返回 PASS(设计 §1.3 P3 |
| 词库加载失败 | 🟡 中 | 降级为写死 4 条 + 日志告警 |
| 误报高频 | 🟡 中 | 词库极小(4 条),运营可手动加白名单(v1.1) |
| 漏报(新词未入库) | 🟡 中 | 依赖运营定期 review;可加 hit-miss 反馈机制 |
| 命中动作升级为 BLOCK 误伤 | 🟠 高 | 维持 WARN 直到 1.1 引入可配置化(评审已确认) |
### 9.1 降级路径
```
ContentModerationService.moderate(text)
├── 正常 → WARN/PASS
├── 词库未加载 → 仅返回空 PASS + 日志告警
└── 异常(如 wordfilter 库缺失)
降级为 PrivacyOnlyModeration(仅隐私正则)
仍异常 → 直接返回 PASS(不阻断)
```
---
## 10. 关联文档
| 文档 | 位置 | 关联点 |
|------|------|--------|
| 关联 PRD | `01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.0.md` | 需求来源 |
| 看板验真测试报告 | `03-测试文档/04-版本测试报告/看板验真-测试报告-20260707.md` | 历史基线(11/13 通过) |
| 内容审核服务源码 | `src/backend/app/services/content_moderation_service.py` | 当前实现 |
| 内容审核测试源码 | `src/backend/tests/test_content_moderation.py` | 13 用例基线 |
| 快速回复规则后台管理 PRD | `01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md` | 同类功能(运营后台词库管理),v1.1 架构可复用 |
| 知识库迭代技术方案 §4 代答排除规则 | `02-技术文档/技术架构/技术方案-REQ-知识-001-知识库迭代-v1.0.md` | 关键词匹配机制参考 |
| 管理后台原型图("代答排除规则"页) | `01-产品文档/08-集成生态/原型-REQ-集成-002-管理后台-v1.0.html` | 后续 v1.1 词库管理 UI 雏形 |
| 项目主文档 #81 | `07-项目管理/任务说明书/IT智能服务台-项目管理主文档.md` | v0.7.1 已上线 |
| Python 3 re 中文失效 | https://docs.python.org/3/library/re.html | `\b` 边界对中文失效背景 |
---
## 11. 变更日志
| 版本 | 日期 | 变更 | 变更人 |
|------|------|------|--------|
| v1.0 | 2026-07-28 | 首次整理:v0.7.1 上线内容回溯为正式技术方案 | 宋献 |