AI测试-04-LLM Function Calling在测试工具里怎么设计输入输出和容错
在 AI 测试平台里,Function Calling 很容易被误解成一个“把自然语言转成工具调用”的轻量能力。
如果只是做 Demo,这么理解问题不大。
但只要它进入测试工具,要求就会立刻变化:
- 输入不能只是一段模糊提示词,而要变成结构化任务上下文
- 输出不能只看模型有没有“说对”,而要看参数是否可执行、可审计、可恢复
- 调用失败不能简单重试,而要先判断失败类型和恢复边界
- 工具执行结果不能只回给模型,还要沉淀进证据链和结果链
也就是说,测试工具里的 Function Calling 不是一个“模型能力展示点”,而是一条工程链路。
这篇文章只聚焦这条链路最容易写坏的几块:
- 输入怎么建模
- 输出怎么约束
- 参数校验放在哪里
- 重试与容错怎么设计
- 哪些失败该交给模型,哪些失败必须由工具层兜底
一、先把边界说清:测试工具里的 Function Calling 不等于聊天助手里的工具调用
聊天助手场景里,模型调用工具通常只需要回答两个问题:
- 要不要调工具
- 调哪个工具
但测试工具里的要求更严格,因为工具调用往往会直接影响:
- 测试环境
- 数据状态
- 执行顺序
- 报告结果
- 证据留存
例如一个 AI 测试代理要做“登录后创建订单并校验回写结果”,背后可能涉及:
- 打开环境
- 获取账号
- 构造测试数据
- 调接口
- 查数据库
- 截图
- 拉日志
- 生成报告片段
这时 Function Calling 如果只按“模型说了一个函数名,程序就照着执行”的方式落地,风险会非常高:
- 参数可能不完整
- 参数可能不合法
- 调用顺序可能错误
- 失败后可能重复写数据
- 成功和失败边界可能不清
所以在测试工具里,更合适的理解是:
Function Calling 是一个带约束的任务编排入口,而不是一个放任模型自由发挥的工具分发器。
二、先设计分层,不要先设计函数表
Function Calling 最常见的一个误区,是先把工具清单铺出来:
open_pageclick_elementcall_apiquery_dbcapture_screenshotassert_text
看起来很完整,但很快就会遇到一个问题:
工具很多,调用很乱,失败很难收。
更稳的做法不是先列函数,而是先拆分层。
1. 任务理解层
这一层负责把外部输入转成可执行上下文,例如:
- 当前任务目标
- 所属环境
- 允许使用的工具范围
- 当前步骤号
- 已有中间结果
- 风险等级
这一层不负责执行,只负责把“模糊需求”收成“结构化任务”。
2. 调用规划层
这一层负责决定:
- 当前这一步是否需要调用工具
- 可以调用哪个工具
- 参数从哪里来
- 这次调用是不是幂等动作
- 是否需要先做前置校验
这层的重点不是让模型自由规划很多步,而是把允许调用的动作收在受控范围内。
3. 参数约束层
这一层负责:
- 参数是否缺失
- 类型是否正确
- 枚举值是否合法
- 是否越权
- 是否命中环境限制
- 是否会破坏已有状态
这一层必须独立于模型输出存在,不能把参数正确性寄托在模型“自己会写对”。
4. 工具执行层
这一层才真正执行:
- HTTP 请求
- UI 操作
- 数据查询
- 文件读写
- 取证动作
它的职责是稳定执行和标准化返回,而不是推理。
5. 结果收口层
这一层负责把执行结果统一成结构化输出,例如:
- 是否成功
- 失败类型
- 重试建议
- 证据引用
- 对后续步骤的影响
如果没有这一层,模型拿到的将是一堆杂乱日志,后续链路会越来越脆。
三、输入设计的关键,不是“提示词写得丰富”,而是“上下文是否足够约束”
测试工具里最常见的失败,不是模型不会调用,而是输入把边界给丢了。
一个可执行的输入,至少要包含 5 类信息。
1. 任务目标
不要只给一句:
帮我执行订单创建测试。
更可执行的输入应该至少带上:
- 业务目标:创建订单并确认支付状态为待支付
- 入口类型:API / UI / 混合
- 成功判定:接口返回 200 且数据库生成订单记录
- 禁止动作:不得调用删除真实订单的接口
2. 环境上下文
例如:
- 环境标识:
test-a - 租户:
mall-regression - 账号池:
buyer-low-risk - 可访问域名或 base URL
- 可调用的数据源范围
如果这层缺失,模型很容易出现:
- 打错环境
- 调错租户
- 用错账号
- 查错库表
3. 当前状态
Function Calling 不能假设“每次调用都从零开始”。
实际链路里经常需要补充:
- 已完成步骤
- 上一步输出
- 当前会话 ID
- 当前页面或资源状态
- 已保留的证据位置
这类上下文决定了后续是继续执行、补执行,还是需要回滚。
4. 工具白名单
不是所有工具都应该暴露给当前任务。
例如一个只做查询验证的任务,工具白名单可以限制为:
call_readonly_apiquery_readonly_dbcapture_screenshotcollect_log_excerpt
如果把写操作工具也一起暴露,模型很可能在恢复阶段多做一步,直接把现场改掉。
5. 输出契约
输入里就应该明确告诉模型:
- 这次工具调用希望返回什么结构
- 哪些字段必填
- 哪些字段失败时也必须返回
- 哪些结果会被后续步骤继续消费
不把这层前置,后面就会在输出解析阶段补锅。
四、输出设计的关键,不是“能解析 JSON”,而是“能不能继续执行”
很多实现把 Function Calling 的输出要求简化成:
- 函数名正确
- 参数能被 JSON 解析
这远远不够。
在测试工具里,真正有价值的输出需要同时满足 4 个条件:
- 可执行
- 可校验
- 可追踪
- 可恢复
一个更稳的工具返回结构,至少应该包含:
1 | { |
这里面真正重要的不是字段多,而是收口清晰:
success给流程判断error_type给失败分类retryable给恢复策略normalized_args给审计和复现evidence给证据链next_action给后续编排
如果只返回:
1 | {"result":"failed"} |
那后面无论是模型、调度器还是报告系统,都无法稳定判断下一步应该做什么。
五、参数约束不能只靠 JSON Schema,还要补业务约束和执行约束
很多工具链在接入 Function Calling 时,会先做一层 schema 校验。
这一步必须做,但只做这一步不够。
更稳的参数约束通常至少分 3 层。
1. 结构约束
这一层回答:
- 字段是否存在
- 类型是否正确
- 枚举值是否在允许范围
- 日期、金额、分页参数格式是否符合要求
这是最基础的一层,适合用 schema 完成。
2. 业务约束
这一层回答:
- 当前任务是否允许这个动作
- 这个账号是否有权限
- 这个资源是否属于当前租户
- 这个 case 是否允许写操作
- 这个参数组合是否会触发危险行为
例如:
- 查询接口允许
order_id - 但退款接口必须校验订单状态是否为已支付
- 数据清理工具只能在沙箱环境调用
这些约束不该交给模型判断,而该交给工具层。
3. 执行约束
这一层回答:
- 当前系统状态是否允许执行
- 依赖资源是否可用
- 是否与前一步冲突
- 是否已超出重试次数
- 是否触发熔断或静默窗口
这层很容易被忽略,但它恰恰决定链路是不是可控。
例如一个 create_user 工具,即使参数正确,也不代表现在就可以执行:
- 当前环境账号池可能已耗尽
- 同租户可能已有重复用户
- 当前压测窗口可能禁止创建新数据
六、最小执行骨架:Function Calling 在测试工具里至少要有这 8 步
如果要把这条链路做成真正可复用能力,最小执行骨架可以收成下面 8 步。
1. 组装任务上下文
把任务目标、环境、账号、证据目录、步骤号和工具白名单组装好。
2. 请求模型生成调用意图
让模型只返回:
- 是否调用工具
- 调哪个工具
- 参数候选值
不要在这一层放任模型直接规划很长的多步链路。
3. 做参数标准化
例如:
- 字段名统一
- 枚举值归一
- 日期格式统一
- 空字符串转空值
这一步可以把模型输出里的轻微格式噪声提前收掉。
4. 做结构校验和业务校验
结构校验不过,直接拒绝执行。
业务校验不过,返回明确失败类型,不进入工具层。
5. 执行工具
执行时必须带:
- request id
- timeout
- trace id
- evidence path
- tool version
否则问题一多就很难定位是哪次调用出了问题。
6. 归一化结果
无论是 HTTP 结果、浏览器动作还是数据库查询,都收成统一结果结构。
7. 判断是否需要重试或降级
不要一失败就让模型重新想一遍。
先根据失败类型判断:
- 参数错误:不重试
- 超时:可重试
- 环境不可用:转降级
- 工具 bug:终止并上报
8. 写回状态和证据
把这次调用的:
- 输入
- 标准化参数
- 工具返回
- 证据引用
- 重试次数
- 最终状态
一起落到任务记录里,后面报告和排障才有基础。
七、失败分类要先做,不然重试链路很容易写歪
Function Calling 最容易失控的地方不是第一次失败,而是失败后的第二次动作。
如果失败没有分类,系统最容易出现三种坏结果:
- 不该重试的错误被重试
- 该重试的错误被直接终止
- 模型为了“完成任务”擅自换了工具或改了参数
更实用的失败分类,通常至少分成下面 5 类。
1. 参数类失败
例如:
- 缺字段
- 类型错误
- 非法枚举值
- 不满足业务前置条件
这类失败通常不应该自动重试。
因为重试不会改变输入本身。
2. 工具可用性失败
例如:
- 工具超时
- 依赖服务 502
- 浏览器实例失联
- 数据库连接池耗尽
这类失败通常可以按策略做有限重试,但要带指数退避和上限。
3. 环境状态失败
例如:
- 测试环境未部署完成
- 账号池为空
- 租户冻结
- 配置尚未生效
这类失败更适合转成等待、切换环境或终止任务,而不是继续压重试。
4. 结果不确定失败
例如:
- 请求超时,但服务端可能已落库
- 页面卡死,但后端订单可能已创建
- 回写接口失败,但主流程可能已完成
这类失败最危险,因为不能直接重复执行写操作。
更稳的做法是先查状态,再决定补执行还是终止。
5. 平台内部失败
例如:
- 结果归一化器崩溃
- 证据写盘失败
- 状态机写入异常
这类失败不该继续交给模型恢复,而应该由平台层直接接管。
八、重试不是多调几次,而是“按失败类型选择恢复动作”
在测试工具里,重试策略至少应该区分四种动作。
1. 同参重试
适合:
- 网络抖动
- 短时超时
- 短暂 5xx
要求:
- 工具幂等
- 调用次数受控
- 每次重试都要记录
2. 补充证据后重试
适合:
- 执行结果不确定
- 调用前后状态不清晰
例如先加一步:
- 查询订单状态
- 查页面是否已跳转
- 拉取接口 trace
确认现场后再决定是否重试。
3. 切换工具或降级执行
适合:
- UI 操作失败但接口可验证
- 主数据源超时但只读副本可查
- 浏览器证据失败但日志仍可收集
这类降级要在平台层预先定义,不应该由模型临场发明。
4. 终止并上报
适合:
- 参数错误
- 越权风险
- 危险写操作
- 连续失败达到上限
终止不是失败设计不够,而是为了保护环境和结果可信度。
九、常见坑不在模型“幻觉”,而在工程链路没有收口
1. 把工具函数设计得过细
例如拆成:
click_login_buttonfill_usernamefill_passwordfill_captcha
这样模型表面上可控,实际上编排会非常脆。
更合适的拆法是围绕业务动作和可验证边界定义工具。
2. 把工具函数设计得过粗
例如一个 complete_order_flow 同时负责:
- 下单
- 支付
- 截图
- 查库
- 回写报告
这会让失败定位和恢复都变得困难。
3. 只校验 schema,不校验业务前置条件
参数类型都对,不代表动作就该执行。
在测试平台里,这类错误最容易直接污染环境。
4. 失败后直接把原始错误丢回模型
如果原始错误是长日志、大段 HTML 或异常堆栈,模型很容易抓不到重点。
更稳的做法是先归一化,再回传关键摘要和证据引用。
5. 没有区分“可重试失败”和“不可重试失败”
最后就会演变成所有失败都重复 3 次,看起来很稳,实际只是在放大破坏。
6. 没有把标准化参数落盘
问题复盘时只能看到模型原始输出,看不到真正执行时的参数版本,排查成本会非常高。
7. 证据链和调用链脱节
工具执行成功了,但:
- 日志找不到
- 截图对不上
- 报告里没有 request id
- 调用和证据没有统一编号
这会让 Function Calling 变成一个“执行过,但说不清”的黑箱。
十、真实案例:一次订单校验任务为什么在“自动恢复”里把数据越写越乱
场景
一个 AI 测试工具需要完成下面这条链路:
- 调用创建订单接口
- 查询订单详情
- 校验数据库中的订单状态
- 输出报告结果
工具层已经接了 Function Calling,并暴露了:
create_orderquery_order_apiquery_order_dbattach_evidence
执行
模型先调用 create_order,返回超时。
平台没有立刻分类失败,而是简单按“失败重试”策略再次调用 create_order。
第二次调用成功后,模型继续调用查询工具,并发现数据库里出现了两条相似订单记录。
现象
最终报告里出现了几个看似矛盾的结果:
- 接口第一次调用超时
- 第二次调用成功
- 数据库中有两条订单
- 页面侧只展示了一条
- 回归结果被判成“状态不一致”
从表面看,像是业务接口幂等有问题。
但继续排查后发现,第一次调用虽然客户端超时,服务端其实已经写入成功。
排查
后续把证据链重新串起来时,暴露出了 4 个设计问题:
create_order被错误标记为“可直接重试”- 失败分类里没有“结果不确定”这一类
- 平台没有在重试前先执行状态确认动作
- 工具返回结构里没有明确的
retryable和next_action
也就是说,真正的问题不在接口本身,而在 Function Calling 这一层把恢复动作设计错了。
修复
后面做了 4 个收口动作:
- 把
create_order标记成“写操作且非盲重试工具” - 新增
UNCERTAIN_RESULT失败类型 - 规定写操作超时后必须先调
query_order_api或query_order_db做状态确认 - 在统一结果结构里强制要求返回
retryable、next_action和normalized_args
改完后,同类问题的恢复策略就从“超时后重复创建”变成了:
- 写操作超时
- 进入结果不确定分支
- 先做状态确认
- 已创建则补证据并继续
- 未创建才允许有限重试
这样链路才真正可控。
十一、把 Function Calling 接进测试工具时,真正要优先做的不是模型调优,而是调用治理
如果要给这类能力排优先级,更值得优先补的是下面几项:
- 明确工具分层和边界
- 定义统一输入上下文
- 定义统一输出结构
- 做三层参数约束
- 建失败分类和恢复动作表
- 把证据链和调用链绑定
只有这些基础收稳之后,模型能力提升才会真正转化成平台能力提升。
否则最常见的结果是:
- Demo 很聪明
- 现场很混乱
- 报告说不清
- 重试越做越危险
Function Calling 在测试工具里真正的价值,不在于“模型会不会调函数”,而在于:
模型发出的每一次工具调用,能不能被平台收成一次可执行、可校验、可恢复、可追踪的受控动作。