AI Agents · 课时

在测试中模拟 LLM 调用

unittest.mock、pytest 固件,以及记录和重放 LLM 响应。

第 2 / 4 课13 个步骤

在测试中模拟 LLM 调用 是 CoddyKit 上的免费 AI Agents 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Agents 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Agents 课程共包含 4 节课。

什么是模拟?

模拟是指用一个返回受控响应的虚假版本替代真实函数或对象。在智能体测试中,我们会模拟 LLM 接口调用,使测试能够即时运行、无需成本,并产生可预测的结果。

Python 的 unittest.mock 模块是执行此操作的标准工具。

unittest.mock.patch() 基础

unittest.mock.patch(target) 会在测试期间临时替换指定名称的对象。target 是一个带点号的字符串,指向被测模块中导入该对象时所使用的对象。

from unittest.mock import patch, MagicMock

# The function under test calls openai.chat.completions.create
# We patch it so no real API call is made

def ask_llm(question: str) -> str:
    import openai
    client = openai.OpenAI(api_key='test')
    resp = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': question}]
    )
    return resp.choices[0].message.content

with patch('openai.OpenAI') as mock_client_class:
    mock_instance = MagicMock()
    mock_client_class.return_value = mock_instance
    mock_instance.chat.completions.create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='Paris'))]
    )
    result = ask_llm('Capital of France?')
    print(result)  # 'Paris' — no API call made

在 pytest 中将 patch 用作装饰器

在 pytest 中作为装饰器使用时,@patch() 会将模拟对象注入为函数参数。测试完成后,模拟对象会自动移除。

from unittest.mock import patch, MagicMock
import pytest

# Assume agent.py contains: import openai; client = openai.OpenAI(...)

@patch('agent.openai.OpenAI')
def test_agent_calls_llm(mock_openai_class):
    # Set up the mock chain
    mock_client = MagicMock()
    mock_openai_class.return_value = mock_client
    mock_client.chat.completions.create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='Paris is the capital of France.'))]
    )

    from agent import ask_llm
    result = ask_llm('What is the capital of France?')

    assert 'Paris' in result
    mock_client.chat.completions.create.assert_called_once()

构建可复用的模拟响应

手动构建模拟响应对象十分繁琐。请创建一个辅助函数,用于构建结构正确、符合 OpenAI SDK 响应格式的模拟对象。

from unittest.mock import MagicMock

def make_mock_response(content: str, tool_calls: list = None) -> MagicMock:
    message = MagicMock()
    message.content = content
    message.tool_calls = tool_calls or []

    choice = MagicMock()
    choice.message = message
    choice.finish_reason = 'stop' if not tool_calls else 'tool_calls'

    response = MagicMock()
    response.choices = [choice]
    response.usage = MagicMock(total_tokens=42)
    return response

# Usage in tests:
# mock_create.return_value = make_mock_response('Hello!')
# mock_create.return_value = make_mock_response('', tool_calls=[...])

# --- demo ---
response = make_mock_response('The weather in Paris is 18C and sunny.')
print('content:', response.choices[0].message.content)
print('finish_reason:', response.choices[0].finish_reason)
print('total_tokens:', response.usage.total_tokens)

模拟响应中的工具调用

测试智能体的工具调用逻辑时,模拟响应必须包含结构正确的 tool_calls 字段,以便智能体的解析代码能够正确处理它。

import json
from unittest.mock import MagicMock

def make_tool_call_response(tool_name: str, arguments: dict) -> MagicMock:
    tool_call = MagicMock()
    tool_call.id = 'call_abc123'
    tool_call.type = 'function'
    tool_call.function = MagicMock()
    tool_call.function.name = tool_name
    tool_call.function.arguments = json.dumps(arguments)

    message = MagicMock()
    message.content = None
    message.tool_calls = [tool_call]

    response = MagicMock()
    response.choices = [MagicMock(message=message, finish_reason='tool_calls')]
    return response

# mock.return_value = make_tool_call_response('search_web', {'query': 'Python tutorials'})

# --- demo ---
response = make_tool_call_response('search_web', {'query': 'Python tutorials'})
call = response.choices[0].message.tool_calls[0]
print('tool name:', call.function.name)
print('tool arguments:', call.function.arguments)
print('finish_reason:', response.choices[0].finish_reason)

用于模拟的 pytest 固件

pytest 固件可以让您定义可复用的设置代码。请创建一个用于修补 LLM 客户端的固件,并将其提供给所有请求它的测试,无需重复编写代码。

import pytest
from unittest.mock import patch, MagicMock

@pytest.fixture
def mock_openai(make_mock_response):
    with patch('myagent.client.chat.completions.create') as mock_create:
        mock_create.return_value = MagicMock(
            choices=[MagicMock(message=MagicMock(
                content='Default mocked response',
                tool_calls=[]
            ))]
        )
        yield mock_create

# Now any test can use it:
def test_agent_responds(mock_openai):
    from myagent import agent
    result = agent.run('Hello')
    assert result is not None
    mock_openai.assert_called_once()

来自 pytest-mock 的 mocker 固件

pytest-mock 提供了一个 mocker 固件,可简化修补操作。它会自动清理模拟对象,并且比直接使用 unittest.mock.patch 具有更简洁的语法。

# pip install pytest-mock

