AI Agent 进阶:智能记忆管理与 Token 优化

🎯 当你的 Agent 聊得越来越久,成本和性能问题就来了

前情回顾:上篇文章的"隐患"

在上一篇文章《从零开始手动实现 AI Agent》中,我们成功构建了一个基础的 AI Agent。它能够:

  • ✅ 与 LLM 进行对话
  • ✅ 维护对话历史,实现多轮对话
  • ✅ 保持上下文连贯性

但是,这个实现有一个致命的问题:

// 上一版的 ChatAgent 结构
type ChatAgent struct {
    APIKey     string
    APIURL     string
    Model      string
    Messages   []Message      // 💣 问题就在这里!
    HTTPClient *http.Client
}

问题是什么?

每次发送消息时,我们都会把完整的对话历史发送给 LLM:

func (ca *ChatAgent) SendMessage(userMessage string) (string, error) {
    // 不断追加消息...
    ca.Messages = append(ca.Messages, Message{...})
    
    request := ChatRequest{
        Model:    ca.Model,
        Messages: ca.Messages,  // 🔥 发送全部历史!
        Stream:   false,
    }
    // ...
}

这会导致三个严重问题

问题 影响 严重程度
Token 爆炸 对话越长,token 消耗指数级增长 🔴 严重
成本失控 API 按 token 计费,长对话=高账单 🔴 严重
上下文溢出 超过模型 context window 限制,直接报错 🔴 致命

一个真实的场景

假设你的 Agent 和用户进行了 50 轮对话:

第 1 轮:发送 1 条消息        → 消耗 ~100 tokens
第 2 轮:发送 3 条消息        → 消耗 ~300 tokens
第 3 轮:发送 5 条消息        → 消耗 ~500 tokens
...
第 50 轮:发送 99 条消息      → 消耗 ~10,000 tokens 😱

累计 token 消耗:不是线性增长,而是 O(n²) 级别的增长!

50 轮对话的总 token 消耗 ≈ 250,000 tokens

如果每 1000 tokens 收费 $0.01,这一次对话就要花 $2.5!【LLM 推理输入、输出都是有对应的 Token 计费的】


解决方案:智能记忆管理

本次更新引入了一套完整的智能记忆管理系统,核心思想是:

“记住重要的,压缩不重要的,丢弃冗余的”

新的架构设计

┌─────────────────────────────────────────────────────────────┐
│                    智能记忆管理系统                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────┐    ┌─────────────────┐                │
│  │  📚 FullHistory │    │ 🎯 Optimized    │                │
│  │   完整历史记忆   │───▶│    Messages     │───▶ 发送给 LLM │
│  │  (本地存储)     │    │   (优化后)       │                │
│  └─────────────────┘    └─────────────────┘                │
│           │                      ▲                          │
│           ▼                      │                          │
│  ┌─────────────────┐    ┌───────┴───────┐                  │
│  │  🔢 Token       │    │  📝 Summary   │                  │
│  │   Estimator     │    │   摘要系统     │                  │
│  │  token 估算器    │    │              │                  │
│  └─────────────────┘    └───────────────┘                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心改进:双轨记忆系统

// 新版 ChatAgent 结构
type ChatAgent struct {
    APIKey            string
    APIURL            string
    Model             string
    FullHistory       []Message  // 📚 完整历史(本地存储,永不丢失)
    OptimizedMessages []Message  // 🎯 优化后的消息(实际发送)
    HTTPClient        *http.Client
    MaxTokens         int        // ⚙️ 最大 token 限制
    KeepRecentRounds  int        // 🔢 保留最近几轮完整对话
    Summary           string     // 📝 旧消息的摘要
}

关键设计:

  1. FullHistory:完整记录所有对话,一条不丢
  2. OptimizedMessages:智能优化后的消息,用于实际发送
  3. Summary:将旧消息压缩成摘要,保留核心信息

实现详解

第一步:Token 估算器

在优化之前,我们需要知道当前消耗了多少 token:

// estimateTokens 粗略估算 token 数量
// 英文:1 token ≈ 4 字符
// 中文:1 token ≈ 1.5 字符
func (ca *ChatAgent) estimateTokens(text string) int {
    chineseChars := 0
    for _, r := range text {
        // 判断是否为中文字符(Unicode 范围)
        if r >= 0x4e00 && r <= 0x9fff {
            chineseChars++
        }
    }
    otherChars := len(text) - chineseChars
    return (chineseChars*2)/3 + otherChars/4
}

