第十篇:Eino ChatModel:将大模型调用抽象成可复用组件

2026-04-03 42 0

承接上一篇的实战内容,前面示例都是基于 adk.ChatModelAgent 间接调用大模型。

Agent 本质是上层封装,内部最终依旧依赖 ChatModel 组件完成 LLM 请求。本篇下沉至最底层核心组件 ChatModel,完整拆解模型组件的抽象设计、接口规范、消息结构体、同步/流式调用、通用配置、多模型适配,最后提供可直接运行的 Demo,不带 Agent,直接操作 ChatModel,弄懂 Eino 所有上层组件的底层根基。

一、ChatModel 在 Eino 整体架构中的定位

先回顾 Eino 分层架构:

数据层(schema) → 组件层(components) → 编排层(compose) → Agent层(adk)

ChatModel 隶属于 components/model,属于最基础、最核心的可插拔组件。

  1. 1. adk 的 ChatModelAgent 只是上层业务封装,接收 BaseChatModel 接口实例;
  2. 2. compose 编排链路里可以直接放入 ChatModel 作为链路节点;
  3. 3. 所有第三方大模型服务商(DeepSeek、OpenAI、通义千问、Ollama)全部适配这套统一接口;
  4. 4. 上层 Agent、Chain、Graph 不需要关心底层是哪家模型,只面向抽象接口编程。

组件层两层分离:抽象接口 + 厂商实现

Eino 采用经典的接口与实现分离设计:

  1. 1. 抽象接口包 github.com/cloudwego/eino/components/model
    定义接口标准,规定所有聊天模型必须具备哪些能力,业务代码依赖接口;
  2. 2. 厂商实现包 eino‑ext
    github.com/cloudwego/eino-ext/components/model/openai、ollama、ark 等适配层。
    只要服务商兼容 OpenAI‑API 协议,都可以直接复用 openai 适配组件,只修改接口地址、密钥、模型名称。

简单理解:BaseChatModel 是插座;各个厂商的 ChatModel 是插头;上层 Agent、编排链路只管插座,不用关心插头品牌。

二、ChatModel 整套接口体系详解

2.1 基础顶层接口 BaseChatModel

type BaseChatModel interface {
    Generate(ctx context.Context, messages []*schema.Message) (*schema.Message, error)
    Stream(ctx context.Context, messages []*schema.Message) (schema.StreamReader[*schema.Message], error)
}

两个最核心方法:

  1. 1. Generate:同步一次性请求,等待模型返回完整应答;适合后台任务、一次性文本处理;
  2. 2. Stream:流式返回,返回 StreamReader 迭代器,逐个读取 token 分片;适合网页对话、终端实时输出,降低首字等待时延。

在此之上衍生出继承接口:

  1. 1. ToolCallingChatModel:继承 BaseChatModel,新增绑定工具集、支持函数调用;
  2. 2. AgenticModel:面向复杂 Agent 场景,支持 Agent‑消息块、多工具类型。

日常普通对话场景只需要使用 BaseChatModel 即可。

三、底层数据载体 schema.Message

所有传入模型、模型返回的数据统一使用 *schema.Message,也是整套框架的数据标准:

消息角色常量

schema.SystemMessage(content string)    // 系统提示词,设定助手人设、规则
schema.UserMessage(content string)      // 用户提问消息
schema.AssistantMessage(content string) // AI 返回消息
schema.ToolMessage(content string, toolCallID string) // 工具返回结果

Message 内置核心字段

  1. 1. Role:消息角色;
  2. 2. Content:普通文本内容;
  3. 3. MultiContent:多模态消息,存放图片、音频等;
  4. 4. ToolCalls:模型发起的工具调用请求;
  5. 5. ResponseMeta:模型返回元信息,最常用 Token‑消耗用量;

重点优势:不同厂商 SDK 的消息结构体互不兼容,Eino 使用 schema.Message 做统一封装,切换模型不需要改动消息组装逻辑。

四、openai 适配层 ChatModelConfig 常用配置项

eino_openai.ChatModelConfig 为 OpenAI 协议模型通用配置,关键参数说明:

  1. 1. BaseURL:服务商接口地址,DeepSeek 需要填写 https://api.deepseek.com
  2. 2. APIKey:服务商密钥;
  3. 3. Model:模型名称;
  4. 4. Temperature:*float32 指针,随机性,0 精准,1 高随机;传 nil 使用模型默认参数;
  5. 5. MaxTokens:单次回答最大 token;
  6. 6. TopP、Stop、PresencePenalty 等原生 OpenAI 请求参数全部支持。

