测试¶
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_schema为False,因此结构化输出通过提示词实现: scripted 最终 turn 必须是 JSON 文档本身(text('{"title": "..."}')),schema instructions 会落在provider.calls[0][0],你可以对它断言。- 异步测试需要异步 runner。 本仓库使用
pytest-asyncio;没有运行中事件循环的普通测试里,Runner.run_sync也可以用。
延伸阅读¶
- 评测:同一个测试替身,用来度量质量,而不是验证接入是否正确
- Provider:
ScriptedProvider也是一个可参考的Provider实现 - 示例:
10_custom_provider.py(离线),以及本仓库的tests/目录