【Eino 框架入门】ChatModel 与 Message:调用大模型的最小代码

16 阅读5分钟

【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)
}

接口带来的好处

  1. 实现可替换:业务代码只依赖接口,换模型只改构造那行
  2. 编排可组合:Agent、Graph 等编排层只依赖接口
  3. 测试可 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,提供 GenerateStream
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() 都是独立的,模型记不住上一句说了什么。想实现多轮对话,得自己把历史消息攒起来。