# In your test file:
def test_agent_with_mocker(mocker):
    mock_create = mocker.patch('myagent.client.chat.completions.create')
    mock_create.return_value = mocker.MagicMock(
        choices=[mocker.MagicMock(message=mocker.MagicMock(
            content='Mocked answer',
            tool_calls=[]
        ))]
    )

    from myagent import agent
    result = agent.run('What is 2+2?')
    assert 'answer' in result.lower() or '4' in result

    mock_create.assert_called_once()
    # No cleanup needed — mocker handles it

使用 vcr.py 记录和重放

vcrpy 会在首次运行时将真实的 HTTP 交互记录到一个“录制”文件中,之后的运行则重放这些交互。对于测试直接使用原始 HTTP 接口而不是 SDK 的代码,这种方式非常理想。

# pip install vcrpy
import vcr
import httpx

@vcr.use_cassette('fixtures/cassettes/openai_chat.yaml')
def test_with_recorded_response():
    # First run: makes a real HTTP call and records it
    # Subsequent runs: uses the recorded cassette (no network, no cost)
    response = httpx.post(
        'https://api.openai.com/v1/chat/completions',
        json={'model': 'gpt-4o-mini', 'messages': [{'role': 'user', 'content': 'Hello'}]},
        headers={'Authorization': 'Bearer YOUR_KEY'}
    )
    data = response.json()
    assert data['choices'][0]['message']['content'] is not None

验证模拟对象是否被正确调用

测试结束后,请验证模拟对象是否使用正确的参数被调用。这可以发现智能体发送了错误模型、缺少参数或不正确消息等问题。

from unittest.mock import patch, MagicMock, call

@patch('myagent.client.chat.completions.create')
def test_agent_sends_correct_model(mock_create):
    mock_create.return_value = MagicMock(
        choices=[MagicMock(message=MagicMock(content='ok', tool_calls=[]))]
    )

    from myagent import agent
    agent.run('Hello')

    # Verify the mock was called with correct arguments
    mock_create.assert_called_once()
    call_kwargs = mock_create.call_args.kwargs
    assert call_kwargs['model'] == 'gpt-4o-mini'
    assert len(call_kwargs['messages']) >= 1
    assert call_kwargs['messages'][0]['role'] == 'system'

在测试中模拟接口错误

请将模拟对象配置为引发异常,以测试智能体如何处理 LLM 故障。这样可以在不造成真实接口故障的情况下,验证错误处理和重试逻辑。

from unittest.mock import patch
import openai

@patch('myagent.client.chat.completions.create')
def test_agent_handles_rate_limit(mock_create):
    # Simulate a rate limit error
    mock_create.side_effect = openai.RateLimitError(
        message='Rate limit exceeded',
        response=None,
        body=None
    )

    from myagent import agent
    result = agent.run('Hello')

    # Agent should handle this gracefully
    assert result['error'] == 'rate_limit'
    # or
    assert result['retry_after'] is not None

在 conftest.py 中组织模拟固件

请将共享固件放在测试目录根目录下的 conftest.py 中。pytest 会自动发现此文件,并使其中的固件对所有测试文件可用,无需导入。

# tests/conftest.py
import pytest
from unittest.mock import patch, MagicMock

@pytest.fixture(autouse=False)
def mock_llm():
    with patch('myagent.client.chat.completions.create') as mock_create:
        mock_create.return_value = MagicMock(
            choices=[MagicMock(message=MagicMock(
                content='Test response',
                tool_calls=[]
            ))]
        )
        yield mock_create

@pytest.fixture
def mock_search_tool():
    with patch('myagent.tools.search_web') as mock_search:
        mock_search.return_value = [{'title': 'Test', 'url': 'https://example.com'}]
        yield mock_search

知识检查:模拟 LLM 调用

请测试您对智能体测试中模拟技术的理解。

回顾:在测试中模拟 LLM 调用

现在,您已经掌握了编写快速、可靠的智能体单元测试所需的工具:

  • 使用 unittest.mock.patch() 将 LLM 客户端替换为模拟对象
  • 构建符合 SDK 响应格式的可复用模拟响应辅助函数
  • 使用结构正确的 tool_calls 字段模拟工具调用
  • 使用 pytest 固件和 conftest.py 在多个测试之间共享模拟对象
  • 使用 pytest-mock 获得更简洁的语法
  • 使用 vcrpy 记录和重放真实的 HTTP 交互

模拟对象是构建快速、易维护的智能体测试套件的基础。

免费开始

用 AI 导师学习 AI Agents — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
60
课程
239

常见问题解答

「在测试中模拟 LLM 调用」课时是免费的吗?

是的 — 「在测试中模拟 LLM 调用」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Agents 课程的其余内容,请升级到 CoddyKit PRO。 AI Agents 课程共包含 4 节课。

「在测试中模拟 LLM 调用」这节课中我会学到什么?

unittest.mock、pytest 固件,以及记录和重放 LLM 响应。 你通过在浏览器中直接运行的动手代码来练习 AI Agents,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Agents 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Agents 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「在测试中模拟 LLM 调用」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Agents 课中编写并运行代码吗?

能。每节 AI Agents 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 代理测试为何不同
  2. 在测试中模拟 LLM 调用
  3. 基于断言的代理测试
  4. 代理流程的集成测试
← 返回 AI Agents