Koog 문서 한국어 번역 08: 그래프 기반 에이전트

·14분 읽기

원문: 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.AssistantString으로 변환하는 효과도 있습니다 (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)에 의존하지 않고 결정론적으로 계산을 수행하는 것을 확인할 수 있습니다.

다음 단계