拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Agents on Rails:打造可复现的LLM Agent基准评测体系

做 LLM Agent 功能时最难受的通常不是 Agent 调不通而是不知道当前的改动到底有没有让 Agent 变好。今天聊的项目可以叫 “Agents on Rails: The LLM Benchmark Project”它不是一个趁手的对话客户端而是一套把 Agent 放到固定轨道上接受检验的基准评测工程。这里的 Rails 不是指某个 Web 框架而是借用了“轨道”的含义——任务集固定、工具边界固定、判定标准固定、报告口径固定让每次升级、每次改 prompt、每次换模型都能获得可比较的结果。很多团队第一个版本的 Agent 能跑通 Demo但进入迭代期后完全失控。场景一多了就分不清某个 prompt 改动是提升了工具调用准确率还是只是把失败从任务 A 转移到了任务 B。场景稍微复杂一点又很难判断 Agent 卡住到底是 LLM 理解问题、工具 schema 问题还是任务本身定义不清。要解决这一串问题最值得投入的其实不是调 prompt而是先搭出一套项目级的 LLM Agent Benchmark 机制。这篇文章会把 “Agents on Rails” 拆成一个可以动手实现的工程方案先说明为什么 Agent 评测需要“轨道式”约束再给出环境准备和项目骨架然后写一个最小可运行的评测执行器设计任务集、判定规则和报告指标最后落到常见报错排查和团队落地清单。整套内容走的是“概念 - 骨架 - 最小闭环 - 验证 - 排错 - 扩展”的顺序读者按顺序读完就能在自己的项目里搭出第一版 Agent 评测体系。1. 先想清楚为什么 LLM Agent 结果不能用“抽几个对话”来评判1.1 从“模型问答评测”到“Agent 行为评测”传统的大模型评测往往是一问一答给输入模型给输出然后用规则或人工判断输出是否正确。这种评测对闲聊、单轮知识问答、文本分类这类任务够用因为结果只有一个短输出错和对都容易界定。Agent 场景完全不同。用户输入一个目标后模型可能要拆计划、选择工具、传参数、读取工具返回结果、再决定下一步动作。一次完整任务里模型会发起多次 LLM 请求中间夹杂若干个工具调用。真正需要评估的不只是最后一句话而是整条“行为轨迹”模型是否选对了工具传给工具的 JSON 参数是否正确工具返回异常后模型能不能恢复或换一条路径模型是否在没有必要时反复调用工具最终答案是否真的基于工具返回结果而不是凭记忆编造。这些行为没法靠“抽几个对话人工看看”来稳定评价。抽测最大的问题是不可复现模型有随机性Agent 又有循环和分支即使同一个 prompt、同一个任务今天跑和明天跑的结果可能都不一致。没有固定数据集、固定工具、固定判定标准任何改动都只能是感觉。1.2 “on Rails”真正约束的是评测链路不是 Agent 行为“Agents on Rails”这个名称的关键在于“rails”也就是轨道。火车可以自由决定运什么货、走哪条线路但任何一班车都必须在铁轨约束下运行不能横冲直撞。Agent 评测也需要这种约束。注意这里的约束不是限制 Agent “只能怎么做”而是约束评测链路本身每个被测 Agent 使用同一套配置加载方式每个任务只在一个明确的场景下运行工具列表通过白名单传给模型多一个工具都不给执行器按固定的调用循环推进请求 - 解析工具调用 - 执行 - 回填结果 - 再次请求成功和失败不是靠肉眼判断而是由预先定义的 checks 完成每次运行都保存完整轨迹、token 消耗、耗时和中间结果。轨道式评测的收益是长期可见的。场景切换时你不需要重新写一堆临时脚本模型升级后只需要把 agent_spec 里的 model 字段换掉然后重新跑任务集。最理想的状态是一次改动提交之前先跑一遍基准任务集分数下降就阻止合并分数上升才允许发布。1.3 一个 Benchmark 项目的四块积木为了后面代码不散先把 Benchmark 项目的组成部分固定下来通常包括四块。第一块是“任务集 Task Set”。任务集描述 Agent 要完成什么目标、有哪些可用工具、怎么判定成功。任务集必须版本化改任务和改评测代码要分开管理否则回归对比时不知道分数变化是代码改动导致的还是任务难度变了。第二块是“被测对象 Agent Under Test”。被测对象不能只写一个模型名。它应该包含模型标识、system prompt、工具白名单、温度、最大步数、API 地址等完整配置。只有把这些参数配置化才能做对照实验。第三块是“执行器 Runner”。Runner 不看任务语义只按固定协议推进把用户输入发给模型如果模型请求调用工具就查找工具注册表校验 JSON 参数执行工具函数把工具结果作为新的消息回填给模型重复上述过程直到模型给出最终答案或达到最大步数。第四块是“判定与报告 Judge and Report”。Runner 只负责跑Judge 负责判断任务是否成功Report 负责汇总成功率、步骤数、耗时等指标并保存中间轨迹。这四个部分的关系很清晰Task Set 定义“测什么”Agent Under Test 定义“测谁”Runner 定义“怎么跑”Judge and Report 定义“怎么判、怎么报”。下面就从环境开始把每一块落到代码里。2. 环境准备与项目骨架2.1 环境要求与依赖做 Agent 评测不要求 GPU。评测过程是把模型当 API 服务访问因此只要有一台能连上模型服务、能跑 Python 的机器即可。本地开发 Laptop 或一台普通 Linux 服务器都够用。建议使用 Python 3.10 及以上版本否则后续写类型标注和现代语法时容易遇到兼容性问题。依赖库越少越好先保留最核心的几个openai1.30.0 python-dotenv1.0.0 pydantic2.0.0这里的openai库其实可以对接所有 OpenAI 兼容接口。市场上主流模型服务大多提供 OpenAI compatible 的 API因此先不引入额外的抽象层保持最小依赖。如果团队实际用的是 Anthropic 或其他协议再把调用层封装成chat_completion一个函数即可。在运行任何真实请求前需要准备环境变量。不要把 API Key 直接写到配置文件或代码里。在项目根目录创建.envOPENAI_API_KEYsk-xxxx OPENAI_API_BASEhttps://api.openai.com/v1注意真实生产环境还要考虑 Key 的权限范围、网络出口放行和模型额度控制不要让评测脚本暴露在公网环境中。2.2 目录结构可以先铺好项目设计从第一天就按可扩展结构铺开避免把所有代码塞进一个 main.py。一个能支撑长期迭代的目录结构大概长这样agents-on-rails/ ├── bench/ │ ├── __init__.py │ ├── registry.py # 工具注册表 │ ├── runner.py # 轨道式执行器 │ ├── judge.py # 判定逻辑 │ ├── report.py # 报告生成 │ └── trace.py # 轨迹数据模型 ├── tasks/ │ ├── demo_calc.json │ └── demo_search.json ├── specs/ │ ├── agent_spec_01.json │ └── agent_spec_02.json ├── tools/ │ └── mock_tools.py ├── reports/ ├── run_bench.py └── requirements.txt各目录的职责如下表目录或文件职责注意事项bench/评测框架核心代码不依赖具体任务属于可复用基础设施tasks/被测任务数据集每一份 JSON 描述一个任务和判定条件specs/被测 Agent 配置模型、prompt、工具白名单都放这里tools/工具函数实现工具函数只做一件事返回可序列化字符串reports/评测报告输出目录文件名建议带时间戳避免覆盖历史报告run_bench.py命令行入口负责连接各模块并生成最终报告把 tasks 与 specs 分离是为了做交叉实验。同一个任务集可以拿去测两个不同 Agent同一个 Agent 也可以跑多个任务集。只有数据与实体分开才能做到横纵双向对比。2.3 用 agent_spec 固定被测 Agent一个 Agent 在被测之前必须先回答一句话“你是谁、能用什么工具、最多跑多少步、模型是什么。”这些信息应该集中放在 JSON 配置里。以 demo 任务中的 Agent 为例{ agent_id: demo_agent_01, model: gpt-4o-mini, api_base: ${OPENAI_API_BASE}, temperature: 0, max_steps: 6, system_prompt: 你是一个严谨的评测用 Agent。你只能使用任务中明确提供的工具不要编造工具返回结果。完成任务后用中文给出最终答案并说明你是基于哪些工具输出得出的结论。, allowed_tools: [ calculate_total ] }字段含义如下字段含义示例值agent_idAgent 唯一标识demo_agent_01model模型名gpt-4o-mini oder 自己的部署名api_baseAPI 地址可以是环境变量引用temperature采样温度0 表示尽量稳定max_steps最多支持多少轮模型-工具循环6system_prompt系统提示词描述角色和约束allowed_tools工具白名单需要与工具注册表名字一致温度这里值得多说一句。做 Benchmark 时通常希望结果稳定所以默认建议 temperature0。但并不是所有模型在 temperature0 时都完全确定不同供应商的语义不同。如果纯粹测业务能力可以在多个 temperature 下各跑多次用“平均成功率”来消减随机性。2.4 为什么先把“工具白名单”纳入配置很多初学者会在代码里直接给 Agent “挂上所有工具”觉得这样更强大。评测项目不应该这样做。原因是如果不限制工具范围同一个任务在不同时间跑模型可能因为工具顺序、描述措辞微小改动而选择不同的工具最后很难复现问题。工具白名单要放进 agent_spec而不是任务文件里。理由是同一个 Agent 面向多个任务时应始终使用同一套能力边界任务文件只负责描述当前场景。后续如果某个任务想测试 Agent 在少工具环境下的表现可以用另一个 spec 来跑同一个 task实现对照实验。工具白名单的配置文件要与 registry 中的函数名严格对应。评测框架启动时要检查 spec 中声明的工具是否真的存在于注册表如果不在直接报错不要等到运行到中途才失败。def validate_spec(spec, registry): missing [name for name in spec[allowed_tools] if name not in registry] if missing: raise ValueError(fagent_spec 声明了未注册工具: {missing})这样的校验能避免最基础的配置错位问题也符合“配置先行、失败尽早”的工程原则。3. 写一个最小可运行的评测执行器3.1 工具注册表Agent 唯一能碰到外界的通道如果一个 Agent 可以在代码里随便执行任意函数评测结果就是不可控的。更安全也更规范的做法是所有工具都通过注册表登记注册表只暴露固定的名、schema、执行函数。Agent 输出工具名和参数Runner 去查注册表并执行。下面在tools/mock_tools.py中定义一个简单的计算工具。import json from typing import Callable, Dict TOOL_REGISTRY: Dict[str, Callable[..., str]] {} def register_tool(name: str): def decorator(fn): TOOL_REGISTRY[name] fn return fn return decorator register_tool(calculate_total) def calculate_total(base: float, rate: float 0.06) - str: 根据基础金额和税率计算含税总金额。 base 不能为负数否则返回错误信息而不是抛出异常。 if base 0: return json.dumps({error: base 不能为负数}) total round(base * (1 rate), 2) return json.dumps({total: total})工具函数有两个特点值得学习。第一所有工具函数返回值都是字符串并且是 JSON 字符串。OpenAI 等模型服务的 tool message 中 content 字段要求字符串所以统一转 JSON 可以避免序列化问题。第二工具执行中遇到业务异常不直接抛异常而是把错误信息包装成 JSON 返回给模型。这样模型能看到错误有机会修正参数再次调用而不是直接中断整个流程。为了让模型知道工具的 JSON Schema还需要单独的 schema 列表TOOL_SCHEMAS [ { type: function, function: { name: calculate_total, description: 根据基础金额和税率计算含税总金额。base 是基础金额rate 是税率默认 0.06。, parameters: { type: object, properties: { base: { type: number, description: 基础金额单位元 }, rate: { type: number, description: 税率默认 0.06 } }, required: [base] } } } ]这个 schema 会原样作为 tools 参数传给模型。schema 写得越准确模型生成参数的格式错误越少。这里rate不是必须字段因为函数里有默认值base必须因为函数没有 base 算不了结果。3.2 轨道式 runner单轮调用 - 工具执行 - 再次调用Runner 是评测链路的心脏。它不是聊天机器人而是一个有终止条件的循环伪代码如下把 system prompt 和用户任务拼成 messages。调用模型接口传入可用工具 schema。检查返回结果中是否有 tool_calls。如果没有 tool_calls说明模型给出了最终回答跳出循环。如果有 tool_calls逐个执行工具调用。把工具执行结果作为 tool 角色消息回填给模型。重复步骤 2-6直到模型给最终回答或达到最大步数。对应到核心代码可以这样实现import json import time import os from openai import OpenAI from tools.mock_tools import TOOL_REGISTRY, TOOL_SCHEMAS def build_client(spec: dict) - OpenAI: api_base spec.get(api_base, ) if api_base.startswith(${): api_base os.getenv(api_base[2:-1], ) return OpenAI( api_keyos.getenv(OPENAI_API_KEY, not-used), base_urlapi_base or os.getenv(OPENAI_API_BASE), ) def run_task(spec: dict, task: dict) - dict: client build_client(spec) allowed_tools { name: schema for schema in TOOL_SCHEMAS for name in [schema[function][name]] if name in spec[allowed_tools] } messages [ {role: system, content: spec[system_prompt]}, {role: user, content: task[prompt]}, ] trace [] step 0 final_answer None while step spec[max_steps]: started time.time() response client.chat.completions.create( modelspec[model], messagesmessages, temperaturespec.get(temperature, 0), toolslist(allowed_tools.values()) if allowed_tools else None, ) elapsed_ms int((time.time() - started) * 1000) message response.choices[0].message step_trace { step: step, latency_ms: elapsed_ms, usage: response.usage.model_dump() if response.usage else {}, tool_calls: [], } if not message.tool_calls: final_answer message.content step_trace[final_answer] final_answer trace.append(step_trace) break for tool_call in message.tool_calls: fn_name tool_call.function.name try: fn_args json.loads(tool_call.function.arguments) output TOOL_REGISTRY[fn_name](**fn_args) except Exception as exc: # noqa: BLE001 output json.dumps({error: str(exc)}) messages.append({ role: tool, tool_call_id: tool_call.id, name: fn_name, content: output, }) step_trace[tool_calls].append({ name: fn_name, arguments: tool_call.function.arguments, output: output, }) trace.append(step_trace) step 1 return { agent_id: spec[agent_id], final_answer: final_answer, last_step: step, trace: trace, }这里面有几处值得展开解释。第一allowed_tools的过滤逻辑。它先遍历所有 schema再逐个检查 name 是否在 spec 白名单中。这样模型只会看到该 Agent 被允许使用的工具。第二工具参数解析。fn_args是从模型返回的字符串里 parse 出来的本质上是不可信输入。因此要用 try-except 包住整个执行过程。模型可能给错参数类型、给空参数、给不可解析的 JSON任何一个问题都不应该让整个评测崩溃。第三run_task 只产出轨迹 raw result不做成功或失败判断。把“跑”和“判”分开可以让同一份轨迹尝试不同的判定规则不用重新调用模型成本很低。3.3 轨迹记录器先记录再判定如果评测报告只留下一个 task_success 字段后续遇到问题根本无法复盘。Agent 行为链路很长真正有用的信息都在轨迹里。每个 step 的 trace 建议包含这些字段字段含义step当前轮次从 0 开始latency_ms这一轮模型请求耗时usage本轮 token 使用量tool_calls本轮触发的工具调用列表tool_calls.name工具名tool_calls.arguments模型返回的原始参数字符串tool_calls.output工具执行后的 JSON 输出final_answer模型最终自然语言回答把 traces 保存下来之后很多事情就变得简单了。失败样本可以回放可以检查是哪一步工具参数传错了成功样本可以复用判断某个 prompt 改动是否让轨迹变得更短或更稳定。评测报告的数据越完整后续做回归分析时越省力。3.4 dry-run不接外部 LLM 也能验证链路真实调用模型会产生费用也不利于调试代码。因此给 Runner 增加一个 dry-run 模式非常有用。在 dry-run 中用一个 fake client 模拟模型第一步就返回一个固定的工具调用参数第二步模拟返回最终答案。这样可以在不联网的情况下验证“模型响应 - 工具执行 - 消息回填 - 最终结束”整条链路。class FakeResponse: def __init__(self, message, usageNone): self.choices [type(Choice, (), {message: message})()] self.usage usage FAKE_USAGE type(Usage, (), {model_dump: lambda self: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20, }}) def dry_run_task(spec: dict, task: dict) - dict: fake_client type(FakeClient, (), { chat: type(FakeChat, (), { completions: type(FakeCompletions, (), { create: lambda **kwargs: FakeResponse( type(Msg, (), { tool_calls: [ type(Call, (), { id: call_demo, function: type(Fn, (), { name: calculate_total, arguments: json.dumps({base: 100, rate: 0.06}), }) }) ], content: None, })(), usageFAKE_USAGE, ) if kwargs.get(tools) else FakeResponse( type(Msg, (), { tool_calls: None, content: 计算得到含税总额是 106.0 元。, })(), usageFAKE_USAGE, ) }) }) }) # 这里可以临时替换 run_task 中构建的 client便于调试这样的 dry-run 样例让团队新成员也能快速理解 Runner 的协议而不需要理解模型 API 的具体细节。正式需求确定后再通过真实模型跑一遍重点观察工具参数和输出是否符合预期。4. 任务集与评判规则怎么设计4.1 任务 JSON一次评测只做一件明确的事任务集是评测的上游输入。任务定义得不清楚后续所有判断都会摇摆。以计算含税金额为例任务文件可以设计成这样{ task_id: demo-calc-001, name: 计算一笔含税金额, prompt: 请使用可用工具计算 100 元基础金额按 6% 税率计算后的含税总额并输出最终数字。, success_checks: [ { check_id: must_call_calc, type: tool_called, params: { tool: calculate_total } }, { check_id: base_arg_correct, type: argument_match, params: { tool: calculate_total, key: base, expected: 100 } }, { check_id: final_contains_total, type: final_contains, params: { value: 106 } } ], metadata: { difficulty: easy, category: calculation } }这个任务只描述“当前要完成什么事”不限定模型必须怎么说。但 success_checks 把成功条件写死了必须调用过 calculate_totalbase 参数必须等于 100最终答案必须包含 106。这样模型如果只凭常识说 106没有调用工具依然会被判失败。这是 Agent 评测与普通问答评测的重大区别评测既要看最终答案也要看过程是否可信。即使最终答案碰巧正确如果模型没有使用工具它在这个评测任务中也应该判为失败因为 Agent 场景要求我们验证“模型是否真的通过工具获取了结果”。4.2 成功条件规则判定为主LLM 语义判定为辅建议先以规则判定为第一层。这类判定稳定、成本低、可解释例如“是否调用过指定工具”“某个参数是否等于预期值”“最终答案是否包含关键字符串”。规则判定的缺点是灵活度有限模型可能用不同措辞输出结果导致 final_contains 失效。因此可以配合一个可选的llm_judge。LLM 语义判定可以这样设计把任务原始描述、Agent 的中间轨迹、最终答案交给一个更强的 judge 模型让 judge 输出 structured JSON。{ check_id: semantic_answer_ok, type: llm_semantic, params: { judge_model: gpt-4o, criteria: 判断 Agent 最终答案是否根据工具返回结果正确回答了任务问题。只输出 success 或 failure。 } }使用 LLM 当裁判时要注意两点。第一judge 要使用与 Agent 不同的模型避免同模型偏向自身输出。第二judge 结果仍要落到固定结构而不是开放文本。不过在第一版项目中没有必要大量引入 LLM 判定。规则判定能覆盖大约 80% 的基础场景LLM 判定更适合语义开放性高、结果不好枚举的任务。顺序上应该先搭建规则判定体系等任务集规模上来后再为确实无法用规则表达的少量任务引入语义判定。4.3 指标口径先统一否则报告很难比较有了多份轨迹之后需要一组稳定的指标来做趋势判断。常用的 Agent 评测指标如下指标计算方式说明任务成功率成功任务数 / 总任务数最核心的北极星指标平均步数总 step 数 / 任务数步数越少说明规划越高效但不要唯步数论工具调用成功率未抛异常的工具调用 / 工具总调用数只代表调用成功了不代表语义正确参数校验通过率关键参数符合预期的调用数 / 工具总调用数比工具调用成功率更严格平均耗时所有 LLM 请求耗时之和 / 任务数包含多次请求不只是单次首 token平均 token 成本总 token 消耗 / 任务数需要监控成本上限失败模式占比某个失败原因的任务数 / 总任务数例如工具错选、参数错、超步数、最终答案遗漏其中失败模式占比是很多人忽略的。只看成功率的坏处是一个 prompt 改动可能把“参数错误”的量转移到“错选工具”上成功率没变但问题性质变了。记录失败模式后可以更精准地定位优化方向。在一个 Benchmark 项目中必须让报告同时包含原始分子分母和明细列表。比如成功率不能只写 0.8要写 8/10并列出哪一个任务失败了这样每次迭代可以直接定位到具体 case。4.4 评测报告为什么要保存完整轨迹很多 Agent 工程做到后面调试都靠日志。日志如果能按任务、按 step、按 tool_call 结构化保存调试效率会高很多。对应到一套报告文件最低限度保存的内容应当包括{ task_id: demo-calc-001, task_success: false, final_answer: 计算结果是 100 元。, failed_checks: [ { check_id: final_contains_total, reason: final_answer 未包含预期字符串 106 } ], trace: [ { step: 0, tool_calls: [ { name: calculate_total, arguments: {\base\: 100, \rate\: 0.06}, output: {\total\: 106.0} } ] } ] }这种报告可以直接给研发看也可以留给后续做统计分析。采集原始轨迹的另外一个收益是当任务集被人为调简单或误改时通过 diff 历史报告就能发现某个本来调用工具的任务现在模型直接凭记忆输出答案了精度看起来没变但可信度大幅下降。5. 跑通评测并做横向对比5.1 运行一次最小评测在实际项目里命令行入口run_bench.py至少应该支持三个参数spec 文件、task 文件或 task 目录、输出目录。示例命令如下cd agents-on-rails python run_bench.py \ --spec specs/agent_spec_01.json \ --task tasks/demo_calc.json \ --output reports/run_20250210_180000.json运行完成后先看程序是否正常结束再看报告文件是否生成。如果配置了 dry-run可以先跑一遍不花钱的流程python run_bench.py --spec specs/agent_spec_01.json --task tasks/demo_calc.json --dry-rundry-run 成功不代表真实链路成功。真实模型容易在解析 tool_calls、生成参数 JSON 时产生各种边界问题。因此第一次接真实模型时建议只跑一两个任务逐步观察输出再扩展任务集。5.2 读懂报告 JSON一份完整报告大致会这样{ summary: { total_tasks: 1, passed_tasks: 1, task_success_rate: 1.0, avg_steps: 2, avg_duration_ms: 3180, total_cost_usd: 0.00042 }, tasks: [ { task_id: demo-calc-001, agent_id: demo_agent_01, task_success: true, checks_passed: 3, checks_total: 3, last_step: 1, duration_ms: 3180, cost_usd: 0.00042, trace: [] } ] }看报告的顺序应当是先看 task_success_rate再看失败任务的 failed_checks最后进入 trace 看具体哪一步偏离了预期。只盯成功率容易错过关键细节因为很多失败任务离成功只差一个工具参数把失败样本归好类后优化方向会清晰很多。5.3 多 Agent 横向对比轨道式评测最擅长的事情就是让多个 Agent 在同一条轨道上比较。假设有两个 spec分别是agent_spec_01.json和agent_spec_02.json想对比两个 Agent 在同一个任务集上的表现可以跑两次并保存报告python run_bench.py --spec specs/agent_spec_01.json --task tasks/demo_calc.json --output reports/run_agent_a.json python run_bench.py --spec specs/agent_spec_02.json --task tasks/demo_calc.json --output reports/run_agent_b.json然后再用一个小的汇总脚本对两份 report summary。这个阶段可以先不追求自动生成复杂图表把成功率、平均步数、平均成本并排列出来即可。横向对比要注意变量的唯一性只允许换一个变量。比如对比两个模型时system prompt 必须完全一致工具 schema 必须一致最大步数必须一致否则无法判断是模型能力差异还是 prompt 差异带来的变化。5.4 把评测接入回归流程评测体系只有接入提交或发布流程才能真正发挥作用。节奏上建议分三步走第一步本地回归。开发者在改 prompt 或工具 schema 前先跑一次任务集保存 baseline report改完后再跑一次用 diff 脚本看成功率、失败模式是否变化。这一步成本低不需要额外环境就能拦截大部分退化。第二步CI 回归。在测试环境准备一组不依赖生产数据的任务集在 PR 或 merge 前自动跑一遍。任务集选择要与改动范围相关。如果改的是计算类工具就跑计算任务如果改的是检索类 Agent就跑检索任务。第三步发布前全量回归。发布到线上前跑完整任务集确认失败率没有超过阈值同时检查工具调用总成本有没有明显上升。接入 CI 时需要注意评测脚本本身会调用外部模型 API可能引入不稳定因素。因此 CI 中建议设置超时时间和失败重试机制。模型服务如果偶发超时不要立刻判负可以保留原始异常日志并重试一次。重试仍然失败再判为环境异常或真失败。6. 常见报错和排查链路6.1 Provider rejected通常是 schema 或 payload 问题现象运行评测时模型服务返回类似llm request failed: provider rejected the request schema or tool payload的错误。整个过程没有进入工具执行阶段请求在发送时就失败了。可能原因主要是工具 schema 格式不符合模型服务的约束。常见的情况包括schema 中使用了模型服务不支持的 JSON Schema 关键字required字段为空数组部分服务不接受description字段过长或包含不可见字符某个 enum 的取值类型与实际字段类型不一致工具数量过多超过服务限制。排查方式先打印出实际发送给模型 API 的完整 request payload尤其是 tools 字段。不要靠猜把 payload 存成 JSON 文件后逐段检查。python -c import json; print(json.dumps(allowed_tools, ensure_asciiFalse, indent2))解决方案先把工具 schema 简化到最小只留 name、description、parameters 的 type、properties、required确认能通过后再逐步增加约束。不要直接照搬其它框架生成的复杂 schema尤其是oneOf、anyOf、allOf这类组合关键字不同服务兼容程度参差不齐。预防建议在 Runner 初始化时做一次 schema 静态校验每次启动评测前打印当前 Agent 的 tools 数量如果工具数量或 schema 结构异常就提前失败。6.2 timeout 和空响应先分清楚是网络、限流还是模型处理慢现象执行器报llm request timed out或者提示the model did not produce a response before the mod...被截断。常见于长任务、参数特别多、模型推理时间超过客户端超时阈值时。可能原因有三类。第一类是网络链路问题本地到模型服务不稳定第二类是限流或排队服务端没有在预期时间内返回第三类是模型确实在处理大型 prompttool schema 太大或历史消息太长导致首 token 时间过久。排查顺序建议按下面这条链路走先用 curl 或简单脚本直接请求一次该模型确认是单次调用就慢还是只有 Agent 运行模式慢检查客户端 timeout 设置把超时从 30 秒临时提高到 120 秒观察是否复现开启流式输出尽早拿到首包避免客户端等待完整响应超时查看服务端返回的 error code是 429 限流还是 503 过载统计单次调用消耗的 token如果是历史消息过长考虑压缩轨迹或截断旧步骤。解决方案中比较实际的是这几种client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_API_BASE), timeout120.0, max_retries2, )这里的 timeout 指整次请求超时max_retries 指自动重试。生产环境不要无限重试否则模型服务端持续不稳定时评测任务会无限堆积。6.3 Agent 死循环和无效工具调用现象Agent 反复调用同一个工具每次传入相同或相似的参数消耗大量 token 但不给最终答案或者最后因为达到 max_steps 被强制截断但没有任何有效结果。常见原因包括工具返回信息不够模型无法从错误信息中确定下一步模型误以为再调用一次就能成功system prompt 没有说明“何时应该停止”或“失败后如何退出”。Runner 层面需要增加保护机制。一种简单有效的做法是记录最近 N 步内相同工具、相同参数的调用次数超过阈值就直接终止并把 stopped_reason 标记为repeated_tool_call。def has_repeated_call(trace, name: str, arguments: str, max_repeat2) - bool: count 0 for step_trace in trace: for call in step_trace.get(tool_calls, []): if call[name] name and call[arguments] arguments: count 1 return count max_repeat更好的做法是让这个信息回流给模型让它意识到自己正在重复尝试。Runner 检测到重复后不直接把所有任务判失败而是给模型追加一条系统消息“你最近重复调用了同一个工具且参数相同请检查是否陷入循环可以基于已有结果直接给出最终回答。”这样既保留了模型纠错机会也能避免无限开销。防止死循环的另一层是任务设计。工具返回内容不能太模糊。比如计算失败时返回信息最好包括“缺少必需参数 base”否则模型看到含糊的 error很容易猜测并反复调用。6.4 分数不稳定非确定性是评测头号敌人现象同一份 spec、同一个 task跑两次结果不一致。第一次成功率 1.0第二次变成 0.8。排查 prompt、工具都没有发现变化。这种现象基本来自模型采样随机性、服务端负载和同一模型的版本差异。处理非确定性的原则是把“单次运行”扩展成“多次取聚合”。建议策略是每个任务在 temperature0 下跑 3 次如果三次结果一致可信度较高如果不一致再做 5 次采样报告平均成功率、标准差和每次的 raw result。确定性任务可以固定 seed多模型对比时也要注明运行时间和模型版本避免同一模型在服务端悄悄升级导致对比失真。同时要把 token 消耗纳入结果统计。很多非显而易见的退步其实不是准确率下降而是同样的成功率下 token 成本暴涨说明模型开始“多绕路”。这类退化在轨导式报告中一对比就会暴露。7. 从玩具 Demo 到可用评测平台7.1 学习环境与生产环境的差别本地 Demo 可以在 main.py 里直接读 JSON 文件一行行打印日志生产评测平台则要处理版本、权限、并发、成本和监控等问题。两者的差别如下表维度学习环境生产/团队使用配置直接写在代码或单文件 JSON由配置中心或独立 repo 管理版本化API Key本地 .env使用密钥管理服务不落盘任务集变更改文件直接跑通过 git 变更任务集与代码同仓库管理报告去向标准输出或单个 JSON存数据库或对象存储支持历史趋势并发控制单线程串行跑限制并发数避免触发模型服务限流失败处理
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门