为什么要区分中英文?

Token 化对不同语言的效率不同:

  • 英文单词通常被完整保留或轻微切分
  • 中文每个字符可能独立成 token

准确估算 token 是优化的基础。

第二步:消息优化器

这是智能记忆管理的核心——根据 token 预算智能裁剪消息:

// optimizeMessages 优化消息列表,控制 token 数量
func (ca *ChatAgent) optimizeMessages() {
    // 如果只有系统消息,直接返回
    if len(ca.FullHistory) <= 1 {
        ca.OptimizedMessages = ca.FullHistory
        return
    }

    // 计算当前 token 数
    totalTokens := 0
    for _, msg := range ca.FullHistory {
        totalTokens += ca.estimateTokens(msg.Content)
    }

    // 如果 token 数在限制内,直接使用完整历史
    if totalTokens <= ca.MaxTokens {
        ca.OptimizedMessages = ca.FullHistory
        return
    }

    // 🔥 需要优化:保留系统消息 + 摘要 + 最近的对话
    optimized := []Message{ca.FullHistory[0]} // 系统消息

    // 添加摘要(如果有)
    if ca.Summary != "" {
        optimized = append(optimized, Message{
            Role:    "system",
            Content: "Previous conversation summary: " + ca.Summary,
        })
    }

    // 保留最近 N 轮对话(每轮包含 user + assistant 两条消息)
    keepCount := ca.KeepRecentRounds * 2
    if keepCount > len(ca.FullHistory)-1 {
        keepCount = len(ca.FullHistory) - 1
    }

    // 添加最近的消息
    if keepCount > 0 {
        recentMessages := ca.FullHistory[len(ca.FullHistory)-keepCount:]
        optimized = append(optimized, recentMessages...)
    }

    ca.OptimizedMessages = optimized
}

优化策略图解:

原始对话历史 (50 轮):
┌─────────────────────────────────────────────────────────┐
│ System │ U1 │ A1 │ U2 │ A2 │ ... │ U48 │ A48 │ U49 │ A49 │ U50 │ A50 │
└─────────────────────────────────────────────────────────┘
         └─────── 旧消息(压缩成摘要)────────┘ └── 保留最近 N 轮 ──┘

优化后 (发送给 LLM):
┌──────────────────────────────────────────┐
│ System │ Summary │ U49 │ A49 │ U50 │ A50 │
└──────────────────────────────────────────┘
    ↓         ↓           └───────────────┘
  人设      历史摘要        完整的最近对话

第三步:智能摘要生成

当对话超过限制时,自动调用 LLM 生成摘要:

// createSummary 将旧消息压缩成摘要
func (ca *ChatAgent) createSummary() error {
    // 计算需要摘要的消息范围
    keepCount := ca.KeepRecentRounds * 2
    if len(ca.FullHistory)-1 <= keepCount {
        return nil // 不需要摘要
    }

    // 获取需要摘要的旧消息
    oldMessages := ca.FullHistory[1 : len(ca.FullHistory)-keepCount]
    if len(oldMessages) == 0 {
        return nil
    }

    // 构建摘要请求
    summaryPrompt := "Please provide a concise summary of the following conversation...\n\n"
    for _, msg := range oldMessages {
        summaryPrompt += fmt.Sprintf("%s: %s\n", msg.Role, msg.Content)
    }

    // 创建临时请求来获取摘要
    summaryRequest := ChatRequest{
        Model: ca.Model,
        Messages: []Message{
            {
                Role:    "system",
                Content: "You are a helpful assistant that creates concise summaries...",
            },
            {
                Role:    "user",
                Content: summaryPrompt + "\n\nProvide a brief summary:",
            },
        },
        Stream: false,
    }

    // 发送请求并获取摘要...
    // ...
    
    ca.Summary = chatResp.Choices[0].Message.Content
    return nil
}

摘要的作用:

将 10 轮对话(可能 2000 tokens)压缩成 200 tokens 的摘要:

原始对话:
User: 我想学习 Go 语言
Assistant: Go 是一门很好的语言...(500字)
User: Go 和 Python 有什么区别?
Assistant: 主要区别在于...(800字)
User: 推荐学习路径?
Assistant: 建议你按以下步骤...(600字)
...(共 10 轮)

