使用 Koog 架構打造 AI 西洋棋玩家
本教學示範如何使用 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 列舉有助於區分王翼入堡 (kingside castling) 與后翼入堡 (queenside castling) 移動。
棋子與位置建模
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 類別管理 8×8 網格與棋子位置。關鍵設計決策包括:
- 內部表示法:使用可變清單的清單 (list of mutable lists) 以進行有效率的存取與修改
- 視覺化顯示:
toString()方法提供清晰的 ASCII 表示法,包含列數 (rank numbers) 與行字母 (file letters) - 位置對應:在西洋棋表示法 (a1-h8) 與內部陣列索引之間進行轉換
ChessGame 邏輯
/**
* 簡易西洋棋遊戲,不檢查移動是否合法。
* 如果輸入的移動有效,則儲存棋盤的正確狀態
*/
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> - 一般移動。例如:p-e2-e4
<piece>-<from>-<to>-<promotion> - 升變移動。例如:p-e7-e8-q。
棋子名稱:
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) { "只有兵可以升變" }
usualMove(Position(from), Position(to))
board.setPiece(Position(to), Piece(PieceType.fromId(promotion), currentPlayer))
}
else -> throw IllegalArgumentException("無效的移動:$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()) {
// 該移動為吃過路兵 (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>(
argsSerializer = Args.serializer(),
descriptor = ToolDescriptor(
name = "move",
description = "根據表示法移動棋子:
${game.moveNotation}",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "notation",
description = "要移動棋子的表示法",
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 "目前遊戲狀態:
${game.getBoard()}
輪到 ${game.currentPlayer()} 移動!請出招!"
}
}Move 工具展示了 Koog 架構的工具整合模式:
- 擴充 SimpleTool:繼承基礎工具功能與型別安全的引數處理
- 可序列化引數:使用 Kotlin 序列化來定義工具'的輸入參數
- 豐富的文件說明:
ToolDescriptor為 LLM 提供關於工具用途與參數的詳細資訊 - 建構函式參數:將
argsSerializer與descriptor傳遞給建構函式 - 執行邏輯:
execute方法處理實際的移動執行並提供格式化的回饋
關鍵設計面向:
- 背景注入 (Context Injection):工具接收
ChessGame執行個體,使其能修改遊戲狀態 - 回饋迴圈:傳回目前的棋盤狀態並提示下一位玩家,維持對話流
- 錯誤處理:依賴遊戲類別進行移動驗證與錯誤回報
代理程式策略設計
記憶體優化技術
import ai.koog.agents.core.environment.ReceivedToolResult
/**
* 西洋棋位置(幾乎)完全由棋盤狀態定義,
* 因此我們可以修整 LLM 的歷程記錄,僅包含系統提示與最後一步。
*/
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 函式為西洋棋遊戲實作了一項關鍵優化。由於西洋棋位置主要由目前的棋盤狀態而非完整的移動歷程決定,我們可以透過僅保留以下內容來顯著減少權杖 (token) 使用量:
- 系統提示:包含代理程式的核心指示與行為準則
- 最新訊息:最近的棋盤狀態與遊戲背景資訊
這種方法:
- 減少權杖消耗:防止對話歷程呈指數級增長
- 保持背景資訊:保留必要的遊戲狀態資訊
- 提升效能:透過更短的提示實現更快的處理
- 支援長時間遊戲:允許進行長時間對弈而不會觸及權杖限制
西洋棋策略展示了 Koog 以圖 (graph) 為基礎的代理程式架構:
節點型別:
nodeCallLLM:處理輸入並產生回應/工具呼叫nodeExecuteTool:使用提供的參數執行Move工具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))) }
// 建立一個具備系統提示與工具儲存庫的聊天代理程式
val agent = AIAgent(
executor = baseExecutor,
strategy = strategy,
llmModel = OpenAIModels.Chat.O3Mini,
systemPrompt = """
你是一位玩西洋棋的代理程式。
你應始終針對 "Your move!" 訊息提議一個移動。
請勿產生幻覺!!!
請勿做出違規移動!!!
只有在認輸或將死時才可傳送訊息!!!
""".trimMargin(),
temperature = 0.0,
toolRegistry = toolRegistry,
maxIterations = 200,
)在此我們將所有組件組合成一個功能齊全的西洋棋代理程式:
關鍵配置:
- 模型選擇:使用
OpenAIModels.Chat.O3Mini以獲得高品質的下棋表現 - 溫度 (Temperature):設定為 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
-----------------
執行已中斷
這個基本代理程式會自主運作,自動進行移動。遊戲輸出顯示了 AI 自我對弈時的一系列移動與棋盤狀態。
進階功能:互動式選項選取
接下來的章節展示了一種更複雜的方法,使用者可以透過從多個 AI 產生的移動中進行選擇,參與 AI 的決策過程。
自訂選項選取策略
import ai.koog.agents.core.feature.choice.ChoiceSelectionStrategy
/**
* `AskUserChoiceStrategy` 允許使用者互動式地從語言模型呈現的選項清單中選取一個選項。
* 該策略使用可自訂的方法來顯示提示與選項,並讀取使用者輸入以決定選取的選項。
*
* @property promptShowToUser 一個將指定的 `Prompt` 格式化並顯示給使用者的函式。
* @property choiceShowToUser 一個將指定的 `LLMChoice` 格式化並呈現給使用者的函式。
* @property print 負責將訊息顯示給使用者的函式,例如用於顯示提示或回饋。
* @property read 用於擷取使用者輸入的函式。
*/
class AskUserChoiceSelectionStrategy(
private val promptShowToUser: (Prompt) -> String = { "目前提示:$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("可用的 LLM 選項")
choices.withIndex().forEach { (index, choice) ->
print("選項編號 ${index + 1}:${choiceShowToUser(choice)}")
}
var choiceNumber = ask(choices.size)
while (choiceNumber == null) {
print("無效的回應。")
choiceNumber = ask(choices.size)
}
return choices[choiceNumber - 1]
}
private fun ask(numChoices: Int): Int? {
print("請選擇一個選項。請輸入 1 到 $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 MessagePart.Tool.Call) {
lastMessage.content
} else {
""
}
})val promptExecutor = PromptExecutorWithChoiceSelection(baseExecutor, askChoiceStrategy)第一種互動式方法使用 PromptExecutorWithChoiceSelection,它將基礎執行器封裝起來並具備選項選取功能。自訂顯示函式會從工具呼叫中擷取移動資訊,向使用者顯示 AI 想要執行的動作。
架構變動:
- 封裝的執行器:
PromptExecutorWithChoiceSelection為任何基礎執行器新增選項功能 - 背景感知顯示:顯示最後一個工具呼叫內容而非完整提示
- 較高溫度 (Temperature):增加至 1.0 以獲得更多樣化的移動選項
進階策略:手動選項選取
val game = ChessGame()
val toolRegistry = ToolRegistry { tools(listOf(Move(game))) }
val agent = AIAgent(
executor = promptExecutor,
strategy = strategy,
llmModel = OpenAIModels.Chat.O3Mini,
systemPrompt = """
你是一位玩西洋棋的代理程式。
你應始終針對 "Your move!" 訊息提議一個移動。
請勿產生幻覺!!!
請勿做出違規移動!!!
只有在認輸或將死時才可傳送訊息!!!
""".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!
可用的 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 = """
你是一位玩西洋棋的代理程式。
你應始終針對 "Your move!" 訊息提議一個移動。
請勿產生幻覺!!!
請勿做出違規移動!!!
只有在認輸或將死時才可傳送訊息!!!
""".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
-----------------
可用的 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 的決策過程。在輸出中,你可以看到:
- 多個選項:AI 產生了 3 個不同的移動選項
- 使用者選取:使用者輸入數字 1-3 以選擇其偏好的移動
- 遊戲繼續:選定的移動被執行,遊戲繼續進行
結論
本教學示範了使用 Koog 架構建置智慧代理程式的幾個關鍵面向:
關鍵要點
- 領域建模:結構良好的資料模型對於複雜應用程式至關重要
- 工具整合:自訂工具讓代理程式能有效地與外部系統互動
- 記憶體管理:戰略性的歷程記錄修整可優化長時間互動的效能
- 策略圖:Koog 以圖為基礎的方法提供了靈活的控制流程
- 互動式 AI:選項選取支援人機協作與透明度
已探索的架構特性
- ✅ 自訂工具建立與整合
- ✅ 代理程式策略設計與以圖為基礎的控制流程
- ✅ 記憶體優化技術
- ✅ 互動式選項選取
- ✅ 多個 LLM 回應處理
- ✅ 具備狀態的遊戲管理
Koog 架構為建置複雜的 AI 代理程式奠定了基礎,這些代理程式能處理複雜、多回合的互動,同時維持效率與透明度。