框架内所有可选数值配置全部使用指针类型,用来区分「用户主动设置数值」和「使用默认值」,所以项目通用泛型指针工具 ptr[T any]()

五、同步调用 Generate 和流式调用 Stream 的适用场景

5.1 Generate(同步一次性返回)

执行流程:

  1. 1. 组装好完整的 []*schema.Message 对话上下文;
  2. 2. 发起 HTTP 请求,阻塞等待模型全部输出完毕;
  3. 3. 返回完整 *schema.Message,可读取 Content、Token 消耗元数据。

适用场景:

  • • 后台批量文本处理、文档摘要;
  • • 不需要实时打字效果、能够接受整体等待;
  • • 需要一次性拿到完整响应再进入下一步 compose 编排节点。

5.2 Stream(流式分片读取)

执行流程:

  1. 1. 调用 model.Stream 获取流读取器 StreamReader;
  2. 2. 使用 Recv() 循环读取每一块增量 token;
  3. 3. io.EOF 代表流正常结束;
  4. 4. 必须 defer stream.Close(),释放网络连接、goroutine 资源,防止内存泄漏。

适用场景:

  • • 网页聊天、终端交互、聊天机器人;
  • • 需要降低首 token 延迟,给到用户即时反馈。

六、ChatModel 组件的可插拔特性与模型切换演示

得益于面向接口编程,上层代码只依赖 model.BaseChatModel,更换大模型只改动模型初始化函数,消息组装、同步/流式调用代码完全不用改动。

  • • DeepSeek:BaseURL = https://api.deepseek.com
  • • OpenAI:BaseURL = https://api.openai.com
  • • Ollama 本地部署模型:更换为 ollama 的 ChatModel 构造函数

七、完整可运行 Demo:直接调用 ChatModel,不使用 Agent

演示内容:通用指针工具、初始化模型、组装 schema 消息、同步 Generate、流式 Stream、首 token 计时、Token‑用量统计、流资源关闭。

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "os"
    "time"

    eino_openai "github.com/cloudwego/eino-ext/components/model/openai"
    "github.com/cloudwego/eino/schema"
)

// ==================== Eino 框架:ChatModel 直接调用 ====================
// 本 demo 演示不经过 Agent,直接使用 ChatModel 的 Generate(同步)和 Stream(流式)。

const (
    baseURL   = "https://api.deepseek.com"
    modelName = "deepseek-v4-flash" // DeepSeek 模型,若不存在可改 deepseek-chat
)

// ptr 泛型指针工具函数,快速获取任意类型变量的指针
func ptr[T any](v T) *T {
    return &v
}

// newChatModel 创建 DeepSeek ChatModel 实例(eino-ext 提供 OpenAI 兼容实现)。
func newChatModel(ctx context.Context) (*eino_openai.ChatModel, error) {
    apiKey := os.Getenv("DEEPSEEK_API_KEY")
    if apiKey == "" {
        return nil, fmt.Errorf("DEEPSEEK_API_KEY 环境变量未设置")
    }
    return eino_openai.NewChatModel(ctx, &eino_openai.ChatModelConfig{
        BaseURL:     baseURL,
        APIKey:      apiKey,
        Model:       modelName,
        Temperature: ptr(float32(0.5)),
    })
}

// buildMessages 构造对话消息:system 设定角色,user 提出问题。
func buildMessages() []*schema.Message {
    return []*schema.Message{
        schema.SystemMessage("你是一个 Go 语言学习高级助手,回答用户关于 Go 的问题,保持专业"),
        schema.UserMessage("简单概括 Go 语言的优势有哪些"),
    }
}

// generateDemo 演示同步生成:一次调用返回完整回复,适合对延迟不敏感的场景。
func generateDemo(ctx context.Context, model *eino_openai.ChatModel, messages []*schema.Message) {
    fmt.Println("============ 同步生成 ============")
    start := time.Now()

    resp, err := model.Generate(ctx, messages)
    if err != nil {
        fmt.Printf("Generate 失败:%v\n", err)
        return
    }

    fmt.Println(resp.Content)
    if resp.ResponseMeta != nil && resp.ResponseMeta.Usage != nil {
        u := resp.ResponseMeta.Usage
        fmt.Printf("Prompt tokens: %d  Completion tokens: %d  Total tokens: %d\n",
            u.PromptTokens, u.CompletionTokens, u.TotalTokens)
    }
    fmt.Printf("[总耗时] %d ms\n\n", time.Since(start).Milliseconds())
}

