跳转至

使用 Koog 框架构建 AI 国际象棋棋手

在 GitHub 上打开 下载 .ipynb

本教程演示了如何使用 Koog 框架构建一个智能的国际象棋对弈智能体。我们将探讨关键概念,包括工具集成、智能体策略、内存优化以及交互式 AI 决策。

你将学到什么

  • 如何为复杂游戏建模领域特定的数据结构
  • 创建智能体可用于与环境交互的自定义工具
  • 实现具有内存管理功能的高效智能体策略
  • 构建具有选择能力的交互式 AI 系统
  • 针对回合制游戏优化智能体性能

设置

首先,让我们导入 Koog 框架并设置开发环境:

%useLatestDescriptors
%use koog

为国际象棋领域建模

创建健壮的领域模型对于任何游戏 AI 都至关重要。在国际象棋中,我们需要表示玩家、棋子及其关系。让我们从定义核心数据结构开始:

核心枚举和类型

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 枚举将每个国际象棋棋子映射到其标准记谱字符,便于解析国际象棋走法。

Side 枚举有助于区分王翼和长易位走法。

棋子和位置建模

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("\n") { (index, row) ->
            "${8 - index} ${row.map { it.toChar() }.joinToString(" ")}"
        } + "\n  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 类管理 8×8 的棋盘网格和棋子位置。关键设计决策包括:

  • 内部表示:使用可变列表的列表,以实现高效的访问和修改
  • 视觉显示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)和兵升变
  • 特殊走法处理:实现吃过路兵和易位逻辑
  • 回合管理:每次走法后自动在玩家之间切换
  • 验证:虽然不验证走法的合法性(信任 AI 做出有效走法),但能正确处理走法解析和状态更新

moveNotation 字符串为 AI 智能体提供了关于可接受走法格式的清晰文档。

与 Koog 框架集成

创建自定义工具

import kotlinx.serialization.Serializable

class Move(val game: ChessGame) : SimpleTool<Move.Args>(
    argsSerializer = Args.serializer(),
    descriptor = ToolDescriptor(
        name = "move",
        description = "Moves a piece according to the notation:\n${game.moveNotation}",
        requiredParameters = listOf(
            ToolParameterDescriptor(
                name = "notation",
                description = "The notation of the piece to move",
                type = ToolParameterType.String,
            )
        )
    )
) {
    @Serializable
    data class Args(val notation: String) : ToolArgs

    override suspend fun execute(args: Args): String {
        game.move(args.notation)
        println(game.getBoard())
        println("-----------------")
        return "Current state of the game:\n${game.getBoard()}\n${game.currentPlayer()} to move! Make the move!"
    }
}

Move 工具展示了 Koog 框架的工具集成模式:1. 扩展 SimpleTool:继承基础工具功能,提供类型安全的参数处理 2. 可序列化参数:使用 Kotlin 序列化定义工具的输入参数 3. 丰富文档ToolDescriptorLLM 提供关于工具用途和参数的详细信息 4. 构造函数参数:将 argsSerializerdescriptor 传递给构造函数 5. 执行逻辑execute 方法处理实际的走棋执行并提供格式化反馈

关键设计方面: - 上下文注入:工具接收 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 函数实现了国际象棋游戏的关键优化。由于棋局状态主要由当前棋盘状态而非完整走棋历史决定,我们可以通过仅保留以下内容显著减少令牌使用:

  1. 系统提示:包含智能体的核心指令和行为准则
  2. 最新消息:最近的棋盘状态和游戏上下文

这种方法: - 减少令牌消耗:防止对话历史呈指数级增长 - 保持上下文:保留必要的游戏状态信息 - 提升性能:通过更短的提示实现更快处理 - 支持长时对局:允许延长游戏时间而不触及令牌限制

国际象棋策略展示了 Koog 基于图的智能体架构:

