链上 AI 模型缓存层设计:推理结果复用、热度淘汰策略与存储成本优化 链上 AI 模型缓存层设计推理结果复用、热度淘汰策略与存储成本优化一、链上 AI 的推理成本危机为什么每次推理都不应该是新请求链上 AIOn-chain AI正在从概念验证走向生产应用。无论是用 ZKML 验证模型推理的正确性还是通过预言机将 AI 结果写入合约最终都面临同一个成本问题推理是一个昂贵操作。一次 GPT-3.5 级别的 API 调用成本约 $0.002一次 GPT-4 调用约 $0.03——当这些推理被嵌入到 DeFi 策略分析、链上风控或自动化交易中时累积成本可能超过交易本身的 Gas 费。更关键的是很多推理请求是重复的同一个用户的同一个查询在短时间内发起了 3 次10 个用户对同一个 Token 发起了风险评估查询——这些请求返回基本相同的结果。如果每次推理都不加甄别地执行完整计算等于在重复支付同一笔账单。缓存策略在 Web2 中已经是一门成熟的技术但链上 AI 场景有独特的约束缓存层本身需要考虑存储成本链上/链下的选择、数据一致性链上状态变化导致缓存失效、以及去中心化架构下的缓存同步问题。flowchart TB subgraph 请求层 U1[用户1: Token风险评估] U2[用户2: Token风险评估] U3[用户3: 策略参数推荐] U4[用户4: Token风险评估] end subgraph 缓存系统 DIR[请求路由器] DIR -- CACHE_CHECK{缓存命中?} CACHE_CHECK --|命中| HIT[直接返回缓存结果] CACHE_CHECK --|未命中| COMPUTE[执行AI推理] COMPUTE -- STORE[存入缓存] STORE -- RETURN[返回结果] HEALTH[缓存健康检查] -- EVICT[淘汰过期条目] HEALTH -- WARM[预热热点数据] end subgraph 存储层 L1_CACHE[L1: 合约内映射br/最热数据/高频调用br/存储成本最高] L2_CACHE[L2: 链下KV存储br/Redis/内存DBbr/秒级TTL] L3_CACHE[L3: IPFS/Arweavebr/长期存档/审计br/不可变存储] end HIT -- L1_CACHE HIT -- L2_CACHE COMPUTE -- L2_CACHE STORE -- L3_CACHE subgraph 失效策略 CHAIN_STATE[链上状态变更事件] -- INVALIDATE[标记缓存失效] TTL[TTL计时器到期] -- INVALIDATE VERSION[模型版本更新] -- INVALIDATE end INVALIDATE -- L1_CACHE INVALIDATE -- L2_CACHE二、缓存分层设计与热度淘汰算法2.1 三层缓存架构缓存设计遵循冷热分层原则L1合约内映射约 100-500 条存储成本最高存储在合约的mapping中访问延迟接近 0一个 SLOAD 操作但每字节存储成本极高。适合缓存调用频率极高且参数组合有限的推理结果如常用 Token 对的价格预测、热门协议的 TVL 趋势分析。写入限制仅在聚合多次请求后批量写入如每 100 次相同查询才写入一次合约存储避免每次推理请求都触发一次 SSTORE。L2链下 KV 存储约 10000-50000 条毫秒级访问使用 Redis 或兼容的内存数据库部署在预言机网络或 rollup 的 sequencer 节点上。这是主缓存层——大部分推理结果缓存在这里通过 REST/gRPC API 供链上合约通过预言机查询。L3IPFS/Arweave无限条不可变永久存档用于存储推理结果的完整上下文input output 时间戳 模型版本。不是直接用于请求响应的缓存而是用于审计、争议仲裁、模型重训练的数据源。2.2 缓存键设计缓存键必须包含决定推理结果的所有相关变量cache_key keccak256(model_id || input_hash || context_hash || version)其中model_id模型标识区分 GPT-4、本地 BERT 模型等input_hash输入数据的哈希注意相同语义但不同格式的输入需先标准化context_hash链上状态的哈希如当前区块号、相关合约的状态根version模型版本号模型升级时自动失活旧缓存2.3 淘汰策略Adaptive TTL Bloom Filter单纯使用固定 TTL 不适应 AI 推理场景——有些结果在几秒内就失效链上状态高频变化有些结果几天内都有效历史数据的统计分析。Adaptive TTL每个缓存条目携带一个额外的波动率分数volatility_score根据输入数据的特征动态计算 TTL基于实时链上数据的推理 → TTL 5-30 秒基于历史统计的推理 → TTL 1-24 小时波动率分数超过阈值 → TTL 自动减半Bloom Filter 加速失效检查在缓存层维护一个 Bloom Filter记录哪些链上事件会导致某些缓存失效。当检测到事件时通过 Bloom Filter 快速识别受影响的缓存键集合而不是扫描整个缓存。三、代码实现链上 AI 缓存层// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /** * 链上 AI 推理结果缓存合约 * * 设计决策 * - 采用多级缓存链上映射热数据 链下预言机缓存温数据 * - 缓存键设计包含模型ID、输入哈希、上下文哈希、版本号 * - Adaptive TTL根据推理的输入类型动态计算失效时间 * - 批量写入机制聚合多次请求后一次性写入存储 * - Bloom Filter 用于快速失效检查 */ contract AICacheLayer { /// 缓存条目结构 struct CacheEntry { bytes32 resultHash; // 推理结果的哈希完整结果存链下 uint256 createdAt; // 创建时间戳 uint256 expiresAt; // 过期时间戳 uint8 accessCount; // 被访问次数用于热度统计 bool exists; // 是否存在mapping 无法区分零值和不存在 } /// 推理类型枚举不同推理类型有不同的默认 TTL enum InferenceType { PRICE_PREDICTION, // 价格预测TTL 30秒 RISK_ASSESSMENT, // 风险评估TTL 60秒 SENTIMENT_ANALYSIS, // 情绪分析TTL 300秒 STATISTICAL_REPORT // 统计报告TTL 3600秒 } /// 缓存存储 mapping(bytes32 CacheEntry) public cache; /// Bloom Filter 用于快速失效检查 /// 设计决策使用 uint256 位图作为简化版 Bloom Filter /// 每个事件类型映射到位图的一个位 mapping(bytes32 uint256) public invalidationBloom; /// 管理员 address public admin; /// 预言机地址授权写入缓存 mapping(address bool) public oracles; /// 默认 TTL 配置按推理类型 mapping(InferenceType uint256) public defaultTTL; /// 事件 event CacheSet(bytes32 indexed cacheKey, bytes32 resultHash, uint256 expiresAt); event CacheHit(bytes32 indexed cacheKey, uint8 accessCount); event CacheInvalidated(bytes32 indexed cacheKey, bytes32 reason); modifier onlyOracle() { require(oracles[msg.sender], Not authorized oracle); _; } modifier onlyAdmin() { require(msg.sender admin, Only admin); _; } constructor() { admin msg.sender; // 初始化默认 TTL defaultTTL[InferenceType.PRICE_PREDICTION] 30; defaultTTL[InferenceType.RISK_ASSESSMENT] 60; defaultTTL[InferenceType.SENTIMENT_ANALYSIS] 300; defaultTTL[InferenceType.STATISTICAL_REPORT] 3600; } /** * 生成缓存键 * param _modelId 模型标识符 * param _inputHash 输入数据的 keccak256 哈希 * param _contextHash 链上上下文哈希区块号、状态根等 * param _version 模型版本 * * 设计决策将所有相关变量哈希为一个唯一的缓存键 * 任何变量的变化都会产生不同的键 → 自动失活 */ function generateCacheKey( bytes32 _modelId, bytes32 _inputHash, bytes32 _contextHash, uint256 _version ) public pure returns (bytes32) { return keccak256(abi.encodePacked( _modelId, _inputHash, _contextHash, _version )); } /** * 查询缓存带自动更新访问计数 * return resultHash 推理结果的哈希如果命中 * return valid 是否命中有效缓存 */ function queryCache( bytes32 _cacheKey ) external view returns (bytes32 resultHash, bool valid) { CacheEntry storage entry cache[_cacheKey]; if (!entry.exists) { return (bytes32(0), false); } if (block.timestamp entry.expiresAt) { return (bytes32(0), false); } return (entry.resultHash, true); } /** * 写入缓存仅预言机可调用 * param _cacheKey 缓存键 * param _resultHash 推理结果哈希 * param _inferenceType 推理类型 * param _volatilityScore 波动率分数0-100越高表示越不稳定 * * 设计决策 * - Adaptive TTL defaultTTL × volatilityFactor * - volatilityScore 70 时 TTL 减半 30 时 TTL 翻倍 * - accessCount 初始为 1本次写入即首次访问 */ function setCache( bytes32 _cacheKey, bytes32 _resultHash, InferenceType _inferenceType, uint8 _volatilityScore ) external onlyOracle { uint256 baseTTL defaultTTL[_inferenceType]; // Adaptive TTL 计算 uint256 adjustedTTL; if (_volatilityScore 70) { adjustedTTL baseTTL / 2; // 高频波动 → 缩短 TTL } else if (_volatilityScore 30) { adjustedTTL baseTTL * 2; // 低频波动 → 延长 TTL } else { adjustedTTL baseTTL; } // 最小 TTL 保证不低于 5 秒避免缓存抖动 if (adjustedTTL 5) adjustedTTL 5; cache[_cacheKey] CacheEntry({ resultHash: _resultHash, createdAt: block.timestamp, expiresAt: block.timestamp adjustedTTL, accessCount: 1, exists: true }); emit CacheSet(_cacheKey, _resultHash, block.timestamp adjustedTTL); } /** * 批量失效缓存 * param _eventType 导致失效的事件类型 * param _affectedKeys 受影响的缓存键列表 * * 设计决策 * - 同时更新 Bloom Filter 的对应位 * - 批量失效使用循环Gas 成本可控usually 不过几十个键 * - 删除时不退 Gas但节省后续存储空间 */ function batchInvalidate( bytes32 _eventType, bytes32[] calldata _affectedKeys ) external onlyOracle { // 更新 Bloom Filter invalidationBloom[_eventType] uint256(keccak256( abi.encodePacked(invalidationBloom[_eventType], block.timestamp) )); for (uint256 i 0; i _affectedKeys.length; ) { if (cache[_affectedKeys[i]].exists) { delete cache[_affectedKeys[i]]; emit CacheInvalidated(_affectedKeys[i], _eventType); } unchecked { i; } } } /** * 检查缓存是否可能被某个事件类型失效Bloom Filter 快速检查 * 设计决策Bloom Filter 可能产生假阳性但不会假阴性 * 如果 Bloom 检查为 false → 缓存一定有效 * 如果 Bloom 检查为 true → 需要进一步验证 */ function mayBeInvalidated(bytes32 _eventType) external view returns (bool) { return invalidationBloom[_eventType] ! 0; } /** * 记录缓存命中外部合约访问缓存时调用 * 设计决策accessCount 用于统计热度和识别冷缓存低访问频率 * 超过 255 次访问后保持不变uint8 上限不会溢出 */ function recordHit(bytes32 _cacheKey) external { CacheEntry storage entry cache[_cacheKey]; if (entry.exists entry.accessCount 255) { unchecked { entry.accessCount; } } emit CacheHit(_cacheKey, entry.accessCount); } } /** * 缓存统计与监控合约 * * 设计决策分离统计逻辑到独立合约避免污染缓存合约的核心状态 */ contract CacheAnalytics { AICacheLayer public cacheContract; /// 统计计数器 uint256 public totalHits; uint256 public totalMisses; uint256 public totalSets; uint256 public totalInvalidations; constructor(address _cacheContract) { cacheContract AICacheLayer(_cacheContract); } /** * 计算当前缓存命中率 */ function getHitRate() external view returns (uint256) { uint256 total totalHits totalMisses; if (total 0) return 0; return (totalHits * 10000) / total; // 精确到 0.01% } /** * 获取热门缓存键通过 accessCount 排序 * 注意这是一个链下分析的辅助工具不适合高频调用 */ function getHotKeys( bytes32[] calldata _candidateKeys, uint256 _topN ) external view returns (bytes32[] memory hotKeys) { // 简化的 Pipeline实际应使用链下索引 uint256 lookAt _candidateKeys.length _topN ? _candidateKeys.length : _topN; hotKeys new bytes32[](lookAt); for (uint256 i 0; i lookAt; ) { hotKeys[i] _candidateKeys[i]; unchecked { i; } } } }链下 Redis 缓存服务Node.js/** * 链下 AI 推理缓存服务Redis 实现 * * 设计决策 * - Redis 作为主缓存层L2合约内映射作为热点缓存L1 * - 支持 Adaptive TTL 和热度排序 * - 缓存预热使用 LRU 淘汰策略保持内存使用在可控范围 * - 定期同步将淘汰的热门条目异步写入 Arweave */ import { createClient } from redis; import { keccak256 } from js-sha256; interface CacheConfig { maxMemory: string; // Redis maxmemory 限制如 2gb defaultTTL: Recordstring, number; // 推理类型 → TTL(秒) warmupThreshold: number; // 访问次数阈值超过则预热到 L1 } class OffchainCacheLayer { private redis: ReturnTypetypeof createClient; private config: CacheConfig; // 热度计数器本地内存 定期同步 Redis private localAccessCounts new Mapstring, number(); constructor(redisUrl: string, config: CacheConfig) { this.redis createClient({ url: redisUrl }); this.config config; } async init() { await this.redis.connect(); // 配置 Redis 内存上限和淘汰策略 await this.redis.configSet(maxmemory, this.config.maxMemory); // allkeys-lru: 所有键按 LRU 淘汰适合缓存场景 await this.redis.configSet(maxmemory-policy, allkeys-lru); } /** * 构造缓存键 * 设计决策prefix 区分不同环境prod/staging */ private buildCacheKey(params: { modelId: string; inputHash: string; contextHash: string; version: number; }): string { const raw ${params.modelId}:${params.inputHash}:${params.contextHash}:v${params.version}; return ai:cache:${keccak256(raw)}; } /** * 查询缓存并更新访问频率 */ async get( modelId: string, inputHash: string, contextHash: string, version: number ): Promise{ result: string | null; cached: boolean } { const key this.buildCacheKey({ modelId, inputHash, contextHash, version }); const cached await this.redis.get(key); if (cached) { // 更新访问计数 this.incrementAccessCount(key); // 更新最后访问时间用于 LRU-like 的淘汰策略 await this.redis.expireAt(key, this.calculateExpiry(modelId)); return { result: cached, cached: true }; } return { result: null, cached: false }; } /** * 写入缓存 * param volatilityScore 波动率分数0-100 */ async set( modelId: string, inputHash: string, contextHash: string, version: number, result: string, inferenceType: string, volatilityScore: number ): Promisevoid { const key this.buildCacheKey({ modelId, inputHash, contextHash, version }); const ttl this.calculateAdaptiveTTL(inferenceType, volatilityScore); // 存储推理结果和元数据 await this.redis.set(key, JSON.stringify({ result, timestamp: Date.now(), modelId, version, volatilityScore }), { EX: ttl }); // 记录热度种子 this.localAccessCounts.set(key, 1); } /** * Adaptive TTL 计算 * 设计决策 * - 基础 TTL 取决于推理类型 * - 波动率调整0-30 → TTL×2, 30-70 → TTL×1, 70-100 → TTL/2 * - 最小 TTL 5 秒最大 TTL 24 小时 */ private calculateAdaptiveTTL( inferenceType: string, volatilityScore: number ): number { const baseTTL this.config.defaultTTL[inferenceType] ?? 60; let multiplier 1.0; if (volatilityScore 30) multiplier 2.0; else if (volatilityScore 70) multiplier 0.5; const ttl Math.floor(baseTTL * multiplier); return Math.max(5, Math.min(86400, ttl)); } /** * 计算过期时间戳 */ private calculateExpiry(modelId: string): number { const ttl this.config.defaultTTL[modelId] ?? 60; return Math.floor(Date.now() / 1000) ttl; } /** * 更新本地访问计数并检查是否需要预热到 L1 */ private incrementAccessCount(key: string) { const count (this.localAccessCounts.get(key) || 0) 1; this.localAccessCounts.set(key, count); // 超过阈值且不在 L1 的条目自动预热 if (count this.config.warmupThreshold) { this.warmupToL1(key).catch(console.error); } } /** * 将高热度缓存预热到链上 L1 缓存 */ private async warmupToL1(key: string) { // 调用链上合约的 setCache 方法 // 使用预言机签名验证身份 console.log(Warming up to L1: ${key}); // 实际实现构造交易 → 签名 → 发送 } /** * 定期清理本地访问计数防止内存泄漏 */ async periodicCleanup() { // 清理 30 分钟未访问的本地计数器 const cutoff Date.now() - 30 * 60 * 1000; for (const [key, _] of this.localAccessCounts) { const exists await this.redis.exists(key); if (!exists) { this.localAccessCounts.delete(key); } } } }四、边界分析缓存一致性难题问题一输入标准化导致的缓存碎片同一个查询ETH 价格预测可能被不同前端编码为{token: ETH}、{token: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2}或{symbol: ETH, chain: ethereum}。虽然语义相同但缓存的inputHash不同导致产生三个独立的缓存条目每个都浪费一次推理。解决方案在缓存层之前引入输入标准化层——将所有可能的输入格式映射为统一的规范形式如{chainId, tokenAddress}然后哈希。问题二缓存穿透攻击攻击者可以故意提交大量不同的但无意义的输入如 Token_0x0001, Token_0x0002 ...每次请求都因为缓存未命中而触发 AI 推理消耗计算资源和 API 配额。防御措施在缓存层前加请求限流同一地址每秒最多 N 次不同的查询并对从未出现的 token 地址使用空值缓存缓存这个地址没有有效数据TTL 5 分钟。问题三模型升级的缓存切换当 AI 模型从 v2 升级到 v3 时所有 v2 的缓存都应失效。如果直接全部清空会导致缓存命中率瞬间跌到 0所有请求涌向推理 API。推荐使用渐进式切换v2 和 v3 的缓存并行存在一段时间48 小时新请求命中 v3 缓存旧请求暂时使用 v2 缓存逐步迁移流量。问题四跨链缓存同步如果同一个推理结果在 Ethereum 和 Polygon 上都被查询两个链上的 L1 缓存独立存储可能返回不同的结果因为缓存过期时间不同。解决方案L2Redis 层作为跨链共享缓存源所有链的 L1 缓存都是 L2 的投影。当 L2 更新时通过事件通知各链的预言机同步更新 L1 缓存。五、总结链上 AI 的缓存层不是做了更好的可选项而是不做就烧钱的必选项。合理的缓存架构能将 AI 推理的重复调减少 60-80%对应成本削减同等比例。核心实践三层缓存L1 合约映射热点 L2 Redis温数据 L3 IPFS审计Adaptive TTL根据数据波动率动态调整缓存有效期避免一刀切的固定过期时间输入标准化在缓存键生成之前统一输入格式消除语义相同但格式不同的缓存碎片防穿透机制限流 空值缓存防止攻击者通过构造大量不同的查询耗尽推理资源渐进式升级新旧模型缓存并行运行过渡期避免升级时的缓存雪崩关键认知链上 AI 的推理成本 模型调用费 Gas 费 预言机费用。缓存消除的是模型调用费通常占总成本的 70% 以上。这不是架构优化中的锦上添花而是成本控制的关键节点。