Koog 문서 한국어 번역 08: 그래프 기반 에이전트
원문: Koog 공식 문서
그래프 기반 에이전트
그래프 기반 에이전트는 동작을 명시적인 상태 머신으로 모델링합니다. 그래프 전략의 노드는 액션(LLM 호출, 툴 실행)을 나타내고, 엣지는 노드 간의 데이터 흐름을 나타냅니다.
그래프 기반 에이전트의 주요 장점은 다음과 같습니다:
- 시각화가 용이함
- 상태 영속성
- 조합 가능한 아키텍처
참고 — 사전 요구사항
- 사전 요구사항:
quickstart-snippets.md:prerequisites참고- 의존성 설정:
quickstart-snippets.md:dependencies참고- API 키 설정:
quickstart-snippets.md:api-key참고이 페이지의 예제는 Ollama를 통해 Llama 3.2를 로컬에서 실행 중인 환경을 전제로 합니다.
이 페이지에서는 기본 에이전트가 사용하는 전략 그래프를 직접 구현하는 방법을 설명합니다. 기본 에이전트는 LLM에 요청을 보낸 후, LLM이 어시스턴트 메시지로 응답하면 결과를 출력하고, 툴 호출을 요청하면 해당 툴을 실행합니다. 툴 호출이 발생한 경우, 에이전트는 툴 실행 결과를 LLM에 다시 전달하고, LLM이 응답을 반환하거나 추가 툴 호출을 요청할 때까지 이 과정을 반복합니다.
다음은 전략 그래프를 도식화한 것입니다:
1---2config:3 flowchart:4 defaultRenderer: "elk"5---6graph TB7 subgraph nodeStart8 Input9 end1011 subgraph nodeFinish12 Output13 end1415 subgraph nodeSendInput16 llmRequest(Request LLM)17 end1819 subgraph nodeExecuteTool20 executeTool(Execute tool call)21 end2223 subgraph nodeSendToolResult24 sendToolResult(Request LLM)25 end2627 Input --String--> llmRequest28 llmRequest --Message.Response--> onToolCall{{onToolCall}}29 llmRequest --Message.Response--> onAssistantMessage{{onAssistantMessage}}30 onAssistantMessage --String--> Output31 onToolCall --Message.Tool.Call--> executeTool --ReceivedToolResult--> sendToolResult32 sendToolResult --Message.Response--> onToolCall33 sendToolResult --Message.Response--> onAssistantMessage전략 그래프 구성하기
Koog에서는 AIAgentGraphStrategyBuilder를 사용해 전략을 구현합니다.
각 노드가 입력 타입과 출력 타입을 가지듯, 전략 전체에도 입력 타입과 출력 타입이 정의됩니다.
이 예제에서는 입력과 출력 타입이 모두 문자열이므로,
이 전략을 구현하는 에이전트는 문자열을 입력받아 문자열을 반환합니다.
전략을 생성하려면 strategy() 함수에 입력 타입과 출력 타입을 제네릭으로 지정하고,
전략의 고유 식별자를 제공한 뒤, 노드와 엣지를 정의합니다.
Kotlin
1val calculatorAgentStrategy = strategy<String, String>("Simple calculator") {2 val nodeSendInput by nodeLLMRequest()3 val nodeExecuteTool by nodeExecuteTool()4 val nodeSendToolResult by nodeLLMSendToolResult()56 edge(nodeStart forwardTo nodeSendInput)7 edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })8 edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })9 edge(nodeExecuteTool forwardTo nodeSendToolResult)10 edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })11 edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })12}Java
1var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator")2 .withInput(String.class)3 .withOutput(String.class);45var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput");6var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool");7var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult");89calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput);10calculatorAgentStrategy.edge(AIAgentEdge.builder()11 .from(nodeSendInput)12 .to(calculatorAgentStrategy.nodeFinish)13 .onIsInstance(Message.Assistant.class)14 .transformed(Message.Assistant::getContent)15 .build());16calculatorAgentStrategy.edge(AIAgentEdge.builder()17 .from(nodeSendInput)18 .to(nodeExecuteTool)19 .onIsInstance(Message.Tool.Call.class)20 .build());21calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult);22calculatorAgentStrategy.edge(AIAgentEdge.builder()23 .from(nodeSendToolResult)24 .to(calculatorAgentStrategy.nodeFinish)25 .onIsInstance(Message.Assistant.class)26 .transformed(Message.Assistant::getContent)27 .build());28calculatorAgentStrategy.edge(AIAgentEdge.builder()29 .from(nodeSendToolResult)30 .to(nodeExecuteTool)31 .onIsInstance(Message.Tool.Call.class)32 .build());이 예제는 미리 정의된 노드만 사용하지만, 커스텀 노드를 직접 만들어 사용할 수도 있습니다.
모든 전략 그래프는 nodeStart에서 nodeFinish까지 엣지로 연결된 경로가 반드시 존재해야 합니다.
엣지에는 조건을 지정할 수 있으며, 그 조건에 따라 어느 엣지를 따라갈지 결정됩니다.
또한 엣지는 이전 노드의 출력을 다음 노드로 전달하기 전에 변환할 수 있습니다.
이는 출력 타입과 입력 타입이 맞지 않는 노드들을 연결할 때 필요합니다.
앞선 예제에서 onToolCall { true }는 이전 노드가 툴 호출 메시지(Message.Tool.Call)를 반환했을 때만
해당 엣지를 따라가도록 합니다.
onAssistantMessage { true }를 사용하면 이전 노드가 어시스턴트 메시지(Message.Assistant)를 반환했을 때만
엣지를 따라갑니다.
이 함수는 어시스턴트 메시지의 내용도 추출하므로, Message.Assistant를 String으로 변환하는 효과도 있습니다
(nodeFinish는 문자열을 입력으로 기대하기 때문입니다).
팁
onAssistantMessage {true}대신 다음과 같이 작성할 수도 있습니다:1onIsInstance(Message.Assistant::class) transformed { it.content }또는:
1onCondition { it is Message.Assistant } transformed { it.asAssistantMessage().content }
에이전트 생성 및 실행
이 전략으로 에이전트 인스턴스를 생성하고 실행해 봅시다:
Kotlin
1val calculatorAgentStrategy = strategy<String, String>("Simple calculator") {2 val nodeSendInput by nodeLLMRequest()3 val nodeExecuteTool by nodeExecuteTool()4 val nodeSendToolResult by nodeLLMSendToolResult()56 edge(nodeStart forwardTo nodeSendInput)7 edge(nodeSendInput forwardTo nodeFinish onAssistantMessage { true })8 edge(nodeSendInput forwardTo nodeExecuteTool onToolCall { true })9 edge(nodeExecuteTool forwardTo nodeSendToolResult)10 edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })11 edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })12}1314val mathAgent = AIAgent(15 promptExecutor = simpleOllamaAIExecutor(),16 llmModel = OllamaModels.Meta.LLAMA_3_2,17 strategy = calculatorAgentStrategy18)1920fun main() = runBlocking {21 val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")22 println(result)23}Java
1var calculatorAgentStrategy = AIAgentGraphStrategy.builder("Simple calculator")2 .withInput(String.class)3 .withOutput(String.class);45var nodeSendInput = AIAgentNode.llmRequest(true, "nodeSendInput");6var nodeExecuteTool = AIAgentNode.executeTool("nodeExecuteTool");7var nodeSendToolResult = AIAgentNode.llmSendToolResult("nodeSendToolResult");89calculatorAgentStrategy.edge(calculatorAgentStrategy.nodeStart, nodeSendInput);10calculatorAgentStrategy.edge(AIAgentEdge.builder()11 .from(nodeSendInput)12 .to(calculatorAgentStrategy.nodeFinish)13 .onIsInstance(Message.Assistant.class)14 .transformed(Message.Assistant::getContent)15 .build());16calculatorAgentStrategy.edge(AIAgentEdge.builder()17 .from(nodeSendInput)18 .to(nodeExecuteTool)19 .onIsInstance(Message.Tool.Call.class)20 .build());21calculatorAgentStrategy.edge(nodeExecuteTool, nodeSendToolResult);22calculatorAgentStrategy.edge(AIAgentEdge.builder()23 .from(nodeSendToolResult)24 .to(calculatorAgentStrategy.nodeFinish)25 .onIsInstance(Message.Assistant.class)26 .transformed(Message.Assistant::getContent)27 .build());28calculatorAgentStrategy.edge(AIAgentEdge.builder()29 .from(nodeSendToolResult)30 .to(nodeExecuteTool)31 .onIsInstance(Message.Tool.Call.class)32 .build());3334var promptExecutor = PromptExecutor.builder()35 .ollama("http://localhost:11434")36 .build();3738AIAgent<String, String> mathAgent = AIAgent.builder()39 .promptExecutor(promptExecutor)40 .llmModel(OllamaModels.Meta.LLAMA_3_2)41 .graphStrategy(calculatorAgentStrategy.build())42 .build();4344 String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);45 System.out.println(result);이 에이전트를 실행하면 다음과 같은 결과가 출력됩니다:
1To calculate this, I'll follow the order of operations:231. Multiply 3 by 4: 3 * 4 = 1242. Multiply the result by 5: 12 * 5 = 6053. Add 10: 60 + 10 = 7064. Add 123: 70 + 123 = 19378The final answer is 193.그러나 이 에이전트에는 툴이 없기 때문에, LLM은 툴 호출을 반환하지 않고 전체 답변을 직접 생성합니다. 실제로 실행되는 흐름은 다음과 같습니다:
1---2config:3 flowchart:4 defaultRenderer: "elk"5---6graph LR7 subgraph nodeStart8 Input9 end1011 subgraph nodeFinish12 Output13 end1415 subgraph nodeSendInput16 llmRequest(Request LLM)17 end1819 Input --String--> llmRequest --Message.Response--> onAssistantMessage{{onAssistantMessage}} --String--> Output이 경우에는 정확한 결과가 나오지만, 실제 답변의 정확도는 LLM의 산술 능력에 따라 달라집니다. 계산의 정확성을 보장하려면 에이전트에 수학 툴을 제공해야 합니다. 그러면 LLM이 결정론적으로 계산을 수행하는 툴을 직접 호출할 수 있게 됩니다.
툴 추가하기
수학 연산을 수행하는 툴을 정의하고 ToolRegistry에 등록합니다:
Kotlin
1@LLMDescription("Tools for performing math operations")2class MathTools : ToolSet {3 @Tool4 @LLMDescription("Adds two numbers and returns the result")5 fun add(a: Int, b: Int): Int {6 // This is not necessary, but it helps to see the tool call in the console output7 println("Adding $a and $b...")8 return a + b9 }10 @Tool11 @LLMDescription("Multiplies two numbers and returns the result")12 fun multiply(a: Int, b: Int): Int {13 // This is not necessary, but it helps to see the tool call in the console output14 println("Multiplying $a and $b...")15 return a * b16 }17}1819val toolRegistry = ToolRegistry {20 tools(MathTools())21}Java
1@LLMDescription("Tools for performing math operations")2public static class MathTools implements ToolSet {3 @Tool4 @LLMDescription("Adds two numbers and returns the result")5 public int add(int a, int b) {6 // This is not necessary, but it helps to see the tool call in the console output7 System.out.println("Adding " + a + " and " + b + "...");8 return a + b;9 }1011 @Tool12 @LLMDescription("Multiplies two numbers and returns the result")13 public int multiply(int a, int b) {14 // This is not necessary, but it helps to see the tool call in the console output15 System.out.println("Multiplying " + a + " and " + b + "...");16 return a * b;17 }18}19public static void main(String[] args) {20 ToolRegistry toolRegistry = ToolRegistry.builder()21 .tools(new MathTools())22 .build();23}툴 레지스트리를 에이전트 설정에 추가합니다:
Kotlin
1val mathAgent = AIAgent(2 promptExecutor = simpleOllamaAIExecutor(),3 llmModel = OllamaModels.Meta.LLAMA_3_2,4 strategy = calculatorAgentStrategy,5 toolRegistry = toolRegistry6)78fun main() = runBlocking {9 val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")10 println(result)11}Java
1AIAgent<String, String> mathAgent = AIAgent.builder()2 .promptExecutor(promptExecutor)3 .llmModel(OllamaModels.Meta.LLAMA_3_2)4 .graphStrategy(calculatorAgentStrategy.build())5 .toolRegistry(toolRegistry)6 .build();78String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);9System.out.println(result);이 에이전트를 실행하면 다음과 같은 결과가 출력됩니다:
1Multiplying 3 and 4...2The output from the first operation was multiplied by 5:35 * 12 = 6045Then, 10 was added to the result:660 + 10 = 7078Finally, 123 was added to the result:970 + 123 = 193이 출력 결과를 보면 에이전트가 계산을 올바르게 수행했지만, multiply 툴을 한 번만 호출했을 뿐
각 연산마다 적절한 툴을 호출하지는 않았습니다.
에이전트가 올바른 툴을 사용하도록 유도하려면, 시스템 프롬프트에서 에이전트의 역할과 툴 사용 지침을 명시해 주면 됩니다.
시스템 프롬프트 제공하기
시스템 프롬프트는 에이전트의 역할과 작업 수행 지침을 정의합니다. 이 예제에서는 복잡한 다단계 계산을 처리하는 방법을 명확히 설명하는 것이 중요합니다:
Kotlin
1val mathAgent = AIAgent(2 promptExecutor = simpleOllamaAIExecutor(),3 llmModel = OllamaModels.Meta.LLAMA_3_2,4 systemPrompt = """5 You are a simple calculator assistant.6 You can add and multiply two numbers using the 'add' and 'multiply' tools.7 When the user provides input, extract the numbers and operations they requested.8 Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result.9 Always respond with a clear, friendly message showing the calculation and result.10 """.trimIndent(),11 toolRegistry = toolRegistry,12 strategy = calculatorAgentStrategy13)1415fun main() = runBlocking {16 val result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.")17 println(result)18}Java
1AIAgent<String, String> mathAgent = AIAgent.builder()2 .promptExecutor(promptExecutor)3 .llmModel(OllamaModels.Meta.LLAMA_3_2)4 .systemPrompt("You are a simple calculator assistant. You can add and multiply two numbers using the 'add' and 'multiply' tools. When the user provides input, extract the numbers and operations they requested. Use the appropriate tool for the first operation, then the next one, and so on, until you calculate the result. Always respond with a clear, friendly message showing the calculation and result.")5 .graphStrategy(calculatorAgentStrategy.build())6 .toolRegistry(toolRegistry)7 .build();89String result = mathAgent.run("Multiply 3 by 4, then multiply the result by 5, then add 10, then add 123.", null);10System.out.println(result);이 에이전트를 실행하면 다음과 같은 결과가 출력됩니다:
1Multiplying 3 and 4...2Multiplying 12 and 5...3Adding 60 and 10...4Adding 70 and 123...5The final result is: 193이제 에이전트가 각 연산마다 적절한 툴을 올바르게 호출하여, 환각(hallucination)에 의존하지 않고 결정론적으로 계산을 수행하는 것을 확인할 수 있습니다.
다음 단계
- 함수형 에이전트 및 플래너 에이전트와 비교해 보세요
- 기능(feature) 설치를 통해 에이전트를 향상시키세요
- 구조화된 출력으로 예측 가능성과 안정성을 높이세요