SuperAPI AI 工具集

ag2-testing

测试与质量

查看项目

更新于 10-15 10:11

技能介绍

用途

ag2-testing 用于测试构建 AG2 智能体的代码、自定义 @tool 函数、中间件以及响应模式,适用于任何你不想发起真实 LLM API 调用的测试场景。它通过 ag2.testing 提供的 TestConfig 来替换模型客户端,从而对 LLM 行为进行模拟。

主要能力

模拟 LLM 响应

将 TestConfig 作为智能体的 config 传入,或在单次 ask() 时传入。每个位置参数对应本次 ask() 内下一次 LLM 调用的模拟响应:字符串表示文本回复,ToolCallEvent 表示工具调用分发。示例:

import pytest
from ag2 import Agent
from ag2.testing import TestConfig

@pytest.mark.asyncio
async def test_mocked_response():
    agent = Agent("test_agent")
    reply = await agent.ask("Hi!", config=TestConfig("This is a mocked response."))
    assert reply.body == "This is a mocked response."

模拟成功的工具调用

先传入 ToolCallEvent(表示模型“决定”调用工具),再传入最终答案:

import pytest
from ag2 import Agent
from ag2.events import ToolCallEvent
from ag2.testing import TestConfig

@pytest.mark.asyncio
async def test_tool_success():
    def my_tool() -> str:
        return "tool execution result"

    agent = Agent("test_agent", tools=[my_tool])
    config = TestConfig(
        ToolCallEvent(name="my_tool"),
        "final result",
    )
    reply = await agent.ask("Please use my_tool", config=config)
    assert reply.body == "final result"

测试工具错误路径

如果工具抛出异常,异常会传播到 ask():

@pytest.mark.asyncio
async def test_tool_raises():
    def failing_tool() -> str:
        raise ValueError("Something went wrong")

    config = TestConfig(
        ToolCallEvent(name="failing_tool"),
        "result",
    )
    agent = Agent("test_agent", config=config, tools=[failing_tool])

    with pytest.raises(ValueError, match="Something went wrong"):
        await agent.ask("Hi!")

工具未找到

如果 LLM 调用了智能体没有的工具,框架会抛出 ToolNotFoundError:

from ag2.exceptions import ToolNotFoundError

@pytest.mark.asyncio
async def test_tool_not_found():
    config = TestConfig(ToolCallEvent(name="unregistered_tool"))
    agent = Agent("test_agent", config=config)
    with pytest.raises(ToolNotFoundError, match="Tool `unregistered_tool` not found"):
        await agent.ask("Hi!")

有用的测试模式

覆盖 Depends 依赖:

def get_production_db():
    raise Exception("Do not call in tests!")

@tool
def read_data(db: Annotated[object, Depends(get_production_db)]) -> str:
    return "Data"

agent = Agent("test", tools=[read_data])
agent.dependency_provider.override(get_production_db, lambda: "mock_db")

覆盖 Inject 依赖,直接向 agent.ask(...) 传入 dependencies={...}:

await agent.ask("Read", dependencies={"database_pool": fake_pool})

捕获流事件:

from ag2 import MemoryStream
from ag2.events import ToolCallEvent

stream = MemoryStream()
collected: list[ToolCallEvent] = []
stream.where(ToolCallEvent).subscribe(lambda e: collected.append(e))

await agent.ask("Test", stream=stream)
assert collected[0].name == "expected_tool"

使用方式与多轮模拟说明

TestConfig 的响应列表是在单次 ask() 内被消耗的,跨该轮中多次重复的 LLM 调用,而不是跨整段对话。因此 TestConfig(ToolCallEvent("my_tool"), "final result") 可以用于带工具调用的一轮:LLM 发出工具调用,工具执行,再次调用 LLM 并得到 "final result"。内部实现上,TestConfig.create() 返回一个全新的客户端,其迭代器从 responses[0] 开始,且每次 ask() 只执行一次;因此每个新的 ask()(无论是 reply.ask(...) 链,还是由网络适配器或自动回复智能体驱动的每一轮)都会把游标重置到第一个响应。列出更多响应并不能表达“对话第 2 轮与第 1 轮不同”。

若希望在多次 ask() 之间产生变化,可以:

  • 通过每次 ask() 的 config= 覆盖传入新的 TestConfig(...),例如 await agent.ask("…", config=TestConfig("turn-2 reply"));或
  • 用 ToolCallEvent 模拟模型,并把每轮逻辑放在有状态的工具中——用闭包或类实例记录被调用次数并据此返回。当轮次循环由测试代码之外的东西驱动时(例如工作流/讨论频道),无法注入每次轮次的 config=,这种做法很有用。

进一步参考

  • 源文档:website/docs/user-guide/testing.mdx。
  • 测试标记 / 异步配置:仓库 pyproject.toml。使用 @pytest.mark.asyncio(项目使用 pytest-asyncio)。
  • 流(用于断言事件):website/docs/user-guide/advanced/stream.mdx。

常见陷阱

  • 忘记 @pytest.mark.asyncio——测试会被跳过或以异常方式失败。
  • 响应数量不匹配——如果智能体在单次 ask() 内发起的 LLM 调用比预期多(例如工具出错后再次调用 LLM),TestConfig 会用尽响应,并传播 StopIteration。需要增加位置参数或断言调用顺序。游标是按 ask() 的,因此所需数量是“一轮内的 LLM 调用次数”,而不是“对话的轮次数”。
  • 只模拟 LLM 而未模拟工具——工具函数仍会运行(可能访问真实 API 或磁盘)。若要隔离 LLM 行为,请模拟工具,或覆写其 Depends 注入测试替身。
  • 在设置了 response_schema 时断言 reply.body——body 是原始文本。应使用 await reply.content() 获取经过校验的值。
  • 在异步测试间共享 Agent 实例——智能体带有可变状态(变量、依赖)。每个测试应构造全新的智能体以保证隔离。
  • 在 CI 中使用真实提供商客户端——按测试或通过 fixture 用 TestConfig 包装提供商配置;不要依赖 OPENAI_API_KEY 等在测试环境中可用。