Day 20 / 共 20 天 · 第 4 周 横切与生态(结业)
构建、测试与收官
最后一天:项目怎么组织、怎么跑测试、debug 技巧、从零搭完整 Agent 的清单、常见坑,以及把 20 天知识连成一张地图。
📍 你在整门课的位置 · 第 4 周 进阶与生态(共 4 周 · 20 天 · 结业)
D16 callbacks→
D17 流式深水→
D18 类型/泛型→
D19 eino-ext→
D20 构建收官 🏁
L01
仓库结构总览
eino/
├── schema/ # Day 03:Message、StreamReader、Document
├── components/ # Day 04:各组件接口(model/tool/prompt/retriever...)
├── compose/ # Day 06-10:编排引擎(chain/graph/workflow/runnable/channel)
├── adk/ # Day 11-14:智能体套件(agent/runner/session/chatmodel)
├── flow/ # Day 15:现成 Agent(agent/react)
├── callbacks/ # Day 16:回调系统
└── internal/ # 内部实现(generic/callbacks/...)
# 实现在另一个仓库:eino-ext/(Day 19)
读法:目录结构就是这 20 天的路线图。schema 在最底、compose 是引擎核心、adk/flow 在最上层。看不懂某段代码时,先定位它在哪一层,就知道该用哪天的知识去理解。
🤔 收官前先问自己:这 20 天到底学到了一条什么主线?
别把 20 天记成 20 个孤立知识点。它其实是一条从下往上的建造线:先定义"数据长什么样"(schema),再定义"零件长什么样"(components),再造"把零件拼起来跑的引擎"(compose),最后用引擎搭出"开箱即用的智能体"(adk/flow),外加三样让它上生产的横切能力(回调/类型/生态)。今天就把这条线在脑子里焊死。
🏗️ 本质:整个 Eino 就像生活中盖一栋楼
就像生活中盖楼:schema 是建材规格(砖多大、钢筋多粗)→ components 是预制构件(门、窗、楼梯的标准接口)→ compose 是施工队和图纸(把构件按图纸装起来)→ adk/flow 是精装样板间(拎包入住的成品)→ callbacks/类型/eino-ext 则是监控探头、验收规范、建材市场。你这 20 天是从"认识一块砖"一路学到"交付一栋精装楼"。
📝 例子:本地为什么能看到 ext/ 目录?(三仓合体)
eino / eino-ext / eino-examples 本是三个独立 GitHub 仓库(各自发版、互不干扰)。但官方脚本
scripts/dev_setup.sh 会把 eino-ext 克隆进本地 ext/、eino-examples 克隆进 examples/,再生成一个 go.work 把三者组成一个工作区——这样你在编辑器里就能跨仓跳转定义、自动补全(脚本注释里说覆盖约 83 个模块)。注意:这三个目录被登记进 .git/info/exclude,只在本地存在、永远不会被 eino 主仓提交。L02
跑测试
go test ./... # 跑所有测试
go test ./compose/... # 只跑编排引擎的测试
go test -run TestChain ./compose # 跑名字含 TestChain 的
go test -race ./compose/... # ★ 带竞态检测(流式/并发多,强烈建议)
go test -cover ./... # 看覆盖率
为什么强调 -race?
Eino 大量用 goroutine(流式 Pipe、并行节点、super-step 并发)。
-race 会在运行时检测"多个 goroutine 不加锁地读写同一块内存"这种竞态 bug——这类 bug 平时可能不复现,上线偶发崩溃最难查。写涉及流/并发的代码,养成 go test -race 的习惯。Eino 测试里读源码是学 API 的最佳途径——每个 *_test.go 都是可运行的用法示例。L03
怎么 mock 模型(不花钱测)
测试 Agent 逻辑时不想真调 OpenAI(慢、花钱、不稳定)。做法:自己实现一个假的 ChatModel——它满足 Day 04 接口,返回你预设的响应:
type fakeModel struct{ reply *schema.Message }
func (f *fakeModel) Generate(ctx, msgs []*schema.Message, _ ...) (*schema.Message, error) {
return f.reply, nil // 直接返回预设回答,不调任何 API
}
func (f *fakeModel) Stream(...) (*schema.StreamReader[*schema.Message], error) { ... }
func (f *fakeModel) WithTools(...) (model.ToolCallingChatModel, error) { return f, nil }
// 测试里把 fakeModel 塞进 Agent,验证编排逻辑对不对
读法:因为一切面向接口(Day 18),你能用"假模型"替换真模型来测编排逻辑,不碰网络。想测"模型要求调工具时 Agent 会不会正确执行工具"?让 fakeModel 返回一个带 tool_calls 的消息即可。这正是"依赖注入 + 面向接口"给测试带来的红利。
mock 模型就像生活中拍电影用替身/道具钞票
真调 OpenAI 又慢又花钱、返回还不稳定,不适合反复跑测试。就像生活中拍打斗戏用替身、拍撒钱镜头用道具钞票:观众(你的编排逻辑)看到的"演出流程"一模一样,但不用真让主演受伤、也不用真烧钱。fakeModel 就是那个"道具模型"——返回你预设的固定台词,让你专心验证 Agent 的编排是否正确,一分钱不花、毫秒级返回。
👶 小白:fakeModel 返回的答案都是我自己写死的,那这测试到底测了个啥?答案本来就是我给的,不等于自问自答吗?
👨🏫 老师:好问题——因为测试的不是模型答得准不准,而是你的编排接得对不对。接着"拍电影"的比方:请替身不是为了验证替身长得像主演,而是为了拍"他挨了一拳后怎么倒、镜头怎么切"这段调度。模型答得好不好是模型厂商的事,你要测的是你写的那部分。
👨🏫 举个具体的:让 fakeModel 固定返回一条带 tool_calls 的消息,然后你验证——Agent 有没有正确识别出"要调工具"、有没有真的执行了对应工具、工具结果有没有拼回消息历史再喂回模型(Day 15 的循环)。这些"接线"逻辑全是你写的、都可能出 bug,而它们和模型具体吐什么内容无关。用真模型反而糟:它每次答的还不一样,你都没法写断言。写死输出正是为了把"不确定的模型"钉成"确定的道具",好让你专心测确定的那部分。
L04
Debug 技巧
- 挂日志回调(Day 16):给每个节点的 OnStart/OnEnd 打印输入输出,看数据在图里怎么流、哪一步变形了。这是排查编排问题的第一手段。
- 看 Compile 报错(Day 09/18):类型对不上、节点连接非法,Compile 就报错,仔细读错误信息里的节点名和类型。
- 先用 Invoke 再上 Stream:调试逻辑时先用非流式 Invoke(拿完整结果好断点),逻辑对了再换 Stream。
- MaxSteps 报错:如果报
ErrExceedMaxSteps,多半是模型陷入循环——检查工具是否总是"没给模型想要的结果"导致它反复调。
最实用的一招:写个日志 handler 全局挂上(Day 16),把每个节点的 RunInfo + 输入输出 dump 出来。Agent"行为诡异"时,这份 trace 能让你一眼看出是模型没按预期输出、还是工具返回了脏数据、还是编排连错了。
L05
从零搭一个完整 Agent 的清单
- 选模型:从 eino-ext 拿一个 ChatModel(Day 19),配好 APIKey
- 写工具:用 InferTool 把业务函数变成 Tool(Day 04/19)
- 写系统提示词 Instruction(定 Agent 的角色和行为准则)
- 建 Agent:NewChatModelAgent 组装模型+工具+提示(Day 12)
- 需要多 Agent?用 Agent-as-Tool 组 Supervisor(Day 13)
- 有敏感操作?在工具里加 Interrupt,配持久化 CheckPointStore(Day 14)
- 挂可观测:日志/监控/追踪 callback(Day 16)
- 用 Runner.Query 驱动,for 循环消费事件流(Day 11)
- 写测试:用 fakeModel mock,go test -race(本日)
你现在有能力做这些了
这张清单里每一项,都对应你这 20 天学过的某一天。从"读懂源码"到"能自己搭生产级 Agent",你已经走完了。照着清单,你能独立交付一个带工具、带 RAG、带人工审批、带监控的真实 Agent 应用。
L06
常见坑速查
- 流读了两次 / 忘了 Copy(Day 17):StreamReader 只能消费一次,多方要读先 Copy,用完 defer Close。
- 一个非流节点冻结全链(Day 06/17):链路里插了个"只要完整值"的节点,流式优势就断在那——尽量让流贯通到最后。
- 类型对不上(Day 18):相邻节点上游 O ≠ 下游 I,Compile 报错,检查类型参数。
- 死循环 MaxSteps(Day 09):设合理上限,且保证工具能真正推进对话。
- State 并发(Day 09):跨节点共享状态一定用 ProcessState(内部加锁),别自己裸读写。
- 中断没持久化(Day 14):生产环境 CheckPoint 必须存 Redis/DB,存内存重启就丢。
L07
20 天知识地图
第1周 地基
- D1 全景四层
- D2 第一个Agent
- D3 schema消息/流
- D4 components接口
- D5 一次调用旅程
第2周 引擎
- D6 Runnable4范式
- D7 Chain
- D8 Graph
- D9 编译与执行
- D10 Workflow
第3周 ADK
- D11 Agent抽象
- D12 ChatModelAgent
- D13 多智能体
- D14 中断恢复
- D15 ReAct精读
第4周 横切生态
- D16 callbacks
- D17 流式深水
- D18 类型安全
- D19 eino-ext
- D20 构建收官
一条主线:schema(数据长什么样)→ components(零件接口)→ compose(把零件编排成流程的引擎)→ adk/flow(把流程封装成开箱即用的智能体)→ callbacks/类型/生态(让它生产可用)。从一条消息的结构,到一个能自主用工具、可中断、可观测的生产级 Agent,你把整条链路走通了。
四条主线由下往上建造(schema→components→compose→adk),横切三样罩在上面让它上生产。
单步走查:一句"北京今天天气如何?"是怎么穿过这四条主线的(跟踪一个具体输入走到尾):
| 步骤 | 此刻发生什么 | 关键数据 / 主线 |
|---|---|---|
| 1 | 用户问题被包成一条消息 | schema.Message{Role: user, Content: "北京今天天气如何?"} ·① schema |
| 2 | 塞进 ChatModel 接口,等实现去调模型 | 走 model.ToolCallingChatModel 接口 ·② components |
| 3 | 编排引擎把消息喂给模型节点,模型说"我要调 get_weather" | 输出带 ToolCalls 的 Message ·③ compose |
| 4 | Agent 循环识别到工具调用,执行 get_weather("北京") | ReAct 循环:想→调工具 ·④ adk |
| 5 | 工具结果拼回消息历史,再问一次模型 | 新增 Role: tool 消息 ·① schema + ④ adk |
| 6 | 模型给出最终答案,边生成边流式吐字给前端 | StreamReader 一段段 Recv ·① schema(流) + ③ compose |
🎵 记忆口诀:一句话记住 Eino 全貌
"数据 schema 打底,零件 components 定型,引擎 compose 编排,智能体 adk 成品;回调看得见、类型不出错、生态随便接。"
再压缩成七字诀:数据·零件·引擎·体,观测·类型·生态齐。背下这两句,Eino 的骨架就长在你脑子里了。
再压缩成七字诀:数据·零件·引擎·体,观测·类型·生态齐。背下这两句,Eino 的骨架就长在你脑子里了。
L08
🎓 结业 + 下一步
恭喜你完成 Eino 20 天源码学习!
你已经从零基础,走到了能读懂 Eino 核心源码、理解它每一个设计取舍、并能独立搭建生产级 Agent 的水平。这 20 天里反复出现的设计哲学——统一抽象、接口与实现分离、流式一等公民、类型安全 fail-closed、依赖注入、成本经济学——不只属于 Eino,它们是所有优秀框架的通用语言。
🚀 下一步建议
- 动手写:照 L05 清单,用 eino-ext 搭一个你自己的小 Agent(比如"查天气+发邮件"助手)。
- 读 examples:Eino 官方 examples 仓库有大量完整示例,现在你都能看懂了。
- 读 *_test.go:每个测试都是活的用法文档。
- 横向对比:接下来本系列还会讲 OpenHands、CrewAI、LangGraph 等——对比它们和 Eino 的设计异同,你会理解得更深。