第七篇:Memory Agent 实战——短期上下文+长期画像+摘要记忆

2026-04-02 70 0

在前文基础上,本篇聚焦Memory Agent(记忆型 Agent) 的核心设计逻辑——解决“把全量聊天历史一股脑塞给 LLM”的致命问题,落地「短期上下文、长期画像、摘要记忆」三类记忆体系,同时详解用户画像的设计思路,让 Agent 既能“记住关键信息”,又能“不背无用包袱”。


一、先避坑:为什么不能塞全量历史?

普通开发者做 Agent 时,最容易犯的错是把所有聊天记录都传给 LLM,这会引发 5 大核心问题:

问题具体影响
上下文窗口有限历史记录太长会超出 LLM 的上下文长度,关键信息反而放不下
Token 成本飙升每轮请求的 Token 数随历史递增,调用成本越来越高
噪声干扰决策几轮前的无关闲聊(如“中午吃什么”)会干扰当前任务判断(如“计算购物总价”)
旧信息过时用户的需求/状态会变(比如上周要低糖牛奶,这周要普通牛奶),旧信息会误导结果
隐私与安全风险聊天记录中的敏感信息(如手机号、地址)被带入模型,造成数据泄露

核心原则:好的记忆系统「提炼状态」,不「堆积聊天记录」——只存关键信息,而非全量对话。


二、三种记忆体系:各管一摊,协同工作

解决全量历史问题的核心是拆分记忆类型,让不同记忆承担不同职责,图片中明确了三类记忆的分工:

记忆类型存什么典型例子存储位置核心价值
短期上下文当前会话最近几轮的临时信息用户刚提的报错、本次购物的临时需求内存/Redis保证单次会话的上下文连贯
长期画像稳定不变的用户核心信息Go 新手、目标做 Gin 项目、偏好低糖牛奶关系型数据库(MySQL)实现跨会话的个性化服务
摘要记忆历史对话压缩出的关键状态“最近完成 Gin 接口设计,卡在 GORM 关联报错”数据库文本字段保留长期会话的关键信息,减少 Token 消耗

示例场景(直观理解)

用户和 Agent 聊“用 Gin+GORM 做项目”:

  • • 短期上下文:记住“用户刚贴的 GORM 报 no such table 错误”;
  • • 长期画像:记住“用户是 Go 新手,核心目标是完成 Gin+GORM 项目”;
  • • 摘要记忆:压缩历史对话为“用户完成接口设计,卡在 GORM 表不存在问题,已建议检查 AutoMigrate”。

三、核心设计 1:用户画像(长期记忆)怎么设计?

长期画像的核心是「稳定、精简、可扩展」,避免存储冗余信息,推荐按“核心维度+键值对”设计:

1. 画像核心字段(通用模板)

字段类型示例字段取值示例说明
用户标识user_iduser_001唯一绑定用户
基础属性skill/levelGo/新手稳定的能力/身份特征
核心目标target完成 Gin+GORM 项目长期不变的核心诉求
偏好/禁忌preference/forbid牛奶=低糖/苹果=不酸个性化选择
最后更新时间update_time1718xxxxxxx(毫秒)便于更新/清理过时信息

2. 画像提取逻辑

  • • 触发时机:用户消息包含“我是/我要/喜欢/不要”等关键词时,调用 LLM 提取画像;
  • • 提取规则:只提取「稳定信息」,临时需求(如“这次买普通牛奶”)不写入画像;
  • • 去重优化:重复提及同一偏好时,覆盖旧值(如“牛奶=低糖”→“牛奶=无糖”)。

3. 画像持久化

  • • 开发阶段:内存存储(如示例);
  • • 生产阶段:存 MySQL(建表)或 Redis(Key=user:{userID}:profile,Value=JSON)。

四、核心设计 2:摘要记忆怎么“压”出来?

短期上下文会越积越长,早期关键信息容易丢失,摘要记忆解决这个问题,有两种核心做法:

方法逻辑优点缺点
滑动窗口只保留最近 N 轮对话,更早的全删简单、无额外成本关键信息可能丢失(如第 3 轮的核心需求)
摘要压缩历史超长度时,用 LLM 压缩成关键状态保留核心信息,减少 Token多一次 LLM 调用,少量细节丢失

推荐摘要结构

