Koog Framework를 사용하여 AI 체스 플레이어 구축
원문: 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 프레임워크의 도구 통합 패턴을 보여줍니다.
- SimpleTool 확장: 유형이 안전한 인수 처리로 기본 도구 기능을 상속합니다.
- 직렬화 가능한 인수: Kotlin 직렬화를 사용하여 도구의 입력 매개변수를 정의합니다.
- 풍부한 문서:
ToolDescriptor는 도구의 목적과 매개변수에 대한 자세한 정보를 LLM에 제공합니다. - 생성자 매개변수:
argsSerializer및descriptor를 생성자에 전달합니다. - 실행 논리:
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 함수는 체스 게임에 중요한 최적화를 구현합니다. 체스 위치는 전체 이동 내역이 아닌 현재 보드 상태에 따라 크게 결정되므로 다음만 유지하면 토큰 사용량을 크게 줄일 수 있습니다.
- 시스템 프롬프트: 상담원의 핵심 지침과 행동 지침이 포함되어 있습니다.
- 최신 메시지: 최신 보드 상태 및 게임 컨텍스트
이 접근 방식은 다음과 같습니다.
- 토큰 소비 감소: 대화 기록의 기하급수적인 증가를 방지합니다.
- 컨텍스트 유지: 필수 게임 상태 정보를 보존합니다.
- 성능 향상: 짧은 프롬프트로 더 빠른 처리
- 장시간 게임 가능: 토큰 제한에 도달하지 않고도 확장된 게임 플레이가 가능합니다.
체스 전략은 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의 의사결정 프로세스를 어떻게 안내할 수 있는지 보여줍니다. 출력에서 다음을 볼 수 있습니다.
- 다중 선택: AI는 3가지 다른 이동 옵션을 생성합니다.
- 사용자 선택: 사용자는 숫자 1~3을 입력하여 원하는 수를 선택합니다.
- 게임 계속: 선택한 동작이 실행되고 게임이 계속됩니다.
결론
이 튜토리얼에서는 Koog 프레임워크를 사용하여 지능형 에이전트를 구축하는 몇 가지 주요 측면을 보여줍니다.
주요 시사점
- 도메인 모델링: 잘 구조화된 데이터 모델은 복잡한 애플리케이션에 매우 중요합니다.
- 도구 통합: 맞춤 도구를 사용하면 상담원이 외부 시스템과 효과적으로 상호 작용할 수 있습니다.
- 메모리 관리: 전략적 기록 트리밍으로 장시간 상호 작용에 대한 성능 최적화
- 전략 그래프: Koog의 그래프 기반 접근 방식은 유연한 제어 흐름을 제공합니다.
- 대화형 AI: 선택 선택을 통해 인간과 AI의 협업 및 투명성이 가능해집니다.
프레임워크 기능 살펴보기
- ✅ 맞춤형 도구 생성 및 통합
- ✅ 에이전트 전략 설계 및 그래프 기반 제어 흐름
- ✅ 메모리 최적화 기술
- ✅ 대화형 선택 선택
- ✅ 다중 LLM 응답 처리
- ✅ 상태 저장 게임 관리
Koog 프레임워크는 효율성과 투명성을 유지하면서 복잡한 다중 턴 상호 작용을 처리할 수 있는 정교한 AI 에이전트를 구축하기 위한 기반을 제공합니다.