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:463Run):吃 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 至关重要。
AgentInput 消息数组 + 是否流式 Run() 内部驱动一张 Graph AsyncIterator[*AgentEvent] event1 · 模型:我要查天气 event2 · 工具返回:25℃ event3 · 模型:今天 25℃
Agent 统一接口:输入一组消息 → Run 驱动内部图 → 返回一串可逐个消费的事件(每一步都能实时看到)。
💬 小白连环问 👶 小白:Run 为什么不直接 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 维护一个 Sessionadk/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),以及它怎么把图执行翻译成事件流。
← Day 10 Workflow Day 12 · ChatModelAgent →