一、一个尴尬的场景
你和 Agent 对话:
你:"我叫张三,是一名后端开发者,主要用 Go。"
Agent:"好的,张三,我记住了。"
你:"帮我看看我的项目里有没有内存泄漏。"
Agent 开始分析代码…
你:"顺便问一下,我叫什么名字?"
Agent:"抱歉,我不知道您的名字。请问您怎么称呼?"
这就是没有记忆的 Agent——每次对话都是一张白纸。
二、Agent 的记忆分层
人的记忆分三层:工作记忆、短期记忆、长期记忆。Agent 也一样。
|
L1 上下文窗口 |
工作记忆 |
当前对话的全部消息 |
LLM 上下文 |
对话结束即失 |
|
L2 会话记忆 |
短期记忆 |
当前会话的关键事实 |
内存 / Redis |
会话结束后失效 |
|
L3 长期记忆 |
长期记忆 |
跨会话的用户画像、知识 |
向量数据库 / 关系数据库 |
永久保存 |
三、L1:上下文窗口——最基础的记忆
这是每个 LLM 天然具备的"记忆"能力——它能看到当前对话中的所有消息。
3.1 窗口大小
|
GPT-5 |
256K tokens |
~17 万字 |
|
Claude Opus |
200K tokens |
~13 万字 |
|
DeepSeek-V3 |
128K tokens |
~8.5 万字 |
|
Gemini 2.0 |
1M tokens |
~67 万字 |
3.2 窗口满了怎么办
当对话太长超出上下文窗口,有三种策略:
策略一:滑动窗口(Sliding Window)
保留最近的 N 条消息,丢弃最早的消息。
[消息1] 用户:我叫张三
[消息2] 助手:你好张三
[消息3] 用户:帮我查天气
[消息4] 助手:北京晴
…
[消息99] 用户:我叫什么名字?
[消息100] 助手:???(因为消息1已被丢弃)
优点:实现简单
缺点:丢失早期关键信息
策略二:摘要压缩(Summarization)
当窗口即将满时,把早期消息压缩成一段摘要。
[摘要] 用户叫张三,后端开发者,正在做一个电商项目。
[消息50] 用户:帮我看看订单服务的代码
[消息51] 助手:好的,正在分析…
优点:保留关键信息
缺点:摘要可能丢失细节;摘要本身也占 token
策略三:结构化记忆(Structured Memory)
把关键信息提取成结构化数据,存在上下文之外。
只在需要时注入回上下文。
{
"user_profile": {
"name": "张三",
"role": "后端开发者",
"tech_stack": ["Go", "PostgreSQL"],
"project": "电商平台"
},
"conversation_state": {
"current_topic": "订单服务代码审查",
"files_reviewed": ["internal/order/service.go"],
"pending_actions": ["检查内存泄漏"]
}
}
优点:精确、节省 token
缺点:需要额外的提取逻辑
四、L2:会话记忆——让 Agent 记得住"刚才的事"
会话记忆是 Agent 在当前对话中提取和维护的关键事实。
4.1 实现方式
type SessionMemory struct {
sessionID string
facts map[string]string // key-value 事实存储
history []Message // 对话历史摘要
}
func (sm *SessionMemory) ExtractFacts(message string) {
// 调用 LLM 从消息中提取事实
// "我叫张三" → {"user.name": "张三"}
// "我用 Go 开发" → {"user.tech_stack": "Go"}
}
func (sm *SessionMemory) GetFact(key string) (string, bool) {
val, ok := sm.facts[key]
return val, ok
}
func (sm *SessionMemory) InjectContext() string {
// 把记忆注入到系统提示词中
return fmt.Sprintf("用户信息:姓名 %s,技术栈 %s",
sm.facts["user.name"], sm.facts["user.tech_stack"])
}
4.2 事实提取的策略
不是所有信息都值得记住。好的 Agent 会判断:
值得记住的信息:
-
用户的身份信息(姓名、职位、公司)
-
用户的偏好("我不喜欢 Python"、"我喜欢简洁的代码")
-
正在进行中的任务("我在重构订单模块")
-
重要的约定("下午三点开会")
不值得记住的信息:
-
临时的闲聊("今天天气不错")
-
一次性的请求("帮我算一下 123 * 456")
-
情绪化的表达("气死我了")
五、L3:长期记忆——跨会话的 Agent 大脑
长期记忆让 Agent 能在不同会话之间记住用户。这是 Agent 真正变得"懂你"的关键。
5.1 存储选型
|
向量数据库 |
语义搜索、相似度匹配 |
灵活、支持模糊检索 |
需要 embedding 模型 |
|
关系数据库 |
结构化事实 |
精确、事务支持 |
不支持语义搜索 |
|
Redis |
高频访问的短期事实 |
极快 |
持久化需额外配置 |
|
文件系统 |
小型项目、原型 |
零依赖 |
不适合大规模 |
5.2 向量数据库的工作流程
Step 1: 提取事实 → "用户张三,后端开发者,技术栈 Go"
Step 2: 生成向量 → [0.12, 0.87, 0.33, …] (768维)
Step 3: 存入向量库 → 关联 user_id=10086
Step 4: 新会话开始时 → 用 user_id 检索相关记忆
Step 5: 召回最相关的 K 条记忆 → 注入上下文
5.3 记忆的时效性
不是所有记忆都永远有效。需要引入衰减机制:
type MemoryItem struct {
Key string
Value string
CreatedAt time.Time
AccessedAt time.Time
AccessCount int
TTL time.Duration // 过期时间
}
// 衰减策略
func (m *MemoryItem) IsStale() bool {
// 1. 超过 TTL 的记忆视为过期
if time.Since(m.CreatedAt) > m.TTL {
return true
}
// 2. 很久没被访问的记忆降级
if time.Since(m.AccessedAt) > m.TTL / 2 {
return true
}
return false
}
衰减策略示例:
-
用户的临时偏好("我今天想吃辣的"):24 小时过期
-
用户的固定属性("我是后端开发者"):30 天刷新一次
-
用户的长期特征("我讨厌 PHP"):永不过期,但访问频率降低后权重下降
六、记忆的注入与提取
6.1 注入时机
记忆需要在正确的时机注入到上下文中:
会话开始时:注入用户画像、长期偏好
工具调用前后:注入当前任务的上下文
上下文窗口将满时:注入摘要
用户提及特定话题时:注入相关记忆
6.2 提取策略
从用户消息中提取记忆需要 LLM 辅助:
系统提示词的一部分:
"从用户的消息中提取关键信息,以 JSON 格式输出。
只提取事实性的、对未来对话有帮助的信息。
忽略情绪化表达和临时性请求。"
用户消息:"我叫张三,是个后端程序员。今天心情不太好,帮我看看这段代码哪里有问题。"
模型输出:
{
"extracted_facts": [
{"key": "user.name", "value": "张三"},
{"key": "user.role", "value": "后端程序员"}
],
"ignored": ["心情不太好"]
}
6.3 冲突解决
当新信息和旧记忆冲突时怎么办?
旧记忆:用户叫张三
新消息:"其实我不叫张三,我叫李四"
处理策略:
1. 置信度比较:如果新信息来自用户明确声明,覆盖旧记忆
2. 时间优先:较新的信息覆盖旧的
3. 人工确认:不确定时问用户"我记得您之前说叫张三,现在改成李四了吗?"
七、实战:给 Agent 加上三层记忆
package main
import (
"context"
"fmt"
"sync"
"time"
)
// L1: 上下文窗口(由 LLM SDK 自带,此处模拟)
type ContextWindow struct {
messages []string
maxSize int
}
func (cw *ContextWindow) Add(msg string) {
cw.messages = append(cw.messages, msg)
if len(cw.messages) > cw.maxSize {
cw.messages = cw.messages[len(cw.messages)-cw.maxSize:]
}
}
// L2: 会话记忆
type SessionMemory struct {
mu sync.RWMutex
facts map[string]string
}
func NewSessionMemory() *SessionMemory {
return &SessionMemory{facts: make(map[string]string)}
}
func (sm *SessionMemory) Set(key, value string) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.facts[key] = value
}
func (sm *SessionMemory) Get(key string) (string, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
v, ok := sm.facts[key]
return v, ok
}
func (sm *SessionMemory) GetAll() map[string]string {
sm.mu.RLock()
defer sm.mu.RUnlock()
copy := make(map[string]string)
for k, v := range sm.facts {
copy[k] = v
}
return copy
}
// L3: 长期记忆(简化版,用 map 模拟向量数据库)
type LongTermMemory struct {
mu sync.RWMutex
memories map[string][]MemoryItem // user_id → memories
}
type MemoryItem struct {
Key string
Value string
CreatedAt time.Time
TTL time.Duration
}
func NewLongTermMemory() *LongTermMemory {
return &LongTermMemory{memories: make(map[string][]MemoryItem)}
}
func (ltm *LongTermMemory) Store(userID, key, value string, ttl time.Duration) {
ltm.mu.Lock()
defer ltm.mu.Unlock()
ltm.memories[userID] = append(ltm.memories[userID], MemoryItem{
Key: key, Value: value, CreatedAt: time.Now(), TTL: ttl,
})
}
func (ltm *LongTermMemory) Recall(userID string) map[string]string {
ltm.mu.RLock()
defer ltm.mu.RUnlock()
result := make(map[string]string)
now := time.Now()
for _, item := range ltm.memories[userID] {
if now.Sub(item.CreatedAt) < item.TTL {
result[item.Key] = item.Value
}
}
return result
}
// Agent 整合三层记忆
type AgentWithMemory struct {
UserID string
Context *ContextWindow
Session *SessionMemory
LongTerm *LongTermMemory
}
func NewAgentWithMemory(userID string) *AgentWithMemory {
return &AgentWithMemory{
UserID: userID,
Context: &ContextWindow{maxSize: 20},
Session: NewSessionMemory(),
LongTerm: NewLongTermMemory(),
}
}
func (a *AgentWithMemory) Process(userMsg string) string {
// 1. 从用户消息中提取事实(模拟 LLM 提取)
if name, ok := extractName(userMsg); ok {
a.Session.Set("user.name", name)
a.LongTerm.Store(a.UserID, "user.name", name, 30 * 24*time.Hour)
}
// 2. 注入长期记忆到上下文
longTermFacts := a.LongTerm.Recall(a.UserID)
sessionFacts := a.Session.GetAll()
contextInject := "【已知信息】\\n"
for k, v := range longTermFacts {
contextInject += fmt.Sprintf("- %s: %s\\n", k, v)
}
for k, v := range sessionFacts {
if _, exists := longTermFacts[k]; !exists {
contextInject += fmt.Sprintf("- %s: %s (本次会话)\\n", k, v)
}
}
// 3. 模拟 LLM 回复
reply := fmt.Sprintf("%s\\n用户说: %s\\n(Agent 基于记忆做出回应)", contextInject, userMsg)
// 4. 更新上下文窗口
a.Context.Add(fmt.Sprintf("用户: %s", userMsg))
a.Context.Add(fmt.Sprintf("助手: %s", reply))
return reply
}
// 简单的名字提取(演示用)
func extractName(msg string) (string, bool) {
var name string
n, _ := fmt.Sscanf(msg, "我叫%s", &name)
return name, n == 1
}
func main() {
agent := NewAgentWithMemory("user_10086")
// 第一次对话
r1 := agent.Process("你好,我叫张三")
fmt.Println("=== 第一次 ===")
fmt.Println(r1)
// 同一会话中的第二次对话
r2 := agent.Process("你还记得我叫什么吗?")
fmt.Println("\\n=== 第二次(同一会话)===")
fmt.Println(r2)
// 模拟新会话(新建 Agent,但长期记忆还在)
newSession := NewAgentWithMemory("user_10086")
newSession.LongTerm = agent.LongTerm // 共享长期记忆
r3 := newSession.Process("你知道我是谁吗?")
fmt.Println("\\n=== 第三次(新会话)===")
fmt.Println(r3)
}
八、记忆的安全与隐私
8.1 用户控制权
好的 Agent 应该让用户拥有对自己记忆的控制权:
-
查看:"我都记得关于你的哪些信息?"
-
删除:"把我上周说的那句话忘掉"
-
修改:"我之前说的不对,其实我用的是 Rust 不是 Go"
-
导出:"把我的记忆数据导出来"
8.2 敏感信息保护
-
密码、密钥、Token 等敏感信息绝不存入长期记忆
-
即使存入会话记忆,也要在会话结束时清除
-
记忆数据应加密存储(AES-256)
8.3 遗忘机制
GDPR 要求的"被遗忘权"在 Agent 中同样重要:
func (ltm *LongTermMemory) Forget(userID string, key string) {
ltm.mu.Lock()
defer ltm.mu.Unlock()
memories := ltm.memories[userID]
filtered := make([]MemoryItem, 0)
for _, item := range memories {
if item.Key != key {
filtered = append(filtered, item)
}
}
ltm.memories[userID] = filtered
}
九、课后实践
任务:实现一个带有三层记忆的 Agent,让它能跨会话记住用户的基本信息。
要求:
实现 L1 上下文窗口(滑动窗口,保留最近 10 轮对话)
实现 L2 会话记忆(从对话中提取关键事实)
实现 L3 长期记忆(用文件或 SQLite 持久化)
支持用户查询"你还记得关于我的什么信息"
进阶挑战:
实现记忆的衰减机制(长时间不访问的记忆权重降低)
实现记忆的冲突检测(新旧信息矛盾时如何处理)
把记忆系统接入 zz365.top 的工具链——比如用 Crontab 定时清理过期记忆
延伸思考:
-
如果多个用户共享一个 Agent(比如团队协作场景),记忆怎么隔离?
-
Agent 自己的"经验"(比如学会的新技能)应该存在哪一层?
-
如果 Agent 记错了,用户纠正后,怎么确保它不再犯同样的错误?
十、延伸阅读
-
论文:《MemGPT: Towards LLMs as Operating Systems》(2023)- 分层记忆的开创性工作
-
论文:《Generative Agents: Interactive Simulacra of Human Behavior》(2023)- 长期记忆在 Agent 中的应用
-
开源项目:Mem0 – 2025-2026 流行的 Agent 记忆层框架
-
工具:Chroma / Qdrant / Pinecone – 向量数据库选型参考
十一、下一讲预告
第5讲:编排——Agent 如何规划复杂任务,我们会深入探讨:Agent 怎么把一个复杂的任务拆解成子任务、怎么决定执行的先后顺序、怎么处理分支和并行,以及 DAG(有向无环图)在 Agent 编排中的应用。
网硕互联帮助中心





评论前必须登录!
注册