跳转至

测试

Agent 代码同样需要离线、免费且结果确定的测试。依赖网络、结果不稳定、每次运行都产生成本的“测试”, 往往很快就无人问津。lovia.testing 提供了适合日常使用的测试替身 ScriptedProvider; 它是完整的 Provider 实现,会依次回放预先编写的轮次。

from lovia import Agent, tool
from lovia.testing import ScriptedProvider, call, text


@tool
def add(a: int, b: int) -> int:
    """把两个数相加。"""
    return a + b


def make_agent() -> Agent:
    return Agent(
        name="calc",
        model=ScriptedProvider([
            call("add", {"a": 2, "b": 3}, call_id="c1"),   # 第 1 轮:请求工具
            text("答案是 5。"),                              # 第 2 轮:最终答案
        ]),
        tools=[add],
    )


async def test_calc_uses_the_tool():
    result = await make_agent().run("2 + 3 等于多少?")
    assert result.output == "答案是 5。"
    assert result.turns == 2

脚本描述模型一侧的对话:每个条目按顺序响应一次模型调用。实际工具仍会执行,只有大模型由脚本替代; 因此测试覆盖的是真实的运行循环,包括 Schema 校验、并发执行、审批门禁、结构化输出解析和 Session 持久化。

构建脚本

Helper 产出
text("Done.") 纯文本轮次(按字符流式输出)
text("Done.", reasoning="hmm...") 带 reasoning delta 的文本,用来测试 ReasoningDelta 消费者
call("search", {"q": "tides"}) 请求一个工具调用的 turn(call_id 默认是 call_<name>
batch(("a", {...}), ("b", {...})) 同一轮请求多个调用,用来测试并发执行

脚本耗尽会抛 AssertionError("ScriptedProvider ran out of canned responses"),轮次数不对会明确失败, 而不是卡住。

断言 Agent 收到了哪些内容

provider 会记录它收到的每个 prompt:

provider = ScriptedProvider([text("ok")])
agent = Agent(name="bot", model=provider, instructions="回答要简短。")
await agent.run("hello")

first_prompt = provider.calls[0]              # 第 1 轮的输入,list[Message]
assert first_prompt[0].role == "system"
assert "回答要简短。" in first_prompt[0].content

provider.calls[i] 是第 i 轮输入的 chat 格式视图。它非常适合测试 动态指令视图注入器,以及 compaction 行为,比如“被清理的结果确实已经离开 view 了吗?”。

按测试目标选择方法

  • 工具本身:普通 pytest;@tool 函数本质上仍是函数。
  • 循环行为(路由、工具选择、修复、护栏、handoff):用上面的 ScriptedProvider。 handoff 和 agent-as-tool 子运行各自从自己的 agent provider 消费脚本; 请给每个 agent 自己的 script。
  • 事件消费者 / UI:脚本化一次运行并迭代 Runner.stream(...);delta 按字符流出,消费者会看到实际的碎片化输出。
  • 行为质量(“答得好吗?”):用 eval。eval 的离线模式也使用同一个 ScriptedProvider,在线模式用真实模型。
  • 在线冒烟测试:加标记,默认跳过,按需运行。本仓库使用 pytest -m live_provider, 并由 LOVIA_LIVE_TESTS=1 控制。

注意事项

  • ScriptedProvider 是一次性的。 它会从共享队列 pop,不可重复,也不并发安全。每次运行都应创建新的 provider(和 agent);在 evaluate() 里传 agent 工厂正是这个原因。
  • supports_json_schemaFalse,因此结构化输出通过提示词实现: scripted 最终 turn 必须是 JSON 文档本身(text('{"title": "..."}')),schema instructions 会落在 provider.calls[0][0],你可以对它断言。
  • 异步测试需要异步 runner。 本仓库使用 pytest-asyncio;没有运行中事件循环的普通测试里, Runner.run_sync 也可以用。

延伸阅读

  • 评测:同一个测试替身,用来度量质量,而不是验证接入是否正确
  • ProviderScriptedProvider 也是一个可参考的 Provider 实现
  • 示例:10_custom_provider.py(离线),以及本仓库的 tests/ 目录