模型编造 API 参数,工具调用连环 500——Function Calling 契约测试

简介: 本文详解Function Calling契约测试:针对模型编造非法参数(如虚构枚举值)导致连环400/500错误的问题,提出“工具网关+Pydantic校验+Mock契约测试”三重防线,确保参数合法才调用API,并结构化反馈错误助模型自纠。附三层测试方法与面试高频题解析。

04 | 模型编造 API 参数,工具调用连环 500——Function Calling 契约测试

AI 测试开发面试高频题:"Function Calling / Tool Use 怎么测?"
工具调用是 Agent 的手脚,也是事故重灾区:模型会编造不存在的参数值,而下游 API 不会惯着它。这篇讲怎么在中间加一道契约防线。

一、真实场景:一个不存在的枚举值,烧掉一晚上 token

某物流公司的客服 Agent 上线了工具调用能力:用户问订单,Agent 自动调 query_order 查状态,符合条件再调 create_refund 发起退款。真实 API 的退款接口要求 status 字段必须是枚举值:pending | shipped | delivered

某天晚上,用户问"我的订单三天没动了,帮我退款"。Agent 判断订单卡在 shipped 状态,发起退款调用——但它传的参数是 "status": "shipped_today"。一个它自己发明的枚举值。

API 返回 400。接下来发生的事才是真正的损失:错误信息被原样塞回给模型("Bad Request"),模型看不懂哪里错了,换了个姿势再试:"status": "已发货""status": "in_transit"……每一次都是编的,每一次都是 400。重试逻辑没有上限,这个死循环跑了四十多分钟,直到值班同学发现 token 消耗曲线异常才手动掐掉。一次本该 3 秒完成的退款请求,烧掉了平时一整天的调用预算。

复盘发现两个叠加的坑:一是工具描述是写在 prompt 里的一段自然语言("status 是订单状态"),没有严格 schema,模型只能猜;二是重试逻辑只判断"调用失败就重试",不看失败原因,也不限制次数。

二、思路:工具调用就是接口调用,先立契约再放行

传统接口测试里,参数合法性靠 schema 校验。Function Calling 也一样——模型生成的每次工具调用,都必须先过一道契约校验,合法才放行到真实 API,不合法就把结构化的错误喂回给模型让它自我纠正。 这道关卡叫工具网关。

三、核心代码:工具网关校验 + Mock 契约测试

第 1 步:用 Pydantic 定义工具契约,网关统一拦截

from enum import Enum
from pydantic import BaseModel, ValidationError

class OrderStatus(str, Enum):
    pending = "pending"
    shipped = "shipped"
    delivered = "delivered"

class RefundParams(BaseModel):
    order_id: str
    status: OrderStatus          # 枚举写死,模型编的值在这里就被拦住
    reason: str

TOOL_MODELS = {
   "create_refund": RefundParams}

def tool_gateway(name: str, raw_args: dict):
    """所有工具调用的唯一入口:先校验,再转发;非法参数不进真实 API"""
    try:
        params = TOOL_MODELS[name](**raw_args)
    except ValidationError as e:
        # 关键:把"哪里错了"结构化地告诉模型,给它自我纠正的机会
        return {
   "error": "invalid_params",
                "detail": [{
   "field": x["loc"][0], "msg": x["msg"]} for x in e.errors()]}
    return call_real_api(name, params.model_dump())

注意 detail 的设计:不是丢一句 "Bad Request",而是明确告诉模型"status 字段取值必须是 pending/shipped/delivered 之一"。同样一次 400,喂回错误信息的质量决定了模型是"改对"还是"继续编"。

第 2 步:契约测试——Mock 掉真实 API,断言模型"开出的调用单"

测 Function Calling,核心不是测 API 返回什么,而是测模型决定调用什么、传了什么。用 Mock 工具层拦截并记录:

def test_refund_call_contract():
    """用户要求退款,Agent 发起的工具调用必须参数合法、一次到位"""
    calls = []
    with mock_tool_server(record=calls):        # Mock 层拦截,不打真实 API
        agent.run("订单 A1002 三天没物流更新了,帮我退款")

    refund_calls = [c for c in calls if c["name"] == "create_refund"]
    assert len(refund_calls) == 1, f"期望调用 1 次,实际 {len(refund_calls)} 次"

    name, args = refund_calls[0]["name"], refund_calls[0]["args"]
    RefundParams(**args)      # 参数非法直接抛 ValidationError,用例失败
    assert args["order_id"] == "A1002"

第 3 步:异常分支覆盖——工具返回错误时,模型该怎么办

故障的第二个坑就在这里。把工具的错误返回也纳入测试:

def test_tool_error_recovery():
    """工具返回 invalid_params 后,模型应修正参数重试,且总次数不超过 3"""
    calls = []
    with mock_tool_server(record=calls,
                          fail_first=1,                      # 第 1 次调用故意返回参数错误
                          fail_response={
   "error": "invalid_params",
                                         "detail": [{
   "field": "status",
                                                     "msg": "取值须为 pending/shipped/delivered"}]}):
        agent.run("订单 A1002 帮我退款")

    assert len(calls) <= 3, "重试未收敛,存在死循环风险"
    assert calls[-1]["args"]["status"] in {
   "pending", "shipped", "delivered"}