按“目标-卡点-建议-下一步”结构化压缩,避免自由文本的冗余:

摘要字段示例值
最近目标完成 Gin + GORM 项目
最近卡点GORM 报 no such table
已给建议检查 AutoMigrate 是否漏注册 Model
下一步运行迁移,验证表结构

使用时将摘要序列化为文本,和长期画像一起传入 Prompt 即可。


五、完整 Memory Agent Demo

整合短期上下文、长期画像、摘要记忆,对齐前文代码风格,非流式调用,通俗易懂:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/sashabaranov/go-openai"
)

// ==================== 1. 三种记忆核心结构体 ====================

// ShortTermMemory 短期上下文:当前会话最近几轮的临时信息(滑动窗口)。
type ShortTermMemory struct {
    ConversationID string   // 会话ID
    Messages       []string // 最近N轮消息(滑动窗口)
    MaxWindowSize  int      // 滑动窗口最大长度(如3轮)
    CreateTime     int64    // 会话最近更新时间
}

// LongTermProfile 长期画像:用户稳定核心信息(跨会话保留)。
type LongTermProfile struct {
    UserID      string            `json:"user_id"`     // 用户唯一标识
    Skill       string            `json:"skill"`       // 技能/水平(如Go/新手)
    Target      string            `json:"target"`      // 核心目标
    Preferences map[string]string `json:"preferences"` // 偏好/禁忌
    UpdateTime  int64             `json:"update_time"` // 最后更新时间
}

// SummaryMemory 摘要记忆:历史对话压缩为结构化关键状态。
type SummaryMemory struct {
    UserID     string `json:"user_id"`     // 关联用户
    LastTarget string `json:"last_target"` // 最近目标
    LastBlock  string `json:"last_block"`  // 最近卡点
    Suggestion string `json:"suggestion"`  // 已给建议
    NextStep   string `json:"next_step"`   // 下一步行动
    CreateTime int64  `json:"create_time"` // 摘要生成时间
}

// MemoryAgent 整合三种记忆的 Agent。
// 支持交互式使用:Init → HandleMessage(可多次)→ Answer(可多次)
type MemoryAgent struct {
    UserID          string          // 当前用户ID
    ShortTermMem    ShortTermMemory // 短期上下文
    LongTermProfile LongTermProfile // 长期画像
    SummaryMem      SummaryMemory   // 摘要记忆
    FinalAnswer     string          // 最后一次回答
    TotalLatency    int64           // 累计耗时(毫秒)

    client *openai.Client // LLM 客户端,Init 时注入后复用,无需每次传参
    inited bool           // 是否已调用 Init,防止未初始化就使用
}

// ==================== 2. 通用工具:客户端创建 + JSON 容错提取 + LLM 调用 ====================

// newDeepSeekClient 创建 DeepSeek 客户端(内部使用,用户通过 Init 注入)。
func newDeepSeekClient() (*openai.Client, error) {
    apikey := os.Getenv("DEEPSEEK_API_KEY")
    if apikey == "" {
        return nil, fmt.Errorf("DEEPSEEK_API_KEY 环境变量未设置")
    }
    cfg := openai.DefaultConfig(apikey)
    cfg.BaseURL = "https://api.deepseek.com"
    return openai.NewClientWithConfig(cfg), nil
}

// extractJSONObject 从 LLM 输出中提取首个 '{' 到末尾 '}' 的子串,容忍代码块和说明文字。
func extractJSONObject(content string) string {
    s := strings.TrimSpace(content)
    s = strings.TrimPrefix(s, "```json")
    s = strings.TrimPrefix(s, "```")
    s = strings.TrimSuffix(s, "```")
    s = strings.TrimSpace(s)

    start := strings.Index(s, "{")
    end := strings.LastIndex(s, "}")
    if start == -1 || end == -1 || end <= start {
        return ""
    }
    return s[start : end+1]
}

