模型编造 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——轨迹测试与熔断》

相关文章
|
2月前
|
人工智能 监控 测试技术
大促流量一来,大模型服务就卡——推理性能压测
本文揭秘大模型性能测试核心差异:告别传统QPS/RT,聚焦TTFT(首字延迟)、TPOT(逐字间隔)等流式指标。通过真实大促事故(首字延迟从0.8秒飙至15秒),剖析容量规划缺失、KV cache打满、无降级等痛点,并给出阶梯压测、拐点识别、混长文本等实战方法,助你应对AI面试高频题。
|
2月前
|
人工智能 监控 测试技术
Agent 半夜陷入死循环,一夜烧掉上万 token——轨迹测试与熔断
AI测试开发高频题:测Agent≠测接口,核心是验证“决策链是否失控”。本文以深夜烧万元的真实事故切入,详解如何通过轨迹记录、循环检测与三重熔断(步数/Token/时间)保障Agent可靠性,并将“不失控”写入自动化测试用例。
|
4月前
|
人工智能 前端开发 Java
让AI Coding 效率翻倍!AGENTS.md 标准化文档详解:兼容Claude/Cursor/Codex全方案
在多AI编程工具并行使用的团队开发场景中,长期存在一个普遍痛点:不同AI客户端各自使用独立规则配置文件,Claude依赖CLAUDE.md、Cursor使用.cursorrules、Codex采用AGENT.md,团队需要同步多份内容高度重合的文档,维护成本成倍上涨。同时AI无法完整理解项目整体架构,前后端代码上下文割裂、私有组件不识别、编码规范不清晰、无法自主构建自测等问题,导致AI生成代码返工率极高,大幅拉低开发效率。
976 0
|
Linux API 调度
深入了解DPDK:如何优化网络包处理性能(上)
深入了解DPDK:如何优化网络包处理性能
深入了解DPDK:如何优化网络包处理性能(上)
SQL 人工智能 DataWorks
306 0
|
9月前
|
存储 缓存 安全
我们来说一下无锁队列 Disruptor 的原理
我是小假 期待与你的下一次相遇 ~
518 7
|
3月前
|
人工智能 JSON 监控
AI Agent标准化工作流实战:10套开箱即用完整模板全解析
多数使用者容易混淆AI Agent与普通聊天机器人:聊天机器人仅完成单次问答交互,而AI Agent可以自主串联读取信息、数据核对逻辑、多维度决策、内容起草、系统更新一整条完整业务链路,仅高风险节点暂停等待人工确认。标准化工作流具备可复用、可审计、低幻觉三大优势,核心思路为先梳理完整业务流程,再配套结构化指令,而非临时编写单次Prompt。本文先拆解Agent工作流五大核心组成模块,再分享10套覆盖市场、财务、销售、研发、运营的开箱即用标准化模板,所有模板自带执行步骤、决策规则与输出规范,可直接落地部署。
464 1
|
6月前
|
消息中间件 存储 Java
【Kafka核心】分区副本、ISR机制、消息存储机制、segment文件、稀疏索引、顺序写
本资料系统梳理Kafka核心机制,涵盖分区副本、ISR同步、Segment分段、稀疏索引、顺序写与PageCache等六大支柱,深入解析LEO/HW、Leader Epoch、零拷贝等关键原理,揭示高吞吐、低延迟、高可用与强一致性的底层实现逻辑,兼具理论深度与生产实践指导价值。
|
6月前
|
消息中间件 存储 缓存
【Kafka核心】Kafka高性能的四大核心支柱:零拷贝、批量发送、页缓存、压缩
本文系统解析Kafka高性能四大支柱:页缓存(规避JVM GC,实现内存级读写)、零拷贝(sendfile/mmap减少CPU拷贝与上下文切换)、批量发送(全链路聚合降低IO次数)及端到端压缩(批次级LZ4/ZSTD降带宽)。四者深度协同,构建高吞吐、低延迟、低成本的完整优化体系。
|
5月前
|
SQL 域名解析 运维
线上服务变慢,到底慢在哪?一份给后端和运维的网络分层排障手册
凌晨 2 点告警炸了:服务 P99 从 200ms 飙到 8s,但 CPU、内存、慢 SQL 全部正常——问题往往藏在你看不到的网络链路里。本文沉淀一套从 DNS、链路、TCP/TLS、HTTP 到 IP 信誉的「五层分层排障方法论」,配合命令行实操和真实案例,给后端、运维、SRE 同学一份可以直接抄进 Runbook 的网络排障手册。
648 3

热门文章

最新文章