用 Go 语言快速构建可扩展、可维护的 AI 应用 ------ 第一章:ChatModel 与 AgenticMessage
AI 应用开发正从"调用一次 API"演变为"编排复杂工作流"。我们需要处理多种大语言模型提供商、管理对话状态、支持工具调用、实现流式输出...... 如何避免代码变成一堆难以维护的 if-else 和硬编码?Eino 给出了答案。
Eino 是一个基于 Go 的 AI 应用开发框架(Agent Development Kit),它通过统一的 Component 接口 抽象出模型、工具、检索器等能力单元,并提供了 Agent、Graph、Chain 等编排抽象,让开发者能够像搭积木一样构建生产级 AI Agent。
本系列将带你从零开始,逐步实现一个完整的智能助手 ------ ChatWithEino 。今天的第一章,我们从最基础的 ChatModel 调用开始,理解 Eino 的核心设计思想,并亲手写出第一个支持流式输出的对话程序。
一、为什么需要 Component 接口?
在传统代码中,调用 OpenAI 和调用 Ark 可能是两套完全不同的 API:
go
// 硬编码 OpenAI
resp, _ := openai.CreateChatCompletion(...)
// 换成 Ark 需要重写所有调用代码
resp, _ := ark.Generate(...)
Eino 定义了统一的 BaseModel 接口:
go
type BaseModel[M any] interface {
Generate(ctx context.Context, input []M, opts ...Option) (M, error)
Stream(ctx context.Context, input []M, opts ...Option) (*schema.StreamReader[M], error)
}
好处显而易见:
- 实现可替换:eino-ext 提供了 OpenAI、Ark、Claude、Ollama 等多种实现,业务代码只依赖接口。切换模型只需修改构造逻辑,业务逻辑一行不改。
- 编排可组合:Agent、Graph 等高层抽象只依赖接口,不关心具体实现。你可以自由组合不同提供商的组件。
- 测试可 Mock:接口天然支持 mock,单元测试不需要真实调用模型,也不会消耗 API 额度。
本章只涉及 ChatModel,后续会逐步引入 Tool、Retriever 等组件,但它们都遵循同样的接口哲学。
二、AgenticMessage:对话的基本单位
Eino 的 Quickstart 系列使用 schema.AgenticMessage 作为统一的消息结构。它比普通的文本消息更强大,能够表达模型在一次回复中的多个有序事件:
go
type AgenticMessage struct {
Role AgenticRoleType // system / user / assistant
ContentBlocks []*ContentBlock // 有序的内容块
ResponseMeta *AgenticResponseMeta // 元信息
Extra map[string]any // 扩展字段
}
ContentBlock 可以包含推理内容(reasoning)、普通文本、工具调用、工具结果等。这对于支持 推理时调用工具(如 DeepSeek‑R1、Claude 的 tool use)至关重要。
常用的构造函数简化了创建:
go
schema.SystemAgenticMessage("You are a helpful assistant.")
schema.UserAgenticMessage("What is the weather today?")
// 手动构造 assistant 回复
&schema.AgenticMessage{
Role: schema.AgenticRoleTypeAssistant,
ContentBlocks: []*schema.ContentBlock{
schema.NewContentBlock(&schema.AssistantGenText{Text: "I don't know."}),
},
}
角色语义与 OpenAI 一致:system 给出系统指令,user 为用户输入,assistant 为模型回复。
三、实战:第一个流式对话程序
我们将编写一个简单的命令行程序,接收用户问题,调用 ChatModel 的流式接口,并逐词打印回复。
前置准备
- 获取代码 (本系列基于
eino-examples仓库):
bash
git clone https://github.com/cloudwego/eino-examples.git
cd eino-examples/quickstart/chatwitheino
-
Go 环境:1.21 或更高版本。
-
选择一个模型提供商(以 OpenAI 为例):
bash
export OPENAI_API_KEY="your-api-key"
export OPENAI_MODEL="gpt-4.1-mini" # 或 gpt-4o, gpt-4o-mini
# 可选:OPENAI_BASE_URL(代理或兼容服务)
# 可选:OPENAI_BY_AZURE=true(使用 Azure OpenAI)
代码解析
入口文件位于 cmd/ch01/main.go。下面是核心逻辑的简化版本:
go
func runTyped[M adk.MessageType](ctx context.Context, instruction, query string) {
// 1. 创建 ChatModel(根据环境变量自动选择 OpenAI/Ark)
cm, err := chatmodel.NewModel[M](ctx)
if err != nil {
log.Fatal(err)
}
// 2. 构造消息:system + user
messages := []M{
msgops.NewSystem[M](instruction),
msgops.NewUser[M](query),
}
// 3. 调用流式接口
stream, err := cm.Stream(ctx, messages)
if err != nil {
log.Fatal(err)
}
defer stream.Close()
// 4. 逐帧读取并打印
for {
frame, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Print(msgops.AssistantDeltaText(frame)) // 只输出增量文本
}
}
执行命令:
bash
go run ./cmd/ch01 -- "用一句话解释 Eino 的 Component 设计解决了什么问题?"
输出示例(流式逐步打印):
[assistant] Eino 的 Component 设计通过定义统一接口,让开发者可以无缝切换不同 LLM 提供商而不影响业务代码。
关键点说明
chatmodel.NewModel[M]:根据MODEL_TYPE环境变量,自动构造 OpenAI 或 Ark 的 agentic model 实例。这就是接口可替换性的体现。msgops.NewSystem/NewUser:封装了AgenticMessage的创建,返回泛型M(即*schema.AgenticMessage)。cm.Stream:返回*schema.StreamReader[M],它是一个可迭代的流。每一帧通常是一个包含ContentBlock的消息片段。msgops.AssistantDeltaText:从一帧消息中提取出本次新增的助手文本。因为流式输出可能将一段文字拆成多帧。
四、从本章到下一章
现在,我们已经能够完成单次对话了。但现实中的 Agent 需要记住多轮对话历史、支持用户跨会话恢复、调用外部工具......
接下来的章节将逐一引入:
| 章节 | 核心能力 | 解决的问题 |
|---|---|---|
| 第二章 | Agent + Runner | 多轮对话,对话状态管理 |
| 第三章 | Memory + Session | 持久化历史,会话恢复 |
| 第四章 | Tool | 文件读取、代码搜索等工具调用 |
| ... | ... | ... |
每一章都在前一章的基础上增加一个核心能力,让你看到架构如何从简单演变为健壮的生产系统。
五、总结
本章我们学到了:
- Eino 的 Component 接口 带来了可替换、可组合、可测试的架构优势。
- AgenticMessage 是一个能够表达复杂对话事件(推理、工具调用、文本)的统一消息结构。
- 通过
BaseModel.Stream可以轻松实现流式对话,提升用户体验。 - 整个 Quickstart 系列遵循渐进式构建的理念,从最简单的 ChatModel 开始,逐步叠加功能。
下一章,我们将引入 Agent 与 Runner,让程序具备多轮对话的能力。敬请期待!
附:本章完整代码
eino-examples/quickstart/chatwitheino/cmd/ch01/main.go
相关资源
本文由 ChatWithEino 智能助手协助撰写,它本身也是用 Eino 构建的 😊
本系列文档基于 Eino v0.3+,Go 1.21+。