// callLLMForJSON 调 LLM → 提取 JSON → 解析到 out(画像提取、摘要生成复用)。
func callLLMForJSON(client *openai.Client, systemPrompt, userContent string, out interface{}) error {
    resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
        Model: "deepseek-v4-flash", // DeepSeek 模型,若不存在可改 deepseek-chat
        Messages: []openai.ChatCompletionMessage{
            {Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
            {Role: openai.ChatMessageRoleUser, Content: userContent},
        },
        Temperature: 0.1,
    })
    if err != nil {
        return fmt.Errorf("LLM 调用失败:%w", err)
    }
    if len(resp.Choices) == 0 {
        return fmt.Errorf("LLM 未返回结果")
    }
    jsonStr := extractJSONObject(resp.Choices[0].Message.Content)
    if jsonStr == "" {
        return fmt.Errorf("未找到 JSON 对象,原始内容:%s", resp.Choices[0].Message.Content)
    }
    if err := json.Unmarshal([]byte(jsonStr), out); err != nil {
        return fmt.Errorf("JSON 解析失败:%w,原始内容:%s", err, jsonStr)
    }
    return nil
}

// callLLMForText 调 LLM → 直接返回 content(用于生成自然语言回答)。
func callLLMForText(client *openai.Client, systemPrompt, userContent string, temperature float32) (string, error) {
    resp, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
        Model: "deepseek-v4-flash",
        Messages: []openai.ChatCompletionMessage{
            {Role: openai.ChatMessageRoleSystem, Content: systemPrompt},
            {Role: openai.ChatMessageRoleUser, Content: userContent},
        },
        Temperature: temperature,
    })
    if err != nil {
        return "", fmt.Errorf("LLM 调用失败:%w", err)
    }
    if len(resp.Choices) == 0 {
        return "", fmt.Errorf("LLM 未返回结果")
    }
    return resp.Choices[0].Message.Content, nil
}

// ==================== 3. 初始化:Init + InitCheck ====================

// Init 初始化 Agent:创建客户端 + 初始化三种记忆。必须在 HandleMessage/Answer 之前调用一次。
func (a *MemoryAgent) Init() error {
    client, err := newDeepSeekClient()
    if err != nil {
        return err
    }
    a.client = client

    // 初始化三种记忆参数
    a.ShortTermMem.ConversationID = fmt.Sprintf("conv_%d", time.Now().UnixMilli())
    a.ShortTermMem.MaxWindowSize = 3
    a.LongTermProfile.UserID = a.UserID
    if a.LongTermProfile.Preferences == nil {
        a.LongTermProfile.Preferences = make(map[string]string)
    }
    a.SummaryMem.UserID = a.UserID
    a.inited = true
    return nil
}

// checkInited 校验 Init 是否调用过,未初始化返回可读错误。
func (a *MemoryAgent) checkInited() error {
    if !a.inited || a.client == nil {
        return fmt.Errorf("MemoryAgent 未初始化,请先调用 Init()")
    }
    return nil
}

// ==================== 4. 短期记忆:滑动窗口管理 ====================

// addShortTermMemory 向短期记忆追加一条消息,超出窗口大小则丢弃最早的。
// 追加后若达到/超过窗口大小,返回 true 提示调用方可以生成摘要。
func (a *MemoryAgent) addShortTermMemory(msg string) (windowFull bool) {
    a.ShortTermMem.Messages = append(a.ShortTermMem.Messages, msg)
    if len(a.ShortTermMem.Messages) > a.ShortTermMem.MaxWindowSize {
        a.ShortTermMem.Messages = a.ShortTermMem.Messages[1:]
        fmt.Printf("📥 短期记忆触发滑动窗口,保留最近 %d 轮\n", a.ShortTermMem.MaxWindowSize)
    }
    a.ShortTermMem.CreateTime = time.Now().UnixMilli()
    fmt.Printf("📥 新增短期记忆:%s\n", msg)
    return len(a.ShortTermMem.Messages) >= a.ShortTermMem.MaxWindowSize
}

// ==================== 5. 长期画像:用户画像提取与合并 ====================

const profileExtractPrompt = `你是用户画像提取助手,仅提取稳定不变的核心信息,返回JSON格式:
{
  "skill": "技能/水平(无则填空字符串)",
  "target": "核心目标(无则填空字符串)",
  "preferences": {"偏好项": "偏好值"}
}
规则:
1. 临时需求(如"这次买普通牛奶")不提取,只提取长期稳定的画像;
2. 只返回JSON,不要输出任何其他文字、不要使用代码块标记;
3. preferences 中重复的偏好项覆盖旧值。`

