Redis在项目中的应用
这个项目里 Redis 主要实现了什么Redis主要承担两类“易失但高频”的状态存储1)登录态/Token 在线校验可撤销 JWT简历里主要写的是在短期记忆中的应用因此1不讨论2)对话短期记忆/工作流 CheckpointLangGraph 持久化单例化 AsyncRedisSaver生命周期由 FastAPI lifespan 管理核心文件suagent-server/src/memory/redis_short_memory.pychat_service构建 agent 时checkpointer await RedisShortMemory.get_acheckpointer()RedisShortMemory用的是langgraph.checkpoint.redis.AsyncRedisSaver / RedisSaversettings.redis_url配置默认 TTLttl 1440注意这里单位取决于 LangGraph saver 的实现代码里传的是default_ttl: 1440这块的意义是把每个 session/graph 运行过程的状态 checkpoint 存在 Redis从而实现断线/重启后可继续多实例部署时共享会话状态短期记忆快速读写比落 DB 更适合高频状态一句话总结“Redis 在对话系统里用作短期记忆/状态机 checkpoint 存储提高会话状态读写性能并支持可恢复。”本项目里 Redis “不像在做”的事情队列/消息流项目的异步消息更偏 RabbitMQmq/*、settings 里也有 RabbitMQ 配置分布式锁/限流没看到SETNX/Lua/Redlock 等模式排行榜/计数INCR/ZSET等PubSub/StreamsPUBLISH/XADD等面试可能结合 Redis 追问的“拓展知识点”1. “短期记忆为什么用 Redis而不是数据库”要点会话状态读写频繁、对延迟敏感Redis 内存结构适合小对象、高并发,DB 适合全量审计/历史查询TTL 天然适合短期状态在“短期记忆”场景下Redis 里到底存的是什么Redis 用于LangGraph 的 checkpointerRedisSaver。它不是简单存一条“聊天文本”而是存一次对话线程thread在某个时刻的工作流状态快照checkpoint。1) Key 存什么核心逻辑 key由thread_id决定thread_id f{user_id}_{agent_id}_{chat_id}checkpointer 会用这个thread_id来区分不同会话线程。所以 Redis 的 key本质上是围绕 thread_id 组织的。你不一定能在业务代码里看到最终 Redis key 长什么样因为langgraph.checkpoint.redis内部会拼接自己的前缀/命名空间例如包含checkpoint、thread、版本号等。2) Value 存什么Value “这条 thread 的状态快照checkpoint state”至少包含以下几类信息2.1messages对话消息列表你在 ChatService._extract_checkpoint_messages() 里就是这么取的state.channel_values[messages]这些messages不是纯字符串而是 LangChain 的消息对象序列化后的结构类型包括HumanMessage用户输入AIMessage模型输出ToolMessage工具调用返回这也是你们会遇到 “AIMessage里有tool_calls但后面没对应ToolMessage” 的原因Redis 里存的是消息序列如果中途异常写入就可能留下半成品序列。2.2channel_values / state工作流运行时状态除了 messagesLangGraph 还会存“图的状态”各个 channel 当前值常见字段就是channel_values本轮执行进度/元信息取决于 LangGraph saver 的实现版本2.3工具调用相关的中间态/元数据尤其是在有 tool calling 的时候checkpoint 很可能包含模型输出的 tool_calls 列表在AIMessage.tool_calls关联 idtool_call_id用于把ToolMessage对上2. “Redis 淘汰策略你怎么选”短期记忆 key 通常都有 TTL淘汰策略要满足两点内存紧张时优先丢短期数据而不是误伤其他 key尽量让热点会话“更容易被清掉”。推荐策略volatile-ttl或volatile-lruvolatile-ttl优点优先淘汰“快过期”的 TTL key很契合短期记忆缺点如果 TTL 分布不均可能不是最优的命中率策略volatile-lru / volatile-lfu理由仍然只在 TTL key 集合内淘汰避免误伤非短期数据。更偏“缓存命中率”导向对大量会话、冷热差异明显时更稳。但volatile-lfu的价值在于当容量不足时优先淘汰“冷的 TTL 会话”把内存留给正在活跃的会话用户体验更稳定。3. 并发一致性同一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否则返回“正在生成请稍候”前端生成message_id做幂等去重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_callitem再统一持久化。(OpenAI开发者)初期parallel_tool_callsfalse。(OpenAI开发者)5 Redis 容量与热 key长对话会把 Redis 当“聊天记录数据库”用当某个会话用户连续高频发消息、前端重复重试/重连导致重复请求或同一会话被多个请求并发写入最危险就会让该 thread_id 相关的 Redis key 变成 热点会话问题在 checkpointer 场景里热点会话是“写热点大 value”组合并发写冲突/状态不一致你们已经遇到的tool_calls不完整本质上就可能由并发或中断写入导致。延迟抖动(大对象读写)checkpoint value 变大后序列化/网络传输成本上升热点会话会把这部分成本无限放大。拖累全局吞吐Redis 单线程模型下热点 key 的频繁操作会挤占其他请求。解决方案A.对同一会话强制串行化最有效做法对thread_id加分布式锁或服务端队列同一会话同一时刻只允许一个生成在跑。理由短期记忆是“工作流状态快照”它天然要求顺序一致。串行化能直接解决并发写导致的 checkpoint 半成品tool_calls/ToolMessage对不齐这是比“发现不一致就删线程”更根治的方案。B.限流/防抖按会话或按用户做请求节流做法同一 session N 秒内只允许一次新请求或上一次未结束前禁止新请求。前端加发送按钮“生成中”状态避免连点。理由很多热点会话不是“用户真的需要高并发”而是 UI/网络导致的重复触发。降低无意义写入直接降低 Redis 压力。C.减小单次读写的数据体积大 value 治本做法短期记忆只保留最近N 轮消息窗口化。旧内容做summary用SummarizationMiddleware可以把它落地成“summary 最近 N 轮原文”结构。把“历史记录”职责下沉到 DB已经在 SessionMiddleware 里落库Redis 只存“生成所需的上下文”短窗口 summary tool状态DB 存全量日志用于展示与审计理由热点会话最大的问题之一是“越聊越大”导致每次 checkpoint 读写成本随时间上升。控制 value 大小能让热点的影响“上限化”。“热点会话就是某个 thread_id 对应的 checkpoint 被高频读写。我们通过会话级串行化锁/排队解决一致性通过窗口化摘要控制 value 大小通过限流/去重减少无效写入6.可用性与降级Redis 短期记忆不可用时的策略不明确问题如果 Redis 连接失败当前对话会缺短期记忆可能导致 agent 质量下降或异常1) 立即降级策略保证服务可用方案 A无短期记忆运行最简单、最可靠做法创建 Agent 时checkpointerNone或不传 checkpointer让对话变成“无状态”。适用Redis 故障期间也要保证能回答允许上下文丢失。理由不依赖 Redis稳定性最好代价是多轮上下文能力下降但服务不中断方案 B从 DB 回放最近 N 轮上下文有质量的降级你们已经有 SessionMiddleware 把 user/assistant 消息落库。做法Redis 不可用时从 DB 拉该 session 最近 N 轮消息拼成messages作为上下文输入不再依赖 checkpointer 保存历史。适用希望故障期仍维持多轮对话质量。理由Redis 故障不影响上下文只是 DB 查询成本更高DB 是稳定存储更适合作为“降级数据源”实践中常用组合A兜底可用 B尽量保质量。2) 防雪崩重试、熔断、隔离非常关键快速失败 熔断Circuit Breaker做法Redis 连不上/超时后不要每个请求都去ping/重连进入熔断窗口例如 30s窗口内直接走降级分支理由否则会出现“每个聊天请求都在重试 Redis”导致 CPU/连接数耗尽形成级联故障超时与降级优先做法所有 Redis 操作必须有短超时连接/读写超时立即降级。理由对话是在线链路宁可“少上下文”也不要“卡死”。3) 故障恢复后的策略保证一致性与体验允许“上下文断档”的明确处理做法Redis 恢复后新一轮开始时可以继续无状态直到用户新开会话或从 DB 重建最近 N 轮上下文再继续理由短期记忆的本质就是“可丢”但要让行为可预期避免用户困惑。清理半成品 checkpoint你们已做了一部分做法恢复后针对异常 thread 做清理/回滚避免读到tool_calls半成品。理由Redis 故障/网络抖动期间更容易出现“不完整写入”。