【Eino 框架入门】ChatModel 与 Message:调用大模型的最小代码
你想在 Go 程序里调用大模型,Eino 给你提供了统一的接口。这篇用最少的代码跑通第一次调用。
Eino 是什么?
Eino 是字节跳动开源的 Go 语言 AI 应用开发框架。它解决这几个问题:
| 问题 | Eino 的方案 |
|---|---|
| 模型太多,API 不统一 | 定义 ChatModel 接口,OpenAI、Ark、Claude 都实现这个接口 |
| 能力分散,难以组合 | 定义 Component 接口,Tool、Retriever 等可插拔 |
| 复杂流程难编排 | 提供 Agent、Graph、Chain 等编排抽象 |
| 生产级能力缺失 | 内置流式输出、中断恢复、可观测性 |
三个仓库:
eino:核心库,定义接口eino-ext:扩展实现(OpenAI、Ark、Ollama 等)eino-examples:示例代码
Component 接口:为什么需要它?
Eino 定义了一组 Component 接口,ChatModel 是最基础的一个:
type BaseChatModel interface {
Generate(ctx context.Context, input []*schema.Message, opts ...Option) (*schema.Message, error)
Stream(ctx context.Context, input []*schema.Message, opts ...Option) (
*schema.StreamReader[*schema.Message], error)
}
接口带来的好处:
- 实现可替换:业务代码只依赖接口,换模型只改构造那行
- 编排可组合:Agent、Graph 等编排层只依赖接口
- 测试可 Mock:接口天然支持 mock,单元测试不用真调模型
// OpenAI
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{...})
// 换成字节跳动的 Ark
cm, _ := ark.NewChatModel(ctx, &ark.ChatModelConfig{...})
// 换成 Ollama 本地模型
cm, _ := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{...})
// 后面的调用完全不用改
stream, _ := cm.Stream(ctx, messages)
Message:对话的基本单位
跟大模型对话,本质是传一个消息列表。每条消息有个角色:
type Message struct {
Role RoleType // system / user / assistant / tool
Content string // 文本内容
ToolCalls []ToolCall // 仅 assistant 消息可能有
}
四种角色:
| 角色 | 作用 | 举例 |
|---|---|---|
system | 系统指令,告诉模型怎么表现 | "你是个程序员助手" |
user | 用户说的话 | "帮我写个函数" |
assistant | 模型回复 | "好的,这是一个函数..." |
tool | 工具调用的返回结果 | {"result": "success"} |
构造函数:
schema.SystemMessage("You are a helpful assistant.")
schema.UserMessage("What is the weather today?")
schema.AssistantMessage("I don't know.", nil) // 第二个参数是 ToolCalls
schema.ToolMessage("tool result", "call_id")
最简单的对话就是一条 System + 一条 User:
messages := []*schema.Message{
schema.SystemMessage("你是个助手"),
schema.UserMessage("你好"),
}
核心代码:三步调通
// 1. 创建 ChatModel
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: "your-api-key",
Model: "gpt-4o-mini",
})
// 2. 构造消息
messages := []*schema.Message{
schema.SystemMessage("你是个助手"),
schema.UserMessage("你好"),
}
// 3. 流式调用
stream, _ := cm.Stream(ctx, messages)
defer stream.Close()
for {
chunk, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
log.Fatal(err)
}
if chunk != nil {
fmt.Print(chunk.Content)
}
}
Stream vs Generate
| 方法 | 特点 | 适用场景 |
|---|---|---|
Stream() | 流式返回,字一个个蹦出来 | 聊天、交互式场景 |
Generate() | 一次性返回完整结果 | 批量处理、后台任务 |
// 流式:字一个个出来
stream, _ := cm.Stream(ctx, messages)
// 非流式:等一会儿,一次性返回
reply, _ := cm.Generate(ctx, messages)
fmt.Println(reply.Content)
大多数场景用 Stream,用户体验更好。
容易踩的坑
1. chunk.Content 是增量
每次 Recv() 拿到的是一小段,不是完整回复。想拿完整内容得自己拼。
2. io.EOF 是正常结束
流读完返回 io.EOF,这不是错误,是告诉你流结束了。
3. chunk 可能为 nil
某些情况下返回的 chunk 是空的,直接访问会 panic:
if chunk != nil {
fmt.Print(chunk.Content)
}
4. 要 Close()
用完 stream 要关掉,不然资源泄漏:
defer stream.Close()
运行示例
# 设置环境变量
export OPENAI_API_KEY="your-key"
export OPENAI_MODEL="gpt-4o-mini"
# 运行
cd eino-examples/quickstart/chatwitheino
go run ./cmd/ch01 -- "用一句话解释 Eino 解决了什么问题?"
输出(流式逐步打印):
[assistant] Eino 通过统一的 Component 接口...
小结
| 概念 | 一句话解释 |
|---|---|
| Component | 定义可替换、可组合、可测试的能力边界 |
| ChatModel | 最基础的 Component,提供 Generate 和 Stream |
| Message | 对话数据的基本单位,通过角色区分语义 |
| Stream | 流式返回,字一个个蹦出来 |
| Generate | 一次性返回完整结果 |
这篇的局限:每次调用都是独立的,模型记不住上一句说了什么。想实现多轮对话,得自己把历史消息攒起来——这是下一章的内容。
【Eino 框架入门】用 ChatModel 调用大模型
你有个 OpenAI 的 API Key,想在 Go 程序里调用大模型,这篇教你用 Eino 框架最简单的方式。
核心就三步
// 1. 创建 ChatModel
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{
APIKey: "your-api-key",
Model: "gpt-4o-mini",
})
// 2. 构造消息
messages := []*schema.Message{
schema.SystemMessage("你是个助手"),
schema.UserMessage("你好"),
}
// 3. 流式调用
stream, _ := cm.Stream(ctx, messages)
for {
frame, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
fmt.Print(frame.Content)
}
Message 有四种角色
跟大模型对话,本质是传一个消息列表。每条消息有个角色:
| 角色 | 作用 | 举例 |
|---|---|---|
| System | 系统指令,告诉模型怎么表现 | "你是个程序员助手" |
| User | 用户说的话 | "帮我写个函数" |
| Assistant | 模型回复 | "好的,这是一个函数..." |
| Tool | 工具调用的返回结果 | {"result": "success"} |
最简单的对话就是一条 System + 一条 User。多轮对话时,历史消息越来越多,全部塞进去就行——大模型会自己理解上下文。
// 三轮对话后的 messages 长这样
messages := []*schema.Message{
schema.SystemMessage("你是个助手"),
schema.UserMessage("你好"),
schema.AssistantMessage("你好!", nil),
schema.UserMessage("我是谁?"),
schema.AssistantMessage("你还没告诉我", nil),
schema.UserMessage("我叫小明"),
}
Stream 和 Generate 的区别
Stream() 是流式的,模型边生成边返回,字一个个蹦出来。Generate() 是一次性返回,用户要等模型全想完才能看到。
// 流式:字一个个出来
stream, _ := cm.Stream(ctx, messages)
// 非流式:等一会儿,一次性返回
reply, _ := cm.Generate(ctx, messages)
fmt.Println(reply.Content)
大多数场景用 Stream,体验更好。只有批量处理、后台任务这种场景用 Generate。
容易踩的坑
frame.Content 是增量。每次 Recv() 拿到的是一小段,不是完整回复。想拿完整内容得自己拼。
io.EOF 是正常结束。流读完返回 io.EOF,这不是错误,是告诉你流结束了。
frame 可能为 nil。某些情况下返回的 frame 是空的,直接访问 frame.Content 会 panic。
要 Close()。用完 stream 要关掉,不然资源泄漏。
for {
frame, err := stream.Recv()
if errors.Is(err, io.EOF) {
break // 正常结束
}
if err != nil {
log.Fatal(err) // 真正的错误
}
if frame != nil { // 判空
fmt.Print(frame.Content)
}
}
stream.Close() // 关闭
ChatModel 是接口,不是具体类型
openai.NewChatModel 返回的是 ChatModel 接口。换模型只改创建那行:
// OpenAI
cm, _ := openai.NewChatModel(ctx, &openai.ChatModelConfig{...})
// 换成字节跳动的 Ark
cm, _ := ark.NewChatModel(ctx, &ark.ChatModelConfig{...})
// 换成 Ollama 本地模型
cm, _ := ollama.NewChatModel(ctx, &ollama.ChatModelConfig{...})
后面的 Stream()、Generate() 调用完全不用改。这就是接口抽象的好处——业务代码不依赖具体实现。
这篇的局限
每次调用 Stream() 都是独立的,模型记不住上一句说了什么。想实现多轮对话,得自己把历史消息攒起来。