Day 11 / 共 20 天 · 第 3 周 ADK 智能体开发
ADK:Agent 抽象
前两周学的是"编排引擎"(怎么把组件连成流程)。本周学 ADK——Eino 在引擎之上封装的"智能体开发套件"。今天讲最核心的 Agent 接口、事件流、Runner。
📍 你在整门课的位置 · 第 3 周 Agent 套件(共 4 周 · 20 天)
D11 ADK 抽象→
D12 ChatModelAgent→
D13 多智能体→
D14 中断恢复→
D15 ReAct 精读
L01
ADK 是什么,为什么要有它
ADK = Agent Development Kit(智能体开发套件),代码在 adk/ 目录。它站在编排引擎(compose)之上,提供"直接可用的 Agent 抽象",让你不必每次都手搭 Graph。
🤔 痛点:只有 compose 图引擎,搭一个"会自己调工具的 Agent"要写多少东西?
你得自己搭"模型节点 → 判断要不要调工具的分支 → 工具节点 → 结果拼回消息历史 → 回到模型节点"这套循环图,还要处理流式、中断、多轮记忆……每做一个 Agent 都重来一遍。ADK 把这套"通用骨架"封装好了:你只管给模型和工具,拿到的就是一个能跑的 Agent。
💡 本质:Agent = 输入一组消息 → 返回一串事件的异步迭代器
无论内部多复杂,ADK 把每个 Agent 都统一成同一个极简契约(
adk/interface.go:463 的 Run):吃 AgentInput,吐 AsyncIterator[*AgentEvent]。"一串事件流"这个统一出口,让不同 Agent 能互相嵌套、组合、路由——这是本周所有玩法的地基。分层再回顾(Day 01)
最底层 schema(消息/流)→ components(模型/工具等零件)→ compose(把零件连成流程的引擎,第2周)→ adk/flow(把流程封装成开箱即用的 Agent,本周)。ADK 就是"我给你个 Agent,你喂问题、收事件流",内部帮你搭好了那张 Graph。Day 02 你其实已经用过 ADK(NewChatModelAgent + Runner),本周揭开它内部。
L02
Agent 接口(真实代码)
所有智能体都实现同一个接口(adk/interface.go):
type Agent interface {
Name(ctx context.Context) string // 智能体名字
Description(ctx context.Context) string // 描述(多智能体路由时用)
Run(ctx context.Context, input *AgentInput,
opts ...AgentRunOption) *AsyncIterator[*AgentEvent] // ★ 核心
}
读法:核心就一个
Run 方法——吃一个 AgentInput(用户输入 + 上下文),返回一个 AsyncIterator[*AgentEvent](异步事件迭代器,你 for 循环一个个取事件)。Name/Description 供"多智能体"场景里互相识别、路由用(Day 13)。为什么返回"事件流迭代器"而不是"一个结果"? 因为 Agent 运行是个过程:它会思考、调工具、再思考……每一步都产出一个事件("我说了这句""我要调这个工具""工具返回了这个")。返回迭代器让你能实时看到 Agent 的每一步,而不是干等最终答案。这对做"打字机效果""显示 Agent 正在调工具"的 UI 至关重要。
Agent 统一接口:输入一组消息 → Run 驱动内部图 → 返回一串可逐个消费的事件(每一步都能实时看到)。
💬 小白连环问
👶 小白:Run 为什么不直接
👨🏫 老师:因为 Agent 干活是个过程,不是一锤子买卖。它可能要调三次工具才憋出答案,你干等十几秒屏幕一片空白,体验很差。
👶 小白:那我就想要最终那句话,迭代器不是多此一举?
👨🏫 老师:你可以只留最后一个事件、前面的丢掉——迭代器给了你"要不要看过程"的选择权;直接返回结果则永远剥夺了这个选择。
👶 小白:这跟我平时用的啥东西像?
👨🏫 老师:就像生活中的快递物流轨迹——你既能只看"已签收",也能一条条追"已揽件/运输中/派送中"。Agent 事件流就是它的"物流轨迹"。
return 最终答案,非要返回个迭代器这么绕?👨🏫 老师:因为 Agent 干活是个过程,不是一锤子买卖。它可能要调三次工具才憋出答案,你干等十几秒屏幕一片空白,体验很差。
👶 小白:那我就想要最终那句话,迭代器不是多此一举?
👨🏫 老师:你可以只留最后一个事件、前面的丢掉——迭代器给了你"要不要看过程"的选择权;直接返回结果则永远剥夺了这个选择。
👶 小白:这跟我平时用的啥东西像?
👨🏫 老师:就像生活中的快递物流轨迹——你既能只看"已签收",也能一条条追"已揽件/运输中/派送中"。Agent 事件流就是它的"物流轨迹"。
🔬 简化版 vs 真实版:教程里的接口做了什么简化?
上面为好懂写的是非泛型版。真实
adk/interface.go 里其实是泛型的:type Agent = TypedAgent[*schema.Message](interface.go:467),Run 返回的是 *AsyncIterator[*TypedAgentEvent[M]](interface.go:463)。多出来的泛型 [M] 解决的问题是:让同一套 Agent 骨架既能处理普通 *schema.Message,也能处理别的消息类型,而不用复制一遍代码。你读源码看到 Typed... 前缀别慌——把 [*schema.Message] 代进去,就是教程这个简化版。L03
AgentInput:喂给 Agent 的东西
type AgentInput struct {
Messages []Message // 对话消息(含用户这轮的问题)
EnableStreaming bool // 是否要流式输出
}
读法:输入就是"一组消息 + 要不要流式"。消息用的正是 Day 03 学的
schema.Message——看,底层抽象在这里复用。EnableStreaming=true 时 Agent 内部走 Stream 范式(Day 06),事件里带的是流。为什么输入是"一组消息"而非"一个字符串"?
因为 Agent 需要上下文。多轮对话时,你要把之前的历史一起给它(system 提示 + 过往 user/assistant 往来 + 这轮的新问题)。用消息数组天然表达。单轮场景就一条 user 消息。
L04
AgentEvent:Agent 吐出的每一步
Agent 运行过程中产出的每个事件(adk/interface.go):
type AgentEvent struct {
AgentName string
Output *AgentOutput // 这步的产出(一条消息,可能是流)
Action *AgentAction // 这步的动作(如"转交给另一个Agent""中断""退出")
Err error // 出错了
}
读法:一个事件三选一:要么有 Output(产出内容)、要么有 Action(要执行的控制动作)、要么有 Err(出错)。你 for 循环收事件时,判断它是哪种,分别处理。
AgentAction 是控制信号:常见的有
TransferToAgent(转交给别的 Agent,Day 13)、Interrupt(中断等待人工,Day 14)、Exit(结束)。Output 是"内容",Action 是"控制流"——事件流同时承载了这两种信息,这样一个迭代器就能表达 Agent 的完整行为。📝 举例:一次问天气,事件流长这样
你问"北京今天天气?",for 循环会依次收到:
event1(Output=模型消息,含"要调 get_weather 工具")→ event2(Output=工具返回"晴 25℃")→ event3(Output=模型最终答"北京今天晴,25℃")。全程你只用判断每个 event.Output != nil 就能把内容渲染到界面上——三步思考被摊平成三个事件,一个 for 循环全收了。类比:就像生活中的微信群消息记录
一串 AgentEvent 就像生活中的微信群消息记录:每条消息都标着"谁发的"(
AgentName)、内容是什么(Output)、或是一个@操作/撤回之类的动作(Action)。你从头往下刷,就能还原整场讨论怎么一步步得出结论——事件流让 Agent 的"思考过程"变得像刷聊天记录一样直观。L05
Runner:驱动 Agent 跑起来
你一般不直接调 agent.Run,而是用 Runner 包一层(adk/runner.go)——它管 Session、消息累积、回调等杂事:
runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: myAgent}) // runner.go:89
iter := runner.Query(ctx, "你好") // runner.go:108
for {
event, ok := iter.Next() // 一个个取事件
if !ok { break } // 取完了
// 处理 event.Output / event.Action / event.Err
}
读法:NewRunner 建运行器 → Query 发问题拿到事件迭代器 → for + Next() 消费。这正是 Day 02 你敲过的那五步的后半段。Runner 是"用户友好的入口",Agent 是"被驱动的核心"。
L06
Session 与消息累积
Runner 维护一个 Session(adk/session.go),记录本次会话累积的消息。每产出一条新消息,Session 把它追加进历史,供 Agent 下一步(或下一轮)使用。
为什么需要 Session?(就像生活中的会议记录本)
Session 就像生活中的会议记录本:第 1 步模型说"我要查天气",第 2 步工具返回"25℃",第 3 步模型要基于"25℃"回答。这些中间消息必须攒起来喂给下一步模型,否则模型就跟"没做记录的与会者"一样当场失忆。Session 把每句话记进本子、随时翻给模型看,多轮对话时还跨轮保留。一句话复述:Session = Agent 的对话记忆本;它跟 Day 09 的 State 不同——State 是图内共享变量,Session 是 Agent 层的对话历史。
L07
Agent 与编排引擎的关系
关键认知:一个 Agent 内部通常就是一张编译好的 Graph(第 2 周学的)。比如 ChatModelAgent(Day 12)内部搭了"模型→分支→工具→回头"的 Graph;它的 Run 方法就是驱动这张图执行,把图的执行过程翻译成 AgentEvent 事件流。
所以两周的知识在这里合流:编排引擎(Graph/Pregel/channel/循环)是"发动机",ADK 的 Agent 是"整车"——把发动机包成方向盘和油门(Run/Query/事件流)交到你手里。理解了 Graph 的循环(Day 08/09),就理解了 Agent 为什么能"反复思考调工具"。下一课 Day 12 就拆开 ChatModelAgent,看它内部那张 Graph 长什么样。
L08
今日小结 + 动手
🧠 今天你应该能回答
- ADK 在四层架构的哪一层?解决什么?
- Agent 接口的核心方法?为什么 Run 返回事件流迭代器?
- AgentEvent 的 Output/Action/Err 分别代表什么?
- Runner 和 Session 各管什么?
- Agent 和 Graph 的关系?
✋ 动手
sed -n '1,80p' adk/interface.go | head -60 # Agent/AgentInput/AgentEvent
sed -n '89,130p' adk/runner.go # NewRunner / Query
grep -n 'type Session\|func.*Session' adk/session.go | head
明天预告 · Day 12:拆开 ChatModelAgent——最常用的 Agent,内部那张"模型↔工具"Graph 的真实搭建代码(
adk/chatmodel.go),以及它怎么把图执行翻译成事件流。