优化子agent提示词
This commit is contained in:
parent
b89dce013c
commit
ea1338c94f
@ -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,
|
||||||
|
};
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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_read、file_search、web_fetch 完成,不要给写权限(bash、file_write、file_edit)\n\
|
- **只读优先**:如果可以只用 file_read、file_search、web_fetch 完成,不要给写权限(bash、file_write、file_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::*;
|
||||||
|
|||||||
@ -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()));
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user