第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 实现 |
|---|---|---|
| 短期记忆 | 内存 / Redis | Eino 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装上了"记忆":
- 三种记忆:短期记对话、长期记偏好、知识库查文档。各司其职,互不冲突。
- Eino Memory 三模式:BufferMemory 全记住、WindowMemory 滑动窗口、SummaryMemory 自动总结——一行
WithMemory()绑定到 Agent。 - RAG 初探:回答问题前去知识库翻资料。问题→向量→检索→拼接→回答。Eino 的 Retriever + Chain 实现。
- 向量本质:语义的"坐标"。意思越近,向量距离越近。
- 记忆架构选型:短期用 Eino Memory,长期用 SQL 数据库,知识记忆用向量数据库。Go 生态齐全。
✅ 知识点检查
学完这一章,试试回答这几个问题:
- [ ] Agent的三种记忆分别是什么?各解决什么问题?
- [ ] Eino 的 BufferMemory、WindowMemory、SummaryMemory 有什么区别?什么时候用哪个?
- [ ] RAG的五个步骤是什么?用自己的话描述一遍。
- [ ] 向量在RAG中扮演什么角色?
- [ ] 短期记忆、长期记忆、知识记忆分别用什么存储方案?
📚 延伸阅读
- Eino Memory 文档:https://www.cloudwego.io/zh/docs/eino/
- Milvus Go SDK:https://milvus.io/docs/go_sdk.md
- pgvector:https://github.com/pgvector/pgvector
- 本书配套源码:关注公众号「图解AI系列」免费领取
🎯 下一章预告
第8章,Agent有脑子、有手脚、有记性了,但还差一样——
"会规划才值钱。让Agent学会拆任务、做计划、错了能改——Eino Chain 和 Graph。"

