Skip to content

第7章 没记性的助理谁敢用

第6章我们让Agent学会调用工具了。但还有一个扎心的问题:你跟它聊了十分钟,转头它就把你叫什么、刚才聊了啥全忘了。没有记忆的Agent,就像一个每天失忆的助理——你敢用它吗?这一章,我们给Agent装上记性,用Eino Memory系统。


7.1 人的记忆分短期和长期,Agent也一样

7.1.1 三种记忆,三种用途

人的记忆不是一种东西——你有"刚才说了什么"的短期记忆,有"我叫什么名字"的长期记忆,还有"去年生日那天发生了什么"的情景记忆。

Agent也一样。它的记忆系统可以分成三种:

记忆类型存什么能记多久典型例子
短期记忆当前对话的上下文当前会话"你刚才说想看北京的天气"
长期记忆用户偏好、历史记录跨会话"你偏好经济舱、靠窗座位"
知识记忆外部文档、数据库持久化"公司的退款政策是7天内无条件退"

7.1.2 短期记忆:LLM的"脑容量"

短期记忆最简单——就是把之前的对话塞进 Prompt 里。

go
messages := []*schema.Message{
    schema.UserMessage("北京天气怎么样?"),
    schema.AssistantMessage("北京今天晴,25度。"),
    schema.UserMessage("那明天呢?"), // 没提"北京",但LLM能根据上文推断
}

LLM看到前面提到了"北京",就知道"明天"指的是"北京明天"。这就是短期记忆。

但问题很明显:每轮对话都塞进去,上下文窗口迟早被撑爆。GPT-4的上下文虽然有128K token,但如果一个用户跟你聊了100轮,全存进去——token费用高得离谱,而且前面的信息会被LLM"遗忘"(注意力稀释)。

所以短期记忆需要管理,后面会讲。

7.1.3 长期记忆:跨会话的"用户档案"

当你第二次打开Agent,它还记得你上次说过什么——这就是长期记忆。

最简单的实现:用数据库存用户的偏好。

go
// 第一次对话
SavePreference(ctx, "user_123", "seat", "靠窗")

// 第二次对话——Agent自动加载偏好
pref := GetPreference(ctx, "user_123", "seat")
// "靠窗"
// Agent自动说:"帮你搜了靠窗的座位"

长期记忆的核心:把用户说过的重要信息存下来,下次会话时自动加载。


7.2 Eino Memory:多轮对话不再失忆

7.2.1 三种 Memory 模式

Eino 提供了三种 Memory 模式,覆盖从简单到复杂的场景:

模式策略适用场景
BufferMemory保存全部对话简单场景,对话不长
WindowMemory只保留最近 N 轮控制上下文长度
SummaryMemory自动总结历史长对话、多轮交互
go
import (
    "github.com/cloudwego/eino/components/memory"
    "github.com/cloudwego/eino/compose"
)

// 模式1:BufferMemory——记住全部
bufMem := memory.NewBufferMemory(ctx, &memory.BufferMemoryConfig{
    MaxMessages: 20, // 最多保存20条消息
})

// 模式2:WindowMemory——滑动窗口
winMem := memory.NewWindowMemory(ctx, &memory.WindowMemoryConfig{
    WindowSize: 6, // 只保留最近6条(3轮对话)
})

// 模式3:SummaryMemory——自动总结
summaryMem := memory.NewSummaryMemory(ctx, &memory.SummaryMemoryConfig{
    Model:       chatModel,   // 用哪个LLM做总结
    MaxSummaryLen: 500,        // 摘要最多500字
})

// 把 Memory 绑定到 Agent
agent, _ := compose.NewAgent[[]*schema.Message, *schema.Message]().
    WithChatModel(chatModel).
    WithMemory(bufMem).    // 装上记忆
    WithTools(tools).
    Compile(ctx)

7.2.2 BufferMemory 实战:记住你的名字

go
// 创建带记忆的 Agent
agent, _ := compose.NewAgent[[]*schema.Message, *schema.Message]().
    WithChatModel(chatModel).
    WithMemory(memory.NewBufferMemory(ctx, &memory.BufferMemoryConfig{
        MaxMessages: 20,
    })).
    Compile(ctx)

// 第一轮
result1, _ := agent.Invoke(ctx, []*schema.Message{
    schema.UserMessage("我叫张三"),
})
fmt.Println(result1.Content) // "很高兴认识你,张三!"

// 第二轮——Agent还记得你叫张三
result2, _ := agent.Invoke(ctx, []*schema.Message{
    schema.UserMessage("我叫什么名字?"),
})
fmt.Println(result2.Content) // "你叫张三!"

7.2.3 WindowMemory:控制上下文长度

BufferMemory的问题是对话长了内存爆炸。WindowMemory只保留最近的N条:

go
winMem := memory.NewWindowMemory(ctx, &memory.WindowMemoryConfig{
    WindowSize: 4, // 只保留最近4条消息(最近2轮)
})

// 对话进行了10轮
// 第1-6轮的消息已被自动丢弃
// Memory 里只有第7-10轮

什么时候用WindowMemory:对话不需要完整历史,但需要知道"刚才说了什么"的场景。比如客服Agent——你不需要记住用户一个月前的问题,但需要记住本轮对话的上下文。


7.2.4 SummaryMemory:自动总结,保持精华

当你既不想记全部、又不想丢弃历史时——用 SummaryMemory。