// updateLongTermProfile 调 LLM 提取画像并合并到长期记忆。仅非空字段更新。
func (a *MemoryAgent) updateLongTermProfile(userMsg string) error {
    var update struct {
        Skill       string            `json:"skill"`
        Target      string            `json:"target"`
        Preferences map[string]string `json:"preferences"`
    }
    if err := callLLMForJSON(a.client, profileExtractPrompt, userMsg, &update); err != nil {
        return fmt.Errorf("提取画像失败:%w", err)
    }
    if update.Skill != "" {
        a.LongTermProfile.Skill = update.Skill
    }
    if update.Target != "" {
        a.LongTermProfile.Target = update.Target
    }
    for k, v := range update.Preferences {
        a.LongTermProfile.Preferences[k] = v
    }
    a.LongTermProfile.UpdateTime = time.Now().UnixMilli()
    fmt.Printf("✅ 更新长期画像:skill=%s target=%s preferences=%v\n",
        a.LongTermProfile.Skill, a.LongTermProfile.Target, a.LongTermProfile.Preferences)
    return nil
}

// ==================== 6. 摘要记忆:历史对话压缩 ====================

const summaryGeneratePrompt = `你是对话摘要助手,将历史对话压缩为结构化关键状态,返回JSON:
{
  "last_target": "用户最近目标",
  "last_block": "用户最近卡点/问题",
  "suggestion": "已给的建议",
  "next_step": "下一步行动"
}
规则:
1. 只提取核心状态,不保留闲聊;
2. 无对应信息则填"无";
3. 只返回JSON,不要输出任何其他文字、不要使用代码块标记。`

// generateSummaryMemory 调 LLM 将短期记忆压缩为结构化摘要。
func (a *MemoryAgent) generateSummaryMemory() error {
    historyText := strings.Join(a.ShortTermMem.Messages, "\n")
    var update struct {
        LastTarget string `json:"last_target"`
        LastBlock  string `json:"last_block"`
        Suggestion string `json:"suggestion"`
        NextStep   string `json:"next_step"`
    }
    if err := callLLMForJSON(a.client, summaryGeneratePrompt, historyText, &update); err != nil {
        return fmt.Errorf("生成摘要失败:%w", err)
    }
    a.SummaryMem.LastTarget = update.LastTarget
    a.SummaryMem.LastBlock = update.LastBlock
    a.SummaryMem.Suggestion = update.Suggestion
    a.SummaryMem.NextStep = update.NextStep
    a.SummaryMem.CreateTime = time.Now().UnixMilli()
    fmt.Printf("✅ 生成摘要记忆:target=%s block=%s suggestion=%s next=%s\n",
        a.SummaryMem.LastTarget, a.SummaryMem.LastBlock, a.SummaryMem.Suggestion, a.SummaryMem.NextStep)
    return nil
}

// ==================== 7. 任务处理:整合三种记忆回答用户 ====================

const answerWithMemoryPrompt = `你是一个具备三种记忆的 Memory Agent,需要结合记忆上下文精准回答用户问题。

你将收到以下三段记忆上下文:
1. 【短期上下文】:当前会话最近几轮对话;
2. 【长期画像】:用户稳定的技能、目标、偏好;
3. 【摘要记忆】:历史对话压缩的关键状态。

回答规则:
1. 优先利用长期画像和摘要记忆理解用户背景;
2. 结合短期上下文给出连贯、针对性的回答;
3. 若用户问技术问题,按画像中的技能水平调整深度(如新手则讲解更细致);
4. 若用户问购物/偏好类问题,严格参考长期画像里的 preferences 做推荐;
5. 回答简洁明了,直接解决用户问题。`