// streamDemo 演示流式生成:逐 token 输出,首 token 延迟低,适合交互场景。
func streamDemo(ctx context.Context, model *eino_openai.ChatModel, messages []*schema.Message) {
    fmt.Println("============ 流式输出 ============")
    start := time.Now()

    stream, err := model.Stream(ctx, messages)
    if err != nil {
        fmt.Printf("Stream 失败:%v\n", err)
        return
    }
    defer stream.Close()

    firstChunk := true
    for {
        chunk, err := stream.Recv()
        if errors.Is(err, io.EOF) {
            break // 流正常结束
        }
        if err != nil {
            fmt.Printf("\n流式读取出错:%v\n", err)
            return
        }
        if firstChunk {
            firstChunk = false
            fmt.Printf("[首 token 时间] %d ms\n", time.Since(start).Milliseconds())
        }
        fmt.Print(chunk.Content)
    }

    fmt.Printf("\n[总耗时] %d ms\n", time.Since(start).Milliseconds())
    fmt.Println("============ 流式输出结束 ============")
}

func main() {
    ctx := context.Background()

    chatModel, err := newChatModel(ctx)
    if err != nil {
        fmt.Printf("初始化模型失败:%v\n", err)
        return
    }

    messages := buildMessages()

    // 1. 同步生成:完整回复 + token 用量统计
    generateDemo(ctx, chatModel, messages)

    // 2. 流式生成:首 token 耗时 + 逐字输出
    streamDemo(ctx, chatModel, messages)
}

八、Demo‑代码逐段要点解析

8.1 泛型 ptr 指针工具

Eino 大量配置字段选用指针类型,用来区分「未赋值(nil)」和「主动设置数值」;
泛型 ptr 可以适配 float32、int、bool、string 所有类型,是项目通用工具。

8.2 newChatModel 模型实例初始化

  1. 1. 读取环境变量密钥,做前置校验;
  2. 2. 填充 OpenAI‑兼容配置结构体;
  3. 3. 返回具体厂商实现的 ChatModel,该结构体实现 BaseChatModel 接口。

8.3 buildMessages 构建标准化对话上下文

全部采用 schema 包内置构造函数生成消息切片,system 设置助手身份,user 设置用户提问;
后续不管同步还是流式调用,传入的消息结构体永远是统一标准。

8.4 generateDemo 同步调用

  1. 1. 记录开始时间用于耗时统计;
  2. 2. 调用 Generate 一次性获取完整应答消息;
  3. 3. 打印回答文本,读取 ResponseMeta.Usage 获取 Token 消耗;
    Token 统计在计费、日志、性能监控场景非常关键。

8.5 streamDemo 流式调用重点注意事项

  1. 1. Stream 返回 StreamReader,defer stream.Close() 强制关闭流
  2. 2. 循环 Recv() 获取增量分片;
  3. 3. io.EOF 是流正常结束标记,不属于异常错误;
  4. 4. 捕获第一个分片的耗时,观测首 token 延迟;
  5. 5. 循环打印分片 Content,实现终端打字机效果。

九、ChatModel 组件的工程价值总结

  1. 1. 统一抽象:BaseChatModel 屏蔽各个大模型服务商 SDK 的差异化,消息结构、调用方法全部标准化;
  2. 2. 组件可替换:面向接口编程,业务代码无需改动即可切换 DeepSeek、OpenAI、本地 Ollama;
  3. 3. 两种原生调用模式:同步 Generate 用于离线任务,流式 Stream 用于交互式对话;
  4. 4. 上层所有高级能力,ChatModelAgent、Chain、Graph 本质上都是对 ChatModel 的二次封装;
  5. 5. 配套标准的消息结构体 schema.Message,支持普通文本、多模态、工具调用、Token 元数据。

相关文章

第十六篇:Eino入门——核心组件总览与场景选型总结
第十五篇:Eino ChatModelAgent 模型自己决定是否调用工具
第十四篇:Eino ToolsNode 工具调用执行器
第十三篇:InvokableTool-把Go函数包装成模型可调用的工具
第十二篇:Eino ToolInfo工具-给模型看的说明书
第十一篇:Eino Message与Prompt上下文编排

发布评论