[Golang]Eino探索之旅-初窥门径

5 阅读3分钟

Eino官方文档入口

官方文档已经非常详细,本文记录在学习Eino框架的一些理解与思考。

传送门:Eino快速开始:https://www.cloudwego.io/zh/docs/eino/quick_start/chapter_01_chatmodel_and_message/

最小对话模型

需求

调用指定模型API,在控制台输入发送给AI的话语,并在控制台流式打印AI的响应。

实现

Eino为以上需求提供了示例代码,下面会基于源码捋一捋思路和流程。

仓库地址:github.com/cloudwego/e…

克隆命令:

git clone https://github.com/cloudwego/eino-examples.git --depth=1

修改模型配置

Eino会默认使用openai的模型,为了方便,我将其修改为智谱旗下的模型(与openai完全兼容),当然,也可以配置其他模型

首先,进入到 eino-examples/quickstart/ 目录, 当前执行目录为 eino-examples ,后续所有相对路径的根目录为 quickstart

cd ./quickstart

找到 eino_assistant 目录下的.env文件,添加如下配置, 注意,值无需引号包裹

export ZHIPU_API_KEY=<your api key>
export ZHIPU_MODEL=<the model name you used>
export ZHIPU_BASE_URL=<官方开放的访问地址,如智谱:https://open.bigmodel.cn/api/paas/v4/>

保存修改,然后在当前控制台执行

source ./quickstart/eino_assistant/.env

这是因为控制台拿到的是系统环境变量的副本,修改了原配置文件,终端私有的环境变量并不会自动更新

接下来验证配置能够正常访问

echo $ZHIPU_MODEL

控制台应该打印你配置的模型名称,如:

image.png

然后打开 ./quickstart/chatwitheino/cmd/ch01 目录下的 main.go文件

修改newChatModel函数,使用自定义模型配置

func newChatModel(ctx context.Context) (model.ToolCallingChatModel, error) {
	if os.Getenv("MODEL_TYPE") == "ark" {
		return ark.NewChatModel(ctx, &ark.ChatModelConfig{
			APIKey:  os.Getenv("ARK_API_KEY"),
			Model:   os.Getenv("ARK_MODEL"),
			BaseURL: os.Getenv("ARK_BASE_URL"),
		})
	}
	return openai.NewChatModel(ctx, &openai.ChatModelConfig{
		APIKey:  os.Getenv("ZHIPU_API_KEY"),
		Model:   os.Getenv("ZHIPU_MODEL"),
		BaseURL: os.Getenv("ZHIPU_BASE_URL"),
	})
}

在学习过程中并未找到MODEL_TYPE的定义位置,直接忽略掉if块,不影响程序运行。

运行项目

最后,执行 ./chatwitheino/cmd/ch01 目录下的main.go

go run ./chatwitheino/cmd/ch01/main.go -- "用一句话解释 Eino 的 Component 设计解决了什么问题?"

应该得到如下输出:

image.png

至此,使用eino框架实现一个对话功能就完成了,接下来探究一下源码,完整代码在仓库中,各位小伙伴可以自行获取

原理与思路

宏观流程

Eino-ch01-宏观原理图.png

源码实现

main
var instruction string
flag.StringVar(&instruction, "instruction", "You are a helpful assistant.", "")
flag.Parse()

定义系统级指令,通过命令行参数指定,默认值为"You are a helpful assistant.", 该指令会和用户输入一并发送给模型

query := strings.TrimSpace(strings.Join(flag.Args(), " "))
if query == "" {
    _, _ = fmt.Fprintln(os.Stderr, "usage: go run ./cmd/ch01 -- \"your question\"")
    os.Exit(2)
}

从命令行提取并拼接用户输入,去除首尾空格。假设执行命令为 go run ./chatwitheino/cmd/ch01/main.go -- "你" -- “好 ”, 那么query的值为"你 好"

ctx := context.Background()
cm, err := newChatModel(ctx)
if err != nil {
    _, _ = fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}

创建空的上下文,通过newChatModel函数构建一个chatModel对象,用于后续动作的执行

messages := []*schema.Message{
    schema.SystemMessage(instruction),
    schema.UserMessage(query),
}

将系统级指令和用户的输入封装为一个统一的Message

stream, err := cm.Stream(ctx, messages)

Stream方法是一个核心方法,它做了以下工作:

  • 构建符合openai/智谱 API的HTTP请求,承载Message
  • 通过网络向ZHIPU_BASE_URL指定的端点发出请求
  • 构建流式连接,等待模型响应

stream对象是一个流式读取器,类型为*schema.StreamReader[*schema.Message]

这是一个泛型结构体,定义语句是type StreamReader[T any] struct,在此处内部泛型类型被传递成schema.Message

for {
    frame, err := stream.Recv()
    if errors.Is(err, io.EOF) {
            break
    }
    if err != nil {
            _, _ = fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
    }
    if frame != nil {
            _, _ = fmt.Fprint(os.Stdout, frame.Content)
    }
}

调用Recv,循环读取部分流,阅读源码,是通过一个channel传输流

这意味着,如果一直没有收到模型的响应,代码会一直阻塞,直到超时。

然后将读取到的内容打印至控制台,直到产生EOF错误,退出循环

注意:EOF错误表示流的所有内容已经读取完毕

newChatModel
return openai.NewChatModel(ctx, &openai.ChatModelConfig{
    APIKey:  os.Getenv("ZHIPU_API_KEY"),
    Model:   os.Getenv("ZHIPU_MODEL"),
    BaseURL: os.Getenv("ZHIPU_BASE_URL"),
})

使用用户定义的模型构造一个chatModel对象,并返回

最后

本文会不定时更新,仍在持续学习中~

如有错误,欢迎指正