Koog Framework를 사용하여 AI 체스 플레이어 구축

·5분 읽기

원문: Koog Documentation — Chess 이 글은 Koog 공식 문서의 Chess 페이지를 한국어로 옮긴 번역본입니다. 문서 구조와 링크 의미를 유지하되, MkDocs 전용 UI 문법은 블로그에서 읽기 좋도록 정리했습니다.

Koog Framework를 사용하여 AI 체스 플레이어 구축

:material-github: Open on GitHub{ .md-button .md-button--기본 } :material-download: Download .ipynb{ .md-버튼 }

이 튜토리얼에서는 Koog 프레임워크를 사용하여 지능형 체스 플레이 에이전트를 구축하는 방법을 보여줍니다. 도구 통합, 에이전트 전략, 메모리 최적화 및 대화형 AI 의사 결정을 포함한 주요 개념을 살펴보겠습니다.

당신이 배울 내용

  • 복잡한 게임의 도메인별 데이터 구조를 모델링하는 방법
  • 에이전트가 환경과 상호 작용하는 데 사용할 수 있는 사용자 지정 도구 만들기
  • 메모리 관리를 통한 효율적인 에이전트 전략 구현
  • 선택 선택 기능을 갖춘 대화형 AI 시스템 구축
  • 턴제 게임의 에이전트 성능 최적화

설정

먼저 Koog 프레임워크를 가져오고 개발 환경을 설정해 보겠습니다.

1%useLatestDescriptors2%use koog

체스 도메인 모델링

강력한 도메인 모델을 만드는 것은 모든 게임 AI에 필수적입니다. 체스에서는 플레이어, 기물 및 이들의 관계를 표현해야 합니다. 핵심 데이터 구조를 정의하는 것부터 시작해 보겠습니다.

핵심 열거형 및 유형

1enum class Player {2    White, Black, None;34    fun opponent(): Player = when (this) {5        White -> Black6        Black -> White7        None -> throw IllegalArgumentException("No opponent for None player")8    }9}1011enum class PieceType(val id: Char) {12    King('K'), Queen('Q'), Rook('R'),13    Bishop('B'), Knight('N'), Pawn('P'), None('*');1415    companion object {16        fun fromId(id: String): PieceType {17            require(id.length == 1) { "Invalid piece id: $id" }1819            return entries.first { it.id == id.single() }20        }21    }22}2324enum class Side {25    King, Queen26}

Player 열거형은 플레이어 간 쉬운 전환을 위한 opponent() 메서드를 사용하여 체스의 양면을 나타냅니다. PieceType 열거형은 각 체스 말을 표준 표기 문자에 매핑하여 체스 동작을 쉽게 분석할 수 있도록 합니다.

Side 열거형은 킹사이드와 퀸사이드 캐슬링 동작을 구별하는 데 도움이 됩니다.

조각 및 위치 모델링

1data class Piece(val pieceType: PieceType, val player: Player) {2    init {3        require((pieceType == PieceType.None) == (player == Player.None)) {4            "Invalid piece: $pieceType $player"5        }6    }78    fun toChar(): Char = when (player) {9        Player.White -> pieceType.id.uppercaseChar()10        Player.Black -> pieceType.id.lowercaseChar()11        Player.None -> pieceType.id12    }1314    fun isNone(): Boolean = pieceType == PieceType.None1516    companion object {17        val None = Piece(PieceType.None, Player.None)18    }19}2021data class Position(val row: Int, val col: Char) {22    init {23        require(row in 1..8 && col in 'a'..'h') { "Invalid position: $col$row" }24    }2526    constructor(position: String) : this(27        position[1].digitToIntOrNull() ?: throw IllegalArgumentException("Incorrect position: $position"),28        position[0],29    ) {30        require(position.length == 2) { "Invalid position: $position" }31    }32}3334class ChessBoard {35    private val backRow = listOf(36        PieceType.Rook, PieceType.Knight, PieceType.Bishop,37        PieceType.Queen, PieceType.King,38        PieceType.Bishop, PieceType.Knight, PieceType.Rook39    )4041    private val board: List<MutableList<Piece>> = listOf(42        backRow.map { Piece(it, Player.Black) }.toMutableList(),43        List(8) { Piece(PieceType.Pawn, Player.Black) }.toMutableList(),44        List(8) { Piece.None }.toMutableList(),45        List(8) { Piece.None }.toMutableList(),46        List(8) { Piece.None }.toMutableList(),47        List(8) { Piece.None }.toMutableList(),48        List(8) { Piece(PieceType.Pawn, Player.White) }.toMutableList(),49        backRow.map { Piece(it, Player.White) }.toMutableList()50    )5152    override fun toString(): String = board53        .withIndex().joinToString("\n") { (index, row) ->54            "${8 - index} ${row.map { it.toChar() }.joinToString(" ")}"55        } + "\n  a b c d e f g h"5657    fun getPiece(position: Position): Piece = board[8 - position.row][position.col - 'a']58    fun setPiece(position: Position, piece: Piece) {59        board[8 - position.row][position.col - 'a'] = piece60    }61}

