智能体测试策略:单元测试、集成测试与模拟LLM实战指南
1. 项目概述:为什么智能体测试是“新基建”?
最近和几个做AI应用的朋友聊天,发现一个挺普遍的现象:大家花大量时间调教智能体(Agent)的提示词(Prompt),让它能理解复杂指令、调用各种工具,但一旦功能上线,稍微改点东西或者换个模型,整个系统就变得“神经兮兮”,要么不干活,要么乱干活。排查起来更是噩梦,你根本不知道是提示词写飘了,还是工具API挂了,或者是LLM(大语言模型)今天“心情不好”。这感觉就像盖了一栋摩天大楼,却没做任何结构安全测试,全凭感觉,心里能不慌吗?
“智能体测试策略:单元测试、集成测试与模拟LLM”这个标题,戳中的正是这个痛点。它不是一个简单的工具使用教程,而是一套保障智能体应用稳定、可靠、可迭代的工程化方法论。简单说,就是把我们熟悉的软件工程测试思想——单元测试、集成测试——引入到AI应用开发中,并针对LLM的不确定性,创造性地引入“模拟LLM”这个关键角色。这相当于为智能体开发了一套“体检”和“消防演习”流程。
这套策略适合谁?如果你是正在或计划开发基于LLM的智能体、聊天机器人、自动化工作流的开发者、产品经理或技术负责人,那么这篇文章就是为你准备的。无论你是用LangChain、LlamaIndex、Dify、Coze还是自研框架,测试的核心逻辑是相通的。它能帮你从“提示词玄学”和“上线祈祷”的初级阶段,走向“代码化、自动化、可验证”的工业化生产阶段。
2. 智能体测试的独特挑战与核心思路
在传统软件开发中,测试相对“单纯”。一个函数,输入固定的参数,输出确定的结果,断言(Assert)很容易写。但智能体测试完全是另一个维度的挑战,理解这些挑战是设计有效策略的前提。
2.1 智能体系统的核心不确定性来源
智能体的“智能”源于LLM,而LLM的本质是一个概率模型。这就带来了几个根本性的测试难题:
- 非确定性输出 :同一个提示词,LLM每次生成的结果可能在语义上相似,但字面表达几乎不可能完全一致。“请介绍北京”可能这次以历史开头,下次以地理开头。传统的字符串完全匹配断言(
assert output == “xxx”)基本失效。 - 复杂上下文与状态 :智能体往往是多轮对话的,它有记忆(Memory),有内部状态(State)。测试不能只针对单次输入输出,而要模拟完整的对话流,验证状态转换是否正确。
- 外部工具调用的集成 :智能体的能力边界通过调用外部工具(Tool/API)来扩展。测试需要覆盖:智能体是否在正确的时机、以正确的参数调用了正确的工具?工具返回结果后,智能体是否能正确解析并融入后续响应?
- 高昂的成本与延迟 :直接调用真实的LLM API(如GPT-4)进行频繁测试,成本高昂且速度慢,无法支撑快速迭代和持续集成(CI)。
2.2 三层测试策略的总体设计
为了系统性地应对这些挑战,我们借鉴并改造经典测试金字塔,为智能体设计一个三层测试策略:
-
基础层:单元测试(Unit Testing) 。目标是验证智能体各个“器官”的功能是否正常。 这层的核心是“隔离” 。我们把智能体拆解成相对独立的组件进行测试,例如:
- 提示词模板 :填充变量后,生成的最终提示词格式是否正确?
- 输出解析器(Output Parser) :给定一段LLM的回复,是否能正确解析成结构化的数据(如JSON、Pydantic对象)?
- 工具(Tool) :单个工具的函数逻辑是否正确?输入输出是否符合预期?
- 模拟LLM(Mock LLM) :在这一层被大量使用,用可控的、确定的“假LLM”来替代真实LLM,使测试快速、稳定、零成本。
-
中间层:集成测试(Integration Testing) 。目标是验证智能体各个“器官”组装起来后,能否协同工作。 这层的核心是“连接”与“流程” 。我们测试:
- 智能体链(Agent Executor) :给定一个用户问题,智能体是否能规划正确的思考步骤(Reasoning),调用正确的工具序列,并最终生成合理的回答?
- 工具调用集成 :模拟工具的执行(或调用测试环境的Mock API),验证智能体对工具返回结果的处 理逻辑。
- 记忆(Memory)集成 :在多轮对话测试中,验证智能体是否能正确存储和提取上下文信息。
-
顶层:端到端测试(E2E Testing) & 模拟LLM验证 。这层会有限度地使用真实LLM或经过精心校准的模拟LLM,在更接近生产的环境中进行场景化测试。例如,用少量真实查询测试关键用户旅程,或用一套评估标准(评估器,Evaluator)对智能体的综合表现进行打分。
一个核心洞见 :在这个金字塔中,“模拟LLM”并非仅仅属于某一层,而是贯穿整个测试体系的“血液”。在单元和集成测试中,我们使用完全可控的Mock LLM来保证测试的确定性和效率;在需要验证LLM交互真实性的环节,我们使用经过配置的、能模拟真实LLM某些行为特征的模拟LLM。
3. 单元测试实战:从提示词到工具解析
单元测试是稳定性的基石。我们用一个具体的智能体功能为例:一个“天气查询智能体”。它有一个提示词模板,一个用于解析LLM回复以决定是否调用天气工具的Output Parser,以及一个模拟的天气工具。
3.1 测试提示词模板
提示词是智能体的“灵魂指令”,但其构造常常是字符串拼接,容易出错。
# weather_agent/prompts.py
from langchain.prompts import PromptTemplate
WEATHER_AGENT_PROMPT = PromptTemplate.from_template(
“””你是一个天气助手。用户可能会询问天气。
如果你认为用户想查询天气,且信息完整(有城市名),就调用天气查询工具。
用户问题:{user_input}
你的思考:”””
)
# test_prompts.py
def test_weather_prompt_template():
# 准备测试输入
test_input = “北京今天天气怎么样?”
# 执行:填充模板
filled_prompt = WEATHER_AGENT_PROMPT.format(user_input=test_input)
# 断言:验证填充后的提示词包含关键部分
assert test_input in filled_prompt
assert “天气助手” in filled_prompt
assert “{user_input}” not in filled_prompt # 确保变量已被替换
# 可以更细致地检查格式,例如是否以“你的思考:”结尾
assert filled_prompt.strip().endswith(“你的思考:”)
print(“提示词模板测试通过。”)
注意 :测试提示词的重点不是内容本身(那是LLM的事),而是 格式和变量替换 。确保没有拼写错误、变量名正确、模板渲染后符合预期结构。这能避免运行时因提示词格式错误导致的诡异问题。
3.2 测试输出解析器
这是单元测试的重中之重。解析器决定了LLM的自由文本输出如何被转化为智能体的结构化决策。
# weather_agent/parsers.py
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import Optional
class AgentDecision(BaseModel):
“””智能体决策模型”””
needs_tool_call: bool = Field(description=“是否需要调用工具”)
tool_name: Optional[str] = Field(description=“如果需要调用工具,工具的名称”)
tool_input: Optional[dict] = Field(description=“调用工具的输入参数”)
reasoning: str = Field(description=“智能体的思考过程”)
decision_parser = PydanticOutputParser(pydantic_object=AgentDecision)
# test_parsers.py
def test_decision_parser_success():
# 模拟一个理想的LLM回复,严格遵循解析器要求的格式
llm_response = “””{
“needs_tool_call”: true,
“tool_name”: “get_weather”,
“tool_input”: {“city”: “北京”},
“reasoning”: “用户明确询问北京天气,需要调用天气查询工具。”
}”””
# 执行解析
decision = decision_parser.parse(llm_response)
# 断言解析结果符合预期
assert decision.needs_tool_call is True
assert decision.tool_name == “get_weather”
assert decision.tool_input == {“city”: “北京”}
assert “北京天气” in decision.reasoning
print(“成功解析测试通过。”)
def test_decision_parser_failure_recovery():
# 模拟一个格式略有瑕疵或部分缺失的LLM回复(更接近现实)
llm_response = “””我需要调用工具。工具是get_weather,城市是上海。因为用户问上海天气。”””
# 注意:这个回复不是标准JSON,解析器应该抛出异常或尝试修复
try:
decision = decision_parser.parse(llm_response)
# 如果解析器有修复逻辑,这里可能成功
print(f“解析器尝试修复后结果:{decision}”)
except Exception as e:
# 预期会抛出解析错误
print(f“预期内的解析失败:{type(e).__name__}”)
# 这里可以测试你的错误处理或重试逻辑
# 例如:记录日志、尝试用更宽松的解析方式、返回一个默认决策等
实操心得 :测试解析器时,要覆盖三种情况:1) 完美情况 :LLM完全按格式输出。2) 常见瑕疵 :LLM输出基本正确但格式稍有偏差(如多余换行、键名大小写)。3) 完全错误 :LLM“胡说八道”,解析器应有健壮的错误处理机制(如抛出可捕获的异常,而不是让整个程序崩溃)。很多智能体不稳定,问题就出在解析器对“坏数据”的处理上。
3.3 测试工具(Tool)本身
工具是智能体与外界交互的桥梁,其本身的可靠性必须保证。
# weather_agent/tools.py
import requests
from typing import Dict
def get_weather(city: str) -> str:
“””查询城市天气。这是一个模拟工具。”””
# 在实际项目中,这里会调用真实的天气API
# 为了测试,我们模拟一个响应
weather_data = {
“北京”: “晴,15-25°C,微风”,
“上海”: “多云,18-28°C,东南风3级”,
“广州”: “雷阵雨,25-32°C,南风4级”,
}
if city in weather_data:
return weather_data[city]
else:
return f“未找到{city}的天气信息。”
# test_tools.py
def test_get_weather_tool_success():
result = get_weather(“北京”)
assert “晴” in result
assert “15-25” in result
print(“工具成功调用测试通过。”)
def test_get_weather_tool_failure():
result = get_weather(“不存在的城市”)
assert “未找到” in result
print(“工具失败处理测试通过。”)
注意 :工具测试应独立于智能体。如果工具调用真实API,在单元测试中应该使用
unittest.mock来模拟网络请求,避免对外部服务的依赖,保证测试速度和稳定性。例如,用@patch(‘requests.get’)来模拟返回预设的天气数据或网络错误。
4. 集成测试实战:组装智能体并验证工作流
单元测试确保零件合格,集成测试则要看组装好的机器能否运转。这里我们使用LangChain的测试工具和模拟LLM。
4.1 配置模拟LLM(Mock LLM)
模拟LLM是集成测试的“发动机”。我们需要它根据输入提示词,返回我们预设的、用于驱动测试流程的响应。
# tests/conftest.py 或测试准备部分
from langchain.llms.fake import FakeListLLM
from unittest.mock import Mock
def create_mock_llm_for_weather_agent():
“””
创建一个模拟LLM,其响应序列模拟了智能体在天气查询场景下的理想对话。
响应顺序必须与智能体执行过程中的调用顺序完全匹配。
“””
# 第一个响应:智能体收到用户输入后的“思考”,决定调用天气工具
response_1 = “””{
“needs_tool_call”: true,
“tool_name”: “get_weather”,
“tool_input”: {“city”: “北京”},
“reasoning”: “用户询问北京天气,需要调用天气查询工具。”
}”””
# 第二个响应:智能体收到工具返回结果后,组织最终答案给用户
response_2 = “根据查询结果,北京今天天气晴朗,气温15到25摄氏度,微风。是个好天气!”
# FakeListLLM 会按顺序返回列表中的响应
mock_llm = FakeListLLM(responses=[response_1, response_2])
return mock_llm
4.2 构建测试用智能体并执行集成测试
现在,我们用模拟LLM、真实的解析器和工具,组装一个测试环境下的智能体。
# test_integration.py
from langchain.agents import AgentExecutor, create_react_agent
from langchain.prompts import PromptTemplate
from .parsers import decision_parser, AgentDecision
from .tools import get_weather
from .conftest import create_mock_llm_for_weather_agent
def test_weather_agent_integration():
# 1. 准备组件
mock_llm = create_mock_llm_for_weather_agent()
tools = [Tool(name=“get_weather”, func=get_weather, description=“查询城市天气”)]
# 2. 使用ReAct范式创建智能体
# 注意:这里需要根据你的解析器自定义AgentOutputParser
from langchain.agents import AgentOutputParser
class CustomOutputParser(AgentOutputParser):
def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
# 这里调用我们之前单元测试过的 decision_parser
decision = decision_parser.parse(llm_output)
if decision.needs_tool_call:
return AgentAction(tool=decision.tool_name, tool_input=decision.tool_input, log=decision.reasoning)
else:
return AgentFinish(return_values={“output”: decision.reasoning}, log=decision.reasoning)
agent_prompt = WEATHER_AGENT_PROMPT # 使用之前定义的提示词
agent = create_react_agent(llm=mock_llm, tools=tools, prompt=agent_prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True) # verbose=True 方便调试
# 3. 执行测试:用户查询天气
user_input = “北京今天天气怎么样?”
try:
result = agent_executor.invoke({“input”: user_input})
final_output = result[“output”]
# 4. 关键断言
# 断言1:最终输出包含天气信息(来自我们预设的mock LLM第二次响应)
assert “晴朗” in final_output or “15到25” in final_output
# 断言2:可以通过mock或回调机制验证工具确实被以正确参数调用了一次
# 这里简化处理,在实际中你可能需要Mock工具函数并检查其调用情况
print(f“智能体最终输出:{final_output}”)
print(“集成测试通过:智能体成功接收问题、决定调用工具、处理工具结果并生成回答。”)
except Exception as e:
pytest.fail(f“智能体执行流程失败:{e}”)
注意事项 :
- 响应序列必须精确匹配 :
FakeListLLM按顺序消耗响应。如果智能体的执行步骤(思考->行动->观察->再思考…)比预设的响应多,测试会因没有更多响应而失败。你需要仔细设计测试场景,确保模拟的对话流与响应序列匹配。- 验证工具调用 :更严谨的做法是使用
unittest.mock来替换真实的get_weather函数,并用assert_called_once_with(city=“北京”)来验证调用参数。这确保了智能体不仅走了流程,还发出了正确的指令。- 测试多轮对话 :集成测试还应覆盖多轮场景。你需要扩展模拟LLM的响应列表,模拟用户追问、切换话题等,并验证智能体的记忆管理是否正常。
5. 模拟LLM的进阶应用与评估
除了 FakeListLLM 这种完全预设响应的模拟,还有更高级的模拟方式,用于应对复杂场景。
5.1 可配置的模拟LLM(Configurable Mock LLM)
有时我们需要模拟LLM的某些行为特征,比如偶尔不按格式输出、在特定关键词下触发特定回答等。
from typing import Any, List, Optional
from langchain.callbacks.manager import CallbackManagerForLLMRun
from langchain.llms.base import LLM
class ConfigurableMockLLM(LLM):
“”“一个可配置的模拟LLM,根据输入内容返回动态响应。”””
# 可以配置一个响应映射字典
response_map: dict = {“你好”: “你好!我是助手。”, “天气”: “{‘needs_tool_call’: true}”}
# 或者配置一个默认响应
default_response: str = “我不知道如何回答这个问题。”
def _call(self, prompt: str, stop: Optional[List[str]] = None, run_manager: Optional[CallbackManagerForLLMRun] = None, **kwargs: Any) -> str:
# 简单的关键词匹配逻辑
for key, response in self.response_map.items():
if key in prompt:
return response
return self.default_response
@property
def _llm_type(self) -> str:
return “configurable-mock”
# 使用示例
mock_llm = ConfigurableMockLLM(
response_map={
“用户问天气”: “{‘needs_tool_call’: true, ‘tool_name’: ‘get_weather’}”,
“工具返回晴朗”: “今天天气很好,适合出行。”
}
)
# 当prompt包含“用户问天气”时,返回第一个响应,驱动工具调用。
5.2 用于评估的模拟LLM与评估器(Evaluator)
在更高层次的测试中,我们不仅关心流程,还关心输出质量。这时可以结合模拟LLM和评估器。
from langchain.evaluation import EvaluatorType, load_evaluator
from langchain.evaluation import StringEvaluator
# 1. 使用一个“裁判”LLM(可以是另一个模拟LLM,也可以是轻量级真实LLM如GPT-3.5-turbo)来评估
criteria = {“helpfulness”: “回答是否对用户有帮助且准确”, “conciseness”: “回答是否简洁”}
evaluator = load_evaluator(EvaluatorType.LABELED_CRITERIA, criteria=criteria, llm=mock_llm_as_judge)
# 2. 在测试中评估智能体输出
def test_agent_output_quality():
agent_output = “北京今天晴,15-25°C。” # 假设这是智能体的最终输出
ground_truth = “北京天气晴朗,气温在15至25摄氏度之间。” # 预期的理想答案
eval_result = evaluator.evaluate_strings(prediction=agent_output, reference=ground_truth, input=“北京天气?”)
# eval_result 可能包含分数和理由
assert eval_result[“score”] > 0.8 # 设定一个质量阈值
print(f“输出质量评估通过,得分:{eval_result[‘score’]},理由:{eval_result[‘reasoning’]}”)
实操心得 :评估器非常有用,但要注意“裁判”LLM本身的偏差。在关键业务场景,最好结合多种评估方式:自动化评估器(用于CI)、人工抽查(定期进行)、线上A/B测试指标(如用户满意度、任务完成率)。完全依赖一个LLM来评估另一个LLM,有时会陷入循环。
6. 将测试融入开发流程:CI/CD实践
测试只有自动化并融入流程,才能发挥最大价值。
6.1 目录结构建议
your_agent_project/
├── src/ # 源代码
│ ├── agents/ # 智能体定义
│ ├── tools/ # 工具函数
│ ├── prompts/ # 提示词模板
│ └── parsers/ # 输出解析器
├── tests/ # 测试代码
│ ├── unit/ # 单元测试
│ │ ├── test_prompts.py
│ │ ├── test_parsers.py
│ │ └── test_tools.py
│ ├── integration/ # 集成测试
│ │ └── test_agent_workflow.py
│ ├── conftest.py # 共享的测试夹具(如mock_llm)
│ └── evaluation/ # 评估测试
│ └── test_quality.py
├── requirements.txt # 项目依赖
├── requirements-test.txt # 测试额外依赖
└── .github/workflows/ # CI/CD 配置文件
└── test.yml
6.2 CI流水线配置示例(GitHub Actions)
# .github/workflows/test.yml
name: Agent CI Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ‘3.10’
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install pytest pytest-cov
- name: Run unit tests
run: |
pytest tests/unit/ -v --cov=src --cov-report=xml
- name: Run integration tests (with mocked LLM)
run: |
pytest tests/integration/ -v
env:
# 如果测试中需要用到API Key,在此处注入Mock或测试环境的Key
OPENAI_API_KEY: “dummy_key_for_testing”
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
6.3 测试数据管理与维护
随着智能体功能变多,测试用例(尤其是模拟LLM的预设响应)会膨胀。建议:
- 将测试用例数据化 :用JSON或YAML文件管理不同场景下的输入和预期输出。
- 使用夹具(Fixtures) :Pytest的
@pytest.fixture可以优雅地复用复杂的测试准备代码,如创建特定配置的智能体。 - 定期更新 :当提示词或工具逻辑变更时,同步更新相关的测试用例和模拟响应。
7. 常见问题、排查技巧与避坑指南
在实际操作中,你会遇到各种坑。以下是一些典型问题及解决思路。
7.1 模拟测试通过,但对接真实LLM后失败
- 问题 :所有基于Mock LLM的测试都通过了,一换到GPT-4,智能体就行为异常。
- 排查 :
- 提示词工程 :Mock LLM的响应是你预设的“完美答案”。真实LLM可能无法理解你的提示词指令。检查提示词是否清晰、有无歧义、是否包含了足够的示例(Few-shot)。
- 输出格式 :这是最常见的问题。你的解析器要求严格的JSON,但真实LLM可能输出Markdown、多余的解释文本。 强化你的提示词 ,使用
json …包裹,或选择支持“结构化输出”的模型/API。 - 容错处理 :在解析步骤增加重试和降级逻辑。例如,第一次解析失败后,尝试用正则表达式提取关键信息,或让LLM重新生成。
7.2 集成测试中工具调用验证失败
- 问题 :测试认为工具应该被调用,但Mock工具函数显示未被调用或参数错误。
- 排查 :
- 使用
unittest.mock进行间谍活动 :不要只依赖流程通顺,用patch和spy来监控工具函数是否真的被调用。from unittest.mock import patch, MagicMock def test_tool_call_verification(): with patch(‘weather_agent.tools.get_weather’, MagicMock(return_value=“模拟天气”)) as mock_tool: # 执行智能体 agent_executor.invoke({“input”: “…”}) # 验证 mock_tool.assert_called_once_with(city=“北京”) - 检查工具描述 :LangChain等框架中,智能体根据工具的描述(description)来决定是否调用。确保描述准确、包含关键动词和名词。
- 使用
7.3 多轮对话测试中状态混乱
- 问题 :测试单轮对话正常,但测试多轮时,智能体忘记之前的内容或给出矛盾回答。
- 排查 :
- 检查记忆(Memory)后端 :在测试中,确保记忆组件(如
ConversationBufferMemory)在每次测试开始时是 全新 的。避免测试间的状态污染。 - 验证记忆键(Keys) :确认对话历史被正确存储在记忆的哪个键下,以及提示词模板中是否正确引用了这个键(如
{chat_history})。 - 模拟LLM的响应需包含上下文感知 :在设计多轮测试的模拟LLM响应列表时,后一个响应应该体现出它“看到”了前一轮的对话历史。这需要你精心设计响应内容。
- 检查记忆(Memory)后端 :在测试中,确保记忆组件(如
7.4 测试执行速度慢或成本高
- 问题 :即使用了Mock,集成测试还是很慢;或者有些测试不得不调用真实LLM,成本受不了。
- 解决 :
- 分层测试,隔离昂贵操作 :单元测试必须快(秒级),全部用Mock。集成测试大部分用Mock。只有少数核心的端到端测试或评估测试,才使用真实LLM,并且可以配置为只在主分支合并或发布前触发。
- 使用廉价模型 :在必须用真实LLM的测试中,使用成本更低的模型,如
gpt-3.5-turbo而不是gpt-4,或使用本地部署的小模型。 - 缓存LLM响应 :使用像
VCR.py这样的库,在第一次运行测试时记录下真实LLM的请求和响应,后续测试直接回放,不再产生实际调用和费用。
7.5 提示词变更导致大量测试失败
- 问题 :稍微优化一下提示词,几十个测试用例因为字符串匹配而失败。
- 解决 :
- 不要断言具体的提示词字符串 :单元测试提示词时,断言核心指令和变量存在即可,不要断言整个字符串。或者使用提示词模板的
template属性进行比对。 - 使用语义化断言 :对于LLM输出或最终答案的断言,避免精确字符串匹配。改用:
- 关键词断言 :
assert “晴朗” in output - JSON结构断言 :使用
json.loads()解析后,断言关键字段的值。 - 使用评估器 :用另一个LLM(或规则)来判断输出是否“语义正确”。
- 关键词断言 :
- 将提示词作为测试数据 :将提示词模板本身存储在文件中,测试时加载它。这样,提示词的修改只需要更新文件,测试代码无需改动。
- 不要断言具体的提示词字符串 :单元测试提示词时,断言核心指令和变量存在即可,不要断言整个字符串。或者使用提示词模板的
智能体测试不是一劳永逸的,它是一个随着智能体能力增长而不断演进的过程。核心在于建立一种“可测试”的开发文化:每增加一个新工具,就为它写单元测试;每实现一个新的工作流,就为它写集成测试。一开始可能会觉得繁琐,但当你需要重构提示词、升级模型版本或增加复杂功能时,这套测试套件将成为你最可靠的“安全网”,让你有信心进行快速迭代,而不是在黑暗中摸索。
更多推荐


所有评论(0)