第三章:多Agent协作 - 让Agent组团干活
本章你将学到
读完这一章,你会掌握:
- 为什么单个Agent不够用,需要多Agent协作
- ADK-Go 中两种核心的多Agent模式:AgentTool 和 SubAgents
- 如何用
agenttool把子Agent当工具调用 - 如何用
SubAgents实现Agent之间的对话转移 - 如何写自定义Agent(不依赖LLM的纯逻辑Agent)
- 工作流Agent:顺序执行、并行执行、循环执行
- Agent的
Description为什么至关重要
为什么需要多Agent
想象一下你开了一家公司。刚开始只有你一个人,什么都自己干——接客户电话、写代码、做设计、搞财务。公司小的时候还凑合,但业务一多就扛不住了。
Agent也是一样的道理。
一个Agent塞太多职责,会出现几个问题:
- Instruction太长太杂 - 你让一个Agent既会搜索、又会写诗、还会分析数据,它的系统指令写出来得有一本书那么厚,LLM看了都迷糊
- 工具冲突 - Gemini API 有个限制:某些工具类型不能混用。比如 Google Search 是内置工具,和自定义的函数工具放一起会报错
- 专注度下降 - 就像人一样,什么都会往往什么都不精。让一个Agent专注做一件事,效果远比"万能Agent"好
所以我们需要拆分——让不同的Agent各司其职,组团协作。
一个人干所有活 vs. 团队协作
┌──────────────┐ ┌─────────────┐
│ 万能Agent │ │ 总指挥Agent │
│ │ └──────┬──────┘
│ 搜索? │ ┌───┼───┐
│ 写诗? │ v v v
│ 翻译? │ ┌────┐┌────┐┌────┐
│ 数据分析? │ │搜索││写诗││翻译│
│ ... │ └────┘└────┘└────┘
│ (我太难了) │
└──────────────┘ 各自精通一个领域
好,理解了动机,我们来看 ADK-Go 提供的两种多Agent协作方式。
两种多Agent模式总览
先有个全局印象,后面再逐一详解:
| 模式 | 实现方式 | 核心思路 | 适用场景 |
|---|---|---|---|
| AgentTool | agenttool.New(子Agent, cfg) |
把子Agent包装成一个工具,根Agent像调用函数一样调用它 | 根Agent需要灵活决定调谁,子Agent之间相互独立 |
| SubAgents | llmagent.Config{SubAgents: [...]} |
子Agent列表直接挂在父Agent下,LLM通过 transfer_to_agent 转移对话控制权 |
Agent之间需要来回对话、转移控制权 |
这两种模式解决的问题不同,用法也不一样。我们一个一个来。
AgentTool模式:把子Agent当工具
核心思路
AgentTool 的思路很直接:子Agent就是一个工具。根Agent分析用户请求,判断应该调用哪个"工具"(其实是哪个子Agent),然后把任务委托过去,子Agent处理完返回结果,根Agent再做汇总。
整个流程是这样的:
用户: "今天北京天气怎么样?"
|
v
┌─────────────┐
│ Root Agent │ 分析问题 --> 这是搜索类问题
│ (总指挥) │ --> 调用 search_agent 工具
└──────┬──────┘
v
┌──────────────┐
│ Search Agent │ 用 Google Search 搜索天气
│ (搜索专家) │ --> 返回搜索结果
└──────┬──────┘
v
┌─────────────┐
│ Root Agent │ 拿到结果,整理后回复用户
│ (总指挥) │ --> "北京今天晴,25度..."
└─────────────┘
实战:搜索 + 诗歌双Agent
这个例子来自 ADK-Go 官方的 examples/tools/multipletools,我们加上中文注释来理解:
package main
import (
"context"
"log"
"os"
"strings"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/agenttool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/adk/tool/geminitool"
)
func main() {
ctx := context.Background()
// 创建Gemini模型
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// ----- 第一步:创建搜索专家Agent -----
// 这个Agent专门负责Google搜索,回答事实性问题
searchAgent, err := llmagent.New(llmagent.Config{
Name: "search_agent",
Model: model,
Description: "专门做Google搜索,回答事实性问题",
Instruction: "你是搜索专家,使用Google Search来回答用户的问题。",
Tools: []tool.Tool{
geminitool.GoogleSearch{}, // Google搜索内置工具
},
})
if err != nil {
log.Fatalf("创建搜索Agent失败: %v", err)
}
// ----- 第二步:创建诗歌专家Agent -----
// 先定义一个写诗的工具函数
type PoemInput struct {
LineCount int `json:"lineCount"` // 诗歌行数
}
type PoemOutput struct {
Poem string `json:"poem"` // 生成的诗歌
}
poemTool, err := functiontool.New(functiontool.Config{
Name: "poem",
Description: "根据指定行数生成诗歌",
}, func(ctx tool.Context, input PoemInput) (PoemOutput, error) {
// 这里简化了,实际可以接入更复杂的诗歌生成逻辑
lines := strings.Repeat("春风又绿江南岸,\n", input.LineCount)
return PoemOutput{Poem: lines}, nil
})
if err != nil {
log.Fatalf("创建诗歌工具失败: %v", err)
}
// 诗歌Agent用上面的工具
poemAgent, err := llmagent.New(llmagent.Config{
Name: "poem_agent",
Model: model,
Description: "专门写诗,可以生成指定行数的诗歌",
Instruction: "你是一位才华横溢的诗人,用poem工具来创作诗歌。",
Tools: []tool.Tool{poemTool},
})
if err != nil {
log.Fatalf("创建诗歌Agent失败: %v", err)
}
// ----- 第三步:创建总指挥Agent -----
// 关键在这里!用 agenttool.New 把子Agent包装成工具
rootAgent, err := llmagent.New(llmagent.Config{
Name: "root_agent",
Model: model,
Description: "可以搜索信息和生成诗歌的助手。",
Instruction: "你是一个智能助手。" +
"如果用户想搜索信息或询问事实性问题,交给search_agent处理。" +
"如果用户想写诗或需要诗歌创作,交给poem_agent处理。",
Tools: []tool.Tool{
agenttool.New(searchAgent, nil), // 搜索Agent变成了一个工具
agenttool.New(poemAgent, nil), // 诗歌Agent也变成了一个工具
},
})
if err != nil {
log.Fatalf("创建根Agent失败: %v", err)
}
// ----- 第四步:启动 -----
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(rootAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v\n\n%s", err, l.CommandLineSyntax())
}
}
agenttool.New 到底做了什么
agenttool.New(agent, cfg) 返回的是一个 tool.Tool 接口。它做了这几件事:
- Name - 直接用子Agent的名字作为工具名
- Description - 直接用子Agent的Description作为工具描述
- Declaration - 自动生成函数声明。如果子Agent没有自定义 InputSchema,默认参数就是一个
request字符串 - Run - 执行时会创建一个全新的 Runner 和 Session,在隔离环境中运行子Agent
// agenttool.New 的签名
func New(agent agent.Agent, cfg *Config) tool.Tool
// Config 可以控制是否跳过汇总
type Config struct {
// 如果为true,子Agent执行完后,根Agent不会再调用LLM来汇总结果
SkipSummarization bool
}
// 最简单的用法,cfg传nil就行
agenttool.New(myAgent, nil)
// 如果不想让根Agent汇总子Agent的结果(直接透传)
agenttool.New(myAgent, &agenttool.Config{
SkipSummarization: true,
})
有个重要的细节:AgentTool 模式下,每次调用子Agent都会创建新的Session。这意味着子Agent不会记住之前的对话。如果你需要子Agent保持对话记忆,应该考虑 SubAgents 模式。
SubAgents模式:Agent之间转移控制权
核心思路
SubAgents 模式的思路不一样:子Agent不是工具,而是同事。父Agent可以把对话控制权转移给子Agent,子Agent也可以把控制权转回去。
ADK-Go 内部会自动生成一个 transfer_to_agent 工具函数,LLM通过调用这个函数来决定把对话交给谁。
用户: "帮我搜一下北京天气,然后写首关于天气的诗"
|
v
┌─────────────┐
│ Root Agent │ 判断 --> 先搜索
│ (总指挥) │ 调用 transfer_to_agent("search_agent")
└──────┬──────┘
v
┌──────────────┐
│ Search Agent │ 搜索完毕,结果写入对话历史
│ (搜索专家) │ 调用 transfer_to_agent("root_agent") 回到总指挥
└──────┬──────┘
v
┌─────────────┐
│ Root Agent │ 拿到搜索结果 --> 再转给诗歌Agent
│ (总指挥) │ 调用 transfer_to_agent("poem_agent")
└──────┬──────┘
v
┌──────────────┐
│ Poem Agent │ 根据天气信息写诗
│ (诗歌专家) │ 写完后转回 root_agent
└──────┬──────┘
v
┌─────────────┐
│ Root Agent │ 汇总结果,回复用户
└─────────────┘
代码示例
package main
import (
"context"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/geminitool"
)
func main() {
ctx := context.Background()
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// 搜索Agent
searchAgent, err := llmagent.New(llmagent.Config{
Name: "search_agent",
Model: model,
Description: "专门做Google搜索,回答事实性问题",
Instruction: "你是搜索专家。用Google Search回答问题。" +
"回答完毕后,把控制权交还给父Agent。",
Tools: []tool.Tool{
geminitool.GoogleSearch{},
},
// 不禁止转移到父Agent(默认就是false,这里写出来方便理解)
DisallowTransferToParent: false,
})
if err != nil {
log.Fatalf("创建搜索Agent失败: %v", err)
}
// 写作Agent
writerAgent, err := llmagent.New(llmagent.Config{
Name: "writer_agent",
Model: model,
Description: "专门写文章和创意内容",
Instruction: "你是一位优秀的作家。根据给定的信息进行创作。" +
"创作完成后,把控制权交还给父Agent。",
DisallowTransferToParent: false,
})
if err != nil {
log.Fatalf("创建写作Agent失败: %v", err)
}
// 总指挥 - 注意这里用的是 SubAgents,不是 Tools
rootAgent, err := llmagent.New(llmagent.Config{
Name: "root_agent",
Model: model,
Description: "智能助手总指挥",
Instruction: "你是总指挥。根据用户需求:\n" +
"- 搜索问题,转给 search_agent\n" +
"- 写作需求,转给 writer_agent\n" +
"- 如果需要先搜索再写作,先转给search_agent,拿到结果后再转给writer_agent\n",
SubAgents: []agent.Agent{searchAgent, writerAgent}, // 关键!
})
if err != nil {
log.Fatalf("创建根Agent失败: %v", err)
}
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(rootAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v\n\n%s", err, l.CommandLineSyntax())
}
}
transfer_to_agent 的工作原理
当你给 LLMAgent 设置了 SubAgents 后,ADK-Go 内部会自动做几件事:
- 注入转移指令 - 在发给LLM的 system prompt 里追加一段话,告诉LLM有哪些Agent可以转移,每个Agent的名字和描述
- 注册 transfer_to_agent 工具 - 自动注册一个函数工具,LLM可以调用它来转移控制权
- Runner负责调度 - Runner 会根据 session 中最后一个事件的 author 来判断下一轮该让哪个Agent处理
ADK-Go 内部生成的提示词大概长这样(你不需要自己写):
You have a list of other agents to transfer to:
Agent name: search_agent
Agent description: 专门做Google搜索,回答事实性问题
Agent name: writer_agent
Agent description: 专门写文章和创意内容
If you are the best to answer the question according to your description,
you can answer it.
If another agent is better for answering the question according to its
description, call 'transfer_to_agent' function to transfer the question
to that agent.
看到了吧?Agent的Description直接决定了LLM会不会把任务转给它。这就是为什么Description非常重要,后面我们单独讲。
控制转移方向
SubAgents 模式默认允许三个方向的转移:
┌──────────┐
│ Parent │ <-- 子Agent可以转回父Agent
│ Agent │
└────┬─────┘
┌────┴─────┐
v v
┌──────────┐ ┌──────────┐
│ Child A │ <--> Child B│ <-- 兄弟Agent之间可以互转
└──────────┘ └──────────┘
但有时候你想限制转移方向,llmagent.Config 提供了两个开关:
llmagent.Config{
// 禁止转移到父Agent
// 设为true后,这个Agent就不会主动把控制权交回给父Agent
DisallowTransferToParent: true,
// 禁止转移到兄弟Agent
// 设为true后,这个Agent只能和自己的子Agent以及父Agent交互
DisallowTransferToPeers: true,
}
什么时候用这些开关?举几个例子:
- 客服系统 - 售后Agent处理到一半不应该突然转给销售Agent,设置
DisallowTransferToPeers: true - 单次任务Agent - 做完就直接返回结果,不需要转回父Agent,设置
DisallowTransferToParent: true - 入口Agent - 只负责分发,不需要自己处理问题,子Agent完成后也不用转回来
AgentTool vs SubAgents:怎么选
这两种模式的核心区别在于Session是否共享和控制权如何流转:
| 特性 | AgentTool | SubAgents |
|---|---|---|
| Session | 子Agent每次调用都创建新Session,隔离的 | 共享同一个Session,共享对话历史 |
| 控制权 | 根Agent始终掌控,子Agent只是被调用 | 控制权可以在Agent之间自由转移 |
| 对话记忆 | 子Agent不记得之前的对话 | 所有Agent共享对话历史 |
| 工具冲突 | 完美解决!每个子Agent独立使用自己的工具 | 同一个Agent树中仍然可能有工具冲突 |
| 灵活性 | 根Agent可以同时调用多个子Agent的结果来决策 | 一次只有一个Agent在处理 |
选择建议:
- 用 AgentTool:当你需要解决工具冲突问题,或者子Agent之间相互独立,不需要共享对话历史
- 用 SubAgents:当你需要Agent之间来回交流,共享对话上下文,或者需要动态决定由谁来接管对话
实际开发中,这两种模式可以混合使用。比如根Agent通过SubAgents转移控制权给某个子Agent,而这个子Agent自己又通过AgentTool调用更底层的"工具Agent"。
Agent的Description:被低估的关键字段
在多Agent系统里,Description 不是随便写写的注释,它是 LLM决策的直接依据。
好的Description vs 坏的Description
// 坏的Description - 太模糊了,LLM不知道什么时候该调用它
llmagent.Config{
Name: "agent_a",
Description: "一个有用的Agent", // LLM: "啥都没说啊..."
}
// 好的Description - 清晰描述能力和适用场景
llmagent.Config{
Name: "search_agent",
Description: "专门做Google搜索,回答关于天气、新闻、实时信息等事实性问题",
}
// 好的Description - 明确边界
llmagent.Config{
Name: "poem_agent",
Description: "专门创作诗歌和文学作品,支持中文古诗、现代诗、歌词等体裁",
}
Description的写作原则
- 一句话说清楚 - 不需要长篇大论,一两句话就够了
- 突出差异化 - 如果有多个Agent,Description要让LLM能区分它们
- 描述能力范围 - 说清楚这个Agent能做什么,不能做什么
- 避免重叠 - 两个Agent的Description如果太像,LLM就不知道该调谁
一个反面教材:
// 这两个Agent的Description太像了,LLM会困惑
searchAgent := llmagent.Config{
Description: "回答用户的问题",
}
writerAgent := llmagent.Config{
Description: "帮助用户解答疑问",
}
// LLM: "这俩有啥区别???"
记住:Description是给LLM看的,不是给人看的。虽然人也要能看懂,但首要目标是让LLM能正确决策。
自定义Agent:不用LLM的纯逻辑Agent
不是所有Agent都需要LLM。有时候你只需要一个固定逻辑的处理单元——比如数据格式转换、规则校验、或者调用某个外部API。
ADK-Go 通过 agent.New 让你创建完全自定义的Agent:
package main
import (
"fmt"
"iter"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/model"
"google.golang.org/adk/session"
)
func main() {
// 创建一个自定义Agent,不需要LLM
timeAgent, err := agent.New(agent.Config{
Name: "time_agent",
Description: "返回当前时间,不需要联网,速度快",
Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
// 返回一个迭代器,yield每一个事件
return func(yield func(*session.Event, error) bool) {
// 获取当前时间
now := time.Now().Format("2006-01-02 15:04:05")
msg := fmt.Sprintf("当前时间是:%s", now)
// 构造事件并yield出去
yield(&session.Event{
LLMResponse: model.LLMResponse{
Content: &genai.Content{
Parts: []*genai.Part{
{Text: msg},
},
},
},
}, nil)
}
},
})
if err != nil {
panic(err)
}
// 这个Agent可以被当作SubAgent或通过AgentTool使用
fmt.Printf("创建了自定义Agent: %s\n", timeAgent.Name())
}
理解 Run 函数的签名
自定义Agent的核心是 Run 函数,我们来拆解一下它的签名:
Run func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error]
这个签名用了 Go 1.23 的迭代器模式(iter.Seq2)。如果你不熟悉,简单理解就是:Run函数通过yield返回一个或多个事件。
// InvocationContext 提供了你需要的所有上下文信息
type InvocationContext interface {
context.Context // 标准Go context
Agent() Agent // 当前Agent
Session() session.Session // 当前Session,可以读写状态
UserContent() *genai.Content // 用户的输入内容
Artifacts() Artifacts // 文件/制品管理
Memory() Memory // 记忆管理
EndInvocation() // 结束整个调用
// ...更多方法
}
来看一个更实际的例子——一个数据校验Agent:
// 数据校验Agent:检查用户输入是否符合要求
validatorAgent, err := agent.New(agent.Config{
Name: "validator_agent",
Description: "校验用户输入的数据格式是否正确",
Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
// 获取用户输入
userContent := ctx.UserContent()
if userContent == nil || len(userContent.Parts) == 0 {
yield(nil, fmt.Errorf("没有收到用户输入"))
return
}
inputText := userContent.Parts[0].Text
var resultMsg string
// 简单的校验逻辑
if len(inputText) < 5 {
resultMsg = "输入太短了,至少需要5个字符"
} else if len(inputText) > 1000 {
resultMsg = "输入太长了,最多1000个字符"
} else {
resultMsg = fmt.Sprintf("校验通过! 输入长度: %d", len(inputText))
}
yield(&session.Event{
LLMResponse: model.LLMResponse{
Content: &genai.Content{
Parts: []*genai.Part{{Text: resultMsg}},
},
},
}, nil)
}
},
})
自定义Agent最大的优势是不消耗LLM Token,执行速度也快。适合做确定性逻辑。
工作流Agent:编排子Agent的执行顺序
到目前为止,我们聊的都是由LLM自主决策的Agent协作。但有时候你不想让LLM决定流程——你希望按照固定的流程执行一系列Agent。
ADK-Go 提供了三种工作流Agent:
1. SequentialAgent - 顺序执行
子Agent按照列表顺序,一个接一个执行。
import (
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/workflowagents/sequentialagent"
)
// 先收集数据,再分析,最后生成报告
seqAgent, err := sequentialagent.New(sequentialagent.Config{
AgentConfig: agent.Config{
Name: "report_pipeline",
Description: "按顺序执行:收集数据 -> 分析 -> 生成报告",
SubAgents: []agent.Agent{
dataCollectorAgent, // 第1步:收集数据
analyzerAgent, // 第2步:分析数据
reportWriterAgent, // 第3步:写报告
},
},
})
执行流程:
dataCollectorAgent --> analyzerAgent --> reportWriterAgent
第1步 第2步 第3步
2. ParallelAgent - 并行执行
子Agent同时并发执行,互不干扰。每个子Agent在自己的 branch 中运行,看不到其他子Agent的对话。
import (
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/workflowagents/parallelagent"
)
// 同时用三种方式分析同一个问题
parallelAnalyzer, err := parallelagent.New(parallelagent.Config{
AgentConfig: agent.Config{
Name: "multi_analyzer",
Description: "并行执行多种分析策略",
SubAgents: []agent.Agent{
sentimentAgent, // 情感分析
keywordAgent, // 关键词提取
summaryAgent, // 摘要生成
},
},
})
执行流程:
┌──> sentimentAgent (情感分析)
|
开始 ----├──> keywordAgent (关键词提取) ----> 全部完成
|
└──> summaryAgent (摘要生成)
并行Agent的一个实际用途:同时生成多个版本的回答,然后交给评审Agent选最好的。
3. LoopAgent - 循环执行
子Agent反复执行,直到达到最大迭代次数或者某个子Agent调用了 Escalate(升级/退出循环)。
import (
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/workflowagents/loopagent"
"google.golang.org/adk/tool/exitlooptool"
)
// 先创建退出循环的工具
exitTool, err := exitlooptool.New()
if err != nil {
log.Fatal(err)
}
// 代码审查Agent,会反复审查直到代码合格
reviewerAgent, err := llmagent.New(llmagent.Config{
Name: "code_reviewer",
Model: model,
Description: "审查代码质量",
Instruction: "检查代码是否有问题。如果代码质量合格,调用exit_loop退出。" +
"如果不合格,提出修改建议。",
Tools: []tool.Tool{exitTool},
})
// 代码修改Agent
fixerAgent, err := llmagent.New(llmagent.Config{
Name: "code_fixer",
Model: model,
Description: "根据审查意见修改代码",
Instruction: "根据code_reviewer的意见修改代码。",
})
// 循环:审查 -> 修改 -> 审查 -> ...
loopReviewer, err := loopagent.New(loopagent.Config{
AgentConfig: agent.Config{
Name: "review_loop",
Description: "反复审查和修改代码直到合格",
SubAgents: []agent.Agent{reviewerAgent, fixerAgent},
},
MaxIterations: 5, // 最多循环5次,防止无限循环
})
执行流程:
┌──────────────────────────────────────────┐
│ │
│ code_reviewer --> code_fixer --> code_reviewer --> ...
│ | |
│ └── 合格? 调用exit_loop退出 ─────────┘
│
└── 最多5次迭代后强制停止 ─────────────────────┘
LoopAgent 的 MaxIterations 参数很重要:
- 设为 0 表示无限循环(直到 Escalate)
- 设为 1 其实就等于 SequentialAgent
- 建议总是设一个合理的上限,防止意外的无限循环
架构图:多Agent协作全景
把前面讲的所有内容组合起来,一个复杂的多Agent系统可能长这样:
┌─────────────────┐
│ Root Agent │
│ (总指挥) │
│ LLMAgent │
└────────┬────────┘
|
┌──────────────┼──────────────┐
v v v
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Search Agent │ │ Writer Agent │ │ Review Loop │
│ (搜索专家) │ │ (写作专家) │ │ (审查循环) │
│ LLMAgent │ │ LLMAgent │ │ LoopAgent │
│ │ │ │ │ │
│ Tools: │ │ SubAgents: │ │ SubAgents: │
│ - Google │ │ 可以转移 │ │ - Reviewer │
│ Search │ │ 控制权 │ │ - Fixer │
└──────────────┘ └──────────────┘ └──────────────┘
|
┌──────┴──────┐
v v
┌──────────┐ ┌──────────┐
│ 翻译Agent │ │ 润色Agent │
│ LLMAgent │ │ 自定义 │
└──────────┘ └──────────┘
这个架构里混合使用了多种模式:
- Root Agent 通过 AgentTool 调用 Search Agent(解决工具冲突)
- Root Agent 通过 SubAgents 与 Writer Agent 转移对话
- Writer Agent 自己也有 SubAgents(翻译和润色),形成子树
- Review Loop 用 LoopAgent 实现迭代式审查
完整实战:智能客服系统
最后来个完整的例子,把前面学的都用上。我们做一个客服系统,有三个部门:
package main
import (
"context"
"fmt"
"iter"
"log"
"os"
"time"
"google.golang.org/genai"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/session"
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/agenttool"
"google.golang.org/adk/tool/functiontool"
)
func main() {
ctx := context.Background()
llm, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("创建模型失败: %v", err)
}
// ===== 自定义Agent:订单查询(不需要LLM,纯逻辑) =====
orderQueryAgent, err := agent.New(agent.Config{
Name: "order_query",
Description: "查询订单状态,根据订单号返回物流信息",
Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] {
return func(yield func(*session.Event, error) bool) {
// 模拟订单查询
result := fmt.Sprintf("订单状态:已发货\n物流单号:SF123456\n预计到达:%s",
time.Now().Add(48*time.Hour).Format("2006-01-02"))
yield(&session.Event{
LLMResponse: model.LLMResponse{
Content: &genai.Content{
Parts: []*genai.Part{{Text: result}},
},
},
}, nil)
}
},
})
if err != nil {
log.Fatal(err)
}
// ===== 售前咨询Agent =====
// 定义查价格的工具
type PriceInput struct {
ProductName string `json:"productName"` // 商品名称
}
type PriceOutput struct {
Price float64 `json:"price"` // 价格
Currency string `json:"currency"` // 货币
InStock bool `json:"inStock"` // 是否有货
}
priceTool, err := functiontool.New(functiontool.Config{
Name: "check_price",
Description: "查询商品价格和库存",
}, func(ctx tool.Context, input PriceInput) (PriceOutput, error) {
// 模拟查询价格
return PriceOutput{
Price: 299.99,
Currency: "CNY",
InStock: true,
}, nil
})
if err != nil {
log.Fatal(err)
}
preSalesAgent, err := llmagent.New(llmagent.Config{
Name: "pre_sales",
Model: llm,
Description: "售前咨询专家,回答商品信息、价格、推荐等问题",
Instruction: "你是售前咨询专家。帮助用户了解商品信息,查询价格," +
"提供购买建议。态度热情友好。",
Tools: []tool.Tool{priceTool},
// 售前Agent不应该跑到售后去
DisallowTransferToPeers: true,
})
if err != nil {
log.Fatal(err)
}
// ===== 售后服务Agent =====
afterSalesAgent, err := llmagent.New(llmagent.Config{
Name: "after_sales",
Model: llm,
Description: "售后服务专家,处理退换货、投诉、订单问题",
Instruction: "你是售后服务专家。帮助用户处理退换货、投诉和订单查询。" +
"态度耐心温和。可以使用order_query工具查询订单。",
Tools: []tool.Tool{
agenttool.New(orderQueryAgent, nil), // 订单查询作为工具
},
DisallowTransferToPeers: true,
})
if err != nil {
log.Fatal(err)
}
// ===== 总指挥Agent =====
rootAgent, err := llmagent.New(llmagent.Config{
Name: "root_agent",
Model: llm,
Description: "客服系统总调度",
Instruction: "你是客服系统的前台接待。根据用户的问题类型:\n" +
"- 商品咨询、价格查询、购买建议 --> 转给 pre_sales\n" +
"- 退换货、投诉、订单查询 --> 转给 after_sales\n" +
"- 简单的打招呼或闲聊,你自己回复就好\n" +
"判断不了的情况,先问清楚用户需要什么帮助。",
SubAgents: []agent.Agent{preSalesAgent, afterSalesAgent},
})
if err != nil {
log.Fatal(err)
}
// ===== 启动 =====
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(rootAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("运行失败: %v\n\n%s", err, l.CommandLineSyntax())
}
}
这个例子混合了三种Agent类型:
- root_agent - LLMAgent,通过 SubAgents 做调度
- pre_sales / after_sales - LLMAgent,各自有自己的工具
- order_query - 自定义Agent(不用LLM),通过 AgentTool 被售后Agent调用
小结
这一章我们学了不少东西,来回顾一下核心要点:
两种协作模式:
- AgentTool (agenttool.New) - 子Agent当工具用,Session隔离,解决工具冲突
- SubAgents (Config.SubAgents) - 共享Session,控制权可以在Agent间转移
三种工作流Agent: - SequentialAgent - 按顺序执行子Agent - ParallelAgent - 并行执行子Agent - LoopAgent - 循环执行直到退出条件
自定义Agent:
- 用 agent.New + 自定义 Run 函数,不需要LLM
- 适合确定性逻辑,不消耗Token
别忘了:
- Description 是LLM做委派决策的关键依据,要写好
- DisallowTransferToParent 和 DisallowTransferToPeers 控制转移方向
- 两种模式可以混合使用
动手练习
练习1:基础多Agent
创建一个多Agent系统,包含: - 一个翻译Agent(中英互译) - 一个摘要Agent(将长文本缩写为100字以内) - 一个总指挥Agent,根据用户需求调用对应的子Agent
用 AgentTool 模式实现。
练习2:SubAgents模式
把练习1改成 SubAgents 模式。对比两种模式的行为差异,特别是: - 子Agent能否看到之前的对话历史? - 连续问两个不同类型的问题,行为有什么不同?
练习3:自定义Agent + 工作流
创建一个"写作流水线": 1. 自定义Agent:生成写作大纲(纯逻辑,不用LLM) 2. LLMAgent:根据大纲写文章 3. LLMAgent:润色文章
用 SequentialAgent 把它们串起来。
练习4(挑战):循环审查
在练习3的基础上加一个审查环节: 1. 写完文章后,审查Agent检查质量 2. 如果不合格,修改Agent根据意见修改 3. 反复循环直到合格(或达到最大次数)
用 LoopAgent + exitlooptool 实现。
提示
- 运行方式和前几章一样:
go run . --console进入控制台对话模式 - 可以加
--web启动Web界面,更方便测试多轮对话 - 如果遇到工具冲突错误,检查是不是把 GoogleSearch 和 functiontool 放在同一个Agent的 Tools 里了
下一章我们将深入探讨 Session 和状态管理 -- Agent之间如何共享数据、如何在多轮对话中保持记忆。