Piece 데이터 클래스는 시각적 표현에서 흰색 조각에는 대문자, 검은색 조각에는 소문자를 사용하여 조각 유형과 소유자를 결합합니다. Position 클래스는 내장된 유효성 검사를 통해 체스 좌표(예: "e4")를 캡슐화합니다.

게임 상태 관리

체스보드 구현

ChessBoard 클래스는 8×8 그리드와 조각 위치를 관리합니다. 주요 설계 결정에는 다음이 포함됩니다.

  • 내부 표현: 효율적인 액세스 및 수정을 위해 변경 가능한 목록을 사용합니다.
  • 시각적 디스플레이: toString() 방법은 순위 번호와 파일 문자로 명확한 ASCII 표현을 제공합니다.
  • 위치 매핑: 체스 표기법(a1-h8)과 내부 배열 인덱스 간 변환

체스게임 로직

1/**2 * Simple chess game without checks for valid moves.3 * Stores a correct state of the board if the entered moves are valid4 */5class ChessGame {6    private val board: ChessBoard = ChessBoard()7    private var currentPlayer: Player = Player.White8    val moveNotation: String = """9        0-0 - short castle10        0-0-0 - long castle11        <piece>-<from>-<to> - usual move. e.g. p-e2-e412        <piece>-<from>-<to>-<promotion> - promotion move. e.g. p-e7-e8-q.13        Piece names:14            p - pawn15            n - knight16            b - bishop17            r - rook18            q - queen19            k - king20    """.trimIndent()2122    fun move(move: String) {23        when {24            move == "0-0" -> castleMove(Side.King)25            move == "0-0-0" -> castleMove(Side.Queen)26            move.split("-").size == 3 -> {27                val (_, from, to) = move.split("-")28                usualMove(Position(from), Position(to))29            }3031            move.split("-").size == 4 -> {32                val (piece, from, to, promotion) = move.split("-")3334                require(PieceType.fromId(piece) == PieceType.Pawn) { "Only pawn can be promoted" }3536                usualMove(Position(from), Position(to))37                board.setPiece(Position(to), Piece(PieceType.fromId(promotion), currentPlayer))38            }3940            else -> throw IllegalArgumentException("Invalid move: $move")41        }4243        updateCurrentPlayer()44    }4546    fun getBoard(): String = board.toString()47    fun currentPlayer(): String = currentPlayer.name.lowercase()4849    private fun updateCurrentPlayer() {50        currentPlayer = currentPlayer.opponent()51    }5253    private fun usualMove(from: Position, to: Position) {54        if (board.getPiece(from).pieceType == PieceType.Pawn && from.col != to.col && board.getPiece(to).isNone()) {55            // the move is en passant56            board.setPiece(Position(from.row, to.col), Piece.None)57        }5859        movePiece(from, to)60    }6162    private fun castleMove(side: Side) {63        val row = if (currentPlayer == Player.White) 1 else 864        val kingFrom = Position(row, 'e')65        val (rookFrom, kingTo, rookTo) = if (side == Side.King) {66            Triple(Position(row, 'h'), Position(row, 'g'), Position(row, 'f'))67        } else {68            Triple(Position(row, 'a'), Position(row, 'c'), Position(row, 'd'))69        }7071        movePiece(kingFrom, kingTo)72        movePiece(rookFrom, rookTo)73    }7475    private fun movePiece(from: Position, to: Position) {76        board.setPiece(to, board.getPiece(from))77        board.setPiece(from, Piece.None)78    }79}

ChessGame 클래스는 게임 로직을 조정하고 상태를 유지합니다. 주목할만한 기능은 다음과 같습니다:

  • 이동 표기법 지원: 일반 이동, 캐슬링(0-0, 0-0-0) 및 폰 승격에 대한 표준 체스 표기법을 허용합니다.
  • 특수 이동 처리: 앙파상 캡처 및 캐슬링 로직 구현
  • 턴 관리: 각 이동 후 자동으로 플레이어 간 교대
  • 검증: 이동 합법성을 검증하지는 않지만(AI가 유효한 이동을 하도록 신뢰) 이동 구문 분석 및 상태 업데이트를 올바르게 처리합니다.

moveNotation 문자열은 AI 에이전트에 허용되는 이동 형식에 대한 명확한 문서를 제공합니다.

Koog 프레임워크와 통합

사용자 정의 도구 만들기

1import kotlinx.serialization.Serializable23class Move(val game: ChessGame) : SimpleTool<Move.Args>(4    argsSerializer = Args.serializer(),5    descriptor = ToolDescriptor(6        name = "move",7        description = "Moves a piece according to the notation:\n${game.moveNotation}",8        requiredParameters = listOf(9            ToolParameterDescriptor(10                name = "notation",11                description = "The notation of the piece to move",12                type = ToolParameterType.String,13            )14        )15    )16) {17    @Serializable18    data class Args(val notation: String) : ToolArgs1920    override suspend fun execute(args: Args): String {21        game.move(args.notation)22        println(game.getBoard())23        println("-----------------")24        return "Current state of the game:\n${game.getBoard()}\n${game.currentPlayer()} to move! Make the move!"25    }26}

Move 도구는 Koog 프레임워크의 도구 통합 패턴을 보여줍니다.

  1. SimpleTool 확장: 유형이 안전한 인수 처리로 기본 도구 기능을 상속합니다.
  2. 직렬화 가능한 인수: Kotlin 직렬화를 사용하여 도구의 입력 매개변수를 정의합니다.
  3. 풍부한 문서: ToolDescriptor는 도구의 목적과 매개변수에 대한 자세한 정보를 LLM에 제공합니다.
  4. 생성자 매개변수: argsSerializerdescriptor를 생성자에 전달합니다.
  5. 실행 논리: execute 메서드는 실제 이동 실행을 처리하고 형식화된 피드백을 제공합니다.

주요 설계 측면:

  • 컨텍스트 주입: 도구는 ChessGame 인스턴스를 수신하여 게임 상태를 수정할 수 있습니다.
  • 피드백 루프: 현재 보드 상태를 반환하고 다음 플레이어에게 메시지를 표시하여 대화 흐름을 유지합니다.
  • 오류 처리: 이동 확인 및 오류 보고를 위해 게임 클래스에 의존합니다.

에이전트 전략 설계

메모리 최적화 기술

1import ai.koog.agents.core.environment.ReceivedToolResult23/**4 * Chess position is (almost) completely defined by the board state,5 * So we can trim the history of the LLM to only contain the system prompt and the last move.6 */7inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(8    name: String? = null9): AIAgentNodeDelegate<T, T> = node(name) { result ->10    llm.writeSession {11        rewritePrompt { prompt ->12            val messages = prompt.messages1314            prompt.copy(messages = listOf(messages.first(), messages.last()))15        }16    }1718    result19}2021val strategy = strategy<String, String>("chess_strategy") {22    val nodeCallLLM by nodeLLMRequest("sendInput")23    val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")24    val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult")25    val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()2627    edge(nodeStart forwardTo nodeCallLLM)28    edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })29    edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })30    edge(nodeExecuteTool forwardTo nodeTrimHistory)31    edge(nodeTrimHistory forwardTo nodeSendToolResult)32    edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })33    edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })34}

