Eino 是字节跳动开源的 Go 语言 AI 应用开发框架,其记忆功能的实现采用了框架与业务层解耦的设计:框架本身不提供内置的 Memory 组件,而是提供基础的消息处理、向量存储等能力,由业务层根据需求实现具体的记忆存储与管理逻辑。
记忆功能通常分为两类:基础会话记忆(短期对话持久化) 和 长期向量记忆(跨会话语义记忆),以下是具体的实现方案:
一、基础会话记忆:实现持久化多轮对话
基础会话记忆用于解决「进程退出后对话历史丢失」的问题,支持跨进程、跨设备恢复当前会话的对话上下文,是最常用的记忆能力。
1. 核心概念
Eino 官方示例中,通过业务层的三个核心概念实现该能力:
|-------------|--------------------------------------------|
| 概念 | 说明 |
| Session | 代表一次完整的对话会话,包含会话 ID、创建时间、对话消息列表,负责单会话的消息管理 |
| Store | 负责管理多个 Session 的存储与生命周期,支持会话的创建、查询、删除、恢复 |
| Memory | 整体的记忆能力抽象,本质是对话历史的持久化方案,可灵活选择存储介质 |
2. 实现流程
整个流程是业务层与框架层的协作过程:
-
保存用户输入:用户输入消息后,先将其持久化到 Session 中
-
加载历史消息:从 Session 中取出当前会话的所有历史对话
-
框架处理消息 :将历史消息传递给 Eino 的
Runner(Agent 执行器),由框架调用大模型生成回复 -
保存助手回复:将模型生成的回复也持久化到 Session 中,完成一轮记忆的更新
3. 代码实现示例
以下是基于 JSONL 文件存储的最简实现(官方示例简化版),你也可以替换为 MySQL、Redis 等存储介质:
package main
import (
"bufio"
"context"
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/cloudwego/eino/adk"
"github.com/cloudwego/eino/schema"
"github.com/cloudwego/eino-ext/components/model/qwen"
"github.com/google/uuid"
)
// Session 代表一个对话会话,消息会持久化到 jsonl 文件
type Session struct {
ID string
CreatedAt time.Time
filePath string
messages []*schema.Message
}
// Append 追加消息到内存和持久化文件
func (s *Session) Append(msg *schema.Message) error {
s.messages = append(s.messages, msg)
data, err := json.Marshal(msg)
if err != nil {
return err
}
f, err := os.OpenFile(s.filePath, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintf(f, "%s\n", data)
return err
}
// GetMessages 获取所有历史消息
func (s *Session) GetMessages() []*schema.Message {
result := make([]*schema.Message, len(s.messages))
copy(result, s.messages)
return result
}
// Store 管理所有会话的存储
type Store struct {
dir string
cache map[string]*Session
}
func NewStore(dir string) (*Store, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Store{dir: dir, cache: make(map[string]*Session)}, nil
}
// GetOrCreate 获取或创建会话,不存在则新建,存在则从磁盘加载
func (s *Store) GetOrCreate(id string) (*Session, error) {
if sess, ok := s.cache[id]; ok {
return sess, nil
}
filePath := filepath.Join(s.dir, id+".jsonl")
if _, err := os.Stat(filePath); err != nil {
if os.IsNotExist(err) {
// 创建新会话
sess := &Session{
ID: id,
CreatedAt: time.Now(),
filePath: filePath,
messages: make([]*schema.Message, 0),
}
// 写入会话头
header := map[string]any{
"type": "session",
"id": id,
"created_at": sess.CreatedAt,
}
headerData, _ := json.Marshal(header)
f, _ := os.Create(filePath)
f.WriteString(string(headerData) + "\n")
f.Close()
s.cache[id] = sess
return sess, nil
}
return nil, err
}
// 从磁盘加载已有会话
return loadSession(filePath)
}
// 从文件加载会话
func loadSession(filePath string) (*Session, error) {
// 省略文件读取与解析逻辑,完整代码可参考官方示例
// 逐行读取 jsonl 文件,恢复会话元信息和消息列表
}
func main() {
// 1. 初始化存储
sessionDir := "./data/sessions"
store, err := NewStore(sessionDir)
if err != nil {
log.Fatal(err)
}
// 2. 获取或创建会话
var sessionID string
flag.StringVar(&sessionID, "session", "", "session id to resume")
flag.Parse()
if sessionID == "" {
sessionID = uuid.NewString()
fmt.Printf("Created new session: %s\n", sessionID)
}
session, err := store.GetOrCreate(sessionID)
if err != nil {
log.Fatal(err)
}
// 3. 初始化 Eino Runner
ctx := context.Background()
llm, err := qwen.NewChatModel(ctx, &qwen.Config{
APIKey: os.Getenv("DASHSCOPE_API_KEY"),
Model: os.Getenv("QWEN_MODEL"),
})
if err != nil {
log.Fatal(err)
}
runner := adk.NewRunner(llm)
// 4. 对话循环
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("you> ")
if !scanner.Scan() {
break
}
line := strings.TrimSpace(scanner.Text())
if line == "" {
break
}
// 步骤1:保存用户消息
userMsg := schema.UserMessage(line)
if err := session.Append(userMsg); err != nil {
log.Fatal(err)
}
// 步骤2:加载历史,调用框架处理
history := session.GetMessages()
events := runner.Run(ctx, history)
// 步骤3:收集回复
content, err := printAndCollectAssistant(events)
if err != nil {
log.Fatal(err)
}
// 步骤4:保存助手回复
assistantMsg := schema.AssistantMessage(content, nil)
if err := session.Append(assistantMsg); err != nil {
log.Fatal(err)
}
}
}
// 处理流式回复,收集完整内容
func printAndCollectAssistant(events *adk.AsyncIterator[*adk.AgentEvent]) (string, error) {
// 省略流式处理逻辑
}
4. 存储方案扩展
上述示例使用 JSONL 文件存储,适合单机简单场景,你可以根据业务需求替换为其他存储:
-
关系型数据库:MySQL、PostgreSQL,适合需要事务、会话管理的场景
-
缓存数据库:Redis,适合分布式场景、高并发会话访问
-
云存储:S3、OSS,适合需要长期归档会话的场景
二、长期向量记忆:实现跨会话的语义记忆
基础会话记忆只能保留当前会话的历史,而长期向量记忆可以突破会话边界,将用户的历史对话、偏好、习惯等信息永久存储,并且支持语义检索,让模型在任意会话中都能回忆起用户的长期信息,同时突破大模型的上下文窗口限制。
1. 实现原理
长期向量记忆的本质是基于 RAG 的记忆管理,和知识库检索的原理一致:
-
记忆向量化:将用户的对话片段、用户信息等记忆内容,通过 Embedding 模型转换为向量
-
向量存储:将向量和原文存储到向量数据库中
-
记忆检索:用户提问时,用问题作为查询,从向量库中检索语义最相关的记忆片段
-
上下文注入:将检索到的记忆片段注入到 Prompt 中,让模型基于记忆生成回答
2. 用到的 Eino 组件
Eino 提供了完整的组件生态,支持快速实现该能力:
|--------------------------|-----------------------------------------------------------------|
| 组件 | 作用 |
| Embedding | 将文本转换为向量,Eino 支持 OpenAI、火山方舟等多种 Embedding 模型 |
| VectorStore | 向量存储,Eino 官方提供了 RedisSearch、ElasticSearch、Chroma、VikingDB 等多种实现 |
| Retriever | 向量检索组件,负责从向量库中召回相关的记忆片段 |
| Document Transformer | 文本拆分,将长对话拆分为合适大小的记忆片段 |
3. 实现步骤
步骤 1:初始化向量存储与检索组件
// 初始化 Embedding 模型
embedding, err := ark.NewEmbeddingModel(ctx, &ark.EmbeddingConfig{
APIKey: os.Getenv("ARK_API_KEY"),
Model: "doubao-embedding-large",
})
// 初始化 Redis 向量存储
vectorStore, err := redis.NewVectorStore(ctx, &redis.VectorStoreConfig{
Addr: "localhost:6379",
Index: "user_memory", // 按用户隔离的话,可以用 user_id 作为索引
Embedding: embedding,
})
// 初始化检索器
retriever := vectorStore.AsRetriever(&redis.RetrieverConfig{
TopK: 5, // 召回最相关的5条记忆
})
步骤 2:记忆存储:将对话历史存入向量库
每次对话结束后,将新的对话片段处理后存入向量库:
// 对话结束后,将新的对话转换为 Document
memoryDoc := &schema.Document{
Content: fmt.Sprintf("用户说:%s,助手回复:%s", userInput, assistantReply),
MetaData: map[string]any{
"user_id": "当前用户ID",
"time": time.Now(),
},
}
// 存入向量库
err = vectorStore.AddDocuments(ctx, []*schema.Document{memoryDoc})
步骤 3:记忆检索:提问时召回相关记忆
用户新的提问时,先检索相关的长期记忆:
// 用户提问
userQuery := "我之前说过我喜欢什么口味的咖啡?"
// 检索相关记忆
memoryDocs, err := retriever.Retrieve(ctx, userQuery)
if err != nil {
log.Fatal(err)
}
// 将记忆转换为文本,注入到 Prompt
memoryText := ""
for _, doc := range memoryDocs {
memoryText += fmt.Sprintf("- %s\n", doc.Content)
}
// 构建带记忆的 Prompt
prompt := fmt.Sprintf(`
你是一个助手,可以回忆用户的长期信息。
以下是和当前问题相关的用户历史记忆:
%s
请基于这些记忆回答用户的问题:%s
`, memoryText, userQuery)
步骤 4:调用模型处理
将构建好的 Prompt 传递给 Eino 的 Runner,模型就会基于长期记忆生成回答了。
三、总结
Eino 框架的记忆功能采用了灵活的分层设计:
-
框架层:提供消息处理、向量存储、检索等基础原子能力,不限制业务的存储方案
-
业务层:根据需求组装组件,实现不同类型的记忆:
-
基础会话记忆:通过 Session+Store 实现当前对话的持久化,支持跨进程恢复
-
长期向量记忆:通过向量存储 + 检索实现跨会话的语义记忆,突破上下文限制
-
你可以根据自己的业务需求,选择合适的记忆方案,也可以在此基础上扩展更复杂的能力,比如记忆摘要、记忆过期清理、记忆搜索等。