go
summaryMem := memory.NewSummaryMemory(ctx, &memory.SummaryMemoryConfig{
    Model:         chatModel,
    MaxSummaryLen: 300,
})

// 对话过长时,SummaryMemory 自动把旧消息压缩成摘要
// Memory 里存的不是原始对话,而是:
// "用户叫张三,28岁,Go程序员,今天问了天气和新闻。"

7.3 让Agent"临时抱佛脚"也能答对——RAG初探

7.3.1 LLM的知识盲区

不管你的LLM多强,它都有一个致命缺陷:训练数据有截止日期

  • 任何LLM都不知道今天的新闻
  • 任何LLM都不知道你公司的内部文档
  • 你的产品手册、退款政策、FAQ——LLM从未见过

如果用户问"你们的退款政策是什么",Agent不能瞎编。这就是 RAG(检索增强生成) 的用武之地。

7.3.2 RAG的工作流程

RAG 的思路很简单:

回答问题前,先去知识库里找到相关资料,拿着资料再回答。

像一个开卷考试——你可以翻书,但不能瞎编。

7.3.3 Eino 的 RAG 实现

go
import (
    "github.com/cloudwego/eino/components/retriever"
    "github.com/cloudwego/eino/compose"
)

// 构建 RAG Chain
ragChain := compose.NewChain[map[string]any, *schema.Message]().
    // 第一步:检索相关文档
    AppendRetriever(retriever).
    // 第二步:把检索结果拼进 Prompt
    AppendLambda(func(ctx context.Context, 
        input map[string]any) (map[string]any, error) {
        docs := input["documents"]
        question := input["question"]
        return map[string]any{
            "prompt": fmt.Sprintf(
                "基于以下文档回答问题:\n%s\n\n问题:%s", 
                docs, question,
            ),
        }, nil
    }).
    // 第三步:发给 LLM 生成回答
    AppendChatModel(chatModel)

// 使用
result, _ := ragChain.Invoke(ctx, map[string]any{
    "question": "退款政策是什么?",
})

7.4 Agent的"记忆仓库"怎么设计

7.4.1 向量是什么

你可能注意到了,RAG里一直在说"向量"。那向量到底是什么?

向量就是语义的"坐标"。 意思相近的两句话,它们的向量位置也很近。

比如:

  • "今天天气真好"的向量和"外面阳光明媚"的向量很接近
  • "今天天气真好"的向量和"如何解微积分"的向量就离得很远

7.4.2 记忆架构的选型

三种记忆在工程上的落地方案:

记忆类型存储方案Go 实现
短期记忆内存 / RedisEino BufferMemory / WindowMemory
长期记忆PostgreSQL / MySQL自定义 struct + GORM
知识记忆(RAG)向量数据库Milvus / Qdrant / pgvector
go
// 长期记忆的简单实现
type UserPreference struct {
    UserID    string    `gorm:"index"`
    Key       string
    Value     string
    UpdatedAt time.Time
}

// 保存偏好
func SavePreference(db *gorm.DB, userID, key, value string) error {
    return db.Clauses(clause.OnConflict{
        UpdateAll: true, // 如果已存在就更新
    }).Create(&UserPreference{
        UserID: userID,
        Key:    key,
        Value:  value,
    }).Error
}

// 加载偏好
func GetPreference(db *gorm.DB, userID, key string) string {
    var pref UserPreference
    db.Where("user_id = ? AND key = ?", userID, key).First(&pref)
    return pref.Value
}

7.4.3 向量数据库选型

数据库适合场景Go SDK
Milvus企业级生产,海量数据官方 Go SDK,性能优秀
Qdrant中等规模,API 友好官方 Go SDK
pgvector已有 PostgreSQL 的团队通过 GORM 扩展

选择建议

  • 原型阶段:直接用 Eino 的 Memory 系统,不需要额外数据库
  • 生产环境:已经用 PostgreSQL 的团队优先考虑 pgvector,零运维增量成本

7.5 本章小结

这一章我们给Agent装上了"记忆":

  1. 三种记忆:短期记对话、长期记偏好、知识库查文档。各司其职,互不冲突。
  2. Eino Memory 三模式:BufferMemory 全记住、WindowMemory 滑动窗口、SummaryMemory 自动总结——一行 WithMemory() 绑定到 Agent。
  3. RAG 初探:回答问题前去知识库翻资料。问题→向量→检索→拼接→回答。Eino 的 Retriever + Chain 实现。
  4. 向量本质:语义的"坐标"。意思越近,向量距离越近。
  5. 记忆架构选型:短期用 Eino Memory,长期用 SQL 数据库,知识记忆用向量数据库。Go 生态齐全。

✅ 知识点检查

学完这一章,试试回答这几个问题:

  • [ ] Agent的三种记忆分别是什么?各解决什么问题?
  • [ ] Eino 的 BufferMemory、WindowMemory、SummaryMemory 有什么区别?什么时候用哪个?
  • [ ] RAG的五个步骤是什么?用自己的话描述一遍。
  • [ ] 向量在RAG中扮演什么角色?
  • [ ] 短期记忆、长期记忆、知识记忆分别用什么存储方案?

📚 延伸阅读


🎯 下一章预告

第8章,Agent有脑子、有手脚、有记性了,但还差一样——

"会规划才值钱。让Agent学会拆任务、做计划、错了能改——Eino Chain 和 Graph。"