Agent 的短期记忆,Redis
大模型本身不带记忆,多轮对话靠的是外部给它「递上下文」。这份最热、最临时、每轮都要读写的记忆,业界几乎都交给 Redis:内存级读写快、TTL 到点自动忘、数据结构又刚好够用。这篇从 Redis 7 种数据类型讲起,讲透为什么短期记忆的最佳解是 Redis,并用「摘要 + 截断中间件 + Redis 存取」落地一个能续聊的会话记忆,最后讲清它与 PostgreSQL 长期记忆如何分工
Leo
2026.09.05 · Updated 2026.09.23
Agent 的短期记忆,Redis 一个就够:从 7 种数据类型到记忆落地全解
为什么写这篇:LLM 是金鱼,Agent 得帮它「过目不忘」
大模型本身是无状态的——你问完上一句,它转头就忘干净。多轮对话能聊下去,靠的不是模型记得,而是每次提问时,由外部把历史上下文重新塞给它。
于是 Agent 天然分裂出两种记忆需求:
- 短期记忆:这一会话里最近几轮说了什么、刚才推理到哪一步。它要热——每轮都被完整读一遍、写一遍,读写得快不快直接决定对话手感;
- 长期记忆:用户上次聊过的偏好、跨会话的完整历史。它要久——可能隔几天、隔几个会话才用一次,要用时最好还能按语义搜回来。
大多数人做的第一版长这样:内存里开个数组装着历史。但一旦服务重启、进程一挂,记忆全没了——模型记住的,还不如你自己记得多。
Redis 就是来补这个窟窿的。一句话先说结论:
短期记忆 = 现用现取 + 到点自动忘。Redis 内存级读写、自带 TTL 过期、数据结构又刚好覆盖消息存取的几种形态,所以它是 Agent 短期记忆的事实默认选择。
先看全景:短期记忆和长期记忆怎么分工
不要一上来就纠结用哪个库。先把两段记忆的生命周期画出来,选型就顺了。
两种记忆不是二选一,而是各管一段生命周期:
| 维度 | 短期记忆(Redis) | 长期记忆(PostgreSQL) |
|---|---|---|
| 管什么 | 最近几轮对话、会话摘要 | 完整历史消息、用户画像、可复用事实 |
| 生命周期 | 分钟 ~ 小时级,自动过期 | 永久,除非显式删除 |
| 读的频率 | 每轮都全量读一遍 | 按需召回(新会话开场、用户提问触发) |
| 检索方式 | 按 key 直取 | 关系查询 + 向量语义检索 |
| 该有的性子 | 快、轻、过期干净 | 稳、全、能 JOIN 能搜 |
| 代价关注点 | 内存贵,只放「热」的 | 磁盘便宜,放得下全部历史 |
后面你会看到:这套分工恰好把 Redis 和 PostgreSQL 各自的强项都用满了。
第一步:先把 Redis 跑起来(30 秒)
短期记忆的场景,用官方镜像起一个就够:
# 一份 docker-compose:Redis + 可选的可视化面板 RedisInsight(类似 pgAdmin)
services:
redis:
image: redis:7-alpine
ports: ["6379:6379"]
command: redis-server --appendonly yes # 开持久化,防宕机丢短期记忆
redisinsight:
image: redis/redisinsight:2.50
ports: ["5540:5540"]
Node 侧连它用的是社区最流行的 ioredis:
npm i ioredis
import Redis from "ioredis";
const redis = new Redis({ host: "localhost", port: 6379, db: 0 });
redis.on("connect", () => console.log("✅ Redis 已连接"));
要点:
- 官方镜像 + 一行配置就是完整可用的 Redis,
redis-server --appendonly yes让内存数据带持久化保险; - 下面的示例统一用 redis-cli 讲命令(在容器里
redis-cli就能进),读起来最直接;写代码时换成ioredis的方法名,语义一一对应; - RedisInsight 是官方 Web GUI,看 key、看 TTL、跑命令都方便,相当于记忆区的「体检面板」。
第二步:Redis 的 7 种数据类型,谁在 Agent 记忆里是主角
先给一张「是什么」的总表,后面逐个说。带 ⭐ 的,是短期记忆的主力数据类型,其余更多用在 Agent 的后台与周边业务上。
| 数据类型 | 一句话 | 最擅长 | Agent 记忆里 |
|---|---|---|---|
| String 字符串 ⭐ | key 对一段文本 | 缓存、计数、带过期的小数据 | 整段会话序列化后塞一个 key |
| Hash 哈希 ⭐ | 一个 key 下多个字段 | 结构化对象、购物车 | 一个会话拆多个字段存 |
| List 列表 ⭐ | 有序可追加的数组 | 队列、聊天记录 | 每轮消息按序入队、按需截断 |
| Set 集合 | 无序去重 | 黑名单、签到、标签 | 「已经讲过的、处理过的」去重 |
| ZSet 有序集合 ⭐ | 带分数的排序集合 | 排行榜 | score 存时间戳 → 按时间窗口取记忆 |
| Bitmap 位图 | 一字节 8 个 0/1 | 海量布尔状态 | 用户活跃日、实验分组标记 |
| Geo 地理位置 | 存经纬度 | 附近检索 | 本地生活类 Agent 找「附近」,跟记忆无关 |
2.1 String —— 整段记忆就塞一个 key
String 是最简单的「key → 一段值」。看家本领是能设过期,而「到点自动忘」正是短期记忆的第一诉求。
# 手机验证码:5 分钟过期
SETEX verification:mobile:13800138000 300 666888
# 登录 token:24 小时过期
SETEX session:token:adf245k 86400 userid:1001
# 计数器自增:文章/视频阅读量
INCR counter:article:1024
# 分布式锁:10 秒过期防死锁
SET lock:order:2001 locked NX EX 10
# Agent 记忆:某个会话最近的对话摘要,1 小时过期
SETEX agent:memory:u1001:s12:summary 3600 "用户想学 PostgreSQL 向量检索"
要点:
- SETEX key 秒 值 = 存值 + 设过期一步到位;没有过期时间的东西在这里没有存在感;
- INCR/DECR 自增自减原子完成,适合给会话轮次、token 用量计数;
- SET key val NX EX 秒 是分布式锁标准写法——多 Agent 并发跑同一个任务时,用它保证「只有一个在执行」;
- 短期记忆最朴素的形态就是它:把整段消息历史 JSON 序列化,作为一个 String 存起来。第三步的落地实现就是这么干的。
2.2 Hash —— 一个会话,拆成多个字段
Hash 是一个 key 底下挂多个 field,像「一张 JSON 只让你改其中一个字段」。适合存结构化了但还要局部更新的对象。
# 用户资料:几个字段一次写入
HSET user:info:1001 name 张三 age 28 phone 13800138000
# 电商购物车:字段=商品 ID,值=数量
HSET cart:user:1001 product:10086 2 product:10087 1
# 一个 Agent 会话拆字段存:messages、summary、last_active 各自可独立更新
HSET agent:session:u1001:s12 messages "{...}" summary "对话摘要" updated_at 1728000000
要点:
- HGETALL 一次拿全字段,HGET key field 只取一个字段,避免为了改一行而把整个对象读写一遍;
- HINCRBY 可以对某个字段原子自增——比如给会话里的某个计数
hincrby key turn 1; - 相比 String 整段存,Hash 的收益是局部更新:messages 变长时不用把 summary 字段也带着读写一遍。
2.3 List —— 把每轮消息按顺序推入队尾
List 是有序、可重复、支持两边进出的数组。消息历史天生就是「一个接一个的追加序列」,List 几乎是为它而生的。它还有个记忆场景的杀手锏:LTRIM 直接截断。
# 聊天/浏览历史:左侧压入最新
LPUSH user:history:1001 看了AI课程 看了Redis教程
# 消息队列:右侧入队,worker 左侧取出
RPUSH queue:task 生成对话摘要 向量入库
# 订单队列
RPUSH queue:order order_1001 order_1002
# 只保留最近 20 条:旧消息自动滚出(短期记忆的“截断”就长这样)
LTRIM queue:order -20 -1
# 取全部:正序
LRANGE user:history:1001 0 -1
要点:
- RPUSH 追加 + LRANGE 正序取回 = 消息「一条一条进来、按序读出」的标准姿势;
- LTRIM key -N -1 把列表削到最近 N 条——比摘要更暴力的短期记忆策略就是它:超了阈值直接丢最老的;
- LPOP/RPOP 一头进一头出就成了任务队列,Agent 后台要异步处理的消息(比如「去把对话摘要落库」)可以排这里。
2.4 Set —— 记住「哪些已经处理过」
Set 无序、自动去重。记忆类 Agent 不常直接用它存上下文,但常拿它做账本:记录「这个我已经知道 / 做过了」。
# 当日签到用户:同一用户加第二次不会重复
SADD sign:20250820:user 1001 1002 1003
# IP 黑名单:判断某 IP 在不在里面
SADD blacklist:ip 192.168.1.100
SISMEMBER blacklist:ip 192.168.1.100 # 返回 1 表示命中
# 共同好友:两个集合求交集
SINTER user:friend:1001 user:friend:1002
# 给某个会话标记「已去重的知识点」
SADD agent:seen:u1001:s12 分布式锁 Redis过期策略
SISMEMBER agent:seen:u1001:s12 分布式锁 # 1 = 讲过,别再重复科普
要点:
- SADD 天然去重,重复写入不报错、不产生重复成员;
- SISMEMBER 是 O(1) 的「有没有」,最适合做已读/已处理判断;
- SINTER(交集)/ SUNION(并集)/ SDIFF(差集) 让你能对多组记忆做集合运算,比如「两个用户共同收藏过的 Agent 主题」。
2.5 ZSet —— score 存时间戳,记忆就带上了时间轴
ZSet = Set + 每个成员带一个分数(score),按分数自动排序。排行榜是它最出名的用法;但在记忆场景里,让 score = 时间戳,它就变成了「带时间轴的记忆」。
# 排行榜:score 就是热度
ZADD rank:course 98 PostgreSQL实战 95 AI-Agent开发 92 Redis从入门到精通
# 按名次取:降序前 3
ZREVRANGE rank:course 0 2
# 记忆场景:score=消息发生的时间戳,按时间窗口取“最近 30 分钟说了什么”
ZADD agent:timeline:u1001:s12 1728000000 "用户问:Redis 怎么存记忆"
ZADD agent:timeline:u1001:s12 1728000060 "助手答:按会话 key + JSON..."
ZRANGEBYSCORE agent:timeline:u1001:s12 1727998200 1728000060
要点:
- ZADD key score member、ZSCORE 查分、ZRANK/ZREVRANGE 查名次,一套就够日常用;
- score 换成本质是数字的时间戳,就能用
ZRANGEBYSCORE圈出一个时间窗——比 List「只能从两端截」灵活; - 想给某条记忆加权(比如用户重复提到的事实热度 +1),用
zadd key score member覆盖写即可提升排序。
2.6 Bitmap —— 一个字节当 8 个开关用
Bitmap 是 String 的位级视图,每个 bit 表示一个布尔值,极其省内存:百万用户各一个 bit,也就 100 多 KB。适合「海量用户 × 少状态」。
# 用户当月签到:第 5 天、第 10 天各置 1
SETBIT user:sign:1001:202508 5 1
SETBIT user:sign:1001:202508 10 1
# 统计这个月签到了几天
BITCOUNT user:sign:1001:202508
# Agent 运营:某用户在 3 号、17 号用过助手
SETBIT agent:active:1001:202508 3 1
SETBIT agent:active:1001:202508 17 1
要点:
- SETBIT key 偏移量 1 / GETBIT key 偏移量,偏移量即「第几天/第几个」;
- BITCOUNT 一行统计出「总共置了多少个 1」——海量签到天数、活跃天数这类问题,用它内存省到极致;
- 它不参与上下文记忆,但 Agent 后台的活跃度统计、A/B 实验分组很常见。
2.7 Geo —— 给「本地生活类 Agent」用的,不是给记忆的
Geo 存经纬度并提供距离与附近检索。它是给「帮我找附近的奶茶店」这类带位置的 Agent 用的,跟上下文记忆没关系——但既然讲了 7 种,你至少要知道「有这把锤子」。
# 记录门店位置:注意参数顺序是 经度 纬度 名称
GEOADD shop:location 116.481028 39.921983 北京总店
GEOADD shop:location 121.473701 31.230416 上海分店
# 两家店直线距离,单位 km
GEODIST shop:location 北京总店 上海分店 km
要点:
- GEOADD 存的是「经度 纬度 名称」,顺序和直觉相反,最容易写反;
- GEODIST 算两点直线距离,
GEOSEARCH能查「某点半径内的店」; - 一个直觉:短期记忆是「时间敏感」,Geo 是「空间敏感」——后者属于会查位置的 Agent,而不是记忆层。
2.8 小结一张速查表
| 数据类型 | 一句话 | 典型业务 | 短期记忆里的角色 |
|---|---|---|---|
| String | key → 一段值 | 验证码、Token、计数器、锁 | 整段历史 JSON / 一条摘要,TTL 自动过期 |
| Hash | key → 多字段 | 用户、购物车 | 会话拆字段存,局部更新 |
| List | 有序追加 | 队列、历史 | 消息逐条入队 + LTRIM 截断 |
| Set | 无序去重 | 黑名单、签到 | 已处理/已讲过去重 |
| ZSet | 带 score 排序 | 排行榜 | score=时间戳,按时间窗取 |
| Bitmap | 位级开关 | 海量签到 | 活跃度、实验分组 |
| Geo | 经纬度 | 附近检索 | 位置型 Agent,非记忆 |
给记忆场景排个座次: 短期内存消息,String、List、Hash、ZSet 是四种可用姿势(下文逐个比);Set / Bitmap / Geo 更多是「Agent 产品要有的后台能力」,知道存在、知道命令,就够。
第三步:记忆的形态——先把「无限变长的历史」这个问题摆出来
无论用哪种 Redis 类型存,存的内容本质是一份消息数组:一条消息有角色(user / assistant / tool)和内容,工具调用还夹着参数与结果。它长这样(结构示意):
[
{ role: "user", content: "记住:我的猫叫小橘" },
{ role: "assistant", content: "好的,已记住" },
{ role: "user", content: "我猫叫什么?" },
// ... 继续膨胀
]
问题来了:这份数组每聊一轮就变长一点,而且每轮都要整份送给 LLM。于是它无限膨胀会带来一串连锁反应:
- 超过模型的上下文窗口——直接报错,聊不下去;
- token 费用线性上涨——历史越长,每轮都按「全部历史」计费;
- 变慢——输入越长,首字延迟越高;
- 注意力稀释——最新意图被淹没在几千行旧消息里,反而答得更差。
所以任何长期运行的 Agent 都要回答同一个问题:历史太长了怎么办? 业界答案是「摘要 + 截断」组合拳,由 Agent 的中间件自动完成:
这套动作在 LangChain / DeepAgents 里是一个叫 summarizationMiddleware(摘要中间件) 的东西,声明式配置即可:
summarizationMiddleware({
model,
summaryPrompt, // 告诉摘要模型要提炼哪些信息
trigger: { messages: 8 }, // 历史 ≥ 8 条时触发压缩
keep: { messages: 4 }, // 只保留最近 4 条,更早的压成摘要
})
要点:
- trigger 控制「何时压缩」——可以按消息条数,也可以按 token 数(
{ tokens: 100000 }),满足即触发; - keep 控制「压缩后留多少新鲜的」——被压掉的旧消息变成一条摘要消息,塞回数组最前面;
- 压缩后的数组里,历史被一句话摘要取代,总量不再无界增长——这是所有记忆方案的前提,Redis 只是负责把这份「已压缩的小数组」存下来。
第四步:把记忆交给 Redis——一个能续聊的最小实现
现在把前两步拼起来,做一个每轮都能接着聊、重启不丢、到点自动过期的短期记忆。核心就三件事:读 Redis → 送 Agent(内部可能触发压缩)→ 写回 Redis。
// 依赖:npm i ioredis @langchain/core @langchain/openai langchain dotenv
// 环境变量:OPENAI_API_KEY / OPENAI_BASE_URL / MODEL_NAME;REDIS_HOST 等可选
import "dotenv/config";
import Redis from "ioredis";
import { ChatOpenAI } from "@langchain/openai";
import { createAgent, HumanMessage, summarizationMiddleware } from "langchain";
import {
mapChatMessagesToStoredMessages, // LangChain 消息对象 → 可 JSON 化的纯对象
mapStoredMessagesToChatMessages, // 反向:纯对象 → 消息对象
} from "@langchain/core/messages";
import * as readline from "node:readline/promises";
// ---------- 1. 一个极简的「Redis 记忆仓库」 ----------
class RedisMessageStore {
constructor({ redis, ttlSeconds }) {
this.redis = redis;
this.ttlSeconds = ttlSeconds; // 短期记忆的“寿命”
}
key(sessionId) {
// key 设计:agent:memory:{谁}:{哪个会话}:messages
return `agent:memory:${sessionId}:messages`;
}
async load(sessionId) {
const raw = await this.redis.get(this.key(sessionId)); // String 整段取
if (!raw) return [];
return mapStoredMessagesToChatMessages(JSON.parse(raw)); // 还原成消息对象
}
async save(sessionId, messages) {
const payload = JSON.stringify(mapChatMessagesToStoredMessages(messages));
// EX 让每次写入都刷新 TTL —— 只要还在聊,记忆就一直续期
await this.redis.set(this.key(sessionId), payload, "EX", this.ttlSeconds);
}
async clear(sessionId) {
await this.redis.del(this.key(sessionId));
}
}
// ---------- 2. 摘要中间件:把会无限变长的历史“锁死” ----------
const summaryPrompt = `你是对话摘要助手。请用中文总结对话,务必保留用户明确说过的
姓名、偏好、日期等关键事实。待摘要的对话:{messages} 摘要:`;
const model = new ChatOpenAI({
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: { baseURL: process.env.OPENAI_BASE_URL },
temperature: 0,
});
const agent = createAgent({
model,
systemPrompt: "你是会话助手。记住用户提到的关键事实,若见摘要请据此继续。",
middleware: [
summarizationMiddleware({
model,
summaryPrompt,
trigger: { messages: 8 }, // 历史 ≥ 8 条 → 压缩
keep: { messages: 4 }, // 只留最近 4 条
}),
],
});
// ---------- 3. 每一轮:读 Redis → invoke → 写回 Redis ----------
const redis = new Redis({ host: "localhost", port: 6379 });
const store = new RedisMessageStore({ redis, ttlSeconds: 1800 }); // 半小时记忆
const SESSION = "u1001:s12"; // 谁 + 哪个会话
async function chat(userText) {
const history = await store.load(SESSION);
const { messages } = await agent.invoke(
{ messages: [...history, new HumanMessage(userText)] },
{ recursionLimit: 30 }
);
await store.save(SESSION, messages); // 注意:存的是压缩后的、有界的那份
return messages.at(-1).content;
}
// 命令行聊几句做演示;也可循环读入
for (const q of ["我叫小明,家住北京", "我住哪?我的名字?"]) {
const answer = await chat(q);
console.log("Q:", q, "\nA:", answer);
}
await redis.quit();
跑起来就是这个效果:第一轮它记住,第二轮它答得出,中间把 Redis 里那个 key 的 TTL 一查,能看到它一直在滚动续期:
# 聊过之后,Redis 里真实存在的记忆
> GET agent:memory:u1001:s12:messages
"[{\"role\":\"human\",...}]" # 一份已压缩过的消息数组
> TTL agent:memory:u1001:s12:messages
1559 # 还剩 ~26 分钟,每次对话都会重置
要点:
- key 设计带上身份和会话:
agent:memory:{用户}:{会话}:messages,一个用户多个会话天然隔离,不同用户互不串味; EX+ 每次写入 = 滚动 TTL:只要还在聊就续命,半小时没人理自动消失——「短期」是被 Redis 的过期机制物理保证的,不用自己写清理任务;- 序列化两个 map 函数是唯一的“胶水”代码:LangChain 消息对象不能直接塞 Redis,先转成纯 JSON 对象再存,读回再还原;
- 存的是压缩后的那份:摘要中间件在 invoke 内部把历史压成「摘要 + 最近 4 条」再返回,写回 Redis 的记忆总量永远是 有界的;
- 服务重启不丢(Redis 在跑)、换机器不丢、多实例共享——内存数组做不到的三件事,它都做到了。
第五步:让 Redis 记得更讲究——四种存法怎么挑
第四步用的是 String 整段存,最简单。但 Redis 有 7 种类型,为什么消息不挑别的?因为它们各有取舍,适合不同诉求:
| 存法 | 每轮怎么读写 | 优势 | 代价 |
|---|---|---|---|
| String + JSON 整段 | GET 整份 / SET 整份 + EX | 最简;压缩中间件返回的本来就是整份数组,天然匹配 | 会话大了整读整写;改动一个字段也要整份搬运 |
| List 逐条入队 | 每轮 RPUSH 一条;读 LRANGE 0 -1 | 天生有序;LTRIM 直接实现“只留最近 N 条”的纯截断 | 摘要是一整段,塞进去会打乱“一条=一轮”的对应;要自己按轮次归档 |
| Hash 分字段 | HGET messages / summary / updated_at 各自读写 | 摘要和原文分离,可单独刷新摘要不碰原文;结构清晰 | 每条消息仍是序列化的一坨,没比 String 省多少,代码稍多 |
| ZSet score=时间戳 | 每轮 ZADD(score=now);ZRANGEBYSCORE 按时间窗取 | 能精确表达「最近 30 分钟说了什么」这类时间窗口语义 | 中间件返回的整份数组要重新摊平成分条写入 |
怎么选,取决于你的记忆策略:
要点:
- 别过度设计:第一步用 String 整段存就够了——它和摘要中间件的产物天然对齐,TTL 一键过期,是最短路径;
- 想要「能局部更新」再上 Hash(把摘要和原文拆字段),想要「纯截断」再上 List + LTRIM,想要「按时间窗」再上 ZSet;
- 上面四种都是同一个 key 命名空间下的事,存量换形态只是把存取代码换掉,记忆的读写语义不用动。
第六步:短期记忆只负责「这一阵」——历史与长期记忆交给 PostgreSQL
Redis 再好,它的定位决定了它不适合做历史沉淀:内存贵、放不下全部历史;要语义检索它更无从谈起(它不是为「按意思找」设计的)。而每轮聊过的内容本身很有价值——用户下个星期可能回来问「我上次是不是让你查过向量数据库?」。
这就轮到 PostgreSQL 上场,两套库形成分工:
- Redis:当前会话的「热」上下文,毫秒级读写 + TTL 自动过期——短期记忆;
- PostgreSQL:每条消息永久落库,带
user_id/conversation_id外键,再给内容挂一列向量、配一个 HNSW 索引——历史 + 长期记忆。想找「上个会话里和向量有关的」,一条带WHERE conversation_id = ?和ORDER BY embedding <=> $1的 SQL 就能按语义召回。
关系上长这样(简化的三张表):
CREATE TABLE conversations (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
title TEXT
);
CREATE TABLE messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('user','assistant','system')),
content TEXT NOT NULL,
embedding vector(1024), -- 向量就是一列,不是另一套库
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON messages USING hnsw (embedding vector_cosine_ops);
于是两种记忆串成一条完整的流水线:
要点:
- 双写但不冲突:Redis 存「本轮要用的压缩版」,PostgreSQL 存「全量的唯一事实源」——Redis 丢了、过期了,历史还在 PG,随时能重建;
- 长期召回不是靠 Redis 翻聊天记录,而是 PG 里按向量找「意思最接近的旧消息」,再把命中的几段塞回本轮 prompt;
- Redis 负责让这轮快、让记忆会过期;PG 负责让记忆永存、让历史可搜。 两个库的选型理由各用其长,谁也不抢谁的活。
收个尾:为什么「短期记忆 = Redis」是个默认解
把全篇的逻辑收成一句话:短期记忆要的是「现用现取 + 到点自动忘 + 多实例共享」,而这三点 Redis 都是原生答案:
- 快:内存级读写,每轮全量读写的操作是 μs~ms 级,对话手感不受历史拖累;
- 会过期:
EX/SETEX/TTL让「短期」有物理保证,不用自己写清理任务、不怕“记性太好”泄漏隐私; - 数据结构刚好够用:整段存用 String、逐条存用 List、拆字段用 Hash、按时间窗用 ZSet——4 种形态覆盖消息存取的所有姿势,Set/Bitmap/Geo 留给周边业务;
- 好扩展:多实例、多进程共享同一份记忆;配合摘要中间件,存进 Redis 的永远是「有界的那份」。
最后一张「该把记忆放哪」的决策图,方便你按需取用:
给你的行动清单: 跑一个 Redis(官方镜像一行配置)→ 用 String 把一个会话的历史 JSON 存进去、带上 EX → 挂上 summarizationMiddleware 让历史永远有界 → 再补一张 PostgreSQL 表把每轮消息完整落库、配一列向量做长期召回。到这一步,你的 Agent 就同时拥有了「这一阵记得住」和「很久以前也想得起」的完整记忆——而其中最短、最热的那一段,Redis 一个就够。
附:7 种数据类型命令速查
| 类型 | 写 | 读 | 常用判断/删除 |
|---|---|---|---|
| String | SET / SETEX / INCR | GET | DEL、TTL |
| Hash | HSET | HGET / HGETALL / HKEYS | HDEL |
| List | LPUSH / RPUSH | LRANGE / LPOP / RPOP | LTRIM、LLEN |
| Set | SADD | SMEMBERS | SISMEMBER、SINTER |
| ZSet | ZADD | ZRANGE / ZREVRANGE / ZSCORE | ZRANK |
| Bitmap | SETBIT | GETBIT | BITCOUNT |
| Geo | GEOADD | GEODIST / GEOSEARCH | — |
Leo
BloggerIndependent developer / Blogger and the maintainer of the original blog “大道至简”. Migrating years of posts and shiyu from WordPress to Next.js.
Reader comments
COMMENTS · 0Leave a comment