尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Harness-Only Benchmark:如何独立评估Agent执行层与工具调用质量

Harness-Only Benchmark:如何独立评估Agent执行层与工具调用质量 同样是 DeepSeek 模型有人在本地搭 Agent 工程时一连串工具调用非常顺畅有人沿用另一套执行框架任务却频繁卡在参数解析、上下文叠加和工具返回处理上。很多人的第一反应是模型推理能力不足但把同一个模型切到另一套 harness 里结果又完全不同。于是我们不得不面对一个问题日常跑 benchmark 时到底是在评估模型还是在评估 harness最近 Hacker News 上有一个讨论标题是“Ask HN: Anyone interested in building a harness-only benchmark?”正好戳中这个痛点。社区里讨论度很高的 deepseek harness 工程实践本质上也是围绕“如何把模型接入工具链并跑稳 Agent 流程”展开的。本文不会去复述那条帖子的每一条回答而是把它当成一个起点系统梳理什么是 harness-only benchmark、为什么要单独做、以及如何用最少代码搭一个可扩展的评测框架。1. 先从概念说起什么是 Harness1.1 通俗理解模型的操作间与脚手架在 LLM Agent 开发里模型本身只是一个“能读能写”的处理器。你给它一段提示词它返回一段文本。但真实业务不能让模型只输出文本而是需要它去查天气、算价格、操作数据库、读取文件、调用第三方 API。这些“调用外部系统并返回可执行结果”的能力不是模型天然具备的。Harness 就是连接模型和外部工具的中间执行层。你可以把它理解成一个“操作间”模型负责思考决定下一步要做什么Harness 负责行动把模型的想法翻译成具体的工具调用Harness 还负责把工具返回的结果重新拼成模型能理解的上下文。如果没有 harness模型就算知道该调天气 API也没有办法真正把 API 请求发出去。所以 harness 的质量会直接影响 Agent 任务能否完成。1.2 专业定义执行控制层从工程角度看harness 是介于模型与应用之间的一组运行时组件。它至少承担以下职责组件主要工作指令解析把用户的自然语言任务拆解成结构化的执行步骤工具调度选择合适的工具并把参数映射成工具要求的格式状态管理维护多轮任务中的上下文、中间结果、历史记录结果处理解析工具返回内容转成模型可用的提示词片段异常恢复当工具调用失败时决定重试、换工具还是终止安全边界控制工具执行权限、限制危险操作、管理沙箱简单来说harness 是 Agent 的“脚手架”。模型住在脚手架上干活脚手架不稳模型能力再强也容易摔跟头。1.3 Harness 与 Model、Agent 的关系很多资料把这几个概念混着用这里做一个简单区分Model负责语言理解与生成例如 DeepSeek 模型输入 prompt 输出 token不直接操作外部系统。Harness执行控制层负责工具调用、上下文维护、错误恢复是 Agent 运行时的骨架。Agent通常指模型、harness、工具集和业务策略组合出来的完整实体。所以当我们讨论“Agent 跑得好不好”时实际是模型能力、harness 工程质量、工具实现三者的叠加结果。如果我们只想知道“harness 写得好不好”就必须把模型能力这个变量隔离掉这就引出了 harness-only benchmark 这个方向。2. 为什么要单独给 Harness 做 Benchmark2.1 传统 Agent Benchmark 的盲区目前很多 Agent benchmark 设计成端到端评测给一个任务让“Agent”去完成然后看最终正确率。这类评测对用户选型有帮助但对开发者定位问题帮助有限。原因是任务失败可能是模型理解错意图也可能是 harness 选错工具还可能是工具返回格式怪异harness 没有正确解析甚至可能是重试策略太激进导致上下文被污染。端到端评测只给一个总分开发者很难知道该优化模型还是优化 harness。尤其在开发 deepseek harness 这类工具链时每天改的都是代码逻辑不是模型权重如果用传统 Agent benchmark 验证分数波动会被模型随机性掩盖。2.2 Harness-only Benchmark 的目标“只针对 harness 的基准测试”并不是要取消模型而是采用一种更可控的评测方式固定模型选择或者只用规则模拟模型输出设计一组能体现 harness 工程能力的任务分别统计工具调用正确率、上下文处理能力、错误恢复能力、稳定性等指标。换句话说harness-only benchmark 的目标是回答这样的问题在给定同样工具集和同样任务的前提下哪套 harness 的调度更稳、参数映射更准、错误恢复更合理、资源消耗更低。这样设计后模型选型、prompt 优化和 harness 工程就能分开评估。对做 Agent 框架的人来说这是比“跑一个综合分”更可操作的验收方式。2.3 为什么是现在讨论这个方向原因是 Agent 开发正在从“调模型”走向“调工程”。早期大家只比较模型推理能力后来发现外部工具链的稳定性对任务完成影响极大。社区里开始出现各种为 LLM 服务定制的执行框架也就是各类 harness 工程实践。工具一多评测标准就会自然分化不能只看“模型聪明不聪明”还要看“脚手架稳不稳”。3. Harness-only Benchmark 应该测什么设计一套 harness-only benchmark核心不是堆任务数量而是确定评测维度。下面是我建议的六个维度每个维度都可以量化。3.1 指令解析与工具选择Harness 需要把自然语言指令映射到具体工具。这个环节最容易出错的是多工具场景下选错工具参数缺失或参数格式错误没有识别出指令里的关键实体。对应指标可以是“工具选择准确率”和“参数映射准确率”。评测时我们给 harness 一组只含关键信息的任务例如“查询北京天气”然后检查它是否选择了 get_weather 工具是否正确传入了 city“北京”。3.2 工具调用正确性这里不仅仅看最终结果还要检查调用过程中的中间记录。比如是否多调用了不必要工具是否把价格列表当作字符串拼接是否把文件搜索和文本替换混在一起。对应指标是“单步工具调用准确率”。这一步最容易暴露 harness 的参数解析边界。3.3 上下文管理与多步规划复杂的 Agent 任务往往需要多轮工具调用。Harness 必须把前一轮的工具返回结果拼进下一轮的上下文。如果上下文被截断、覆盖或重复拼接任务就会失败。评测时可以设计需要连续调用两次以上工具的任务检查中间状态是否被正确保留。对应指标是“多步任务成功率”和“上下文溢出率”。3.4 错误恢复与容错工具不可能一直稳定。网络超时、API 返回异常、参数类型不匹配都可能出现。优秀的 harness 应该能根据错误信息决定重试、换参数、换工具还是终止并给用户明确说明。盲目重试会导致无限循环过早终止又会降低任务成功率。对应指标是“异常处理成功率”和“无效重试率”。3.5 稳定性与可复现性同一任务多次执行结果波动大是 harness 工程的大忌。稳定性评测最好在固定模型版本、固定随机种子、固定工具 Mock 数据的条件下进行。可以重复跑 10 次统计成功率的方差。3.6 效率与成本Harness 的调度策略会影响 token 消耗和延迟。调用模型次数越多成本越高工具调用越繁琐响应越慢。效率指标可以统计平均工具调用步数平均模型调用次数平均耗时单任务 token 消耗。这些指标和任务成功率一样重要尤其是在生产环境里一个“每次都成功但多烧三倍 token”的 harness并不一定适合线上使用。4. 实战搭建一个最小的 Harness-only Benchmark 框架下面用 Python 实现一个最小但可扩展的评测框架。它会定义任务协议、工具注册表、Harness 接口和评测统计。你可以在这个基础上接入自己的 harness 实现。4.1 项目结构推荐使用下面的目录结构方便后续扩展harness-benchmark/ ├── benchmark/ │ ├── __init__.py │ ├── task.py │ ├── tools.py │ ├── metrics.py │ └── runner.py ├── harnesses/ │ ├── __init__.py │ └── simple_harness.py ├── tasks/ │ └── demo_tasks.py ├── requirements.txt └── main.py本文不需要第三方依赖使用 Python 3.10 就可以直接运行。4.2 定义任务协议任务协议是整个评测框架的地基。它需要同时表达“用户期望”“可用工具”“黄金工具调用序列”和“最终结果校验方式”。文件benchmark/task.pyfrom dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional dataclass class ToolCall: 预期发生的一次工具调用。 tool_name: str arguments: Dict[str, Any] dataclass class Task: 一条 benchmark 任务。 task_id: str instruction: str available_tools: List[str] expected_tool_calls: List[ToolCall] expected_final_result: Any evaluator: Optional[Callable[[Any, Any], bool]] None def check_final_result(self, actual: Any) - bool: if self.evaluator is not None: return bool(self.evaluator(self.expected_final_result, actual)) return self.expected_final_result actual代码里的expected_tool_calls是“黄金工具调用序列”。评测时我们会把 harness 实际产生的调用记录和它做比较这样不仅能看最终结果对不对还能看过程对不对。4.3 定义 Harness 接口为了让不同 harness 都能接入评测框架我们需要一个抽象基类。每个 harness 只需要实现一个run方法。文件harnesses/init.pyfrom abc import ABC, abstractmethod from typing import Any, Dict, List, Optional from benchmark.task import Task dataclass class ToolCallRecord: 实际产生的一次工具调用记录。 tool_name: str arguments: Dict[str, Any] output: str error: Optional[str] None dataclass class HarnessResult: 一次任务执行的完整结果。 task_id: str tool_call_records: List[ToolCallRecord] final_result: Any is_success: bool steps: int duration_ms: float error: Optional[str] None class Harness(ABC): abstractmethod def run(self, task: Task, tools: List[Any]) - HarnessResult: ...这里有一个细节tools是工具函数列表harness 内部需要把它们登记成“名字到函数”的映射。实际项目中工具注册通常由 harness 自己管理这里为了评测简单统一由外部传入。4.4 实现一个简单的规则 Harness为了演示框架流程我们先写一个不依赖模型的简单 harness。它通过规则从指令中提取关键词选择工具并映射参数。文件harnesses/simple_harness.pyimport time from typing import Any, Dict, List from benchmark.task import Task from . import Harness, HarnessResult, ToolCallRecord class SimpleHarness(Harness): 基于规则的演示版 harness不调用大模型。 def _call_tool( self, tool_name: str, arguments: Dict[str, Any], tool_registry: Dict[str, Any], records: List[ToolCallRecord], ) - Any: if tool_name not in tool_registry: raise KeyError(f工具不存在: {tool_name}) tool_fn tool_registry[tool_name] output tool_fn(**arguments) records.append( ToolCallRecord( tool_nametool_name, argumentsarguments, outputstr(output), ) ) return output def run(self, task: Task, tools: List[Any]) - HarnessResult: tool_registry {fn.__name__: fn for fn in tools} records: List[ToolCallRecord] [] start time.time() error None try: if 天气 in task.instruction: city 北京 if 上海 in task.instruction: city 上海 final_result self._call_tool( get_weather, {city: city}, tool_registry, records ) elif 总价 in task.instruction or 金额 in task.instruction: prices [29.9, 15.5, 8.8] final_result self._call_tool( calc_total_price, {prices: prices}, tool_registry, records ) elif 搜索 in task.instruction or 文件 in task.instruction: files [report-2024.pdf, report-2025.pdf, data.txt] final_result self._call_tool( search_file, {keyword: report, files: files}, tool_registry, records, ) else: raise ValueError(无法识别的指令类型) is_success task.check_final_result(final_result) if final_result is not None else False except Exception as exc: is_success False final_result None error str(exc) duration_ms (time.time() - start) * 1000 return HarnessResult( task_idtask.task_id, tool_call_recordsrecords, final_resultfinal_result, is_successis_success, stepslen(records), duration_msround(duration_ms, 4), errorerror, )注意这个 SimpleHarness 只是为了跑通评测框架。真实场景下你可以在这个位置接入真实的 LLM harness把“解析指令”“规划步骤”“生成参数”替换成模型调用逻辑。例如社区里常用的 deepseek harness 思路就是把 DeepSeek 模型的输出解析成结构化工具调用再由执行模块真正调用工具。本文先不过度展开模型调用保证演示可复现。4.5 定义 Mock 工具为了避免评测过程依赖真实外部 API我们定义三个本地工具。它们的作用是模拟真实工具返回让评测过程离线可跑。文件benchmark/tools.pyfrom typing import List def get_weather(city: str) - str: weather_map { 北京: 晴25℃, 上海: 多云28℃, 广州: 阵雨30℃, } return weather_map.get(city, f暂无 {city} 的天气数据) def calc_total_price(prices: List[float]) - float: return round(sum(float(price) for price in prices), 2) def search_file(keyword: str, files: List[str]) - List[str]: return [file_name for file_name in files if keyword in file_name]在真实项目中这些工具函数可以替换为 HTTP 调用、数据库查询、文件系统操作等。评测时尽量把外部依赖 Mock 掉避免网络不稳定影响结果。4.6 编写评测指标评测指标不能只看最终成功与否还要看“工具调用过程”是否和预期一致。这里实现两个核心指标任务成功率和工具调用准确率。文件benchmark/metrics.pyfrom typing import List from .task import Task, ToolCall from . import HarnessResult, ToolCallRecord def _tool_calls_match(expected: List[ToolCall], actual: List[ToolCallRecord]) - bool: if len(expected) ! len(actual): return False for exp, act in zip(expected, actual): if exp.tool_name ! act.tool_name: return False if exp.arguments ! act.arguments: return False return True def compute_summary(results: List[HarnessResult], tasks: List[Task]) - dict: total len(tasks) success_count sum(1 for r in results if r.is_success) task_map {task.task_id: task for task in tasks} tool_match_count 0 total_tool_calls 0 for result in results: task task_map[result.task_id] total_tool_calls len(result.tool_call_records) if _tool_calls_match(task.expected_tool_calls, result.tool_call_records): tool_match_count 1 summary { total_tasks: total, task_success_rate: round(success_count / total, 4) if total else 0, tool_call_accuracy: round(tool_match_count / total, 4) if total else 0, total_tool_calls: total_tool_calls, avg_tool_calls: round(total_tool_calls / total, 4) if total else 0, avg_duration_ms: round( sum(r.duration_ms for r in results) / len(results), 4 ) if results else 0, } return summary这里把任务成功率和工具调用准确率分成两个指标。有些 harness 最终结果正确但过程中多调用了一次工具这也会被工具调用准确率扣分。这种设计更贴近工程调优的实际需要。4.7 Runner 与入口Runner 负责把任务、工具、harness 组合起来执行并输出结构化结果。文件benchmark/runner.pyfrom typing import Any, List, Tuple from .metrics import compute_summary from .task import Task from . import Harness, HarnessResult def run_harness_on_task( harness: Harness, task: Task, tool_registry: List[Any], ) - HarnessResult: return harness.run(task, tool_registry) def run_benchmark( harness: Harness, tasks: List[Task], tool_registry: List[Any], ) - Tuple[dict, List[HarnessResult]]: results [] for task in tasks: result run_harness_on_task(harness, task, tool_registry) results.append(result) summary compute_summary(results, tasks) return summary, results文件tasks/demo_tasks.pyfrom benchmark.task import Task, ToolCall DEMO_TASKS [ Task( task_idT001, instruction查询北京的天气并告诉我结果, available_tools[get_weather, calc_total_price, search_file], expected_tool_calls[ ToolCall(get_weather, {city: 北京}), ], expected_final_result晴25℃, ), Task( task_idT002, instruction计算订单总价商品价格分别是 29.9、15.5、8.8, available_tools[get_weather, calc_total_price, search_file], expected_tool_calls[ ToolCall(calc_total_price, {prices: [29.9, 15.5, 8.8]}), ], expected_final_result54.2, ), Task( task_idT003, instruction在文件列表中搜索包含 report 关键字的文件, available_tools[get_weather, calc_total_price, search_file], expected_tool_calls[ ToolCall( search_file, { keyword: report, files: [report-2024.pdf, report-2025.pdf, data.txt], }, ), ], expected_final_result[report-2024.pdf, report-2025.pdf], ), ]文件main.pyimport json from benchmark.runner import run_benchmark from benchmark.tools import calc_total_price, get_weather, search_file from harnesses.simple_harness import SimpleHarness from tasks.demo_tasks import DEMO_TASKS TOOL_REGISTRY [get_weather, calc_total_price, search_file] def main() - None: harness SimpleHarness() summary, results run_benchmark(harness, DEMO_TASKS, TOOL_REGISTRY) print( Summary ) print(json.dumps(summary, ensure_asciiFalse, indent2)) print(\n Detail ) for result in results: print(json.dumps({ task_id: result.task_id, success: result.is_success, steps: result.steps, error: result.error, final_result: result.final_result, }, ensure_asciiFalse, indent2)) if __name__ __main__: main()4.8 运行与验证在项目根目录执行python3 main.py预期输出大致如下 Summary { total_tasks: 3, task_success_rate: 1.0, tool_call_accuracy: 1.0, total_tool_calls: 3, avg_tool_calls: 1.0, avg_duration_ms: 0.1875 } Detail { task_id: T001, success: true, steps: 1, error: null, final_result: 晴25℃ } ...要注意一点这里get_weather返回“晴25℃”而 demo task 的expected_final_result也写成“晴25℃”两者相等。实际项目中最终结果的校验通常更复杂建议使用自定义 evaluator而不是依赖字符串完全相等。到这里一个最简 harness-only benchmark 框架已经跑通了。你可以把SimpleHarness替换成自己的 harness 实现也可以继续扩充任务集。这套代码的价值在于把“任务定义”“工具注册”“harness 执行”“指标统计”拆成了清晰协议后续接入新 harness 不需要改动评测逻辑。5. 更进一步如何构造高质量评测集框架只是第一步真正决定 benchmark 价值的是任务集质量。下面是我在实践中的一些建议。5.1 分层设计任务不要只放“简单工具调用”任务至少要分三层L1 基础调用单个工具、单次调用验证指令解析和参数映射。L2 复合任务需要多个工具按顺序调用验证状态管理和上下文拼接。L3 对抗任务工具返回异常、参数缺失、指令存在歧义验证容错和恢复策略。L1 适合快速回归L2 能体现真实业务复杂度L3 是最能暴露 harness 工程短板的层次。很多 harness 在 L1 上跑得不错一到 L3 就频繁崩原因就是缺少异常处理设计。5.2 黄金工具调用序列每个任务除了预期最终结果还应该记录“预期的工具调用序列”。这是因为最终结果一样并不意味着执行过程一样。举一个例子任务计算两个城市温差正确流程先查北京温度再查上海温度最后做减法错误流程只查了北京温度通过模型内部知识猜上海温度得出同样结果。如果只检查最终结果会漏掉这类“作弊”行为。记录黄金工具调用序列能有效识别 harness 是否走了合理路径。5.3 自动化校验器最终结果不能只做字符串相等判断。建议给每个任务配置一个 evaluator允许自定义校验规则。def close_to(expected, actual, threshold0.01): return abs(expected - actual) threshold这样可以适配浮点数、JSON 字段、列表排序等复杂场景。5.4 防止评测集泄漏如果任务内容过于接近真实业务数据模型或 harness 在训练阶段可能“见过”类似问题导致分数虚高。缓解方法包括为评测集生成随机参数每次跑测试时重新采样维护公开开发集和私有测试集核心指标以私有测试集为准。对 harness 来说泄漏的影响比模型小因为 harness 主要依赖结构化逻辑不是靠记忆。但也要防止工具名、参数格式被硬编码进 harness 实现。6. 常见问题与排查思路在搭建和运行 harness-only benchmark 的过程中你可能会遇到下面这些问题。6.1 同一个模型在不同 Harness 中结果波动大问题现象常见原因解决思路模型相同换 harness 后成功率明显下降指令解析或工具调用逻辑不同用 trace 对比两次执行过程差异某一类任务总是失败参数映射规则不匹配检查工具注册表与参数 schema出现这类问题先别急着归因模型。建议给 harness 增加 trace 日志记录每一步的输入输出。对比正确 harness 和错误 harness 的 trace通常能很快定位是参数格式、上下文修剪还是工具选择策略的问题。6.2 工具调用参数格式不统一问题现象常见原因解决思路同一工具有时接收字符串、有时接收对象harness 没有统一 schema为每个工具定义 JSON Schema中文参数被错误编码解析层未做编码处理统一使用 UTF-8并在测试中加中文用例建议在工具注册表里显式声明参数类型harness 在调用工具前先做类型校验。6.3 LLM 调用超时和成本失控问题现象常见原因解决思路单个任务耗时过长模型重试次数过多设置最大步数和最大重试次数token 消耗超出预算harness 反复将完整历史塞入上下文增加上下文摘要机制harness-only benchmark 的好处是你可以先用 mock 模型跑通流程再接入真实模型做压力测试。成本评估建议放在模型接入之后再统计避免开发阶段烧掉大量 token。6.4 评测结果不可复现问题现象常见原因解决思路同一任务多次执行分数不同模型采样引入随机性固定随机种子或设置 temperature0外部 API 返回不稳定评测依赖了网络用 Mock 工具替换外部服务6.5 安全边界问题在真实 Agent 工程中harness 可能会调用删除文件、写数据库、发送消息等敏感工具。Benchmark 任务不能直接把这些操作放到生产环境执行否则会造成不可逆影响。解决方法是在评测环境中使用 Mock 工具对破坏性工具设置沙箱执行前必须经过授权并保留完整审计日志。这一点不仅是工程建议也是生产环境的基本底线。7. 最佳实践与工程建议一套可长期使用的 harness-only benchmark不应该是一次性脚本而应该像单元测试一样持续运行、持续维护。7.1 评测与模型解耦为了让 benchmark 结果稳定建议在 CI 阶段使用“固定模型输出”或“规则模型”。只有当你需要验证真实场景时再切换到真实 LLM。这样既能快速回归 harness 逻辑又能控制成本。7.2 固定依赖版本工具函数、harness 代码、模型版本、prompt 模板都要做版本管理。评测结果如果出现异常波动先检查是不是依赖版本变了。建议在评测结果中记录Python 版本相关依赖版本模型名称与权重版本评测任务集版本。7.3 保存完整 Trace不要只保存一个分数。每个任务执行后把完整的 harness trace 保存成 JSON 文件包含工具调用记录、中间输出、耗时、错误信息。这样当任务失败时你能直接查看是哪一步出了问题。7.4 使用 Mock 工具保证安全Benchmark 中至少 80% 的工具调用应该走 Mock。Mock 工具不是简单的假数据而是模拟真实工具的行为边界包括异常返回、超时、空结果。这样能提前发现 harness 的容错短板。7.5 设置最大执行步数任何任务都要设置最大步数防止 harness 进入死循环。推荐在 Harness 接口中增加max_steps参数超过步数立即终止并标记失败。这也能防止在真实环境中烧光预算。7.6 增量维护任务集任务集不能只增不减。每修复一个 harness bug就补一条对应的回归任务避免同一个问题再次出现。同时定期淘汰过于简单或已经被 harness 写死的任务。7.7 关注成本指标Harness 工程调优很容易只盯着成功率忽略成本。建议在 summary 中同时输出avg_tool_calls和avg_duration_ms。如果一个改动让成功率提升 2%但平均工具调用次数翻倍那这个改动在生产环境的价值要打一个问号。8. 总结与进一步方向Harness-only benchmark 的核心思路很简单把模型能力和执行工程分开打分。它不能代替端到端 Agent 评测但它能帮开发者快速定位问题究竟出在模型还是出在工具链。社区里的 deepseek harness 实践本质上也是在做执行工程优化这类项目尤其需要一套独立的执行层评测标准。如果你正在维护自己的 Agent 框架建议从本文的最小框架开始补上任务集、Mock 工具和 trace 日志慢慢形成一套可回归的本地评测环境。下一步可以考虑接入真实 LLM对比不同模型在相同 harness 下的表现增加并发评测能力验证 harness 在多任务场景下的稳定性把评测结果接入 CI让每次代码提交都自动跑分根据 trace 数据定位失败原因再针对性地优化错误恢复策略。评测框架的价值在于它让优化不再是“凭感觉试”。把模型、工具链、执行策略分开打分之后Harness 工程就有了可量化的迭代方向。如果你也在做 Agent 框架不妨先搭一套 harness-only benchmark很快就能发现自己日常靠人工试错不容易暴露的问题。
返回列表