测试
概述
测试功能为 Koog 框架中的 AI 代理流水线、子图以及工具交互提供了一个全面的测试框架。它使开发者能够使用模拟 (Mock) LLM(大语言模型)执行器、工具库和代理环境来创建受控的测试环境。
目的
此功能的主要目的是通过以下方式促进基于代理的 AI 功能的测试:
- 针对特定提示词模拟 LLM 响应
- 模拟工具调用及其结果
- 测试代理流水线子图及其结构
- 验证数据在代理节点间的正确流向
- 为预期行为提供断言
配置与初始化
设置测试依赖项
在设置测试环境之前,请确保已添加以下依赖项:
// build.gradle.kts
dependencies {
testImplementation("ai.koog:agents-test:LATEST_VERSION")
testImplementation(kotlin("test"))
}模拟 LLM 响应
测试的基本形式涉及模拟 LLM 响应以确保行为的确定性。您可以使用 MockLLMBuilder 和相关实用程序来执行此操作。
// 创建模拟 LLM 执行器
val mockLLMApi = getMockExecutor {
// 模拟简单的文本响应
mockLLMAnswer("Hello!") onRequestContains "Hello"
// 模拟默认响应
mockLLMAnswer("I don't know how to answer that.").asDefaultResponse
}import ai.koog.agents.core.tools.ToolRegistry;
import ai.koog.agents.testing.tools.MockExecutor;
import ai.koog.prompt.executor.model.PromptExecutor;
// 创建工具库(空)
ToolRegistry toolRegistry = ToolRegistry.builder().build();
// 创建模拟 LLM 执行器
PromptExecutor mockLLMApi = MockExecutor.builder()
.toolRegistry(toolRegistry)
.mockLLMAnswer("Hello!").onRequestContains("Hello")
.mockLLMAnswer("I don't know how to answer that.").asDefaultResponse()
.build();模拟工具调用
您可以根据输入模式模拟 LLM 调用特定的工具:
// 模拟工具调用响应
mockLLMToolCall(CreateTool, CreateTool.Args("solve")) onRequestEquals "Solve task"
// 模拟工具行为 - 不带 lambda 的最简形式
mockTool(PositiveToneTool) alwaysReturns "The text has a positive tone."
// 需要执行额外操作时使用 lambda
mockTool(NegativeToneTool) alwaysTells {
// 执行一些额外操作
println("Negative tone tool called")
// 返回结果
"The text has a negative tone."
}
// 根据特定实参模拟工具行为
mockTool(AnalyzeTool) returns "Detailed analysis" onArguments AnalyzeTool.Args("analyze deeply")
// 使用条件实参匹配模拟工具行为
mockTool(SearchTool) returns "Found results" onArgumentsMatching { args ->
args.query.contains("important")
}上面的示例演示了模拟工具的不同方式,从简单到复杂:
alwaysReturns:最简单的形式,直接返回一个值而不使用 lambda。alwaysTells:当您需要执行额外操作时使用 lambda。returns...onArguments:为精确匹配的实参返回特定结果。returns...onArgumentsMatching:根据自定义实参条件返回结果。
启用测试模式
要在代理上启用测试模式,请在 AIAgent 构造函数块中使用 withTesting() 函数:
// 创建启用了测试模式的代理
AIAgent(
promptExecutor = mockLLMApi,
toolRegistry = toolRegistry,
llmModel = llmModel
) {
// 启用测试模式
withTesting()
}高级测试
测试图结构
在测试详细的节点行为和边连接之前,验证代理图的整体结构非常重要。这包括检查所有必需的节点是否存在,并已在预期的子图中正确连接。
测试功能提供了一种全面的方式来测试代理的图结构。对于具有多个子图和互连节点的复杂代理,这种方法特别有价值。
基础结构测试
从验证代理图的基础结构开始:
AIAgent(
// 构造函数实参
promptExecutor = mockLLMApi,
toolRegistry = toolRegistry,
llmModel = llmModel
) {
testGraph<String, String>("test") {
val firstSubgraph = assertSubgraphByName<String, String>("first")
val secondSubgraph = assertSubgraphByName<String, String>("second")
// 断言子图连接
assertEdges {
startNode() alwaysGoesTo firstSubgraph
firstSubgraph alwaysGoesTo secondSubgraph
secondSubgraph alwaysGoesTo finishNode()
}
// 验证第一个子图
verifySubgraph(firstSubgraph) {
val start = startNode()
val finish = finishNode()
// 按名称断言节点
val askLLM = assertNodeByName<String, Message.Assistant>("callLLM")
val callTool = assertNodeByName<ToolCalls, ReceivedToolResults>("executeTool")
// 断言节点可达性
assertReachable(start, askLLM)
assertReachable(askLLM, callTool)
}
}
}测试节点行为
节点行为测试允许您验证代理图中的节点是否针对给定输入产生了预期输出。 这对于确保代理逻辑在不同场景下正常运行至关重要。
基础节点测试
从对单个节点进行简单的输入和输出验证开始:
assertNodes {
// 测试基础文本响应
askLLM withInput "Hello" outputs assistantMessage("Hello!")
// 测试工具调用响应
askLLM withInput "Solve task" outputs assistantMessage(CreateTool, CreateTool.Args("solve"))
}上面的示例演示了如何测试以下行为:
- 当 LLM 节点接收到
Hello作为输入时,它以简单的文本消息作为响应。 - 当接收到
Solve task时,它以工具调用作为响应。
测试工具运行节点
您还可以测试运行工具的节点:
assertNodes {
// 使用特定实参测试工具运行
callTool withInput ToolCalls(listOf(toolCallMessagePart(
SolveTool,
SolveTool.Args("solve")
))) outputs ReceivedToolResults(listOf(toolResult(SolveTool, SolveTool.Args("solve"), "solved")))
}这验证了当工具执行节点收到特定的工具调用签名时,它会产生预期的工具结果。
高级节点测试
对于更复杂的场景,您可以测试具有结构化输入和输出的节点:
assertNodes {
// 测试同一节点的不同输入
askLLM withInput "Simple query" outputs assistantMessage("Simple response")
// 测试复杂参数
askLLM withInput "Complex query with parameters" outputs assistantMessage(
AnalyzeTool,
AnalyzeTool.Args(query = "parameters", depth = 3)
)
}您还可以测试具有详细结果结构的复杂工具调用场景:
assertNodes {
// 测试带结构化结果的复杂工具调用
callTool withInput ToolCalls(listOf(toolCallMessagePart(
AnalyzeTool,
AnalyzeTool.Args(query = "complex", depth = 5)
))) outputs ReceivedToolResults(listOf(toolResult(AnalyzeTool, AnalyzeTool.Args(query = "complex", depth = 5), AnalyzeTool.Result(
analysis = "Detailed analysis",
confidence = 0.95,
metadata = mapOf("source" to "database", "timestamp" to "2023-06-15")
))))
}这些高级测试有助于确保您的节点能够正确处理复杂的数据结构,这对于精密的代理行为至关重要。
测试边连接
边连接测试允许您验证代理图是否正确地将一个节点的输出路由到适当的下一个节点。这可以确保您的代理根据不同的输出遵循预期的工作流路径。
基础边测试
从简单的边连接测试开始:
assertEdges {
// 测试文本消息路由
askLLM withOutput assistantMessage("Hello!") goesTo giveFeedback
// 测试工具调用路由
askLLM withOutput assistantMessage(CreateTool, CreateTool.Args("solve")) goesTo callTool
}此示例验证了以下行为:
- 当 LLM 节点输出简单的文本消息时,流程将定向到
giveFeedback节点。 - 当它输出工具调用时,流程将定向到
callTool节点。
测试条件路由
您可以根据输出内容测试更复杂的路由逻辑:
assertEdges {
// 不同的文本响应可以路由到不同的节点
askLLM withOutput assistantMessage("Need more information") goesTo askForInfo
askLLM withOutput assistantMessage("Ready to proceed") goesTo processRequest
}高级边测试
对于复杂的代理,您可以根据工具结果中的结构化数据测试条件路由:
assertEdges {
// 根据工具结果内容测试路由
callTool withOutput ReceivedToolResults(listOf(toolResult(
AnalyzeTool,
AnalyzeTool.Args(query = "parameters", depth = 3),
AnalyzeTool.Result(analysis = "Needs more processing", confidence = 0.5)
))) goesTo processResult
}您还可以基于不同的结果属性测试复杂的决策路径:
assertEdges {
// 根据置信度级别路由到不同的节点
callTool withOutput ReceivedToolResults(listOf(toolResult(
AnalyzeTool,
AnalyzeTool.Args(query = "parameters", depth = 3),
AnalyzeTool.Result(analysis = "Complete", confidence = 0.9)
))) goesTo finish
callTool withOutput ReceivedToolResults(listOf(toolResult(
AnalyzeTool,
AnalyzeTool.Args(query = "parameters", depth = 3),
AnalyzeTool.Result(analysis = "Uncertain", confidence = 0.3)
))) goesTo verifyResult
}这些高级边测试有助于确保您的代理根据节点输出的内容和结构做出正确的决策,这对于创建智能且具有上下文感知能力的工作流至关重要。
完整测试示例
这是一个展示完整测试场景的用户情景:
您正在开发一个音调分析代理,用于分析文本的音调并提供反馈。该代理使用工具来检测正面、负面和中性的音调。
以下是测试此代理的方法:
@Test
fun testToneAgent() = runTest {
// 创建一个列表来跟踪工具调用
var toolCalls = mutableListOf<String>()
var result: String? = null
// 创建工具库
val toolRegistry = ToolRegistry {
// 一个特殊的工具,此类代理所必需
tool(SayToUser)
with(ToneTools) {
tools()
}
}
// 创建事件处理程序
val eventHandler = EventHandler {
onToolCallStarting { tool, args ->
println("[DEBUG_LOG] Tool called: tool ${tool.name}, args $args")
toolCalls.add(tool.name)
}
handleError {
println("[DEBUG_LOG] An error occurred: ${it.message}
${it.stackTraceToString()}")
true
}
handleResult {
println("[DEBUG_LOG] Result: $it")
result = it
}
}
val positiveText = "I love this product!"
val negativeText = "Awful service, hate the app."
val defaultText = "I don't know how to answer this question."
val positiveResponse = "The text has a positive tone."
val negativeResponse = "The text has a negative tone."
val neutralResponse = "The text has a neutral tone."
val mockLLMApi = getMockExecutor(toolRegistry, eventHandler) {
// 为不同的输入文本设置 LLM 响应
mockLLMToolCall(NeutralToneTool, ToneTool.Args(defaultText)) onRequestEquals defaultText
mockLLMToolCall(PositiveToneTool, ToneTool.Args(positiveText)) onRequestEquals positiveText
mockLLMToolCall(NegativeToneTool, ToneTool.Args(negativeText)) onRequestEquals negativeText
// 模拟 LLM 在工具返回结果时仅以工具响应作为响应的行为
mockLLMAnswer(positiveResponse) onRequestContains positiveResponse
mockLLMAnswer(negativeResponse) onRequestContains negativeResponse
mockLLMAnswer(neutralResponse) onRequestContains neutralResponse
mockLLMAnswer(defaultText).asDefaultResponse
// 工具模拟 (Mock)
mockTool(PositiveToneTool) alwaysTells {
toolCalls += "Positive tone tool called"
positiveResponse
}
mockTool(NegativeToneTool) alwaysTells {
toolCalls += "Negative tone tool called"
negativeResponse
}
mockTool(NeutralToneTool) alwaysTells {
toolCalls += "Neutral tone tool called"
neutralResponse
}
}
// 创建策略
val strategy = toneStrategy("tone_analysis")
// 创建代理配置
val agentConfig = AIAgentConfig(
prompt = prompt("test-agent") {
system(
"""
You are an question answering agent with access to the tone analysis tools.
You need to answer 1 question with the best of your ability.
Be as concise as possible in your answers.
DO NOT ANSWER ANY QUESTIONS THAT ARE BESIDES PERFORMING TONE ANALYSIS!
DO NOT HALLUCINATE!
""".trimIndent()
)
},
model = mockk<LLModel>(relaxed = true),
maxAgentIterations = 10
)
// 创建启用了测试模式的代理
val agent = AIAgent(
promptExecutor = mockLLMApi,
toolRegistry = toolRegistry,
strategy = strategy,
eventHandler = eventHandler,
agentConfig = agentConfig,
) {
withTesting()
}
// 测试正面文本
agent.run(positiveText)
assertEquals("The text has a positive tone.", result, "Positive tone result should match")
assertEquals(1, toolCalls.size, "One tool is expected to be called")
// 测试负面文本
agent.run(negativeText)
assertEquals("The text has a negative tone.", result, "Negative tone result should match")
assertEquals(2, toolCalls.size, "Two tools are expected to be called")
// 测试中性文本
agent.run(defaultText)
assertEquals("The text has a neutral tone.", result, "Neutral tone result should match")
assertEquals(3, toolCalls.size, "Three tools are expected to be called")
}对于具有多个子图的更复杂的代理,您还可以测试图结构:
@Test
fun testMultiSubgraphAgentStructure() = runTest {
val strategy = strategy("test") {
val firstSubgraph by subgraph(
"first",
tools = listOf(DummyTool, CreateTool, SolveTool)
) {
val callLLM by nodeLLMRequest(allowToolCalls = false)
val executeTool by nodeExecuteTools()
val sendToolResult by nodeLLMSendToolResults()
val giveFeedback by node<String, String> { input ->
llm.writeSession {
appendPrompt {
user("Call tools! Don't chat!")
}
}
input
}
edge(nodeStart forwardTo callLLM)
edge(callLLM forwardTo executeTool onToolCalls { true })
edge(callLLM forwardTo giveFeedback onTextMessage { true })
edge(giveFeedback forwardTo giveFeedback transformed { it })
edge(executeTool forwardTo nodeFinish transformed { it.toolResults.first().output })
}
val secondSubgraph by subgraph<String, String>("second") {
edge(nodeStart forwardTo nodeFinish)
}
edge(nodeStart forwardTo firstSubgraph)
edge(firstSubgraph forwardTo secondSubgraph)
edge(secondSubgraph forwardTo nodeFinish)
}
val toolRegistry = ToolRegistry {
tool(DummyTool)
tool(CreateTool)
tool(SolveTool)
}
val mockLLMApi = getMockExecutor(toolRegistry) {
mockLLMAnswer("Hello!") onRequestContains "Hello"
mockLLMToolCall(CreateTool, CreateTool.Args("solve")) onRequestEquals "Solve task"
}
val basePrompt = prompt("test") {}
AIAgent(
toolRegistry = toolRegistry,
strategy = strategy,
eventHandler = EventHandler {},
agentConfig = AIAgentConfig(prompt = basePrompt, model = OpenAIModels.Chat.GPT4o, maxAgentIterations = 100),
promptExecutor = mockLLMApi,
) {
testGraph("test") {
val firstSubgraph = assertSubgraphByName<String, String>("first")
val secondSubgraph = assertSubgraphByName<String, String>("second")
assertEdges {
startNode() alwaysGoesTo firstSubgraph
firstSubgraph alwaysGoesTo secondSubgraph
secondSubgraph alwaysGoesTo finishNode()
}
verifySubgraph(firstSubgraph) {
val start = startNode()
val finish = finishNode()
val askLLM = assertNodeByName<String, Message.Assistant>("callLLM")
val callTool = assertNodeByName<ToolCalls, ReceivedToolResults>("executeTool")
val giveFeedback = assertNodeByName<Any?, Any?>("giveFeedback")
assertReachable(start, askLLM)
assertReachable(askLLM, callTool)
assertNodes {
askLLM withInput "Hello" outputs assistantMessage("Hello!")
askLLM withInput "Solve task" outputs assistantMessage(CreateTool, CreateTool.Args("solve"))
callTool withInput ToolCalls(listOf(toolCallMessagePart(
SolveTool,
SolveTool.Args("solve")
))) outputs ReceivedToolResults(listOf(toolResult(SolveTool, SolveTool.Args("solve"), "solved")))
callTool withInput ToolCalls(listOf(toolCallMessagePart(
CreateTool,
CreateTool.Args("solve")
))) outputs ReceivedToolResults(listOf(toolResult(CreateTool, CreateTool.Args("solve"), "created")))
}
assertEdges {
askLLM withOutput assistantMessage("Hello!") goesTo giveFeedback
askLLM withOutput assistantMessage(CreateTool, CreateTool.Args("solve")) goesTo callTool
}
}
}
}
}API 参考
有关与测试功能相关的完整 API 参考,请参阅 agents-test 模块的参考文档。
常见问题解答与故障排除
如何模拟特定的工具响应?
使用 MockLLMBuilder 中的 mockTool 方法:
val mockExecutor = getMockExecutor {
mockTool(myTool) alwaysReturns myResult
// 或带有条件
mockTool(myTool) returns myResult onArguments myArgs
}如何测试复杂的图结构?
使用子图断言、verifySubgraph 和节点引用:
testGraph<Unit, String>("test") {
val mySubgraph = assertSubgraphByName<Unit, String>("mySubgraph")
verifySubgraph(mySubgraph) {
// 获取节点引用
val nodeA = assertNodeByName<Unit, String>("nodeA")
val nodeB = assertNodeByName<String, String>("nodeB")
// 断言可达性
assertReachable(nodeA, nodeB)
// 断言边连接
assertEdges {
nodeA.withOutput("result") goesTo nodeB
}
}
}如何根据输入模拟不同的 LLM 响应?
使用模式匹配方法:
getMockExecutor {
mockLLMAnswer("Response A") onRequestContains "topic A"
mockLLMAnswer("Response B") onRequestContains "topic B"
mockLLMAnswer("Exact response") onRequestEquals "exact question"
mockLLMAnswer("Conditional response") onCondition { it.contains("keyword") && it.length > 10 }
}import ai.koog.agents.testing.tools.MockExecutor;
import ai.koog.prompt.executor.model.PromptExecutor;
PromptExecutor promptExecutor = MockExecutor.builder()
.mockLLMAnswer("Response A").onRequestContains("topic A")
.mockLLMAnswer("Response B").onRequestContains("topic B")
.mockLLMAnswer("Exact response").onRequestEquals("exact question")
.mockLLMAnswer("Conditional response").onCondition(s -> s.contains("keyword") && s.length() > 10)
.build();故障排除
模拟执行器总是返回默认响应
检查您的模式匹配是否正确。模式区分大小写,且必须与指定的完全匹配。
工具调用未被拦截
确保:
- 工具库已正确设置。
- 工具名称完全匹配。
- 工具操作配置正确。
图断言失败
- 验证节点名称是否正确。
- 检查图结构是否符合您的预期。
- 使用
startNode()和finishNode()方法获取正确的入口点和出口点。