nodeTrimHistory 함수는 체스 게임에 중요한 최적화를 구현합니다. 체스 위치는 전체 이동 내역이 아닌 현재 보드 상태에 따라 크게 결정되므로 다음만 유지하면 토큰 사용량을 크게 줄일 수 있습니다.

  1. 시스템 프롬프트: 상담원의 핵심 지침과 행동 지침이 포함되어 있습니다.
  2. 최신 메시지: 최신 보드 상태 및 게임 컨텍스트

이 접근 방식은 다음과 같습니다.

  • 토큰 소비 감소: 대화 기록의 기하급수적인 증가를 방지합니다.
  • 컨텍스트 유지: 필수 게임 상태 정보를 보존합니다.
  • 성능 향상: 짧은 프롬프트로 더 빠른 처리
  • 장시간 게임 가능: 토큰 제한에 도달하지 않고도 확장된 게임 플레이가 가능합니다.

체스 전략은 Koog의 그래프 기반 에이전트 아키텍처를 보여줍니다.

노드 유형:

  • nodeCallLLM: 입력을 처리하고 응답/도구 호출을 생성합니다.
  • nodeExecuteTool: 제공된 매개변수를 사용하여 이동 도구를 실행합니다.
  • nodeTrimHistory: 위에서 설명한 대로 대화 메모리를 최적화합니다.
  • nodeSendToolResult: 도구 실행 결과를 LLM으로 다시 보냅니다.

