實(shí)戰(zhàn):Redis 會(huì)話狀態(tài)、工具調(diào)用與分布式鎖設(shè)計(jì))
1. 為什么 AI Agent 繞不開(kāi) Redis 這層緩存做過(guò) AI Agent 項(xiàng)目的人大概都有過(guò)這種體驗(yàn)本地跑個(gè) demo 順風(fēng)順?biāo)坏┌巡l(fā)量拉起來(lái)或者把會(huì)話狀態(tài)放到多實(shí)例部署的環(huán)境里整個(gè)系統(tǒng)就開(kāi)始變得又慢又亂。模型調(diào)用本身已經(jīng)夠貴了結(jié)果每次用戶發(fā)一句話Agent 還要重新去查一遍歷史對(duì)話、重新拼一遍工具調(diào)用結(jié)果、重新算一遍向量檢索的中間態(tài)——這些重復(fù)勞動(dòng)堆在一起延遲和成本直接失控。這就是 Redis 在 AI Agent 架構(gòu)里真正的位置。它不是順便加個(gè)緩存而是 Agent 的短期記憶層和狀態(tài)協(xié)調(diào)層。一個(gè)典型的 Agent 循環(huán)是這樣的接收用戶輸入 → 規(guī)劃Planning→ 調(diào)用工具Tool Calling→ 觀察結(jié)果Observation→ 再規(guī)劃 → 輸出。這個(gè)循環(huán)里每一步產(chǎn)生的中間狀態(tài)如果都靠數(shù)據(jù)庫(kù)或者內(nèi)存變量扛著多輪對(duì)話一長(zhǎng)、并發(fā)一高系統(tǒng)就會(huì)崩。Redis 的定位就是把這些高頻讀寫、生命周期短、結(jié)構(gòu)靈活的數(shù)據(jù)接過(guò)來(lái)。我先把這篇要講清楚的東西擺出來(lái)AI Agent 場(chǎng)景下 Redis 到底緩存什么、怎么設(shè)計(jì) key、怎么處理緩存一致性、分布式鎖怎么用、序列化怎么選、線上常見(jiàn)的超時(shí)和失效問(wèn)題怎么排查。適合正在搭 Agent 后端、或者已經(jīng)把 Agent 跑起來(lái)但被性能和狀態(tài)問(wèn)題折磨的開(kāi)發(fā)者。如果你只是想讓 Agent 跑通一個(gè) demo那 Redis 可以先放一放但只要涉及多輪會(huì)話、多實(shí)例部署、工具調(diào)用結(jié)果復(fù)用這層緩存基本躲不掉。有一點(diǎn)要先說(shuō)明白AI Agent 和傳統(tǒng) Web 后端的緩存需求不完全一樣。傳統(tǒng)緩存大多是讀多寫少、結(jié)果穩(wěn)定比如商品詳情、用戶資料。而 Agent 的緩存對(duì)象往往是會(huì)話態(tài)、工具調(diào)用結(jié)果、規(guī)劃中間態(tài)這些東西變化快、和具體會(huì)話強(qiáng)綁定、還經(jīng)常需要設(shè)置較短的 TTL。用傳統(tǒng)那套緩存整個(gè)接口響應(yīng)的思路來(lái)做 Agent 緩存很容易踩坑。下面我按實(shí)際項(xiàng)目里的順序一層層拆。2. Agent 里到底哪些數(shù)據(jù)該進(jìn) Redis很多人一上來(lái)就問(wèn)Redis 緩存怎么配但更該先問(wèn)的是我到底要緩存什么。選錯(cuò)了緩存對(duì)象后面 key 設(shè)計(jì)、過(guò)期策略、一致性方案全是白費(fèi)功夫。我按 Agent 運(yùn)行時(shí)實(shí)際產(chǎn)生的數(shù)據(jù)分幾類說(shuō)。2.1 會(huì)話上下文最核心也最容易失控的部分多輪對(duì)話的上下文是 Agent 緩存的第一大頭。用戶和 Agent 聊了 20 輪第 21 輪進(jìn)來(lái)時(shí)你需要把前面的對(duì)話歷史喂給模型。如果每次都從數(shù)據(jù)庫(kù)全量拉取再拼接延遲會(huì)隨著輪次線性增長(zhǎng)。常見(jiàn)的做法是把會(huì)話上下文以列表結(jié)構(gòu)存在 Redis 里比如用List或者Stream。每輪對(duì)話追加一條讀取時(shí)按需截取最近 N 輪。這里有個(gè)關(guān)鍵決策是緩存完整歷史還是只緩存最近窗口。我的經(jīng)驗(yàn)是Redis 里只保留熱窗口——比如最近 10 到 20 輪更早的歷史落庫(kù)需要時(shí)再按需回?fù)?。原因很?jiǎn)單模型的上下文窗口是有限的你緩存再多它也吃不下反而白白占內(nèi)存。import redis import json r redis.Redis(hostlocalhost, port6379, decode_responsesTrue) def append_turn(session_id, role, content): key fagent:ctx:{session_id} turn json.dumps({role: role, content: content}, ensure_asciiFalse) r.rpush(key, turn) # 只保留最近 20 輪控制內(nèi)存 r.ltrim(key, -20, -1) r.expire(key, 3600) # 1 小時(shí)不活躍就回收 def get_context(session_id): key fagent:ctx:{session_id} turns r.lrange(key, 0, -1) return [json.loads(t) for t in turns]這段代碼看著簡(jiǎn)單但ltrim那行是精髓。我見(jiàn)過(guò)太多項(xiàng)目忘了裁剪結(jié)果一個(gè)長(zhǎng)會(huì)話把 Redis 內(nèi)存吃滿最后觸發(fā)淘汰策略把別的 key 擠掉引發(fā)連鎖故障。2.2 工具調(diào)用結(jié)果省下的是真金白銀Agent 調(diào)用外部工具搜索、數(shù)據(jù)庫(kù)查詢、第三方 API往往又慢又貴。同一個(gè)會(huì)話里用戶可能反復(fù)問(wèn)相關(guān)的問(wèn)題觸發(fā)相同的工具調(diào)用。這時(shí)候把工具調(diào)用結(jié)果按工具名 參數(shù)指紋緩存起來(lái)命中一次就省一次外部調(diào)用。這里要注意的是參數(shù)指紋怎么算。不能簡(jiǎn)單用參數(shù)的 JSON 字符串因?yàn)樽侄雾樞?、空格差異?huì)導(dǎo)致同樣的語(yǔ)義算出不同的 key。穩(wěn)妥的做法是對(duì)參數(shù)做規(guī)范化排序后再哈希。import hashlib def tool_cache_key(tool_name, params: dict): normalized json.dumps(params, sort_keysTrue, ensure_asciiFalse) digest hashlib.sha256(normalized.encode()).hexdigest()[:16] return fagent:tool:{tool_name}:{digest}TTL 設(shè)置上工具結(jié)果的新鮮度要求不同。搜索類結(jié)果可能幾分鐘就過(guò)時(shí)設(shè) 300 秒而一些靜態(tài)查詢比如匯率換算的固定公式結(jié)果可以設(shè)幾小時(shí)。別一刀切。2.3 規(guī)劃中間態(tài)與向量檢索緩存Agent 的 Planning 階段經(jīng)常會(huì)產(chǎn)生一些中間結(jié)構(gòu)比如任務(wù)分解列表、候選工具排序。這些數(shù)據(jù)生命周期極短可能就幾秒到幾十秒但讀寫頻率很高。用 Redis 的String存 JSON 或者Hash存字段都行關(guān)鍵是 TTL 要短。向量檢索這塊要單獨(dú)提一句。RAG 場(chǎng)景下同一個(gè) query 的 embedding 和檢索結(jié)果可以緩存。embedding 計(jì)算是 CPU/GPU 密集操作緩存命中能顯著降延遲。但要注意embedding 緩存對(duì) query 的精確匹配要求高稍微改一個(gè)字就是另一個(gè) key所以命中率取決于用戶提問(wèn)的重復(fù)度。實(shí)際項(xiàng)目里我會(huì)給 embedding 緩存設(shè)較長(zhǎng)的 TTL比如 24 小時(shí)因?yàn)檎Z(yǔ)義相近的 query 復(fù)用價(jià)值高。緩存對(duì)象推薦結(jié)構(gòu)典型 TTL備注會(huì)話上下文List / Stream30min - 2h需裁剪窗口工具調(diào)用結(jié)果String (JSON)5min - 數(shù)小時(shí)按工具特性定規(guī)劃中間態(tài)String / Hash10s - 60s極短生命周期EmbeddingString (二進(jìn)制)數(shù)小時(shí) - 24h注意內(nèi)存占用限流計(jì)數(shù)String (INCR)與窗口對(duì)齊配合過(guò)期3. Key 設(shè)計(jì)與序列化那些看起來(lái)不重要卻天天出事的地方緩存對(duì)象定了接下來(lái)是 key 怎么設(shè)計(jì)、value 怎么序列化。這兩件事在 demo 階段無(wú)所謂上了生產(chǎn)就是天天出問(wèn)題的地方。3.1 Key 命名要有層級(jí)感和可掃描性我推薦用冒號(hào)分隔的層級(jí)命名比如agent:ctx:{session_id}、agent:tool:{tool}:{hash}。好處是可以用SCAN按前綴批量操作也方便在監(jiān)控里按前綴統(tǒng)計(jì)內(nèi)存占用。但有個(gè)坑必須提醒不要用KEYS agent:ctx:*去掃線上 Redis。KEYS是 O(N) 操作會(huì)阻塞整個(gè)實(shí)例。線上要用SCAN游標(biāo)迭代。我見(jiàn)過(guò)有人寫了個(gè)定時(shí)任務(wù)用KEYS清理過(guò)期會(huì)話結(jié)果每次執(zhí)行 Redis 就卡頓幾秒Agent 請(qǐng)求大面積超時(shí)。def scan_keys(pattern, batch100): cursor 0 while True: cursor, keys r.scan(cursorcursor, matchpattern, countbatch) for k in keys: yield k if cursor 0: break另外key 里如果包含用戶可控的內(nèi)容比如 session_id 直接來(lái)自前端一定要做校驗(yàn)或哈希防止有人構(gòu)造超長(zhǎng) key 或者惡意 key 撐爆內(nèi)存。3.2 序列化選型JSON、MessagePack 還是 Pickle序列化這塊我的排序是JSON 優(yōu)先MessagePack 次之Pickle 盡量別用。JSON 的好處是可讀、跨語(yǔ)言、調(diào)試方便。缺點(diǎn)是體積大、數(shù)字精度有損。對(duì)于會(huì)話上下文這種需要人肉排查的數(shù)據(jù)JSON 是首選。MessagePack 體積能壓到 JSON 的 60% 左右序列化速度也快適合工具調(diào)用結(jié)果這種量大且不需要人讀的場(chǎng)景。代價(jià)是調(diào)試時(shí)得額外解碼。Pickle 我基本不用。一是跨語(yǔ)言不友好二是反序列化有安全風(fēng)險(xiǎn)三是 Python 版本升級(jí)容易出兼容問(wèn)題。Agent 項(xiàng)目經(jīng)常要和其他語(yǔ)言的服務(wù)比如 Rust 寫的推理服務(wù)交互Pickle 直接出局。import msgpack def set_tool_result(key, data, ttl300): packed msgpack.packb(data, use_bin_typeTrue) r.set(key, packed, exttl) def get_tool_result(key): raw r.get(key) if raw is None: return None return msgpack.unpackb(raw, rawFalse)提示如果 value 里包含二進(jìn)制數(shù)據(jù)比如 embedding 向量別硬塞進(jìn) JSON用 MessagePack 或者直接存 bytes。JSON 處理二進(jìn)制會(huì)先 base64 編碼體積膨脹 33%。3.3 大 key 與熱 key 的預(yù)防Agent 場(chǎng)景特別容易產(chǎn)生大 key。比如把整個(gè)會(huì)話的所有工具調(diào)用結(jié)果塞進(jìn)一個(gè) Hash聊得越久這個(gè) Hash 越大。大 key 的危害是讀寫慢、刪除時(shí)阻塞、集群模式下數(shù)據(jù)傾斜。預(yù)防手段有幾個(gè)一是拆分把工具結(jié)果按調(diào)用 ID 單獨(dú)存 key會(huì)話里只存引用二是限制單個(gè) value 大小超過(guò)閾值就落庫(kù)三是監(jiān)控用redis-cli --bigkeys或者M(jìn)EMORY USAGE定期排查。熱 key 則是另一個(gè)極端某個(gè) key 被高頻訪問(wèn)把單個(gè)分片的 CPU 打滿。Agent 里常見(jiàn)的熱 key 是全局配置或者熱門工具的結(jié)果。解決辦法是本地緩存加一層或者給 key 加隨機(jī)后綴做分片讀。4. 緩存一致性Agent 狀態(tài)和數(shù)據(jù)庫(kù)怎么不打架這是最容易出事故的部分。Agent 的會(huì)話狀態(tài)既在 Redis 又在數(shù)據(jù)庫(kù)兩邊不同步就會(huì)出現(xiàn)用戶看到的歷史和實(shí)際存的不一樣這種詭異問(wèn)題。4.1 先搞清楚你的讀寫模式一致性方案沒(méi)有銀彈得先看你的讀寫模式。Agent 場(chǎng)景大致分兩種寫少讀多比如工具調(diào)用結(jié)果、embedding寫一次讀很多次。這種用 Cache-Aside 就行讀時(shí)先查緩存沒(méi)有再查庫(kù)并回填。寫多讀多比如會(huì)話上下文每輪都在寫。這種如果還用 Cache-Aside每次寫都要更新庫(kù)再刪緩存延遲高。更實(shí)際的做法是以 Redis 為主存儲(chǔ)異步落庫(kù)。也就是說(shuō)會(huì)話的實(shí)時(shí)狀態(tài)以 Redis 為準(zhǔn)后臺(tái)任務(wù)定期把數(shù)據(jù)同步到數(shù)據(jù)庫(kù)做持久化。def save_turn(session_id, turn): # 先寫 Redis保證實(shí)時(shí)性 append_turn(session_id, turn[role], turn[content]) # 異步落庫(kù)不阻塞主流程 async_persist_queue.put({session_id: session_id, turn: turn})這個(gè)方案的風(fēng)險(xiǎn)是 Redis 掛了會(huì)丟最近的數(shù)據(jù)。所以關(guān)鍵會(huì)話要么用 Redis 的 AOF 持久化要么接受最多丟最近幾輪的業(yè)務(wù)容忍度。這個(gè)取舍必須和產(chǎn)品方對(duì)齊不能自己拍腦袋。4.2 刪除緩存還是更新緩存經(jīng)典問(wèn)題。我的結(jié)論是優(yōu)先刪除緩存而不是更新緩存。原因是更新緩存容易在并發(fā)下產(chǎn)生臟數(shù)據(jù)——兩個(gè)請(qǐng)求同時(shí)更新后寫的可能先到緩存里就留下了舊值。刪除緩存則讓下次讀自然回源邏輯更簡(jiǎn)單。但刪除也有坑就是刪除后瞬間大量請(qǐng)求回源造成緩存擊穿。Agent 場(chǎng)景里如果某個(gè)熱門會(huì)話的緩存被刪緊接著幾十個(gè)請(qǐng)求同時(shí)回源查庫(kù)數(shù)據(jù)庫(kù)可能扛不住。這時(shí)候要用互斥鎖或者邏輯過(guò)期來(lái)兜底。4.3 延遲雙刪與最終一致如果業(yè)務(wù)對(duì)一致性要求高可以用延遲雙刪更新庫(kù) → 刪緩存 → 延遲幾百毫秒再刪一次。第二次刪是為了清掉在更新庫(kù)到第一次刪緩存這個(gè)窗口內(nèi)被其他請(qǐng)求回填的舊值。不過(guò)說(shuō)實(shí)話Agent 場(chǎng)景里大部分?jǐn)?shù)據(jù)對(duì)強(qiáng)一致的要求沒(méi)那么高。會(huì)話上下文差一兩輪、工具結(jié)果稍微舊一點(diǎn)用戶基本無(wú)感。真正需要強(qiáng)一致的是計(jì)費(fèi)、配額、權(quán)限這類數(shù)據(jù)這些我建議直接走數(shù)據(jù)庫(kù)或者用 Redis 的原子操作別摻和緩存那一套。5. 分布式鎖Agent 并發(fā)下的狀態(tài)保護(hù)Agent 多實(shí)例部署后同一個(gè)會(huì)話可能被多個(gè)實(shí)例同時(shí)處理。比如用戶快速發(fā)了兩條消息兩個(gè)實(shí)例同時(shí)讀到同一份上下文各自追加最后就丟了一條。這時(shí)候需要分布式鎖。5.1 用 SET NX EX 實(shí)現(xiàn)基礎(chǔ)鎖Redis 分布式鎖最基礎(chǔ)的實(shí)現(xiàn)就是SET key value NX EX ttl。NX 保證只有第一個(gè)請(qǐng)求能設(shè)置成功EX 保證鎖會(huì)自動(dòng)過(guò)期避免死鎖。import uuid def acquire_lock(lock_key, ttl10): token str(uuid.uuid4()) ok r.set(lock_key, token, nxTrue, exttl) return token if ok else None def release_lock(lock_key, token): # 用 Lua 保證判斷 token 再刪除的原子性 lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end r.eval(lua, 1, lock_key, token)釋放鎖必須用 Lua 腳本先比對(duì) token 再刪除。否則可能出現(xiàn)A 的鎖過(guò)期了B 拿到了鎖這時(shí) A 執(zhí)行完來(lái)刪鎖把 B 的鎖刪了。這個(gè) bug 非常隱蔽線上排查起來(lái)很痛苦。5.2 鎖的粒度別鎖太大也別鎖太小鎖的粒度是個(gè)經(jīng)驗(yàn)活。鎖整個(gè)會(huì)話agent:lock:session:{id}能保證同一會(huì)話串行處理但會(huì)降低并發(fā)。鎖單個(gè)操作比如只鎖追加消息這一步并發(fā)高但可能保護(hù)不全。我的做法是會(huì)話級(jí)的寫操作鎖整個(gè)會(huì)話讀操作不加鎖。因?yàn)樽x多寫少鎖寫不鎖讀能兼顧一致性和吞吐。如果會(huì)話內(nèi)還有更細(xì)的并發(fā)點(diǎn)比如多個(gè)工具并行調(diào)用再單獨(dú)加細(xì)粒度鎖。5.3 鎖超時(shí)與續(xù)期鎖的 TTL 設(shè)多長(zhǎng)是個(gè)難題。設(shè)短了業(yè)務(wù)沒(méi)執(zhí)行完鎖就過(guò)期別的請(qǐng)求進(jìn)來(lái)亂套設(shè)長(zhǎng)了萬(wàn)一持鎖進(jìn)程崩了鎖要等很久才釋放。穩(wěn)妥方案是看門狗續(xù)期持鎖期間起一個(gè)后臺(tái)線程每隔 TTL/3 的時(shí)間檢查業(yè)務(wù)是否還在執(zhí)行是就續(xù)期。Redisson 這類客戶端內(nèi)置了這個(gè)機(jī)制。如果自己實(shí)現(xiàn)要注意續(xù)期和釋放的競(jìng)態(tài)別續(xù)期續(xù)到一個(gè)已經(jīng)被別人拿走的鎖上。注意分布式鎖不是萬(wàn)能的。如果業(yè)務(wù)本身能設(shè)計(jì)成冪等很多場(chǎng)景根本不需要鎖。加鎖之前先問(wèn)自己這個(gè)操作重復(fù)執(zhí)行會(huì)怎樣如果無(wú)害就別加鎖省得引入復(fù)雜度和性能損耗。6. 線上高頻故障超時(shí)、失效與內(nèi)存告警前面講的都是設(shè)計(jì)這一節(jié)講線上真會(huì)遇到的故障。我把最常見(jiàn)的幾類列出來(lái)附上排查思路。6.1 command timed out別急著怪 RedisRedis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException這個(gè)報(bào)錯(cuò)很多人第一反應(yīng)是Redis 掛了。實(shí)際上大部分情況是客戶端側(cè)的問(wèn)題。排查順序我一般這樣走先看 Redis 服務(wù)端慢日志SLOWLOG GET確認(rèn)有沒(méi)有慢命令。再看客戶端連接池配置是不是連接數(shù)不夠?qū)е屡抨?duì)。檢查有沒(méi)有大 key 操作比如HGETALL一個(gè)幾萬(wàn)字段的 Hash??淳W(wǎng)絡(luò)尤其是跨機(jī)房訪問(wèn)RTT 高的時(shí)候默認(rèn)超時(shí)很容易觸發(fā)。Lettuce 默認(rèn)命令超時(shí)是 60 秒但很多框架會(huì)覆蓋成更短的值。Agent 場(chǎng)景里如果某個(gè)工具調(diào)用結(jié)果特別大序列化反序列化耗時(shí)也會(huì)算進(jìn)超時(shí)。我遇到過(guò)一次就是工具返回了幾 MB 的 JSON反序列化慢導(dǎo)致偶發(fā)超時(shí)。后來(lái)改成只緩存必要字段就好了。6.2 緩存失效引發(fā)的雪崩如果大量 key 在同一時(shí)刻過(guò)期請(qǐng)求會(huì)同時(shí)涌向數(shù)據(jù)庫(kù)。Agent 場(chǎng)景里如果所有會(huì)話的 TTL 都是固定 1 小時(shí)且用戶集中在某個(gè)時(shí)段活躍就可能在整點(diǎn)附近觸發(fā)雪崩。解決辦法是給 TTL 加隨機(jī)抖動(dòng)import random def set_with_jitter(key, value, base_ttl): jitter random.randint(0, int(base_ttl * 0.1)) r.set(key, value, exbase_ttl jitter)這樣過(guò)期時(shí)間就分散開(kāi)了。另外熱點(diǎn)數(shù)據(jù)可以用邏輯過(guò)期value 里存一個(gè)過(guò)期時(shí)間戳物理上不過(guò)期讀到邏輯過(guò)期時(shí)異步刷新避免大量請(qǐng)求同時(shí)回源。6.3 內(nèi)存告警與淘汰策略Redis 內(nèi)存滿了會(huì)觸發(fā)淘汰。Agent 場(chǎng)景下如果淘汰策略設(shè)成allkeys-lru可能把正在用的會(huì)話上下文淘汰掉導(dǎo)致用戶失憶。如果設(shè)成noeviction寫入直接報(bào)錯(cuò)。我的建議是會(huì)話上下文這類不能丟的數(shù)據(jù)單獨(dú)放一個(gè)實(shí)例或用單獨(dú)的 DB配置volatile-lru只淘汰設(shè)了過(guò)期時(shí)間的 key。而工具結(jié)果、embedding 這類可重建的數(shù)據(jù)可以容忍淘汰。物理隔離比在一個(gè)實(shí)例里靠策略區(qū)分更可靠。故障現(xiàn)象常見(jiàn)根因排查入口command timed out連接池不足 / 大 key / 網(wǎng)絡(luò) RTTSLOWLOG、連接池監(jiān)控緩存雪崩TTL 集中過(guò)期檢查 TTL 是否加抖動(dòng)用戶會(huì)話丟失內(nèi)存淘汰 / key 被誤刪淘汰策略、內(nèi)存監(jiān)控?cái)?shù)據(jù)不一致雙寫時(shí)序問(wèn)題檢查刪除/更新順序鎖失效TTL 過(guò)短 / 未續(xù)期鎖監(jiān)控、業(yè)務(wù)耗時(shí)統(tǒng)計(jì)6.4 主從與集群下的坑如果用了主從或者集群有幾個(gè)坑要提前知道。主從復(fù)制是異步的主節(jié)點(diǎn)寫入后還沒(méi)同步到從節(jié)點(diǎn)就宕機(jī)鎖可能丟失。集群模式下涉及多個(gè) key 的操作比如 Lua 腳本里操作多個(gè) key要求這些 key 在同一個(gè) slot否則會(huì)報(bào) CROSSSLOT 錯(cuò)誤。解決辦法是用 hash tag比如agent:ctx:{session123}:turns和agent:ctx:{session123}:meta大括號(hào)里的內(nèi)容相同就會(huì)落到同一個(gè) slot。7. 我踩過(guò)的幾個(gè)真實(shí)坑和對(duì)應(yīng)經(jīng)驗(yàn)理論講完了說(shuō)幾個(gè)我自己項(xiàng)目里真踩過(guò)的坑這些在文檔里基本看不到。第一個(gè)坑用 List 存會(huì)話讀取時(shí)全量拉取。一開(kāi)始圖省事LRANGE key 0 -1把整個(gè)會(huì)話拉出來(lái)。會(huì)話短的時(shí)候沒(méi)事聊到幾十輪后每次讀取都要傳輸幾百 KB延遲肉眼可見(jiàn)地漲。后來(lái)改成只拉最近 N 輪更早的按需回?fù)蒲舆t立刻降下來(lái)。教訓(xùn)是緩存讀取要按需別圖省事全量拉。第二個(gè)坑工具結(jié)果緩存沒(méi)做參數(shù)規(guī)范化。同樣的查詢前端傳參字段順序不一樣算出來(lái)的 key 就不一樣緩存命中率極低。加了sort_keysTrue之后命中率從 20% 漲到 70% 多。這個(gè)改動(dòng)一行代碼收益巨大。第三個(gè)坑分布式鎖 TTL 設(shè)太短。有個(gè)工具調(diào)用偶爾要跑 15 秒鎖 TTL 設(shè)了 10 秒結(jié)果鎖提前過(guò)期兩個(gè)請(qǐng)求同時(shí)寫會(huì)話數(shù)據(jù)錯(cuò)亂。后來(lái)加了續(xù)期機(jī)制才解決。教訓(xùn)是鎖 TTL 要大于業(yè)務(wù) P99 耗時(shí)且要有續(xù)期兜底。第四個(gè)坑監(jiān)控缺失。上線初期沒(méi)做緩存命中率監(jiān)控出了問(wèn)題只能靠猜。后來(lái)加了命中率、平均耗時(shí)、大 key 數(shù)量這幾個(gè)指標(biāo)排查效率完全不一樣。Agent 緩存這種動(dòng)態(tài)性強(qiáng)的場(chǎng)景沒(méi)有監(jiān)控基本等于盲開(kāi)。第五個(gè)坑把 embedding 向量用 JSON 存。一個(gè) 1536 維的向量JSON 序列化后體積膨脹好幾倍內(nèi)存消耗遠(yuǎn)超預(yù)期。改成二進(jìn)制存儲(chǔ)后內(nèi)存占用直接砍半。涉及數(shù)值數(shù)組的場(chǎng)景一定要用二進(jìn)制序列化。8. 一套可落地的 Agent 緩存分層方案最后把我實(shí)際項(xiàng)目里用的分層方案整理出來(lái)可以直接參考。第一層本地緩存進(jìn)程內(nèi)。存全局配置、工具元數(shù)據(jù)這類幾乎不變的數(shù)據(jù)。用 LRU 結(jié)構(gòu)容量小命中快不占 Redis 資源。第二層Redis 熱數(shù)據(jù)。存會(huì)話上下文、工具結(jié)果、規(guī)劃中間態(tài)。按數(shù)據(jù)類型分 DB 或分實(shí)例配置不同的淘汰策略和 TTL。第三層數(shù)據(jù)庫(kù)持久化。會(huì)話歷史、重要工具結(jié)果落庫(kù)作為最終真相來(lái)源。Redis 里的數(shù)據(jù)丟了可以從這里重建。讀寫路徑上讀優(yōu)先本地再 Redis最后數(shù)據(jù)庫(kù)寫以 Redis 為主異步落庫(kù)。鎖只在會(huì)話級(jí)寫操作上加讀不加鎖。TTL 全部加抖動(dòng)熱點(diǎn)數(shù)據(jù)用邏輯過(guò)期。這套方案不是最優(yōu)解但在多實(shí)例、中等并發(fā)、成本敏感的 Agent 項(xiàng)目里跑得比較穩(wěn)。具體參數(shù)還得根據(jù)你的 QPS、數(shù)據(jù)量、一致性要求去調(diào)。我個(gè)人的體會(huì)是Agent 緩存治理這件事設(shè)計(jì)階段多想一步比上線后救火省十倍力氣。尤其是 key 設(shè)計(jì)和 TTL 策略改起來(lái)牽一發(fā)動(dòng)全身一開(kāi)始就定好規(guī)矩后面會(huì)輕松很多。