Day 05 / 共 20 天 · 第 1 周收官
一次调用的完整旅程
把 Day 03(数据)+ Day 04(组件)串起来,看一次带 RAG + 工具的对话,数据怎么在各层流转,为第 2 周的“编排引擎”铺路。
L01
一个真实场景
设想一个"带知识库的客服 Agent",用户问:"你们的退货政策是什么?" 它需要:① 从知识库检索退货政策文档;② 把文档 + 问题拼成提示词;③ 调模型生成回答。这正好用上 Day 04 的多个组件。
今天不深入代码,重点是看清数据(Day 03 的 Message/Document/Stream)怎么在组件(Day 04)之间流动,从而理解"为什么需要一个编排引擎把它们连起来"——那就是第 2 周 compose 的主题。
L02
端到端旅程动画
下面每步依次高亮,颜色表示它属于哪层(🔵schema 数据 / 🟢component 组件 / 🟣compose 编排 / 🟠agent):
1
用户问题 → User Messageschema.Message{Role: User, Content: "退货政策?"}
数据2
Retriever 检索知识库Retrieve(query) → []*schema.Document(退货政策文档)
组件3
ChatTemplate 拼提示词Format({docs, question}) → []*Message(含 System+检索资料+User)
组件4
ChatModel 生成Stream(messages) → StreamReader[*Message](边生成边吐)
组件5
流式输出 → 拼成完整答案StreamReader 逐 chunk → ConcatMessages → 完整 Message
数据6
这三步"连起来"就是 compose 的活Chain: Retriever → ChatTemplate → ChatModel,compile 成一个 Runnable
编排看出来了吗——每一步的输出正好是下一步的输入:问题(Message) → 检索(Document) → 提示词(Message) → 模型(Stream)。手写的话你得自己把这些组件一个个调、把输出接到下一个的输入。compose 就是帮你“自动接线”的引擎(L05)。
💡 生活类比:一次调用 = 工厂流水线,compose = 传送带
这条链就像一条装配流水线:每个组件是一个工位(检索工位、拼提示词工位、生成工位),物料(数据)从上一工位流到下一工位,每站加工一次。compose 就是工位间的传送带 + 配电箱——它负责把物料准确送到下一站、给每站供电、并保证前一站的产物正好是下一站能接的形状。本教程 Day05 世界观:一次调用 = 物料在流水线上走一趟。
🧍 第一人称·请求之旅:现在你是那句"退货政策?"
你就是物料,跟着传送带走一趟:
① 我进厂,被包成一条
② 到检索工位(Retriever),工人拿我当线索去仓库翻资料,往我身上贴了几张
③ 到拼装工位(ChatTemplate),把资料和我的问题拼成一份完整"工单"
④ 到生成工位(ChatModel),大脑读完工单,开始边想边吐零件(
⑤ 质检打包:
① 我进厂,被包成一条
User Message。② 到检索工位(Retriever),工人拿我当线索去仓库翻资料,往我身上贴了几张
Document(退货政策条款)。③ 到拼装工位(ChatTemplate),把资料和我的问题拼成一份完整"工单"
[]*Message(System 放资料、User 放我)。④ 到生成工位(ChatModel),大脑读完工单,开始边想边吐零件(
StreamReader,一个字一个字出)。⑤ 质检打包:
ConcatMessages 把散装零件拼成一条完整的 Assistant Message——我作为"成品"出厂,就是用户看到的答案。L03
数据在各层怎么变
用 Day 03 学的数据类型,跟踪"退货政策?"这句话的变形:
"退货政策?" // 你的原始输入(字符串)
→ schema.Message{Role: User, Content: "退货政策?"} // 包成 User 消息
→ Retriever.Retrieve → []*schema.Document{ {Content:"退货政策:7天无理由...", Score:0.9} } // 检索到的文档
→ ChatTemplate.Format → []*schema.Message{ // 拼成给模型的消息列表
{Role: System, Content: "你是客服,依据资料回答:\n退货政策:7天无理由..."},
{Role: User, Content: "退货政策?"},
}
→ ChatModel.Stream → StreamReader[*Message] // 模型流式返回
→ ConcatMessages → schema.Message{Role: Assistant, Content: "我们支持 7 天无理由退货..."} // 拼成完整答案
📝 一步的输入→输出(第 3 步 ChatTemplate)
输入:
Format 后输出:
这条
vs = {docs: [{Content:"退货政策:7天无理由..."}], question: "退货政策?"}Format 后输出:
[{Role:System, Content:"你是客服,依据资料回答:\n退货政策:7天无理由..."}, {Role:User, Content:"退货政策?"}]这条
[]*Message 正好是下一步 ChatModel.Stream 的输入——上一步的输出类型 = 下一步的输入类型,积木才能对接上。你一句 Invoke → compose 按边依次驱动组件 → 每个组件吞吐的都是最底层 schema 数据类型。越往下越基础。
Message 和 Document 是“通用货币”(Day 03 讲的):正因为大家都用这几个类型,组件之间才能像积木一样拼接——Retriever 吐 Document、ChatTemplate 吃 Document 吐 Message、ChatModel 吃 Message 吐 Stream。统一的数据契约 = 可组合性的前提。
用一张单步走查表跟踪"退货政策?"这个具体输入,看每一步的类型和关键值怎么变:
| 步骤 | 此刻发生什么 | 数据类型 → 关键值 |
|---|---|---|
| 入口 | 用户原始输入 | string → "退货政策?" |
| 1 | 包成用户消息 | Message → {Role:User, Content:"退货政策?"} |
| 2 Retriever | 按问题检索知识库 | []*Document → [{Content:"7天无理由...", Score:0.9}] |
| 3 ChatTemplate | 资料+问题拼成消息列表 | []*Message → [System(含资料), User(问题)] |
| 4 ChatModel | 模型流式生成 | StreamReader[*Message] → 逐 chunk:"我们"→"支持"→"7 天"… |
| 5 Concat | 把流拼成完整答案 | Message → {Role:Assistant, Content:"我们支持 7 天无理由退货..."} |
⚠️ 小白常误以为:数据从头到尾就是一个字符串在传。其实:它在每一步都变身成不同类型(string→Message→Document→Message→Stream→Message),正因为相邻两步的"出参类型 = 入参类型",组件才拼得上。一句话记住:一次调用,就是数据在几种 schema 类型间接力变身。
L04
流式贯穿全程
注意第 4 步 ChatModel 用的是 Stream——它边生成边吐。如果整条链路每个节点都支持流式,那"退货政策的回答"能一个字一个字地实时显示给用户(打字机效果),而不是憋完整段再出。
但如果某个节点只支持非流呢? 比如你在模型后加了个"敏感词过滤"节点,它只实现了
Invoke(要完整输入)。这时 Eino 会自动把模型的流用 ConcatMessages 拼成完整消息再喂给过滤节点——流到这里就"凝固"了。这套“流↔非流自动转换”就是 Day 01 提的 Runnable 4 范式,明天 Day 06 精讲。今天你只要感受到:流式是 Eino 一等公民,且能和非流节点自动衔接。💡 生活类比(接着流水线世界观):非流节点 = 必须等一整批到齐才开工的打包机
流水线上大多工位能"来一个零件加工一个"(流式)。但"敏感词过滤"这类工位像打包机——必须等一整箱零件全到齐才能称重打包(要完整输入)。传送带(compose)会自动在它前面先把零件攒满一箱(
ConcatMessages 凝固成完整消息)再送进去,你完全不用手动干预。一句话记住:流水线上遇到"只收整批"的工位,传送带会自动帮你先攒齐。L05
为什么需要"编排"
🤔 痛点:积木有了,怎么把它们"连起来跑"?
Day 03/04 你已经有了数据类型(Message/Document)和组件积木(Retriever/ChatTemplate/ChatModel)。可积木本身不会自己动——谁把 Retriever 的输出接到 ChatTemplate 的输入?谁在模型只会流式、下个节点只要完整值时做转换?谁在需要循环/分支时管流程?全靠你手写胶水,一多就乱。
💡 本质:编排引擎 = 帮你"自动接线 + 管流程"的那层
compose 做的事,就是把"一堆组件 + 它们的连接关系"编译成一个能跑的整体:自动把上一个的输出喂给下一个、自动做流↔非流转换、支持循环分支、建图期就查类型。编排引擎就像家里的配电箱:你不用把每根电线手动拧到一起,插上就通电。
你可能会想:这三个组件我自己手写代码一个个调不就行了?确实能,但会很痛苦:
| 手写调用的麻烦 | compose 编排帮你解决 |
|---|---|
| 手动把每个组件的输出接到下一个的输入 | 自动连线(Chain 自动连边) |
| 手动处理流式/非流的转换 | Runnable 4 范式自动转换 |
| 要循环/分支时代码乱成一团 | Graph 的边和 Branch(支持循环并行) |
| 类型接错了运行时才崩 | 编译期/建图期类型检查 |
| 加日志/追踪要改每个组件 | callbacks 切面统一注入 |
编排引擎(compose)就是 Eino 的心脏:它把"一堆组件 + 它们的连接关系"变成一个可执行、类型安全、原生流式、可观测的整体。第 2 周(Day 06-10)我们就钻进它——先学 Runnable(统一执行抽象),再学 Chain(自动连线的直线)、Graph(可循环的图)、Workflow(字段级映射)。学透了 compose,第 3 周的 Agent 就是水到渠成——因为 Agent 本质就是一张 compose 图(Day 01 讲过)。
L06
第 1 周收官
Day 01 项目全景:四大支柱、schema→components→compose→adk 四层
Day 02 环境 & 第一个 Agent:五步搭 ChatModelAgent、react 直觉
Day 03 schema:Message(一体化)、StreamReader(流式精髓)、Concat(拼接)
Day 04 components:ChatModel/Tool/ChatTemplate/RAG 三件套接口
你现在有了 Eino 的地基:知道数据长什么样、有哪些组件积木、它们怎么串成一次调用。接下来三周往上盖:
- 第 2 周(compose 编排引擎):把积木连起来跑——Runnable / Chain / Graph / Workflow。框架心脏。
- 第 3 周(ADK):搭会自己干活的 Agent——ChatModelAgent / 多智能体 / 中断恢复 / ReAct 精读。
- 第 4 周(进阶):callbacks / 流式内部 / 类型安全 / 生态收官。
L07
今日小结 + 动手
🧠 第 1 周自测
- 一次带 RAG + 工具的对话,数据怎么在组件间流转?(Message→Document→Message→Stream)
- 为什么 Message/Document 叫"通用货币"?
- 流式怎么贯穿全程?遇到非流节点怎么办?(ConcatMessages 凝固)
- 为什么需要 compose 编排?它替你解决了哪 5 类麻烦?
- 为什么说"学透 compose,Agent 就水到渠成"?
✋ 动手
# 回顾第 1 周的核心文件,为第 2 周做准备
head -40 schema/message.go # 数据
sed -n '36,50p' components/model/interface.go # 组件
grep -n "func NewChain\|func NewGraph\|func NewWorkflow" compose/*.go | head # 明天的主角
# 明天开始读 compose/runnable.go —— 先瞄一眼那 4 个方法
sed -n '32,40p' compose/runnable.go
第 2 周预告 · Day 06:进入框架心脏。Day 06 讲 Runnable 与 4 种流式范式(Invoke/Stream/Collect/Transform)——理解"组件只实现一个、框架自动补齐四个"这个 Eino 最精妙的设计,是读懂后面一切的钥匙。