Day 02 / 共 20 天 · 第 1 周 建立心智
环境搭建 & 第一个 Agent
今天动手:装 Go、拉依赖,用最小代码跑通一个 ChatModelAgent,感受"给个模型 + 工具 = 会自己干活的 Agent"。
📍 你在整门课的位置 · 第 1 周 建立心智(共 4 周 · 20 天)
D1 全景架构→
D2 环境搭建→
D3 schema 数据→
D4 组件接口→
D5 一次调用旅程
L01
今天的目标
Day 01 建立了地图。今天把环境搭起来,跑通"第一个 Agent",并建立三个直觉:Eino 的 Agent 是"吐事件流"的、react 循环是它的心跳、工具能用一个普通 Go 函数自动生成。
没有环境也能学
今天所有代码我都会讲清"每行在干嘛",你光看也能理解。真机操作是加分项。
L02
装 Go 与拉依赖
Eino 需要 Go 1.18+(用了泛型)。三步:
# 1. 装 Go(若没有):https://go.dev/dl/ 或 brew install go
go version # 确认 >= 1.18
# 2. 新建你的项目,拉 eino 核心 + eino-ext(实现)
mkdir my-eino-app && cd my-eino-app && go mod init my-eino-app
go get github.com/cloudwego/eino
go get github.com/cloudwego/eino-ext/components/model/openai # 某个模型实现
# 3. 配 API key(走 OpenAI/兼容接口)
export OPENAI_API_KEY=sk-...
go get 是什么? Go 的包管理命令,把某个库下载并记进
go.mod(依赖清单)。为什么要拉两个仓? 回忆 Day 01——eino(核心)只有接口,真正能调 OpenAI 的实现在 eino-ext。所以要拉 eino + 你需要的那个 eino-ext/components/model/xxx。L03
三仓关系(再强调一次)
| 仓库 | 装什么 | 你会 import 什么 |
|---|---|---|
eino(本教程主角) | 接口 + 编排引擎 + Agent 套件 | compose、adk、schema、components/*(接口) |
eino-ext | 组件实现 + 回调 handler + 调试工具 | eino-ext/components/model/openai 等 |
eino-examples | 示例代码 | (参考用,不 import) |
本仓
examples/ 是空的——示例都在独立的 eino-examples 仓。所以你在本仓找不到"跑起来的例子"很正常。本教程会把关键用法直接写给你看。L04
最小 Agent 五步(真实 API)
🤔 痛点:不用 Eino,自己拿 Go 拼一个"会查天气的助手"要写多少胶水?
你得:手写 HTTP 拼 OpenAI 的 JSON、把"工具定义"手动塞进请求、解析模型返回里的
tool_calls、自己 json.Unmarshal 出参数、调你的天气函数、把结果拼成一条 tool 消息、再把整个消息列表塞回去问第二遍……而且模型可能要连调好几轮工具,这套"解析→执行→回填→再问"的循环要你自己写。换个模型还得重来。💡 本质:Eino 把"调模型 + 解析工具调用 + 执行 + 回填 + 循环"打包成一个 Agent
你只提供两样东西——一个 ChatModel(大脑)+ 一组 Tool(手脚),Eino 的 ChatModelAgent 就替你把那套 ReAct 循环跑起来。你的代码从"写一大坨请求/解析/循环"缩成"造 Agent → Query → 收事件"。下面这五步就是全部。
用 ADK 搭一个最小 Agent,就五步(对照 adk/runner.go + adk/chatmodel.go 的真实 API):
// 1. 造一个 ChatModel(实现来自 eino-ext)
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{Model: "gpt-4o", APIKey: key})
// 2. 造一个 ChatModelAgent(adk/chatmodel.go:484 NewChatModelAgent)
agent, _ := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "assistant",
Description: "一个有用的助手",
Instruction: "You are a helpful assistant.",
Model: cm,
ToolsConfig: adk.ToolsConfig{ /* Tools: []tool.BaseTool{天气工具...} */ },
MaxIterations: 20, // react 循环最多转 20 圈(默认 20,react.go:345)
})
// 3. 造一个 Runner(adk/runner.go:89 NewRunner)
runner := adk.NewRunner(ctx, adk.RunnerConfig{Agent: agent, EnableStreaming: true})
// 4. 跑!Query 是便捷入口(runner.go:108),返回一个"事件迭代器"
iter := runner.Query(ctx, "北京今天天气怎么样?")
// 5. 消费事件流(关键:Agent 是"吐事件流"的,不是"返回一个值")
for {
event, ok := iter.Next()
if !ok { break } // 迭代结束
if event.Err != nil { /* 处理错误 */ }
if event.Output != nil && event.Output.MessageOutput != nil {
msg, _ := event.Output.MessageOutput.GetMessage() // 流式会自动拼成完整消息
fmt.Println(msg.Content)
}
}
读法:造模型 → 用模型+工具造 Agent → 用 Agent 造 Runner → Query 开跑 → for 循环消费事件。注意第 5 步不是"拿一个返回值",而是
for iter.Next() 一个个接事件——这是 Eino Agent 的核心用法。📝 最小例子:输入 → 输出
输入(你写的一行):
Agent 内部自动发生:模型判断"要查天气" → 自动调
输出(你
你没写任何"解析 tool_calls / 回填 / 再问一遍"的代码——那一整圈都被 Agent 包了。
iter := runner.Query(ctx, "北京今天天气怎么样?")Agent 内部自动发生:模型判断"要查天气" → 自动调
get_weather("北京") → 拿到 "晴,25℃" → 把结果回填再问模型 → 模型给出人话。输出(你
for 循环里收到的最后一条消息):msg.Content == "北京今天晴,气温 25℃,适合出行。"你没写任何"解析 tool_calls / 回填 / 再问一遍"的代码——那一整圈都被 Agent 包了。
装依赖 → 建大脑(ChatModel)→ 组装成 Agent → Query 开跑收事件。②的实现来自 eino-ext,③④的构造器在本仓 adk/。
L05
关键直觉:Agent 是"吐事件流"的
看 adk/interface.go:453 的 Agent 接口定义——Run 不返回结果,而是返回一个异步事件迭代器:
type TypedAgent[M MessageType] interface {
Name(ctx) string
Description(ctx) string
Run(ctx, input *TypedAgentInput[M], opts ...AgentRunOption) *AsyncIterator[*TypedAgentEvent[M]]
}
type Agent = TypedAgent[*schema.Message] // interface.go:467 最常用的别名
为什么 Agent 只有 3 个方法、还返回迭代器? ①
Name/Description/Run 三件——Description 尤其重要:当这个 Agent 被当作工具或子 Agent 时,Description 就是给"上级模型"看的"你能干嘛"的说明(Day 13 讲)。② Run 返回 AsyncIterator[AgentEvent]——因为 Agent 干活是流式、多步的:它会一边想一边调工具,每一步产出一个"事件"(模型说了话/调了工具/要中断/完成了),你用 for iter.Next() 实时消费。这跟 claude-code 教程里"agent 循环边跑边 yield 消息"是同一种流式思想。事件结构 AgentEvent(interface.go:419)含:AgentName、Output(消息,可能是流)、Action(动作:Exit/Transfer/Interrupt/BreakLoop)、Err。Action 就是 Agent 用来“控制流程”的信号(Day 13/14 会用到)。
💡 生活类比:Agent 吐事件流,就像实习生边干边汇报
你交给实习生一个任务,他不会闷头几小时后突然甩给你结果,而是一边干一边冒泡:「我先查下资料」(一个事件)→「查到了,正在整理」(一个事件)→「初稿好了您看看」(一个事件)。
Run 返回的迭代器就是这条"汇报流",你 for iter.Next() 一条条接。本教程 Day02 的世界观:Agent = 一个会自己想、自己动手、边干边汇报的实习生。🧍 第一人称·请求之旅:现在你是一次 Query
跟着走一遍,你就是那句「北京今天天气怎么样?」:
① 我被
② 我被送到大脑 ChatModel,它看了看我,说「这得查工具」,于是产出一条带
③ 工具节点执行
④ 我(现在带着天气资料)又回到 ChatModel,它这次不调工具了,直接给出人话「北京今天晴,25℃」——最后一个事件。
⑤ 迭代器
① 我被
runner.Query 接住,包成一条 User Message 塞进消息列表。② 我被送到大脑 ChatModel,它看了看我,说「这得查工具」,于是产出一条带
ToolCalls 的 Assistant Message——这是我人生第一个事件,被 iter.Next() 收走。③ 工具节点执行
get_weather("北京"),拿到「晴 25℃」,包成 Tool Message 追加进列表。④ 我(现在带着天气资料)又回到 ChatModel,它这次不调工具了,直接给出人话「北京今天晴,25℃」——最后一个事件。
⑤ 迭代器
ok=false,我的旅程结束。你看到的就是我一路攒下来的最后那条完整消息。L06
react 循环:Agent 的心跳(直觉)
你上面那个 Agent,收到"北京天气"后内部转的圈就是 ReAct 循环(Day 12/15 精读真实代码,今天先建立直觉):
你的问题→
ChatModel模型思考→
要调工具?查天气→
执行工具拿到天气→
结果喂回模型
↑ 模型还要调工具 → 回到"ChatModel"再来一轮;不调了 → 输出最终答案,结束
ReAct = Reason + Act(推理 + 行动):模型先"想"(要不要用工具、用哪个),然后"做"(调工具),看到结果再想下一步,循环往复直到能给最终答案。Eino 的 ChatModelAgent 内部就是用一张 compose 图搭出这个循环的(Day 12 会看到真实代码:模型节点 + 工具节点 + 一条"回到模型"的边)。
MaxIterations(默认 20)是循环上限,防止无限转圈。💡 生活类比:ReAct 循环 = 实习生查资料完成任务
实习生接到「查北京天气写一句话」:先想「我不知道,得查」(Reason)→ 动手打电话问天气台(Act)→ 看结果「哦,晴 25℃」→ 再想「够了,可以答了」→ 交活。要是资料不够,他会再打一个电话(再转一圈)。
MaxIterations=20 就像规定「最多打 20 个电话,别没完没了」。用一张走查表跟踪「北京天气」这个输入在循环里每一圈发生了什么、消息列表长成什么样:
| 圈 | 此刻发生什么 | 消息列表末尾新增 |
|---|---|---|
| 入口 | 用户提问进来 | User:"北京天气?" |
| 第 1 圈 | 模型思考 → 决定调工具 | Assistant{ToolCalls:[get_weather("北京")]} |
| (工具) | 执行 get_weather,拿到结果 | Tool:"晴 25℃" |
| 第 2 圈 | 模型看到工具结果 → 这次不调了 | Assistant:"北京今天晴,25℃" |
| 结束 | 无 ToolCalls,循环退出 | (输出最终答案) |
⚠️ 小白常误以为:Agent 调一次工具就结束了。其实:只要模型回复里还带
ToolCalls,就会再转一圈——它可能连着查天气、查航班、查酒店好几轮,直到某一圈模型不再要求调工具,才输出最终答案。上表里"第 2 圈"模型没再要工具,循环才停。🧠 一句话记住
「想 → 调 → 看 → 再想,不调了就收工」——这就是 ReAct,也是后面所有 Agent 的心跳。
L07
定义一个工具:一个 Go 函数就够
那个"查天气"的工具怎么来?Eino 提供了从普通 Go 函数自动生成工具的能力(components/tool/utils 的 InferTool):
type WeatherReq struct {
City string `json:"city" jsonschema:"description=城市名"` // struct tag 描述参数
}
// 一个普通函数 → 自动变成一个 Tool
weatherTool, _ := utils.InferTool(
"get_weather", // 工具名
"查询某城市的天气", // 描述(给模型看,决定何时调用)
func(ctx context.Context, req WeatherReq) (string, error) {
return "北京今天晴,25℃", nil // 你的真实逻辑
},
)
InferTool 帮你干了什么? 它读你函数的参数类型(
WeatherReq)和 struct tag,自动生成给模型看的参数 JSON Schema(Day 03 讲的 ToolInfo),还自动处理"模型传来的 JSON 字符串 → 你的 struct"和"你的返回值 → 回给模型的字符串"的编解码。你只写业务逻辑,工具的 schema 和编解码全自动。这跟 claude-code 教程 Day 07 讲的"zod schema 一处声明两处受益"异曲同工——都是"从类型自动生成工具描述"。把这个 weatherTool 放进 L04 第 2 步的 ToolsConfig.Tools 里,Agent 就会在需要时自己调它。
💡 生活类比:给工具 = 给实习生配一部"专线电话"
延续实习生世界观:你的实习生(模型)本身不知道实时天气,但你在他桌上放了一部标着「查天气热线」的电话(工具)。
InferTool 的 name + description 就是电话上的标签——实习生正是靠这个标签判断「这个任务该拿起哪部电话」。标签写得越清楚(description 越准),他越不会打错电话。一句话记住:工具好不好用,一半看描述写得清不清楚。L08
今日小结 + 动手
🧠 今天你应该能回答
- 为什么要拉 eino + eino-ext 两个仓?
- 造一个 Agent 的五步(模型→Agent→Runner→Query→消费事件)?
- Agent 的 Run 为什么返回迭代器而非返回值?(流式多步,边跑边吐事件)
- ReAct 循环是什么?MaxIterations 防什么?
- 怎么用一个 Go 函数定义工具?(InferTool 自动生成 schema + 编解码)
✋ 动手
# 1. 看 Agent 接口定义(L05)
sed -n '453,467p' adk/interface.go
# 2. 看 ChatModelAgent 构造与 Runner
grep -n "func NewChatModelAgent\|func NewRunner\|func.*Query" adk/chatmodel.go adk/runner.go | head
# 3. 看事件结构
sed -n '419,438p' adk/interface.go
# 4. 看工具接口(明天/Day04 细读)
sed -n '1,60p' components/tool/interface.go | head -40
明天预告 · Day 03:Agent 跑起来了,我们往下钻——最底层的数据类型
schema。重点是 Message(一个结构体怎么同时当输入/输出/模板)和 StreamReader(Eino 最精妙的流式抽象)。