缓存与多级存储:别让 AI 无脑直查数据库


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 的双轨缓存架构

当前 miniagentServiceContainer 中就明确维护了两套缓存管理体系:

miniagent的两种缓存架构

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

流程可以理解为:

Request Runtime Object

Object Cache

存在?

返回

Builder

构建对象

写入缓存


对象缓存必须处理“构建击穿”

假设两个请求同时需要:

AgentRunner(agent_id=10)

对象还没创建。

如果只是:

if key not in cache:
    cache[key] = await build()

可能出现:

Request A → build
Request B → build
Request C → build

同一个昂贵对象被创建三遍。

这本质上就是一种:

Cache Breakdown(缓存击穿)

只不过这里击穿的不是数据库,而是:

昂贵对象构建过程。

miniagentAsyncLazyCache 为每个 Key 使用:

asyncio.Lock()

并采用双重检查:

async with self._locks[key]:

    if key in self._store:
        return self._store[key]

    value = await self._builder(...)

于是:

获得锁

等待

Request A

同一个 Key

Request B

Request C

Single Flight

Request A
构建对象

Request B / C
等待

写入缓存

Request A
返回对象

Request B / C
直接复用缓存对象

这里的:

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
    )

本质上是:

LLM Config Changed

Dependency Graph

Web Search

SQL Agent

Agent Runner

KB Retrieval

这比:

cache.clear()

稳妥得多。

因为它知道:

哪些对象依赖哪些配置。


2. 值缓存,解决“不要重复查数据”

值缓存是我们平时最熟悉的缓存。

它缓存的是:

Permission Set
Query Result
...

它的核心目标是:

避免重复数据库查询或重复计算。


值缓存采用 Cache-Aside

一个典型值缓存流程:

Request

Value Cache

命中?

返回

Database

取得值

写入缓存

这种模式通常叫:

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
...

不断增长。

所以 miniagentMemoryCacheStore 使用:

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)

也就是:

Yes

No

Permission Request

Value Cache

命中?

返回权限

Database

User → Role → Permission

写缓存

这样就避免了:

每个 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

miniagentMemoryCacheStore 已经维护:

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。


开源代码


🪐祝您好运🪐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值