本提交为 .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-*/
19 KiB
技术方案 - 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.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_block,2026-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 = WARN(v1.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 关键时序
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 核心实现
# content_moderation_service.py
class ContentModerationService:
def __init__(self):
self.wf = Wordfilter() # wordfilter 0.2.7,DFA 算法
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 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 命中动作决策
# v1.0 固定 WARN
action = ModerationAction.WARN
未来扩展(v1.1):
# 根据 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 修改建议生成
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 表
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 表
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 计划)
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(已实现)
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,架构可复用。
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 个配置项时需同步:
src/backend/app/core/config.py字段docker-compose.yml的backend.environment.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 关键测试场景
# 中文+手机号场景(修复验证)
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 上线内容回溯为正式技术方案 | 宋献 |