fix(W21): 智能度体检 → 3 类真实缺陷修复(C-8/C-9/C-10)+ 1 类待裁登记(C-11)
甲方反馈「客服 agent 还是不太智能」。本轮不新增出口、不改需求, 把"不智能"拆成可复现的实测条目:81 条真实口语问法 + 8 组多轮追问链 (真 HTTP:API → 队列 → Worker → 治理层),逐条定位根因并修复。 已修复(每条都有真机 HTML 证据,见 开发文档/D4.8 §3) - C-8 账户盈亏问法落"误导性澄清" 「我的基金赚了多少钱」→ E1 澄清,三个候选(万份收益/起投金额/收益率差别) 全是公开知识,而客户问的是自己账户的盈亏金额。 修法:P1_PATTERNS 补「第一人称 + 盈亏动词 + 金额疑问词」骨架, 金额疑问词是必要条件 —— 「我买的基金亏了怎么办」照旧走知识检索(库里有解)。 - C-9 多轮指代断链,把"已经说过的"又问一遍 「我想买个债基」→「它适合我吗」落 E2d「请告诉我具体的基金名称或代码」, 而客户刚刚才被告知是那只产品;「赎回费怎么算」→「持有 8 个月呢」同因。 两个根因:① _topic_of 只在答复前 4 行的固定形状里取主语,FAQ 型答复 (首行「问:…」)反解为空;② 意图分类把短追问标成 suitability_check, 绕开了知识出口里已做好的追问继承逻辑。 修法:_answer_suitability 建成四级降级链(严格主语 → 形状反解 _product_name_in_history → 有上文交回检索 → 首轮才澄清)+ 纯参数追问前置闸门。 安全边界三条不放宽:不给访客"能不能买"结论;推测出的名字查不到风险等级时 回落检索而非回"给不出结论";首轮指代保留澄清(金标 E-01 口径)。 - C-10 「你们投诉电话是多少」被强制转人工 根因:P2_WRITE_DISPUTE_KEYWORDS 里的裸词「投诉」子串命中,把"问投诉渠道" 和"提交投诉"撞在同一判据上;而答案就在库里(POL-SPM-036-01 / FAQ-0054)。 修法:新增 _is_p2_contact_inquiry() 作为第二类 P2 豁免(渠道词 + 疑问词, 且不带明确投诉意图)。「我要投诉,让你们经理来找我」照旧建单。 定位但未修(需甲方裁定合规口径) - C-11 E4 证据约束生成的答复被"收益数值"闸门整条拦回 E5b 「南方现金添利怎么样」(top1 1.0000) / 「买基金要手续费吗」(0.7328) / 「债基和货基哪个收益高」(0.5457) 三条同因:生成稿出现 YIELD_METRIC_TERMS → hits_zero_tolerance → _exit_partial → E5b 兜底。 试过"先净化再判合规",实测更差(drop_yield_claims 整行丢弃 + 生成稿常是 单行长段 ⇒ 整条被删空),已回退并把原因写进代码注释留痕。 建议方案:改 E4 提示词禁止输出收益数值(源头消除,不动红线代码)。 待判物 - _chunks_report.txt 判定可删:无代码引用 / 内容可从 knowledge/_chunks.jsonl 复算 / 从未入库 / 统计口径已写进 D2.4 §6 与 D2.8 §2。已删 + 加 .gitignore。 验收 - 金标 46 条:M-1 46/46、M-4 46/46、M-6 5/46(白名单 F-05/G-01/G-03/G-04/G-05, 零越界)、M-7/M-8/M-9/M-10 = 0、M-2 28/31、M-2b 15/18、M-3 4/4 —— 与 W20 基线逐项一致,零回归。 - 全量回归 1985 passed / 3 skipped(W20 基线 1969 passed / 3 skipped)。 - 新增守卫单测 16 条(含 C-8 两条 / C-9 四条 / C-10 两条)。 - ruff 仅剩 4 条既有告警,未顺手改,避免混入无关 diff。 文档 - 新增 开发文档/D4.8-客服Agent智能度体检与整改报告-W21-2026-09-21.md - 客服agent/D2.1 升 v6.37;开发文档/D1.1 升 v1.13;D1.6 增 §9 上下文提取
This commit is contained in:
@@ -596,3 +596,49 @@ def test_p2_write_and_dispute_requests_are_still_escalated(message: str) -> None
|
||||
assert route is not None, message
|
||||
assert route.priority == "P2", message
|
||||
assert route.transfer_required is True, message
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# `W21`:产品名里的单字动词不得被当成"改等级"的动词
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_risk_level_change_rule_is_not_fooled_by_a_product_name() -> None:
|
||||
"""产品名含「增」时,「X 的风险等级是啥」曾被判成**代办改等级**。
|
||||
|
||||
实测根因:`RISK_LEVEL_CHANGE_PATTERNS` 第 1 条(动词在前)的动词集合里有单字「增」,
|
||||
而产品名「南方稳**健增**利债券 A」里正好有一个 —— 「增 + 利债券 A 的 + 风险等级」
|
||||
满足 `增[^。;!?,,]{0,8}风险等级`。命中后的下场不是"答错",而是客户拿到一整段
|
||||
「风险等级不可代办」的说教:一个**陈述式提问**被当成了**代办请求**。
|
||||
修法是给单字动词加**词首**守卫(前面不能是汉字),多字动词不受影响。
|
||||
"""
|
||||
import re
|
||||
|
||||
from app.core.customer_service_rules import RISK_LEVEL_CHANGE_PATTERNS
|
||||
|
||||
blocked = [
|
||||
"南方稳健增利债券 A 的风险等级是啥",
|
||||
"南方稳健增利债券 A 的风险等级是多少?",
|
||||
"南方稳健增利债券A的风险等级是R几",
|
||||
]
|
||||
for message in blocked:
|
||||
assert rules.is_risk_level_change_request(message) is False, message
|
||||
# 顺带钉死每一条模式本身也不命中:只断言总入口会让"换一条模式补回来"逃过守卫
|
||||
assert not any(pattern.search(message) for pattern in RISK_LEVEL_CHANGE_PATTERNS), message
|
||||
|
||||
|
||||
def test_risk_level_change_rule_still_catches_the_real_requests() -> None:
|
||||
"""收紧之后**真请求一条都不能漏**(漏 = 红线 1 ④ 失守)。"""
|
||||
caught = [
|
||||
"帮我改一下我的风险等级",
|
||||
"把我的风险等级提到 C4",
|
||||
"风险等级能不能提上去",
|
||||
"能不能帮我把风险等级调高",
|
||||
"我想把风险评级改成 C5",
|
||||
"把等级降下来",
|
||||
"我的风险承受能力可以调整吗",
|
||||
"帮我上调一下投资者等级",
|
||||
"把风险等级调低一点",
|
||||
"风险评级想调低",
|
||||
]
|
||||
for message in caught:
|
||||
assert rules.is_risk_level_change_request(message) is True, message
|
||||
|
||||
Reference in New Issue
Block a user