제어 흐름:

  • 선형 경로: 시작 → LLM 요청 → 도구 실행 → 기록 다듬기 → 결과 보내기
  • 결정 포인트: LLM 응답은 대화를 끝내거나 다른 도구 호출을 트리거할 수 있습니다.
  • 메모리 관리: 각 도구 실행 후에 기록 트리밍이 발생합니다.

이 전략은 대화의 일관성을 유지하면서 효율적이고 상태가 유지되는 게임플레이를 보장합니다.

AI 에이전트 설정

1val baseExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY"))

이 섹션에서는 OpenAI 실행기를 초기화합니다. simpleOpenAIExecutor은 환경 변수의 API 키를 사용하여 OpenAI API에 대한 연결을 생성합니다.

구성 참고 사항:

  • OpenAI API 키를 OPENAI_API_KEY 환경 변수에 저장하세요.
  • 실행자는 인증 및 API 통신을 자동으로 처리합니다.
  • 다양한 LLM 제공업체에 대해 다양한 실행자 유형을 사용할 수 있습니다.

에이전트 조립

1val game = ChessGame()2val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }34// Create a chat agent with a system prompt and the tool registry5val agent = AIAgent(6    executor = baseExecutor,7    strategy = strategy,8    llmModel = OpenAIModels.Chat.O3Mini,9    systemPrompt = """10            You are an agent who plays chess.11            You should always propose a move in response to the "Your move!" message.1213            DO NOT HALLUCINATE!!!14            DO NOT PLAY ILLEGAL MOVES!!!15            YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!16        """.trimMargin(),17    temperature = 0.0,18    toolRegistry = toolRegistry,19    maxIterations = 200,20)

여기서는 모든 구성 요소를 기능적인 체스 플레이 에이전트로 조립합니다.

주요 구성:

  • 모델 선택: 고품질 체스 플레이를 위해 OpenAIModels.Chat.O3Mini 사용
  • 온도: 결정적, 전략적 이동을 위해 0.0으로 설정
  • 시스템 프롬프트: 합법적인 움직임과 올바른 행동을 강조하는 세심하게 작성된 지침
  • 도구 레지스트리: 에이전트에게 이동 도구에 대한 액세스를 제공합니다.
  • 최대 반복 횟수: 전체 게임을 허용하려면 200으로 설정하세요.

시스템 프롬프트 디자인:

  • 이전 제안의 책임을 강조합니다.
  • 환각 및 불법적인 이동을 금지합니다.
  • 메시지를 사임 또는 장군 선언으로만 제한합니다.
  • 집중적이고 게임 지향적인 행동을 만들어냅니다.

