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:72workflow.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 内部就持有一个 *Graphchain.go:37NewChain 底层 new 了个 graph)。你每次 AppendXxx,它就自动把"上一个节点"连到"当前节点",编译时再自动补上到 START/END 的边。你只管按顺序 Append 组件,连边这件烦事框架全包。
🏭 用「工厂流水线」来理解本讲(后面统一用这个类比) Chain 就像生活中的工厂流水线:工件从传送带一头进去,依次经过打磨、喷漆、包装工位,另一头出成品——固定顺序、单向、不回头。
自动连边就像传送带:你只管把工位(组件)按顺序摆好,传送带自动把上一工位的半成品送到下一工位,你不用手动搬。
AppendParallel(局部并行)就像并排三个同款工位:一批工件同时分给三个工位加工,做完在下游合流,比一个个排队快。

心智模型就是流水线A → B → C,前一个的输出就是后一个的输入。

START ChatTemplate ChatModel ToolsNode END
START ChatTemplate ChatModel ToolsNode END 每次 AppendXxx 自动补一条边(含到 START/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 时自动连了。
📝 最小例子:一句输入怎么流过整条链 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:560addNode——每次 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:98addEndIfNeeded)——你最后一个 Append 的节点自动连到出口。所以你也不用手动写"连到 END"。

START 和 END 是什么? 每张图有两个保留节点:START(入口,你的输入从这进)和 END(出口,最终结果从这出)。Chain 自动帮你连好 START → 第一个节点最后一个节点 → END。Graph(Day 08)里这两条边要你自己连。Chain 的全部"魔法"就是:帮你自动连所有边(包括到 START/END),你只管 Append 组件。
L06

局部并行与分支

Chain 虽是直线,但支持两种"局部复杂结构"(整体仍是一条主线):

  • AppendParallelchain.go:459):一段并行——多个节点同时跑,结果汇聚给下一个。比如"同时调 3 个检索器"。
  • AppendBranchchain.go:342):一段分支——根据条件走不同节点。
并行怎么理解? 比如你要同时问三个知识库,等三个都返回再汇总。AppendParallel 让这三个检索节点并发执行(Go 的 goroutine,三个一起跑而非排队),全部完成后结果合并,再流向下一个节点。这比串行快 3 倍。但注意:Chain 的并行/分支是"局部"的,整体还是一条主线;真正复杂的拓扑(循环、任意汇聚)要用 Graph。
L07

Chain 只是 Graph 的封装

最后点破:Chain 内部就持有一个 *Graphchain.go:72gg *Graph[I, O])。所有 Append 最终都调 Graph 的 AddNode/AddEdge

所以三种编排的真相是ChainWorkflow 都是内部 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),以及它为什么能支持"模型↔工具"这种循环。
← Day 06 Runnable Day 08 · Graph 图编排 →