重构 UI Editor LLM repair history
将 request_with_feedback 改为 run_with_repair_history 并统一 max_retries 与 validater 命名。 让 requester 接收 append-only LlmMessage history,业务校验失败追加序列化响应和错误反馈。 迁移 visual_binding 调用方并补充 retry-only 与业务反馈 history 测试。 同步 UI Editor 结构化请求 repair history 决策文档。
This commit is contained in:
+16
-20
@@ -5,7 +5,7 @@ use crate::platform_session::current_platform_session;
|
||||
use crate::ui_editor::commands::separation::*;
|
||||
use crate::ui_editor::commands::utils::{
|
||||
parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm,
|
||||
request_with_feedback, strict_json_schema,
|
||||
run_with_repair_history, strict_json_schema,
|
||||
};
|
||||
use crate::ui_editor::state::State;
|
||||
use crate::ui_editor::utils::NodeId;
|
||||
@@ -275,30 +275,26 @@ async fn visual_binding(
|
||||
)
|
||||
.with_strict(true);
|
||||
let base_prompt = gen_binding_prompt(nodes.to_vec());
|
||||
let result = request_with_feedback(
|
||||
let initial_history = vec![LlmMessage::user_multimodal(vec![
|
||||
LlmMessageContentPart::InputText { text: base_prompt },
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: source_url.clone(),
|
||||
},
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: processed_url.clone(),
|
||||
},
|
||||
])];
|
||||
let result = run_with_repair_history(
|
||||
2,
|
||||
|feedback| {
|
||||
let prompt = feedback.map_or_else(
|
||||
|| base_prompt.clone(),
|
||||
|error| format!("{base_prompt}\n上一次输出错误:{error}\n请修正并完整返回。"),
|
||||
);
|
||||
let source_url = source_url.clone();
|
||||
let processed_url = processed_url.clone();
|
||||
initial_history,
|
||||
|history| {
|
||||
let tool = tool.clone();
|
||||
let client = client.clone();
|
||||
let llm_config = llm_config.clone();
|
||||
async move {
|
||||
let request = LlmRunRequest::new(vec![LlmMessage::user_multimodal(vec![
|
||||
LlmMessageContentPart::InputText { text: prompt },
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: source_url,
|
||||
},
|
||||
LlmMessageContentPart::InputImage {
|
||||
image_url: processed_url,
|
||||
},
|
||||
])])
|
||||
.with_function_tools(vec![tool.clone()])
|
||||
.with_tool_choice(LlmToolChoice::Required);
|
||||
let request = LlmRunRequest::new(history)
|
||||
.with_function_tools(vec![tool.clone()])
|
||||
.with_tool_choice(LlmToolChoice::Required);
|
||||
request_ui_editor_llm(&client, &llm_config, request)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::agent::request_game_creator_llm_text;
|
||||
use crate::config::{apply_game_creator_llm_reasoning_effort, parse_game_creator_llm_api_kind};
|
||||
use base64::Engine as _;
|
||||
use platform_llm::{LlmClient, LlmError, LlmRunRequest, LlmRunResponse};
|
||||
use platform_llm::{LlmClient, LlmError, LlmMessage, LlmRunRequest, LlmRunResponse};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Serialize;
|
||||
use std::fs::File;
|
||||
use std::future::Future;
|
||||
use std::io::Read;
|
||||
@@ -26,31 +27,39 @@ pub(crate) async fn request_ui_editor_llm(
|
||||
request_game_creator_llm_text(client, llm, request).await
|
||||
}
|
||||
|
||||
/// 结构化 LLM 请求的小型 repair harness:第一次请求或校验失败后,
|
||||
/// 将错误反馈给模型并只额外重试一次。网络/模型调用本身的错误也会
|
||||
/// 进入第二次请求的反馈文本;调用方负责在第二次失败后决定业务状态。
|
||||
pub(crate) async fn request_with_feedback<T, Request, Fut, Validate>(
|
||||
more_turn: usize,
|
||||
request: Request,
|
||||
validate: Validate,
|
||||
/// 按 append-only history 重试结构化 LLM 请求;仅业务校验失败会追加反馈消息。
|
||||
pub(crate) async fn run_with_repair_history<T, Requester, Fut, Validater>(
|
||||
max_retries: usize,
|
||||
initial_history: Vec<LlmMessage>,
|
||||
requester: Requester,
|
||||
validater: Validater,
|
||||
) -> Result<T, String>
|
||||
where
|
||||
Request: Fn(Option<String>) -> Fut,
|
||||
T: Serialize,
|
||||
Requester: Fn(Vec<LlmMessage>) -> Fut,
|
||||
Fut: Future<Output = Result<T, String>>,
|
||||
Validate: Fn(&T) -> Result<(), String>,
|
||||
Validater: Fn(&T) -> Result<(), String>,
|
||||
{
|
||||
let mut feedback = None;
|
||||
for attempt in 0..=more_turn {
|
||||
let result = request(feedback.clone())
|
||||
.await
|
||||
.and_then(|value| validate(&value).map(|_| value));
|
||||
match result {
|
||||
Ok(value) => return Ok(value),
|
||||
Err(error) if attempt < more_turn => feedback = Some(error),
|
||||
let mut history = initial_history;
|
||||
for attempt in 0..=max_retries {
|
||||
let value = match requester(history.clone()).await {
|
||||
Ok(value) => value,
|
||||
Err(_error) if attempt < max_retries => continue,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
match validater(&value) {
|
||||
Ok(()) => return Ok(value),
|
||||
Err(error) if attempt < max_retries => {
|
||||
let serialized = serde_json::to_string(&value)
|
||||
.map_err(|serialize_error| format!("序列化修复反馈失败:{serialize_error}"))?;
|
||||
history.push(LlmMessage::system(format!(
|
||||
"上一次模型输出:\n{serialized}\n\n业务校验失败:\n{error}\n\n请修正并完整返回。"
|
||||
)));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
unreachable!("repair harness always returns within requested turns")
|
||||
unreachable!("repair history runner always returns within requested retries")
|
||||
}
|
||||
|
||||
pub(crate) fn parse_limited_llm_tool_arguments(
|
||||
@@ -173,15 +182,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn feedback_harness_zero_more_turn_calls_once_without_feedback() {
|
||||
async fn repair_history_zero_retries_calls_once_with_initial_history() {
|
||||
let calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let seen = calls.clone();
|
||||
let result = request_with_feedback(
|
||||
let initial_history = vec![LlmMessage::user("初始 prompt")];
|
||||
let result = run_with_repair_history(
|
||||
0,
|
||||
move |feedback| {
|
||||
initial_history.clone(),
|
||||
move |history| {
|
||||
let seen = seen.clone();
|
||||
async move {
|
||||
seen.lock().unwrap().push(feedback);
|
||||
seen.lock().unwrap().push(history);
|
||||
Ok::<_, String>(serde_json::json!({"ok": true}))
|
||||
}
|
||||
},
|
||||
@@ -190,7 +201,85 @@ mod tests {
|
||||
.await
|
||||
.expect("single turn should succeed");
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
assert_eq!(calls.lock().unwrap().as_slice(), &[None]);
|
||||
assert_eq!(calls.lock().unwrap().as_slice(), &[initial_history]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repair_history_request_error_retries_without_appending_history() {
|
||||
let calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let attempts = std::sync::Arc::new(std::sync::Mutex::new(0usize));
|
||||
let seen_calls = calls.clone();
|
||||
let seen_attempts = attempts.clone();
|
||||
let initial_history = vec![LlmMessage::user("初始 prompt")];
|
||||
let result = run_with_repair_history(
|
||||
1,
|
||||
initial_history.clone(),
|
||||
move |history| {
|
||||
seen_calls.lock().unwrap().push(history);
|
||||
let attempt = {
|
||||
let mut attempts = seen_attempts.lock().unwrap();
|
||||
let attempt = *attempts;
|
||||
*attempts += 1;
|
||||
attempt
|
||||
};
|
||||
async move {
|
||||
if attempt == 0 {
|
||||
Err("网络错误".to_string())
|
||||
} else {
|
||||
Ok::<_, String>(serde_json::json!({"ok": true}))
|
||||
}
|
||||
}
|
||||
},
|
||||
|_| Ok(()),
|
||||
)
|
||||
.await
|
||||
.expect("retry-only error should recover");
|
||||
assert_eq!(result, serde_json::json!({"ok": true}));
|
||||
assert_eq!(
|
||||
calls.lock().unwrap().as_slice(),
|
||||
&[initial_history.clone(), initial_history]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repair_history_business_failure_appends_serialized_value_and_error() {
|
||||
let calls = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
|
||||
let attempts = std::sync::Arc::new(std::sync::Mutex::new(0usize));
|
||||
let seen_calls = calls.clone();
|
||||
let seen_attempts = attempts.clone();
|
||||
let initial_history = vec![LlmMessage::user("初始 prompt")];
|
||||
let result = run_with_repair_history(
|
||||
1,
|
||||
initial_history.clone(),
|
||||
move |history| {
|
||||
seen_calls.lock().unwrap().push(history);
|
||||
let mut attempts = seen_attempts.lock().unwrap();
|
||||
let attempt = *attempts;
|
||||
*attempts += 1;
|
||||
async move { Ok::<_, String>(serde_json::json!({"attempt": attempt})) }
|
||||
},
|
||||
|value: &serde_json::Value| {
|
||||
if value["attempt"] == 0 {
|
||||
Err("业务校验失败".to_string())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("business feedback should recover");
|
||||
assert_eq!(result, serde_json::json!({"attempt": 1}));
|
||||
let calls = calls.lock().unwrap();
|
||||
assert_eq!(calls.len(), 2);
|
||||
assert_eq!(calls[0], initial_history);
|
||||
assert_eq!(calls[1].len(), 2);
|
||||
assert_eq!(calls[1][0], LlmMessage::user("初始 prompt"));
|
||||
assert_eq!(
|
||||
calls[1][1],
|
||||
LlmMessage::system(
|
||||
"上一次模型输出:\n{\"attempt\":0}\n\n业务校验失败:\n业务校验失败\n\n请修正并完整返回。"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8034,6 +8034,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- AGC LLM 对话入口在解析 Router 凭据和访问上游前先读取用户 `wallet_balance`。余额为 `0` 时直接返回 `409 MUD_POINTS_INSUFFICIENT`,客户端显示“泥点余额不足”;不创建、续期或使用 Router 账号。余额读取失败同样失败关闭,返回“泥点余额暂时不可用”。
|
||||
- 余额大于 `0` 的请求继续走 Router,成功后仍按 best-effort 后置结算;退款占用、冻结或扣费时余额不足的处理继续由钱包事务和既有结算规则负责。
|
||||
|
||||
## 2026-09-09 UI Editor 结构化请求 repair history
|
||||
|
||||
- UI Editor 的结构化 LLM repair 由 `run_with_repair_history` 统一维护 append-only `LlmMessage` history;调用方只构造初始 prompt 并提供 `requester(history) -> Result<T, String>`。
|
||||
- `validater(&T) -> Result<(), String>` 只负责业务校验。网络、模型、tool 缺失、JSON 或反序列化错误只按原 history 重试;只有业务校验失败才把序列化后的响应和校验错误合并为一条 system message 追加到 history。
|
||||
- history 仅存在本次请求内存中,不重复图片、不截断、不扩展 `platform-llm` 消息协议;重试次数参数统一使用 `max_retries`。
|
||||
|
||||
## 2026-08-29 DirectProject 受控联网搜索默认与边界
|
||||
|
||||
- 正式产品本次只覆盖 `DirectProject` 单 Codex Agent。`Provider`、`ToolHost`、`DirectHome` 不是 Agent,也不是本次联网主链路;不新增全路由联网或工具桥。唯一受控联网工具为 `agc_tools.agc_web_search`,链路固定为 Codex MCP 工具目录 -> 客户端 loopback `DirectToolBridge` -> 有界 Bing RSS HTTPS -> 过滤 / 脱敏 -> MCP 结果回传。
|
||||
|
||||
Reference in New Issue
Block a user