在前文基础上,本篇聚焦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_id | user_001 | 唯一绑定用户 |
| 基础属性 | skill/level | Go/新手 | 稳定的能力/身份特征 |
| 核心目标 | target | 完成 Gin+GORM 项目 | 长期不变的核心诉求 |
| 偏好/禁忌 | preference/forbid | 牛奶=低糖/苹果=不酸 | 个性化选择 |
| 最后更新时间 | update_time | 1718xxxxxxx(毫秒) | 便于更新/清理过时信息 |
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 的必备能力。
MiaoAll