mirror of
https://github.com/LHY0125/Gobang-Game.git
synced 2026-06-28 16:35: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 board;
|
||||
pub mod rules;
|
||||
pub mod record;
|
||||
pub mod llm;
|
||||
pub mod network;
|
||||
pub mod record;
|
||||
pub mod rules;
|
||||
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