很多 AI 项目上线前,接口回归是绿的,演示也顺利。可一到真实用户手里,还是出了事:同一笔售后申请因为网络重试被处理两次,客服看到两条退款单,订单系统的库存也被重复释放。
这类事故最让人不舒服的地方在于:没有一个接口“失败”。每一次调用都返回 200,每一段模型输出也都礼貌、通顺。出错的是测试对象还停留在“接口返回什么”,没有走到“业务动作究竟发生了几次”。
先别问模型答得对不对,先问它有没有越权做事
传统接口测试的基本假设是:调用一次,服务处理一次。可接入 AI 后,这个假设失效了。模型可能重试工具、改写参数、在同一轮里多次调用;编排器也可能在超时后自动补偿。对“退款、发券、改价、工单创建”这种写操作,最终风险并不在模型有没有说错话,而在副作用能否被控制。
以售后退款为例,测试至少要把下面四件事放进一份可执行的契约:
- 是否先完成订单资格校验,再允许创建退款;
- 同一个申请号重放三次,是否仍然只产生一条退款记录;
- 高金额、证据冲突、用户无权限时,是否转人工而不是继续执行;
- Trace 是否能串起模型版本、策略版本、工具序列与订单最终状态。
这不是把测试写复杂,而是在替业务守住“动作不能出错”的底线。OpenAI 的 Agent Evals 指南也把工具调用、护栏和交接放在完整 Trace 中评估;这说明今天的 AI 质量不该只评分最终文本。参考:Agent Evals
一个可以直接放进 CI 的动作校验
下面的例子不是为了展示 Python。它验证的是退款工作流里最关键的两个业务约束:校验先于退款,以及高风险必须人工接管。
def assert_refund_contract(trace: dict) -> None:
tools = [s for s in trace["spans"] if s["kind"] == "tool"]
names = [s["name"] for s in tools]
# 不允许模型跳过资格校验直接写退款单
assert names.index("check_refund_eligibility") < names.index("create_refund")
create_refund = next(s for s in tools if s["name"] == "create_refund")
# 重试或重放时,写操作必须可去重
assert create_refund["attributes"].get("idempotency_key")
risk = trace["risk"]
if risk["evidence_conflict"] or risk["refund_amount"] > 1000:
assert trace["business_outcome"] == "manual_review"
真正的工程价值是:模型、Prompt、工具说明任何一个版本变了,CI 都能用同一批高风险样本告诉你——这次变化是不是把“多退一次”的门重新打开了。
给测试工程师的升级动作
从今天起,不妨把测试用例里的“预期结果”拆成三层:
- 模型层:是否理解了意图;
- 工具层:调用顺序、参数和权限是否正确;
- 业务层:订单、金额、库存、审批状态是否只产生了预期结果。
能把第三层说清楚的人,才是在做 AI 测试开发,而不只是给 AI 接口补断言。