// answerWithMemory 调 LLM 整合三种记忆回答用户最后一条问题。
func (a *MemoryAgent) answerWithMemory() error {
    if len(a.ShortTermMem.Messages) == 0 {
        return fmt.Errorf("短期记忆为空,尚无用户问题可回答")
    }
    shortTermStr := fmt.Sprintf("【短期上下文】(最近%d轮):\n%s",
        a.ShortTermMem.MaxWindowSize, strings.Join(a.ShortTermMem.Messages, "\n"))
    longTermStr := fmt.Sprintf("【长期画像】技能=%s,目标=%s,偏好=%v",
        a.LongTermProfile.Skill, a.LongTermProfile.Target, a.LongTermProfile.Preferences)
    summaryStr := fmt.Sprintf("【摘要记忆】目标=%s,卡点=%s,建议=%s,下一步=%s",
        a.SummaryMem.LastTarget, a.SummaryMem.LastBlock, a.SummaryMem.Suggestion, a.SummaryMem.NextStep)

    lastUserMsg := a.ShortTermMem.Messages[len(a.ShortTermMem.Messages)-1]
    userContent := fmt.Sprintf("%s\n\n%s\n\n%s\n\n用户当前问题:%s",
        shortTermStr, longTermStr, summaryStr, lastUserMsg)

    answer, err := callLLMForText(a.client, answerWithMemoryPrompt, userContent, 0.3)
    if err != nil {
        return fmt.Errorf("生成回答失败:%w", err)
    }
    a.FinalAnswer = answer
    fmt.Printf("✅ 结合三种记忆生成回答(%d字)\n", len(a.FinalAnswer))
    return nil
}

// ==================== 8. Memory Agent 公开 API:Init → HandleMessage → Answer ====================

// 画像更新触发关键词:用户消息含这些词时触发画像提取。
var profileTriggerWords = []string{"是", "要", "喜欢", "目标", "新手", "偏好", "买"}

// HandleMessage 处理单条用户消息:
// 1. 写入短期记忆(滑动窗口);
// 2. 命中关键词则提取并更新长期画像;
// 3. 达到/超过窗口大小则自动生成摘要记忆。
// 错误不中断流程,仅打印告警(记忆机制允许部分失败)。
func (a *MemoryAgent) HandleMessage(msg string) error {
    start := time.Now()
    if err := a.checkInited(); err != nil {
        return err
    }

    // 1. 写入短期记忆
    windowFull := a.addShortTermMemory(msg)

    // 2. 触发画像更新(命中关键词)
    for _, word := range profileTriggerWords {
        if strings.Contains(msg, word) {
            if err := a.updateLongTermProfile(msg); err != nil {
                fmt.Printf("⚠️ 更新画像失败:%v\n", err)
            }
            break
        }
    }

    // 3. 短期记忆满窗口 → 自动生成摘要记忆
    if windowFull {
        fmt.Println("\n----- 短期记忆已满,自动生成摘要记忆 -----")
        if err := a.generateSummaryMemory(); err != nil {
            fmt.Printf("⚠️ 生成摘要失败:%v\n", err)
        }
    }

    a.TotalLatency += time.Since(start).Milliseconds()
    return nil
}

// Answer 基于当前所有记忆回答用户最后一条问题,返回回答字符串。
// 可在任意时刻调用(不需要等到所有消息处理完)。
func (a *MemoryAgent) Answer() (string, error) {
    start := time.Now()
    if err := a.checkInited(); err != nil {
        return "", err
    }

    fmt.Println("\n----- 结合三种记忆回答用户 -----")
    if err := a.answerWithMemory(); err != nil {
        a.TotalLatency += time.Since(start).Milliseconds()
        return "", err
    }
    a.TotalLatency += time.Since(start).Milliseconds()
    return a.FinalAnswer, nil
}

// RunMemory 兼容入口:一次性处理一组用户消息并返回最终回答。
// 等同于 Init → 循环 HandleMessage → Answer。
func (a *MemoryAgent) RunMemory(userMsgs []string) (string, error) {
    fmt.Println("===== Memory Agent 启动 =====")
    fmt.Printf("当前用户:%s\n\n", a.UserID)

    if err := a.Init(); err != nil {
        a.FinalAnswer = err.Error()
        return a.FinalAnswer, err
    }
    for i, msg := range userMsgs {
        fmt.Printf("----- 处理用户消息 %d -----\n", i+1)
        if err := a.HandleMessage(msg); err != nil {
            return "", err
        }
    }
    return a.Answer()
}

// ==================== 主函数:交互式演示 ====================
// 使用流程:
// 1. Init 初始化(创建客户端+记忆参数)
// 2. HandleMessage(第1-3条:建立技能画像+购物偏好+技术问题摘要)
// 3. Answer 回答最后一条问题
// 4. HandleMessage(第4条:新问题"苹果甜/酸买哪种")—— 复用步骤2积累的记忆
// 5. Answer 回答,验证长期画像的「苹果不要酸的」偏好被正确召回
// ====================

