1. 从客服质检的痛点说起:为什么我选择动手做这个 Demo
电商客服这个岗位,表面看是“打字回复”,实际背后是一整套高压力的情绪劳动。我身边做运营的朋友经常吐槽:大促期间一个客服同时挂十几个会话,客户上来就是“你们是不是骗子”“再不发货我就投诉”,客服稍微回慢一点,情绪就炸了。而质检环节更头疼——传统质检靠人工抽检,一个质检员一天最多听几十通录音或翻几百条聊天记录,覆盖率低得可怜,等发现问题时,客户早就流失了。
我这次动手做的,就是一个电商客服质检 Demo,核心目标很明确:把客服和客户的对话实时过一遍,自动完成三件事——意图识别(客户到底想干嘛)、情绪监控(双方情绪有没有失控苗头)、危险话术拦截(客服有没有说出违规承诺或激化矛盾的词)。整套东西我用Jev来搭,配合TypeSafe的思路做结构化输出,最终跑通了一个能演示、能扩展的原型。
为什么选 Jev?因为这类质检场景对“输出结构稳定性”要求极高。你想想,如果模型返回的是一段自由文本,我还得再写正则去抠字段,那工程上就是灾难。Jev 在结构化生成和类型约束上的表现,让我可以把“意图”“情绪分”“风险等级”直接定义成 schema,模型按格式吐结果,下游代码拿到的就是干净的对象。这对做 Demo 来说,省掉了大量胶水代码。
这篇文章适合谁看?如果你是做电商中台、客服系统、或者对 LLM 落地质检感兴趣的后端/算法同学,这篇可以直接抄作业。我会把整体设计、Jev 的接入方式、意图分类体系、情绪打分逻辑、危险话术规则引擎,以及我踩过的坑,全部摊开讲。哪怕你之前没接触过 Jev,跟着走一遍也能理解这套质检链路是怎么转起来的。
2. 整体架构设计:质检链路是怎么串起来的
2.1 为什么是“规则 + 模型”双轨,而不是纯模型
一开始我也想过纯模型方案:把对话丢给 Jev,让它直接输出“是否违规、情绪如何”。但实测下来,纯模型在危险话术拦截上有个致命问题——漏报。比如客服说“这个我私下给你补”,模型可能觉得语气挺友好,判定为正常。但业务上这是典型的“私下承诺”,属于高危。规则引擎的好处是确定性:只要命中关键词或句式,立刻拦截,不跟你讲概率。
所以我的架构是双轨并行:
- 模型轨:Jev 负责意图识别和情绪监控,输出结构化结果。
- 规则轨:本地维护一份危险话术库,用 AC 自动机做多模式匹配,毫秒级命中。
两轨结果最后在一个决策融合层汇合,输出最终的质检标签。这样做的好处是,模型负责“理解语义”,规则负责“守住底线”,各司其职。
2.2 数据流全景:从一条消息到一张质检单
整个链路我拆成五步:
- 消息接入:模拟客服系统推送的对话流,每条消息带
session_id、role(客服/客户)、content、timestamp。 - 预处理:清洗文本(去表情、去多余空格)、按会话窗口聚合最近 N 轮对话。
- 模型推理:把聚合后的对话喂给 Jev,拿到意图标签、情绪分值、风险提示。
- 规则匹配:同一份文本过危险话术库,命中则打上规则标签。
- 融合输出:合并模型和规则结果,生成质检单,写入结果表。
这里有个设计细节:为什么要按会话窗口聚合,而不是单条消息分析?因为意图和情绪都是上下文相关的。客户单独说一句“好的”,你根本判断不出情绪;但结合前一轮客服说“退款要等 7 天”,这个“好的”可能就是不满的隐忍。所以我固定取最近 6 轮对话作为分析窗口,实测这个长度在客服场景里比较平衡,既够上下文,又不会让 token 爆掉。
2.3 TypeSafe 思路在质检里的价值
TypeSafe 这个词听起来玄,落到质检 Demo 里其实很实在:让模型的输出必须符合预定义的类型结构。我定义了一个质检结果类型,包含:
intent: 枚举,比如咨询物流、申请退款、投诉服务、闲聊。emotion_score: 0 到 1 的浮点数,越高越负面。risk_level: 枚举,low、medium、high。risk_reason: 字符串,说明为什么判风险。
Jev 在生成时被约束成只能输出这个结构。这样一来,下游的融合层拿到的就是强类型对象,不用做任何解析容错。我试过用普通模型输出 JSON,十次里有两三次会多带解释文字或者字段名拼错,而 Jev 的约束生成把这类问题压到了几乎为零。
3. Jev 接入实操:从环境准备到第一次跑通
3.1 环境准备与依赖安装
我是在 Windows 上做的开发,Jev 的本地部署对 Windows 支持还算友好。整体步骤不复杂,但有几个点容易卡住。
首先确认你的机器有 Python 3.10 以上,我用的 3.11。然后建虚拟环境,这一步别省,因为 Jev 的依赖里有些版本和系统全局包会冲突:
python -m venv venv venv\Scripts\activate接着安装核心依赖。我这里列的是我实际跑通的版本组合,你可以直接抄:
pip install jev-sdk==0.4.2 pip install pydantic==2.6.1 pip install pyahocorasick==2.0.0pyahocorasick是做危险话术多模式匹配用的,后面会讲。pydantic用来定义 TypeSafe 的 schema,和 Jev 配合很顺。
注意:Jev 的 SDK 版本更新比较快,如果你装完发现接口对不上,先看官方文档的版本说明,别硬套我这套版本号。我写这篇时用的是 0.4.2,接口是稳定的。
3.2 申请密钥与初始化客户端
Jev 需要密钥才能调用。申请流程这里不展开,按官方指引走就行。拿到密钥后,我建议放到环境变量里,别硬编码:
set JEV_API_KEY=your_key_here初始化客户端:
import os from jev import JevClient client = JevClient(api_key=os.environ["JEV_API_KEY"])这里有个小坑:Windows 下环境变量设置后,当前终端不生效,得重开一个终端。我第一次弄的时候折腾了十分钟,以为是密钥错了,其实是终端没刷新。
3.3 定义质检输出的 TypeSafe Schema
这是整个 Demo 的核心。我用 Pydantic 定义结构,Jev 会按这个结构约束生成:
from pydantic import BaseModel, Field from enum import Enum class Intent(str, Enum): LOGISTICS = "咨询物流" REFUND = "申请退款" COMPLAINT = "投诉服务" CHITCHAT = "闲聊" OTHER = "其他" class RiskLevel(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" class QualityResult(BaseModel): intent: Intent emotion_score: float = Field(ge=0.0, le=1.0) risk_level: RiskLevel risk_reason: str = Field(max_length=100)定义好之后,调用时把 schema 传进去,Jev 就会按这个格式返回。我实测下来,字段类型和枚举约束都能被严格遵守,emotion_score也不会跑出 0 到 1 的范围。
3.4 第一次调用与结果验证
先拿一段模拟对话试水:
dialog = """ 客户:我上周买的鞋子到现在还没发货,你们到底怎么回事? 客服:您好,我帮您查一下,请稍等。 客户:等什么等,都等一周了,再不发货我就退款投诉! """ result = client.generate( prompt=f"分析以下客服对话,输出意图、情绪分、风险等级和原因:\n{dialog}", schema=QualityResult, ) print(result.intent) # 投诉服务 print(result.emotion_score) # 0.85 print(result.risk_level) # high第一次跑通看到结构化输出的时候,确实挺爽。意图识别准确,情绪分也合理,风险等级因为客户提到“投诉”被判为 high。这一步验证了模型轨是通的。
4. 意图识别体系:怎么让模型分得准
4.1 意图分类的粒度设计
意图分类最怕两件事:太粗和太细。太粗,比如只分“咨询”和“投诉”,那质检报告没价值;太细,分几十个类,模型容易混,维护也累。
我最后定了五个大类,覆盖电商客服 90% 以上的场景:
| 意图标签 | 典型触发语 | 质检关注点 |
|---|---|---|
| 咨询物流 | 发货、快递、到哪了 | 响应时效 |
| 申请退款 | 退款、退货、不想要了 | 流程合规 |
| 投诉服务 | 投诉、差评、曝光 | 情绪安抚 |
| 闲聊 | 谢谢、好的、在吗 | 无需干预 |
| 其他 | 无法归类的 | 人工复核 |
这个粒度是我反复调过的。一开始我加了“咨询价格”“咨询尺码”这些,后来发现模型经常和“咨询物流”混,因为客户一句话里可能同时问价格和发货。合并成大类后,准确率明显上来了。
4.2 用 Few-shot 示例提升分类稳定性
Jev 支持在 prompt 里给示例。我发现给每个意图配 2 到 3 个典型例子,分类稳定性提升很明显。比如:
few_shot = """ 示例1: 客户:我的订单什么时候发货? 意图:咨询物流 示例2: 客户:这个我不要了,给我退钱。 意图:申请退款 示例3: 客户:你们客服态度太差了,我要投诉! 意图:投诉服务 """把这些示例拼进 prompt,模型对边界情况的判断会稳很多。我对比过,不加示例时“投诉服务”和“申请退款”偶尔会混,加了之后基本不混了。
4.3 意图识别的边界情况处理
实际对话里有一类难缠的情况:客户一句话包含多个意图。比如“你们发货这么慢,我要退款,还要投诉你们”。这时候模型只能输出一个意图,怎么办?
我的处理策略是:按优先级取最高的。优先级顺序是 投诉服务 > 申请退款 > 咨询物流 > 闲聊。因为投诉是最高风险,质检必须优先捕捉。这个优先级我写在 prompt 里明确告诉模型,让它遇到多意图时按这个规则选。
另外还有一类“意图漂移”:客户一开始咨询物流,聊着聊着变成投诉。我的做法是每个会话窗口独立分析,不继承上一轮意图。这样虽然会丢失一些连续性,但避免了错误传播。实测下来,独立分析反而更稳。
5. 情绪监控:给对话装一个“情绪温度计”
5.1 情绪分值的定义与计算逻辑
情绪监控我用的是一维分值:0 表示非常平静,1 表示极度愤怒。为什么不用“正面/中性/负面”三分类?因为质检需要的是趋势,不是快照。分值能让我看到情绪是往上走还是往下走。
Jev 在 prompt 里被要求输出这个分值。我给的指令是:
根据客户和客服的对话,评估客户当前的情绪激烈程度。0 表示完全平静,1 表示极度愤怒。只输出数值,不要解释。
实测下来,模型给分和人工标注的相关性挺高。我抽了 50 条对话做人工对比,偏差超过 0.2 的只有 4 条,主要出现在反讽语境,比如“你们可真厉害啊”,模型有时给 0.3,人工觉得应该是 0.7。这类反讽是情绪识别的老大难,后面我会讲怎么补。
5.2 情绪趋势判断:单点分值不够用
单点情绪分只能看当下,但质检真正关心的是情绪有没有失控趋势。所以我加了一个简单的趋势判断:取最近 3 个会话窗口的情绪分,做线性拟合,斜率大于阈值就标记为“情绪上升”。
def emotion_trend(scores): if len(scores) < 3: return "insufficient_data" # 简单斜率计算 n = len(scores) x_mean = (n - 1) / 2 y_mean = sum(scores) / n numerator = sum((i - x_mean) * (s - y_mean) for i, s in enumerate(scores)) denominator = sum((i - x_mean) ** 2 for i in range(n)) slope = numerator / denominator if slope > 0.15: return "rising" elif slope < -0.15: return "falling" return "stable"这个斜率阈值 0.15 是我调出来的。太低会频繁误报,太高又抓不住苗头。0.15 对应的是“三个窗口内情绪分涨了 0.3 左右”,这个幅度在客服场景里已经值得预警了。
5.3 客服侧情绪也要盯
很多人做质检只盯客户情绪,但客服情绪同样重要。客服如果情绪崩了,回复会变得机械甚至带刺,反而激化矛盾。所以我在 prompt 里让 Jev 同时输出客服情绪分,虽然 schema 里我只留了客户情绪分做主字段,但客服情绪分我存在了扩展字段里,用于内部预警。
实测发现一个规律:客服情绪分先于客户情绪分下降。也就是说,客服开始烦躁的时候,客户往往还没到爆发点。这个信号如果用好,可以在客户爆发前就介入,比如换一个客服接手。这个发现是我在跑了几百条模拟对话后总结出来的,挺有价值。
6. 危险话术拦截:规则引擎怎么守住底线
6.1 危险话术的分类与词库构建
危险话术我分了四类:
- 违规承诺:私下补偿、保证退款、承诺时效。
- 激化矛盾:你爱买不买、随便你投诉。
- 推卸责任:这不是我们的问题、你自己没看清。
- 敏感信息:引导私下交易、索要个人联系方式。
词库我是手工整理的,加上从历史质检记录里挖的高频违规句。整理下来大概 200 多条模式。这里要说明,词库不是越多越好,太多会误伤正常对话。我控制在 200 条左右,覆盖主要风险点。
6.2 AC 自动机实现毫秒级匹配
200 多条模式如果用 for 循环逐条匹配,一条对话要跑 200 次,太慢。我用 AC 自动机做多模式匹配,一次扫描就能找出所有命中:
import ahocorasick def build_automaton(patterns): A = ahocorasick.Automaton() for idx, pattern in enumerate(patterns): A.add_word(pattern, (idx, pattern)) A.make_automaton() return A automaton = build_automaton(danger_patterns) def match_danger(text): hits = [] for end_index, (idx, pattern) in automaton.iter(text): hits.append(pattern) return hits实测一条 500 字的对话,匹配耗时在 1 毫秒以内,完全不影响整体链路。
6.3 规则与模型的融合决策
规则命中后,怎么和模型结果融合?我的策略是:
- 规则命中
high风险词,直接覆盖模型结果,判high。 - 规则命中
medium风险词,如果模型也判medium或以上,维持;否则提升到medium。 - 规则未命中,以模型结果为准。
这样做的逻辑是:规则是底线,模型是补充。规则说高危,那就是高危,不给模型翻案的机会。规则没说话,才让模型发挥语义理解的优势。
7. 实操全流程:从零跑通一个质检会话
7.1 模拟数据构造
为了演示,我构造了一段典型对话:
session = { "session_id": "test_001", "messages": [ {"role": "customer", "content": "我上周买的鞋子还没发货"}, {"role": "agent", "content": "您好,我帮您查一下"}, {"role": "customer", "content": "查什么查,都一周了"}, {"role": "agent", "content": "这个我私下给您补个优惠券吧"}, {"role": "customer", "content": "我不要优惠券,我要投诉你们"}, ] }这段对话里,客户意图是投诉,情绪在上升,客服说了“私下补优惠券”属于违规承诺。正好能测全链路。
7.2 完整质检流程代码
def quality_check(session): # 1. 聚合对话 dialog = "\n".join( f"{'客户' if m['role']=='customer' else '客服'}:{m['content']}" for m in session["messages"] ) # 2. 模型推理 model_result = client.generate( prompt=build_prompt(dialog), schema=QualityResult, ) # 3. 规则匹配 rule_hits = match_danger(dialog) # 4. 融合决策 final_risk = model_result.risk_level if any(p in HIGH_RISK_PATTERNS for p in rule_hits): final_risk = RiskLevel.HIGH elif rule_hits and final_risk == RiskLevel.LOW: final_risk = RiskLevel.MEDIUM return { "session_id": session["session_id"], "intent": model_result.intent, "emotion_score": model_result.emotion_score, "risk_level": final_risk, "risk_reason": model_result.risk_reason, "rule_hits": rule_hits, }跑这段代码,输出大概是:
{ "session_id": "test_001", "intent": "投诉服务", "emotion_score": 0.88, "risk_level": "high", "risk_reason": "客户明确表示要投诉,情绪激烈", "rule_hits": ["私下给您补"] }意图、情绪、风险、规则命中全齐了。这个结果直接可以写进质检单。
7.3 结果落库与可视化
Demo 里我用 SQLite 存结果,方便演示:
import sqlite3 conn = sqlite3.connect("qc_results.db") conn.execute(""" CREATE TABLE IF NOT EXISTS qc ( session_id TEXT, intent TEXT, emotion_score REAL, risk_level TEXT, risk_reason TEXT, rule_hits TEXT ) """)可视化我没做太重,就用简单的统计:每天高危会话数、意图分布、平均情绪分。这些指标对运营来说已经够用了。
8. 常见问题与排查技巧实录
8.1 模型输出不稳定怎么办
现象:同样的对话,跑两次意图标签不一样。
排查:先看 temperature 设置。Jev 默认温度可能偏高,我把它调到 0.1 后,稳定性明显提升。另外检查 prompt 里有没有歧义表述,比如“分析对话”这种模糊指令,改成“按以下规则分类”会稳很多。
解决:温度降到 0.1,prompt 里明确分类规则,加 few-shot 示例。
8.2 情绪分和人工判断偏差大
现象:反讽语句情绪分偏低。
排查:反讽是语义理解的难点,模型容易按字面理解。
解决:我在 prompt 里加了一句“注意识别反讽、阴阳怪气等表达,这类应判为高负面情绪”。加了之后,反讽场景的偏差小了很多。另外可以维护一个反讽句式库,命中后直接给情绪分加 0.3。
8.3 规则误伤正常对话
现象:客服说“我帮您申请退款”,命中了“退款”相关规则,被判风险。
排查:规则词库太宽泛,“退款”本身不是危险词,危险的是“保证退款”“一定退款”。
解决:把规则从单词改成短语模式,比如“保证退款”“肯定能退”。同时给规则加角色限定,只匹配客服说的话,客户说“退款”不触发。
8.4 常见问题速查表
| 问题 | 可能原因 | 解决方向 |
|---|---|---|
| 意图分类混淆 | 分类粒度过细 | 合并相似意图 |
| 情绪分偏低 | 反讽未识别 | 加反讽提示或句式库 |
| 规则误报 | 词库太宽泛 | 改短语匹配,加角色限定 |
| 输出格式错乱 | 未用 schema 约束 | 启用 TypeSafe 结构 |
| 响应慢 | 对话窗口太长 | 限制最近 6 轮 |
8.5 几个我踩过的坑
第一个坑:Windows 下路径分隔符问题。我一开始把词库文件路径写成data/danger.txt,在 Windows 上跑报错。改成os.path.join拼接就好了。
第二个坑:Jev 的 schema 里枚举值中文。我一开始担心中文枚举会出问题,实测没问题,Jev 能正确输出中文枚举值。但如果你下游要做数据库存储,建议还是映射成英文,避免编码问题。
第三个坑:会话窗口取太长导致 token 超限。我一开始取最近 20 轮,结果长对话直接爆 token。后来改成 6 轮,并且对每条消息做长度截断,超过 200 字就截断。这个策略在客服场景够用,因为客服对话通常短平快。
9. 后续可以怎么扩展
这套 Demo 跑通后,我脑子里已经有好几个扩展方向。第一个是实时拦截:现在我是事后分析,如果改成流式处理,客服刚打出危险话术就弹窗提醒,价值更大。第二个是质检报告自动生成:把每天的质检结果汇总成报告,自动发给运营主管。第三个是意图识别的持续优化:把人工复核的结果回流,做 few-shot 示例的动态更新,让模型越用越准。
Jev 在这套链路里的角色很清晰:负责语义理解和结构化输出。它的 TypeSafe 特性让我省了很多解析代码,这是我最满意的地方。如果你也在做类似的质检或对话分析项目,建议先把 schema 定义清楚,再往上搭逻辑,这样后面改起来不会乱。
最后分享一个小技巧:情绪分的阈值不要拍脑袋定。我是拿了一批历史对话,人工标了情绪等级,然后看模型分值分布,找到区分“平静”和“愤怒”的分界点。我最后定的是 0.6,超过就算负面。这个值因业务而异,你得用自己的数据去校准。