Files
group_fqcd_jr/docs/24-客服Agent阶段性总结与下阶段计划.md
T
lzf_0626 d40d808dd0 docs(24): 客服 Agent 阶段性总结与下阶段计划
覆盖区间:从"客服能答常见问题但换个说法就时灵时不灵"到"能对客户给出适当性结论"。

只写已经跑通并验证过的事实(附实测数据)与明确还没做的事,不含计划外推测。
内容:知识检索质量(字面兜底/行级切分/粒度选择,含两个我自己引入的回归)、
多轮上下文(换话题串味 / 只补主语不补内容)、适当性裁决接入、知识库内容补充、
调试前端;三个教训(_topic_of 反解文本咬了三次、"接口留好了线没接上"的模式、
阈值必须和数据规模与形状一起看);当前状态;下阶段计划与待决策事项;遗留缺口清单。

同时更正此前口述的两处错误:领先远程是 12 个提交(不是 16),
闲聊提示词配置是"从未落库"(不是"某次发布弄丢")。
2026-09-11 09:33:48 +08:00

12 KiB
Raw Blame History

24 · 客服 Agent 阶段性总结与下阶段计划

本文覆盖一段具体的工作区间:从"客服能答公募基金常见问题"到"客服能对客户给出适当性结论"。

写法说明:只写已经跑通并验证过的事实(附实测数据),以及明确还没做的事。 不含计划外的推测,也不含"应该差不多了"这类没验证的判断。


一、这一阶段的起点和终点

起点:客服 Agent 能回答常见问题,但有两个客户一用就会撞上的毛病——

  1. 同一个问题换个说法就时灵时不灵:「季季盈90天最低投多少钱」答得出来,「季季盈90天的起投金额是多少」却转人工;
  2. 问「我这个等级能不能买它」答不了:只会把 C1 的通用规则复述一遍,给不出结论。

终点:四个出口(知识直返 / 适当性裁决 / 引导人工 / 闲聊)都能稳定工作,c1客户能买它吗 这类问题会给出明确结论,且结论依据档案等级而非客户自述。


二、做了什么

1. 知识检索质量

三条都是客户实测先撞上、我再定位的:

现象 根因 做法 实测结果
「季季盈90天的起投金额是多少」转人工 专有名词在 embedding 空间不占优势,向量 top1 仅 0.6291,够不到 0.75 硬门槛 产品名走字面兜底召回(title like + 最长公共子串) 该问句唯一命中 PROD-007
问「起投多少」和问「风险高吗」拿到一模一样的整节表格 切分粒度太粗:一个叶子标题 = 一整节(表格 + 说明) Markdown 表格拆成行级自解释子块,父块保留;知识块 160 → 636 该问句 top1 0.6291 → 0.869,命中的正是「起投金额」那一行
问「介绍一下」只返回一行「产品期限 90天封闭期」 拆细之后某一行抢答了概括性问题 命中子块时一并带回整节父块,由 Agent 按问句选粒度 「起投多少」给一行、「介绍一下」给整节

顺带修掉两个由我引入的回归,都已固化成测试:

  • 字面匹配无条件优先时,把「基金赎回几天到账」答成了操作步骤(手册章节标题「5.2 基金赎回流程」与问句有 4 字重合)。→ 字面匹配改为只在向量结果不够确定时介入,重叠门槛从 4 字提到 6 字。
  • 把「doc_id 不含两位数字后缀」当成整节块,导致 FAQ/政策/公司信息的块全被误判、把正确答案挤出 top1。

2. 多轮上下文

现象 根因 做法
客户换话题时答非所问:先问季季盈、再问赎回,第二问答的是季季盈 检索问句无条件拼接上一轮客户原话 只在客户这一句自己说不清楚时才带上文(有明确指代词,或短到不构成完整意图)
追问「那它风险高吗」拿到整节表格 拼接的是上一轮整句,检索词被摊宽,只能命中粗粒度块 带上文时只带"在说哪个产品",不带上一轮的原话——上文的作用是补主语,不是补内容

3. 适当性裁决(这一阶段的主要新增能力)

