mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-29 00:45:55 +08:00
feat(core): LLM AI — OpenAI 兼容 API 调用 + prompt/parse
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
pub mod board;
|
pub mod board;
|
||||||
pub mod rules;
|
|
||||||
pub mod record;
|
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod record;
|
||||||
|
pub mod rules;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
use crate::ai::AiEngine;
|
||||||
|
use crate::board::Board;
|
||||||
|
use crate::types::{CellState, Color, Position};
|
||||||
|
|
||||||
|
/// 大模型 AI — 通过 HTTP API 调用
|
||||||
|
pub struct LlmAi {
|
||||||
|
endpoint: String,
|
||||||
|
api_key: String,
|
||||||
|
model: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LlmAi {
|
||||||
|
pub fn new(endpoint: &str, api_key: &str, model: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
endpoint: endpoint.to_string(),
|
||||||
|
api_key: api_key.to_string(),
|
||||||
|
model: model.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将棋盘序列化为 prompt
|
||||||
|
pub fn board_to_prompt(board: &Board, color: Color) -> String {
|
||||||
|
let mut s = String::from("你是一位五子棋高手。当前棋盘状态(0=空,1=黑,2=白):\n");
|
||||||
|
for x in 0..board.size {
|
||||||
|
for y in 0..board.size {
|
||||||
|
let ch = match board.get(Position::new(x, y)) {
|
||||||
|
CellState::Empty => '0',
|
||||||
|
CellState::Occupied(Color::Black) => '1',
|
||||||
|
CellState::Occupied(Color::White) => '2',
|
||||||
|
};
|
||||||
|
s.push(ch);
|
||||||
|
s.push(' ');
|
||||||
|
}
|
||||||
|
s.push('\n');
|
||||||
|
}
|
||||||
|
let color_str = match color {
|
||||||
|
Color::Black => "黑棋(1)",
|
||||||
|
Color::White => "白棋(2)",
|
||||||
|
};
|
||||||
|
s.push_str(&format!(
|
||||||
|
"\n你是{}, 请返回最佳落子坐标 (格式: x,y)",
|
||||||
|
color_str
|
||||||
|
));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析 LLM 响应中的坐标
|
||||||
|
pub fn parse_response(response: &str) -> Option<Position> {
|
||||||
|
let bytes = response.as_bytes();
|
||||||
|
for (i, &b) in bytes.iter().enumerate() {
|
||||||
|
if b != b',' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 从逗号向前找数字起始
|
||||||
|
let x_start = (0..i)
|
||||||
|
.rev()
|
||||||
|
.take_while(|&j| bytes[j].is_ascii_digit())
|
||||||
|
.last()
|
||||||
|
.unwrap_or(i);
|
||||||
|
if x_start == i {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 从逗号向后跳过空格找数字起始
|
||||||
|
let y_start = (i + 1..bytes.len()).find(|&j| bytes[j].is_ascii_digit())?;
|
||||||
|
// 从 y_start 向后找数字结束
|
||||||
|
let y_end = (y_start + 1..bytes.len())
|
||||||
|
.find(|&j| !bytes[j].is_ascii_digit())
|
||||||
|
.unwrap_or(bytes.len());
|
||||||
|
|
||||||
|
let x_str = &response[x_start..i];
|
||||||
|
let y_str = &response[y_start..y_end];
|
||||||
|
if let (Ok(x), Ok(y)) = (x_str.parse::<usize>(), y_str.parse::<usize>()) {
|
||||||
|
return Some(Position::new(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AiEngine for LlmAi {
|
||||||
|
fn best_move(&self, board: &Board, color: Color) -> Option<Position> {
|
||||||
|
let prompt = Self::board_to_prompt(board, color);
|
||||||
|
let client = reqwest::blocking::Client::new();
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"model": self.model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
"max_tokens": 50,
|
||||||
|
"temperature": 0.3
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&self.endpoint)
|
||||||
|
.header("Authorization", format!("Bearer {}", self.api_key))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let json: serde_json::Value = resp.json().ok()?;
|
||||||
|
let content = json["choices"][0]["message"]["content"].as_str()?;
|
||||||
|
Self::parse_response(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_coordinate() {
|
||||||
|
assert_eq!(LlmAi::parse_response("7,8"), Some(Position::new(7, 8)));
|
||||||
|
assert_eq!(LlmAi::parse_response("(7, 8)"), Some(Position::new(7, 8)));
|
||||||
|
assert_eq!(
|
||||||
|
LlmAi::parse_response("坐标是 10,5"),
|
||||||
|
Some(Position::new(10, 5))
|
||||||
|
);
|
||||||
|
assert_eq!(LlmAi::parse_response("no coordinate"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_board_to_prompt() {
|
||||||
|
let board = Board::new(15);
|
||||||
|
let prompt = LlmAi::board_to_prompt(&board, Color::Black);
|
||||||
|
assert!(prompt.contains("黑棋(1)"));
|
||||||
|
assert!(prompt.contains("0 0 0"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user