
LLM Agent 越来越擅长调用外部工具但怎么评估它“真的会用工具”成了很多团队头疼的问题。手工写测试用例又慢又覆盖不全用真实用户日志又涉及数据合规和场景碎片化。后来我们在做 Agent 评估平台时接触到 Agent Seer 的思路——从工具规格本身直接合成评测场景这彻底改变了我们构造评估数据的方式。这篇文章会从问题背景、核心概念、原理拆解、代码实现到工程落地完整介绍如何基于工具规格理解来合成评测场景。无论你是做 LLM 应用开发、Agent 框架设计还是负责模型评估体系建设本文都值得一看。1. 背景与核心概念1.1 为什么需要 Agent Seer先看一个实际场景。你接入了某个 Agent 框架它支持查询天气、定闹钟、查航班、发邮件等工具。你要验证 Agent 是否能正确调用这些工具于是开始手写测试用例用例 1用户说“北京明天天气怎么样” 期望调用 get_weather 工具传入 location北京date明天写了几条之后你会发现两个问题写不全每个工具参数组合都很多天气工具有城市、日期、温度单位排列组合下来几十种情况人工写不过来。跟不上工具接口一变所有测试用例全部要改维护成本极高。测不准很多反向用例、边界用例、多工具协作场景人工根本想不到。Agent Seer 的核心思路正是让评估系统自己去“读”工具规格说明然后自动生成评测场景。它把评测数据生产的模式从“人工编写”变成“基于工具规格自动合成”。1.2 什么是工具规格“工具规格”是 Agent 能够理解和调用工具的全部必要信息。典型的工具规格包含规格项说明示例工具名称唯一标识get_weather功能描述说明工具能力根据城市和日期查询天气参数列表每个参数名、类型、必填性city: string, required返回值说明描述返回结构weather_data调用约束使用限制仅支持中国城市依赖关系是否依赖其他工具需要先获取城市ID在 OpenAI Function Calling 体系中工具规格就是 JSON Schema 风格的函数描述在 ReAct 框架中工具规格就是工具的 description 字段。1.3 评测场景是什么评测场景是“一条完整的测试输入”。它通常包含用户原始输入模拟真实用户表达。目标工具调用期望 Agent 调用哪些工具参数是什么。执行环境约束比如当前系统时间、用户上下文。预期结果回复是否包含关键信息工具调用是否符合预期。传统评测场景由人工编写Agent Seer 则使用 LLM 或多策略生成器从工具规格中自动合成大量场景。2. 环境准备与版本说明2.1 运行环境建议本文示例代码以 Python 为主推荐环境如下操作系统Linux / macOS / Windows 均可 Python 版本3.10 依赖包openai、pydantic、pyyaml、pandas LLM 接口支持 OpenAI 风格接口的模型服务版本需要根据你的项目实际情况调整。示例代码中的模型调用部分以 OpenAI 风格接口为例实际使用时请替换为你自己的 API 地址和密钥。2.2 安装依赖pip install openai pydantic pyyaml pandas2.3 示例项目结构agent_seer_demo/ ├── spec/ │ └── tools.json ├── seer/ │ ├── __init__.py │ ├── spec_parser.py │ ├── scene_generator.py │ └── executor.py ├── output/ │ └── scenes.json └── main.py3. 核心原理拆解3.1 工具规格理解从文本到结构化语义第一步系统需要解析工具规格。这里的“理解”不只是读取字段还包括意图归纳这个工具到底解决什么问题。参数依赖分析哪些参数是核心参数哪些参数可以从上下文中推导。边界提取什么情况下工具不可用参数有什么限制。关系发现这个工具是否依赖于其他工具的执行结果。下面是一个工具规格的 JSON 示例{ name: get_weather, description: 根据城市和日期查询天气信息支持实时天气和未来7天预报, parameters: { type: object, properties: { city: { type: string, description: 城市中文名例如北京、上海, required: true }, date: { type: string, description: 日期格式 YYYY-MM-DD默认今天, required: false } } }, return_schema: { temperature: float, condition: string, humidity: float }, constraints: [ 仅支持中国主要城市, date 不能早于今天 ] }解析阶段要做的工作是把上述 JSON 转换成统一的结构化对象便于后续生成场景时使用。3.2 评测场景合成策略Agent Seer 的“合成”不是随机拼词。常用的策略有基于规则模板从工具规格中提取参数使用预定义模板拼接场景。基于 LLM 生成把工具规格作为上下文输入给 LLM让它生成多样化、覆盖边界的场景。基于变体改写已有少量种子场景通过改写实体、语气、上下文生成更多变体。基于对话轨迹模拟模拟多轮对话让 Agent 在连续交互中动态决定是否调用工具。在实际落地中推荐规则模板和 LLM 生成结合。规则负责稳定性LLM 负责多样性。3.3 场景质量评估生成之后不能直接使用。需要评估可执行性这个场景是否能在评测环境中运行。一致性期望的工具调用是否真的与用户请求匹配。覆盖度是否覆盖了所有工具、所有参数组合。难度分布场景难度是否合理分层。Agent Seer 会为每个场景标注质量指标不合格的场景会被过滤或重新生成。4. 完整实战从工具规格合成评测场景这一节我们会实现一个简化版的 Agent Seer命名为 mini_seer重点演示工具规格理解、场景合成、评测执行三个环节。4.1 定义工具规格首先创建一个工具规格文件。// 文件路径spec/tools.json { tools: [ { name: get_weather, description: 查询指定城市在某一天的天气情况包含温度、天气现象、湿度, parameters: { city: { type: string, description: 城市中文名, required: true }, date: { type: string, description: 日期格式 YYYY-MM-DD, required: false } }, constraints: [ date 不能早于今天 ] }, { name: create_reminder, description: 为用户创建一条提醒事项到时间后系统会通知用户, parameters: { content: { type: string, description: 提醒内容, required: true }, time: { type: string, description: 提醒时间格式 YYYY-MM-DD HH:mm, required: true } }, constraints: [ time 必须晚于当前时间 ] } ] }4.2 编写工具规格解析器解析器负责把原始的 JSON 转成可用的 Python 对象并提取关键信息。# 文件路径seer/spec_parser.py import json from typing import Dict, List, Any class ToolSpec: def __init__(self, name: str, description: str, parameters: Dict, constraints: List[str]): self.name name self.description description self.parameters parameters self.constraints constraints def get_required_params(self) - List[str]: 获取必填参数列表 required [] props self.parameters.get(properties, {}) required_list self.parameters.get(required, []) for param_name, param_info in props.items(): if param_name in required_list or param_info.get(required, False): required.append(param_name) return required def get_optional_params(self) - List[str]: 获取可选参数列表 props self.parameters.get(properties, {}) required set(self.get_required_params()) return [p for p in props.keys() if p not in required] def __repr__(self): return fToolSpec {self.name} params{list(self.parameters.get(properties, {}).keys())} def load_tool_specs(spec_path: str) - List[ToolSpec]: 从 JSON 文件加载工具规格 with open(spec_path, r, encodingutf-8) as f: data json.load(f) specs [] for tool in data[tools]: specs.append( ToolSpec( nametool[name], descriptiontool[description], parameterstool[parameters], constraintstool.get(constraints, []), ) ) return specs解析器把每个工具规格变成一个 ToolSpec 对象后面生成器和执行器都基于这些对象工作。4.3 编写场景合成器合成器是本例的核心。我们提供两种策略规则模板生成和 LLM 生成。规则模板生成规则模板从参数约束出发对每个工具生成基础场景。# 文件路径seer/scene_generator.py import json import random from datetime import datetime, timedelta from typing import List, Dict from .spec_parser import ToolSpec def _random_date(start: datetime, end: datetime) - str: 生成 start 到 end 之间的随机日期字符串 delta (end - start).days random_day start timedelta(daysrandom.randint(0, max(delta, 0))) return random_day.strftime(%Y-%m-%d) def generate_scenes_by_template(specs: List[ToolSpec]) - List[Dict]: 基于规则模板生成评测场景 scenes [] today datetime.now() tomorrow today timedelta(days1) for spec in specs: required_params spec.get_required_params() optional_params spec.get_optional_params() # 为每个必填参数构造一个“缺参”场景 for param in required_params: scene { scene_id: f{spec.name}_missing_{param}, tool: spec.name, type: missing_required_param, user_input: f请帮我使用 {spec.name} 完成一个操作但是不要提供 {param} 参数, expected: { should_call_tool: True, tool_name: spec.name, required_params: required_params, missing_param: param }, difficulty: easy } scenes.append(scene) # 构造一个正常调用场景 if get_weather in spec.name: city 北京 date _random_date(today, tomorrow timedelta(days6)) user_input f帮我查一下{city}在{date}的天气情况 expected { should_call_tool: True, tool_name: spec.name, params: {city: city, date: date} } scenes.append({ scene_id: f{spec.name}_normal, tool: spec.name, type: normal_call, user_input: user_input, expected: expected, difficulty: easy }) return scenes规则模板生成的特点是可控制、可解释适合作为基线场景集合。LLM 生成LLM 生成则侧重多样性和边界覆盖。# 文件路径seer/scene_generator.py接续 import os from openai import OpenAI client OpenAI( api_keyos.getenv(OPENAI_API_KEY, your-api-key), base_urlos.getenv(OPENAI_BASE_URL, https://api.openai.com/v1) ) def generate_scenes_by_llm(specs: List[ToolSpec], num_scenes: int 5) - List[Dict]: 使用 LLM 根据工具规格生成评测场景 spec_text for spec in specs: spec_text f工具名: {spec.name}\n spec_text f描述: {spec.description}\n spec_text f参数: {json.dumps(spec.parameters, ensure_asciiFalse)}\n spec_text f约束: {spec.constraints}\n spec_text ---\n prompt f 你是一个评测场景生成器。请根据以下工具规格生成 {num_scenes} 个用户评测场景。 工具规格 {spec_text} 要求 1. 每条场景包含 user_input用户输入和 expected_call期望调用的工具及参数。 2. user_input 要贴近真实用户表达不要只复述工具名。 3. 尽量覆盖边界情况、模糊表达、多轮上下文。 4. 以 JSON 数组格式输出。 输出示例 [ {{ user_input: 周末想出门帮我看看周六北京天气怎么样, expected_call: {{ tool_name: get_weather, params: {{city: 北京, date: 本周六日期}} }} }} ] response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个严谨的评测数据工程师。}, {role: user, content: prompt} ], temperature0.8 ) content response.choices[0].message.content # 清理可能的 markdown 代码块标记 content content.strip() if content.startswith(): content content.split(\n, 1)[1] content content.rsplit(, 1)[0] try: llm_scenes json.loads(content) except json.JSONDecodeError: print([WARN] LLM 输出不是合法 JSON返回空列表) return [] scenes [] for i, item in enumerate(llm_scenes): scenes.append({ scene_id: fllm_{i}, tool: item.get(expected_call, {}).get(tool_name, unknown), type: llm_generated, user_input: item.get(user_input, ), expected: item.get(expected_call, {}), difficulty: medium }) return scenesLLM 生成场景时最好在 prompt 中给定输出格式和示例显著提高 JSON 解析成功率。4.4 编写评测执行器评测执行器负责把场景喂给待测 Agent并比对实际结果和期望结果。# 文件路径seer/executor.py import json from typing import Dict, List class AgentEvaluator: def __init__(self, agent_func): agent_func: 一个可调用对象接收 user_input 字符串 返回包含 tool_calls 和 response 的字典。 这里定义好接口方便接入任何 Agent 框架。 self.agent_func agent_func def run_scene(self, scene: Dict) - Dict: user_input scene[user_input] expected scene[expected] # 调用待测 Agent try: result self.agent_func(user_input) except Exception as e: return { scene_id: scene[scene_id], passed: False, error: str(e) } actual_tool_calls result.get(tool_calls, []) expected_tool expected.get(tool_name) expected_params expected.get(params, {}) # 检查工具名是否匹配 tool_match any(tc.get(name) expected_tool for tc in actual_tool_calls) # 检查参数是否匹配简化比较只检查期望参数是否都存在 param_match False for tc in actual_tool_calls: if tc.get(name) expected_tool: actual_args tc.get(arguments, {}) if all(k in actual_args and str(actual_args[k]) str(v) for k, v in expected_params.items()): param_match True break passed tool_match and param_match return { scene_id: scene[scene_id], passed: passed, tool_match: tool_match, param_match: param_match, expectation: expected, actual: result } def run_batch(self, scenes: List[Dict]) - List[Dict]: return [self.run_scene(s) for s in scenes] def report(results: List[Dict]) - Dict: total len(results) passed sum(1 for r in results if r.get(passed)) accuracy passed / total if total 0 else 0 return { total: total, passed: passed, failed: total - passed, accuracy: round(accuracy, 4) }4.5 组装主流程# 文件路径main.py import json from seer.spec_parser import load_tool_specs from seer.scene_generator import generate_scenes_by_template, generate_scenes_by_llm from seer.executor import AgentEvaluator, report # 1. 加载工具规格 specs load_tool_specs(spec/tools.json) print(f已加载 {len(specs)} 个工具规格) for spec in specs: print(f - {spec}) # 2. 生成评测场景 template_scenes generate_scenes_by_template(specs) print(f规则模板生成场景数: {len(template_scenes)}) # LLM 生成需要 API可自行决定是否开启 # llm_scenes generate_scenes_by_llm(specs, num_scenes10) # print(fLLM 生成场景数: {len(llm_scenes)}) all_scenes template_scenes # 实际使用时可以合并 llm_scenes # 3. 保存场景 with open(output/scenes.json, w, encodingutf-8) as f: json.dump(all_scenes, f, ensure_asciiFalse, indent2) print(场景已保存到 output/scenes.json) # 4. 模拟一个 Agent 执行器测试用 def mock_agent(user_input): 模拟 Agent 总是调用 get_weather 工具 return { tool_calls: [ { name: get_weather, arguments: {city: 北京, date: 2025-01-10} } ], response: 北京2025-01-10天气晴温度-3°C } # 5. 评估 evaluator AgentEvaluator(mock_agent) results evaluator.run_batch(all_scenes) metrics report(results) print(评估结果:) print(json.dumps(metrics, ensure_asciiFalse, indent2))这里 mock_agent 只是一个占位实现。实际项目中你只需要让 agent_func 返回符合约定的结构即可无缝接入你的 Agent 系统。4.6 运行与验证cd agent_seer_demo python main.py预期输出大致如下已加载 2 个工具规格 - ToolSpec get_weather params[city, date] - ToolSpec create_reminder params[content, time] 规则模板生成场景数: 5 场景已保存到 output/scenes.json 评估结果: { total: 5, passed: 1, failed: 4, accuracy: 0.2 }由于 mock_agent 总是调用 get_weather 无法处理 create_reminder 场景所以准确率不高这符合预期。真实使用中你应该换成一个可运行的 Agent。5. 工具规格理解的进阶方向5.1 多级场景谱系设计一个高质量的评测体系场景不应是平铺的而是分层的。层级场景类型说明L1简单单工具调用一句话触发一个工具L2参数解析同一工具不同参数组合L3多工具协作一个请求需要多个工具结果L4上下文推理需要结合历史对话推断工具参数L5边界与异常工具不可用、参数非法、用户反悔Agent Seer 在合成场景时应该显式标注场景层级。这样后续对 Agent 的能力分析可以落到“到底弱在 L2 还是 L4”。5.2 工具依赖图真实业务中工具之间存在依赖。例如查询订单状态需要先调用 get_order get_order 需要先调用 get_user_id_by_phone工具规格理解模块应该自动构建这种依赖图并在合成场景时生成“需要多工具按顺序调用”的评测场景。# 示例工具依赖关系描述 { name: cancel_order, depends_on: [get_order, get_user_id_by_phone], description: 取消订单需要先获取订单信息再取消 }依赖图的价值在于它让 Agent Seer 不只测“单个工具是否会被调用”还能测“Agent 是否按正确顺序调用多个工具”。5.3 与合成数据质量评估闭环合成场景本身也是数据需要有质量反馈。简单做法是让 Agent 执行完场景后把结果回灌到场景生成器对“长期失败的场景”做保留分析对“所有 Agent 都能通过的场景”下调难度避免评测数据集退化。6. 评测指标设计场景合成出来后评测指标也不能只看一个“准确率”。推荐从下面几个维度看。6.1 工具调用正确率工具调用正确率 工具名和参数全部正确的场景数 / 总场景数这是最核心的指标。参数比较时需要注意不同工具的参数类型不同日期、金额、手机号这类格式化的参数需要做归一化。6.2 过调用率Agent 在不需要调用工具的时候错误地调用了工具属于“过调用”。例如用户只是想闲聊“今天天气怎么样”这句话里的“天气”只是表达情绪而不是真的想查天气Agent 却调了 get_weather。过调用率 非预期工具调用场景数 / 不应调用工具的场景总数这个指标很容易被忽略但在实际业务中非常重要因为过调用会带来额外 API 成本和错误动作。6.3 参数幻觉率Agent 在调用工具时生成了规则中不存在的参数或编造了工具没有的能力属于参数幻觉。例如 get_weather 工具没有 wind_speed 参数但 Agent 输出了 wind_speed3这就是参数幻觉。参数幻觉率 包含幻觉参数的调用次数 / 工具调用总次数6.4 场景覆盖度场景覆盖度 已合成场景覆盖的工具行为点 / 工具规格中所有行为点行为点可以理解为“必填参数缺失”“可选参数缺省”“边界约束触发”等最小测试单元。7. 常见问题与排查思路7.1 场景生成结果不可执行问题现象常见原因解决思路LLM 生成的场景参数格式错误LLM 生成时没有严格遵循 JSON Schema在 prompt 中嵌入参数 Schema并增加少量校验生成的 user_input 和期望调用不匹配LLM 生成自由度太高加入“期望调用必须与用户输入语义严格对齐”约束场景时间参数是过去时间Prompt 没有给出当前时间生成时注入当前日期并在 prompt 中限制时间范围7.2 LLM 输出解析失败错误信息: JSONDecodeError: Expecting value排查顺序打印 LLM 原始返回内容确认是否包含 markdown 代码块。清理 json 标记。若包含中文引号替换为英文引号。若仍失败改用 Pydantic 约束输出格式或者使用支持结构化输出的模型。7.3 生成的评测场景太难或太简单现象accuracy 长时间接近 100% 或长期为 0说明场景难度分布出了问题。解决方案是为场景增加 difficulty 字段。统计每个难度级别的 pass 率。对 pass 率过高的场景增加多工具协作或上下文推理条件。对 pass 率过低的场景拆分成更小的子场景。7.4 Agent 评测结果不稳定同一场景同一模型多次执行结果不一致通常是温度参数造成的。建议评测时把模型 temperature 设为 0。每个场景执行多次取多数投票结果。对随机性敏感的 Agent记录单次输出以便复盘。8. 最佳实践与工程建议8.1 从规则模板起步再引入 LLM 生成不要一开始就让 LLM 全量生成评测场景。先做规则模板保证基线场景的稳定性和覆盖率再逐步加入 LLM 生成增强多样性。8.2 场景必须带可解释的预期合成出来的场景必须有明确的 expected 结构最好标注“为什么期望这样调用”。没有可解释预期的场景在评测失败时无法定位是 Agent 的问题还是场景本身的问题。8.3 建立场景版本管理工具规格会演进评测场景也会变化。建议使用独立的 git 仓库管理评测场景每个场景带元数据创建时间。关联的工具版本。生成策略规则/LLM/人工。历史通过率。8.4 注意合成数据的污染风险如果 LLM 生成场景时模型本身和待测 Agent 是同一个模型就会存在“自己考自己”的污染风险。建议场景生成使用更高级或不同的模型。生成后的场景经过人工抽检。定期从评测集中移除与线上真实分布相差过大的合成场景。8.5 提前定义安全边界在评测环境中工具调用往往是模拟的。但如果你把同样的评测流程接到生产环境必须注意评测工具调用要加沙箱。涉及真实发送短信、邮件、支付的操作一律使用 mock。严格控制评测环境的权限不要使用生产数据库。9. 总结与下一步方向Agent Seer 的核心贡献在于改变了评测数据的生产方式不再依赖人工逐条编写测试用例而是让系统从工具规格出发自动合成结构化、可追踪、可度量的评测场景。配合规则模板和 LLM 生成策略可以在覆盖率、多样性和可维护性之间取得平衡。如果你准备在团队内落地这套方法我最想提醒的是不要一次性追求大规模生成。先把 3 到 5 个核心工具接入用规则模板把基线场景跑通再逐步加入 LLM 生成、场景分级和依赖图这样整个体系的稳定性会好很多。下一步可以往几个方向继续做把工具规格理解从“JSON Schema”升级为带语义的“工具本体”。加入多轮对话场景自动合成覆盖 Agent 的上下文记忆能力。引入失败场景聚类分析自动定位 Agent 的能力短板。将评测场景结果回灌到模型微调流程形成数据闭环。如果这篇文章对你有帮助可以动手把示例中的 mini_seer 跑起来替换成你自己的工具规格列表体验一下从工具规格到评测场景的完整链路。遇到任何问题欢迎在评论区留言讨论。