摘要后:
"用户正在学习 Go 语言,关注与 Python 的对比,已讨论了基础语法、
并发特性和推荐的学习路径。用户偏好实践性学习方式。"

第四步:优化后的发送流程

func (ca *ChatAgent) SendMessage(userMessage string) (string, error) {
    // 1️⃣ 添加到完整历史(永不丢失)
    ca.FullHistory = append(ca.FullHistory, Message{
        Role:    "user",
        Content: userMessage,
    })

    // 2️⃣ 优化消息
    ca.optimizeMessages()

    // 3️⃣ 检查是否需要创建摘要
    optimizedTokens := 0
    for _, msg := range ca.OptimizedMessages {
        optimizedTokens += ca.estimateTokens(msg.Content)
    }

    if optimizedTokens > ca.MaxTokens {
        // 创建/更新摘要
        if err := ca.createSummary(); err != nil {
            fmt.Printf("Warning: Failed to create summary: %v\n", err)
        } else {
            // 摘要成功,重新优化
            ca.optimizeMessages()
        }
    }

    // 4️⃣ 发送优化后的消息(而不是完整历史!)
    request := ChatRequest{
        Model:    ca.Model,
        Messages: ca.OptimizedMessages,  // 🎯 关键变化!
        Stream:   false,
    }
    
    // ... 发送请求 ...

    // 5️⃣ 将回复添加到完整历史
    ca.FullHistory = append(ca.FullHistory, Message{
        Role:    "assistant",
        Content: assistantMessage,
    })

    // 6️⃣ 更新优化列表
    ca.optimizeMessages()

    return assistantMessage, nil
}

流程图:

用户输入
    │
    ▼
┌───────────────┐
│ 添加到        │
│ FullHistory   │ ← 完整保存
└───────┬───────┘
        │
        ▼
┌───────────────┐
│ 优化消息      │ ← 智能裁剪
│ optimizeMsg() │
└───────┬───────┘
        │
        ▼
┌───────────────┐    超过限制    ┌───────────────┐
│ 检查 token    │ ──────────────▶│ 创建摘要      │
│ 是否超限      │                │ createSummary │
└───────┬───────┘                └───────┬───────┘
        │                                │
        │◀───────────────────────────────┘
        │
        ▼
┌───────────────┐
│ 发送优化后    │
│ 的消息给 LLM  │ ← 只发关键内容
└───────┬───────┘
        │
        ▼
    返回回复

新增功能:会话统计与调试

1. 统计指令 stats

查看当前会话的状态:

if input == "stats" {
    totalMsgs, tokens := ca.GetStats()
    fmt.Printf("数据统计:\n")
    fmt.Printf("   Total messages: %d\n", totalMsgs)
    fmt.Printf("   Estimated tokens: %d\n", tokens)
    fmt.Printf("   Summary: %s\n", func() string {
        if ca.Summary == "" {
            return "None"
        }
        return "Active (" + fmt.Sprintf("%d chars", len(ca.Summary)) + ")"
    }())
    continue
}

输出示例:

You: stats
数据统计:
   Total messages: 24
   Estimated tokens: 3456
   Summary: Active (234 chars)

2. 调试指令 get_optimize_messages

将优化后的消息导出到文件,方便调试:

if input == "get_optimize_messages" {
    // 导出优化后的消息到 text.txt
    ca.optimizeMessages()
    
    file, err := os.Create("text.txt")
    // ...
    
    for i, msg := range ca.OptimizedMessages {
        writer.WriteString(fmt.Sprintf("Message %d:\n", i+1))
        writer.WriteString(fmt.Sprintf("Role: %s\n", msg.Role))
        writer.WriteString(fmt.Sprintf("Content:\n%s\n", msg.Content))
    }
    
    // 添加统计信息
    writer.WriteString(fmt.Sprintf("Token saved: %d (%.1f%%)\n", 
        tokens-optimizedTokens,
        float64(tokens-optimizedTokens)/float64(tokens)*100))
    
    fmt.Printf("优化后的数据已经保存到 text.txt\n")
}

输出示例:

You: get_optimize_messages
优化后的数据已经保存到 text.txt
   Total messages: 5 (optimized from 25)
   Estimated tokens: 456 (saved 3000 tokens)

配置参数说明

新版 Agent 支持灵活配置:

// 使用默认配置
agent := NewChatAgent(apiKey, apiURL, model)

