测试你的智能体#

用确定性的模型测试运行时行为,再用少量按需启用的在线评估测试模型质量。它们回答的是不同的问题:“我的工具、路由和清理是否正常工作?”不需要网络请求;“这个模型能否可靠地解决任务?”则需要。

把工具当作 Python 操作来测试#

装饰器会返回一个 AgentTool,因此可以用参数映射来调用它。这样无需让模型选择它,就能检验校验和处理函数:

import pytest
from pydantic import ValidationError
from zett_agent.tools.base import tool

@tool(guidelines="Use for exact integer addition.")
def add(left: int, right: int) -> int:
    """Add two integers exactly."""
    return left + right

@pytest.mark.asyncio
async def test_add() -> None:
    assert await add({"left": 20, "right": 22}) == 42
    with pytest.raises(ValidationError):
        await add({"left": "not-an-integer", "right": 22})

在测试环境中安装 pytest 和 pytest-asyncio。直接调用工具会向外传播异常;而在智能体运行内部,普通的工具异常会变成失败的工具结果。对重要的集成,请同时测试这两种边界情况。

用脚本预设模型的决策#

实现公开的 AgentModel 协议,就能在测试中精确决定模型返回什么。这里没有专门的 mock 智能体 API:真实的运行时仍会恢复历史记录、校验工具、分发事件并进行清理。

class MathModel:
    """Script a model/tool/model round trip without making a network request."""

    retry = RetryOptions(max_retries=0)

    async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
        if isinstance(request.messages[-1], ToolMessage):
            answer = f"Answer: {request.messages[-1].content}"
            yield ModelEvent.text(answer)
            yield ModelEvent.completed(ModelResponse(AssistantMessage(content=answer)))
        else:
            assert any(definition.name == "add" for definition in request.tools)
            yield ModelEvent.reasoning("Use exact arithmetic.")
            yield ModelEvent.completed(
                ModelResponse(AssistantMessage(tool_calls=(ToolCall("sum-1", "add", {"left": 20, "right": 22}),)))
            )

这个适配器先请求 add,然后使用返回的工具结果作答。它的导入、工具、分发器和断言都在 streaming_tools.py 中。成功的适配器必须返回一个包含完整消息的终止 ModelResponse。仅有文本增量并不构成最终答案。关于流的约束条件,参见 实现模型适配器。

断言行为,而非偶然的时序#

对于工具调用工作流,请验证请求中包含预期的工具、处理函数收到正确的参数、模型收到其结果,以及客户端返回预期的答案。对于并发工具,应通过工具调用 ID 进行关联,而不是假设完成顺序。

扩展同一个测试,以覆盖以下情况:

  • 无效参数,以及模型能够从中恢复的处理函数错误。

  • 两个相互隔离的会话,以及重新打开存储后恢复的历史记录。

  • 流的提前关闭、请求超时,以及请求本地的清理。

  • 被拒绝或重复的审批响应,而不只是审批成功。

  • 预算耗尽,以及提供商流在部分输出之后失败。

使用事件、队列或显式的测试屏障来同步并发。避免依赖时序的 sleep。许多这类边界都在 示例 的示例中包含断言。

在没有真实端点的情况下测试真实适配器#

注入 httpx.MockTransport,即可测试内置提供商的请求编码和流式响应处理:

"""Run a real provider adapter against an in-memory HTTP transport."""

import asyncio
import json

import httpx

from zett_agent.client import create_agent
from zett_agent.model import ReasoningEffort, RetryOptions
from zett_agent.providers.openai import OpenAIProvider


def respond(request: httpx.Request) -> httpx.Response:
    assert request.url.path == "/v1/chat/completions"
    payload = json.loads(request.content)
    assert payload["messages"][-1]["content"] == "Hello"
    assert payload["stream"] is True
    chunks = [
        {"choices": [{"delta": {"content": "Hello from a mock endpoint."}}]},
        {"choices": [{"delta": {}, "finish_reason": "stop"}]},
    ]
    body = "".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks)
    return httpx.Response(
        200,
        headers={"content-type": "text/event-stream"},
        content=body + "data: [DONE]\n\n",
    )


async def main() -> None:
    model = OpenAIProvider(
        model="fixture-model",
        api_key="offline-test-key",
        transport=httpx.MockTransport(respond),
        retry=RetryOptions(max_retries=0),
    )
    try:
        client = await create_agent(model, reasoning_effort=ReasoningEffort.OFF)
        reply = await client.run("Hello")
        assert reply.content == "Hello from a mock endpoint."
        print(f"Mock provider reply: {reply.content}")
    finally:
        await model.aclose()


if __name__ == "__main__":
    asyncio.run(main())

这个程序使用真实的 OpenAI 适配器,搭配本地预设的 SSE 响应。它会断言请求路径和用户消息,因此不需要凭据或网络访问。在检出目录中运行 uv run python docs/_examples/provider_mock.py:

Mock provider reply: Hello from a mock endpoint.

下载 provider_mock.py。其他提供商暴露相同的 transport 注入点,但它们的传输格式不同。通过的 mock 测试只能证明你的协议夹具可用,而不能证明线上端点支持所请求的每一项模型特性。

让测试安全且可复现#

文件和 SQLite 数据库请使用临时目录。绝不要让测试构造默认的用户数据存储,或发现个人的 MCP/技能文件。请传入显式的路径、显式的服务器列表和显式的技能根目录。离线测试中不要出现凭据,并单独选择启用在线调用。

可下载的文档程序由 make docs-examples 在禁用网络连接并使用临时工作目录的情况下运行。make check 还会额外检查格式、运行时覆盖率门槛,以及一次无警告的隔离文档构建。

下一步:关于生产环境边界,参见 集成到应用,关于完整的清理测试,参见 取消并复用智能体。