用 pytest 守住 AI 智能体写库边界
文章将模型输出视为待审核的操作提案,由确定性后端代码决定是否写入 CRM,再用 pytest 测试决策逻辑。示例涉及事件去重、候选记录唯一性、姓名规范化、不可见字符检测与电话号码格式化。
AI 深度解读
AI 客服在 WhatsApp 上答得很顺,不代表它把资料写进了正确客户的 CRM。作者提出的办法是:让 LLM 提交操作提案,由后端规则决定是否写入,再用 pytest 检查这些规则和实际副作用。
这篇的价值在于把「模型看起来靠谱」变成几条可以验证的业务边界:客户是否唯一、是否确认修改、重复事件是否会重复入库。示例很小,也暴露了从演示代码走向生产时需要补上的缺口。
为什么要把写入决定交给后端
作者把流程拆成两步:模型返回提案,后端验证后执行。提案中包含 event_id、待修改的 campos,以及客户确认标记 confirmado_pelo_cliente。
模型可以提出修改,但写入权限应由确定性的代码控制。
下面这段代码负责决定去向,并执行写入或转交人工:
def decidir(proposta: dict, candidatos: list, eventos_processados: set) -> str:
if proposta["event_id"] in eventos_processados:
return "ignorar"
if len(candidatos) != 1:
return "revisao_humana"
if not proposta.get("confirmado_pelo_cliente"):
return "pedir_confirmacao"
return "gravar"
def processar(proposta, candidatos, eventos_processados, fila, crm):
decisao = decidir(proposta, candidatos, eventos_processados)
if decisao == "ignorar":
return decisao
if decisao == "revisao_humana":
fila.append({"event_id": proposta["event_id"], "motivo": "identificacao_ambigua"})
elif decisao == "gravar":
crm.append({"contato": candidatos[0], **proposta["campos"]})
eventos_processados.add(proposta["event_id"])
return decisao
要看清 len(candidatos) != 1 这一行:找到零个客户和找到多个客户,都会进入人工复核。只有唯一候选、已有确认、事件未处理,才允许写入。
这些分支不需要调用 LLM 就能测试。不过,「规则能测」不等于「规则已经覆盖所有风险」,更不等于整套 AI 系统已经验证完毕。
姓名和电话相同,为什么仍可能认错人
原文的前几项测试围绕输入规范化展开。
姓名查询键使用 unicodedata.normalize("NFKC", nome).casefold().strip()。测试验证了两件事:组合重音写法 Joa\u0303o 与 João 得到相同查询键;样式字母 𝐉𝐨𝐚𝐨 会得到 joao。这里并没有证明所有重音都会被去掉。
作者明确限定:查询键只用于寻找可能重复的客户,不能覆盖原始姓名。为了方便检索而做的转换,不该变成对客户资料的静默改写。
电话则通过 phonenumbers.parse 解析,默认地区为 BR,再用 phonenumbers.format_number 输出 phonenumbers.PhoneNumberFormat.E164 格式。(27) 99999-0000 和 +55 27 99999 0000 都应得到 +5527999990000。这个默认地区属于示例前提,其他业务不能直接照搬。
不可见字符采取另一种处理方式:检测并报告,不自动删除。unicodedata.category 用来识别 Cf,以及普通空格之外的 Zs;测试覆盖了零宽空格 U+200B 和不换行空格 U+00A0。
这些工具能减少匹配干扰,但示例没有把它们接入客户检索流程,也没有证明「唯一候选」必然就是正确客户。
测试要检查写了什么,不能只看返回什么
原文给出 5 类场景,再附加一个幂等性测试:姓名规范化、电话规范化、不可见字符检测、未确认禁止写入、身份歧义转人工,以及重复 webhook 不重复写入。
其中这段最能体现测试重点:既检查决策结果,也检查 CRM 和人工队列的变化。
def test_sem_confirmacao_nao_grava():
fila, crm = [], []
r = processar(proposta(confirmado=False), ["id_1"], set(), fila, crm)
assert r == "pedir_confirmacao" and crm == []
def test_duas_marinas_vao_para_fila():
fila, crm = [], []
r = processar(proposta(), ["marina_1", "marina_2"], set(), fila, crm)
assert r == "revisao_humana"
assert crm == []
assert fila[0]["motivo"] == "identificacao_ambigua"
关键是最后一行:返回「转人工」还不够,队列必须收到带原因的事件。不过,这里验证的是列表收到记录,尚未验证真实客服系统里的人员接单。
运行命令是 pip install pytest phonenumbers,然后执行 pytest -v。原文列出了测试代码,没有提供实际运行日志或通过率。
幂等性测试将同一提案处理两次,断言 len(crm) == 1。作者也提醒,生产环境要在数据库中对 event_id 建立 UNIQUE 约束,内存 set 只是为了让示例运行。
我的判断
我赞成把这套测试放在 AI 接入 CRM 之前。相比手动聊几轮、看回复是否自然,它直接检查数据库写入与人工分流,更贴近业务损失发生的位置。但作者没有给出两种方法的缺陷检出率对照,效果判断仍属于工程经验。
示例有两个需要补上的边界。首先,确认标记只做真假值判断;如果输入没有类型校验,字符串也可能被当成已确认。原文没有说明如何从客户消息中可靠地产生这个字段。
其次,processar 连「等待确认」也会登记为已处理。后续若沿用同一个 event_id 提交确认,就会被忽略。是否有问题,取决于系统如何区分消息事件与一次修改请求,原文没有说明。
这套测试能验证写入规则,不能替代模型提案质量和完整业务流程的测试。
原文还提到 NFC × NFKC、LGPD 下的确认与同意、handoff 和 QA 矩阵,但没有展开,不能据此认定这些问题已经解决。
能拿来做什么
- 在 Node 或其他后端中,把模型提案交给独立决策函数处理,让「允许写入」具备明确条件,并检查确认字段的类型与来源。
- 给 AI 修改客户资料的流程补上副作用断言:未确认时没有写入,身份歧义时人工队列确实收到事件,重复 webhook 只产生一次修改。
- 保留原始姓名,单独生成检索键;电话规范化时明确地区,并补测解析失败的输入。
- 测试「提出修改、等待确认、确认后写入」的完整流程,同时用数据库唯一约束承接生产环境的重复事件保护。