前言
在上一篇文章中,我们通过异步压缩优化,将用户感知延迟从6.17秒降低到0.98秒,实现了6.3倍的性能提升。但在实际测试中,我发现了新的性能瓶颈:每次对话都要查询PostgreSQL读取历史上下文,高并发场景下数据库成为瓶颈。
本文将分享如何通过Redis缓存层,将读操作性能提升1.89倍,吞吐量提升2倍。
本文是《AI Agent记忆系统实战》系列第三篇,完整代码已开源。
一、为什么需要缓存层?
1.1 真实Agent的工作流程
让我们先看看一个真实的AI Agent是如何工作的:
# 每轮对话的标准流程
async def agent_chat(user_id, session_id, user_message):
# 1. 读取历史上下文(关键步骤!)
history = await memory.query_messages(user_id, session_id, limit=50)
# 2. 构造prompt
prompt = build_prompt(history, user_message)
# 3. 调用LLM
response = await llm.chat(prompt)
# 4. 保存新消息
await memory.add_message(user_id, session_id, "assistant", response)
return response
关键发现:每次对话都要读取历史上下文(50-100条消息),这是一个高频读取操作。
1.2 PostgreSQL的性能瓶颈
在没有缓存的情况下,每次读取都要查询PostgreSQL:
-- 每次对话都要执行这个查询
SELECT * FROM messages
WHERE conversation_id = ?
ORDER BY created_at DESC
LIMIT 50;
问题出现在哪?
| 场景 | 问题 |
|---|---|
| 单用户长对话 | 查询延迟随消息量增长(100条 vs 10000条) |
| 高并发 | 100个用户同时查询,数据库连接池耗尽 |
| 重复查询 | 用户连续对话,相同数据被反复查询 |
1.3 实测数据对比
我进行了一个真实场景测试:20个用户并发,每用户500条历史,50轮对话,80%读操作
| 指标 | 无缓存 | Redis缓存 | 提升 |
|---|---|---|---|
| P50延迟 | 11.14ms | 5.36ms | 2.08x ⚡ |
| 读操作平均 | 12.65ms | 6.70ms | 1.89x |
| 吞吐量 | 1,688/s | 3,364/s | 1.99x 🚀 |
| 缓存命中率 | 0% | 100% | - |
结论:在读密集场景下,Redis缓存能将性能提升2倍!
二、三层记忆架构设计
2.1 整体架构
┌─────────────────────────────────────────────────────┐
│ AI Agent 对话 │
└─────────────────────────────────────────────────────┘
↓
┌──────────────────────────────┐
│ 短期记忆 (Deque) │
│ - 最近10轮对话 │
│ - 内存存储,立即访问 │
│ - 响应时间: <1ms │
└──────────────────────────────┘
↓ 溢出
┌──────────────────────────────┐
│ 中期记忆 (PostgreSQL) │
│ - 完整对话历史 │
│ - 持久化存储 │
│ - 响应时间: 10-50ms │
└──────────────────────────────┘
↑
【新增】
┌──────────────────────────────┐
│ 缓存层 (Redis) │
│ - 热点数据缓存 │
│ - TTL自动过期 │
│ - 响应时间: 1-2ms │
└──────────────────────────────┘
2.2 读写流程
读取路径(三级缓存):
用户请求
↓
Deque(短期记忆)
↓ miss
Redis(缓存层)
↓ miss
PostgreSQL(持久层)
↓
写回Redis
↓
返回结果
写入路径(双写一致):
新消息
↓
写入Deque
↓ 溢出?
同时写入PostgreSQL + Redis
2.3 关键设计决策
Q1: 为什么选Redis而不是Memcached?
| 特性 | Redis | Memcached |
|---|---|---|
| 数据结构 | 支持Hash、List、Set | 只支持String |
| 持久化 | 支持RDB/AOF | 不支持 |
| TTL粒度 | 键级别 | 键级别 |
| 适用场景 | 复杂数据结构 | 简单KV |
选择Redis的原因:
- ✅ 用户画像需要Hash结构
- ✅ 支持持久化,避免缓存雪崩
- ✅ 更丰富的数据类型
Q2: 为什么消息用JSON,用户画像用Hash?
# 消息列表:JSON(整体操作)
messages_key = "messages:user_123:session_456"
# 存储:[{role: "user", content: "..."}, {...}]
# 特点:每次读取完整列表,适合用JSON
# 用户画像:Hash(字段操作)
profile_key = "profile:user_123:session_456"
# 存储:{name: "张三", age: "25", interest: "编程"}
# 特点:可以单独更新某个字段,适合用Hash
Q3: TTL如何设置?
# 根据访问频率设置不同TTL
cache_ttl = {
"messages": 3600, # 1小时(高频访问)
"profile": 10800, # 3小时(中频访问)
"summary": 7200 # 2小时(低频访问)
}
设置原则:
- 访问频率高 → TTL短(数据更新快)
- 访问频率低 → TTL长(减少数据库压力)
三、Redis存储层实现
3.1 核心代码
# src/memory/redis_storage.py
import json
import aioredis
from typing import Dict, List, Optional
class RedisStorage:
"""Redis缓存存储"""
def __init__(
self,
host: str = "localhost",
port: int = 6379,
db: int = 0,
default_ttl: int = 3600
):
self.host = host
self.port = port
self.db = db
self.default_ttl = default_ttl
self.redis = None
async def connect(self):
"""连接Redis"""
self.redis = await aioredis.from_url(
f"redis://{self.host}:{self.port}/{self.db}",
encoding="utf-8",
decode_responses=True
)
async def close(self):
"""关闭连接"""
if self.redis:
await self.redis.close()
# ========== 消息缓存(String/JSON) ==========
def _message_list_key(self, user_id: str, session_id: str) -> str:
"""消息列表的key"""
return f"messages:{user_id}:{session_id}"
async def cache_messages(
self,
user_id: str,
session_id: str,
messages: List[Dict],
ttl: Optional[int] = None
):
"""缓存消息列表"""
key = self._message_list_key(user_id, session_id)
json_messages = json.dumps(messages, ensure_ascii=False)
await self.redis.setex(key, ttl or self.default_ttl, json_messages)
async def get_cached_messages(
self,
user_id: str,
session_id: str
) -> Optional[List[Dict]]:
"""获取缓存的消息"""
key = self._message_list_key(user_id, session_id)
value = await self.redis.get(key)
if value:
return json.loads(value)
return None
# ========== 用户画像缓存(Hash) ==========
def _profile_key(self, user_id: str, session_id: str) -> str:
"""用户画像的key"""
return f"profile:{user_id}:{session_id}"
async def cache_profile(
self,
user_id: str,
session_id: str,
profile: Dict[str, str],
ttl: Optional[int] = None
):
"""缓存用户画像"""
key = self._profile_key(user_id, session_id)
await self.redis.hset(key, mapping=profile)
await self.redis.expire(key, ttl or self.default_ttl)
async def get_cached_profile(
self,
user_id: str,
session_id: str
) -> Optional[Dict[str, str]]:
"""获取缓存的用户画像"""
key = self._profile_key(user_id, session_id)
profile = await self.redis.hgetall(key)
return profile if profile else None
3.2 为什么消息用JSON而不是List?
考虑过的方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| JSON String ⭐ | 原子操作,序列化简单 | 需要整体读写 |
| Redis List | 支持部分读取 | 复杂对象需要额外序列化 |
| Redis Stream | 天然支持时间序列 | 查询不够灵活 |
最终选择JSON的原因:
# 场景:Agent需要读取最近50条消息
# 方案1:JSON - 一次读取
messages = json.loads(await redis.get(key))[-50:]
# 方案2:List - 需要多次操作
messages = await redis.lrange(key, -50, -1)
# 还需要反序列化每条消息
messages = [json.loads(m) for m in messages]
JSON方案在我们的场景下更简单、更高效。
四、集成到中期记忆
4.1 改造MidTermMemory
# src/memory/mid_term_with_redis.py
class MidTermMemoryWithRedis:
"""带Redis缓存的中期记忆"""
def __init__(
self,
pg_storage: PostgreSQLStorage,
redis_storage: RedisStorage,
max_turns: int = 10,
cache_ttl: int = 3600
):
self.short_term = ShortTermMemory(max_turns=max_turns)
self.pg_storage = pg_storage
self.redis = redis_storage
self.cache_ttl = cache_ttl
# 缓存命中率统计
self.cache_hits = 0
self.cache_misses = 0
async def add_message(self, user_id, session_id, role, content, tokens=None):
"""添加消息(双写策略)"""
# 1. 添加到短期记忆
self.short_term.add_message(role, content)
# 2. 检查是否溢出
overflow = self.short_term.check_overflow()
if overflow:
# 3. 溢出消息保存到PostgreSQL
conv = await self.pg_storage.get_or_create_conversation(
user_id, session_id
)
await self.pg_storage.add_messages(conv.id, overflow)
# 4. 同时更新Redis缓存
all_messages = await self.pg_storage.query_messages(conv.id)
messages_dict = [
{"role": m.role, "content": m.content}
for m in all_messages
]
await self.redis.cache_messages(
user_id, session_id, messages_dict, ttl=self.cache_ttl
)
async def query_messages(self, user_id, session_id, limit=None):
"""查询消息(三级缓存)"""
# 1. 先查Redis
cached = await self.redis.get_cached_messages(user_id, session_id)
if cached is not None:
self.cache_hits += 1
return cached[-limit:] if limit else cached
# 2. Redis miss,查PostgreSQL
self.cache_misses += 1
conv = await self.pg_storage.get_or_create_conversation(
user_id, session_id
)
pg_messages = await self.pg_storage.query_messages(conv.id, limit=limit)
# 3. 写回Redis
messages_dict = [
{"role": m.role, "content": m.content}
for m in pg_messages
]
if messages_dict:
await self.redis.cache_messages(
user_id, session_id, messages_dict, ttl=self.cache_ttl
)
return messages_dict
def get_cache_hit_rate(self) -> float:
"""计算缓存命中率"""
total = self.cache_hits + self.cache_misses
return self.cache_hits / total if total > 0 else 0.0
4.2 缓存更新策略
关键问题:如何保证缓存和数据库的一致性?
我们采用的是双写 + TTL过期策略:
# 写入流程
async def add_message(...):
# 1. 写数据库
await pg_storage.add_messages(...)
# 2. 更新缓存(全量刷新)
all_messages = await pg_storage.query_messages(...)
await redis.cache_messages(..., ttl=3600)
为什么不用删除缓存策略?
# 方案1:删除缓存(Cache-Aside模式)
await pg_storage.add_messages(...)
await redis.delete(key) # 删除旧缓存
# 问题:下次读取时cache miss,增加延迟
# 方案2:更新缓存(Write-Through模式) ⭐
await pg_storage.add_messages(...)
await redis.cache_messages(...) # 立即更新
# 优点:下次读取立即命中缓存
五、性能测试
5.1 测试设计
为什么要模拟真实Agent场景?
之前的简单测试没有体现Redis的价值,因为:
- ❌ 数据量太小(每用户只有几十条消息)
- ❌ 读操作太少(70%写 vs 实际Agent 80%读)
- ❌ 读取量太小(每次1条 vs 实际需要50条上下文)
改进后的测试配置:
NUM_USERS = 20 # 并发用户数
HISTORY_MESSAGES = 500 # 每用户历史消息
TEST_ROUNDS = 50 # 测试轮数
READ_RATIO = 0.8 # 80%读操作
CONTEXT_SIZE = 50 # 每次读50条(模拟上下文窗口)
# 总数据量:20 × 500 = 10,000条消息
# 总操作数:20 × 50 = 1,000次操作
# 读操作数:1,000 × 80% = 800次读取
5.2 测试结果
延迟对比
| 指标 | 无Redis | Redis缓存 | 提升倍数 |
|---|---|---|---|
| P50延迟 | 11.14ms | 5.36ms | 2.08x ⚡ |
| P95延迟 | 22.25ms | 12.54ms | 1.77x |
| P99延迟 | 45.85ms | 20.57ms | 2.23x |
| 平均延迟 | 11.00ms | 5.75ms | 1.91x |
读写性能对比
| 操作类型 | 无Redis | Redis缓存 | 提升倍数 |
|---|---|---|---|
| 读操作平均 | 12.65ms | 6.70ms | 1.89x |
| 读操作P95 | 19.27ms | 12.28ms | 1.57x |
| 写操作平均 | 4.79ms | 1.84ms | 2.60x |
吞吐量对比
| 指标 | 无Redis | Redis缓存 | 提升倍数 |
|---|---|---|---|
| 吞吐量 | 1,688/s | 3,364/s | 1.99x 🚀 |
| 总耗时 | 0.59s | 0.30s | 节省49.8% |
| 缓存命中率 | 0% | 100% | - |
5.3 关键发现
1. Redis对读密集场景提升明显
读操作占比:80%
读操作提升:1.89倍
整体提升:2.08倍
结论:读操作越多,Redis优势越明显
2. 缓存命中率达到100%
测试场景:多轮连续对话
首次查询:Cache Miss → PostgreSQL
后续查询:Cache Hit → Redis
命中率:800次读取中800次命中 = 100%
3. 吞吐量翻倍的原因
无Redis:所有请求打到PostgreSQL
- 连接池限制:30个连接
- 排队等待:请求阻塞
有Redis:90%+请求命中缓存
- 直接从Redis返回
- PostgreSQL压力降低90%
- 吞吐量提升2倍
六、实战经验总结
6.1 Redis最佳实践
1. TTL设置建议
# 根据数据特点设置不同TTL
cache_config = {
# 高频读取、更新快 → 短TTL
"messages": 3600, # 1小时
# 中频读取、更新慢 → 中TTL
"summary": 7200, # 2小时
# 低频读取、更新慢 → 长TTL
"profile": 10800, # 3小时
}
原则:
- TTL太短:缓存失效频繁,失去意义
- TTL太长:数据可能过期,占用内存
- 根据业务特点平衡
2. 缓存键命名规范
# 推荐:分层命名,便于管理
"messages:{user_id}:{session_id}"
"profile:{user_id}:{session_id}"
"summary:{user_id}:{session_id}:{summary_id}"
# 好处:
# 1. 清晰的命名空间
# 2. 便于批量删除(pattern匹配)
# 3. 便于监控统计
3. 缓存更新时机
# 场景1:写入新消息
async def add_message(...):
await db.save()
await cache.update() # 立即更新
# 场景2:批量操作
async def import_history(...):
await db.bulk_insert()
# 不更新缓存,等待TTL过期
# 原则:频繁操作立即更新,批量操作延迟更新
6.2 踩过的坑
坑1:并发session导致的连接冲突
# 错误做法:多个协程共享一个session
session = async_session_maker()
tasks = [task1(session), task2(session), ...]
await asyncio.gather(*tasks)
# 报错:Method 'close()' can't be called here
# 正确做法:每个协程创建独立session
async def task(session_maker):
session = session_maker() # 独立session
try:
await do_work(session)
finally:
await session.close()
坑2:缓存雪崩
# 问题:所有缓存同时过期
for user in users:
await redis.setex(key, ttl=3600, value)
# 解决:随机TTL
import random
ttl = 3600 + random.randint(0, 600) # 3600-4200秒
await redis.setex(key, ttl, value)
坑3:大key问题
# 问题:单个key过大(>10MB)
messages = [...] # 10000条消息
await redis.set(key, json.dumps(messages))
# 解决:分页存储
for i in range(0, len(messages), 100):
chunk = messages[i:i+100]
await redis.set(f"{key}:page:{i//100}", json.dumps(chunk))
6.3 生产环境建议
1. 监控指标
# 必须监控的指标
metrics = {
"cache_hit_rate": 0.95, # 命中率 >90%
"avg_latency": 5.0, # 平均延迟 <10ms
"redis_memory": "500MB", # 内存使用
"redis_connections": 20, # 连接数
}
# 告警阈值
if cache_hit_rate < 0.8:
alert("缓存命中率过低")
if avg_latency > 20:
alert("Redis延迟过高")
2. 降级方案
async def query_messages_with_fallback(...):
try:
# 尝试从Redis读取
cached = await redis.get_cached_messages(...)
if cached:
return cached
except RedisError:
logger.warning("Redis不可用,降级到PostgreSQL")
# 降级:直接查PostgreSQL
return await pg_storage.query_messages(...)
3. 容量规划
# 估算Redis内存需求
单条消息大小:200 bytes
单用户消息数:500条
单用户占用:200 * 500 = 100KB
活跃用户数:10000
总内存需求:100KB * 10000 = 1GB
# 建议:预留2-3倍buffer
推荐配置:2-3GB内存
七、下一步计划
7.1 长期记忆层
当前已完成:短期记忆(Deque)+ 中期记忆(PostgreSQL + Redis)
下一步:长期记忆层(向量数据库)
中期记忆(对话历史)
↓ 定期提取
长期记忆(知识图谱)
↓
- 用户画像
- 对话摘要
- 实体关系
- 语义向量
7.2 集成到真实Agent
将记忆系统集成到我的agent-orchestration-patterns项目:
# ReAct模式 + 记忆系统
async def react_with_memory(question):
# 1. 读取历史上下文
history = await memory.query_messages(user_id, session_id, limit=10)
# 2. ReAct推理
thought = await llm.think(question, history)
action = await llm.act(thought)
observation = await tools.execute(action)
# 3. 保存记忆
await memory.add_message(user_id, session_id, "assistant", observation)
总结
本文分享了如何通过Redis缓存层优化AI Agent记忆系统的性能:
核心收获:
- 架构设计:三层记忆架构(Deque → Redis → PostgreSQL)
- 性能提升:读操作快1.89倍,吞吐量翻倍
- 实战经验:TTL设置、降级方案、容量规划
关键数据:
- P50延迟降低:11.14ms → 5.36ms(2.08倍)
- 读操作提升:12.65ms → 6.70ms(1.89倍)
- 吞吐量提升:1,688/s → 3,364/s(1.99倍)
- 缓存命中率:100%
适用场景:
- ✅ 读密集型应用(Agent、聊天机器人)
- ✅ 高并发场景(100+用户同时在线)
- ✅ 长对话历史(500+消息)
下一篇文章将分享长期记忆层的设计与实现,敬请期待!
完整代码已开源:[GitHub链接] 系列文章:
- 第一篇:三层记忆架构设计
- 第二篇:异步压缩优化(6.3倍提升)
- 第三篇:Redis缓存优化(2倍提升)← 本文
欢迎关注、点赞、收藏!有问题欢迎评论区讨论 🚀