云计算百科
云计算领域专业知识百科平台

第4讲:记忆层——让 Agent 不健忘


一、一个尴尬的场景

你和 Agent 对话:

你:"我叫张三,是一名后端开发者,主要用 Go。"

Agent:"好的,张三,我记住了。"

你:"帮我看看我的项目里有没有内存泄漏。"

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 编排中的应用。


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 第4讲:记忆层——让 Agent 不健忘
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!