增加输出窗口token数限制,防止截断导致解析出错
This commit is contained in:
@@ -342,7 +342,7 @@ const DEFAULT_GAME_INDEX_HTML: &str = r#"<!doctype html>
|
||||
"#;
|
||||
|
||||
const DEFAULT_EDITOR_BASE_URL: &str = "http://127.0.0.1:3000";
|
||||
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 5000;
|
||||
const GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS: u32 = 320000;
|
||||
const GAME_CREATOR_PLANNER_MAX_OUTPUT_TOKENS: u32 = 900;
|
||||
const GAME_CREATOR_LLM_REQUEST_TIMEOUT_MS: u64 = 180_000;
|
||||
const GAME_CREATOR_AGENT_LOOP_MAX_PASSES: u8 = 3;
|
||||
@@ -1657,22 +1657,35 @@ async fn request_generator_game_draft_with_client(
|
||||
group_briefs_markdown: &str,
|
||||
agenda_markdown: &str,
|
||||
) -> Result<LlmGameDraft, String> {
|
||||
let system_prompt = game_creator_system_prompt();
|
||||
let user_prompt = game_creator_generator_user_prompt(
|
||||
prompt,
|
||||
short_memory,
|
||||
long_memory,
|
||||
spec_markdown,
|
||||
findings_markdown,
|
||||
group_briefs_markdown,
|
||||
agenda_markdown,
|
||||
);
|
||||
let request = LlmTextRequest::new(vec![
|
||||
LlmMessage::system(game_creator_system_prompt()),
|
||||
LlmMessage::user(game_creator_generator_user_prompt(
|
||||
prompt,
|
||||
short_memory,
|
||||
long_memory,
|
||||
spec_markdown,
|
||||
findings_markdown,
|
||||
group_briefs_markdown,
|
||||
agenda_markdown,
|
||||
)),
|
||||
LlmMessage::system(system_prompt),
|
||||
LlmMessage::user(user_prompt.clone()),
|
||||
])
|
||||
.with_max_tokens(GAME_CREATOR_LLM_MAX_OUTPUT_TOKENS);
|
||||
let response = request_game_creator_llm_text(client, request)
|
||||
.await
|
||||
.map_err(|error| format!("LLM 生成失败:{error}"))?;
|
||||
let response = match request_game_creator_llm_text(client, request).await {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
// LLM 调用失败(如 “LLM 返回内容为空”)时,把本次输入连同错误一并落盘,便于复现定位。
|
||||
persist_llm_game_draft_error_input(
|
||||
system_prompt,
|
||||
user_prompt.as_str(),
|
||||
&error.to_string(),
|
||||
);
|
||||
return Err(format!("LLM 生成失败:{error}"));
|
||||
}
|
||||
};
|
||||
// 先把原始返回落盘,再解析;解析失败(如输出截断)时仍能从仓库里拿到完整原文。
|
||||
persist_llm_game_draft_snapshot(response.content.as_str());
|
||||
let content = strip_llm_thinking_blocks(response.content.as_str());
|
||||
parse_llm_game_draft_response(content.as_str())
|
||||
}
|
||||
@@ -3573,6 +3586,105 @@ fn parse_llm_game_draft_response(content: &str) -> Result<LlmGameDraft, String>
|
||||
.map_err(|error| format!("解析 LLM 游戏草案失败:{error}"))
|
||||
}
|
||||
|
||||
// 找到仓库根(包含 apps/ai-game-creator-shell/src-tauri/Cargo.toml 的目录),
|
||||
// 复用本地 env 那套向上查找逻辑,以便把原始 LLM 草案落到仓库内而非 tmp 项目目录。
|
||||
fn game_creator_repo_root() -> Option<PathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
roots.push(cwd);
|
||||
}
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
roots.push(parent.to_path_buf());
|
||||
}
|
||||
}
|
||||
for root in roots {
|
||||
for directory in root.ancestors().take(8) {
|
||||
let marker = directory
|
||||
.join("apps")
|
||||
.join("ai-game-creator-shell")
|
||||
.join("src-tauri")
|
||||
.join("Cargo.toml");
|
||||
if marker.is_file() {
|
||||
return Some(directory.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// 把生成器原始返回(含被截断、解析失败的内容)保存到仓库 apps/ai-game-creator-shell/.llm-drafts/,
|
||||
// 便于排查 “EOF while parsing a string” 这类输出截断问题。尽力而为,不阻断生成主流程。
|
||||
fn persist_llm_game_draft_snapshot(raw_content: &str) {
|
||||
// 单元测试同样会走生成路径,避免 cargo test 往仓库写快照文件。
|
||||
if cfg!(test) {
|
||||
return;
|
||||
}
|
||||
let Some(repo_root) = game_creator_repo_root() else {
|
||||
eprintln!("llm.draft.snapshot.skip: 未能定位仓库根目录");
|
||||
return;
|
||||
};
|
||||
let dir = repo_root
|
||||
.join("apps")
|
||||
.join("ai-game-creator-shell")
|
||||
.join(".llm-drafts");
|
||||
if let Err(error) = fs::create_dir_all(&dir) {
|
||||
eprintln!("llm.draft.snapshot.dir.failed: {}: {error}", dir.display());
|
||||
return;
|
||||
}
|
||||
let path = dir.join(format!("draft-{}.txt", unix_millis()));
|
||||
if let Err(error) = fs::write(&path, raw_content) {
|
||||
eprintln!(
|
||||
"llm.draft.snapshot.write.failed: {}: {error}",
|
||||
path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 同步更新 latest.txt,方便直接打开最近一次草案。
|
||||
let latest = dir.join("latest.txt");
|
||||
let _ = fs::write(&latest, raw_content);
|
||||
eprintln!("llm.draft.snapshot.saved: {}", path.display());
|
||||
}
|
||||
|
||||
// LLM 调用失败(例如返回内容为空)时,把本次发送给模型的输入(system + 完整 user prompt)
|
||||
// 连同错误信息一起落盘到仓库 .llm-drafts/,便于按同样输入复现与定位。尽力而为,不阻断主流程。
|
||||
fn persist_llm_game_draft_error_input(system_prompt: &str, user_prompt: &str, error: &str) {
|
||||
// 单元测试同样会走生成路径,避免 cargo test 往仓库写文件。
|
||||
if cfg!(test) {
|
||||
return;
|
||||
}
|
||||
let Some(repo_root) = game_creator_repo_root() else {
|
||||
eprintln!("llm.draft.error-input.skip: 未能定位仓库根目录");
|
||||
return;
|
||||
};
|
||||
let dir = repo_root
|
||||
.join("apps")
|
||||
.join("ai-game-creator-shell")
|
||||
.join(".llm-drafts");
|
||||
if let Err(io_error) = fs::create_dir_all(&dir) {
|
||||
eprintln!(
|
||||
"llm.draft.error-input.dir.failed: {}: {io_error}",
|
||||
dir.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
let body = format!(
|
||||
"# LLM 生成失败输入快照\n\n## 错误\n{error}\n\n## System Prompt\n{system_prompt}\n\n## User Prompt(含用户需求/短长期记忆/spec/agenda/组 brief/findings)\n{user_prompt}\n",
|
||||
);
|
||||
let path = dir.join(format!("error-input-{}.txt", unix_millis()));
|
||||
if let Err(io_error) = fs::write(&path, &body) {
|
||||
eprintln!(
|
||||
"llm.draft.error-input.write.failed: {}: {io_error}",
|
||||
path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 同步更新 latest-error-input.txt,方便直接打开最近一次失败输入。
|
||||
let latest = dir.join("latest-error-input.txt");
|
||||
let _ = fs::write(&latest, &body);
|
||||
eprintln!("llm.draft.error-input.saved: {}", path.display());
|
||||
}
|
||||
|
||||
fn strip_llm_thinking_blocks(content: &str) -> String {
|
||||
let mut output = String::new();
|
||||
let mut rest = content;
|
||||
|
||||
Reference in New Issue
Block a user