把底座早就存在的 check_suitability 工具接进客服(接口留好了、线没接上,本项目第三次遇到同一类情况)。

  • 新增 suitability_check 意图,意图码三处对齐(AgentDefinition.supported_intents + agent_intent_config 的 active 行 + 发布版 agent_tools 白名单)。第三处缺失是静默失败关闭,所以这一步必须做。
  • 新出口按三条硬约束走:
    1. 产品风险等级从知识库查(不猜、也不采信问句里的 "R2" 字样);
    2. 产品名只取上一轮回答里的主语(来自知识块字段,可信);
    3. 客户等级交给 check_suitability 按档案解析,不采信客户自称。
  • 任何一步拿不到确定值就转人工 —— 这个出口会给出"能不能买"的结论,宁可答不了也不能答错。
  • 配置已发布(版本 181 active,5 条配置项),白名单 customer_service:suitability_check = [search_knowledge, check_suitability]。

实测:C3 客户能买它吗 会先说明判断依据("以您在公司留存的测评结果为准,不以本次自述为准"),再给结论。9001 因为在 fin_risk_assessment 里没有测评记录,得到"暂时无法购买 + 请先完成风险测评"——这是合规上的正确行为,不是故障。

4. 知识库内容

  • 为 C1–C5 各补一条「能买什么产品」问答,答案全部取自《个人投资者适当性管理指南》原文(第十二条匹配矩阵 + 第十四条跨级禁止 + 第十五条豁免规则),未自行编写规则。起因是「C1 客户能买什么」因缺"保守型"这个锚点而转人工(top1 0.5633、gap 0.0236)。
  • 顺带修正 load_knowledge_milvus.py 里过时的自检期望(该问句现在命中 FAQ 而非 POL-AST,两者内容一致)。

5. 调试前端

tools/chat_console.py:一个能直接聊天的本地控制台,页面展示识别到的意图与是否引导人工,方便观察路由。

为什么做成独立进程,而不是给底座加接口:底座目前没有登录接口(按计划推迟)。任何让浏览器直接拿令牌的做法——无论是 dev token 端点还是把私钥下发前端——都等于把"任意身份"开放给任何能访问服务的人。控制台把令牌签发与调用全部留在服务端进程内(私钥不出进程),底座代码零改动、零新增路由。

跑法:D:\conda\envs\jr_py313\python.exe tools\chat_console.py,然后打开 http://127.0.0.1:8098。 跑之前请停掉常驻 Worker —— 控制台自己驱动这一条 run,Worker 会抢走队列。


三、三个值得记下的教训

1. _topic_of 靠反解文本猜主语,咬了三次

它从回答文本里猜"这一轮在说哪个产品",于是每加一种回答格式就得补一条规则。三次咬人全部是客户实测先发现的:

次序 咬人的格式 后果
1 FAQ 回答首行 问:C1 客户能买什么产品? 把孤零零的"问"字当主语,污染检索词
2 适当性回答首行没有冒号 追问丢失指代对象,转人工
3 自述等级说明行占了首行 产品名被挤到第二行,追问又一次转人工

已做的补救(方案 A):tests/unit/service/test_customer_service_topic_matrix.py 把四个出口的真实产出都过一遍 _topic_of,谁再改回答格式或加出口,测试立刻红。写这个矩阵当场又抓出两个真 bug:

  • 整节块的主语带着章节号(2.1 南方季季盈90天),而下游要求产品名是块的子串——带编号永远匹配不上,适当性查询会静默失败、退化成转人工。这个故障从 _topic_of 最初写出来就存在,只是之前的追问恰好都走"行级子块"那条路径,一直没被触发。
  • 政策条款(第十二条 …)和表格行(| C1保守型 | ✅ 可购买 |)也会被当成主语。

还没做的根治(方案 B):让出口显式声明主题,不再反解文本。会碰 app/core/contracts.py,等需要动契约时再做。

2. "接口留好了,线没接上"是这个项目的常见模式

已发现的同类情况:user_facts/profile_snapshots 有表无 ORM、Neo4j 有驱动无装配、短期记忆有契约无装载器、图投影有 Worker 无生产者、check_suitability 有工具无白名单。

它们的共同表现是静默降级或静默失败,不报错。结论:每接一个能力,都要验证数据真的流过一遍,不能只看代码在。

3. 阈值必须和数据规模、数据形状一起看

  • IVF_FLAT + nlist=128 用在 26~73 行的集合上,我一度怀疑近似检索在丢召回 —— 实测 nprobe 从 1 拉到 128,分数一个数字都没变。猜测作废,索引不用动。
  • HIGH_SCORE=0.75 对"整节 Markdown 表格"这种长块永远达不到(产品类问句 top1 普遍 0.62~0.65),于是全靠 gap≥0.07 这条通道过 —— 而 gap 在"具体命中 + 泛化问答对"混排时又太紧。块切细之后分数自然上到 0.869,问题自己消失了:调阈值之前先看数据形状。

