Day 07 / 共 20 天 · 第 2 周 编排引擎
Chain 链式编排
最简单的编排:组件一个接一个串成流水线。今天讲 Chain 的用法和"自动连边"的原理,并厘清 Chain/Graph/Workflow 三者关系。
📍 你在整门课的位置 · 第 2 周 编排引擎(共 4 周 · 20 天)
D6 流式范式→
D7 Chain 链→
D8 Graph 图→
D9 编译执行→
D10 Workflow
L01
三种编排,一张图看懂
compose 提供三种编排 API,它们其实是同一个内部结构 graph 的三层封装(chain.go:72、workflow.go:45 都持有内部 graph)。区别只在"你怎么描述节点的连接关系":
Chain 链
一条直线 A→B→C,自动连边,数据顺次流过。最简单。
Graph 图
节点 + 你手动连的边 + 分支,可循环。最灵活(Agent 用它)。
Workflow 工作流
按字段连线(A 的 output.x → B 的 input.y),DAG 不可循环。最精细。
一句话记住:Chain = 自动连线的直线,Graph = 手动连线、可循环的图,Workflow = 按字段连线的 DAG。三者最终都变成内部
graph → 编译成 runner → 包成 Day 06 的 Runnable。今天先学最简单的 Chain。L02
Chain 是什么:一条流水线
🤔 痛点:直接用 Graph 手动连边,简单流程也太啰嗦
一个"拼提示词 → 调模型 → 解析"的直线流程,用 Graph 你得:AddNode 三次、AddEdge 从 START 连到第一个、每两个之间连一条、最后连到 END——五六行连边代码,还容易漏连、连错。可它明明就是一条直线啊。
💡 本质:Chain 是 Graph 的"语法糖",一条直线自动连边
Chain 内部就持有一个
*Graph(chain.go:37 的 NewChain 底层 new 了个 graph)。你每次 AppendXxx,它就自动把"上一个节点"连到"当前节点",编译时再自动补上到 START/END 的边。你只管按顺序 Append 组件,连边这件烦事框架全包。🏭 用「工厂流水线」来理解本讲(后面统一用这个类比)
Chain 就像生活中的工厂流水线:工件从传送带一头进去,依次经过打磨、喷漆、包装工位,另一头出成品——固定顺序、单向、不回头。
自动连边就像传送带:你只管把工位(组件)按顺序摆好,传送带自动把上一工位的半成品送到下一工位,你不用手动搬。
AppendParallel(局部并行)就像并排三个同款工位:一批工件同时分给三个工位加工,做完在下游合流,比一个个排队快。
自动连边就像传送带:你只管把工位(组件)按顺序摆好,传送带自动把上一工位的半成品送到下一工位,你不用手动搬。
AppendParallel(局部并行)就像并排三个同款工位:一批工件同时分给三个工位加工,做完在下游合流,比一个个排队快。
心智模型就是流水线:A → B → C,前一个的输出就是后一个的输入。
START→
ChatTemplate→
ChatModel→
ToolsNode→
END
Chain:组件一个接一个串成直线,边全自动(chain.go:37 NewChain / chain.go:198 AppendChatTemplate)
什么时候用 Chain?
固定顺序、没有循环的流程。典型:"拼提示词 → 调模型 → 解析结果"。如果你需要循环(比如 Agent 那种"模型↔工具反复")或复杂分支汇聚,就得用 Graph(Day 08)。先学 Chain 建立"节点+边+编译成 Runnable"的直觉,再升级到 Graph 就顺了。
L03
一个真实例子(源码注释里的用法)
chain.go:52 注释里的真实用法——builder 链式调用:
chain := compose.NewChain[inputType, outputType]() // 输入/输出类型
chain.AppendChatTemplate(tpl). // 加提示词模板节点
AppendChatModel(model). // 加模型节点
AppendToolsNode(tools) // 加工具节点
r, err := chain.Compile(ctx) // 编译成 Runnable(Day 06)
out, err := r.Invoke(ctx, input) // 跑!
读法:
NewChain 开一条链 → AppendXxx 一个个加组件(返回 chain 本身,所以能 . 链式接着调)→ Compile 编译 → 拿到 Runnable 就能 Invoke/Stream。注意你从来不用手写"把 template 的输出接到 model 的输入"——Append 时自动连了。📝 最小例子:一句输入怎么流过整条链
① ChatTemplate 把变量填进模板 →
② 这串消息作为输入喂给 ChatModel → 模型返回
③ 到 END,
你没写任何"把①的输出接到②的输入"的代码——Append 时已自动连好。
r.Invoke(ctx, {city:"北京"}) 进来后:① ChatTemplate 把变量填进模板 →
[{role:user, content:"北京今天天气如何?"}];② 这串消息作为输入喂给 ChatModel → 模型返回
{role:assistant, content:"北京今天晴..."};③ 到 END,
Invoke 返回这条 assistant 消息。你没写任何"把①的输出接到②的输入"的代码——Append 时已自动连好。
builder 模式:每个
AppendXxx 返回 chain 自己,所以能一路 .AppendA().AppendB().AppendC() 串着写,很流畅。这在 Go 里叫"流式接口/链式调用"。L04
自动连边的原理(真实代码)
为什么不用手动连边?看 chain.go:560 的 addNode——每次 Append 都自动把"上一批节点"连到"当前节点":
🅰️ 如果没有 Chain,让你用 Graph 手写会是这样
g.AddNode("tpl", tpl); g.AddNode("model", m); g.AddNode("tools", t)
g.AddEdge(START, "tpl") // 别忘了连入口
g.AddEdge("tpl", "model") // 手动连
g.AddEdge("model", "tools") // 手动连
g.AddEdge("tools", END) // 别忘了连出口
四行 AddEdge,漏一条、连错一条都要 debug。Chain 把这套"记住上一个、连到当前、补 START/END"全自动化了——真实源码就是下面这段 addNode:// 简化自 chain.go:560-600
func (c *Chain) addNode(node ...) {
key := 生成节点 key
c.gg.AddNode(key, node) // 底层往内部 Graph 加节点
for _, preKey := range c.preNodeKeys {
c.gg.AddEdge(preKey, key) // ★ 自动把上一批节点连到当前节点
}
c.preNodeKeys = []string{key} // 记住"当前节点",供下一次 Append 连
}
读法:每加一个节点,就自动从"上一个节点"连一条边过来,并把"当前节点"记进
preNodeKeys 供下次用。所以你 AppendA().AppendB(),框架自动连了 A→B。为什么 preNodeKeys 是复数(一批)? 因为 Chain 支持局部并行(L06)——
AppendParallel 会让"上一批"是多个并行节点,下一个节点自动从它们全部连过来(多入一出的汇聚)。所以用"一批 key"而非"一个 key"。Chain 帮你把"连边"这件烦事全自动化了——这就是它比 Graph 简单的地方。L05
自动补 END
编译时 Chain 还自动补一条到 END 的边(chain.go:98 的 addEndIfNeeded)——你最后一个 Append 的节点自动连到出口。所以你也不用手动写"连到 END"。
START 和 END 是什么? 每张图有两个保留节点:
START(入口,你的输入从这进)和 END(出口,最终结果从这出)。Chain 自动帮你连好 START → 第一个节点 和 最后一个节点 → END。Graph(Day 08)里这两条边要你自己连。Chain 的全部"魔法"就是:帮你自动连所有边(包括到 START/END),你只管 Append 组件。L06
局部并行与分支
Chain 虽是直线,但支持两种"局部复杂结构"(整体仍是一条主线):
AppendParallel(chain.go:459):一段并行——多个节点同时跑,结果汇聚给下一个。比如"同时调 3 个检索器"。AppendBranch(chain.go:342):一段分支——根据条件走不同节点。
并行怎么理解?
比如你要同时问三个知识库,等三个都返回再汇总。
AppendParallel 让这三个检索节点并发执行(Go 的 goroutine,三个一起跑而非排队),全部完成后结果合并,再流向下一个节点。这比串行快 3 倍。但注意:Chain 的并行/分支是"局部"的,整体还是一条主线;真正复杂的拓扑(循环、任意汇聚)要用 Graph。L07
Chain 只是 Graph 的封装
最后点破:Chain 内部就持有一个 *Graph(chain.go:72 的 gg *Graph[I, O])。所有 Append 最终都调 Graph 的 AddNode/AddEdge。
所以三种编排的真相是:
Chain 和 Workflow 都是内部 graph 结构的"糖衣"——Chain 帮你自动连边,Workflow 帮你按字段连边,底层都是同一个 Graph 引擎(Day 08/09 讲)。学 Chain 的价值是"低门槛入门编排";真正的引擎原理在 Graph。你写简单流水线用 Chain(省事),写 Agent 那种循环图用 Graph(灵活)。Graph[I,O] 本身也只是内部无类型 graph 的泛型包装(generic_graph.go:93)。⚠️ 小白常误以为:Chain 和 Graph 是两套独立的引擎、性能或能力不同。其实:Chain 内部就是一个 Graph,Append 全部转成 Graph 的 AddNode/AddEdge。它俩不是"两个引擎",而是"同一个引擎的两种写法"——Chain 是自动挡,Graph 是手动挡。
🎵 一句口诀记住三兄弟
「Chain 自动连、Graph 手动连、Workflow 按字段连;三者同根都是 graph,编译一律成 Runnable。」
L08
今日小结 + 动手
🧠 今天你应该能回答
- Chain/Graph/Workflow 三者关系?(同一 graph 的三层封装)
- Chain 什么时候用?(固定顺序、无循环的流水线)
- 为什么不用手动连边?(addNode 自动连上一批到当前 + 自动补 START/END)
- Chain 的局部并行/分支?
- Chain 和 Graph 的本质关系?(Chain 内部就是个 Graph)
✋ 动手:对着真实代码读一遍
# 1. Chain 用法注释(L03)
sed -n '52,62p' compose/chain.go
# 2. 自动连边(L04)
sed -n '560,600p' compose/chain.go
# 3. 自动补 END(L05)
sed -n '98,121p' compose/chain.go
# 4. Chain 内部持有 Graph(L07)
sed -n '72,82p' compose/chain.go
明天预告 · Day 08:Chain 是直线,Graph 是可循环的图——Agent 的基础。Day 08 讲 Graph 的节点、边、分支(Branch),以及它为什么能支持"模型↔工具"这种循环。