func main() {
    agent := &MemoryAgent{UserID: "user_001"}
    fmt.Println("===== Memory Agent 交互式演示 =====")
    fmt.Printf("当前用户:%s\n\n", agent.UserID)

    // 步骤1:初始化 Agent
    if err := agent.Init(); err != nil {
        fmt.Printf("初始化失败:%v\n", err)
        return
    }

    // 步骤2:先输入 3 条消息建立记忆
    buildUp := []string{
        "我是Go新手,目标做一个Gin+GORM的项目",    // 触发画像:skill=Go新手, target=Gin+GORM项目
        "GORM报no such table错误,怎么解决?", // 短期记忆,后续摘要素材
        "我买牛奶只买低糖的,苹果不要酸的",           // 触发画像:preferences{牛奶:低糖, 苹果:不要酸的}
    }
    for i, msg := range buildUp {
        fmt.Printf("----- 用户第 %d 条消息 -----\n", i+1)
        if err := agent.HandleMessage(msg); err != nil {
            fmt.Printf("HandleMessage 失败:%v\n", err)
        }
    }

    // 步骤3:回答第 3 条问题(GORM no such table)
    ans1, err := agent.Answer()
    if err != nil {
        fmt.Printf("⚠️ 生成回答失败:%v\n", err)
    } else {
        fmt.Println("\n===== 第 3 条问题的回答(GORM 报错解决)=====")
        fmt.Println(ans1)
    }

    // 步骤4:用户追加新问题(购买决策,依赖长期画像里的「苹果不要酸的」偏好)
    newQuestion := "在超市里苹果有甜的有酸的,我买哪种?"
    fmt.Println("\n\n================ 用户新提问 ================")
    fmt.Printf("用户:%s\n\n", newQuestion)
    if err := agent.HandleMessage(newQuestion); err != nil {
        fmt.Printf("HandleMessage 失败:%v\n", err)
    }

    // 步骤5:再次回答,验证偏好被正确召回
    ans2, err := agent.Answer()
    if err != nil {
        fmt.Printf("⚠️ 生成回答失败:%v\n", err)
        return
    }
    fmt.Println("\n===== 新问题的回答(应建议买甜苹果)=====")
    fmt.Println(ans2)

    fmt.Printf("\n📌 会话信息:会话ID=%s 短期记忆=%d条 画像偏好=%d项 总耗时=%dms\n",
        agent.ShortTermMem.ConversationID, len(agent.ShortTermMem.Messages),
        len(agent.LongTermProfile.Preferences), agent.TotalLatency)
}

六、核心逻辑解析

1. 短期记忆:滑动窗口控长度

  • • 核心:设置MaxWindowSize(如 3 轮),超出则删除最早的消息,避免历史无限堆积;
  • • 场景:适合保存“本次会话的临时问题/需求”,保证上下文连贯的同时控制 Token 数。

2. 长期画像:结构化存稳定信息

  • • 设计原则:只存“不变/慢变”的信息,比如“Go 新手”“低糖牛奶偏好”,临时需求不写入;
  • • 提取逻辑:通过 LLM 自动提取,避免手动写规则(如“我是 Go 新手”→skill=Go/新手);
  • • 持久化:示例中是内存,生产环境可存 MySQL(建表user_profile)或 Redis(JSON 格式)。

3. 摘要记忆:结构化压缩历史

  • • 触发时机:短期记忆满窗口时生成,避免全量历史传入 LLM;
  • • 压缩逻辑:按“目标-卡点-建议-下一步”结构化生成,比自由文本更精简、更易复用;
  • • 价值:既保留历史关键信息,又大幅减少 Token 消耗(比如 10 轮对话压缩成 4 个字段)。

七、运行效果(三种记忆协同工作)

===== Memory Agent 交互式演示 =====
当前用户:user_001

----- 用户第 1 条消息 -----
📥 新增短期记忆:我是Go新手,目标做一个Gin+GORM的项目
✅ 更新长期画像:skill=Go新手 target=做一个Gin+GORM的项目 preferences=map[]
----- 用户第 2 条消息 -----
📥 新增短期记忆:GORM报no such table错误,怎么解决?
----- 用户第 3 条消息 -----
📥 新增短期记忆:我买牛奶只买低糖的,苹果不要酸的
✅ 更新长期画像:skill=Go新手 target=做一个Gin+GORM的项目 preferences=map[牛奶:低糖 苹果:不酸]

