优化子agent提示词

This commit is contained in:
xiaoski 2026-05-29 11:22:48 +08:00
parent b89dce013c
commit ea1338c94f
4 changed files with 197 additions and 69 deletions

View File

@ -7,4 +7,7 @@ pub mod system_prompt;
pub use agent_loop::{AgentLoop, AgentError, AgentProcessResult}; pub use agent_loop::{AgentLoop, AgentError, AgentProcessResult};
pub use context_compressor::{ContextCompressor, estimate_tokens}; pub use context_compressor::{ContextCompressor, estimate_tokens};
pub use sub_agent::{DelegateContext, ExecutionMode, SubAgentConfig, SubAgentError, SubAgentManager, SubAgentResult, TaskNotification, TaskStatus}; pub use sub_agent::{DelegateContext, ExecutionMode, SubAgentConfig, SubAgentError, SubAgentManager, SubAgentResult, TaskNotification, TaskStatus};
pub use system_prompt::{build_system_prompt, PromptContext, PromptSection, SystemPromptBuilder}; pub use system_prompt::{
build_system_prompt, build_sub_agent_system_prompt, PromptContext, PromptSection,
SystemPromptBuilder,
};

View File

@ -6,11 +6,13 @@ use dashmap::DashMap;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use uuid::Uuid; use uuid::Uuid;
use crate::agent::system_prompt::build_sub_agent_system_prompt;
use crate::agent::AgentLoop; use crate::agent::AgentLoop;
use crate::agent::AgentError; use crate::agent::AgentError;
use crate::bus::ChatMessage; use crate::bus::ChatMessage;
use crate::config::LLMProviderConfig; use crate::config::LLMProviderConfig;
use crate::providers::{create_provider, LLMProvider}; use crate::providers::{create_provider, LLMProvider};
use crate::skills::SkillsLoader;
use crate::tools::ToolRegistry; use crate::tools::ToolRegistry;
tokio::task_local! { tokio::task_local! {
@ -116,6 +118,7 @@ pub struct SubAgentManager {
active_tasks: Arc<DashMap<String, CancellationToken>>, active_tasks: Arc<DashMap<String, CancellationToken>>,
notify_tx: tokio::sync::mpsc::UnboundedSender<TaskNotification>, notify_tx: tokio::sync::mpsc::UnboundedSender<TaskNotification>,
max_concurrent_background_tasks: usize, max_concurrent_background_tasks: usize,
skills_loader: Option<Arc<SkillsLoader>>,
} }
impl SubAgentManager { impl SubAgentManager {
@ -125,6 +128,7 @@ impl SubAgentManager {
storage: Option<Arc<crate::storage::Storage>>, storage: Option<Arc<crate::storage::Storage>>,
notify_tx: tokio::sync::mpsc::UnboundedSender<TaskNotification>, notify_tx: tokio::sync::mpsc::UnboundedSender<TaskNotification>,
max_concurrent_background_tasks: usize, max_concurrent_background_tasks: usize,
skills_loader: Option<Arc<SkillsLoader>>,
) -> Self { ) -> Self {
Self { Self {
provider_config, provider_config,
@ -133,6 +137,7 @@ impl SubAgentManager {
active_tasks: Arc::new(DashMap::new()), active_tasks: Arc::new(DashMap::new()),
notify_tx, notify_tx,
max_concurrent_background_tasks, max_concurrent_background_tasks,
skills_loader,
} }
} }
@ -150,46 +155,17 @@ impl SubAgentManager {
Arc::new(filtered) Arc::new(filtered)
} }
pub fn build_system_prompt(&self, config: &SubAgentConfig, tools: &ToolRegistry) -> String { fn get_skills_prompt(&self, tools: &ToolRegistry) -> Option<String> {
let timeout_human = format_duration(config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS)); let has_get_skill = tools.iter().iter().any(|(name, _)| name == "get_skill");
let tool_descriptions = tools.describe_for_prompt(); if has_get_skill {
if let Some(ref loader) = self.skills_loader {
let http_only_note = if config.allowed_tools.is_none() let prompt = loader.build_skills_prompt();
|| config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request")) if !prompt.is_empty() {
{ return Some(prompt);
"- When using http_request, only the GET method is permitted. \ }
Do NOT use POST, PUT, DELETE, or any other method." }
} else { }
"" None
};
format!(
"You are a sub-agent working on a delegated task. Complete the task below \
and return a single, self-contained result.\n\
\n\
## Task\n\
{task}\n\
\n\
## Rules\n\
- Focus ONLY on the task above. Do not explore unrelated topics.\n\
- Use tools only when necessary for the task.\n\
- Do NOT use the delegate tool sub-agent recursion is forbidden.\n\
- If the task cannot be completed, explain why clearly.\n\
- Return only the final result. Do not describe your process.\n\
{http_only}\n\
- Timeout: {timeout_human}. If approaching the limit, return partial results.\n\
\n\
## Available Tools\n\
{tool_descriptions}\n\
\n\
## Workspace\n\
{workspace}",
task = config.prompt,
http_only = http_only_note,
timeout_human = timeout_human,
tool_descriptions = tool_descriptions,
workspace = self.provider_config.workspace_dir.display(),
)
} }
pub fn build_sub_agent( pub fn build_sub_agent(
@ -228,8 +204,20 @@ impl SubAgentManager {
) -> Result<SubAgentResult, SubAgentError> { ) -> Result<SubAgentResult, SubAgentError> {
let task_id = generate_task_id(); let task_id = generate_task_id();
let tools = self.filter_tools(&config.allowed_tools); let tools = self.filter_tools(&config.allowed_tools);
let system_prompt = self.build_system_prompt(&config, &tools);
let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS); let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
let timeout_human = format_duration(timeout_secs);
let http_get_only = config.allowed_tools.is_none()
|| config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request"));
let skills_prompt = self.get_skills_prompt(&tools);
let system_prompt = build_sub_agent_system_prompt(
&config.prompt,
&timeout_human,
&tools,
&self.provider_config.workspace_dir,
&self.provider_config.model_id,
skills_prompt,
http_get_only,
);
let agent = self.build_sub_agent(&config, tools) let agent = self.build_sub_agent(&config, tools)
.map_err(|e| SubAgentError::ProviderCreation(e.to_string()))?; .map_err(|e| SubAgentError::ProviderCreation(e.to_string()))?;
@ -352,8 +340,20 @@ impl SubAgentManager {
} }
let tools = self.filter_tools(&config.allowed_tools); let tools = self.filter_tools(&config.allowed_tools);
let system_prompt = self.build_system_prompt(&config, &tools);
let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS); let timeout_secs = config.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
let timeout_human = format_duration(timeout_secs);
let http_get_only = config.allowed_tools.is_none()
|| config.allowed_tools.as_ref().is_some_and(|v| v.iter().any(|t| t == "http_request"));
let skills_prompt = self.get_skills_prompt(&tools);
let system_prompt = build_sub_agent_system_prompt(
&config.prompt,
&timeout_human,
&tools,
&self.provider_config.workspace_dir,
&self.provider_config.model_id,
skills_prompt,
http_get_only,
);
let provider_config = self.provider_config.clone(); let provider_config = self.provider_config.clone();
let storage = self.storage.clone(); let storage = self.storage.clone();
let notify_tx = self.notify_tx.clone(); let notify_tx = self.notify_tx.clone();

View File

@ -56,6 +56,30 @@ impl SystemPromptBuilder {
} }
} }
/// Create a builder with sub-agent specific sections.
pub fn with_sub_agent_defaults(
task: &str,
timeout: &str,
skills_prompt: Option<String>,
http_get_only: bool,
) -> Self {
let mut sections: Vec<Box<dyn PromptSection>> = vec![
Box::new(SubAgentIdentitySection {
task: task.to_string(),
timeout: timeout.to_string(),
}),
Box::new(ToolHonestySection),
Box::new(SafetySection),
Box::new(SubAgentToolsSection { http_get_only }),
Box::new(WorkspaceSection),
Box::new(DateTimeSection),
];
if let Some(sp) = skills_prompt {
sections.push(Box::new(SubAgentSkillsSection { skills_prompt: sp }));
}
Self { sections }
}
/// Add a custom section to the builder. /// Add a custom section to the builder.
pub fn add_section(mut self, section: Box<dyn PromptSection>) -> Self { pub fn add_section(mut self, section: Box<dyn PromptSection>) -> Self {
self.sections.push(section); self.sections.push(section);
@ -360,32 +384,105 @@ impl PromptSection for DelegationSection {
fn build(&self, _ctx: &PromptContext<'_>) -> String { fn build(&self, _ctx: &PromptContext<'_>) -> String {
"## 子 Agent 委托原则\n\n\ "## 子 Agent 委托原则\n\n\
使 delegate Agent\n\ 使 delegate Agent\n\
\n\ \n\
### \n\ ### \n\
- 使 mode=\"parallel\"\n\ - 使 mode=\"parallel\"\n\
- 使 mode=\"background\"\n\ - 使 mode=\"background\"\n\
- \n\ - \n\
\n\ \n\
### \n\ ### \n\
- **** Agent \n\ - **** Agent \n\
- **** file_readfile_searchweb_fetch bashfile_writefile_edit\n\ - **** file_readfile_searchweb_fetch bashfile_writefile_edit\n\
- **** delegate Agent\n\ - **** delegate Agent\n\
- **** Agent \n\ - **** Agent \n\
\n\ \n\
### \n\ ### Skill \n\
- prompt \n\ - skill allowed_tools get_skill\n\
- prompt \"跳过 .tmp 文件\"\n\ - prompt Agent 使 get_skill \n\
- \n\ - \"使用 get_skill action='get' skill_name='pdf' 加载 PDF 处理技能后完成任务\"\n\
\n\ \n\
### \n\ ### \n\
- 使 mode=\"parallel\",任务定义在 tasks 数组中\n\ - prompt \n\
- \n\ - prompt \"跳过 .tmp 文件\"\n\
- 5 \n\ - \n\
\n\ \n\
### \n\ ### \n\
- 30s 使 mode=\"background\"\n\ - 使 mode=\"parallel\",任务定义在 tasks 数组中\n\
- ".to_string() - \n\
- 5 \n\
\n\
### \n\
- 30s 使 mode=\"background\"\n\
- ".to_string()
}
}
// === Sub-Agent Prompt Sections ===
/// Sub-agent identity and task instructions.
pub struct SubAgentIdentitySection {
pub task: String,
pub timeout: String,
}
impl PromptSection for SubAgentIdentitySection {
fn name(&self) -> &str {
"sub_agent_identity"
}
fn build(&self, _ctx: &PromptContext<'_>) -> String {
format!(
"## 子 Agent\n\n\
Agent Agent Agent\n\
\n\
## \n\n\
{}\n\
\n\
## \n\
- \n\
- 使\n\
- 使 delegate \n\
- \n\
- \n\
- {}",
self.task, self.timeout,
)
}
}
/// Sub-agent available tools description.
pub struct SubAgentToolsSection {
pub http_get_only: bool,
}
impl PromptSection for SubAgentToolsSection {
fn name(&self) -> &str {
"sub_agent_tools"
}
fn build(&self, ctx: &PromptContext<'_>) -> String {
let mut s = String::from("## 可用工具\n\n");
s.push_str(&ctx.tools.describe_for_prompt());
if self.http_get_only {
s.push_str("\n\n**注意**:使用 http_request 时只允许 GET 方法,禁止 POST、PUT、DELETE 等。");
}
s
}
}
/// Sub-agent skills information, injected when get_skill tool is available.
pub struct SubAgentSkillsSection {
pub skills_prompt: String,
}
impl PromptSection for SubAgentSkillsSection {
fn name(&self) -> &str {
"sub_agent_skills"
}
fn build(&self, _ctx: &PromptContext<'_>) -> String {
self.skills_prompt.clone()
} }
} }
@ -445,6 +542,33 @@ pub fn build_system_prompt(
SystemPromptBuilder::with_defaults().build(&ctx) SystemPromptBuilder::with_defaults().build(&ctx)
} }
/// Build a system prompt for a sub-agent with all relevant operational sections.
pub fn build_sub_agent_system_prompt(
task: &str,
timeout_human: &str,
tools: &ToolRegistry,
workspace_dir: &Path,
model_name: &str,
skills_prompt: Option<String>,
http_get_only: bool,
) -> String {
let ctx = PromptContext {
workspace_dir,
model_name,
tools,
session_id: None,
memory_context: None,
has_compressed_history: false,
};
SystemPromptBuilder::with_sub_agent_defaults(
task,
timeout_human,
skills_prompt,
http_get_only,
)
.build(&ctx)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -870,6 +870,7 @@ impl SessionManager {
Some(storage.clone()), Some(storage.clone()),
notify_tx, notify_tx,
max_concurrent_background_tasks, max_concurrent_background_tasks,
Some(skills_loader.clone()),
)); ));
tools.register(crate::tools::DelegateTool::new(sub_agent_manager.clone())); tools.register(crate::tools::DelegateTool::new(sub_agent_manager.clone()));