四、当前状态

项 值
分支 qyqy_develop_1,领先 origin 13 个提交(含本文,尚未推送)
门禁 ruff 干净 / mypy 113 文件无问题 / 497 unit+contract / 29 integration
知识库 636 块(faq / product / policy 三个集合)
发布配置 版本 181 active,5 条配置项
新增测试 5 个文件:test_knowledge_keyword_recall、test_knowledge_granularity、test_customer_service_search_query、test_customer_service_suitability、test_customer_service_topic_matrix
新增工具 tools/chat_console.py

本阶段的提交(自 07a922f 起):

c369a91 用格式矩阵把 _topic_of 的第四、五口咬痕也堵上
8b1ed70 自述等级的说明行不能挡住产品名
c564494 追问适当性结论时不能丢掉产品名
958fd62 锁住适当性出口的每条分支
33fbb0e 接入适当性裁决,回答"我这个等级能不能买它"
ee9de1b FAQ 型回答不能把"问"字当成追问的主语
7b3a728 为 C1-C5 各补一条「能买什么产品」的问答
a7135d2 补交检索问句的断言更新
4c2b147 产品知识拆到表格行级,并按问句选粒度
a6c09fa 检索问句只在客户这一句说不清楚时才带上文
570493e 知识检索增加产品名的字面兜底召回
07a922f 新增本地客服控制台(可聊天的调试前端)

五、下阶段计划

要你先决策的两件事(挡着后续验证)

  1. 给 9001 补一条风险测评记录(fin_risk_assessment,测试数据、会明确标注)。 不补的话,适当性出口永远只能演示"不能购买"这一半——"可以购买 + 需签风险揭示书"的完整路径看不到,也没法验证。
  2. 闲聊提示词配置从未落库。查证结果:整个库里涉及提示词的配置项是 0 行(我之前说"某次发布弄丢的"是错的,已更正)。版本 174 的编号虽然叫 cs-prompt-…,但没留下任何配置项,闲聊出口一直在用代码里的默认提示词(有兜底,功能正常)。 要按当初"走配置"的要求补上的话:必须先确认 tools/publish_chitchat_prompt.py 会继承版本 181 的配置项,否则会把刚配好的工具白名单清空。config_release 是整版本替换语义。

紧接着要做的(不依赖上面的决策)

  1. 收掉 _topic_of 的文本反解(方案 B):让出口显式声明主题。现在有方案 A 兜着,不急,但它是唯一一处"格式一改就可能悄悄答错"的地方。
  2. transfer_required 落库:目前它只在返回值里,API 不返回、消息表不存,前端只能靠"回答里是否含兜底话术开头"来判断。这是权宜之计。
  3. reconcile_graph.py --all 还是手工的:图投影的对账没有自动化触发。

后续功能(按依赖顺序)

  1. 投顾 Agent:画像(fin_customer_profile + profile_snapshots)、图投影(Neo4j)、适当性裁决都已就绪,可以开始。
  2. 风控 Agent:需要先有规则引擎,尚未启动。
  3. 登录接口:docs/19 里列为未解决项、当时有意推迟。前端要变成"能用"的产品级界面,这一步绕不过去。

已知但暂不处理的缺口

  • projection_reconciliation_service 的事件层 outbox(MemorySyncOutbox)没有生产者。
  • RUN_NOT_CANCELLABLE 这个错误码被复用在了状态冲突场景(语义不贴切)。
  • docs/05 §9.5 缺 agent-intent-configs 的 GET 说明;列表类接口普遍没有 next_cursor/has_more。
  • 客服热线仍是占位符 400-XXX-XXXX(customer_service.py 的 HOTLINE),真号码确定后应改为读配置项。

六、给组员的一句话

如果你要在这套底座上加新 Agent,先读 docs/19(接入实操 + 11 个真实坑)和 docs/23(记忆分层与画像)。

本阶段最值得抄走的一条经验:新接一个能力之后,别只看代码在不在,要跑一遍确认数据真的流过。这个项目里"接口留好了、线没接上"出现过至少五次,而它们的表现都是静默降级——不报错,只是回答变得又慢又差,或者一直转人工。