很多同学在接触 AI Agent 开发时会发现:写一个 Skill 并不难,难的是怎么测试这个 Skill。尤其是当 Skill 要调用工具、依赖大模型输出、甚至还要处理图片等多模态输入时,传统的测试思路往往派不上用场。本文会围绕 Skill 测试这个主题,拆解 Pytest、Agent 集成测试、LLM + LVM 效果评测这三套方案,并把每一套方案的适用场景、代码示例和坑点都讲清楚。如果你正在做 Agent 开发、Skill 封装或 LLM 应用落地,这篇文章值得收藏。
揭秘 Skill 测试 3 大方案:Pytest + Agent + Skills + LVM + LLM(只讲干货)
1. 为什么 Skill 测试不能照搬传统测试
1.1 什么是 Skill
在 AI Agent 开发语境下,Skill(技能)指的是把某个领域的操作步骤、工具调用、上下文知识和输出规范封装成可复用的能力单元。比如“前端开发 Skill”可以封装 HTML/CSS 代码生成的规则,“数据分析 Skill”可以封装数据读取、清洗、可视化的固定流程。
Skill 和普通函数的区别在于:普通函数的输入和输出都是确定性的,而 Skill 的运行过程往往包含大模型推理、工具调用、外部系统交互,输出结果存在一定的不确定性。这就给测试带来了全新的挑战。
1.2 传统测试与 Skill 测试的核心差异
传统单元测试关注“输入 -> 输出”的确定性断言,比如 assert add(1, 2) == 3。而 Skill 测试至少需要覆盖三个层面:
- 确定性逻辑:Skill 内部的参数校验、状态流转、工具调用参数拼接是否正确。
- 集成链路:Agent 是否能在合适的场景下正确选择并调用 Skill,Skill 是否能和大模型、外部工具正常协作。
- 生成效果:大模型生成的最终结果是否满足用户需求,是否存在幻觉、格式错误、信息缺失。
换句话说,Skill 测试既要测“代码逻辑”,又要测“模型行为”,还要测“人机交互效果”。如果只测其中一层,线上很容易出问题。
1.3 本文的三大方案定位
结合当前 Agent 开发的主流实践,我把 Skill 测试拆成三个可落地的方案:
- 方案一:Pytest 工具级测试,解决 Skill 内部逻辑的确定性问题。
- 方案二:Agent 运行时集成测试,解决 Skill 在真实 Agent 链路中的调用问题。
- 方案三:LLM + LVM 效果评测,解决生成结果质量与多模态场景的验收问题。
这三个方案不是互相替代的关系,而是从底向上逐层覆盖。下面逐一拆解。
2. 环境准备与测试策略设计
2.1 环境说明
本文示例以 Python 3.10+ 环境为基础,测试框架使用 Pytest,Agent 运行时以常见的函数调用式 Agent 为例。实际项目中,你可能使用 LangChain、LlamaIndex、自研 Agent 框架或商业化平台,但测试思路是通用的。
需要准备的依赖包括:
pip install pytest pytest-asyncio如果你需要调用 OpenAI 兼容接口做效果评测,还需要安装对应 SDK:
pip install openai版本需要根据你的项目实际情况调整,本文示例以常见环境为例,重点演示配置思路。
2.2 测试金字塔策略
我的建议是把 Skill 测试设计成三层金字塔:
| 层级 | 测试对象 | 执行频率 | 失败影响 |
|---|---|---|---|
| 第一层:Pytest 单元测试 | Skill 内部函数、工具调用参数 | 每次提交 | 定位最准 |
| 第二层:Agent 集成测试 | Skill 注册、路由、调用链路 | 每次发版 | 发现链路问题 |
| 第三层:LLM/LVM 评测 | 生成结果质量、多模态效果 | 发布前/定期 | 反映用户体验 |
越底层的测试越应该频繁执行,越上层的测试越依赖真实模型,执行成本也越高。将三层结合,才能覆盖 Skill 从实现到交付的全链路质量。
3. 方案一:Pytest 工具级测试
3.1 为什么先用 Pytest 测 Skill
Skill 虽然包含大模型推理,但绝大部分 Skill 仍然有大量确定性逻辑。比如:
- 解析用户输入中的参数。
- 根据参数决定调用哪个工具。
- 拼接工具调用所需的 JSON 请求体。
- 对工具返回结果做后处理。
这些逻辑完全可以用 Pytest 做确定性断言。Pytest 的优势在于 fixture 管理、参数化测试、断言直观,还能和 pytest-asyncio 配合测试异步 Skill。
把 Skill 中的确定性逻辑单独抽出来测试,可以在不消耗 Token、不依赖模型的情况下快速发现问题。这也是所有 Skill 测试方案中最便宜、最稳定的一层。
3.2 先解决 Skill 的可测试性
很多 Skill 不好测,是因为逻辑全写在一个巨大的 prompt 里,或者全写在一个 process 方法中。要想让 Skill 可测试,需要在设计阶段就把“纯逻辑”和“模型调用”分开。
举个例子,下面是一个电商客服 Skill 的简化结构:
# skill.py class CustomerServiceSkill: def __init__(self, llm_client, tool_client): self.llm_client = llm_client self.tool_client = tool_client def parse_intent(self, user_input: str) -> dict: """从用户输入中解析意图,纯逻辑函数,可单测""" if "退款" in user_input: return {"intent": "refund", "keywords": ["退款"]} elif "物流" in user_input: return {"intent": "logistics", "keywords": ["物流"]} else: return {"intent": "unknown", "keywords": []} def build_tool_params(self, intent: str, order_id: str) -> dict: """根据意图构造工具调用参数,纯逻辑函数,可单测""" if intent == "refund": return {"api": "refund", "order_id": order_id} elif intent == "logistics": return {"api": "track", "order_id": order_id} return {} async def run(self, user_input: str, order_id: str): """Skill 主入口,包含模型推理和工具调用""" intent_info = self.parse_intent(user_input) params = self.build_tool_params(intent_info["intent"], order_id) if not params: return await self.llm_client.generate_response(user_input) tool_result = await self.tool_client.call(params) return await self.llm_client.generate_response(tool_result)这样设计之后,parse_intent 和 build_tool_params 都不依赖外部模型和工具,可以直接用 Pytest 做测试。
3.3 Pytest fixture 与参数化实战
下面编写针对上述 Skill 的 Pytest 测试。重点是测试纯逻辑函数,并用 fixture 准备测试数据。
# test_skill.py import pytest from skill import CustomerServiceSkill class MockLLMClient: async def generate_response(self, content): return f"mock response: {content}" class MockToolClient: async def call(self, params): return {"status": "ok", "params": params} @pytest.fixture def skill(): llm_client = MockLLMClient() tool_client = MockToolClient() return CustomerServiceSkill(llm_client=llm_client, tool_client=tool_client) @pytest.mark.parametrize( "user_input,expected_intent", [ ("我想申请退款", "refund"), ("我的订单什么时候发货", "logistics"), ("你好", "unknown"), ], ) def test_parse_intent(skill, user_input, expected_intent): result = skill.parse_intent(user_input) assert result["intent"] == expected_intent def test_build_tool_params_refund(skill): params = skill.build_tool_params("refund", "12345") assert params == {"api": "refund", "order_id": "12345"} def test_build_tool_params_unknown(skill): params = skill.build_tool_params("unknown", "12345") assert params == {}3.4 异步 Skill 测试
如果 Skill 的 run 方法是异步的,可以用 pytest-asyncio 标记测试:
# test_skill_async.py import pytest pytestmark = pytest.mark.asyncio async def test_skill_run_with_refund(skill): result = await skill.run("我想申请退款", "12345") assert "mock response" in result这里的关键是把 MockLLMClient 和 MockToolClient 注入到 Skill 中,测试过程不产生真实模型调用,也不消耗 Token,执行速度非常快。
方案一的核心结论:凡是 Skill 里不依赖模型的逻辑,都应该用 Pytest 做确定性测试。这能让 Skill 的基础质量得到保障,也为上层测试打下基础。
4. 方案二:Agent 运行时集成测试
4.1 集成测试要解决什么问题
单元测试通过只能说明 Skill 内部逻辑正确,但 Agent 系统中还有一个关键环节:Agent 是否能在合适的时机选择这个 Skill,并且把上下文正确传递给它。
很多时候,Skill 单测通过,但放到 Agent 里就失灵。常见原因包括:
- Agent 的意图识别没有触发该 Skill。
- Agent 传给 Skill 的上下文格式不对。
- Skill 执行完没有把结果正确返回给 Agent。
- 多个 Skill 之间存在路由冲突。
这些问题是单测覆盖不到的,必须通过 Agent 运行时集成测试来验证。
4.2 如何设计 Agent 集成测试
集成测试的思路是:构造一个接近真实的 Agent 运行环境,但把外部依赖(LLM、工具、数据库)替换成可控的 Mock 或 Test Double。
下面以一段简化代码演示。
# agent_harness.py class Agent: def __init__(self, skills: dict, llm_client): self.skills = skills self.llm_client = llm_client async def handle(self, user_input: str): # 简化:根据关键词选择 Skill for name, skill in self.skills.items(): if name in user_input: return await skill.run(user_input) return await self.llm_client.generate_response(user_input)对应的集成测试如下:
# test_agent_integration.py import pytest pytestmark = pytest.mark.asyncio from agent_harness import Agent from skill import CustomerServiceSkill class MockLLMClient: async def generate_response(self, content): return f"fallback: {content}" class MockToolClient: async def call(self, params): return {"status": "ok", "params": params} @pytest.fixture def agent(): llm_client = MockLLMClient() tool_client = MockToolClient() skill = CustomerServiceSkill(llm_client=llm_client, tool_client=tool_client) skills_map = {"客服": skill} return Agent(skills=skills_map, llm_client=llm_client) async def test_agent_routes_to_skill(agent): result = await agent.handle("客服,我想申请退款") assert "mock response" in result async def test_agent_fallback_to_llm(agent): result = await agent.handle("今天天气怎么样") assert "fallback" in result4.3 使用记录型 Mock 校验调用顺序
集成测试除了关心返回结果,还要关注 Agent 调用了哪些 Skill、以及调用顺序是否正确。可以使用记录型 Mock 来捕获调用历史:
class RecordingToolClient: def __init__(self): self.calls = [] async def call(self, params): self.calls.append(params) return {"status": "ok", "params": params} async def test_agent_tool_call_order(): tool_client = RecordingToolClient() llm_client = MockLLMClient() skill = CustomerServiceSkill(llm_client=llm_client, tool_client=tool_client) agent = Agent(skills={"客服": skill}, llm_client=llm_client) await agent.handle("客服,我想申请退款") assert len(tool_client.calls) == 1 assert tool_client.calls[0]["api"] == "refund"这种测试可以确保 Skill 在 Agent 中的调用链路符合预期。需要注意的是,集成测试仍然使用 Mock,因此不会触发真实模型,可以在 CI 中快速执行。
4.4 集成测试与 Harness 的区别
热词里经常出现 “harness 和 agent 区别”。简单理解:Agent 是业务系统的一部分,负责处理用户请求;Harness 是测试装置,负责模拟运行环境、注入依赖、记录调用过程。本文中的 Agent 集成测试实际上就是一个轻量级 Harness。在生产项目中,你可以把 Harness 独立成一个测试模块,专门负责 Agent 编排和调用模拟。
集成测试覆盖的是“链路是否正确”,还不足以回答“结果好不好”。这就要用到第三层方案。
5. 方案三:LLM + LVM 效果评测
5.1 为什么要做效果评测
Pytest 和集成测试都验证了流程,但 Skill 的最终输出是面向用户的自然语言内容。如果生成结果质量差,即使流程全通,用户依然不满意。
效果评测要回答的问题包括:
- 生成结果是否准确、完整?
- 是否存在幻觉?
- 是否遵循了 Skill 中定义的格式规范?
- 多模态场景下,模型对图片的理解是否正确?
- 结果是否与预期风格、口径一致?
这类测试无法用 assert 简单判断,因此需要引入“评测集 + 大模型裁判”的机制。
5.2 LLM-as-Judge 的思路
LLM-as-Judge 是用一个大模型作为评审,对被测 Skill 的生成结果进行打分或给出判断结论。这种方案成本适中、扩展性好,适合批量评测。
一个典型的评测流程是:
- 准备一批带标准答案或评分标准的测试用例。
- 用被测 Skill 生成结果。
- 让评审模型根据评分标准对结果打分。
- 汇总评分,得到 Skill 的整体效果指标。
下面是一个简化示例:
# evaluator.py import json from openai import OpenAI client = OpenAI() def evaluate_with_llm(skill_output: str, expected: str) -> dict: prompt = f"""请根据以下标准评审 AI 生成结果。 【期望结果】:{expected} 【实际结果】:{skill_output} 请打分(1-10),并给出简短原因。 只返回 JSON:{{"score": 0, "reason": ""}}""" response = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": prompt}], response_format={"type": "json_object"}, ) return json.loads(response.choices[0].message.content)使用时要根据你实际接入的模型接口调整。对测试用例集循环执行,即可得到平均分和失败用例列表。
5.3 LVM 在多模态 Skill 评测中的作用
LVM(Large Vision Model,大视觉模型)在本语境下指具备图像理解能力的大模型。当 Skill 涉及截图理解、UI 还原、图片内容识别、图表解读等场景时,传统文本评测无法覆盖视觉信息,必须引入 LVM 作为评测辅助。
举个例子:一个“前端开发 Skill”接收 UI 设计图然后生成页面。此时需要评测以下维度:
- 页面布局是否和设计图一致。
- 关键元素(按钮、文字、图片)是否遗漏。
- 颜色、间距、字体是否合理。
这些维度可以通过 LVM 对设计图和生成页面的截图进行对比评分。具体来说,可以让视觉模型分别描述两张图片的结构和内容,再让文本模型对比差异,生成结构化的评审结论。
需要说明的是,LVM 评测目前仍在快速发展中,不同模型的理解能力差异较大。建议在评测环境中固定视觉模型版本,避免因模型升级导致评测结果波动。另外,本文中的 LVM 特指大视觉模型,和 Linux 中的逻辑卷管理(Logical Volume Manager)不是一个概念,读者不要混淆。
5.4 评测集建设
无论是 LLM 评测还是 LVM 评测,都离不开高质量的评测集。评测集应包含:
- 典型用例:覆盖 Skill 最常见的调用场景。
- 边界用例:覆盖输入为空、参数缺失、格式异常等边界情况。
- 困难用例:覆盖复杂任务、长文本、多轮对话、多模态混合输入。
- 标准答案或评分标准:用于给模型评审提供参照。
评测集建议以 JSON 文件维护,这样便于版本管理和自动化执行:
[ { "id": 1, "input": "生成一个带登录表单的首页", "expected": "页面应包含用户名输入框、密码输入框和登录按钮", "category": "frontend" }, { "id": 2, "input": "描述这张图片中的图表趋势", "expected": "应准确描述折线图的上升和下降趋势", "category": "multimodal" } ]效果评测适合在每个版本发布前执行,也可以作为夜间任务定期运行,用来监控模型升级和 prompt 调整对 Skill 质量的影响。
6. 完整实战:一个 Skill 如何跑通三层测试
为了让你更直观地理解三层方案如何协同工作,下面用一个“电商客服退款 Skill”完整走一遍。
6.1 项目结构
skill_demo/ ├── skill.py # Skill 核心实现 ├── agent_harness.py # 简易 Agent 运行时 ├── evaluator.py # LLM 评测逻辑 ├── test_cases.json # 评测集 ├── tests/ │ ├── test_skill.py # 第一层:Pytest 单测 │ ├── test_agent_integration.py # 第二层:集成测试 │ └── test_evaluation.py # 第三层:效果评测6.2 第一层:运行 Pytest 单测
在 skill.py 和 tests/test_skill.py 完成编写后,执行:
pytest tests/test_skill.py -v预期输出应当是全部用例通过。这一层验证的是 Skill 内部解析逻辑和参数构建逻辑。
6.3 第二层:运行 Agent 集成测试
执行:
pytest tests/test_agent_integration.py -v这一层验证 Agent 能否在正确的场景下调用“客服” Skill,同时验证工具调用顺序。
6.4 第三层:运行效果评测
效果评测可以单独执行:
python -m pytest tests/test_evaluation.py -vtest_evaluation.py 的简化实现如下:
# tests/test_evaluation.py import json import pytest from skill import CustomerServiceSkill from evaluator import evaluate_with_llm class MockLLMClient: async def generate_response(self, content): return f"退款申请已受理,订单号:{content}" class MockToolClient: async def call(self, params): return {"status": "ok", "order_id": params["order_id"]} @pytest.mark.asyncio async def test_skill_output_quality(): llm_client = MockLLMClient() tool_client = MockToolClient() skill = CustomerServiceSkill(llm_client=llm_client, tool_client=tool_client) with open("test_cases.json", "r", encoding="utf-8") as f: cases = json.load(f) for case in cases: result = await skill.run(case["input"], "12345") verdict = evaluate_with_llm(result, case["expected"]) assert verdict["score"] >= 7, f"用例 {case['id']} 评分过低:{verdict}"实际项目中,效果评测通常不会作为阻塞性单测频繁运行,因为调用真实模型会带来成本和延迟。更常见的做法是把评分落到数据库或报表,汇总后人工判断是否发版。
7. 常见问题与排查思路
在 Skill 测试落地过程中,很多团队会遇到相同的问题。下面整理一张排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Pytest 测试通过但 Agent 不调用 Skill | 测试只测了函数,没测路由逻辑 | 增加 Agent 集成测试,检查技能路由匹配规则 |
| Agent 调用 Skill 后返回空结果 | Skill 内部异常被吞掉 | 检查 Skill 的异常处理,确保有日志输出兜底结果 |
| LLM 评测分数不稳定 | 评审模型版本不一致或 prompt 没固定 | 固定评审模型版本,统一评分 prompt 和温度参数 |
| 多模态评测结果不可信 | 视觉模型对图像理解能力不足 | 换用更强的 LVM,或增加多模型交叉评审 |
| 测试执行太慢 | 每层测试都调真实模型 | 底层用 Mock,只让最上层评测调用真实模型 |
| 评测集容易过时 | 没有版本管理和职责分工 | 用 Git 管理评测集,每次变更走评审流程 |
| 工具调用报 IO 错误 | 外部服务不稳定或参数异常 | 检查工具服务状态,添加重试机制和超时配置 |
对于热词中提到的“lvm 分区报老是报 io 错误”,如果你的测试环境中确实存在 Linux 逻辑卷管理相关存储问题,请优先检查磁盘空间、RAID 状态和文件系统挂载参数,但这属于基础设施排查范畴,和 AI Skill 测试无关,不要混在一起处理。
8. 最佳实践与工程建议
8.1 设计阶段就要考虑可测试性
Skill 的设计应遵循“逻辑与模型分离”原则。所有不依赖模型推理的部分都抽成普通函数,并保证输入数据是可序列化的结构。这样一来,Pytest 单测和 Mock 注入都会变得非常容易。
8.2 把测试环境与生产环境隔离
集成测试和效果评测应使用独立的 API Key、独立的工具服务实例,避免对生产环境造成污染。涉及退款、删除、写入等危险操作时,测试环境必须使用模拟数据,并且遵循最小权限原则。
8.3 建立三层测试的自动化流水线
我建议在 CI 流程中做如下配置:
- 每次提交:运行 Pytest 单测,要求全部通过。
- 每次发版:运行 Agent 集成测试,保证链路可用。
- 发版前或夜间:运行 LLM/LVM 效果评测,生成质量报告。
这样既能保证速度,又能覆盖质量。
8.4 效果评测要保存历史数据
效果评测一次性的通过率说明不了问题。建议把每次评测的分值、用例 ID、模型版本、prompt 版本都保存下来,形成趋势数据。当 Skill 效果下降时,可以快速定位是模型升级、prompt 修改还是评测集变更导致的。
8.5 注意 Token 成本与评测节奏
LLM/LVM 评测会消耗大量 Token,尤其当评测集规模较大时。可以按用例类型分层抽样:
- 典型用例全量评测。
- 边界用例抽样评测。
- 困难用例人工评审。
这样可以控制成本,同时保持质量监控的有效性。
8.6 不要陷入“测试万能”的误区
测试能提升质量,但不能替代设计。一个 prompt 混乱、函数职责不明的 Skill,无论测试写得多好,线上体验都会打折扣。建议先在设计阶段把 Skill 的输入输出、调用边界、错误处理定义清楚,再用三层测试方案去守护这些契约。