ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建Agent评测框架:真实任务、过程追踪与评分归因实战

从零搭建Agent评测框架:真实任务、过程追踪与评分归因实战 1. 为什么我要自己搭一套 Agent 评测框架模型厂商的宣传页我看了大半年几乎每一家都在说自己的 Agent 能力“业界领先”。但真把同一个任务丢给不同模型跑结果经常和宣传对不上有的模型在演示视频里行云流水实际跑三步就开始胡编工具返回值有的模型宣传里没提但在多轮任务里稳得离谱。问题出在评测口径上——厂商用的是自己挑过的任务集评分标准也是自己定的这种评测对实际选型几乎没有参考价值。我搭这套框架的出发点很朴素用我自己的真实任务按我自己的评分规则跑出我能信的数据。它不追求学术意义上的完备性也不打算做成通用 benchmark 平台就是一套能落地的、可复现的 Agent 真实任务评测框架。核心解决四件事任务怎么设计才不虚、执行过程怎么追踪才不漏、评分怎么归因才不冤、结果怎么对比才有意义。适合谁来参考如果你正在做 Agent 选型、要给团队定模型基线、或者单纯想知道自己写的 Agent 到底几斤几两这套东西可以直接抄。不需要你有评测背景但需要你愿意花半天时间把任务集和评分规则想清楚——这部分偷懒后面跑出来的数据全是噪音。我踩过的第一个坑就是一开始图省事直接拿网上的公开评测集跑跑完发现任务太“干净”了工具调用全是理想路径和真实业务里各种脏输入、超时、返回格式错乱完全不是一回事。所以后面所有任务都是我按真实场景重新设计的这也是这套框架和现成方案最大的区别。2. 评测框架的整体设计与选型思路2.1 核心设计原则任务真实、过程可见、评分可归因整套框架我定了三条硬原则后面所有实现都围绕它们展开。第一条是任务真实。任务必须来自真实使用场景包含真实的工具、真实的输入噪声、真实的失败可能性。比如“查天气”这种任务我不会用因为太干净了我会用“根据用户一段口语化的行程描述调用多个工具补齐信息并生成一份可执行的日程”这种任务里模型要处理歧义、要决定调用顺序、要处理工具返回的异常。第二条是过程可见。只记录最终答案是不够的必须把每一步的思考、工具调用、参数、返回值、耗时全部落盘。原因很简单Agent 失败往往不是答案错而是中间某一步走偏了。没有过程记录你根本不知道是模型能力问题还是你的工具描述写得有问题。第三条是评分可归因。评分不能只给一个总分要能拆到“任务理解”“工具选择”“参数正确性”“结果可用性”这几个维度。这样跑完一轮你能明确知道某个模型是“脑子不行”还是“手不行”。提示这三条原则里过程可见是最容易被忽略的。很多人只存最终输出结果排查问题时两眼一抹黑只能重跑浪费大量时间和额度。2.2 技术选型为什么用 Python SQLite 轻量编排选型上我没有追求时髦。编排层用 Python 手写了一个不到 300 行的调度器没有上 LangChain 这类重框架。原因是我需要完全掌控每一步的执行和记录重框架的抽象层反而会挡住我埋点。实测下来手写调度器虽然前期多花两小时但后面调试和加功能省下的时间远超这个成本。存储用 SQLite单文件、零配置、支持复杂查询。评测数据量级通常在几千到几万条 traceSQLite 完全扛得住而且方便我把整个评测库打包发给同事复现。有人会问为什么不用 JSON 文件答案很简单我需要按维度聚合查询比如“所有工具选择错误的 case 里模型 A 和模型 B 各占多少”这种查询用 SQL 一行搞定用 JSON 得写一堆脚本。评分环节我做了两层一层是规则评分针对有明确正确答案的维度比如工具名是否匹配、参数是否合法用代码判定另一层是模型评分针对开放性维度比如最终答案是否可用用一个独立的评分模型来打分。两层结合既保证客观性又覆盖主观质量。2.3 和现成评测方案的差异我为什么不直接用公开 benchmark公开 benchmark 最大的问题是任务分布和你的真实场景不匹配。它们通常聚焦在特定类型任务上比如网页操作、代码修复而你的业务可能是客服对话加订单处理。用不匹配的任务集评测跑出来的排名对你没有指导意义。第二个问题是评分标准黑盒。很多 benchmark 只给最终分数不告诉你扣分扣在哪。我需要知道模型是理解错了任务还是工具调用格式不对还是最后一步总结跑偏了。这些信息决定了我是换模型还是改 prompt。第三个问题是不可复现。有些评测依赖特定版本的模型接口模型一升级历史数据就没法对比了。我这套框架把每次运行的模型版本、prompt 版本、工具定义版本全部记录在案任何时候都能复现某一次评测的完整上下文。3. 任务设计怎么让评测任务不虚3.1 任务分层从单步到多轮覆盖真实复杂度我把任务分成三层每层考察的能力不同。L1 单工具任务模型只需要调用一个工具并返回结果。这层主要筛掉那些连基本工具调用格式都搞不对的模型。比如“查询订单 12345 的物流状态”工具定义清晰参数明确。这层通过率低于 90% 的模型基本不用往下测了。L2 多工具串联任务需要按顺序调用多个工具前一个的输出是后一个的输入。这层考察的是模型的规划能力和上下文传递能力。比如“用户说上周买的鞋子想退货先查订单再查退货政策最后生成退货单”。这层最容易暴露的问题是模型把工具返回值理解错导致后续调用参数错误。L3 多轮交互任务任务过程中需要和用户模拟器交互信息不完整需要主动追问。这层考察的是模型的对话管理和信息补全能力。比如“用户想订机票但没说日期”模型应该追问而不是瞎猜。这层通过率普遍偏低也是区分模型真实水平的关键层。提示任务分层不是为了排名好看而是为了定位问题。如果 L1 通过率就低说明模型基础能力有问题如果 L1 高但 L3 低说明模型在多轮场景下容易迷失这时候要考虑是不是上下文管理策略需要调整。3.2 任务设计中的噪声注入让评测更接近真实真实场景里用户输入不会像测试用例那么规整。所以我在任务里刻意注入了三类噪声。第一类是语言噪声口语化表达、错别字、中英文混用。比如“帮我看下我昨天下单的那个东西到哪了”而不是“查询订单状态”。这考察模型对模糊表达的理解能力。第二类是工具噪声工具返回里包含无关字段、格式偶尔不标准、偶尔超时。这考察模型的容错能力。很多模型在工具返回干净时表现很好一旦返回里多几个字段就开始胡编。第三类是干扰噪声在任务描述里加入无关信息考察模型能否抓住重点。比如用户说“我最近在减肥想买个低卡零食对了帮我查下我上个月的账单”模型应该聚焦在查账单上而不是被零食带偏。实测下来注入噪声后各模型的通过率普遍下降 15 到 30 个百分点这个下降幅度本身就是很有价值的信号——它反映了模型在真实环境下的鲁棒性。3.3 任务集的规模与分布多少任务才够用我的经验是每个能力维度至少 20 个任务总任务数不少于 100 个。低于这个数单次运行的随机波动会掩盖真实差异。我试过用 30 个任务跑同一个模型两次运行的结果差了 8 个百分点这个波动没法用来做决策。任务分布上我按 L1:L2:L3 3:4:3 的比例配置。L1 用来快速筛除L2 是主力考察层L3 用来区分头部模型。如果你的业务场景特别偏重某一层可以调整比例但建议每层都不少于 20 个任务。另外任务集要定期更新。模型在迭代你的业务也在变化半年前的任务集可能已经不能反映当前的真实需求。我一般每季度替换 20% 的任务保持任务集的新鲜度。4. 执行追踪怎么把每一步都记清楚4.1 追踪数据结构设计一条 trace 应该包含什么每条 trace 我设计了固定的字段结构核心包括任务 ID、模型标识、模型版本、prompt 版本、工具定义版本、开始时间、结束时间、总耗时、总 token 消耗、最终输出、以及一个 steps 数组。steps 数组里每一步记录步骤序号、步骤类型思考/工具调用/最终回答、模型原始输出、解析后的工具名、解析后的参数、工具实际返回值、工具执行耗时、以及这一步的 token 消耗。这个结构看起来字段多但每一个在排查问题时都用得上。我特别强调记录模型原始输出这一点。很多框架只记录解析后的结构化数据一旦解析出错你根本不知道是模型输出格式不对还是你的解析器有 bug。保留原始输出任何时候都能回溯。4.2 埋点实现在调度器里怎么插入追踪逻辑埋点我放在调度器的三个位置。第一处是模型调用前后记录请求的完整 prompt 和模型返回的原始文本。第二处是工具执行前后记录工具名、参数、返回值和耗时。第三处是每一步解析后记录解析结果和解析是否成功。实现上我用了一个装饰器模式把追踪逻辑和业务逻辑解耦。这样调度器代码保持干净追踪逻辑集中在一处方便维护。具体做法是写一个trace_step装饰器包在模型调用和工具调用函数外面自动记录进出参和时间。import time import json from functools import wraps def trace_step(step_type): def decorator(func): wraps(func) def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) elapsed time.time() - start trace_record { step_type: step_type, input: str(args) str(kwargs), output: str(result), elapsed: round(elapsed, 3), timestamp: time.time() } # 写入当前 trace 的 steps 数组 current_trace[steps].append(trace_record) return result return wrapper return decorator这段代码不复杂但效果很好。加上之后任何一次运行我都能精确知道时间花在哪、token 花在哪。4.3 追踪数据的存储与查询SQLite 表结构设计存储上我用了三张表runs表存每次运行的元信息traces表存每条任务 trace 的概要steps表存每一步的明细。三张表通过 run_id 和 trace_id 关联。runs表字段run_id、model_name、model_version、prompt_version、tool_version、start_time、end_time、total_tasks、passed_tasks。traces表字段trace_id、run_id、task_id、task_level、final_output、total_steps、total_tokens、total_elapsed、score、score_detail。steps表字段step_id、trace_id、step_index、step_type、raw_output、parsed_tool、parsed_args、tool_result、elapsed、tokens。这个结构的好处是查询灵活。比如我想知道“模型 A 在 L2 任务里工具选择错误的平均步数”一条 SQL 就能出来。如果只存 JSON这种查询得写脚本遍历效率差很多。提示steps 表的数据量会比较大建议按 run_id 建索引查询时先过滤 run_id 再关联避免全表扫描。我一开始没建索引查一次要十几秒建完索引降到毫秒级。5. 评分归因怎么打分才不冤5.1 评分维度拆解四个维度覆盖 Agent 核心能力我把评分拆成四个维度每个维度独立打分最后加权汇总。任务理解权重 20%模型是否正确理解了任务目标。判定方式是看模型第一步的思考内容是否指向正确方向。这个维度用模型评分因为理解程度很难用规则判定。工具选择权重 30%模型是否选对了工具。这个用规则评分对比模型调用的工具名和预期工具序列。顺序对但工具错扣一半分工具对但顺序错扣三成分。参数正确性权重 30%工具调用的参数是否合法且正确。规则评分检查参数名是否在工具定义里、参数值是否符合类型要求、关键参数值是否和预期一致。结果可用性权重 20%最终输出是否可直接使用。模型评分从完整性、准确性、格式规范性三个角度打分。这个权重分配是根据我的业务场景调的。如果你的场景里工具选择更关键可以把工具选择权重提到 40%。权重没有标准答案关键是要固定下来保证不同模型之间可比。5.2 规则评分实现工具选择和参数校验的代码逻辑规则评分这部分我写了一个校验器核心逻辑是对比实际调用序列和预期调用序列。def score_tool_selection(actual_calls, expected_calls): if not actual_calls: return 0.0 actual_names [c[tool] for c in actual_calls] expected_names [c[tool] for c in expected_calls] # 工具集合匹配度 actual_set set(actual_names) expected_set set(expected_names) if not expected_set: return 1.0 set_score len(actual_set expected_set) / len(expected_set) # 顺序匹配度 order_score 0.0 if actual_names expected_names: order_score 1.0 elif actual_set expected_set: order_score 0.5 return round(set_score * 0.6 order_score * 0.4, 3)参数校验类似逐个检查参数名和参数值。参数名不在工具定义里直接判错参数值类型不对判错关键参数值不匹配按比例扣分。这套规则评分的优势是完全可复现同样的输入永远得到同样的分数不受评分模型波动影响。劣势是只能覆盖有明确预期的维度开放性维度还得靠模型评分。5.3 模型评分设计怎么让评分模型打分更稳定模型评分最大的问题是不稳定。同一个输出评分模型两次打分可能差 1 到 2 分。我的解决办法是三点。第一评分 prompt 要极度具体。不要写“请评估答案质量”要写“请从以下三个角度评估信息是否完整缺少关键信息扣分、数据是否准确与工具返回不一致扣分、格式是否规范缺少必要字段扣分每项 0 到 10 分”。第二给评分模型提供参考标准。我会在 prompt 里附上预期答案的关键要点让评分模型有对照。没有对照评分模型只能凭感觉打。第三多次评分取平均。同一个输出让评分模型打三次取平均值。实测下来三次平均能把波动控制在 0.3 分以内基本可接受。提示评分模型建议用比被测模型更强的模型避免“弱模型评强模型”导致的系统性偏差。如果预算有限至少保证评分模型和被测模型不是同一个。5.4 归因分析从分数到问题定位的完整链路评分跑完只是开始真正有价值的是归因分析。我的做法是先看总分分布再看维度分分布最后看具体 case。总分分布能告诉你模型的大致水平。如果某个模型总分明显低于其他先看它的 L1 通过率L1 低说明基础能力有问题不用往下分析。维度分分布能告诉你问题出在哪。比如工具选择分低但参数分高说明模型知道该调什么工具但参数填不对这时候要检查工具定义的参数描述是否清晰。具体 case 分析是最后一步也是最花时间的。我会把每个失败 case 的 trace 拉出来逐步看模型在哪一步走偏。常见的问题包括模型把工具返回的 JSON 理解错了、模型在多轮对话里丢失了上下文、模型在工具报错后没有重试而是直接放弃。这套归因链路跑下来你不仅知道模型行不行还知道为什么不行以及怎么改。6. 实操过程从零跑通一轮完整评测6.1 环境准备与依赖安装环境很简单Python 3.10 以上装几个基础库就行。pip install openai sqlite3 pandas tabulateopenai库用来调模型接口sqlite3是 Python 内置的pandas用来做结果分析tabulate用来打印对比表格。不需要装任何评测框架整套逻辑自己写。目录结构我这样组织agent-eval/ tasks/ # 任务定义 l1_tasks.json l2_tasks.json l3_tasks.json tools/ # 工具定义和实现 tool_defs.json tool_impl.py runner/ # 调度器和追踪 scheduler.py tracer.py scorer/ # 评分逻辑 rule_scorer.py model_scorer.py results/ # 评测结果 eval.db config.yaml # 模型配置这个结构清晰每个模块职责单一改哪部分都不会影响其他部分。6.2 任务定义文件编写JSON 结构说明任务定义我用 JSON每个任务包含task_id、level、description、expected_tools、expected_params、expected_output_keypoints、max_steps。{ task_id: L2_001, level: 2, description: 用户说上周买的鞋子想退货请先查询订单再查询退货政策最后生成退货单, expected_tools: [query_order, query_return_policy, create_return], expected_params: { query_order: {order_keyword: 鞋子}, create_return: {reason: 退货} }, expected_output_keypoints: [退货单号, 退货地址, 预计退款时间], max_steps: 8 }expected_params只写关键参数不要求全匹配避免过严导致误判。max_steps用来防止模型陷入死循环超过步数直接判失败。6.3 调度器核心逻辑一次任务执行的完整流程调度器的主循环逻辑是这样的加载任务、构造初始 prompt、进入循环、每轮调用模型、解析输出、如果是工具调用就执行工具并把结果拼回上下文、如果是最终回答就结束、记录 trace。def run_task(task, model_client, tools): trace init_trace(task) messages build_initial_messages(task) for step in range(task[max_steps]): response model_client.chat(messages) trace[steps].append(record_model_step(step, response)) parsed parse_response(response) if parsed[type] final_answer: trace[final_output] parsed[content] break elif parsed[type] tool_call: tool_result execute_tool(parsed[tool], parsed[args], tools) trace[steps].append(record_tool_step(step, parsed, tool_result)) messages.append(build_tool_message(parsed, tool_result)) else: # 解析失败记录并继续 messages.append(build_error_message(parsed)) trace[total_steps] len(trace[steps]) return trace这个循环看起来简单但每个环节都有细节。比如解析失败时不能直接判失败要给模型一次纠正机会因为有时候只是格式问题。再比如工具执行要加超时避免某个工具卡死拖垮整个评测。6.4 跑一轮评测命令、耗时与资源消耗实录跑一轮 100 个任务的评测用中等规模的模型实测耗时约 40 分钟token 消耗约 200 万。这个消耗量级对个人开发者来说可以接受如果预算紧张可以先跑 30 个任务的子集做快速筛选。python runner/scheduler.py --config config.yaml --tasks tasks/ --output results/eval.db跑完后用分析脚本生成对比报告python scorer/report.py --db results/eval.db --models model_a,model_b,model_c报告会输出每个模型的总分、各维度分、各层级通过率以及失败 case 的归因分布。我一般先看总分排名再看维度分找差异最后挑几个典型失败 case 深入看 trace。7. 常见问题与排查技巧实录7.1 模型输出解析失败格式问题的三种典型情况解析失败是最常见的问题我遇到的主要有三种。第一种是模型在工具调用外面包了多余文字。比如输出“好的我来帮你查询订单 {tool_call: ...}”解析器如果只认纯 JSON 就会失败。解决办法是解析器要能提取文本中的 JSON 片段而不是要求整个输出是 JSON。第二种是参数值类型不对。比如工具定义要求整数模型传了字符串 123。解决办法是在执行工具前做类型转换能转就转转不了再判错。第三种是模型一次输出多个工具调用。有些模型会在一轮里返回两个工具调用而我的调度器一次只处理一个。解决办法是要么支持并行工具调用要么只取第一个并记录警告。提示解析失败不要直接判任务失败给模型一次重试机会。实测下来约 60% 的解析失败在第二次尝试时能成功直接判失败会低估模型真实能力。7.2 工具执行超时与异常怎么保证评测不中断工具执行必须加超时和异常捕获否则一个工具卡住整个评测就挂了。我的做法是每个工具调用包一层 try-except 加超时控制。import signal def execute_with_timeout(func, args, timeout10): def handler(signum, frame): raise TimeoutError(tool execution timeout) signal.signal(signal.SIGALRM, handler) signal.alarm(timeout) try: result func(**args) return {success: True, data: result} except Exception as e: return {success: False, error: str(e)} finally: signal.alarm(0)超时或异常时把错误信息作为工具返回值拼回上下文让模型自己决定怎么处理。这本身也是评测的一部分——考察模型的容错能力。7.3 评分偏差排查规则评分和模型评分不一致怎么办规则评分和模型评分偶尔会打架。比如规则评分说工具选择正确但模型评分说最终答案不可用。这种情况通常是模型选对了工具但没用好返回结果。排查方法是把两个评分的明细都拉出来对比。如果规则评分高但模型评分低重点看最终输出和工具返回的差异通常是模型在总结环节丢失了关键信息。如果规则评分低但模型评分高可能是规则太严比如预期工具序列和实际序列只是顺序不同但结果一样这时候要考虑放宽规则。我一般会定期校准两套评分的一致性抽 20 个 case 人工核对确保评分逻辑没有系统性偏差。7.4 常见问题速查表问题现象可能原因排查方法解决方向解析失败率高模型输出格式不稳定看原始输出解析器加容错给重试机会工具选择分低工具描述不清看模型思考内容优化工具定义描述参数分低参数类型或命名歧义对比预期和实际参数明确参数类型和示例多轮任务失败上下文丢失看每轮 messages加摘要或关键信息置顶评分波动大评分模型不稳定同输出多次评分多次取平均细化评分标准评测耗时过长任务步数过多看 steps 分布设 max_steps优化任务这张表是我踩坑踩出来的每次遇到新问题就往里加一行。现在它已经成了我排查评测问题的第一入口。8. 结果分析与模型选型建议8.1 怎么读评测报告总分之外的三个关键信号总分排名只是起点真正决定选型的是三个信号。第一个信号是层级通过率曲线。如果模型 L1 高、L2 骤降说明它单步能力可以但规划能力弱适合简单任务。如果 L1 到 L3 下降平缓说明它综合能力强适合复杂场景。第二个信号是失败归因分布。同样是 70 分一个模型失败集中在参数错误另一个集中在任务理解前者改改工具描述可能就能提升后者是模型能力天花板改 prompt 效果有限。第三个信号是稳定性。同一个模型跑三次分数波动小于 3 分的算稳定大于 8 分的说明它在某些任务上表现随机生产环境要慎用。8.2 不同业务场景下的选型策略选型没有绝对的最优只有匹配。如果你的场景是单步工具调用为主比如查询类、计算类优先看 L1 通过率和参数正确性分这两个指标高的模型基本够用不用为 L3 能力付溢价。如果你的场景是多步流程自动化比如订单处理、工单流转重点看 L2 通过率和工具选择分同时要关注模型在工具报错后的恢复能力。如果你的场景是多轮对话交互比如客服、助手L3 通过率和任务理解分是核心同时要测模型在信息不完整时的追问行为是否合理。我自己的经验是不要用一个模型打天下。简单任务用便宜模型复杂任务用强模型按任务层级路由成本能降一半以上效果还不打折。8.3 评测框架的持续迭代任务集和评分规则的更新节奏框架搭好不是终点。我的更新节奏是任务集每季度更新 20%评分规则每半年校准一次模型版本每次升级都重跑基线。任务集更新主要做两件事替换已经太简单的任务加入新出现的业务场景。评分规则校准主要是抽检人工评分和自动评分的一致性发现偏差就调整权重或评分 prompt。还有一点很重要保留历史评测数据。模型迭代很快半年前的数据现在看可能已经过时但对比历史数据能看出模型的进步趋势这个趋势对长期选型很有参考价值。9. 我在实操中踩过的坑和总结的技巧第一个坑是任务描述写得太详细。一开始我怕模型理解不了把任务写得像需求文档结果模型直接照着描述一步步做根本不需要规划能力。后来我把描述改得口语化、留白模型才真正开始“思考”。任务描述要像真实用户说话不能像测试用例。第二个坑是评分权重拍脑袋定。我一开始四个维度平均分跑完发现工具选择明明是最关键的但权重只有 25%导致排名失真。后来我按业务重要性重新分配权重排名才符合直觉。权重一定要和业务目标对齐不能图省事平均分。第三个坑是忽略 token 成本。只看通过率不看成本选出来的模型可能效果最好但贵得离谱。后来我在报告里加了“每通过一个任务的 token 成本”这个指标选型时综合考虑效果和成本实际落地时预算才可控。第四个坑是评测环境不稳定。有一次跑评测时网络波动好几个任务因为接口超时失败我还以为是模型问题。后来加了重试机制和网络异常标记把环境问题和模型问题分开数据才干净。最后一个技巧先跑小样本再跑全量。100 个任务跑一轮 40 分钟如果任务定义有 bug跑完才发现就浪费了。我现在的习惯是先跑 10 个任务验证流程确认没问题再跑全量。这个习惯帮我省了大量时间。这套框架我用了大半年从最初的手忙脚乱到现在跑一轮评测基本不用盯着中间改了很多版。它不完美但足够真实、足够可复现能帮我在一堆宣传里找到真正能用的模型。如果你也在做 Agent 选型建议别信宣传页自己搭一套跑一遍数据会告诉你答案。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进