テスト
概要
テスト機能は、KoogフレームワークにおけるAIエージェントのパイプライン、サブグラフ、およびツール連携をテストするための包括的なフレームワークを提供します。これにより、開発者はモックLLM (大規模言語モデル) エグゼキューター、ツールレジストリ、エージェント環境を備えた、制御されたテスト環境を構築できます。
目的
この機能の主な目的は、以下の方法でエージェントベースのAI機能のテストを容易にすることです。
- 特定のプロンプトに対するLLMの応答をモックする
- ツール呼び出しとその結果をシミュレートする
- エージェントパイプラインのサブグラフとその構造をテストする
- エージェントノードを介したデータの正しいフローを検証する
- 期待される動作に対するアサーションを提供する
設定と初期化
テスト依存関係の設定
テスト環境を設定する前に、以下の依存関係を追加していることを確認してください。
// build.gradle.kts
dependencies {
testImplementation("ai.koog:agents-test:LATEST_VERSION")
testImplementation(kotlin("test"))
}
LLM応答のモック
テストの基本的な形式は、決定論的な動作を保証するためにLLM応答をモックすることです。これは MockLLMBuilder
および関連ユーティリティを使用して行うことができます。
// Create a mock LLM executor
val mockLLMApi = getMockExecutor(toolRegistry) {
// Mock a simple text response
mockLLMAnswer("Hello!") onRequestContains "Hello"
// Mock a default response
mockLLMAnswer("I don't know how to answer that.").asDefaultResponse
}
ツール呼び出しのモック
入力パターンに基づいて、LLMが特定のツールを呼び出すようにモックできます。
// Mock a tool call response
mockLLMToolCall(CreateTool, CreateTool.Args("solve")) onRequestEquals "Solve task"
// Mock tool behavior - simplest form without lambda
mockTool(PositiveToneTool) alwaysReturns "The text has a positive tone."
// Using lambda when you need to perform extra actions
mockTool(NegativeToneTool) alwaysTells {
// Perform some extra action
println("Negative tone tool called")
// Return the result
"The text has a negative tone."
}
// Mock tool behavior based on specific arguments
mockTool(AnalyzeTool) returns AnalyzeTool.Result("Detailed analysis") onArguments AnalyzeTool.Args("analyze deeply")
// Mock tool behavior with conditional argument matching
mockTool(SearchTool) returns SearchTool.Result("Found results") onArgumentsMatching { args ->
args.query.contains("important")
}
上記の例は、単純なものからより複雑なものまで、ツールをモックするさまざまな方法を示しています。
alwaysReturns
: 最も単純な形式で、ラムダなしで直接値を返します。alwaysTells
: 追加のアクションを実行する必要がある場合にラムダを使用します。returns...onArguments
: 正確な引数の一致に対して特定の値を返します。returns...onArgumentsMatching
: カスタム引数条件に基づいて結果を返します。
テストモードの有効化
エージェントでテストモードを有効にするには、AIAgent
コンストラクタブロック内で withTesting()
関数を使用します。
// Create the agent with testing enabled
AIAgent(
executor = mockLLMApi,
toolRegistry = toolRegistry,
llmModel = llmModel
) {
// Enable testing mode
withTesting()
}
高度なテスト
グラフ構造のテスト
詳細なノードの動作とエッジ接続をテストする前に、エージェントのグラフ全体の構造を検証することが重要です。これには、必要なすべてのノードが存在し、期待されるサブグラフ内で適切に接続されていることを確認することが含まれます。
テスト機能は、エージェントのグラフ構造をテストするための包括的な方法を提供します。このアプローチは、複数のサブグラフと相互接続されたノードを持つ複雑なエージェントにとって特に価値があります。
基本的な構造テスト
まず、エージェントのグラフの基本的な構造を検証します。
AIAgent(
// Constructor arguments
executor = mockLLMApi,
toolRegistry = toolRegistry,
llmModel = llmModel
) {
testGraph<String, String>("test") {
val firstSubgraph = assertSubgraphByName<String, String>("first")
val secondSubgraph = assertSubgraphByName<String, String>("second")
// Assert subgraph connections
assertEdges {
startNode() alwaysGoesTo firstSubgraph
firstSubgraph alwaysGoesTo secondSubgraph
secondSubgraph alwaysGoesTo finishNode()
}
// Verify the first subgraph
verifySubgraph(firstSubgraph) {
val start = startNode()
val finish = finishNode()
// Assert nodes by name
val askLLM = assertNodeByName<String, Message.Response>("callLLM")
val callTool = assertNodeByName<ToolArgs, ToolResult>("executeTool")
// Assert node reachability
assertReachable(start, askLLM)
assertReachable(askLLM, callTool)
}
}
}
ノードの動作テスト
ノードの動作テストでは、エージェントのグラフ内のノードが、与えられた入力に対して期待される出力を生成することを確認できます。これは、エージェントのロジックがさまざまなシナリオで正しく機能することを保証するために不可欠です。
基本的なノードテスト
個々のノードに対するシンプルな入力と出力の検証から始めます。
assertNodes {
// Test basic text responses
askLLM withInput "Hello" outputs assistantMessage("Hello!")
// Test tool call responses
askLLM withInput "Solve task" outputs toolCallMessage(CreateTool, CreateTool.Args("solve"))
}
上記の例は、以下の動作をテストする方法を示しています。
- LLMノードが入力として
Hello
を受け取ると、シンプルなテキストメッセージで応答します。 Solve task
を受け取ると、ツール呼び出しで応答します。
ツール実行ノードのテスト
ツールを実行するノードもテストできます。
assertNodes {
// Test tool runs with specific arguments
callTool withInput toolCallMessage(
SolveTool,
SolveTool.Args("solve")
) outputs toolResult(SolveTool, "solved")
}
これにより、ツール実行ノードが特定のツール呼び出しシグネチャを受け取ったときに、期待されるツール結果を生成することを確認できます。
高度なノードテスト
より複雑なシナリオでは、構造化された入力と出力を持つノードをテストできます。
assertNodes {
// Test with different inputs to the same node
askLLM withInput "Simple query" outputs assistantMessage("Simple response")
// Test with complex parameters
askLLM withInput "Complex query with parameters" outputs toolCallMessage(
AnalyzeTool,
AnalyzeTool.Args(query = "parameters", depth = 3)
)
}
また、詳細な結果構造を持つ複雑なツール呼び出しシナリオをテストすることもできます。
assertNodes {
// Test a complex tool call with a structured result
callTool withInput toolCallMessage(
AnalyzeTool,
AnalyzeTool.Args(query = "complex", depth = 5)
) outputs toolResult(AnalyzeTool, AnalyzeTool.Result(
analysis = "Detailed analysis",
confidence = 0.95,
metadata = mapOf("source" to "database", "timestamp" to "2023-06-15")
))
}
これらの高度なテストは、ノードが複雑なデータ構造を正しく処理することを確認するのに役立ち、これは洗練されたエージェントの動作にとって不可欠です。
エッジ接続のテスト
エッジ接続のテストにより、エージェントのグラフが、あるノードからの出力を適切な次のノードに正しくルーティングすることを確認できます。これにより、エージェントがさまざまな出力に基づいて意図したワークフローパスをたどることが保証されます。
基本的なエッジテスト
シンプルなエッジ接続テストから始めます。
assertEdges {
// Test text message routing
askLLM withOutput assistantMessage("Hello!") goesTo giveFeedback
// Test tool call routing
askLLM withOutput toolCallMessage(CreateTool, CreateTool.Args("solve")) goesTo callTool
}
この例は、以下の動作を確認します。
- LLMノードがシンプルなテキストメッセージを出力すると、フローは
giveFeedback
ノードに転送されます。 - ツール呼び出しを出力すると、フローは
callTool
ノードに転送されます。
条件付きルーティングのテスト
出力の内容に基づいて、より複雑なルーティングロジックをテストできます。
assertEdges {
// Different text responses can route to different nodes
askLLM withOutput assistantMessage("Need more information") goesTo askForInfo
askLLM withOutput assistantMessage("Ready to proceed") goesTo processRequest
}
高度なエッジテスト
洗練されたエージェントの場合、ツール結果の構造化データに基づいて条件付きルーティングをテストできます。
assertEdges {
// Test routing based on tool result content
callTool withOutput toolResult(
AnalyzeTool,
AnalyzeTool.Result(analysis = "Needs more processing", confidence = 0.5)
) goesTo processResult
}
また、さまざまな結果プロパティに基づいて複雑な意思決定パスをテストすることもできます。
assertEdges {
// Route to different nodes based on confidence level
callTool withOutput toolResult(
AnalyzeTool,
AnalyzeTool.Result(analysis = "Complete", confidence = 0.9)
) goesTo finish
callTool withOutput toolResult(
AnalyzeTool,
AnalyzeTool.Result(analysis = "Uncertain", confidence = 0.3)
) goesTo verifyResult
}
これらの高度なエッジテストは、エージェントがノード出力の内容と構造に基づいて正しい決定を行うことを保証するのに役立ち、これはインテリジェントでコンテキストを認識するワークフローを作成するために不可欠です。
完全なテスト例
以下に、完全なテストシナリオを示すユーザー事例を示します。
あなたは、テキストのトーンを分析し、フィードバックを提供するトーン分析エージェントを開発しています。このエージェントは、ポジティブ、ネガティブ、ニュートラルのトーンを検出するためのツールを使用します。
このエージェントをテストする方法を以下に示します。
@Test
fun testToneAgent() = runTest {
// Create a list to track tool calls
var toolCalls = mutableListOf<String>()
var result: String? = null
// Create a tool registry
val toolRegistry = ToolRegistry {
// A special tool, required with this type of agent
tool(SayToUser)
with(ToneTools) {
tools()
}
}
// Create an event handler
val eventHandler = EventHandler {
onToolCall { 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) {
// Set up LLM responses for different input texts
mockLLMToolCall(NeutralToneTool, ToneTool.Args(defaultText)) onRequestEquals defaultText
mockLLMToolCall(PositiveToneTool, ToneTool.Args(positiveText)) onRequestEquals positiveText
mockLLMToolCall(NegativeToneTool, ToneTool.Args(negativeText)) onRequestEquals negativeText
// Mock the behavior where the LLM responds with just tool responses when the tools return results
mockLLMAnswer(positiveResponse) onRequestContains positiveResponse
mockLLMAnswer(negativeResponse) onRequestContains negativeResponse
mockLLMAnswer(neutralResponse) onRequestContains neutralResponse
mockLLMAnswer(defaultText).asDefaultResponse
// Tool mocks
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
}
}
// Create a strategy
val strategy = toneStrategy("tone_analysis")
// Create an agent configuration
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
)
// Create an agent with testing enabled
val agent = AIAgent(
promptExecutor = mockLLMApi,
toolRegistry = toolRegistry,
strategy = strategy,
eventHandler = eventHandler,
agentConfig = agentConfig,
) {
withTesting()
}
// Test the positive text
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")
// Test the negative text
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")
//Test the neutral text
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 nodeExecuteTool()
val sendToolResult by nodeLLMSendToolResult()
val giveFeedback by node<String, String> { input ->
llm.writeSession {
updatePrompt {
user("Call tools! Don't chat!")
}
}
input
}
edge(nodeStart forwardTo callLLM)
edge(callLLM forwardTo executeTool onToolCall { true })
edge(callLLM forwardTo giveFeedback onAssistantMessage { true })
edge(giveFeedback forwardTo giveFeedback onAssistantMessage { true })
edge(giveFeedback forwardTo executeTool onToolCall { true })
edge(executeTool forwardTo nodeFinish transformed { it.content })
}
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.Response>("callLLM")
val callTool = assertNodeByName<Message.Tool.Call, ReceivedToolResult>("executeTool")
val giveFeedback = assertNodeByName<Any?, Any?>("giveFeedback")
assertReachable(start, askLLM)
assertReachable(askLLM, callTool)
assertNodes {
askLLM withInput "Hello" outputs Message.Assistant("Hello!")
askLLM withInput "Solve task" outputs toolCallMessage(CreateTool, CreateTool.Args("solve"))
callTool withInput toolCallSignature(
SolveTool,
SolveTool.Args("solve")
) outputs toolResult(SolveTool, "solved")
callTool withInput toolCallSignature(
CreateTool,
CreateTool.Args("solve")
) outputs toolResult(CreateTool, "created")
}
assertEdges {
askLLM withOutput Message.Assistant("Hello!") goesTo giveFeedback
askLLM withOutput toolCallMessage(CreateTool, CreateTool.Args("solve")) goesTo callTool
}
}
}
}
}
APIリファレンス
テスト機能に関連する完全なAPIリファレンスについては、agents-testモジュールのリファレンスドキュメントを参照してください。
よくある質問とトラブルシューティング
特定のツール応答をモックするにはどうすればよいですか?
MockLLMBuilder
の mockTool
メソッドを使用します。
val mockExecutor = getMockExecutor {
mockTool(myTool) alwaysReturns myResult
// Or with conditions
mockTool(myTool) returns myResult onArguments myArgs
}
複雑なグラフ構造をテストするにはどうすればよいですか?
サブグラフのアサーション、verifySubgraph
、およびノード参照を使用します。
testGraph<Unit, String>("test") {
val mySubgraph = assertSubgraphByName<Unit, String>("mySubgraph")
verifySubgraph(mySubgraph) {
// Get references to nodes
val nodeA = assertNodeByName<Unit, String>("nodeA")
val nodeB = assertNodeByName<String, String>("nodeB")
// Assert reachability
assertReachable(nodeA, nodeB)
// Assert edge connections
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 }
}
トラブルシューティング
モックエグゼキューターが常にデフォルト応答を返す
パターンマッチングが正しいことを確認してください。パターンは大文字と小文字を区別し、指定されたとおりに正確に一致する必要があります。
ツール呼び出しが傍受されない
以下のことを確認してください。
- ツールレジストリが適切に設定されている。
- ツール名が正確に一致している。
- ツールアクションが正しく設定されている。
グラフアサーションが失敗する
- ノード名が正しいことを確認してください。
- グラフ構造が期待どおりであることを確認してください。
startNode()
およびfinishNode()
メソッドを使用して、正しい開始点と終了点を取得してください。