客服团队想在对话入口先挂一个 Agent,真正的卡点通常不是模型答得好不好,而是数据面:会话记录、订单详情、内部备注搅在同一段上下文里,交给谁都不放心。可以先按“Agent 实际读了什么 → 字段分三档 → 拒答与转人工兜底 → 历史工单回放校准”这个顺序推进,每一步都有配置、日志或工单记录可以核对,不靠主观判断。
判断方向是先收窄数据面再谈效果:抽样真实会话,标出被引用过的字段及来源系统,未被引用的先剔除;进入上下文的字段分可直接用、需脱敏、禁止进入三档,每档写明来源系统作为核对依据;再用拒答与转人工规则兜住边界并落日志。验证靠历史工单回放看误拒与漏拒,逐条调档。边界是:没回放过的字段不要临时放开。
在会话记录里找出真正被读到的字段
先别急着讨论“哪些数据能交出去”,先看清楚“Agent 现在到底读到了什么”。做法是从工单系统导出一批真实会话,逐条标注:回复文本里引用了哪些字段,这些字段来自哪个系统的哪个接口。标注只认两类证据——回复里出现了该字段的值(含脱敏片段),或者上下文快照里该字段确实被传入、且输出内容依赖它。凭“这个字段可能有用”不算证据。
标注表建议固定成几列,后面能直接转成策略配置:
| 字段名 | 来源系统 / 接口 | 是否被引用 | 初步处置 |
|---|---|---|---|
| order_no | 订单中台 / 订单详情 | 是 | 可直接用 |
| receiver_phone | 订单中台 / 订单详情 | 是(需核对) | 需脱敏 |
| cs_internal_note | 工单系统 / 内部备注 | 是 | 禁止进入 |
| risk_flag | 风控 / 名单查询 | 否 | 先剔除 |
标完先做减法:未被引用的字段直接从上下文剔除,不要留着“以后可能用得上”。这一步既缩小了后续分档的讨论范围,也为回放对比留下了基线——之后任何字段要加回来,都得在回放里说明它解决了哪一类问题。
把客服字段分成可直接用、需脱敏、禁止进入三档
分档的理由要能指向具体来源系统,不能只写“敏感”。同一个系统里出来的字段,处置也可能不同。
可直接用:订单号、商品名称、订单状态、售后单状态、下单时间、物流公司、运单号、知识库里的标准问题标题。这些字段来自订单中台订单详情接口与知识库,本身不含个人标识,出问题也能用工单号回查原值。
需脱敏:收件人姓名、手机号、收货地址、邮箱、支付流水号、发票抬头。这些字段来自订单中台与支付系统,客服答复需要核对但不需要完整值。脱敏在上下文组装时做一次,不要指望模型自己不打全。
def mask_phone(v: str) -> str:
s = (v or '').strip()
if len(s) < 7:
return '***'
return s[:3] + '****' + s[-4:]
def mask_name(v: str) -> str:
s = (v or '').strip()
return s[0] + '*' if s else '***'
def mask_address(v: str) -> str:
# 只保留到区/街道,去掉门牌号与房间号
return v.split()[0] if v and v.split() else '***'
禁止进入:身份证号、完整银行卡号、支付密码与短信验证码、工单内部备注(含客服主观评价与追责记录)、风控名单标记、供应商成本与毛利、其他客户的会话内容、员工账号与内网地址。理由是这些字段分别来自工单系统的内部备注、风控接口和财务系统,与当前问题能否回答无关,一旦进入上下文,泄露责任在业务侧,追溯时也很难自证。
设定拒答与转人工的触发条件
三档管的是“能看到什么”,触发条件管的是“答不了怎么办”。以下提问特征建议直接转人工,不做尝试性作答:涉及账户安全(改绑手机号、重置密码、索取验证码);涉及资金争议(退款金额不一致、要求赔付、要求书面承诺);涉及法律与监管(律师函、投诉至监管、起诉);关键实体在当前上下文缺失(订单号查不到、物流信息为空);连续两轮未解决且诉求前后矛盾;用户明确要求人工;用户主动报出禁止档字段要求核对。
话术只讲下一步和等待预期,不承诺结果,例如“这个需要核对账户信息,我马上为你转接人工同事,预计等待几分钟,请保持会话不关闭”。记录方式建议固定成日志字段:trace_id、命中规则名、触发字段、档位、是否转人工、脱敏后的诉求摘要。规则本身写成可替换的配置,便于回放时逐条开关:
handoff_rules:
- id: security_account
when: intent in [change_phone, reset_password, get_otp]
action: handoff
- id: money_dispute
when: intent == refund and amount_mismatch == true
action: handoff
- id: missing_entity
when: required_fields_missing(order_no)
action: clarify_once_then_handoff
- id: forbidden_field_in_query
when: query_contains(id_card, bank_card, otp)
action: refuse_and_handoff
拒答但不转人工的情形也要单列,比如与订单无关的闲聊、要求评价竞品的优劣,直接回一句“这个问题我这边给不出可靠答复”,并写入日志,避免和转人工混在一起统计。
用历史工单回放检验分档规则
分档和触发条件写完后,不要直接开门迎客,先拿一批历史工单离线回放:按当前策略重建上下文,跑规则判定,再和人工当时的最终处理结果对齐。结果分三类——判断一致、误拒(本来能自动答却转了人工)、漏拒(本该转人工却自动答了,可用人工结果里的升级或投诉标记反查)。统计只记条数和逐条清单,别急着折算比例。
# replay.py 骨架:本地离线跑,不调用线上模型
import json
POLICY = {'order_no': 'free', 'receiver_phone': 'mask', 'cs_internal_note': 'forbidden'}
def rebuild_context(ticket):
ctx = {}
for field, slot in ticket['context_snapshot'].items():
tier = POLICY.get(field, 'forbidden')
if tier == 'forbidden':
continue
ctx[field] = mask(field, slot['value']) if tier == 'mask' else slot['value']
return ctx
for line in open('tickets.jsonl', encoding='utf-8'):
t = json.loads(line)
ctx = rebuild_context(t)
verdict = decide(ctx, t['question']) # auto / clarify / handoff
print(t['ticket_id'], verdict, t['human_outcome'])
回放结果要落到一张修正记录上,逐条改档位,而不是整体重写策略。例如:物流状态类问题误拒集中,说明 logistics_status 被过度脱敏,可移回可直接用;个别工单出现漏拒,人工备注里有赔付升级动作,就给“退款金额不一致”补一条触发;把内部备注留在禁止档但发现答复质量明显下降,先确认被引用的是哪一句,再决定是否只放行该工单的公开回复字段。
修正记录建议包含:轮次、现象、涉及字段或规则、调整动作、回放中的依据工单号。每次调档后再跑一遍同一批工单,看误拒与漏拒是否互相挤压。规则稳定前,入口保持“Agent 起草 + 人工确认”的模式更稳妥;等回放清单里没有明显漏拒,再考虑放开自动答复的场景。