Eino 入门教程:用 Go 构建你的第一个 Chain(Prompt 模板 + 大模型节点串联实战)
引言:从「调 SDK」到「编排」
第 5 篇《Go AI 框架选型横评》的结论是:要建复杂 LLM 流水线就选 Eino。本篇进入实操——不聊 Agent、不聊图编排,先把 Eino 里使用频率最高的 Chain 吃透。
回顾第 1 篇《大模型 API 基础》,我们直接调 OpenAI SDK 完成了「一次调用」。但真实应用几乎从不只调一次:通常是「拼提示词 → 调模型 → 处理输出 → 再调模型」,或者「检索 → 拼提示词 → 调模型」。把这些步骤的胶水代码写在业务函数里,很快会变成一坨难测试、难复用的面条代码。
Eino 的 Chain 就是解决这个问题的最小抽象:把「一段固定顺序的步骤」声明出来,交给框架去执行。它本质上是链式语法糖,底下就是一张图(Graph),两个节点一条边,所以学会 Chain 也就等于学会了 Eino 图编排的一半。
一、Chain 的三个核心概念
| 概念 | 说明 | 对应 API |
|---|---|---|
| Chain | 有类型的步骤序列,输入输出类型在创建时用泛型定死 | compose.NewChain[I, O]() |
| 节点 | 链上的每一步:ChatTemplate、ChatModel、Lambda 等 | AppendChatTemplate / AppendChatModel / AppendLambda |
| Runnable | Compile() 之后的可执行体,可 Invoke、Stream、复用 |
chain.Compile(ctx) |
两个关键设计决定了它好用:
- 泛型定类型:
NewChain[map[string]any, *schema.Message]明确写出「输入一个 map,输出一条消息」,编译期就能发现类型不匹配,而不是运行到一半才 panic。 - 声明式串联:
AppendA().AppendB().AppendC()链式写法天然表达「先 A 后 B 再 C」,数据流就是代码的阅读顺序。
二、环境准备
go mod init mychain
go get 'github.com/cloudwego/eino@v0.9.23'
go get 'github.com/cloudwego/eino-ext/components/model/openai@v0.1.13'
两条 go get 都带明确版本号。Eino 主模块与 eino-ext 的组件(model、prompt、retriever 等)分开发版,安装时两边都要指定版本。
API Key 照旧放进环境变量 OPENAI_API_KEY;国内读者把 BaseURL 指向兼容 OpenAI 协议的端点即可,代码不用改。
三、第一个 Chain:Prompt 模板 → 大模型
先看不带框架时我们要手写什么:构造 messages 切片、管理 System Prompt、调模型、解析响应。用 Chain 之后,前两步变成一个声明:
package main
import (
"context"
"fmt"
"os"
"github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/schema"
)
func main() {
ctx := context.Background()
// 1. 创建模型节点
chatModel, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
Model: "gpt-4o-mini", // 换成你服务商可用的模型名
APIKey: os.Getenv("OPENAI_API_KEY"),
})
if err != nil {
panic(err)
}
// 2. 创建 Prompt 模板:System 定角色,{xxx} 是 FString 占位符
template := prompt.FromMessages(schema.FString,
&schema.Message{Role: schema.System, Content: "你是一位资深 {extra}工程师,回答简洁准确。"},
&schema.Message{Role: schema.User, Content: "用一句话解释什么是 {topic}"},
)
// 3. 声明 Chain:输入 map,输出消息切片
chain := compose.NewChain[map[string]any, []*schema.Message]().
AppendChatTemplate(template).
AppendChatModel(chatModel)
// 4. 编译
runnable, err := chain.Compile(ctx)
if err != nil {
panic(err)
}
// 5. 运行:传变量,拿回 messages
resp, err := runnable.Invoke(ctx, map[string]any{
"extra": "Go 语言",
"topic": "goroutine",
})
if err != nil {
panic(err)
}
fmt.Println(resp[len(resp)-1].Content) // 最后一条即模型回复
}
逐段拆解:
- 模板里的
{xxx}:schema.FString采用 Python 风格的{var}占位符(Eino 还支持 GoTemplate 的{{.Var}}和 Jinja2 风格),运行时从输入 map 取同名 key 渲染。同一个 Chain 换个 map 值就是一次全新的对话上下文,模板复用、数据隔离。 - 类型流转:Chain 的泛型参数
[map[string]any, []*schema.Message]决定了第一步 ChatTemplate 的Format收到 map、吐出消息切片,第二步 ChatModel 恰好消费消息切片——类型对不上会在Compile时直接报错,而不是运行时。 - 输出取最后一条:模型节点输出的是完整 messages 列表,最后一条才是本次回复。
四、给 Chain 加一步:Lambda 自定义节点
实际应用里经常在模型前后插业务逻辑:过滤敏感词、解析 JSON、记日志。用 AppendLambda 加一个普通 Go 函数节点:
// 在 AppendChatModel 之后追加一个后处理节点
chain = chain.AppendLambda(compose.InvokableLambda(func(ctx context.Context, msgs []*schema.Message) (string, error) {
last := msgs[len(msgs)-1]
// 例如:统一去掉首尾空白,或在这里做 JSON 解析、落库
return "【AI】" + last.Content, nil
}))
此时 Chain 的输出类型从 []*schema.Message 变成了 string,所以创建时泛型要相应改成 compose.NewChain[map[string]any, string]。Lambda 让 Chain 的输入输出类型完全由你定义,框架只负责把中间各段的类型衔接好。
常见可插入的节点速查:
| 需求 | 节点 | Append 方法 |
|---|---|---|
| 渲染提示词 | ChatTemplate | AppendChatTemplate |
| 调大模型 | ChatModel | AppendChatModel |
| 自定义业务逻辑 | Lambda | AppendLambda |
| 调用工具(第 2 篇主题) | ToolsNode | AppendToolsNode |
| 文本转向量(第 3 篇主题) | Embedder | AppendEmbedding |
| 知识库检索(RAG 预告) | Retriever | AppendRetriever |
五、Invoke 还是 Stream
第 4 篇《SSE 流式输出教程》我们手写了流式协议,Eino 里 Chain 编出来的 Runnable 直接支持:
// Invoke:一次性拿完整结果
resp, err := runnable.Invoke(ctx, map[string]any{"extra": "Go 语言", "topic": "channel"})
// Stream:流式拿增量
sr, err := runnable.Stream(ctx, map[string]any{"extra": "Go 语言", "topic": "channel"})
if err != nil {
panic(err)
}
defer sr.Close()
for {
chunk, err := sr.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
panic(err)
}
fmt.Print(chunk[len(chunk)-1].Content)
}
注意新版 eino 的流读取方法是 Recv()(旧版部分示例中的 Next() 已更名)。Compile 一次、Invoke/Stream 随便调多少次——Runnable 是可复用的,这也是 Chain 比「每次重新拼代码」省心的地方。
六、什么时候该上 Graph
Chain 适合固定顺序的流水线。一旦需要分支(「用户问技术问题走 A 链,闲聊走 B 链」)、循环(多轮工具调用)、并行(同时查库和查缓存),就该用 compose.NewGraph 显式编排了,那是本系列后续图编排篇的内容。选择标准一句话:数据流是直线用 Chain,是图用 Graph;Chain 编不下去的那天,迁移成本几乎为零,因为 Chain 本来就是 Graph。
版本说明
本文代码 API 均于 2026-11-13 逐字核对自以下官方源码与发布页,安装请以下列版本为准:
| 依赖 | 版本 | 安装命令 |
|---|---|---|
| eino | v0.9.23(2026-11-09 发布,官方 releases 页核实) | go get github.com/cloudwego/eino@v0.9.23 |
| eino-ext OpenAI 组件 | v0.1.13(官方 tags 页核实) | go get github.com/cloudwego/eino-ext/components/model/openai@v0.1.13 |
官方文档:cloudwego.io/docs/eino;源码核对仓库:github.com/cloudwego/eino(compose/chain.go、components/prompt/chat_template.go、schema/message.go、schema/stream.go)与 github.com/cloudwego/eino-ext(components/model/openai/chatmodel.go @ v0.1.13)。文中 NewChain / AppendChatTemplate / AppendChatModel / AppendLambda / Compile / Recv 等 API 签名均抄录自上述源码。
本文是「Go AI 应用开发」系列第 6 篇(共 18 篇规划),阶段二「Go AI 框架」第二篇。上一篇:《Go AI 框架选型横评:Eino vs LangChainGo vs Genkit vs GoAI SDK》;系列前篇:《大模型 API 基础》、《Function Calling 教程》、《Embedding 与语义检索》、《SSE 流式输出教程》。下一篇预告:《Eino 图编排:分支、循环与人机协同中断恢复》。
- 《Go 程序设计语言》(Go 语言圣经,机械工业出版社) —— Go 官方团队出品,公认入门第一书
- 《Go 语言实战》(异步图书,人民邮电出版社) —— 实战导向,面试前巩固工程手感
- 《Go 语言编程之旅:一起用 Go 做项目》(博文视点,电子工业出版社) —— 项目驱动入门,从零写出完整应用
- Keychron K1Max 矮轴机械键盘(Mac/Win 双系统,QMK/VIA 开源改键) —— 轻薄矮轴 + 开源固件,程序员桌面常客
- 罗技 MX Master 3S 无线鼠标(Flow 跨屏,静音电磁滚轮) —— 人体工学设计,写代码一整天手不酸
以上链接含京东联盟推广代码;通过链接购买本站可能获得少量佣金,不影响你的购买价格。
每周更新 Go + AI 应用开发实战:AI 面试官完整项目、MCP、Agent 工程化,完整源码随文章首发。
微信扫码,或搜索「Go2AI之路」关注,第一时间获取系列更新与源码,不再错过任何一篇。