// 或者使用自定义配置
agent := NewChatAgentWithConfig(
    apiKey, 
    apiURL, 
    model,
    8000,  // MaxTokens: 最大 token 限制
    10,    // KeepRecentRounds: 保留最近 10 轮完整对话
)

配置建议

场景 MaxTokens KeepRecentRounds 说明
省钱模式 2000 3 最低成本,适合简单问答
标准模式 8000 10 平衡成本和上下文
长对话 16000 20 需要更多上下文
分析任务 32000 5 单次输入大,对话轮次少

效果对比

优化前 vs 优化后

假设进行 100 轮对话,每轮平均 200 tokens:

优化前(旧版):

第 100 轮发送的 tokens = 100 × 200 × 2 = 40,000 tokens
累计消耗 ≈ 2,000,000 tokens
成本 ≈ $20+

优化后(新版,保留最近 5 轮):

第 100 轮发送的 tokens = 摘要(500) + 5 × 200 × 2 = 2,500 tokens
累计消耗 ≈ 300,000 tokens
成本 ≈ $3

节省 85% 的成本! 🎉

功能对比表

功能 旧版 新版
多轮对话
记忆管理 ❌ 无限增长 ✅ 智能优化
Token 控制 ✅ 自动控制
摘要功能 ✅ AI 自动生成
成本优化 ✅ 节省 80%+
会话统计 ✅ stats 命令
调试工具 ✅ 导出功能
上下文窗口保护 ❌ 可能溢出 ✅ 自动保护

完整代码结构

┌────────────────────────────────────────────────────────────┐
│                    main.go 新结构                          │
├────────────────────────────────────────────────────────────┤
│  📦 package main                                           │
│  📥 import (...)                                           │
│                                                            │
│  📋 type Message struct { ... }                            │
│  📋 type ChatRequest struct { ... }                        │
│  📋 type ChatResponse struct { ... }                       │
│                                                            │
│  🤖 type ChatAgent struct {                                │
│      FullHistory       []Message  // 完整历史              │
│      OptimizedMessages []Message  // 优化后消息            │
│      MaxTokens         int        // token 限制           │
│      KeepRecentRounds  int        // 保留轮次             │
│      Summary           string     // 摘要                 │
│  }                                                         │
│                                                            │
│  🔧 func NewChatAgent(...)                                 │
│  🔧 func NewChatAgentWithConfig(...)  // 🆕 带配置创建     │
│  🔢 func estimateTokens(...)          // 🆕 token 估算     │
│  📝 func optimizeMessages(...)        // 🆕 消息优化       │
│  📝 func createSummary(...)           // 🆕 摘要生成       │
│  📨 func SendMessage(...)             // ⚡ 优化后的发送   │
│  🧹 func ClearHistory()                                    │
│  📊 func GetStats(...)                // 🆕 统计信息       │
│  🔄 func Run()                        // ⚡ 新增命令支持   │
│                                                            │
│  🚀 func main() { ... }                                    │
└────────────────────────────────────────────────────────────┘

总结

本次更新解决了什么问题?

  1. Token 爆炸 → 智能裁剪 + 自动摘要
  2. 成本失控 → 控制每次发送的 token 数量
  3. 上下文溢出 → MaxTokens 限制保护

核心设计思想

完整记忆 + 智能压缩 = 低成本 + 高质量
  • FullHistory:确保不丢失任何信息
  • Summary:用 AI 压缩旧信息,保留语义
  • OptimizedMessages:只发送必要内容

关于长 Token 压缩

这里给出的是最为简单的 Token 压缩方式,简单暴力,但是最终如何还是需要看业务场景中怎么压缩才能够保证 LLM 识别准确性:

  1. 文中是所有消息上下文都压缩成一个摘要
  2. 也可以参考 Minus 的压缩方式,可以几轮对话压缩出一个摘要,最终也是有一个摘要列表
  3. 对于一些原数据信息,比如 AI Agent 的设定、工具的调用描述、工具的调用结果等,这些数据需要保留原数据,不宜压缩

附录:命令速查

命令 功能
stats 查看会话统计(消息数、token 数、摘要状态)
get_optimize_messages 导出优化后的消息到 text.txt
clear 清空对话历史
exit / quit 退出程序

作者注:Token 优化是 AI Agent 工程化的重要一环。一个好的 Agent 不仅要"聪明",还要"省钱"。通过智能记忆管理,我们实现了两全其美——既保持了对话的连贯性,又大幅降低了成本。

Happy Coding! 🚀

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