文章目录
AI 写业务代码时,有一种非常典型的倾向:
async def get_user(user_id: int):
return await user_db.get_by_id(user_id)
功能没错。但如果项目里所有读取都变成:
查用户 → 数据库
查权限 → 数据库
查模型 → 数据库
查 Agent → 数据库
查知识库 → 数据库
查配置 → 数据库
系统很快就会进入一种状态:
每个函数都很正确,整体架构却越来越低效。
问题不只是“有没有缓存”,而是:
AI 没有先判断:我要复用的是一个“值”,还是一个“已经构建好的运行对象”?
这正是 miniagent 当前缓存架构最值得借鉴的地方。
它不是简单地做一套缓存,而是明确分成两类:
Object Cache
对象缓存
Value Cache
值缓存
两者解决的根本不是同一个问题。
📌 技术名片
Cache / 缓存
把获取成本较高、又可能被重复使用的内容暂时保存起来,以减少重复构建、重复计算或重复访问底层存储。
Object Cache / 对象缓存
缓存已经构建完成的运行时对象,例如:
AgentRunner、Pipeline、Router、Manager。
它解决的是:
不要重复创建昂贵对象。
Value Cache / 值缓存
缓存普通数据,例如:
权限集合、查询结果、JSON、字符串、计算结果。
它解决的是:
不要重复查询和重复计算。
Multi-Level Storage / 多级存储
根据速度、容量、成本和持久性,把数据放在不同层次,例如:
L1 本地内存 → L2 Redis → Database。
这里的:
L1(Level 1,第一级) 通常是进程内存;
L2(Level 2,第二级) 通常是 Redis 一类共享缓存;
Database(数据库) 则作为最终权威数据源。
💡 通俗理解:缓存不只是“仓库”,还有“装配好的机器”
可以把软件系统想象成一家工厂,数据库像原材料仓库。
如果员工每次需要一把电钻,都跑到仓库:
取零件
↓
装马达
↓
装钻头
↓
调试
↓
使用
显然很浪费。更加合理的是:
第一次:
仓库 → 组装电钻 → 使用
以后:
直接拿现成电钻 → 使用
这就是:
对象缓存。
而另一种情况是:
员工只想查:
今天库存还剩多少?
第一次去仓库确认:
库存 = 128
短时间内其他人再问,就没必要每个人都重新盘点仓库。
可以先记下来:
库存缓存 = 128
这就是:
值缓存。
所以两种缓存虽然名字相似,本质完全不同:
对象缓存
→ 缓存“已经组装好的东西”
值缓存
→ 缓存“已经查出来的数据”
这正是 miniagent 的双轨缓存思路。
为什么不能只讲“加 Redis”?
很多 AI 遇到缓存问题时,会出现另一个极端:
“可以加 Redis。”
Redis 的确很常用,但它并不能解决所有缓存问题。
比如 miniagent 中的:
AgentRunner
SmartRouter
WebSearchPipeline
KBRetrievalPipeline
VectorStoreManager
这些并不是普通 JSON。它们内部可能持有:
LLM Client
Tool instances
Repository references
asyncio.Lock
Database objects
Runtime state
Pipeline objects
这类对象,通常:
不能简单序列化
不能直接跨进程复用
依赖当前进程资源
所以它们更适合:
Process-Local Object Cache(进程本地对象缓存)
而不是:
Redis.set("agent_runner", ...)
因此,缓存架构首先应该回答的不是:
用不用 Redis?
而是:
我缓存的到底是什么?
一、miniagent 的双轨缓存架构
当前 miniagent 在 ServiceContainer 中就明确维护了两套缓存管理体系:

self.cache_registry = ObjectCacheRegistry()
self.object_cache_invalidator = ObjectCacheInvalidator(
self.cache_registry
)
from app.infra.cache.store_registry import (
cache_registry as value_cache_registry
)
self.value_cache_registry = value_cache_registry
1. 对象缓存,解决“不要重复构建”
对象缓存适合什么?
典型场景:
AgentRunner
WebSearchPipeline
SQLAgent
SmartRouter
KBRetrievalPipeline
VectorStoreManager
这些 Runtime Object(运行时对象)的构建过程可能包括:
读取数据库配置
↓
创建 LLM Client
↓
加载工具
↓
组装 Pipeline
↓
绑定 Repository
↓
建立 Runtime
↓
完成初始化
如果每一次请求都执行一遍:
Request
↓
build()
↓
use()
↓
destroy
那大量 CPU、I/O 和初始化成本都会浪费在重复构建上。
所以 miniagent 的思路是:
第一次使用
↓
没有缓存
↓
构建对象
↓
放入 Object Cache
↓
以后直接复用
对象缓存的核心:AsyncLazyCache
miniagent 的对象缓存核心位于:
app/runtime/cache/lazy_cache.py
其中:
AsyncLazyCache[K, V]
提供:
get_or_build()
语义就是:
有就返回,没有就构建。
核心逻辑:
async def get_or_build(self, key, *args, **kwargs):
if key in self._store:
return self._store[key]
...
value = await self._builder(
key,
*args,
**kwargs
)
self._store[key] = value
return value
流程可以理解为:
对象缓存必须处理“构建击穿”
假设两个请求同时需要:
AgentRunner(agent_id=10)
对象还没创建。
如果只是:
if key not in cache:
cache[key] = await build()
可能出现:
Request A → build
Request B → build
Request C → build
同一个昂贵对象被创建三遍。
这本质上就是一种:
Cache Breakdown(缓存击穿)
只不过这里击穿的不是数据库,而是:
昂贵对象构建过程。
miniagent 的 AsyncLazyCache 为每个 Key 使用:
asyncio.Lock()
并采用双重检查:
async with self._locks[key]:
if key in self._store:
return self._store[key]
value = await self._builder(...)
于是:
这里的:
Single Flight(单飞机制)
指的是:
同一个 Key 同一时间只允许一个任务负责加载或构建。
这是 miniagent 对象缓存非常重要的设计。
对象缓存为什么通常不依赖 TTL?
对象缓存和值缓存在这里开始明显分叉。
比如:
AgentRunner
并不是因为:
1 小时到了
就自然失效。
真正导致它失效的通常是:
Agent 配置修改
LLM 修改
Tool 修改
KB 修改
Embedding 修改
Router 配置修改
也就是说:
对象的有效性主要取决于“依赖配置有没有改变”。
因此更适合:
Event-Driven Invalidation(事件驱动失效)
而不是:
TTL(Time To Live,生存时间)
miniagent 如何做对象缓存失效?
miniagent 专门设计了:
CacheInvalidationService
位于:
app/runtime/cache/invalidation.py
例如 Agent 配置变化:
def on_agent_changed(self, agent_id):
if agent_id:
self.registry.invalidate(
CacheType.AGENT_RUNNER,
agent_id
)
于是:
Agent Changed
↓
AgentRunner 已过时
↓
Invalidate
↓
下一次调用
↓
重新 Build
这就是典型的:
配置驱动 Runtime 重建。
对象缓存失效其实是“依赖图治理”
比如 LLM 配置修改。
它影响的可能不只是:
LLM Runtime
还可能影响:
WebSearchPipeline
SQLAgent
AgentRunner
KBRetrievalPipeline
所以 miniagent 会统一:
def on_llm_changed(self):
self.registry.invalidate_all(
CacheType.WEB_SEARCH_PIPELINE
)
self.registry.invalidate_all(
CacheType.SQL_AGENT
)
self.registry.invalidate_all(
CacheType.AGENT_RUNNER
)
self.registry.invalidate_all(
CacheType.KB_RETRIEVAL_PIPELINE
)
本质上是:
这比:
cache.clear()
稳妥得多。
因为它知道:
哪些对象依赖哪些配置。
2. 值缓存,解决“不要重复查数据”
值缓存是我们平时最熟悉的缓存。
它缓存的是:
Permission Set
Query Result
...
它的核心目标是:
避免重复数据库查询或重复计算。
值缓存采用 Cache-Aside
一个典型值缓存流程:
这种模式通常叫:
Cache-Aside Pattern(旁路缓存模式)
应用程序自己控制:
先读 Cache
↓
Miss 后读 Database
↓
再写 Cache
miniagent 的 Value Cache 基础设施
它位于:
app/infra/cache/
├── factory.py
├── memory.py
└── store_registry.py
统一通过:
create_cache_backend(...)
创建。
例如:
create_cache_backend(
namespace="auth",
backend_type="memory",
)
目前支持:
MemoryCacheStore
同时为未来:
Redis
预留接口。
这意味着业务代码依赖的是:
缓存能力
而不是:
某个具体缓存产品。
值缓存需要 LRU
值缓存最容易失控的方式是:
cache[key] = value
然后永远不删。
于是:
100
1000
10000
100000
...
不断增长。
所以 miniagent 的 MemoryCacheStore 使用:
LRU(Least Recently Used,最近最少使用)
self._cache = LRUCache(
maxsize=max_size
)
缓存满以后:
最久没有使用的数据优先被淘汰。
值缓存需要 TTL
值缓存还面临:
数据会过时。
比如用户权限。
数据库已经修改,但缓存还是旧值。
因此 miniagent 提供:
mset_with_ttl()
mget_ttl()
通过:
TTL(Time To Live,生存时间)
限制数据最长可使用时间。
例如:
TTL = 3600 秒
过期以后:
Cache Miss
↓
重新查询数据库
↓
重新缓存
这对于普通数据缓存非常合理。
miniagent 的权限缓存就是典型值缓存
在 AuthPermission 中:
cached = self._cache.mget_ttl(
[self._cache_key(user_id)]
)[0]
if cached is not None:
return self._decode(cached)
return await self._load_permissions(user_id)
也就是:
这样就避免了:
每个 API 请求都重新查一遍 RBAC 权限链。
值缓存不仅要 TTL,还要主动失效
只靠 TTL 不够。
比如管理员刚刚撤销用户权限。
如果:
TTL = 3600 秒
理论上旧权限可能继续存在近一小时。
因此还要:
Invalidate Cache(主动失效缓存)
权限变化时:
Database Update
↓
Invalidate Value Cache
↓
下一次请求
↓
Cache Miss
↓
重新加载最新权限
所以一个完整值缓存必须同时考虑:
Read
Write
TTL
Invalidate
Capacity
Stats
而不是只有:
get / set
值缓存还会遇到缓存穿透
假设有人反复请求:
user_id = 999999999
缓存没有:
MISS
数据库也没有。
下一次:
MISS
↓
Database
还是没有。
这就是:
Cache Penetration(缓存穿透)
即:
查询一个本来就不存在的数据,导致请求不断穿过缓存访问数据库。
缓存穿透怎么处理?
一种简单办法:
Negative Cache(负缓存 / 空值缓存)
例如:
user:999999999 = NOT_FOUND
TTL = 60 秒
下一次:
Cache Hit
↓
NOT_FOUND
↓
直接返回
数据库不用再次查询。
空值缓存 TTL 通常应该短一点。
例如:
正常值:3600 秒
空值:60 秒
避免数据刚刚创建后仍被旧的空值阻挡太久。
值缓存也会遇到热点 Key 击穿
假设:
system_config
是高频热点。
正常:
1000 Requests
↓
Cache
突然 TTL 到期:
Cache Expired
于是:
Request A ─┐
Request B ─┤
Request C ─┤
... ├──→ Database
Request N ─┘
这就是:
Cache Breakdown(缓存击穿)
解决方式仍然可以使用:
Single Flight / Per-Key Lock
也就是说:
大量 Miss
↓
同 Key 只允许一个请求回源
↓
其他请求等待
↓
缓存重建
↓
统一返回
miniagent 的 Object Cache 已经原生具备这类 Single Flight 思想。
未来如果某些 Value Cache 成为高并发热点,也可以沿用这个思路。
缓存雪崩也属于 Value Cache 更典型的问题
如果 AI 为所有缓存统一:
ttl = 3600
大量数据又在相近时间写入:
11:00 写入
↓
12:00 集中过期
就可能出现:
Cache Avalanche(缓存雪崩)
也就是:
大量缓存同时失效,大批请求瞬间落到数据库。
缓解方法包括:
TTL Jitter
TTL 随机抖动
Multi-Level Cache
多级缓存
Rate Limiting
限流
3. 为什么 Object Cache 不适合 Redis,而 Value Cache 很适合?
看一个最简单的对比。
AgentRunner
可能包含:
LLM Client
Tool
asyncio.Lock
Repository
Pipeline
Runtime State
特点:
复杂
不可直接序列化
依赖当前进程
适合:
Local Memory
Permission Set
例如:
[
"system:user:list",
"system:user:create"
]
特点:
简单
可序列化
跨进程可共享
适合:
Memory
↓
Redis
因此未来 miniagent 横向扩展时,更合理的是:
Object Cache
→ 仍保持每个进程各自一份 Runtime
Value Cache
→ 可以演进为 Redis 共享缓存
这才是符合对象性质的设计。
4. miniagent 还把两种缓存的 Registry 分开了
Object Cache Registry
负责:
有哪些 Runtime Cache?
失效哪个 Runtime Object?
全部失效?
按条件失效?
查看 Runtime Cache 状态?
对应:
CacheRegistry
支持:
invalidate
invalidate_all
invalidate_where
stats
list_names
Value Cache Registry
负责:
有哪些 namespace?
底层 backend 是什么?
当前多少 key?
命中率多少?
删除哪些 key?
清空哪个 namespace?
对应:
CacheStoreRegistry
这两套 Registry 的职责实际上非常清晰:
Object Registry
→ Runtime 生命周期治理
Value Registry
→ Key-Value 数据治理
5. 缓存必须可观测
AI 加缓存时还容易忽略一个问题:
缓存到底有没有用?
至少应该知道:
Hits
Misses
Hit Rate
Current Size
TTL Expirations
miniagent 的 MemoryCacheStore 已经维护:
self._hits
self._misses
self._ttl_expirations
并通过:
get_stats()
返回:
current_size
hits
misses
hit_rate
ttl_expirations
所以缓存不是:
“感觉应该快一点了。”
而应该能回答:
到底命中了多少?
二、给 AI 建立明确的缓存规则
可以把下面这段加入 Project Rules。
## 缓存架构
miniagent 使用两个不同的缓存系统:
1. 对象缓存
2. 值缓存
切勿混淆它们的职责。
### 对象缓存
对象缓存用于存储开销较大的运行时对象,例如:
- AgentRunner
- 管道
- 路由器
- 向量存储管理器
- 运行时组件
规则:
- 使用 AsyncLazyCache。
- 使用延迟构建。
- 使用单次构建模式,防止重复并发构建。
- 将运行时对象保持在进程本地。
- 不要将运行时对象序列化到 Redis。
- 优先使用事件驱动的失效机制。
- 当依赖配置发生更改时,使受影响的运行时对象失效。
### 值缓存
值缓存用于存储可序列化的值,例如:
- 权限集
- 查询结果
- 计算结果
- 简单数据对象
规则:
- 使用共享缓存后端抽象。
- 使用命名空间隔离不同的域。
- 应用容量限制,例如最近最少使用 (LRU) 算法。
- 在数据可能过期的情况下使用生存时间 (TTL)。
- 在重要写入操作后使相关键失效。
- 考虑对缺失数据进行负缓存。
- 必要时保护热键免受缓存失效的影响。
- 分布式后端(例如 Redis)应包含在此处。
### 存储层次结构
值缓存可能演化为:
L1 内存
→ L2 Redis
→ 数据库
对象缓存是一个独立的运行时生命周期系统,
不应被视为另一个 L1/L2 值缓存层。
可以概括为:
复杂运行对象走 Object Cache;普通数据走 Value Cache。
对象缓存按配置变化失效,值缓存按 TTL 和数据变化失效。
不要因为都叫 Cache,就让 AI 用同一套逻辑处理。
三、提示词落地:不要只说“给它加缓存”
错误提示词:
给这个功能加缓存。
AI 根本不知道应该是哪一种。
更好的提示词是:
请先判断该场景属于 Object Cache 还是 Value Cache。
如果缓存的是构建成本较高的 Runtime Object:
1. 复用现有 AsyncLazyCache;
2. 使用 get_or_build;
3. 保留 Single Flight;
4. 不引入 TTL 作为主要失效方式;
5. 明确依赖哪些配置;
6. 配置变化时通过 ObjectCacheInvalidator 精准失效。
如果缓存的是普通可序列化数据:
1. 复用现有 Value Cache Backend;
2. 使用独立 namespace;
3. 明确 key;
4. 明确 max_size;
5. 明确 TTL;
6. 明确数据变更后的 invalidate;
7. 判断是否存在缓存穿透;
8. 判断热点 key 是否存在击穿风险;
9. 不在业务代码私自创建 dict/LRU 缓存。
这样 AI 在动手之前,先做一个最重要的架构判断:
我缓存的是对象,还是值?
总结
缓存与多级存储真正要纠正的,不只是:
AI 无脑直查数据库。
还包括另一种同样常见的问题:
AI 无脑重复创建昂贵对象。
所以在 miniagent 中,更准确的缓存理念应该是:
Object Cache
解决“不要重复造”
Value Cache
解决“不要重复查”
对象缓存通过:
AsyncLazyCache
+
Lazy Build
+
Single Flight
+
Event Invalidation
管理 Runtime Object 生命周期。
值缓存通过:
Cache Backend
+
Namespace
+
LRU
+
TTL
+
Invalidate
+
Stats
减少数据库和计算压力。
而 Value Cache 未来还可以自然扩展成:
L1 Memory
↓
L2 Redis
↓
Database
这才是一套完整而清晰的缓存与多级存储架构。
对 AI 编程来说,真正应该写进规则的不是:
“记得使用缓存。”
而是:
先判断你是在重复“造对象”,还是重复“查数据”;然后使用对应的缓存体系,而不是随手往业务代码里塞一个 dict。
开源代码
🪐祝您好运🪐

7156

被折叠的 条评论
为什么被折叠?