----- 短期记忆已满,自动生成摘要记忆 -----
✅ 生成摘要记忆:target=做一个Gin+GORM的项目 block=GORM报no such table错误 suggestion=无 next=无

----- 结合三种记忆回答用户 -----
✅ 结合三种记忆生成回答(507字)

===== 第 3 条问题的回答(GORM 报错解决)=====
好的,已记住你的偏好:**牛奶要低糖,苹果要不酸的**。下次推荐会按这个来。

另外,你之前遇到的 GORM `no such table` 错误,最常见原因是**还没有创建对应的数据库表**。GORM 可以在初始化数据库后自动建表,例如:

 db.AutoMigrate(&YourModel{})

这样启动项目时会自动创建缺失的表,避免 `no such table` 问题。如果已经加了还是报错,检查一下连接的数据库名和模型定义是否正确。


================ 用户新提问 ================
用户:在超市里苹果有甜的有酸的,我买哪种?

📥 短期记忆触发滑动窗口,保留最近 3 轮
📥 新增短期记忆:在超市里苹果有甜的有酸的,我买哪种?
✅ 更新长期画像:skill=Go新手 target=做一个Gin+GORM的项目 preferences=map[牛奶:低糖 苹果:不酸]

----- 短期记忆已满,自动生成摘要记忆 -----
✅ 生成摘要记忆:target=确定购买哪种苹果 block=不确定哪些苹果是甜的而非酸的 suggestion=选择甜苹果,可通过询问店员或查看标签确认 next=购买甜苹果

----- 结合三种记忆回答用户 -----
✅ 结合三种记忆生成回答(133字)

===== 新问题的回答(应建议买甜苹果)=====
你应该买**甜苹果**,因为你的长期偏好是“苹果不酸”。如果不确定哪种甜,可以看标签或问店员。

📌 会话信息:会话ID=conv_1786529827018 短期记忆=3条 画像偏好=2项 总耗时=21959ms

八、拓展落地建议

1. 记忆存储优化

  • • 短期记忆:Redis 设置过期时间(如 30 分钟),自动清理无效会话;
  • • 长期画像:MySQL 分表存储(按 user_id 哈希),支持海量用户;
  • • 摘要记忆:和画像存在同一张表,新增summary文本字段,便于关联查询。

2. 隐私与安全

  • • 存储前脱敏:画像/摘要中过滤手机号、地址等敏感信息;
  • • 调用时过滤:短期记忆中自动屏蔽敏感词,避免传入 LLM。

3. 记忆更新策略

  • • 长期画像:定期(如每周)让 LLM 校验画像是否过时(如“用户是否还是 Go 新手”);
  • • 摘要记忆:每轮会话结束后更新,而非仅满窗口时更新。

九、核心总结

1. 记忆 Agent 的核心原则

  • • 不堆全量历史:通过“短期+长期+摘要”拆分记忆,只传关键信息给 LLM;
  • • 结构化存储:画像和摘要都用固定字段,避免自由文本的冗余和噪声;
  • • 按需触发:画像/摘要仅在必要时更新,减少 LLM 调用成本。

2. 三种记忆的核心价值

  • • 短期上下文:保单次会话连贯;
  • • 长期画像:实现跨会话个性化;
  • • 摘要记忆:平衡“保留关键信息”和“控制 Token 成本”。

3. 用户画像设计关键

  • • 只存稳定信息,不存临时需求;
  • • 按“基础属性+核心目标+偏好”结构化设计,易扩展;
  • • 自动提取+定期校验,减少人工维护成本。

Memory Agent 的核心不是“记得多”,而是“记得准、用得省”——通过三类记忆的协同,让 Agent 既懂用户的长期特征,又能跟上当前会话的节奏,同时控制调用成本和隐私风险,这是工业级 Agent 的必备能力。

相关文章

第十六篇:Eino入门——核心组件总览与场景选型总结
第十五篇:Eino ChatModelAgent 模型自己决定是否调用工具
第十四篇:Eino ToolsNode 工具调用执行器
第十三篇:InvokableTool-把Go函数包装成模型可调用的工具
第十二篇:Eino ToolInfo工具-给模型看的说明书
第十一篇:Eino Message与Prompt上下文编排

发布评论