Redis在项目中的应用

这个项目里 Redis 主要实现了什么?

Redis 主要承担两类"易失但高频"的状态存储

1) 登录态/Token 在线校验(可撤销 JWT)

简历里主要写的是在短期记忆中的应用,因此1)不讨论

2) 对话短期记忆/工作流 Checkpoint(LangGraph 持久化)

核心文件:suagent-server/src/memory/redis_short_memory.py

  • chat_service 构建 agent 时:
    checkpointer = await RedisShortMemory.get_acheckpointer()
  • RedisShortMemory 用的是:
    • langgraph.checkpoint.redis.AsyncRedisSaver / RedisSaver
    • settings.redis_url
    • 配置默认 TTL:ttl = 1440(注意:这里单位取决于 LangGraph saver 的实现;代码里传的是 default_ttl: 1440

这块的意义是:

  • 把每个 session/graph 运行过程的状态 checkpoint 存在 Redis ,从而实现:
    • 断线/重启后可继续
    • 多实例部署时共享会话状态
    • 短期记忆快速读写(比落 DB 更适合高频状态)

一句话总结:

  • "Redis 在对话系统里用作短期记忆/状态机 checkpoint 存储,提高会话状态读写性能并支持可恢复。"

本项目里 Redis "不像在做"的事情

  • 队列/消息流 (项目的异步消息更偏 RabbitMQ:mq/*、settings 里也有 RabbitMQ 配置)
  • 分布式锁/限流 (没看到 SETNX/Lua/Redlock 等模式)
  • 排行榜/计数INCR/ZSET 等)
  • PubSub/StreamsPUBLISH/XADD 等)

面试可能结合 Redis 追问的"拓展知识点"

E. "你们短期记忆为什么用 Redis,而不是数据库?"

  • 要点
    • 会话状态读写频繁、对延迟敏感
    • Redis 内存结构更适合小对象、高并发
    • TTL 天然适合短期状态
  • 延伸:如果要水平扩展/多实例,Redis 作为共享状态存储就很自然

F. "如何保证 Redis 写入/读取的原子性?"

你目前 token revoke / refresh 是"先读再删再写"的多步:

  • 要点
    • 在并发刷新、重复请求时可能出现竞态(比如重复 refresh)
  • 可讲的提升
    • Lua 脚本把"校验存在 + 删除 + 写新 token"做成原子操作
    • 或用 WATCH/MULTI/EXEC(事务)实现 CAS

G. "Redis 持久化/淘汰策略你怎么选?"

即便你项目里没显式配置,面试常问:

  • 持久化
    • token/短期记忆一般不要求强持久化,可用 AOF everysec 或甚至关闭(看容灾要求)
  • 淘汰策略
    • volatile-ttl / allkeys-lru 等,结合是否所有 key 都有 TTL
  • 指标 :命中率、内存碎片率、慢查询、key 数量、热点 key
    我会围绕 RedisShortMemory(LangGraph RedisSaver/AsyncRedisSaver)这条链路,拆成:数据模型/Key 形态与生命周期并发与一致性容量与性能可用性与降级安全与隔离 五个维度,指出当前实现潜在风险,并给出可落地的改造方案(含你可以在面试里怎么讲)。我先把 Agent checkpointer 的实际使用方式再确认一下(例如 session_id/namespace 如何映射到 Redis key)。

在 ChatService 里已经遇到过一个典型问题:Redis 中 checkpoint 的 messages 可能出现"tool_calls 不完整" ,所以写了 _fix_incomplete_messages() 检测并在必要时 checkpointer.adelete() 清掉整个 thread。


潜在问题与解决方案

1) 并发一致性:同一 thread_id 可能被多请求同时写入(高频实际问题)

问题

thread_id = f"{agent_id}_{session_id}",但实际线上会出现:

  • 同一个 session 用户在前端连点两次发送
  • 或 SSE 断线重连触发重复请求

风险/现象

  • checkpoint 中消息顺序错乱
  • 出现已经处理过的:AIMessage(tool_calls=...) 后面缺少对应 ToolMessage
  • 你现在的"修复手段"是 整线程删除,会导致用户上下文直接清空(体验差,且可能误伤)

解决方案------对同一 thread_id 做串行化

  • Redis 分布式锁:SET lock:{thread_id} value NX PX <timeout>
  • 获锁后才允许执行 agent;否则返回"正在生成,请稍候/取消前一个请求"

4) "不完整 tool_calls" 的处理方式过于粗暴:直接删 thread(数据损失)

tool_call 不一致 :最典型的表现是:模型发了一个或多个 function_call,但应用层把结果回到了错误的 call_id、不是按 call_id 去配对、在还有未完成 tool call(pending/running)时又插入了新的用户输入,或者重试时把同一轮又执行了一遍。OpenAI 当前文档明确说明,在 Responses 里,tool call 和 tool output 是两类不同的 item ,靠 call_id 关联;会话状态还会持久化消息、tool call、tool output;并且模型可能在一个 turn 中同时发起多个函数调用 。(OpenAI开发者)

本质上不是"模型偶发抽风",而更像是 agent loop / orchestration 的一致性问题 。官方也明确建议把函数调用做成严格 schema,并在应用层做校验与边界情况处理;strict: true 可以让参数更可靠地遵守 schema,而不是"尽力而为"。(OpenAI开发者)

风险/现象

  • 误判会导致把原本完整的历史也删了
  • 用户看到"突然失忆",难以定位根因(因为你把现场清掉了)

tool_call 不一致 = agent 执行链没有保持"模型调用 → 工具执行 → 结果回填 → 状态推进"的强一致。

好的做法是:显式状态机 + call_id 级别账本 + schema 严格化 + 幂等 + tracing。

  • 1.显示状态机:agent loop 必须有明确状态门禁**
    MODEL_RETURNED -> EXTRACT_CALLS -> PERSIST_PENDING_CALLS -> EXECUTE_TOOLS -> APPEND_FUNCTION_CALL_OUTPUTS -> RESUME_MODEL
    pending_calls > 0 时,不接受新的用户 turn 去和这轮混跑。因为 conversation item 本身就包含 tool call 和 tool output,混入新 turn 最容易把链路弄叉。(OpenAI开发者)
  • 2.call_id级别账本
    把每个 tool call 落成显式 ledger
    至少记录:conversation_id / call_id / tool_name / arguments / status(pending|running|done|failed) / started_at / finished_at / tool_result。核心键是 call_id。官方文档已经把 call_id 定义为 tool call 与 tool output 的映射标识。(OpenAI开发者)
    • 防重复执行
    • 可恢复
    • 幂等控制
      对有副作用的工具做幂等 例如发消息、扣款、创建工单、写数据库,统一要求传入或内部生成 idempotency_key = conversation_id + call_id。这样就算网络重试、worker 重跑、或者局部恢复,也不会造成重复执行。
方案一:数据库表 用 Postgres 建一张 tool_call_ledger
方案二:Redis + DB Redis 做短期锁和状态缓存,DB 做持久化真相源。
  • 3.schema 严格化

    工具 schema 开 strict: true,应用层仍然做校验**
    strict: true 可以提高参数与 schema 的一致性;官方也建议启用 strict mode。同时,帮助中心也强调:应用仍然需要处理边界情况,并在必要时用校验和重试机制兜底。(OpenAI开发者)

  • 4.幂等

    只按 call_id 回填 tool output**,回填时要校验三件事:

    • call_id 是否存在且状态为 pending/running
    • 这个 call_id 是否已经完成过
    • 返回结果是否来自同一个 tool_name + arguments_hash
      任何一项不满足,都不要继续推进模型,而是进入恢复逻辑。官方示例也是把 function_call_output 里的 call_id 设置为原始 tool call 的 call_id。(OpenAI开发者)
  • 5.用 tracing 复盘每一次异常路径。(OpenAI GitHub)

  • Responses API 的 stateful conversation 或 previous_response_id 做会话链路。(OpenAI开发者)
  • 每轮先完整收集 response.output 中所有 function_call item,再统一持久化。(OpenAI开发者)
  • 初期 parallel_tool_calls=false。(OpenAI开发者)

5) Redis 容量与热 key:长对话会把 Redis 当"聊天记录数据库"用

问题

checkpointer 会把状态(包含 messages)持续累加;你在 get_chat_history() 里还会优先从 checkpointer 读 messages。若对话很长:

  • 每个 thread 的 value 会越来越大
  • 每次读写的网络/序列化成本上升

风险/现象

  • Redis 内存增长快
  • 单个 key value 过大导致延迟抖动(大对象读写)
  • 出现热点 thread(活跃用户)拖慢整体

解决方案

  • 控制短期记忆窗口 :只保留最近 N 轮(例如 10 轮)
    • 更进一步:配合你已有的 SummarizationMiddleware,把旧内容压缩成 summary,再保留少量原文
  • 把"历史记录"职责下沉到 DB (你其实已经在 SessionMiddleware 里落库了)
    • Redis 只存"生成所需的上下文"(短窗口 + summary + tool状态)
    • DB 存全量日志用于展示与审计
  • 监控与告警
    • key 数量、内存占用、平均 value 大小(采样)
    • 慢查询、网络带宽、命中率(如果有缓存层)

6) 隔离与安全:thread_id 未包含 user_id,跨用户可能碰撞/越权(取决于 session_id 全局性)

问题

你用的是:thread_id = agent_id + "_" + chat_id(session_id)

如果 session_id 是全局唯一的雪花 ID,一般不会碰撞;但从隔离语义上:

  • 同一个 session 本应属于某个 user
  • thread_id 不含 user_id 时,排障/迁移/权限审计会更难

风险/现象

  • 极端情况下(比如 session_id 不是全局唯一、或被猜到/复用、或测试环境数据混用),可能读到别人的上下文

解决方案

  • thread_id 改成更强隔离:thread_id = f"u:{user_id}|a:{agent_id}|s:{session_id}"
  • 同时对 key 加统一前缀区分环境:prod:lg:checkpoint:{thread_id}(避免不同环境共享 Redis 时串数据)

7) 可用性与降级:Redis 短期记忆不可用时的策略不明确

问题

如果 Redis 连接失败:

  • 当前对话会缺短期记忆,可能导致 agent 质量下降或异常
  • 你没有明确"无 Redis"时 checkpointer 的 fallback

解决方案

  • 降级策略 (按业务优先级选)
    • 降级 1:无 checkpointer 运行(无状态对话),但仍可回复
    • 降级 2:用进程内内存 checkpointer(仅单实例有效)
    • 降级 3:从 DB 拉最近 N 轮拼成上下文(你已经有落库日志)
  • 健康检查与熔断
    • Redis unhealthy 时不要频繁重试创建 saver(避免雪崩)
    • 快速失败 + 告警 + 自动恢复

推荐的"更稳"的短期记忆设计(你可以在面试里这么讲)

设计目标

  • 正确性:不读到半写入状态(tool_calls 完整性)
  • 并发可控:同一会话串行化
  • 成本可控:只保留短窗口 + summary
  • 可用性:Redis 挂了能降级

落地方案(最小改造版)

  • 单例化 AsyncRedisSaver(生命周期由 FastAPI lifespan 管理)
  • thread_id 加 user_id + 前缀
  • thread_id 加分布式锁
  • 限制上下文窗口(最近 N 轮 + summary)
  • 不再用"发现异常就整线程删除"作为常规手段,改为"回滚/只读 committed + 最后兜底再清理"
相关推荐
qq_283720052 小时前
MySQL 8.0新特性高频面试题 30 道(超详细答案)
数据库·mysql·面试·mysql8·高频试题
taWSw5OjU2 小时前
FastAPI + PostgreSQL 实战:给应用装上“缓存”和“日志”翅膀
缓存·postgresql·fastapi
无尽的罚坐人生2 小时前
hot 100 146. LRU 缓存
java·开发语言·缓存
wAEWQ6Ib72 小时前
[拆解LangChain执行引擎]支持自然语言查询的长期存储
数据库·oracle·langchain
吴声子夜歌2 小时前
Node.js——操作MongoDB
数据库·mongodb·node.js
RDCJM2 小时前
nginx 代理 redis
运维·redis·nginx
希望永不加班2 小时前
SpringBoot 缓存注解:@Cacheable/@CacheEvict 使用
java·spring boot·spring·缓存·mybatis
oYD3FlT322 小时前
MyBatis-缓存与注解式开发
java·缓存·mybatis