覆盖区间:从"客服能答常见问题但换个说法就时灵时不灵"到"能对客户给出适当性结论"。 只写已经跑通并验证过的事实(附实测数据)与明确还没做的事,不含计划外推测。 内容:知识检索质量(字面兜底/行级切分/粒度选择,含两个我自己引入的回归)、 多轮上下文(换话题串味 / 只补主语不补内容)、适当性裁决接入、知识库内容补充、 调试前端;三个教训(_topic_of 反解文本咬了三次、"接口留好了线没接上"的模式、 阈值必须和数据规模与形状一起看);当前状态;下阶段计划与待决策事项;遗留缺口清单。 同时更正此前口述的两处错误:领先远程是 12 个提交(不是 16), 闲聊提示词配置是"从未落库"(不是"某次发布弄丢")。
12 KiB
24 · 客服 Agent 阶段性总结与下阶段计划
本文覆盖一段具体的工作区间:从"客服能答公募基金常见问题"到"客服能对客户给出适当性结论"。
写法说明:只写已经跑通并验证过的事实(附实测数据),以及明确还没做的事。 不含计划外的推测,也不含"应该差不多了"这类没验证的判断。
一、这一阶段的起点和终点
起点:客服 Agent 能回答常见问题,但有两个客户一用就会撞上的毛病——
- 同一个问题换个说法就时灵时不灵:「季季盈90天最低投多少钱」答得出来,「季季盈90天的起投金额是多少」却转人工;
- 问「我这个等级能不能买它」答不了:只会把 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白名单)。第三处缺失是静默失败关闭,所以这一步必须做。 - 新出口按三条硬约束走:
- 产品风险等级从知识库查(不猜、也不采信问句里的 "R2" 字样);
- 产品名只取上一轮回答里的主语(来自知识块字段,可信);
- 客户等级交给
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 新增本地客服控制台(可聊天的调试前端)
五、下阶段计划
要你先决策的两件事(挡着后续验证)
- 给 9001 补一条风险测评记录(
fin_risk_assessment,测试数据、会明确标注)。 不补的话,适当性出口永远只能演示"不能购买"这一半——"可以购买 + 需签风险揭示书"的完整路径看不到,也没法验证。 - 闲聊提示词配置从未落库。查证结果:整个库里涉及提示词的配置项是 0 行(我之前说"某次发布弄丢的"是错的,已更正)。版本 174 的编号虽然叫
cs-prompt-…,但没留下任何配置项,闲聊出口一直在用代码里的默认提示词(有兜底,功能正常)。 要按当初"走配置"的要求补上的话:必须先确认tools/publish_chitchat_prompt.py会继承版本 181 的配置项,否则会把刚配好的工具白名单清空。config_release是整版本替换语义。
紧接着要做的(不依赖上面的决策)
- 收掉
_topic_of的文本反解(方案 B):让出口显式声明主题。现在有方案 A 兜着,不急,但它是唯一一处"格式一改就可能悄悄答错"的地方。 transfer_required落库:目前它只在返回值里,API 不返回、消息表不存,前端只能靠"回答里是否含兜底话术开头"来判断。这是权宜之计。reconcile_graph.py --all还是手工的:图投影的对账没有自动化触发。
后续功能(按依赖顺序)
- 投顾 Agent:画像(
fin_customer_profile+profile_snapshots)、图投影(Neo4j)、适当性裁决都已就绪,可以开始。 - 风控 Agent:需要先有规则引擎,尚未启动。
- 登录接口:
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(记忆分层与画像)。
本阶段最值得抄走的一条经验:新接一个能力之后,别只看代码在不在,要跑一遍确认数据真的流过。这个项目里"接口留好了、线没接上"出现过至少五次,而它们的表现都是静默降级——不报错,只是回答变得又慢又差,或者一直转人工。