从零开始手动实现 AI Agent(二)
AI Agent 进阶:智能记忆管理与 Token 优化,当你的 Agent 聊得越来越久,成本和性能问题就来了
目录
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 // 📝 旧消息的摘要
}
关键设计:
- FullHistory:完整记录所有对话,一条不丢
- OptimizedMessages:智能优化后的消息,用于实际发送
- 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() { ... } │
└────────────────────────────────────────────────────────────┘
总结
本次更新解决了什么问题?
- Token 爆炸 → 智能裁剪 + 自动摘要
- 成本失控 → 控制每次发送的 token 数量
- 上下文溢出 → MaxTokens 限制保护
核心设计思想
完整记忆 + 智能压缩 = 低成本 + 高质量
- FullHistory:确保不丢失任何信息
- Summary:用 AI 压缩旧信息,保留语义
- OptimizedMessages:只发送必要内容
关于长 Token 压缩
这里给出的是最为简单的 Token 压缩方式,简单暴力,但是最终如何还是需要看业务场景中怎么压缩才能够保证 LLM 识别准确性:
- 文中是所有消息上下文都压缩成一个摘要
- 也可以参考 Minus 的压缩方式,可以几轮对话压缩出一个摘要,最终也是有一个摘要列表
- 对于一些原数据信息,比如 AI Agent 的设定、工具的调用描述、工具的调用结果等,这些数据需要保留原数据,不宜压缩
附录:命令速查
| 命令 | 功能 |
|---|---|
stats |
查看会话统计(消息数、token 数、摘要状态) |
get_optimize_messages |
导出优化后的消息到 text.txt |
clear |
清空对话历史 |
exit / quit |
退出程序 |
作者注:Token 优化是 AI Agent 工程化的重要一环。一个好的 Agent 不仅要"聪明",还要"省钱"。通过智能记忆管理,我们实现了两全其美——既保持了对话的连贯性,又大幅降低了成本。
Happy Coding! 🚀
更多推荐

所有评论(0)