PicoBot/src/tools/task/prompt.rs
oudecheng 457f6b2408 feat(retry): 为 LLM 主调用添加可配置重试机制,默认 3 次
- config: ProviderConfig 增加 max_retries 字段(serde default=3,向后兼容)
- config: LLMProviderConfig 透传 max_retries,不进 ProviderRuntimeConfig(保持 provider 构造包纯净)
- agent: AgentRuntimeConfig 增加 max_retries,归属 agent 行为层
- agent_loop: 流式 + summary 两个调用点实现重试循环
  - 指数退避 1s/2s/4s,仅对 429/502/503/504/timeout/connection reset 重试
  - 流式仅在未 emit delta 时重试(AtomicBool 跟踪),避免重复输出
  - 退避 sleep 期间响应 cancel_signal,取消优先
- 前端: ProviderConfig 类型和表单增加 max_retries 字段
- 测试: 7 个单元测试(3 判定 + 4 行为),540 个 lib 测试全绿
2026-08-04 12:53:33 +08:00

179 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::types::SubagentDef;
use crate::config::LLMProviderConfig;
/// 子代理系统提示词构建器
pub struct SubagentPromptBuilder;
impl SubagentPromptBuilder {
/// 构建子代理系统提示词(包含系统环境信息和技能索引)
pub fn build(
def: &SubagentDef,
description: &str,
prompt: &str,
config: &LLMProviderConfig,
skills_index: Option<&str>,
) -> String {
let base_prompt = Self::interpolate_template(def, description, prompt);
let env_info = crate::agent::generate_system_env_prompt(config);
// 组合提示词:基础 + 环境 + 技能索引(可选)
match skills_index {
Some(index) if !index.is_empty() => {
format!("{}\n\n{}\n\n{}", base_prompt, env_info, index)
}
_ => format!("{}\n\n{}", base_prompt, env_info),
}
}
/// 构建恢复任务的提示词
pub fn build_resume_prompt(session_description: &str, additional_prompt: &str) -> String {
format!(
"你正在继续执行一个之前创建的子代理任务。\n\n\
任务描述: {}\n\n\
继续执行指令: {}\n\n\
你应该:\n\
1. 回顾之前的工作进度(如果已有历史)\n\
2. 继续完成任务,不要偏离目标\n\
3. 完成后给出简洁的总结\n\
4. 不要尝试创建新的子代理任务\n\n\
注意: 你在一个独立的执行上下文中,没有访问主对话历史的权限。",
session_description, additional_prompt
)
}
/// 插值提示词模板
fn interpolate_template(def: &SubagentDef, description: &str, prompt: &str) -> String {
// 自定义子代理使用通用模板
let base = if def.prompt_template.is_empty() {
"你是一个专注的子代理,正在执行一个独立任务。\n\n\
任务描述: {{description}}\n\n\
你应该:\n\
1. 专注于完成任务,不要偏离目标\n\
2. 使用可用的工具进行必要操作\n\
3. 完成后给出简洁的总结\n\
4. 当任务复杂度较高时,可以使用 `task` 工具创建子代理来处理独立子任务\n\n\
任务追踪:\n\
你可以使用 `todo_write` 工具追踪子任务进度。规则:同一时间只有一个 in_progress完成后再标记下一个3步以上才使用。\n\n\
注意: 你没有访问主对话历史的权限,这是一个独立的执行上下文。"
} else {
&def.prompt_template
};
let mut result = base
.replace("{{description}}", description)
.replace("{{prompt}}", prompt);
if let Some(ref body) = def.body {
result.push_str("\n\n");
result.push_str(body);
}
result
}
}
/// 从子代理输出提取简洁摘要
pub fn extract_summary(content: &str) -> String {
// 取第一段或前 500 字符
let first_paragraph = content
.lines()
.take_while(|line| !line.trim().is_empty())
.collect::<Vec<_>>()
.join("\n");
if first_paragraph.len() > 500 {
first_paragraph.chars().take(500).collect()
} else if first_paragraph.is_empty() {
content.chars().take(200).collect()
} else {
first_paragraph
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tools::task::types::SubagentSource;
fn test_def() -> SubagentDef {
SubagentDef {
name: "general".to_string(),
description: "测试".to_string(),
prompt_template: "任务: {{description}}\n指令: {{prompt}}".to_string(),
body: None,
capability: crate::domain::CapabilityPolicy::default(),
max_execution_secs: None,
source: SubagentSource::Builtin,
path: None,
provider: None,
model: None,
}
}
#[test]
fn test_interpolates_template() {
let def = test_def();
let result = SubagentPromptBuilder::build(
&def,
"审查代码",
"检查安全漏洞",
&LLMProviderConfig {
provider_type: "openai".to_string(),
name: "test".to_string(),
base_url: "http://localhost".to_string(),
api_key: "test".to_string(),
extra_headers: std::collections::HashMap::new(),
llm_timeout_secs: 120,
memory_maintenance_timeout_secs: 600,
max_retries: 3,
model_id: "test".to_string(),
temperature: None,
max_tokens: None,
context_window_tokens: None,
model_extra: std::collections::HashMap::new(),
max_tool_iterations: 1,
tool_result_max_chars: 1000,
context_tool_result_trim_chars: 1000,
max_images_in_context: 1,
max_image_age_rounds: 10,
},
None,
);
assert!(result.contains("任务: 审查代码"));
assert!(result.contains("指令: 检查安全漏洞"));
}
#[test]
fn test_appends_body() {
let mut def = test_def();
def.body = Some("额外指令".to_string());
let result = SubagentPromptBuilder::build(
&def,
"描述",
"指令",
&LLMProviderConfig {
provider_type: "openai".to_string(),
name: "test".to_string(),
base_url: "http://localhost".to_string(),
api_key: "test".to_string(),
extra_headers: std::collections::HashMap::new(),
llm_timeout_secs: 120,
memory_maintenance_timeout_secs: 600,
max_retries: 3,
model_id: "test".to_string(),
temperature: None,
max_tokens: None,
context_window_tokens: None,
model_extra: std::collections::HashMap::new(),
max_tool_iterations: 1,
tool_result_max_chars: 1000,
context_tool_result_trim_chars: 1000,
max_images_in_context: 1,
max_image_age_rounds: 10,
},
None,
);
assert!(result.contains("额外指令"));
}
}