这条用例盯的就是"连环 400":收到结构化错误后,模型第二次调用必须把枚举改对,而不是换个词继续编。

四、沉淀成方法:Function Calling 测试三层

层级 测什么 核心手段
参数契约层 模型会不会编参数 Pydantic/JSON Schema 校验 + Mock 断言参数合法性
异常分支层 工具报错时会不会失控 Mock 返回 4xx/5xx/超时,断言自我纠正、重试有上限
任务完成层 端到端目标达成吗 任务成功率 + 调用次数/成本统计

配套三条工程纪律:一是工具描述必须用严格 JSON Schema,枚举值、必填项、取值范围写死,自然语言描述"大概是个状态"就是埋雷;二是真实 API 永远藏在网关后面,模型的一切调用先过校验,测试环境全走 Mock;三是每次工具调用全量落日志(工具名 + 参数 + 返回),出问题时能像这次一样把完整的"作案过程"回放出来。

五、面试追问,你答得上来吗

  1. 模型为什么会编造参数?怎么降低概率?——答:根因是工具描述模糊 + 模型把参数生成当"续写"。手段按性价比排序:严格 JSON Schema 带枚举和描述、给每个枚举值加注释、开启各平台的 strict 工具调用模式、把校验错误结构化喂回给模型。注意这些都只是"降低概率",所以网关拦截这道硬防线不能省——校验是兜底,提示词优化是减少触发。
  2. 工具调用的异常分支和普通接口的异常测试有什么不同?——答:普通接口异常测试断的是"系统返回正确错误码";工具调用异常测试断的是模型的下一步行为——它是修正参数重试、换个工具、还是向用户求助?所以用例里要有行为序列断言和次数上限,防止"重试"变成"死循环烧钱"。
  3. 工具的接口本身升级了(加了必填字段),你怎么保证 Agent 不崩?——答:工具 schema 进版本管理,schema 变更必须跑两遍回归:契约测试套件验证校验逻辑跟上了,黄金任务集验证模型在新 schema 下还能正确调用——这和 prompt 变更是一个道理:工具 schema、prompt、模型是绑定的三元组,动任何一个都要回归。

下一篇预告:《Agent 半夜陷入死循环,一夜烧掉上万 token——轨迹测试与熔断》

相关文章
人工智能 缓存 前端开发
6134 18
人工智能 JavaScript 开发工具
3025 4
缓存 JavaScript Shell
1375 1
|
12天前
|
存储 弹性计算 缓存
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
本文更新了2026年阿里云全系列云服务器租赁活动报价,所有特惠资源均可前往阿里云活动中心选购,整体覆盖从个人入门到企业级高性能场景的全梯度需求。其中轻量应用服务器主打极致性价比,2核2G峰值200M带宽配置每日10点、15点限时抢购价仅38元/年,2核4G配置379元/年起;高性价比的经济型e实例、通用算力型u2i实例覆盖2核4G至4核32G全档位,适配开发测试与中小型企业业务;搭载英特尔至强6处理器的第九代c9i企业级实例算力较上代提升20%,支撑高并发生产环境,不同实例规格价差清晰,用户可根据自身业务负载与预算灵活选型。
2067 121
阿里云服务器租赁费用:新版租赁收费标准及活动报价参考
|
13天前
|
人工智能 程序员 API
Codex 接入 DeepSeek-V4-Flash:还能补上识图,提供两套方案
Codex 接入 DeepSeek-V4-Flash 怎么配?本文覆盖 CLI 与桌面端,再用 qwen3-vl-flash 补识图,两套方案可直接照做
1662 13
缓存 人工智能 算法
635 1
|
10天前
|
编解码 弹性计算 云计算
MiniMax-H3 视频生成模型 — 一键部署与使用指南
MiniMax-H3是MiniMax开源的33B全模态视频生成模型,支持文生视频、图生视频、参考生视频三种模式,原生输出2K/15秒带立体声音频视频,已原生适配ComfyUI,并可通过阿里云计算巢一键部署。(239字)
|
11天前
|
人工智能 API 开发工具
2026 零基础本地 AI 漫剧完整实操教程(8G 笔记本显卡可用|附可直接复制命令与代码)
本方案提供完全离线、本地运行的漫剧全自动制作流程:RTX3060/4050 8G显卡即可驱动,涵盖Qwen写分镜→ComfyUI统一角色绘图→LTX2.3图生微动画→Qwen3-TTS本地配音→FFmpeg自动合成,全程无水印、免API、不限次。专为低显存优化,解决变脸、闪烁、爆内存三大痛点。(239字)
|
19天前
|
云安全 人工智能 运维
阿里云联动百位企业安全专家,共识Agent防御最佳实践
当Agent成为新员工,你的安全边界在哪里?
1983 10
阿里云联动百位企业安全专家,共识Agent防御最佳实践

热门文章

最新文章