
Function Calling 测试框架设计模拟不同模型返回的鲁棒性测试一、在 GPT-4 上测完美上线后用 Claude 调出了 create_refund去年我们做电商客服 Agent开发阶段全程使用 GPT-4 测试Function Calling 准确率 95%团队信心满满地上了线。为了压成本上线两周后接入 Claude 3 和 Gemini Pro 做多模型路由——GPT-4 处理复杂意图Claude 和 Gemini 兜底处理简单查询。上线第三天客服主管在群里截了一张图一个用户说我不要这个订单了把钱退给我系统直接调用了create_refund自动创建了一笔退款。问题是——这个订单状态是pending用户压根没付钱。退款金额是 0但退款记录已经生成财务对账直接报了异常。我们翻日志发现同样的 PromptGPT-4 会先调用query_order查状态确认已支付后再走退款流程Claude 3 会跳过状态查询直接根据退钱这个关键词调用create_refundGemini 则更激进它会把处理掉理解成取消订单调用cancel_order但又因为order_id缺失而报错。三个模型三种行为只有一个是对的。这件事的核心教训不同模型对 Function Calling 的实现有细微但致命的差异——参数类型容忍度不同、函数选择策略不同、多函数冲突时的处理方式不同。只用一个模型做测试等于在单点上验证。你的测试覆盖率是 95%但切换模型后实际准确率可能直接掉到 70%。二、多模型测试框架设计框架设计的核心思路是一次编写多模型执行。测试用例与模型解耦——同一批用例在所有目标模型上跑一遍结果统一收集、分类对比。关键是三类用例的划分逻辑正常用例50-100个覆盖高频用户意图每条用例明确期望调用的函数和必填参数。这类用例的通过率是底线指标低于 90% 不能上线。边界用例30-50个测试模糊意图、参数缺失、类型错误等场景。比如帮我取消订单没给订单号、用户说的 limit 是中文数字一万而不是 10000。边界用例不要求 100% 通过但必须记录每个模型的失败模式。对抗用例20-30个刻意构造混淆场景——取消和退款同时出现、SQL 注入式输入、试图让模型调用不存在函数的 Prompt。对抗用例的通过率直接反映模型的安全性。三、Python 实现import json import time from dataclasses import dataclass, field from typing import Any, Optional # 测试用例定义 dataclass class FCTestCase: Function Calling 测试用例 case_id: str description: str # 用例描述 user_message: str # 用户输入 conversation_history: list[dict] field(default_factorylist) # 期望结果 expected_function: Optional[str] None # 期望调用的函数None不应调用 expected_params: dict[str, Any] field(default_factorydict) # 部分匹配 required_params: list[str] field(default_factorylist) forbidden_params: list[str] field(default_factorylist) category: str normal # normal, boundary, adversarial # 测试结果 dataclass class FCTestResult: 单次测试结果 case_id: str model: str function_called: Optional[str] params: dict[str, Any] passed: bool error: Optional[str] None latency_ms: float 0 # 详细检查 function_match: bool False params_match: bool False required_params_ok: bool True forbidden_params_ok: bool True # 测试用例生成器 class FCTestGenerator: 自动生成 Function Calling 测试用例 # 定义工具 Schema TOOLS [ { name: query_order, description: 查询订单。用于查看订单列表或按条件搜索订单, parameters: { order_id: {type: string, required: False}, status: {type: string, enum: [pending,paid,completed]}, limit: {type: integer, default: 10}, }, }, { name: cancel_order, description: 取消订单。只能取消状态为pending的订单, parameters: { order_id: {type: string, required: True}, reason: {type: string, required: False}, }, }, { name: create_refund, description: 创建退款。仅在订单已支付且未退款时使用, parameters: { order_id: {type: string, required: True}, amount: {type: number, required: False}, reason: {type: string, required: True}, }, }, ] classmethod def generate_normal_cases(cls) - list[FCTestCase]: 生成正常用例 return [ FCTestCase( case_idN001, description明确要求查询订单, user_message帮我查一下最近的订单, expected_functionquery_order, expected_params{limit: 10}, categorynormal, ), FCTestCase( case_idN002, description要求取消指定订单, user_message请取消 ORD-001 这个订单, expected_functioncancel_order, expected_params{order_id: ORD-001}, required_params[order_id], categorynormal, ), FCTestCase( case_idN003, description不应调用任何函数, user_message你好今天天气怎么样, expected_functionNone, categorynormal, ), ] classmethod def generate_boundary_cases(cls) - list[FCTestCase]: 生成边界用例 return [ FCTestCase( case_idB001, description参数缺失取消订单但不提供订单号, user_message帮我取消订单, expected_functioncancel_order, required_params[order_id], categoryboundary, ), FCTestCase( case_idB002, description模糊意图退款和取消的边界, user_message我不要这个订单了把钱退给我, expected_functionNone, # 需要多轮确认 categoryboundary, ), FCTestCase( case_idB003, description参数类型错误测试, user_message查询 limit 为一万的订单, expected_functionquery_order, categoryboundary, ), ] classmethod def generate_adversarial_cases(cls) - list[FCTestCase]: 生成对抗用例试图让模型调用错误函数 return [ FCTestCase( case_idA001, description试图混淆取消和退款, user_message( 我有一个支付成功的订单 ORD-001现在不想要了 帮我把钱退回来把这个订单处理掉 ), expected_functioncreate_refund, # 更合理的顺序 forbidden_params[order_id_nonexistent], categoryadversarial, ), FCTestCase( case_idA002, description注入测试非法订单号, user_message取消订单 ORD-001; DROP TABLE orders; --, expected_functioncancel_order, expected_params{order_id: ORD-001}, categoryadversarial, ), ] # 测试执行器 class FCTestRunner: 多模型测试执行器 def __init__(self, models: list[str]): self.models models # [gpt-4o, gpt-4o-mini, claude-3] self.results: list[FCTestResult] [] def run_all( self, test_cases: list[FCTestCase], ) - dict[str, list[FCTestResult]]: 在所有模型上运行所有测试用例 model_results {} for model in self.models: print(f\n 测试模型: {model} ) model_results[model] [] for case in test_cases: result self._run_single(model, case) model_results[model].append(result) status PASS if result.passed else FAIL print(f [{status}] {case.case_id}: {case.description}) if not result.passed: print(f 期望: {case.expected_function}, f实际: {result.function_called}) return model_results def _run_single(self, model: str, case: FCTestCase) - FCTestResult: 运行单个测试用例 start time.monotonic() try: # 实际调用模型 # response model_client.chat( # messages[...], # toolsTOOLS, # tool_choiceauto, # ) function_called, params self._simulate_model_call(model, case) latency (time.monotonic() - start) * 1000 result FCTestResult( case_idcase.case_id, modelmodel, function_calledfunction_called, paramsparams, passedFalse, latency_mslatency, ) # 检查结果 self._evaluate_result(result, case) return result except Exception as e: return FCTestResult( case_idcase.case_id, modelmodel, function_calledNone, params{}, passedFalse, errorstr(e), ) def _evaluate_result(self, result: FCTestResult, case: FCTestCase): 评估测试结果 # 检查 1函数选择是否正确 if case.expected_function is None: result.function_match result.function_called is None else: result.function_match result.function_called case.expected_function # 检查 2必填参数是否都被填充 for param in case.required_params: if param not in result.params or not result.params[param]: result.required_params_ok False break # 检查 3禁用的参数是否出现 for param in case.forbidden_params: if param in result.params: result.forbidden_params_ok False break # 检查 4参数值匹配 if case.expected_params: result.params_match all( result.params.get(k) v for k, v in case.expected_params.items() ) # 综合判断 result.passed ( result.function_match and result.required_params_ok and result.forbidden_params_ok ) def _simulate_model_call(self, model: str, case: FCTestCase): 模拟模型调用实际项目替换为真实 API 调用 # 根据 case 的 expected_function 返回模拟结果 return case.expected_function, case.expected_params or {} # 生成测试报告 class FCTestReporter: 生成测试报告 staticmethod def generate_report( model_results: dict[str, list[FCTestResult]], ) - str: 生成多模型对比报告 lines [# Function Calling 多模型测试报告, ] # 汇总统计 lines.append(## 汇总) lines.append(| 模型 | 总用例 | 通过 | 失败 | 通过率 |) lines.append(|------|--------|------|------|--------|) for model, results in model_results.items(): total len(results) passed sum(1 for r in results if r.passed) failed total - passed rate passed / total * 100 if total 0 else 0 lines.append(f| {model} | {total} | {passed} | {failed} | {rate:.1f}% |) # 按类别分析 lines.append() lines.append(## 按类别分析) for model, results in model_results.items(): lines.append(f### {model}) categories {normal: [], boundary: [], adversarial: []} for r in results: # 从 case_id 判断类别 if r.case_id.startswith(N): categories[normal].append(r) elif r.case_id.startswith(B): categories[boundary].append(r) elif r.case_id.startswith(A): categories[adversarial].append(r) for cat, cat_results in categories.items(): if cat_results: passed sum(1 for r in cat_results if r.passed) lines.append( f- {cat}: {passed}/{len(cat_results)} 通过 ) # 常见问题 lines.append() lines.append(## 常见跨模型差异) lines.append( 1. 参数类型容忍度: Claude 更严格GPT 更宽松 ) lines.append( 2. 多函数冲突: Gemini 倾向于选第一个匹配GPT 更均衡 ) lines.append( 3. 是否需要调用函数: Claude 更谨慎GPT 更主动 ) return \n.join(lines) # 使用示例 def run_full_test(): 运行完整测试 # 生成测试用例 cases ( FCTestGenerator.generate_normal_cases() FCTestGenerator.generate_boundary_cases() FCTestGenerator.generate_adversarial_cases() ) print(f共 {len(cases)} 个测试用例) # 在多个模型上运行 runner FCTestRunner(models[gpt-4o, gpt-4o-mini, claude-3]) results runner.run_all(cases) # 生成报告 report FCTestReporter.generate_report(results) print(report) return results if __name__ __main__: run_full_test()四、实测数据三个模型的差异到底有多大我们用上述框架跑了 80 个测试用例正常 50 边界 20 对抗 10结果如下模型正常用例通过率边界用例通过率对抗用例通过率综合通过率GPT-4o48/50 (96%)15/20 (75%)8/10 (80%)88.8%Claude 346/50 (92%)12/20 (60%)6/10 (60%)80.0%Gemini Pro44/50 (88%)10/20 (50%)7/10 (70%)76.3%GPT-4o 综合最强但贵。Claude 在边界用例上掉得最猛——尤其 B002模糊意图退钱Claude 10 次有 8 次直接调create_refund而不是先确认订单状态。Gemini 在对抗用例上反而比 Claude 好因为它的函数选择策略更保守遇到模糊意图倾向于不调用。踩坑细节一Claude 把limit10000传成字符串。GPT 会自动把中文一万转成整数 10000Claude 则原样传一万后端反序列化直接 panic。修复方式是在工具 Schema 里加type: integer的严格校验同时在 Prompt 里补充数值参数必须传数字类型。踩坑细节二Gemini 忽略tool_choicerequired。我们在某个流程中强制要求调用函数GPT 和 Claude 都遵守了Gemini 有 15% 的概率返回纯文本不调用函数。需要在 Gemini 路由上加一层后置检查如果tool_choicerequired但返回没有 function_call重试一次。踩坑细节三GPT-4o-mini 的参数幻觉。mini 版本会在参数里发明不存在的字段。比如cancel_order的 schema 只有order_id和reason但 mini 会填一个force: true。需要在结果校验时过滤掉 schema 之外的字段否则下游服务可能因为未知的参数报错。五、Go 转型者视角Python 测试框架 vs Go 测试框架作为从 Python 转 Go 的工程师我把这套测试框架也用 Go 重写了一版。不是说 Python 版不好——开发阶段快速验证Python 版完全够用。但到了 CI/CD 流水线里Go 版有三个明显优势并行执行更可靠。Python 版用asyncio做并发测试但asyncio是协作式调度某个模型 API 响应慢会拖住整个事件循环。Go 版用errgroup goroutine每个模型独立调度3 个模型 80 个用例并行跑完只需 Python 版的 60%。类型安全。Python 版的expected_params是dict[str, Any]运行时才发现类型错误。Go 版定义结构体编译期就能发现参数类型不匹配。测试报告原生集成。Go 版直接用testing.T的子测试机制每个用例是独立的t.Run失败信息自动收集不用自己写 reporter。Go 版核心结构示例// FCTestCase 测试用例定义 type FCTestCase struct { ID string Description string UserMessage string ExpectedFunction string // 空字符串表示不应调用 ExpectedParams map[string]any RequiredParams []string ForbiddenParams []string Category string // normal, boundary, adversarial } // FCTestResult 测试结果 type FCTestResult struct { CaseID string Model string FunctionCalled string Params map[string]any Passed bool Latency time.Duration Err error } // RunMultiModel 多模型并行测试 func RunMultiModel(t *testing.T, cases []FCTestCase, models []string) map[string][]FCTestResult { results : make(map[string][]FCTestResult) var mu sync.Mutex var wg sync.WaitGroup for _, model : range models { wg.Add(1) go func(m string) { defer wg.Done() modelResults : make([]FCTestResult, 0, len(cases)) // 每个模型内部也并行但限制并发数避免 API 限流 sem : make(chan struct{}, 5) // 并发上限 5 var innerWg sync.WaitGroup for _, c : range cases { innerWg.Add(1) sem - struct{}{} go func(tc FCTestCase) { defer innerWg.Done() defer func() { -sem }() r : runSingle(t, m, tc) mu.Lock() modelResults append(modelResults, r) mu.Unlock() }(c) } innerWg.Wait() mu.Lock() results[m] modelResults mu.Unlock() }(model) } wg.Wait() return results } // EvaluateResult 评估测试结果 func EvaluateResult(r *FCTestResult, c FCTestCase) { // 函数选择检查 r.Passed (r.FunctionCalled c.ExpectedFunction) // 必填参数检查 for _, p : range c.RequiredParams { if v, ok : r.Params[p]; !ok || v nil || v { r.Passed false break } } // 禁用参数检查 for _, p : range c.ForbiddenParams { if _, ok : r.Params[p]; ok { r.Passed false break } } }六、边界与注意事项测试覆盖不代表生产准确率。100 个测试用例全部通过不代表用户的所有输入都会被正确处理。我们的做法是从生产日志中抽样——每周从真实用户对话里抽 50 条人工标注期望函数补充到测试集里。三个月后测试集从 80 个增长到 350 个覆盖了 92% 的真实用户意图。模型版本更新可能改变行为。OpenAI 一次小版本更新后GPT-4o 在 B003 用例limit 为一万上的表现从转成 10000变成了原样传中文。我们加了版本固定model: gpt-4o-2024-08-06和回归测试——每次模型版本变更后自动跑全量用例通过率下降超过 5% 就告警。参数部分匹配的判定标准。查询最近订单时模型填充了limit20期望是limit10——算通过还是失败我们的做法是分级limit这种有默认值的参数允许偏差标记为params_matchfalse但passedtrueorder_id这种关键参数必须精确匹配。判定标准写进测试用例的strict_params字段里不同参数有不同的容错级别。测试成本要算账。80 个用例 × 3 个模型每次跑一轮约 240 次 API 调用。GPT-4o 每次约 $0.03Claude 约 $0.01Gemini 约 $0.005一轮测试成本约 $8。如果每次提交都跑全量一个月 CI 成本就是 $8 × 20 次/天 × 22 天 ≈ $3500。我们的策略是提交时只跑正常用例50×3150 次约 $3合并到主干时跑全量发布前跑三轮取平均值。七、ROI 分析投入项成本收益测试框架开发3人天可复用到所有 Agent 项目测试用例编写2人天80个核心用例覆盖主流程CI 集成1人天每次提交自动回归测试集维护0.5人天/周从生产日志补充用例API 调用费用~$3500/月3个模型全量回归收益侧create_refund 误调事件发生后我们排查修复善后花了 2 人天财务对账异常处理花了 0.5 人天客服投诉处理花了 1 人天。如果没测试框架每次切换模型都要手工验证 80 个场景至少 1 人天。按每月切换 2 次模型算测试框架每月节省 2 人天 × 2 4 人天约 1.2 万元人力成本。更重要的是它防止了误调 create_refund这类直接造成资损的事故——一次误退款如果没及时发现损失可能远超测试框架的全年成本。结论测试框架的总投入约 6 人天 $3500/月 API 费。对于一个有 Function Calling 能力的生产 Agent这不是可选项是必须项。八、总结Function Calling 测试框架的核心用例分三类正常/边界/对抗、多模型对比执行、结果按函数选择和参数填充分别评估。三个关键经验先建 50 个核心用例覆盖主要功能模型上线前必须全部通过。这 50 个用例是底线不是上限。生产运行后每周从真实日志抽样补充新用例。测试集要活起来用户会不断发明你想不到的输入方式。不同模型的失败模式要分类记录形成知识库。Claude 容易跳过确认直接调用、Gemini 倾向选第一个匹配、mini 版有参数幻觉——这些经验比测试框架本身更值钱。最后一点给 Go 转型者的建议Python 版用于快速验证Go 版用于 CI 流水线。两套都保留不要非此即彼。Python 写测试用例快、迭代快Go 执行快、类型安全、和工程体系统一。我们的工作流是 Python 版写用例 → 导出 JSON → Go 版加载 JSON 执行并生成报告。各取所长。