节点类型: - nodeCallLLM:处理输入并生成响应/工具调用 - nodeExecuteTool:使用提供的参数执行走棋工具 - nodeTrimHistory:按上述描述优化对话记忆 - nodeSendToolResult:将工具执行结果发送回 LLM

控制流程: - 线性路径:开始 → LLM 请求 → 工具执行 → 历史修剪 → 发送结果 - 决策点LLM 响应可结束对话或触发另一个工具调用 - 内存管理:每次工具执行后进行历史修剪

该策略在保持对话连贯性的同时,确保了高效、有状态的游戏体验。

设置AI智能体

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

本节初始化我们的 OpenAI 执行器。simpleOpenAIExecutor 使用环境变量中的 API 密钥创建与 OpenAI 的 API 的连接。

配置说明: - 将您的 OpenAI API 密钥存储在 OPENAI_API_KEY 环境变量中 - 执行器自动处理身份验证和 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.Chat.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.Chat.O3Mini 实现高质量的象棋对弈 - 温度参数:设置为 0.0,确保确定性的策略走法 - 系统提示词:精心设计的指令,强调合法走法与规范行为 - 工具注册:为智能体提供走子工具的访问权限 - 最大迭代次数:设置为 200,确保对局完整进行

系统提示词设计: - 强调走子提议的责任 - 禁止幻觉走法与非法移动 - 限制消息内容仅包含认输或将死声明 - 创建专注的、以对局为导向的行为

运行基础智能体

import kotlinx.coroutines.runBlocking

println("Chess Game started!")

val initialMessage = "Starting position is ${game.getBoard()}. White to move!"

runBlocking {
    agent.run(initialMessage)
}
象棋对局开始!
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
-----------------



执行被中断

该基础智能体自主对弈,自动执行走子。对局输出展示了AI自我对弈时的走子序列与棋盘状态。

高级功能:交互式选择机制

后续章节将展示更复杂的实现方式,用户可通过从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 实现了 Koog 的 ChoiceSelectionStrategy 接口,使人类能够参与AI决策过程:

核心特性: - 可定制化显示:提供格式化提示与选项的函数 - 交互式输入:使用标准输入/输出进行用户交互 - 输入验证:确保用户输入在有效范围内 - 灵活I/O:可配置打印与读取函数以适应不同环境

应用场景: - 人机协作对弈 - 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.Chat.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)
}
国际象棋游戏开始!

可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 1 到 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
-----------------

可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 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 * * * * * * * *
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
-----------------可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 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 * * * * * 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


执行被中断

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.Chat.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)
}

国际象棋游戏开始!
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


可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 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 * * * * * * *

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
-----------------可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 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 * * * * * 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


可用的 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, additionalInfo={}))]
选项 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={}))]
选项 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={}))]
请选择一个选项。输入 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 * * * * * 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


执行被中断

交互式示例展示了用户如何引导 AI 的决策过程。在输出中,你可以看到:

  1. 多个选项AI 生成 3 个不同的走法选项
  2. 用户选择:用户输入 1-3 的数字来选择偏好的走法
  3. 游戏继续:执行选定的走法,游戏继续进行

结论

本教程展示了使用 Koog 框架构建智能代理的几个关键方面:

关键要点

  1. 领域建模:结构良好的数据模型对于复杂应用至关重要
  2. 工具集成:自定义工具使代理能够有效地与外部系统交互
  3. 内存管理:策略性的历史记录修剪优化了长交互的性能
  4. 策略图:Koog 的基于图的方法提供了灵活的控制流
  5. 交互式 AI:选项选择实现了人机协作和透明度

探索的框架特性

  • ✅ 自定义工具创建与集成
  • ✅ 代理策略设计与基于图的控制流
  • ✅ 内存优化技术
  • ✅ 交互式选项选择
  • ✅ 多 LLM 响应处理
  • ✅ 有状态游戏管理Koog框架为构建复杂的人工智能代理提供了基础,这些代理能够处理复杂的多轮交互,同时保持高效性和透明度。