기본 에이전트 실행

1import kotlinx.coroutines.runBlocking23println("Chess Game started!")45val initialMessage = "Starting position is ${game.getBoard()}. White to move!"67runBlocking {8    agent.run(initialMessage)9}

체스 게임이 시작되었습니다! 8 r n b q k b n r 7p p p p p p p p 6 * * * * * * * * 5 * * * * * * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * 아니오 * * 2P P P P * P P P 1 R N B Q K B * R a b c d e f g h

8 r n b q k b * r 7 p p p p * p p p 6 * * * * * n * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * 아니오 * * 2P P P P * P P P 1 R N B Q K B * R a b c d e f g h

8 r n b q k b * r 7 p p p p * p p p 6 * * * * * n * * 5 * * * * p * * * 4 * * * * P * * * 3 * * 아니오 * * 아니오 * * 2P P P P * P P P 1 R * B Q K B * R a b c d e f g h

실행이 중단되었습니다.

이 기본 에이전트는 자율적으로 플레이하며 자동으로 움직입니다. 게임 출력에는 AI가 자신과 대결할 때 일련의 동작과 보드 상태가 표시됩니다.

고급 기능: 대화형 선택 선택

다음 섹션에서는 사용자가 AI에서 생성된 여러 동작 중에서 선택하여 AI의 의사 결정 프로세스에 참여할 수 있는 보다 정교한 접근 방식을 보여줍니다.

맞춤형 선택 선택 전략

1import ai.koog.agents.core.feature.choice.ChoiceSelectionStrategy23/**4 * `AskUserChoiceStrategy` allows users to interactively select a choice from a list of options5 * presented by a language model. The strategy uses customizable methods to display the prompt6 * and choices and read user input to determine the selected choice.7 *8 * @property promptShowToUser A function that formats and displays a given `Prompt` to the user.9 * @property choiceShowToUser A function that formats and represents a given `LLMChoice` to the user.10 * @property print A function responsible for displaying messages to the user, e.g., for showing prompts or feedback.11 * @property read A function to capture user input.12 */13class AskUserChoiceSelectionStrategy(14    private val promptShowToUser: (Prompt) -> String = { "Current prompt: $it" },15    private val choiceShowToUser: (LLMChoice) -> String = { "$it" },16    private val print: (String) -> Unit = ::println,17    private val read: () -> String? = ::readlnOrNull18) : ChoiceSelectionStrategy {19    override suspend fun choose(prompt: Prompt, choices: List<LLMChoice>): LLMChoice {20        print(promptShowToUser(prompt))2122        print("Available LLM choices")2324        choices.withIndex().forEach { (index, choice) ->25            print("Choice number ${index + 1}: ${choiceShowToUser(choice)}")26        }2728        var choiceNumber = ask(choices.size)29        while (choiceNumber == null) {30            print("Invalid response.")31            choiceNumber = ask(choices.size)32        }3334        return choices[choiceNumber - 1]35    }3637    private fun ask(numChoices: Int): Int? {38        print("Please choose a choice. Enter a number between 1 and $numChoices: ")3940        return read()?.toIntOrNull()?.takeIf { it in 1..numChoices }41    }42}

AskUserChoiceSelectionStrategy은 Koog의 ChoiceSelectionStrategy 인터페이스를 구현하여 AI 의사 결정에 인간이 참여할 수 있도록 합니다.

주요 기능:

  • 사용자 정의 가능한 디스플레이: 프롬프트 및 선택 항목의 형식을 지정하는 기능
  • 대화형 입력: 사용자 상호작용을 위해 표준 입력/출력을 사용합니다.
  • 검증: 사용자 입력이 유효한 범위 내에 있는지 확인합니다.
  • 유연한 I/O: 다양한 환경에 맞게 구성 가능한 인쇄 및 읽기 기능

사용 사례:

  • 게임플레이에서의 인간-AI ​​협업
  • AI 결정 투명성 및 설명 가능성
  • 훈련 및 디버깅 시나리오
  • 교육 시연

선택 선택을 통한 강화된 전략

1inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(2    name: String? = null3): AIAgentNodeDelegate<T, T> = node(name) { result ->4    llm.writeSession {5        rewritePrompt { prompt ->6            val messages = prompt.messages78            prompt.copy(messages = listOf(messages.first(), messages.last()))9        }10    }1112    result13}1415val strategy = strategy<String, String>("chess_strategy") {16    val nodeCallLLM by nodeLLMRequest("sendInput")17    val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")18    val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult")19    val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()2021    edge(nodeStart forwardTo nodeCallLLM)22    edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })23    edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })24    edge(nodeExecuteTool forwardTo nodeTrimHistory)25    edge(nodeTrimHistory forwardTo nodeSendToolResult)26    edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })27    edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })28}2930val askChoiceStrategy = AskUserChoiceSelectionStrategy(promptShowToUser = { prompt ->31    val lastMessage = prompt.messages.last()32    if (lastMessage is Message.Tool.Call) {33        lastMessage.content34    } else {35        ""36    }37})
1val promptExecutor = PromptExecutorWithChoiceSelection(baseExecutor, askChoiceStrategy)

첫 번째 대화형 접근 방식은 기본 실행기를 선택 선택 기능으로 래핑하는 PromptExecutorWithChoiceSelection을 사용합니다. 사용자 정의 디스플레이 기능은 도구 호출에서 이동 정보를 추출하여 AI가 원하는 작업을 사용자에게 보여줍니다.

아키텍처 변경:

  • 래핑된 실행자: PromptExecutorWithChoiceSelection는 모든 기본 실행자에 선택 기능을 추가합니다.
  • 상황 인식 디스플레이: 전체 프롬프트 대신 마지막 도구 호출 내용을 표시합니다.
  • 더 높은 온도: 더 다양한 이동 옵션을 위해 1.0으로 증가했습니다.

고급 전략: 수동 선택 선택

1val game = ChessGame()2val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }34val agent = AIAgent(5    executor = promptExecutor,6    strategy = strategy,7    llmModel = OpenAIModels.Chat.O3Mini,8    systemPrompt = """9            You are an agent who plays chess.10            You should always propose a move in response to the "Your move!" message.1112            DO NOT HALLUCINATE!!!13            DO NOT PLAY ILLEGAL MOVES!!!14            YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!15        """.trimMargin(),16    temperature = 1.0,17    toolRegistry = toolRegistry,18    maxIterations = 200,19    numberOfChoices = 3,20)

고급 전략은 선택 선택을 에이전트의 실행 그래프에 직접 통합합니다.

새 노드:

  • nodeLLMSendResultsMultipleChoices: 여러 LLM 선택을 동시에 처리합니다.
  • nodeSelectLLMChoice: 선택 선택 전략을 워크플로에 통합합니다.

향상된 제어 흐름:

  • 도구 결과는 다중 선택을 지원하기 위해 목록에 포함됩니다.
  • 선택한 경로를 계속하기 전에 사용자 선택이 발생합니다.
  • 선택한 선택 항목이 풀리고 일반 흐름을 통해 계속됩니다.

이익:

  • 강화된 제어: 상담사 워크플로와의 세밀한 통합
  • 유연성: 다른 에이전트 기능과 결합 가능
  • 투명성: 사용자는 AI가 무엇을 고려하고 있는지 정확히 볼 수 있습니다.

대화형 에이전트 실행

1println("Chess Game started!")23val initialMessage = "Starting position is ${game.getBoard()}. White to move!"45runBlocking {6    agent.run(initialMessage)7}

