
在本地跑通一个 27B 规模的开源模型然后把它接到 Agent 能力测试平台上跑完整链路中间踩了不少坑。尤其是小模型在做工具调用、多步规划和记忆保持时表现和大模型差距比想象中明显但也有一套可复现的评估方法。本文会完整拆解这套“小模型 Agent 能力测试天梯”的搭建过程包含本地部署、Agent 框架选型、测试任务设计、评分脚本和常见排错适合正在做本地模型选型、Agent 应用落地或想要系统评估模型能力的开发者参考。1. 背景为什么小模型也要认真测 Agent 能力1.1 从大模型到 Agent 的转变过去两年大家关注的重点主要是“模型能不能回答对”也就是问答、摘要、翻译这类单轮任务。但到了 Agent 阶段模型不再只是生成一段文字而是要在一个循环里反复做决策理解用户目标、拆解子任务、选择工具、读取结果、修正计划、再执行下一步。这种能力差异用传统的 benchmark比如 MMLU、CEval很难测出来。一个在知识问答上拿到高分的模型可能在 Agent 场景里连“先查天气再订车”这种两步任务都跑不通。原因是 Agent 任务更看重指令遵循、结构化输出、工具调用准确性、错误恢复和长上下文的注意力分配这些恰好是小模型最容易拉胯的地方。所以在模型选型阶段不能只看跑分要专门建一套 Agent 能力测试天梯把模型放在真实 Agent 执行链路里用标准任务去压测。1.2 小模型做 Agent 的挑战这里的“小模型”没有绝对定义。本文讨论的是 27B 这个规模它比动辄上百 B 的大模型轻量很多普通单卡工作站就能量化运行但又比 7B、14B 模型的综合能力强不少。在实际测试中27B 模型做 Agent 会面临几个典型挑战。第一是工具调用格式不稳定。小模型在 Function Calling 时经常出现参数名写错、JSON 不合法、多余解释文本混入工具调用结果的情况。这在评测时直接表现为“工具调用失败率高”。第二是多步规划容易断层。两步以内的问题大多能完成但五步以上的任务经常做到第三步就开始偏离原始目标或者重复执行同一个子任务。第三是长上下文下的注意力衰减。Agent 执行过程中会积累工具返回结果、历史消息和中间计划当上下文超过一定长度模型会忽略早期约束甚至把工具输出当成用户指令来执行产生幻觉式行为。第四是错误恢复能力弱。工具执行失败后大模型通常会重新组织计划小模型则容易陷入死循环或者直接对用户说“我做不到”。这些挑战都需要通过结构化的测试来量化而不是凭感觉判断。1.3 本文要解决的问题这篇文章会给出一个可落地的方案包含三层内容。第一层是环境讲解如何在本地用量化方式跑起 Qwen 系列的 27B 模型并暴露成 OpenAI 兼容接口方便后续接入 Agent 测试脚本。第二层是概念把 Agent 开发中容易混淆的 Tool、Skill、Harness、多 Agent 主从模式讲清楚因为评测任务的设计必须基于这些概念。第三层是实战从零搭建一个 Agent 能力测试天梯包含测试任务定义、工具注册、Agent 执行引擎、评分与报告输出最终能对比不同模型版本的 Agent 能力差异。2. 环境准备在本地把 27B 模型跑起来2.1 推理框架选型本地跑 27B 模型主流方案有 llama.cpp、Ollama 和 vLLM 三种。三者定位不同我分别说明。llama.cpp 适合纯 CPU 环境或低显存环境通过 GGUF 量化可以把模型压得很小但并发能力弱适合单机调试。Ollama 是 llama.cpp 的封装安装简单、命令友好适合快速验证模型是否可用也内置了 OpenAI 兼容接口测试脚本接起来很方便。vLLM 适合 GPU 显存充裕、需要高并发吞吐的场景。它的 OpenAI 兼容接口更完整Function Calling 支持也更成熟但环境配置相对复杂对 CUDA 版本有要求。本文的测试天梯场景单机单卡、顺序跑多个测试任务Ollama 和 vLLM 都足够。如果只是验证思路推荐先用 Ollama等测试任务量上来再切 vLLM。2.2 模型量化方案27B 模型原始权重对显存要求很高一般需要量化后再本地运行。量化主要影响显存占用和推理速度也会影响 Agent 场景下的输出稳定性。常见量化格式有三种。GGUF 是 llama.cpp 系列框架的量化格式支持 4bit 到 8bit 的多种等级Ollama 直接支持。做 Agent 测试时推荐优先尝试 Q4_K_M 和 Q5_K_M 这两个等级前者省显存后者质量更稳。GPTQ 和 AWQ 是面向 GPU 推理的量化格式配合 vLLM 使用。两者在批量推理时性能更好但 Agent 场景更多是单请求多轮对话优势不如吞吐测试明显。这里要特别提醒量化等级不是越高越好。Agent 任务对输出格式的敏感度远高于普通问答4bit 低比特量化后模型在 Function Calling 时更容易输出不合法 JSON。我建议在显存允许的前提下尽量用 Q5_K_M 或 6bit 量化稳定性提升明显。2.3 通过 Ollama 启动 OpenAI 兼容服务下面以 Ollama 为例演示如何启动一个 OpenAI 兼容接口。先把模型下载到本地命令如下# 拉取模型实际模型名以你本地 Ollama 里的为准 ollama pull qwen2.5:27b拉取完成后设置 Ollama 允许外部访问并启动服务# Linux / macOS 上设置环境变量后启动 export OLLAMA_HOST0.0.0.0:11434 ollama serve启动后Ollama 默认会在http://localhost:11434/v1暴露一个 OpenAI 兼容接口。也就是说我们可以在 Python 里用openai库直接连接它不需要额外写 HTTP 调用。验证接口是否正常可以执行curl http://localhost:11434/v1/models如果返回 JSON 列表说明接口可用。整个部署思路就是把本地模型包装成一个本地版的“OpenAI 服务”所有下游 Agent 代码不感知底层是 Ollama 还是 vLLM。如果你的场景需要更高并发可以改用 vLLM 启动同款模型。启动命令大致如下但具体参数需要按 vLLM 版本调整python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --served-model-name qwen-local启动后同样会暴露一个 OpenAI 兼容接口。需要留意的是不同框架的 API 参数有差异示例中写的是 vLLM 常见启动方式实际使用时要结合当前版本文档确认。3. Agent 核心概念拆解评测前先厘清边界3.1 Agent 的基本组成在搭建测试天梯之前必须先统一概念。Agent 应用一般由这几部分组成。LLM 是决策核心负责理解目标和决定下一步动作。工具是一组可被模型调用的外部能力比如查天气、执行 SQL、访问文件系统、调用内部 API。通常每个工具对应一个函数定义包含名称、描述、参数列表。执行环境负责真正运行工具代码并把结果返回给模型。记忆模块负责保存对话历史、中间状态和长期偏好让 Agent 在多轮任务中不丢失信息。在测试中我们要关注模型在这几个部分之间的交互能力。模型必须能根据工具描述选择正确的工具、生成正确的调用参数、理解工具返回结果并决定继续执行还是结束任务。3.2 Agent 与 Tool、Skill、Harness 的区别日常交流中很多人把 Tool、Skill、Agent 混着说实际它们有清晰边界。Tool 是单个可执行函数粒度最小。比如“获取当前时间”“计算两数之和”就是两个工具。Skill 是一组具有语义关联的工具组合有时还包含配套的提示词和流程。例如“数据分析技能”可能包含“读取 CSV”“数据清洗”“生成可视化”多个工具外加一套分析流程。Skill 更像可复用的能力包而 Tool 是能力包里的原子操作。Agent 是运行在 LLM 之上、能自主调用 Tool/Skill 来完成目标的完整系统。它在循环中工作感知、决策、行动、观察然后继续感知。Agent 之间也可以协作形成多 Agent 系统。Harness 是承载 Agent 运行的框架或运行环境。它负责 LLM 调用的调度、工具执行、上下文管理、超时控制、日志记录。Harness 不决定“做什么”它决定“怎么跑”。比如 LangChain 的 AgentExecutor、AutoGen 的 GroupChat都可以看作不同形态的 Harness。在做测试天梯时我们测的是 Agent 能力但实际测的是“模型 Harness”的组合效果。同一个模型换一个 Harness测试结果可能差异很大。3.3 多 Agent 主从模式热词里提到的“最新的多 Agent 设计里主从模式本质上是将 subagent 视作另一种 tool 进行调用”这个观点很准确。所谓主从模式就是有一个主 AgentPlanner负责接收用户任务、拆解子任务然后把子任务分发给不同的子 AgentExecutor。主 Agent 并不直接执行工具它把“运行一个子 Agent”当成一次工具调用。这种设计有几个好处。一是职责清晰主 Agent 专注规划子 Agent 专注执行二是上下文隔离子 Agent 的执行过程不会全量塞回主对话降低上下文污染三是便于扩展增加新能力只需增加一个 subagent不需要改主 Agent 的逻辑。但也带来了测试难点。子 Agent 本身也依赖 LLM小模型在主从模式下相当于“双层决策”每一层都可能出错错误会逐层放大。测试任务必须覆盖这种模式否则无法发现模型在主从协作上的短板。4. Agent 能力测试天梯设计4.1 测试维度Agent 能力的测试不能只用一个“能不能完成任务”的二元结果。我建议从五个维度设计测试天梯。维度一是工具选择准确性。任务给出多个工具模型能否选中正确的一个。维度二是参数生成规范性。模型生成的 Function Call 参数是否完整、类型是否正确、命名是否匹配。维度三是多步规划能力。任务需要按顺序调用多个工具模型能否自主规划并逐步执行。维度四是错误恢复能力。设计一个工具执行失败的任务观察模型能否根据报错信息修正计划而不是死循环或直接放弃。维度五是记忆与上下文保持。任务中包含临时约束比如“每步执行前先提醒用户剩余步数”观察模型在多轮后是否遗忘。每个维度设计若干子任务每个子任务对应一个可量化评分标准最终汇总成天梯分数。4.2 测试任务示例下面是一个测试任务的 JSON 示例定义了一个四步任务{ task_id: agent_test_003, category: planning_and_tool_use, description: 用户想知道天气并安排一个会议提醒, expected_steps: [ 调用 get_weather_forecast 获取城市天气, 从天气结果中提取天气状况, 调用 create_reminder 创建提醒, 输出最终总结 ], available_tools: [ get_weather_forecast, get_current_time, create_reminder, send_email ], max_rounds: 8, passing_score: 3 }每个测试用例都带上预期步骤、可用工具、最大轮数和通过分数。评测程序读取这个 JSON把任务注入 Agent然后根据执行过程和最终结果打分。4.3 测试框架结构测试天梯整体可以分为四层。任务层维护测试用例库覆盖不同维度和难度。运行层负责加载任务、创建 Agent 实例、注入上下文并控制执行轮数。观测层记录模型每一步的思考、工具调用、工具结果和最终输出这是打分的依据。评分层根据预期步骤和结果计算分数输出天梯报告。这套结构的好处是任务和代码分离。新增测试任务时只需要在 JSON 里加配置不需要改核心代码。5. 实战搭建本地小模型 Agent 测试脚本5.1 项目结构我在实战中使用的项目结构如下agent-bench/ ├── tasks/ │ └── test_001.json ├── tools/ │ └── custom_tools.py ├── engine/ │ └── agent_engine.py ├── eval/ │ └── scorer.py ├── main.py └── requirements.txttasks存放测试任务配置tools存放 Agent 可调用的工具函数engine是 Agent 执行循环eval是评分模块main.py是入口脚本。5.2 编写模型调用客户端由于 Ollama 暴露了 OpenAI 兼容接口我们可以直接使用openai库。下面是客户端封装负责调用本地模型并支持工具定义# 文件路径engine/agent_engine.py 中的客户端部分 from openai import OpenAI client OpenAI( base_urlhttp://localhost:11434/v1, api_keyollama, # 本地服务不校验 key任意值即可 ) def chat_with_model(messages, toolsNone, modelqwen2.5:27b): kwargs {model: model, messages: messages} if tools: kwargs[tools] tools try: response client.chat.completions.create(**kwargs) return response.choices[0].message except Exception as e: print(f[client error] {e}) return None这段代码把模型调用收敛到一个函数里。测试时只需要替换model名称就能对比不同模型。5.3 编写工具注册与 Function Calling接下来定义工具函数。这里的工具是本地模拟的服务用来测试模型的调用能力# 文件路径tools/custom_tools.py import json import random import datetime def get_weather_forecast(city: str) - str: 模拟天气查询返回一个固定格式的天气信息 weather_list [晴, 多云, 小雨, 阴] weather random.choice(weather_list) return json.dumps({city: city, weather: weather, temperature: 20 random.randint(-5, 5)}, ensure_asciiFalse) def get_current_time() - str: 获取当前时间 return datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S) def create_reminder(content: str, time: str) - str: 创建一个提醒 return json.dumps({status: created, content: content, time: time}, ensure_asciiFalse) TOOL_FUNCTIONS { get_weather_forecast: get_weather_forecast, get_current_time: get_current_time, create_reminder: create_reminder, } TOOL_SCHEMAS [ { type: function, function: { name: get_weather_forecast, description: 查询指定城市的天气情况, parameters: { type: object, properties: { city: {type: string, description: 城市名称例如 北京} }, required: [city] } } }, { type: function, function: { name: get_current_time, description: 获取当前日期和时间, parameters: {type: object, properties: {}} } }, { type: function, function: { name: create_reminder, description: 创建一条提醒事项, parameters: { type: object, properties: { content: {type: string, description: 提醒内容}, time: {type: string, description: 提醒时间} }, required: [content, time] } } } ]工具注册分成两部分TOOL_FUNCTIONS是实际执行时调用的 Python 函数TOOL_SCHEMAS是传给模型的函数描述。模型只能看到描述不能看到实现。5.4 编写 Agent 执行引擎Agent 执行引擎实现一个简单的 ReAct 循环把系统提示词、用户任务和工具定义传给模型检查输出是工具调用还是最终回答。如果是工具调用就执行对应函数把结果作为 tool 消息追加回消息列表然后进入下一轮。# 文件路径engine/agent_engine.py import json from tools.custom_tools import TOOL_FUNCTIONS, TOOL_SCHEMAS SYSTEM_PROMPT 你是一个智能助手请根据用户需求调用工具完成任务。工具调用必须使用标准 function call 格式。 def run_agent(task_description: str, max_rounds: int 8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task_description} ] trace [] for step in range(max_rounds): message chat_with_model(messages, toolsTOOL_SCHEMAS) if message is None: trace.append({step: step, event: error, detail: model call failed}) break tool_calls getattr(message, tool_calls, None) if not tool_calls: # 模型直接返回最终答案 trace.append({step: step, event: final_answer, content: message.content}) return trace for call in tool_calls: fn_name call.function.name args_text call.function.arguments trace.append({step: step, event: tool_call, name: fn_name, arguments: args_text}) try: args json.loads(args_text) if args_text else {} fn TOOL_FUNCTIONS[fn_name] result fn(**args) result_text str(result) except Exception as e: result_text f工具执行失败: {e} trace.append({step: step, event: tool_result, name: fn_name, result: result_text}) messages.append({ role: assistant, tool_calls: [call.dict()], }) messages.append({ role: tool, tool_call_id: call.id, content: result_text }) trace.append({step: timeout, event: max_rounds_exceeded}) return trace这个实现是目前最简可行的一种实际生产环境还需要增加超时控制、并发限制和重试机制。这里有一个值得注意的细节工具执行结果通过role: tool的消息送回模型tool_call_id必须与模型返回的调用 ID 对应。如果这个 ID 对不上模型可能无法理解调用结果。5.5 编写评测与打分逻辑评分模块负责把任务 JSON 里的预期步骤与实际执行轨迹做对比。我采用“关键步骤命中 最终结果检查”的方式打分# 文件路径eval/scorer.py import json def score_task(task: dict, trace: list): expected_steps task.get(expected_steps, []) score 0 details [] for idx, expected in enumerate(expected_steps): hit False for item in trace: if item.get(event) tool_call and item.get(name) in expected: hit True break if hit: score 1 details.append(f步骤{idx 1}: 通过) else: details.append(f步骤{idx 1}: 未完成期望包含 {expected}) # 检查是否出现死循环或超时 if any(item.get(event) max_rounds_exceeded for item in trace): details.append(提示: 达到最大轮数可能陷入死循环) return score, details def load_tasks(tasks_dir: str): import os, glob tasks [] for path in glob.glob(os.path.join(tasks_dir, *.json)): with open(path, encodingutf-8) as f: tasks.append(json.load(f)) return tasks评分设计不需要太复杂重点是可复现。相同任务、相同模型、相同 Harness 的情况下多次运行结果应该一致或接近一致。5.6 运行测试与结果解读入口脚本main.py把上面模块串起来# 文件路径main.py from eval.scorer import load_tasks, score_task from engine.agent_engine import run_agent def main(): tasks load_tasks(tasks) for task in tasks: print(f\n 运行任务: {task[task_id]} ) trace run_agent(task[description], max_roundstask.get(max_rounds, 8)) score, details score_task(task, trace) for detail in details: print(detail) print(f任务 {task[task_id]} 得分: {score}/{len(task[expected_steps])}) if __name__ __main__: main()运行命令python main.py预期输出会包含每一步的工具调用记录和最终得分。如果模型在某个任务上得分偏低可以进一步查看 trace判断是工具选择错误、参数生成错误还是执行到一半放弃。从多次测试结果来看27B 模型在两步工具调用类任务上通常表现不错但在五步以上的规划任务上会出现明显的得分下滑主要丢分点集中在“中间步骤跳过”和“参数格式不合法”两类问题上。6. 常见问题与排查思路6.1 “Agent execution provider did not respond in time”这个报错在 Agent 框架中很常见通常表示执行组件在限定时间内没有返回结果。可能有几种原因。模型推理超时是最常见的一种。27B 模型在 CPU 或低显存环境下单轮推理可能需要几十秒超过了 Agent 框架的默认超时阈值。排查思路是先确认模型推理耗时手动调用一次模型接口观察返回时间。如果确实慢需要调大 Agent 框架的超时配置如果还不行就要考虑换更高量化精度或减少并发。还需要检查工具执行是否卡住。比如工具内部有网络请求或死循环会导致整个 Agent 执行流程挂起。建议给所有工具调用加超时保护避免单点卡死影响测试结果。6.2 显存不足与推理崩溃本地跑 27B 模型最常见的问题是显存不够。现象是启动服务后第一次推理就报 CUDA OOM或者推理几轮后崩溃。遇到这种情况先确认模型量化等级。显存紧张时优先使用 4bit GGUF 量化同时把上下文长度调小。上下文长度对显存占用影响很大Agent 任务默认 8192 就可能吃掉额外几个 GB。另一个容易被忽略的是并发请求。如果测试脚本同时发多个请求单张显卡很容易被撑爆。测试阶段应该把并发设置为 1以串行方式跑完所有任务。6.3 Function Calling 返回格式不稳定小模型在 Function Calling 时偶尔会给出非法 JSON比如参数名加上了中文引号、布尔值写成字符串、数组末尾多逗号。这类错误会导致json.loads失败工具无法执行。排查时先看原始返回内容确认是模型问题还是 SDK 解析问题。如果是模型问题可以在系统提示词里增加格式约束或者在解析失败后把错误信息重新喂给模型让它修正。更稳妥的方案是加一层格式兜底比如用正则提取 JSON 片段或者用json.loads的strict模式逐级解析。但要注意兜底逻辑不能过度依赖毕竟评测目的是暴露模型真实能力。6.4 上下文被快速占满Agent 多轮执行会不断累积消息尤其是工具返回结果很大的时候可能一轮任务就占满上下文窗口。当上下文过长模型可能忽略早期指令导致任务失败。建议在测试框架里加上下文压缩机制比如超过阈值时把旧的工具结果摘要化只保留关键信息。但这会引入额外变量测试时要标注清楚使用了压缩策略否则结果对比不公平。6.5 多 Agent 协作时子任务不收敛主从模式下主 Agent 把子任务发给子 Agent如果子 Agent 执行结果不明确主 Agent 可能反复派发同一任务形成死循环。排查思路是在 trace 中检查是否有重复的工具调用。如果存在反复调用相同工具且入参相同的情况说明模型没有从工具结果中提取到有效信息。可以在工具返回结果中加入更明确的状态字段帮助模型判断任务是否完成。7. 最佳实践与工程建议7.1 本地部署方面在本地部署推理服务时优先固定版本。Ollama、vLLM 乃至模型的版本变化都可能影响 Agent 能力表现。测试报告里必须记录推理框架和模型版本否则结果无法复现。显存不足时不要盲目降低量化精度。建议先压测不同量化等级在同一批 Agent 任务上的得分找精度和速度的平衡点。7.2 Agent 编排方面建议将 Agent 编排逻辑与模型实现解耦。客户端只依赖 OpenAI 兼容接口这样模型可以从 Ollama 切换到 vLLM甚至切换到云端 API评测代码不需要大改。对复杂任务优先拆成多个子 Agent。主 Agent 负责规划和结果汇总子 Agent 负责具体执行。这种模式对小模型更友好因为每个 Agent 承担的上下文压力更小。7.3 测试与评分方面测试任务库要定期更新。Agent 评测天然存在“过拟合”问题模型可能会记住常见任务的格式。建议维护两个任务集一个用于开发调试一个用于最终验收且验收集不能出现在调试过程中。评分要区分“过程得分”和“结果得分”。有些任务即使过程跑偏最终结果也碰巧正确。只统计结果会掩盖模型的规划薄弱点只统计过程又会忽略实际可用性。两者结合更合理。7.4 安全与权限边界Agent 测试如果涉及真实工具调用必须明确权限边界。比如工具能读取文件、执行命令或访问数据库测试环境一定要与生产环境隔离使用最小权限账号。在代码层面所有工具调用都要加白名单校验。测试脚本只允许调用预先注册的本地模拟工具不要直接暴露真实 API。这样可以避免模型在测试中意外触发危险操作。日志记录也很重要。Agent 执行过程中的人工不可读日志要保留完整 trace方便事后审计。尤其在涉及业务数据或用户信息时trace 中可能会包含敏感内容要注意脱敏后再存储。8. 总结与下一步这篇文章从本地部署开始带大家完整搭建了一套针对 27B 小模型的 Agent 能力测试天梯。核心收获可以总结为三点。第一Agent 能力测试必须放在真实执行链路里做传统基准分只能作为参考。通过工具调用、多步规划、错误恢复等维度设计任务才能真正看出小模型在 Agent 场景下的短板。第二本地推理框架和量化方案会直接影响模型表现。同样的模型用不同量化等级跑Function Calling 的稳定性差距很明显。做评测前先把环境固定下来。第三评测脚本要尽量模块化任务、工具、执行引擎、评分逻辑分离方便后续扩展。实际项目中这套结构也可以作为 Agent 应用开发的基础框架继续演进。接下来可以继续尝试的方向包括把测试任务扩展到多 Agent 主从模式验证主 Agent 对 subagent 结果的调度能力或者在上下文压缩策略上做更多实验观察能否改善长任务下的丢分问题。建议先把现有脚本跑通记录一批基线数据再逐步调整测试维度。