Koog 프레임워크로 AI 체스 플레이어 구축하기
이 튜토리얼은 Koog 프레임워크를 사용하여 지능형 체스 플레이 에이전트를 구축하는 방법을 보여줍니다. 도구 통합, 에이전트 전략, 메모리 최적화 및 상호작용형 AI 의사 결정을 포함한 주요 개념을 탐구합니다.
학습 목표
- 복잡한 게임을 위한 도메인별 데이터 구조를 모델링하는 방법
- 에이전트가 환경과 상호작용하는 데 사용할 수 있는 사용자 지정 도구 만들기
- 메모리 관리를 통한 효율적인 에이전트 전략 구현
- 선택 기능이 있는 상호작용형 AI 시스템 구축
- 턴제 게임을 위한 에이전트 성능 최적화
설정
먼저 Koog 프레임워크를 임포트하고 개발 환경을 설정해 보겠습니다.
%useLatestDescriptors
%use koog체스 도메인 모델링
견고한 도메인 모델을 만드는 것은 모든 게임 AI에 필수적입니다. 체스에서는 플레이어, 말(piece) 및 이들 간의 관계를 나타내야 합니다. 핵심 데이터 구조를 정의하는 것부터 시작하겠습니다.
핵심 열거형 및 유형
enum class Player {
White, Black, None;
fun opponent(): Player = when (this) {
White -> Black
Black -> White
None -> throw IllegalArgumentException("No opponent for None player")
}
}
enum class PieceType(val id: Char) {
King('K'), Queen('Q'), Rook('R'),
Bishop('B'), Knight('N'), Pawn('P'), None('*');
companion object {
fun fromId(id: String): PieceType {
require(id.length == 1) { "Invalid piece id: $id" }
return entries.first { it.id == id.single() }
}
}
}
enum class Side {
King, Queen
}Player 열거형은 체스의 두 진영을 나타내며, 플레이어 간의 손쉬운 전환을 위한 opponent() 메서드를 포함합니다. PieceType 열거형은 각 체스 말을 표준 표기 문자(notation character)에 매핑하여 체스 수(move)를 쉽게 파싱할 수 있도록 합니다.
Side 열거형은 킹 사이드와 퀸 사이드 캐슬링 수를 구분하는 데 도움이 됩니다.
말(Piece) 및 위치 모델링
data class Piece(val pieceType: PieceType, val player: Player) {
init {
require((pieceType == PieceType.None) == (player == Player.None)) {
"Invalid piece: $pieceType $player"
}
}
fun toChar(): Char = when (player) {
Player.White -> pieceType.id.uppercaseChar()
Player.Black -> pieceType.id.lowercaseChar()
Player.None -> pieceType.id
}
fun isNone(): Boolean = pieceType == PieceType.None
companion object {
val None = Piece(PieceType.None, Player.None)
}
}
data class Position(val row: Int, val col: Char) {
init {
require(row in 1..8 && col in 'a'..'h') { "Invalid position: $col$row" }
}
constructor(position: String) : this(
position[1].digitToIntOrNull() ?: throw IllegalArgumentException("Incorrect position: $position"),
position[0],
) {
require(position.length == 2) { "Invalid position: $position" }
}
}
class ChessBoard {
private val backRow = listOf(
PieceType.Rook, PieceType.Knight, PieceType.Bishop,
PieceType.Queen, PieceType.King,
PieceType.Bishop, PieceType.Knight, PieceType.Rook
)
private val board: List<MutableList<Piece>> = listOf(
backRow.map { Piece(it, Player.Black) }.toMutableList(),
List(8) { Piece(PieceType.Pawn, Player.Black) }.toMutableList(),
List(8) { Piece.None }.toMutableList(),
List(8) { Piece.None }.toMutableList(),
List(8) { Piece.None }.toMutableList(),
List(8) { Piece.None }.toMutableList(),
List(8) { Piece(PieceType.Pawn, Player.White) }.toMutableList(),
backRow.map { Piece(it, Player.White) }.toMutableList()
)
override fun toString(): String = board
.withIndex().joinToString("
") { (index, row) ->
"${8 - index} ${row.map { it.toChar() }.joinToString(" ")}"
} + "
a b c d e f g h"
fun getPiece(position: Position): Piece = board[8 - position.row][position.col - 'a']
fun setPiece(position: Position, piece: Piece) {
board[8 - position.row][position.col - 'a'] = piece
}
}Piece 데이터 클래스는 말 유형을 소유자와 결합하며, 시각적 표현에서 백 말을 대문자로, 흑 말을 소문자로 사용합니다. Position 클래스는 내장된 유효성 검사를 통해 체스 좌표(예: "e4")를 캡슐화합니다.
게임 상태 관리
ChessBoard 구현
ChessBoard 클래스는 8x8 그리드와 말 위치를 관리합니다. 주요 설계 결정은 다음과 같습니다.
- 내부 표현: 효율적인 접근 및 수정을 위해 변경 가능한 리스트(mutable list)의 리스트를 사용
- 시각적 표시:
toString()메서드는 랭크 숫자와 파일 문자가 포함된 명확한 ASCII 표현을 제공합니다 - 위치 매핑: 체스 표기법(a1-h8)과 내부 배열 인덱스 간의 변환
ChessGame 로직
/**
* Simple chess game without checks for valid moves.
* Stores a correct state of the board if the entered moves are valid
*/
class ChessGame {
private val board: ChessBoard = ChessBoard()
private var currentPlayer: Player = Player.White
val moveNotation: String = """
0-0 - short castle
0-0-0 - long castle
<piece>-<from>-<to> - usual move. e.g. p-e2-e4
<piece>-<from>-<to>-<promotion> - promotion move. e.g. p-e7-e8-q.
Piece names:
p - pawn
n - knight
b - bishop
r - rook
q - queen
k - king
""".trimIndent()
fun move(move: String) {
when {
move == "0-0" -> castleMove(Side.King)
move == "0-0-0" -> castleMove(Side.Queen)
move.split("-").size == 3 -> {
val (_, from, to) = move.split("-")
usualMove(Position(from), Position(to))
}
move.split("-").size == 4 -> {
val (piece, from, to, promotion) = move.split("-")
require(PieceType.fromId(piece) == PieceType.Pawn) { "Only pawn can be promoted" }
usualMove(Position(from), Position(to))
board.setPiece(Position(to), Piece(PieceType.fromId(promotion), currentPlayer))
}
else -> throw IllegalArgumentException("Invalid move: $move")
}
updateCurrentPlayer()
}
fun getBoard(): String = board.toString()
fun currentPlayer(): String = currentPlayer.name.lowercase()
private fun updateCurrentPlayer() {
currentPlayer = currentPlayer.opponent()
}
private fun usualMove(from: Position, to: Position) {
if (board.getPiece(from).pieceType == PieceType.Pawn && from.col != to.col && board.getPiece(to).isNone()) {
// the move is en passant
board.setPiece(Position(from.row, to.col), Piece.None)
}
movePiece(from, to)
}
private fun castleMove(side: Side) {
val row = if (currentPlayer == Player.White) 1 else 8
val kingFrom = Position(row, 'e')
val (rookFrom, kingTo, rookTo) = if (side == Side.King) {
Triple(Position(row, 'h'), Position(row, 'g'), Position(row, 'f'))
} else {
Triple(Position(row, 'a'), Position(row, 'c'), Position(row, 'd'))
}
movePiece(kingFrom, kingTo)
movePiece(rookFrom, rookTo)
}
private fun movePiece(from: Position, to: Position) {
board.setPiece(to, board.getPiece(from))
board.setPiece(from, Piece.None)
}
}ChessGame 클래스는 게임 로직을 조정하고 상태를 유지합니다. 주목할 만한 기능은 다음과 같습니다.
- 수 표기법 지원: 일반 수, 캐슬링(0-0, 0-0-0) 및 폰 승급을 위한 표준 체스 표기법을 허용합니다
- 특별 수 처리: 앙파상(en passant) 잡기 및 캐슬링 로직을 구현합니다
- 턴 관리: 각 수 이후 플레이어 간에 자동으로 교대합니다
- 유효성 검사: 수의 합법성을 검사하지는 않지만(AI가 유효한 수를 둘 것으로 신뢰), 수 파싱 및 상태 업데이트를 올바르게 처리합니다
moveNotation 문자열은 AI 에이전트에게 허용되는 수 형식에 대한 명확한 문서를 제공합니다.
Koog 프레임워크와 통합하기
사용자 지정 도구 만들기
import kotlinx.serialization.Serializable
class Move(val game: ChessGame) : SimpleTool<Move.Args>() {
@Serializable
data class Args(val notation: String) : ToolArgs
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor(
name = "move",
description = "Moves a piece according to the notation:
${game.moveNotation}",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "notation",
description = "The notation of the piece to move",
type = ToolParameterType.String,
)
)
)
override suspend fun doExecute(args: Args): String {
game.move(args.notation)
println(game.getBoard())
println("-----------------")
return "Current state of the game:
${game.getBoard()}
${game.currentPlayer()} to move! Make the move!"
}
}Move 도구는 Koog 프레임워크의 도구 통합 패턴을 보여줍니다.
- SimpleTool 확장: 타입 안전 인자(argument) 처리와 함께 기본 도구 기능을 상속합니다
- 직렬화 가능한 인자: Kotlin 직렬화를 사용하여 도구의 입력 매개변수를 정의합니다
- 풍부한 문서화:
ToolDescriptor는 도구의 목적과 매개변수에 대한 자세한 정보를 LLM에 제공합니다 - 실행 로직:
doExecute메서드는 실제 수 실행을 처리하고 형식화된 피드백을 제공합니다
주요 설계 측면:
- 컨텍스트 주입: 도구는
ChessGame인스턴스를 받아 게임 상태를 수정할 수 있습니다 - 피드백 루프: 현재 보드 상태를 반환하고 다음 플레이어에게 프롬프트를 표시하여 대화 흐름을 유지합니다
- 오류 처리: 수 유효성 검사 및 오류 보고를 위해 게임 클래스에 의존합니다
에이전트 전략 설계
메모리 최적화 기술
import ai.koog.agents.core.environment.ReceivedToolResult
/**
* Chess position is (almost) completely defined by the board state,
* So we can trim the history of the LLM to only contain the system prompt and the last move.
*/
inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(
name: String? = null
): AIAgentNodeDelegate<T, T> = node(name) { result ->
llm.writeSession {
rewritePrompt { prompt ->
val messages = prompt.messages
prompt.copy(messages = listOf(messages.first(), messages.last()))
}
}
result
}
val strategy = strategy<String, String>("chess_strategy") {
val nodeCallLLM by nodeLLMRequest("sendInput")
val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")
val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult")
val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()
edge(nodeStart forwardTo nodeCallLLM)
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })
edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })
edge(nodeExecuteTool forwardTo nodeTrimHistory)
edge(nodeTrimHistory forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
}nodeTrimHistory 함수는 체스 게임에 중요한 최적화를 구현합니다. 체스 위치는 전체 수 기록보다는 현재 보드 상태에 의해 크게 결정되므로, 다음 항목만 유지하여 토큰 사용량을 크게 줄일 수 있습니다.
- 시스템 프롬프트: 에이전트의 핵심 지침과 행동 가이드라인을 포함합니다
- 최신 메시지: 가장 최근의 보드 상태 및 게임 컨텍스트
이 접근 방식은 다음을 제공합니다.
- 토큰 소비 감소: 대화 기록의 기하급수적 증가를 방지합니다
- 컨텍스트 유지: 필수 게임 상태 정보를 보존합니다
- 성능 향상: 더 짧은 프롬프트로 더 빠른 처리
- 장기 게임 가능: 토큰 제한에 도달하지 않고 확장된 게임 플레이를 허용합니다
체스 전략은 Koog의 그래프 기반 에이전트 아키텍처를 보여줍니다.
노드 유형:
nodeCallLLM: 입력을 처리하고 응답/도구 호출을 생성합니다nodeExecuteTool: 제공된 매개변수로 Move 도구를 실행합니다nodeTrimHistory: 위에서 설명한 대로 대화 메모리를 최적화합니다nodeSendToolResult: 도구 실행 결과를 LLM에 다시 보냅니다
제어 흐름:
- 선형 경로: 시작 → LLM 요청 → 도구 실행 → 기록 정리 → 결과 전송
- 결정 지점: LLM 응답은 대화를 종료하거나 다른 도구 호출을 트리거할 수 있습니다
- 메모리 관리: 각 도구 실행 후에 기록 정리가 발생합니다
이 전략은 대화의 일관성을 유지하면서 효율적이고 상태를 유지하는 게임 플레이를 보장합니다.
AI 에이전트 설정
val baseExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY"))이 섹션에서는 OpenAI 실행기(executor)를 초기화합니다. simpleOpenAIExecutor는 환경 변수의 API 키를 사용하여 OpenAI의 API에 대한 연결을 생성합니다.
구성 참고 사항:
OPENAI_API_KEY환경 변수에 OpenAI API 키를 저장합니다- 실행기는 인증 및 API 통신을 자동으로 처리합니다
- 다양한 LLM 공급자를 위한 여러 실행기 유형을 사용할 수 있습니다
에이전트 조립
val game = ChessGame()
val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }
// Create a chat agent with a system prompt and the tool registry
val agent = AIAgent(
executor = baseExecutor,
strategy = strategy,
llmModel = OpenAIModels.Reasoning.O3Mini,
systemPrompt = """
You are an agent who plays chess.
You should always propose a move in response to the "Your move!" message.
DO NOT HALLUCINATE!!!
DO NOT PLAY ILLEGAL MOVES!!!
YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!
""".trimMargin(),
temperature = 0.0,
toolRegistry = toolRegistry,
maxIterations = 200,
)여기에서는 모든 구성 요소를 기능적인 체스 플레이 에이전트로 조립합니다.
주요 구성:
- 모델 선택: 고품질 체스 플레이를 위해
OpenAIModels.Reasoning.O3Mini사용 - 온도: 결정론적이고 전략적인 수를 위해 0.0으로 설정
- 시스템 프롬프트: 합법적인 수와 적절한 행동을 강조하는 신중하게 작성된 지침
- 도구 레지스트리: 에이전트에 Move 도구에 대한 액세스를 제공합니다
- 최대 반복: 전체 게임을 허용하기 위해 200으로 설정
시스템 프롬프트 설계:
- 수 제안 책임 강조
- 환각 및 불법 수 금지
- 메시징을 항복 또는 체크메이트 선언으로만 제한
- 집중된 게임 중심 행동 생성
기본 에이전트 실행
import kotlinx.coroutines.runBlocking
println("Chess Game started!")
val initialMessage = "Starting position is ${game.getBoard()}. White to move!"
runBlocking {
agent.run(initialMessage)
}Chess Game started!
8 r n b q k b n r
7 p p p p p p p p
6 * * * * * * * *
5 * * * * * * * *
4 * * * * P * * *
3 * * * * * * * *
2 P 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 * * * * * * * *
2 P 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 * * * * * N * *
2 P 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 * * * * * N * *
2 P 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 * * N * * N * *
2 P P P P * P P P
1 R * B Q K B * R
a b c d e f g h
-----------------
The execution was interrupted
이 기본 에이전트는 자율적으로 플레이하며 수를 자동으로 둡니다. 게임 출력은 AI가 스스로 플레이할 때의 수의 순서와 보드 상태를 보여줍니다.
고급 기능: 상호작용형 선택(Choice Selection)
다음 섹션에서는 사용자가 여러 AI 생성 수 중에서 선택하여 AI의 의사 결정 과정에 참여할 수 있는 보다 정교한 접근 방식을 보여줍니다.
사용자 지정 선택 전략
import ai.koog.agents.core.feature.choice.ChoiceSelectionStrategy
/**
* `AskUserChoiceStrategy` allows users to interactively select a choice from a list of options
* presented by a language model. The strategy uses customizable methods to display the prompt
* and choices and read user input to determine the selected choice.
*
* @property promptShowToUser A function that formats and displays a given `Prompt` to the user.
* @property choiceShowToUser A function that formats and represents a given `LLMChoice` to the user.
* @property print A function responsible for displaying messages to the user, e.g., for showing prompts or feedback.
* @property read A function to capture user input.
*/
class AskUserChoiceSelectionStrategy(
private val promptShowToUser: (Prompt) -> String = { "Current prompt: $it" },
private val choiceShowToUser: (LLMChoice) -> String = { "$it" },
private val print: (String) -> Unit = ::println,
private val read: () -> String? = ::readlnOrNull
) : ChoiceSelectionStrategy {
override suspend fun choose(prompt: Prompt, choices: List<LLMChoice>): LLMChoice {
print(promptShowToUser(prompt))
print("Available LLM choices")
choices.withIndex().forEach { (index, choice) ->
print("Choice number ${index + 1}: ${choiceShowToUser(choice)}")
}
var choiceNumber = ask(choices.size)
while (choiceNumber == null) {
print("Invalid response.")
choiceNumber = ask(choices.size)
}
return choices[choiceNumber - 1]
}
private fun ask(numChoices: Int): Int? {
print("Please choose a choice. Enter a number between 1 and $numChoices: ")
return read()?.toIntOrNull()?.takeIf { it in 1..numChoices }
}
}AskUserChoiceSelectionStrategy는 AI 의사 결정에 인간의 참여를 가능하게 하기 위해 Koog의 ChoiceSelectionStrategy 인터페이스를 구현합니다.
주요 기능:
- 사용자 지정 가능한 표시: 프롬프트와 선택을 서식 지정하는 함수
- 상호작용형 입력: 사용자 상호작용을 위해 표준 입/출력을 사용
- 유효성 검사: 사용자 입력이 유효한 범위 내에 있는지 확인
- 유연한 I/O: 다양한 환경을 위한 구성 가능한 print 및 read 함수
사용 사례:
- 게임 플레이에서의 인간-AI 협업
- AI 의사 결정 투명성 및 설명 가능성
- 훈련 및 디버깅 시나리오
- 교육용 시연
선택 기능이 강화된 전략
inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(
name: String? = null
): AIAgentNodeDelegate<T, T> = node(name) { result ->
llm.writeSession {
rewritePrompt { prompt ->
val messages = prompt.messages
prompt.copy(messages = listOf(messages.first(), messages.last()))
}
}
result
}
val strategy = strategy<String, String>("chess_strategy") {
val nodeCallLLM by nodeLLMRequest("sendInput")
val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")
val nodeSendToolResult by nodeLLMSendToolResult("nodeSendToolResult")
val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()
edge(nodeStart forwardTo nodeCallLLM)
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })
edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })
edge(nodeExecuteTool forwardTo nodeTrimHistory)
edge(nodeTrimHistory forwardTo nodeSendToolResult)
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
}
val askChoiceStrategy = AskUserChoiceSelectionStrategy(promptShowToUser = { prompt ->
val lastMessage = prompt.messages.last()
if (lastMessage is Message.Tool.Call) {
lastMessage.content
} else {
""
}
})val promptExecutor = PromptExecutorWithChoiceSelection(baseExecutor, askChoiceStrategy)첫 번째 상호작용형 접근 방식은 PromptExecutorWithChoiceSelection을 사용하며, 이는 기본 실행기를 선택 기능으로 래핑합니다. 사용자 지정 표시 함수는 도구 호출에서 수 정보를 추출하여 AI가 무엇을 하려는지 사용자에게 보여줍니다.
아키텍처 변경:
- 래핑된 실행기:
PromptExecutorWithChoiceSelection은 모든 기본 실행기에 선택 기능을 추가합니다 - 컨텍스트 인식 표시: 전체 프롬프트 대신 마지막 도구 호출 내용을 보여줍니다
- 더 높은 온도: 더 다양한 수 옵션을 위해 1.0으로 증가
고급 전략: 수동 선택
val game = ChessGame()
val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }
val agent = AIAgent(
executor = promptExecutor,
strategy = strategy,
llmModel = OpenAIModels.Reasoning.O3Mini,
systemPrompt = """
You are an agent who plays chess.
You should always propose a move in response to the "Your move!" message.
DO NOT HALLUCINATE!!!
DO NOT PLAY ILLEGAL MOVES!!!
YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!
""".trimMargin(),
temperature = 1.0,
toolRegistry = toolRegistry,
maxIterations = 200,
numberOfChoices = 3,
)고급 전략은 선택을 에이전트의 실행 그래프에 직접 통합합니다.
새로운 노드:
nodeLLMSendResultsMultipleChoices: 여러 LLM 선택을 동시에 처리합니다nodeSelectLLMChoice: 선택 전략을 워크플로에 통합합니다
향상된 제어 흐름:
- 도구 결과는 여러 선택을 지원하기 위해 리스트로 래핑됩니다
- 사용자 선택은 선택된 경로를 계속 진행하기 전에 발생합니다
- 선택된 선택은 래핑이 해제되고 일반적인 흐름을 통해 계속됩니다
장점:
- 더 큰 제어: 에이전트 워크플로와 세분화된 통합
- 유연성: 다른 에이전트 기능과 결합 가능
- 투명성: 사용자는 AI가 무엇을 고려하는지 정확히 볼 수 있습니다
상호작용형 에이전트 실행
println("Chess Game started!")
val initialMessage = "Starting position is ${game.getBoard()}. White to move!"
runBlocking {
agent.run(initialMessage)
}Chess Game started!
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b n r
7 p p p p p p p p
6 * * * * * * * *
5 * * * * * * * *
4 * * * * P * * *
3 * * * * * * * *
2 P P P P * P P P
1 R N B Q K B N R
a b c d e f g h
-----------------
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b n r
7 p p p p * p p p
6 * * * * * * * *
5 * * * * p * * *
4 * * * * P * * *
3 * * * * * * * *
2 P P P P * P P P
1 R N B Q K B N R
a b c d e f g h
-----------------
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b n r
7 p p p p * p p p
6 * * * * * * * *
5 * * * * p * * *
4 * * * * P * * *
3 * * * * * N * *
2 P P P P * P P P
1 R N B Q K B * R
a b c d e f g h
-----------------
The execution was interrupted
import ai.koog.agents.core.feature.choice.nodeLLMSendResultsMultipleChoices
import ai.koog.agents.core.feature.choice.nodeSelectLLMChoice
inline fun <reified T> AIAgentSubgraphBuilderBase<*, *>.nodeTrimHistory(
name: String? = null
): AIAgentNodeDelegate<T, T> = node(name) { result ->
llm.writeSession {
rewritePrompt { prompt ->
val messages = prompt.messages
prompt.copy(messages = listOf(messages.first(), messages.last()))
}
}
result
}
val strategy = strategy<String, String>("chess_strategy") {
val nodeCallLLM by nodeLLMRequest("sendInput")
val nodeExecuteTool by nodeExecuteTool("nodeExecuteTool")
val nodeSendToolResult by nodeLLMSendResultsMultipleChoices("nodeSendToolResult")
val nodeSelectLLMChoice by nodeSelectLLMChoice(askChoiceStrategy, "chooseLLMChoice")
val nodeTrimHistory by nodeTrimHistory<ReceivedToolResult>()
edge(nodeStart forwardTo nodeCallLLM)
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })
edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })
edge(nodeExecuteTool forwardTo nodeTrimHistory)
edge(nodeTrimHistory forwardTo nodeSendToolResult transformed { listOf(it) })
edge(nodeSendToolResult forwardTo nodeSelectLLMChoice)
edge(nodeSelectLLMChoice forwardTo nodeFinish transformed { it.first() } onAssistantMessage { true })
edge(nodeSelectLLMChoice forwardTo nodeExecuteTool transformed { it.first() } onToolCall { true })
}val game = ChessGame()
val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }
val agent = AIAgent(
executor = baseExecutor,
strategy = strategy,
llmModel = OpenAIModels.Reasoning.O3Mini,
systemPrompt = """
You are an agent who plays chess.
You should always propose a move in response to the "Your move!" message.
DO NOT HALLUCINATE!!!
DO NOT PLAY ILLEGAL MOVES!!!
YOU CAN SEND A MESSAGE ONLY IF IT IS A RESIGNATION OR A CHECKMATE!!!
""".trimMargin(),
temperature = 1.0,
toolRegistry = toolRegistry,
maxIterations = 200,
numberOfChoices = 3,
)println("Chess Game started!")
val initialMessage = "Starting position is ${game.getBoard()}. White to move!"
runBlocking {
agent.run(initialMessage)
}Chess Game started!
8 r n b q k b n r
7 p p p p p p p p
6 * * * * * * * *
5 * * * * * * * *
4 * * * * P * * *
3 * * * * * * * *
2 P P P P * P P P
1 R N B Q K B N R
a b c d e f g h
-----------------
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b n r
7 p p p p * p p p
6 * * * * * * * *
5 * * * * p * * *
4 * * * * P * * *
3 * * * * * * * *
2 P P P P * P P P
1 R N B Q K B N R
a b c d e f g h
-----------------
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b n r
7 p p p p * p p p
6 * * * * * * * *
5 * * * * p * * *
4 * * * * P * * *
3 * * * * * N * *
2 P P P P * P P P
1 R N B Q K B * R
a b c d e f g h
-----------------
Available LLM choices
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Choice number 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, additionalInfo={}))]
Please choose a choice. Enter a number between 1 and 3:
8 r n b q k b * r
7 p p p p * p p p
6 * * * * * n * *
5 * * * * p * * *
4 * * * * P * * *
3 * * * * * N * *
2 P P P P * P P P
1 R N B Q K B * R
a b c d e f g h
-----------------
The execution was interrupted
상호작용형 예시는 사용자가 AI의 의사 결정 과정을 어떻게 안내할 수 있는지 보여줍니다. 출력에서 다음을 확인할 수 있습니다.
- 여러 선택: AI는 3가지 다른 수 옵션을 생성합니다
- 사용자 선택: 사용자는 선호하는 수를 선택하기 위해 1-3번을 입력합니다
- 게임 계속: 선택된 수가 실행되고 게임이 계속됩니다
결론
이 튜토리얼은 Koog 프레임워크로 지능형 에이전트를 구축하는 여러 핵심 측면을 보여줍니다.
주요 내용
- 도메인 모델링: 잘 구조화된 데이터 모델은 복잡한 애플리케이션에 필수적입니다
- 도구 통합: 사용자 지정 도구는 에이전트가 외부 시스템과 효과적으로 상호작용할 수 있도록 합니다
- 메모리 관리: 전략적인 기록 정리(history trimming)는 장기적인 상호작용에 대한 성능을 최적화합니다
- 전략 그래프: Koog의 그래프 기반 접근 방식은 유연한 제어 흐름을 제공합니다
- 상호작용형 AI: 선택 기능은 인간-AI 협업 및 투명성을 가능하게 합니다
탐색된 프레임워크 기능
- ✅ 사용자 지정 도구 생성 및 통합
- ✅ 에이전트 전략 설계 및 그래프 기반 제어 흐름
- ✅ 메모리 최적화 기술
- ✅ 상호작용형 선택(choice selection)
- ✅ 다중 LLM 응답 처리
- ✅ 상태를 유지하는 게임 관리
Koog 프레임워크는 효율성과 투명성을 유지하면서 복잡하고 여러 턴에 걸친 상호작용을 처리할 수 있는 정교한 AI 에이전트를 구축하기 위한 기반을 제공합니다.
