客服 Agent 重构收口:五出口决策链 + 知识库档位隔离 + 前端入参边界(答辩演示版本)
一、客服 Agent 智能增强(正面回应"不智能、动不动就转人工")
- 决策链由 2 个出口扩到 5 个:E1 澄清 / E2 计算型 / E3 知识直返 / E4 证据约束生成 / E5 分级回退
- 转人工从"默认动作"降为最后一档 E5c,只保留 4 类白名单:
P0 反诈 / P1 账户与个人数据 / P2 写操作与争议 / 用户明确要求人工
- 46 条金标实测(修复前 → 修复后):
转人工率 43.5% → 10.9%;出口准确率 45.7% → 100%;事实正确率 69.6% → 100%
禁忌违反 1 → 0;档位越权 / 无出处数字 / 误拒 四项零容忍全 0
- 安全不变量 INV-1~INV-5;零容忍规则未删,改的是挂载点
(输出侧字面黑名单 → 检索层档位隔离 + 判定层合规词表 + 输出守护)
二、知识库:档位单点化与物理隔离
- 新增 app/core/knowledge_tier.py 作为档位规则唯一落点(G-03),
knowledge_contracts.py 原定义块改为显式再导出(X as X,非副本)
- 档位过滤由 bool 默认值(fail-open)改为 tiers 必填集合(缺参即 TypeError)
- Milvus 侧四集合按 visibility 分区键物理隔离;双 schema 收敛为一套
- 新增 app/core/actor.py:访客三元组与匿名判定的唯一构造/判定点(G-01/G-01b)
- 新增 app/core/fund_fee_rules.py:费率计算纯函数
三、前端入参边界对齐(本轮 W11 新修,4 处"校验宽于存储")
- message 加 max_length=8000(与浮窗 widget.js 的 maxlength 一致)
- session_id 加 1—64;idempotency_key 上限 128 → 64(对齐列宽 String(64))
- feedback_type 加 max_length=32(对齐列宽 String(32))
- 8 条路径参数补 min_length=1 + max_length=64 + 字符集正则
({session_id} / {run_id} / {handover_id})
- 改前超限值会落到 MySQL 才失败(500);改后一律 422 AGENT_INPUT_INVALID + 字段级定位
- 新增 tests/unit/api/test_frontend_boundaries.py(33 例),含"端点表 ↔ OpenAPI 全量对照"
四、投顾模块整体清除(D4.4 / D4.5)
- 删除投顾相关 controller / schema / model / repository / service 及门户页面
- tools/portal_api_check.py 同步作废 AD003/AD005/AD011/A047 四条用例与 advisor_t 登录
(端点与账号均已不存在,此前稳定报 3 条假红)
五、验证(提交前实测)
- pytest -q:1856 passed / 2 skipped / 0 failed
- ruff check app tools tests:19(= 基线);mypy app:2(= 基线)
- 前端接口契约体检 portal_api_check.py:38 项,通过 34,失败 0,跳过 4
- 全链路冒烟 e2e_smoke_test.py --read-only:31/31
- HTTP 全链路探针 http_probe.py:11/11 succeeded
- 跨文档一致性 _consistency.py:GATE PASS
- 真机边界复验 12 条:12/12 符合预期
六、纪律与文档
- 可改文件白名单 A-09(docs/46)与底座会签申请单 A-10(docs/47,组 1—组 4 全部受理)
- 零 DDL:未新增/修改任何表结构,89 张业务表与基线一致
- 证据留痕:docs/evidence/**(含 46 条金标 score、快照、清除与重建记录)
- 未提交(刻意排除,见提交说明):仓库内 客服agent/ 与 开发文档/ 是 2026-09-16 前的
过期副本(Todolist 440 行 vs 权威 D2.1 1167 行),权威正本在仓库外;
_chunks_report.txt 是 tools/build_knowledge_chunks.py 生成的本地产物
This commit is contained in:
@@ -1,173 +0,0 @@
|
||||
# 设计文档:客服 Agent 本体(子项目 B)
|
||||
|
||||
> 🗂 **过程产物 · 结论已归档**(2026-09-14 批注)
|
||||
> 本设计**已实现**(`CustomerServiceAgent` 已注册)。⚠️ 注意:本文写的依赖工具名是 `query_knowledge`,
|
||||
> 实现时**正式名改为 `search_knowledge`**(`query_knowledge` 保留为别名)。
|
||||
> **判断当前进度请看 `docs/验收与审计/phase1-acceptance-report.md`**。
|
||||
|
||||
> 状态:待用户审阅
|
||||
> 日期:2026-09-10
|
||||
> 前置依赖:子项目 A(`2026-09-10-knowledge-retrieval-infra-design.md`)必须先完成,本文档依赖其 `query_knowledge` 工具。
|
||||
> 权威来源优先级:行为边界以《客服Agent一期运行边界基线_v1.md》为最高优先级(该文档自身声明"高于历史问答、示例话术和模型自由生成内容");
|
||||
> 底座接入方式以 `docs/14-Agent组员统一接入说明书.md` 和 `TODO.md` T8.1 为准;
|
||||
> 二者冲突时以边界基线为准(业务行为),接入方式冲突时以底座文档为准(工程约束)。
|
||||
|
||||
## 1. 背景
|
||||
|
||||
`TODO.md` T8.1 要求客服 Agent 覆盖 FAQ、产品咨询、政策解释、闲聊、转人工五类意图。桌面胜宇提供的三份"一期"材料把这个需求收窄为一套更具体、更严格的产品规则:一个虚构品牌"奶龙基金责任有限公司"及其客服"奶龙基金智能助手",服务范围只是解释已审核公开信息和指引办理路径,不触碰任何个人账户数据、不代办交易、不提供投资建议。
|
||||
|
||||
## 2. Agent 定义
|
||||
|
||||
```python
|
||||
class CustomerServiceAgent(BaseAgent):
|
||||
definition = AgentDefinition(
|
||||
agent_type="customer_service",
|
||||
version="1.0.0",
|
||||
allowed_roles=("customer",),
|
||||
allowed_portals=("api",),
|
||||
allowed_tools=("query_knowledge",),
|
||||
supported_intents=("faq", "product_inquiry", "policy_explain",
|
||||
"chitchat", "transfer_human"),
|
||||
)
|
||||
```
|
||||
|
||||
`supported_intents` 沿用 `docs/02` 的五类,供底座自动意图分类器使用、供配置中心按意图配置工具白名单使用。但**这五类分类结果不是 `handle()` 内部安全路由的唯一依据**,理由见第 3 节。
|
||||
|
||||
**范围确认:本 Agent 只服务已登录客户端模块,游客版客服是完全独立的功能**
|
||||
|
||||
《一期运行边界基线》§2 把服务对象分成"访客"和"已登录用户"两档,一度让人以为本 Agent 需要同时处理未登录访问——
|
||||
但结合整体前端规划确认:产品分为面向游客的公开站点(公司介绍/产品介绍/登录页/游客版智能客服)和登录后按角色
|
||||
(客户/风控/投顾等)区分的客户端,**本文档设计的 `customer_service` Agent 只是客户端里的一个模块,只服务已登录客户**;
|
||||
游客版智能客服是公开站点上完全独立的另一套功能,不经过本 Agent,也不在本文档范围内。
|
||||
|
||||
这与底座鉴权架构(`app/api/dependencies/auth.py` 要求任何 Agent 运行都必须携带有效 JWT,没有匿名访问路径)天然吻合,
|
||||
不需要额外改动底座。第 3 节 P1 分支"已登录场景可附加引导至我的账户页面"这句话因此永远成立——能走到本 Agent 的
|
||||
请求都必然来自已登录客户。《一期运行边界基线》里关于"访客"那一档的描述,适用对象是游客版智能客服(独立功能,
|
||||
未来若需要设计,是另一个独立任务),不适用于本文档。
|
||||
|
||||
## 3. 核心设计决策:安全路由与知识检索分离
|
||||
|
||||
《一期运行边界基线》第 9 节明确要求:安全、权限、个人数据、写操作、投诉赔偿类问题"必须在 RAG 检索前完成识别与拦截",且"不应仅依赖向量检索命中"。这意味着不能把"要不要查知识库、要不要转人工"这件事完全交给:
|
||||
|
||||
- 底座自动跑的 LLM 意图分类器(`self._classified_intent`,本身是私有属性,且分类结果依赖模型输出,不是确定性的);
|
||||
- 向量检索是否命中某条"应该拒答"的知识条目(万一语义相似度没打准,就会漏判)。
|
||||
|
||||
因此 `handle()` 内部自己实现一个**确定性的安全路由**(关键词/正则规则,不调用模型),独立于底座自动意图分类,逐条对照《一期运行边界基线》§5 和《向量化前流程文档》§3 的优先级表:
|
||||
|
||||
```text
|
||||
P0 安全优先(诈骗/盗号/验证码泄露/资金风险关键词命中)
|
||||
→ 固定安全话术 + transfer_required=True,不查知识库
|
||||
|
||||
P1 本人/他人账户数据(持仓/收益/订单/银行卡/投诉进度/风险测评结果类关键词)
|
||||
→ 固定降级话术("当前客服 Agent 无法读取该项本人数据")
|
||||
已登录场景可附加引导至"我的账户"页面;不查知识库
|
||||
|
||||
P2 转人工诉求(代办交易/资料修改/销户/投诉赔偿/法律争议/明确要求人工)
|
||||
→ 固定转人工话术 + transfer_required=True,不查知识库
|
||||
|
||||
P3 公开静态问题、闲聊、人设问答
|
||||
→ 调用 query_knowledge 工具检索(闲聊类问答本身也在 105 条知识库内,
|
||||
走同一条检索路径,不单独分支)
|
||||
注意:子项目 A 第一版只有 fin_faq_collection 有数据,product_inquiry/
|
||||
policy_explain 两个意图对应的集合暂时是空的,命中率等同于未命中,
|
||||
会自然落入下面的 P4 兜底话术,这是已知的第一版限制,不是 bug。
|
||||
|
||||
P4 低置信度/未命中
|
||||
→ 固定"无法确认,建议转人工"话术,不得猜测或编造
|
||||
```
|
||||
|
||||
这个安全路由表本身**不依赖底座的 `IntentClassifier`**,因此不需要解决"`handle()` 里读不到 `self._classified_intent`"这个此前发现的底座缺口——本 Agent 完全绕开这个问题,用自己的确定性规则做主要业务判断。底座的自动分类结果仍然会被记录进 `CoreResult.intent`(供审计和统计用),但不驱动业务分支。
|
||||
|
||||
安全路由的关键词/正则规则来源:《一期运行边界基线》§4 的禁止事项分类描述和《一期向量化前流程文档》§3 的场景描述,实现时需要把这些自然语言描述转成具体的匹配规则列表,作为 Agent 内部常量维护(不需要走数据库配置化,因为这是硬编码的合规红线,不是可调业务参数)。
|
||||
|
||||
## 4. 知识命中后的回复生成
|
||||
|
||||
按《向量化前流程文档》的要求,标准答案不应被模型自由改写。结合确认过的决策:
|
||||
|
||||
- **命中 1 条**:直接返回该条 `content_text` 原文,不调用模型。
|
||||
- **命中多条**:允许调用 `self.generate_with_model()` 把这几条标准答案"组织一下语言",但 Prompt 必须明确约束"只能使用给定的几段文字内容,不得添加任何未出现在原文中的新事实、数字、承诺或渠道信息",属于"复述/合并"而非"生成新内容"。
|
||||
- **未命中或全部相似度低于阈值**:走 P4 固定话术,不调用模型编造。
|
||||
|
||||
`source_references` 由 `query_knowledge` 工具调用自动生成(`ToolExecutor` 已有机制),Agent 不需要自己拼引用。
|
||||
|
||||
**`CoreResult.intent` 必须由 `handle()` 显式设置,不能留空等底座自动分类结果回填**:底座会在 `handle()`
|
||||
执行前自动跑一次独立的 LLM 意图分类(结果存在 `self._classified_intent`,`handle()` 内部读不到,也不应该依赖它,
|
||||
见第 3 节)。`BaseAgent._execute_governed()` 的规则是 `result.intent or self._classified_intent`——如果
|
||||
`handle()` 返回的 `CoreResult.intent` 是 `None`,最终写入 `conversation_message.intent` 的会是那个独立分类器的结果,
|
||||
它完全可能和 Agent 本节安全路由实际走的分支对不上(例如安全路由判定为 P1 账户数据问题直接拒答,但自动分类器
|
||||
独立跑出来的结果是 `intent=faq, confidence=0.8`,两者没有关联)。
|
||||
|
||||
因此 `handle()` 必须根据自己在第 3 节安全路由里实际走的分支,显式构造并返回准确的 `IntentResult`:
|
||||
|
||||
```text
|
||||
P0/P1/P2(安全/账户数据/转人工诉求) → intent="transfer_human", confidence=1.0, needs_clarification=False
|
||||
P3 命中知识库 → intent 取 "faq"/"product_inquiry"/"policy_explain" 中实际命中的一个,
|
||||
confidence 用检索相似度或固定较高值
|
||||
P3 闲聊/人设问答 → intent="chitchat", confidence=1.0
|
||||
P4 未命中/低置信度 → intent="transfer_human", confidence=<实际值>, needs_clarification=True
|
||||
```
|
||||
|
||||
这样落库的 `intent` 字段才能真实反映业务分支,供审计、统计和(如果第 6 节采用方案二)闲聊连续计数使用。
|
||||
|
||||
## 5. 转人工
|
||||
|
||||
Agent 只做两件事:① 在回复文本里包含《一期向量化前流程文档》§3 规定的固定联系方式(人工客服 15936583816,工作日 09:00-18:00 等);② 在 `CoreResult` 上设置 `transfer_required=True` 和 `transfer_reason`。
|
||||
|
||||
**联系方式文案的来源需要澄清**:由于第 3 节 P0/P1/P2 分支明确"不查知识库",这几个分支的固定话术(含电话、邮箱、地址等联系方式)
|
||||
**只能是硬编码在 Agent 代码里的常量**,不可能像 P3 分支那样从 `query_knowledge` 动态取——安全关键路径不应该依赖额外的
|
||||
检索调用才能返回正确结果。这意味着联系方式如果未来变更,需要同时改两个地方:QA 知识库源文件(供 P3 检索到的知识内容使用)
|
||||
和 Agent 代码里的常量(供 P0/P1/P2 安全话术使用)。这是刻意的取舍,不是疏漏。
|
||||
|
||||
是否真正调用 `POST /conversations/{session_id}/handover-requests` 创建 `svc_handover_ticket`,由前端根据 `transfer_required` 标记决定是否调用,不属于本 Agent 职责——这与 `AGENTS.md`"Agent 只能提出转人工请求,不能分配、接单、解决或关闭工单"的约束一致,也是本轮讨论中厘清的、胜宇材料与团队大文档表面冲突实际不冲突的点(见子项目 A 文档记录之外,本文档单独说明:大文档设想的"生成完整工单"是前端调用已有接口后的结果,不是 Agent 的新增职责)。
|
||||
|
||||
## 6. 闲聊计数与软引导
|
||||
|
||||
《一期运行边界基线》§3 和《向量化前流程文档》§3 都提到"连续闲聊第 4 条仅做一次自然业务引导"。
|
||||
|
||||
**实现方式需要先说明一个架构约束**:业务 Agent 不允许直接创建 `SQLAlchemy Session`、直接查
|
||||
`ConversationRepository` 或任何 Model(`AGENTS.md`/`docs/14` 明文禁止),第一版设计里"直接查
|
||||
`conversation_message` 最近 3 条"的写法违反了这条规则,已修正如下:
|
||||
|
||||
这是一条 UX 软性提示规则,不是合规红线(不像 P0-P2 那样错了会有实质风险),因此**第一版不追求跨请求的精确计数**,
|
||||
按以下简化方式实现,两种都不需要新增底座能力:
|
||||
|
||||
- 方案一(推荐,第一版采用):不做跨轮次持久计数,只在单次 `handle()` 内根据本次和上一轮用户消息的表面特征做启发式判断,
|
||||
或者干脆先不实现"连续第 4 条"的精确触发,只保证"闲聊类回复本身语气自然、不生硬",把精确计数列为已知的第一版简化项。
|
||||
- 方案二(如果产品认为这条规则必须精确执行):需要底座新增一个只读工具(例如
|
||||
`get_recent_intent_history`,走 `ToolExecutor` 注册,权限和其他工具一致地声明),Agent 通过
|
||||
`self.call_tool(...)` 调用,而不是绕过工具体系直接查库。这属于对底座的小额扩展,需要和底座负责人确认是否本轮一起做。
|
||||
|
||||
本设计文档默认采用方案一,方案二作为备选留给用户决定是否升级范围。
|
||||
|
||||
## 7. 合规治理的兼容性
|
||||
|
||||
`handle()` 产出的 `CoreResult` 仍然会经过底座既有的 `governance.review_output()`(负面词过滤、引用来源校验、手机号/证件号脱敏),不需要额外适配——因为 Agent 返回的是知识库原文或"复述"文本,本身已经是审核过的合规内容,治理层是兜底而非主要防线,两层防线不冲突。
|
||||
|
||||
## 8. 测试计划
|
||||
|
||||
采纳《客服Agent一期向量化前流程文档_v1.md》§5 第五步给出的验收测试清单,逐项覆盖:
|
||||
|
||||
```text
|
||||
公开产品资料、15 点规则、费率、适当性问题 → 正确命中知识库并作答
|
||||
本人持仓、订单状态、投诉进度问题 → 一律固定降级话术,不返回任何数据
|
||||
明确要求人工 → 固定转人工话术 + transfer_required=True
|
||||
验证码/密码泄露、诈骗场景 → 立即安全话术,不继续收集信息
|
||||
投资推荐类问题 → 明确拒绝给出具体建议
|
||||
连续闲聊达到第 4 条 → 触发一次软性业务引导,之后不重复
|
||||
低置信度/无法判断的问题 → 固定"无法确认"话术,不编造
|
||||
```
|
||||
|
||||
另外补充底座接入层面的常规测试(参照 `docs/14`):未授权角色/入口调用拒绝、工具白名单外调用拒绝、模型格式错误时安全路由仍正常工作(因为安全路由不依赖模型)。
|
||||
|
||||
## 9. 范围外事项
|
||||
|
||||
- 真实创建 `svc_handover_ticket` 工单——前端职责。
|
||||
- `fin_product_collection`/`fin_policy_collection` 的实际内容——见子项目 A 文档第 2 节。
|
||||
- 知识库内容的在线编辑/审核——见子项目 A 文档第 2、8 节。
|
||||
- "我的账户"页面本身——独立前端页面和后端专属接口,本 Agent 只做跳转提示,不实现该页面。
|
||||
- 游客版智能客服(公开站点上面向未登录访客的客服功能)——与本 Agent 是完全独立的两套功能,不共用后端实现,见第 2 节范围确认。
|
||||
**已确认后续也要接入本底座**,但这涉及匿名身份认证(`AGENTS.md` 列为高风险区域的 Authentication),
|
||||
需要单独作为子项目 C 走一次完整的 brainstorm 和设计评审,不在本文档展开,也不应该为了预留接口而现在改动
|
||||
`build_request_context`/JWT 相关代码。子项目 C 启动时的关键设计问题:匿名身份怎么签发和校验、新增角色
|
||||
`role=guest` 的权限边界(大概率只能调用 `query_knowledge`,不能召回记忆、不能有任何客户归属数据)、
|
||||
游客会话如何过渡到登录后的正式会话(如果产品要求两者衔接)。
|
||||
Reference in New Issue
Block a user