Day 05 / 共 20 天 · 第 1 周收官

一次调用的完整旅程

把 Day 03(数据)+ Day 04(组件)串起来,看一次带 RAG + 工具的对话,数据怎么在各层流转,为第 2 周的“编排引擎”铺路。

📍 你在整门课的位置 · 第 1 周 建立心智(共 4 周 · 20 天)
D1 全景架构 D2 环境搭建 D3 schema 数据 D4 组件接口 D5 一次调用旅程
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 世界观:一次调用 = 物料在流水线上走一趟。
🧍 第一人称·请求之旅:现在你是那句"退货政策?" 你就是物料,跟着传送带走一趟:
我进厂,被包成一条 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) 输入vs = {docs: [{Content:"退货政策:7天无理由..."}], question: "退货政策?"}
Format 后输出[{Role:System, Content:"你是客服,依据资料回答:\n退货政策:7天无理由..."}, {Role:User, Content:"退货政策?"}]
这条 []*Message 正好是下一步 ChatModel.Stream 的输入——上一步的输出类型 = 下一步的输入类型,积木才能对接上。
一次请求:从上层 API 一路落到底层数据类型 你的调用 compose 编排 components 组件 schema 数据 Invoke("退货政策?") Runnable 按边逐节点执行 Retriever ChatTemplate ChatModel []*Document []*Message StreamReader
你一句 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 最精妙的设计,是读懂后面一切的钥匙。
← Day 04 components Day 06 · Runnable 与 4 种流式范式 →