使用 ADK-Rust 构建多智能体系统
了解何时以及如何使用不同的多智能体模式——从简单的协调到复杂的基于图的编排——并利用 Rust 的类型安全和高性能。
1. 简介
问题:触及瓶颈的AI
您已经构建了您的第一个AI代理。它令人印象深刻——可以回答问题、总结文档,甚至可能编写代码。但随后现实来袭:
"您能帮我查看上一个发票,并协助我设置 API 吗?"
您的代理陷入困境。它没有经过账单系统、开发者文档和故障排除工作流程的训练。它的指令提示词已经达到2000个token,试图涵盖所有内容。响应质量随之下降。
这就是单代理的瓶颈。随着您的应用程序增长,您将面临痛苦的权衡:
- 臃肿的提示词: 每增加一项新功能都意味着更长的指令、更高的延迟以及模型更多的困惑
- 万金油: 一个代理同时处理账单、支持和销售,结果在三者上都表现平平
- 难以维护: 更改账单逻辑不应冒着破坏您的支持流程的风险
- 缺乏专业化: 您的数学代理无法拥有计算器工具,而您的研究代理却拥有网页搜索——它们共享一切
解决方案:专业化代理协同工作
多代理系统通过将复杂任务分解为专业角色来解决此问题。您不是创建一个不堪重负的通才,而是创建专注的专家:
- 客户服务: 一个 coordinator 将用户路由到账单、技术支持或销售专家——每个专家都拥有专注的训练和工具
- 内容创作: 一个研究代理收集事实,一个 writer 撰写叙述,一个编辑进行润色——每个代理都精通一项技能
- 代码生成: 规划师设计架构,coder 负责实现,审阅者发现错误——不同的视角提升质量
结果如何?每个代理都保持专注,提示词易于管理,并且您可以在不影响支持的情况下更新计费逻辑。这是面向 AI 的微服务。
您将学到什么
ADK-Rust 提供了三种渐进式强大的多代理编排模式。本教程将教您:
- 根据您的需求何时使用每种模式
- 如何使用生产就绪的 Rust 代码实现它们
- 为什么架构权衡对您的特定用例很重要
2. 选择正确的模式
在深入代码之前,让我们了解每种模式的特点:
| 模式 | 最适合 | 控制级别 | 复杂性 |
|---|---|---|---|
| 协调器 | 对话交接 | LLM 决定 | 低 |
| AgentTool | 响应处理 | 协调器处理 | 中 |
| 主管图 | 复杂工作流 | 完整状态管理 | 高 |
3. 模式 1:协调器(子代理)
🎯 用例:客户服务路由
您正在构建一个客户服务机器人。用户可能会询问账单、请求技术帮助或咨询新功能。每个领域都需要专业知识,但用户不应该需要知道联系哪个部门。
协调器模式使用自动代理转移。当您通过 .sub_agent() 添加子代理时,ADK-Rust 会注入一个 transfer_to_agent 工具。LLM 根据对话决定何时移交。
主要特点
- 无缝移交:用户自然地与专家继续对话
- LLM 驱动的路由:coordinator 根据对话上下文做出决定
- 对话连续性:会话历史记录在转移过程中保持不变
- 无响应处理:转移后专家直接与用户对话
实现
use adk_rust::prelude::*;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Specialist: Billing Agent
// Clear description helps the coordinator know when to transfer
let billing_agent = LlmAgentBuilder::new("billing_agent")
.description(
"Handles all billing questions: invoices, payments, refunds, subscription plans, and account charges. Transfer here for any money-related questions."
)
.instruction(
"You are a billing specialist. Answer questions about invoices, payments, and subscription plans. Be concise and accurate. If asked about technical issues, suggest transferring to support."
)
.model(model.clone())
.build()?;
// Specialist: Technical Support Agent
let support_agent = LlmAgentBuilder::new("support_agent")
.description(
"Provides technical support: troubleshooting, bug reports, feature questions, and integration help. Transfer here for any technical problems."
)
.instruction(
"You are a technical support specialist. Help users troubleshoot issues step by step. Ask clarifying questions when needed. Be patient and thorough."
)
.model(model.clone())
.build()?;
// Coordinator: Routes to specialists
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Customer service coordinator")
.instruction(
"You are a friendly customer service coordinator. Your job is to:\n 1. Greet users warmly\n 2. Understand their needs\n 3. Route to the right specialist:\n - Billing questions → transfer to billing_agent\n - Technical issues → transfer to support_agent\n 4. Handle general questions yourself\n\n Always explain who you're connecting them with."
)
.model(model.clone())
.sub_agent(Arc::new(billing_agent))
.sub_agent(Arc::new(support_agent))
.build()?;
// Run with the built-in launcher
Launcher::new(Arc::new(coordinator))
.run()
.await?;
Ok(())
}对话示例
用户: 您好,我有一个关于账单的问题
[coordinator]: 您好!我很乐意帮助您解决账单问题。让我为您转接我们的账单专家,他可以协助您。
系统: 🔄 转接至:billing_agent
[billing_agent]: 您好!我是账单专家。我可以帮助您处理发票、付款和订阅问题。您想了解关于账单的什么信息?
用户: 为什么我这个月被收了两次费?
[billing_agent]: 我将为您调查那笔重复收费...
✅ 何时使用协调器
- 用户应直接与专家互动
- 路由决策简单明了
- 您无需处理专家响应
- 对话流程是线性的(一次一位专家)
4. 模式 2:将代理作为工具 (AgentTool)
🎯 用例:知识聚合
您正在构建一个智能助手,用于回答跨多个领域的问题。用户提问“250 的 15% 是多少,这个数字在历史上有什么意义?”您需要调用数学专家,然后调用知识问答专家,并结合他们的答案。
AgentTool 模式将代理封装为可调用的工具。与子代理不同,coordinator 以编程方式调用专家,并在回复用户之前接收他们的响应进行处理或组合。
与协调器的主要区别
协调器(子代理)
- • 专家直接与用户对话
- • 一次一位专家
- • 无需响应处理
AgentTool
- • 协调器接收响应
- • 可调用多位专家
- • 聚合和总结
实现
use adk_agent::LlmAgentBuilder;
use adk_tool::{AgentTool, FunctionTool};
use adk_core::ToolContext;
use serde_json::{json, Value};
use std::sync::Arc;
// Calculator tool for the math agent
async fn calculator(
_ctx: Arc<dyn ToolContext>,
args: Value
) -> Result<Value, adk_core::AdkError> {
let operation = args["operation"].as_str().unwrap_or("add");
let a = args["a"].as_f64().unwrap_or(0.0);
let b = args["b"].as_f64().unwrap_or(0.0);
let result = match operation {
"add" => a + b,
"multiply" => a * b,
"percent" => a * (b / 100.0),
_ => return Err(adk_core::AdkError::Tool(
format!("Unknown operation: {}", operation)
)),
};
Ok(json!({ "result": result }))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.5-flash")?);
// Create calculator tool
let calc_tool = FunctionTool::new(
"calculator",
"Performs arithmetic: add, multiply, percent. Args: operation (string), a (number), b (number)",
calculator,
);
// Math Expert agent - has its own tools
let math_agent = LlmAgentBuilder::new("math_expert")
.description(
"A math expert that performs calculations. Use for any math-related questions, percentages, or numerical analysis."
)
.instruction(
"You are a math expert. Use the calculator tool for calculations. Show your work step by step. Be precise."
)
.model(model.clone())
.tool(Arc::new(calc_tool))
.build()?;
// Trivia Expert agent - uses LLM knowledge
let trivia_agent = LlmAgentBuilder::new("trivia_expert")
.description(
"A trivia and history expert. Use for questions about historical facts, pop culture, science facts, and trivia."
)
.instruction(
"You are a trivia expert with vast knowledge across domains. Answer questions accurately and include interesting related facts."
)
.model(model.clone())
.build()?;
// Wrap agents as tools with configuration
let math_tool = AgentTool::new(Arc::new(math_agent))
.skip_summarization(false) // Summarize lengthy responses
.forward_artifacts(true); // Pass through any generated files
let trivia_tool = AgentTool::new(Arc::new(trivia_agent))
.skip_summarization(false);
// Coordinator uses agents as tools
let coordinator = LlmAgentBuilder::new("coordinator")
.description("Smart assistant that combines expert knowledge")
.instruction(
"You are a helpful assistant with access to expert agents:\n - math_expert: For calculations and math problems\n - trivia_expert: For facts, history, and trivia\n\n When questions span multiple domains, call multiple experts and synthesize their responses into a cohesive answer."
)
.model(model)
.tool(Arc::new(math_tool))
.tool(Arc::new(trivia_tool))
.build()?;
Launcher::new(Arc::new(coordinator)).run().await?;
Ok(())
}示例:多领域问题
用户: 250 的 15% 是多少,这个数字在历史上有什么意义?
系统: // 协调器调用 math_expert 工具
[math_expert 响应]: 250 的 15% 是 37.5
系统: // 协调器调用 trivia_expert 工具
[trivia_expert 回应]: 37 和 38 在历史上不那么引人注目,但 37.5°C 是人体体温...
系统: // 协调器进行综合
[coordinator]: 250 的 15% 等于 37.5。有趣的是,37.5°C (99.5°F) 接近人体平均体温 37°C,使其成为一个具有医学意义的数字!
✅ 何时使用 AgentTool
- 您需要整合多位专家的回应
- 协调器应总结或筛选专家输出
- 专家拥有自己的工具(嵌套能力)
- 您希望对代理调用进行程序化控制
5. 模式 3:主管图
🎯 用例:内容创作流程
您正在构建一个内容创作系统。给定一个主题,您需要:(1) 研究它,(2) 撰写文章,(3) 添加代码示例。supervisor 根据任务动态决定顺序,并且工作人员可以循环返回进行修订。
主管图模式使用 ADK-Rust 的基于图的工作流系统。supervisor 代理动态路由到工作器,具有完整的状态管理和循环执行支持。
为何使用图?
- 动态路由:主管根据当前状态决定下一个工作器
- 循环执行:工作器可以循环返回进行迭代
- 共享状态:所有节点读/写一个共同的状态对象
- 条件边:基于 LLM 决策的不同路径
- 递归限制:防止无限循环
实现
use adk_agent::LlmAgentBuilder;
use adk_graph::{
StateGraph,
edge::{START, END},
node::{AgentNode, ExecutionConfig, NodeOutput},
state::State,
};
use adk_model::GeminiModel;
use serde_json::json;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let api_key = std::env::var("GOOGLE_API_KEY")?;
let model = Arc::new(GeminiModel::new(&api_key, "gemini-2.0-flash")?);
// Supervisor: Decides which worker should act next
let supervisor = LlmAgentBuilder::new("supervisor")
.description("Routes tasks to specialized workers")
.instruction(
"You are a task supervisor. Based on the task and work done so far, decide who should work next.\n\n Workers available:\n - researcher: Gathers information and facts\n - writer: Writes content based on research\n - coder: Creates code examples\n\n Respond with ONLY one word: 'researcher', 'writer', 'coder', or 'done'."
)
.model(model.clone())
.build()?;
// Workers with specialized roles
let researcher = LlmAgentBuilder::new("researcher")
.instruction("Research the topic. Provide key facts as bullet points.")
.model(model.clone())
.build()?;
let writer = LlmAgentBuilder::new("writer")
.instruction("Write engaging content based on the research provided.")
.model(model.clone())
.build()?;
let coder = LlmAgentBuilder::new("coder")
.instruction("Write clean, documented code examples for the topic.")
.model(model.clone())
.build()?;
// Create AgentNodes with input/output mappers
let supervisor_node = AgentNode::new(Arc::new(supervisor))
.with_input_mapper(|state| {
let task = state.get("task").and_then(|v| v.as_str()).unwrap_or("");
let history = state.get("history")
.and_then(|v| v.as_array())
.map(|arr| arr.iter()
.filter_map(|h| h.get("agent").and_then(|a| a.as_str()))
.map(|s| format!("- {} completed", s))
.collect::<Vec<_>>()
.join("\n"))
.unwrap_or_default();
adk_core::Content::new("user").with_text(format!(
"Task: {}\n\nWork completed:\n{}\n\nWho next?",
task,
if history.is_empty() { "None yet" } else { &history }
))
})
.with_output_mapper(|events| {
let mut updates = std::collections::HashMap::new();
for event in events {
if let Some(content) = event.content() {
let text: String = content.parts.iter()
.filter_map(|p| p.text())
.collect();
let next = if text.to_lowercase().contains("researcher") {
"researcher"
} else if text.to_lowercase().contains("writer") {
"writer"
} else if text.to_lowercase().contains("coder") {
"coder"
} else {
"done"
};
updates.insert("next_agent".to_string(), json!(next));
}
}
updates
});
// Build the graph
let graph = StateGraph::with_channels(&[
"task", "next_agent", "history",
"research_output", "written_content", "code_output"
])
.add_node(supervisor_node)
.add_node(AgentNode::new(Arc::new(researcher)))
.add_node(AgentNode::new(Arc::new(writer)))
.add_node(AgentNode::new(Arc::new(coder)))
// Finalize node compiles all outputs
.add_node_fn("finalize", |ctx| async move {
let research = ctx.get("research_output").and_then(|v| v.as_str());
let content = ctx.get("written_content").and_then(|v| v.as_str());
let code = ctx.get("code_output").and_then(|v| v.as_str());
let result = format!(
"=== FINAL OUTPUT ===\n\n{}\n\n{}\n\n{}",
research.unwrap_or("No research"),
content.unwrap_or("No content"),
code.unwrap_or("No code")
);
Ok(NodeOutput::new().with_update("final_result", json!(result)))
})
// Graph structure
.add_edge(START, "supervisor")
.add_conditional_edges(
"supervisor",
|state| state.get("next_agent")
.and_then(|v| v.as_str())
.unwrap_or("done")
.to_string(),
[
("researcher", "researcher"),
("writer", "writer"),
("coder", "coder"),
("done", "finalize"),
],
)
// Workers cycle back to supervisor
.add_edge("researcher", "supervisor")
.add_edge("writer", "supervisor")
.add_edge("coder", "supervisor")
.add_edge("finalize", END)
.compile()?
.with_recursion_limit(15); // Prevent infinite loops
// Execute
let mut input = State::new();
input.insert("task".to_string(), json!("Create a guide about Rust error handling"));
input.insert("history".to_string(), json!([]));
let result = graph.invoke(input, ExecutionConfig::new("content-thread")).await?;
println!("{}", result.get("final_result").and_then(|v| v.as_str()).unwrap_or(""));
Ok(())
}✅ 何时使用主管图
- 工作流顺序是动态的,并由LLM决定
- 工作人员可能需要迭代或循环返回
- 复杂状态需要在代理之间共享
- 您需要检查点或可恢复的工作流
- 任务分解需要多个顺序步骤
6. 模式比较
| 特性 | 协调器 | AgentTool | 主管图 |
|---|---|---|---|
| 用户与谁对话 | 直接与专家对话 | 仅与协调器对话 | 最终输出 |
| 多代理调用 | ❌ 一次一个 | ✅ 可并行 | ✅ 编排 |
| 响应处理 | ❌ | ✅ | ✅ |
| 循环工作流 | ❌ | ❌ | ✅ |
| 共享状态 | 仅限会话 | 仅限会话 | 完整图状态 |
| 设置复杂性 | 🟢 低 | 🟡 中 | 🔴 高 |
7. 结论
多智能体系统允许您通过结合专业智能体来构建复杂的 AI 应用程序。根据您的需求选择模式:
- 协调器: 设置快速,非常适合客户服务路由
- AgentTool: 当您需要处理或组合响应时
- 主管图: 复杂、动态、多步骤的工作流