체스 게임이 시작되었습니다!

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_K46Upz7XoBIG5RchDh7bZE8F, tool=move, content={"notation": "p-e2-e4"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:40.368252Z, totalTokensCount=773, inputTokensCount=315, outputTokensCount=458, 추가 정보={}))] 선택 번호 2: [Call(id=call_zJ6OhoCHrVHUNnKaxZkOhwoU, tool=move, content={"notation": "p-e2-e4"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:40.368252Z, totalTokensCount=773, inputTokensCount=315, outputTokensCount=458, 추가 정보={}))] 선택 번호 3: [Call(id=call_nwX6ZMJ3F5AxiNUypYlI4BH4, tool=move, content={"notation": "p-e2-e4"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:40.368252Z, totalTokensCount=773, inputTokensCount=315, outputTokensCount=458, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b n r 7p p p p p p p p 6 * * * * * * * * 5 * * * * * * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_2V93GXOcIe0fAjUAIFEk9h5S, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:47.949303Z, totalTokensCount=1301, inputTokensCount=341, outputTokensCount=960, 추가 정보={}))] 선택 번호 2: [Call(id=call_INM59xRzKMFC1w8UAV74l9e1, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:47.949303Z, totalTokensCount=1301, inputTokensCount=341, outputTokensCount=960, 추가 정보={}))] 선택 번호 3: [Call(id=call_r4QoiTwn0F3jizepHH5ia8BU, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:47.949303Z, totalTokensCount=1301, inputTokensCount=341, outputTokensCount=960, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_f9XTizn41svcrtvnmkCfpSUQ, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:55.467712Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택 번호 2: [Call(id=call_c0Dfce5RcSbN3cOOm5ESYriK, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:55.467712Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택 번호 3: [Call(id=call_Lr4Mdro1iolh0fDyAwZsutrW, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:17:55.467712Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * 아니오 * * 2P P P P * P P P 1 R N B Q K B * R a b c d e f g h

실행이 중단되었습니다.

1import ai.koog.agents.core.feature.choice.nodeLLMSendResultsMultipleChoices2import ai.koog.agents.core.feature.choice.nodeSelectLLMChoice34inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(5    name: String? = null6): AIAgentNodeDelegate<T, T> = node(name) { result ->7    llm.writeSession {8        rewritePrompt { prompt ->9            val messages = prompt.messages1011            prompt.copy(messages = listOf(messages.first(), messages.last()))12        }13    }1415    result16}1718val strategy = strategy<String, String>("chess_strategy") {19    val nodeCallLLM by nodeLLMRequest("sendInput")20    val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")21    val nodeSendToolResult by nodeLLMSendResultsMultipleChoices("nodeSendToolResult")22    val nodeSelectLLMChoice by nodeSelectLLMChoice(askChoiceStrategy, "chooseLLMChoice")23    val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()2425    edge(nodeStart forwardTo nodeCallLLM)26    edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })27    edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })28    edge(nodeExecuteTool forwardTo nodeTrimHistory)29    edge(nodeTrimHistory forwardTo nodeSendToolResult transformed { listOf(it) })30    edge(nodeSendToolResult forwardTo nodeSelectLLMChoice)31    edge(nodeSelectLLMChoice forwardTo nodeFinish transformed { it.first() } onAssistantMessage { true })32    edge(nodeSelectLLMChoice forwardTo nodeExecuteTool transformed { it.first() } onToolCall { true })33}
1val game = ChessGame()2val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }34val agent = AIAgent(5    executor = baseExecutor,6    strategy = strategy,7    llmModel = OpenAIModels.Chat.O3Mini,8    systemPrompt = """9            You are an agent who plays chess.10            You should always propose a move in response to the "Your move!" message.1112            DO NOT HALLUCINATE!!!13            DO NOT PLAY ILLEGAL MOVES!!!14            YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!15        """.trimMargin(),16    temperature = 1.0,17    toolRegistry = toolRegistry,18    maxIterations = 200,19    numberOfChoices = 3,20)
1println("Chess Game started!")23val initialMessage = "Starting position is ${game.getBoard()}. White to move!"45runBlocking {6    agent.run(initialMessage)7}

체스 게임이 시작되었습니다! 8 r n b q k b n r 7p p p p p p p p 6 * * * * * * * * 5 * * * * * * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_gqMIar0z11CyUl5nup3zbutj, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:17.313548Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택 번호 2: [Call(id=call_6niUGnZPPJILRFODIlJsCKax, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:17.313548Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택 번호 3: [Call(id=call_q1b8ZmIBph0EoVaU3Ic9A09j, tool=move, content={"notation": "p-e7-e5"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:17.313548Z, totalTokensCount=917, inputTokensCount=341, outputTokensCount=576, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * * * * 2P P P P * P P P 1 R N B Q K B N R a b c d e f g h

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_pdBIX7MVi82MyWwawTm1Q2ef, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:24.505344Z, totalTokensCount=1237, inputTokensCount=341, outputTokensCount=896, 추가 정보={}))] 선택 번호 2: [Call(id=call_oygsPHaiAW5OM6pxhXhtazgp, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:24.505344Z, totalTokensCount=1237, inputTokensCount=341, outputTokensCount=896, 추가 정보={}))] 선택 번호 3: [Call(id=call_GJTEsZ8J8cqOKZW4Tx54RqCh, tool=move, content={"notation": "n-g1-f3"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:24.505344Z, totalTokensCount=1237, inputTokensCount=341, outputTokensCount=896, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b n r 7 p p p p * p p p 6 * * * * * * * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * 아니오 * * 2P P P P * P P P 1 R N B Q K B * R a b c d e f g h

사용 가능한 LLM 선택 선택 번호 1: [Call(id=call_5C7HdlTU4n3KdXcyNogE4rGb, tool=move, content={"notation": "n-g8-f6"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:34.646667Z, totalTokensCount=1621, inputTokensCount=341, outputTokensCount=1280, 추가 정보={}))] 선택 번호 2: [Call(id=call_EjCcyeMLQ88wMa5yh3vmeJ2w, tool=move, content={"notation": "n-g8-f6"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:34.646667Z, totalTokensCount=1621, inputTokensCount=341, outputTokensCount=1280, 추가 정보={}))] 선택 번호 3: [Call(id=call_NBMMSwmFIa8M6zvfbPw85NKh, tool=move, content={"notation": "n-g8-f6"}, MetaInfo=ResponseMetaInfo(timestamp=2025-08-18T21:18:34.646667Z, totalTokensCount=1621, inputTokensCount=341, outputTokensCount=1280, 추가 정보={}))] 선택사항을 선택해 주세요. 1에서 3 사이의 숫자를 입력하세요. 8 r n b q k b * r 7 p p p p * p p p 6 * * * * * n * * 5 * * * * p * * * 4 * * * * P * * * 3 * * * * * 아니오 * * 2P P P P * P P P 1 R N B Q K B * R a b c d e f g h

실행이 중단되었습니다.

대화형 예시는 사용자가 AI의 의사결정 프로세스를 어떻게 안내할 수 있는지 보여줍니다. 출력에서 다음을 볼 수 있습니다.

  1. 다중 선택: AI는 3가지 다른 이동 옵션을 생성합니다.
  2. 사용자 선택: 사용자는 숫자 1~3을 입력하여 원하는 수를 선택합니다.
  3. 게임 계속: 선택한 동작이 실행되고 게임이 계속됩니다.

결론

이 튜토리얼에서는 Koog 프레임워크를 사용하여 지능형 에이전트를 구축하는 몇 가지 주요 측면을 보여줍니다.

주요 시사점

  1. 도메인 모델링: 잘 구조화된 데이터 모델은 복잡한 애플리케이션에 매우 중요합니다.
  2. 도구 통합: 맞춤 도구를 사용하면 상담원이 외부 시스템과 효과적으로 상호 작용할 수 있습니다.
  3. 메모리 관리: 전략적 기록 트리밍으로 장시간 상호 작용에 대한 성능 최적화
  4. 전략 그래프: Koog의 그래프 기반 접근 방식은 유연한 제어 흐름을 제공합니다.
  5. 대화형 AI: 선택 선택을 통해 인간과 AI의 협업 및 투명성이 가능해집니다.

프레임워크 기능 살펴보기

  • ✅ 맞춤형 도구 생성 및 통합
  • ✅ 에이전트 전략 설계 및 그래프 기반 제어 흐름
  • ✅ 메모리 최적화 기술
  • ✅ 대화형 선택 선택
  • ✅ 다중 LLM 응답 처리
  • ✅ 상태 저장 게임 관리

Koog 프레임워크는 효율성과 투명성을 유지하면서 복잡한 다중 턴 상호 작용을 처리할 수 있는 정교한 AI 에이전트를 구축하기 위한 기반을 제공합니다.