agentsview 聚合用量缓存:让 30 天 Token 统计从全量扫描降到亚秒级
2026/9/17 8:20:44 网站建设 项目流程

agentsview 聚合用量缓存:让 30 天 Token 统计从全量扫描降到亚秒级

【免费下载链接】agentsviewLocal-first session search, analytics, insights, and token use statistics for coding agents, supporting Claude Code, Codex, and more than 20 other agents.项目地址: https://gitcode.com/GitHub_Trending/ag/agentsview

本文基于 agentsview 仓库的 Usage Aggregate Cache 设计文档 展开,并结合 internal/db 下的实际实现代码,完整讲解这套「一次性兄弟数据库 + 时区日粒度 rollup + 窄异常行」的缓存体系:它如何让 SQLite 后端的日用量、Top 会话、计费会话数等聚合查询不再逐条对每条带 token 的消息做排序、定价和传输,同时保证缓存失效、多进程退役、时区规则变更等边界场景下结果与实时路径逐字节一致。读完后你将理解:聚合缓存的两层派生数据如何划分、freshness 契约的完整请求流程、去重异常组的收敛条件,以及 2 秒 warm 30 天性能门禁背后的验证手段。

一、为什么需要一个「可随时删除」的兄弟数据库

agentsview 的 SQLite 后端此前对聚合类用量请求走的是「live path」:把窗口内每条带 token 的消息全部取出,在 Go 侧逐条做模型解析、费率档位选择、按行金额舍入和去重排序。会话历史越深,这个成本线性甚至超线性增长。

Usage Aggregate Cache 的方案核心是一句话:聚合读取改为查一个派生的、可随时删除的兄弟数据库(sibling database),归档库(archive)始终是权威数据源。由此得到几个设计约束(见 设计文档):

  • 缓存只服务四类聚合:日用量(daily usage)、Top 会话、计费会话数、宽松匹配计数;单会话明细仍走有界 live path,PostgreSQL 后端保留 live 实现,并用完整结果pgtest夹具与 SQLite 结果对齐。
  • 删除任何被识别的缓存代次(generation)不丢失任何用户数据——因为归档库才是权威。
  • 发布目标:在生产规模的受保护克隆(protected production-scale clone)上,warm 状态的 30 天 CLI 完整结果不超过 2 秒,且缓存构建前后的结果逐字节一致。

失败语义同样明确:聚合缓存读取失败就是错误,绝不回退到 live 路径(这一约束在 实施计划文档 中被再次强调),避免「缓存没算完就悄悄返回近似值」这类难以排查的静默降级。

二、两层派生数据:facts 是构建基座,rollups 是读取层

缓存内部严格分为两层,这一分层直接决定了 warm 读取的成本模型。

2.1 facts 层:窄事实,只含解析结果,不含转录内容

facts 层存储的是解析后的消息与 usage 事件字段,但不含任何转录正文。它的意义是让「换一个时区重建 rollup」这件事与被文档明确指出的 39 GB 归档库及其 JSON 列解耦——重建不再需要回读大字段。

从 usage_cache_schema.go 的usageCacheSchemaSQL可以看到 facts 层的真实形态:

  • usage_cached_sessions记录每个已缓存会话的来源指纹:source_sync_markersource_transcript_revusage_event_fingerprintinstall_revision,这是后续所有失效判断的锚点;
  • usage_facts是主事实表(WITHOUT ROWID,主键(cached_session_id, fact_index)),保存 token 各类计数(input/output/reasoning/cache_creation/cache_read)、web_search_requests、上报成本(reported_cost_microdollars+cost_source)、request_scoped标记,以及去重身份字段(claude_message_idclaude_request_idsource_uuidusage_dedup_key),并用三个部分索引(partial indexes)分别覆盖这三类去重键,服务跨会话身份检查;
  • cursor_usage_facts单独存放 Cursor 用量事件,带charged_microdollarsis_headless

设计文档中一句关键描述是:facts 层只是构建基座(build substrate),warm 聚合请求只读 rollups 和异常行,绝不回退 live 聚合路径

2.2 rollups 层:时区日粒度聚合

rollups 层按「逻辑粒度(session_id, local_day, model)」保存每日贡献,外加活动行(activity rows)和窄去重异常。每条日行(daily row)保存:

  • token 各类计数、web-search 请求数;
  • 定价身份:priced_modelmatched_patternrate_okrate_hashpricing_timestampband_threshold(费率档位阈值,无档位时为 -1,见 usage_rollup.go 的installUsageRollupRows);
  • 按行舍入后的估算成本estimated_cost_microdollars、节省savings_microdollars、权威成本标记authoritative_cost_microdollars
  • 请求计数(computed request/aggregate count、reported count、base request count)与被丢弃的快照输出discarded_snapshot_output_tokens

模型解析、费率档位选择、金额舍入与结果组装全部由 Go 负责,SQL 只做窄索引读取——这与 usage_rollup.go 中构建侧调用export包定价解析器(export.PricingResolver)的事实一致。

安装(install)粒度是「每个源会话一个 rollup install」,对应表usage_rollup_installs,其中每个 install 携带来源指纹、fact_install_revision、烘焙进行的baked_agent/baked_started_atpricing_hashinstall_revision。会话的其余元数据(项目、机器、自动化状态、终止状态、策展字段等过滤条件)保持为 live 归档元数据,不进入缓存;唯一烘焙进去的是agentstarted_at:前者参与通用去重键,后者为没有自身时间戳的事实提供日期,且这两个精确值在每次读取时都会被重新校验(usage_rollup.go 的readUsageRollupInstalls中,baked[session] != [2]string{agent, startedAt}即判定 install 过期)。

2.3 去重异常:只有「不可约」的组才落盘

去重是这套缓存里语义最复杂的部分。构建某个会话 rollup 时按组分类,一组只有在其解析结果可证明不随查询窗口或 live 过滤器变化时,才允许收敛(finalize)进日行。收敛条件(设计文档逐条列出,全部是必要条件):

  1. 组内每个成员都属于正在构建的会话,且共享同一个本地日期——因为查询窗口以整天为单位,这样的组作为整体要么在窗口内、要么在窗口外;
  2. 对通用source:/usage:组,每个成员还必须共享同一个模型和 headless 状态——因为 live 的模型过滤和自动化过滤在通用排序之前生效;
  3. 组内没有任何成员同时连接 snapshot 去重与通用去重(携带 usage 键的快照幸存者会在读取时重新进入通用排序);
  4. 组内没有任何成员携带 Copilot 权威成本——它在整个窗口内是顺序依赖的按会话选择;
  5. 组身份不出现在任何其他已缓存会话中;对 usage 键,还不能出现在 Cursor 事实库中。usage_facts上的部分身份索引就是为这个检查保守地服务的(例如source:身份忽略烘焙的 agent)。

收敛时应用与读取路径完全一致的排序规则:最大 output 的快照胜出、归属跟随最早一行、最大 web-search 计数带过去、落败快照的 output 记为 discarded。只有不可约的组(跨会话、跨天、跨模型的真实重复)才存为窄异常行(usage_rollup_exceptions,带group_kindsnapshot/generalgroup_key)。由此,异常量随真实重复组数量增长,而不是随带 token 的消息数量增长——这是 warm 读取能做到亚秒级的根本原因。Cursor 事实则完全保留在异常层:它们的键可能与会话 usage 键冲突,且过滤器依赖逐行 headless 状态,因此以一个按 Cursor high-water mark 键控的合成源 install(usage_rollup.go 中的usageRollupCursorSessionID = "\x00cursor")承载。

读取时的路径:查询只加载与请求窗口相交的、来自当前已校验候选会话的异常行,然后在 Go 中套用既有的窗口内排序、归属、过滤与定价规则;已收敛组和普通事实留在索引化日行路径上。

失效联动同样精巧:由于 rollup 按源会话安装,改一个转录就重建该会话;而当 fill、Cursor 批次或删除改变了某个会话贡献的去重身份集合时,同一个缓存事务会删除所有持有该身份的其他会话的时区 rollup install,且 rollup 安装事务内部会重新校验「没有已收敛身份新增了外部成员」,否则按「源已移动」重试。于是「已收敛日行绝不会在获得兄弟后存活」这一不变量成立:跨会话赢家变化会立即反映——下一次请求校验全部候选会话,并从所有成员的最新异常行重新解析该组。usage_rollup.go 中installUsageRollupBuildsBEGIN IMMEDIATE事务里重新加载跨会话身份集合并断言currentCross.subsetOf(buildCross),构建与安装之间竞争的重分类最多重试usageRollupMaxBuildAttempts = 8次。

三、Freshness 契约:一次聚合请求的七步

设计文档给出的请求流程可以完整还原,usage_cache_consumers.go 的queryUsageRollups是其对应实现:

  1. 捕获快照:归档数据库 ID、候选会话、来源指纹、精确的agentstarted_at、live 过滤器元数据、定价行、Cursor high-water mark、请求时区;
  2. 在获取缓存写锁之前关闭归档快照——避免跨数据库持锁窗口;
  3. 打开代次:由缓存格式版本 + 归档database_id命名的那个代次;
  4. 只填充缺失或过期的候选会话的规范化事实(usageFillCoordinator.Ensure,usage_cache_fill.go);
  5. 为请求时区构建缺失或过期的 rollupsusageRollupCoordinator.Ensure);
  6. 安装前重新检查来源指纹与烘焙元数据
  7. 在一个钉住的缓存读事务内校验所有必需 install,然后才组装结果。

几个容易被忽视的细节:

  • 定价失效不靠时间戳。定价代次使用规范的、与顺序无关的 effective-pricing 摘要;日行还保留按模型解析出的费率哈希。在 usage_rollup.go 中,usagePricingIdentity组合export.EffectivePricingDigest(rows)pricingpkg.BillingPolicyVersion()——后者很重要:定价策略本身的代码级变化(如摘要只能哈希目录行、哈希不到的解析规则变更)也能通过版本字符串触发重建。
  • sync_marker不是单调版本:它的触发器对一组可变时间戳字段取最大值,值可能变小。因此安装时比较的是提取后的完整来源指纹,而不是 marker 的顺序。
  • 被硬删除的会话按更新的态丢弃;其他「源在移动」或「归档忙」的竞态最多重试 3 次(usageFillMaxAttempts = 3),然后失败而不是返回过期用量——宁可报错也不给错数。
  • 请求取消只是把等待者从共享 fill 工作中摘除,fill 本身继续跑,让重试风暴收敛;cached_at只作诊断,不参与任何新鲜度判断。

queryUsageRollups的重试循环与这套契约一一对应:capture → acquire generation → deletion sweep → fill → rollup ensure → cache read,任何阶段遇到errUsageCacheSourceChanged就重新捕获快照重试,超过usageFillMaxAttempts才把最后一个错误抛给调用方。

四、文件与代次:多进程安全地退役缓存

缓存文件放在归档库同目录,文件名即代次身份。usage_cache_schema.go 中usageCacheGenerationPath生成usage-cache-v{版本}-{databaseID 的 SHA-256 前 16 字节 hex}.db格式或数据库 ID 变化会选中一个全新代次,而不是迁移旧文件。当前格式版本为 12,注释里完整保留了 v5→v12 每次重建的缘由(定价解析规则变化、计费身份保留、跨进程租约引入、特定模型费率修正等),是一份很好的「为什么缓存代次要整体重建」的案例记录。

退役协议(retirement protocol)保证了多进程共存时的安全删除:

  • 删除或替换任何代次前,必须同时用 SQLiteapplication_id0x41565543)和usage_cache_metadata.cache_kindagentsview-usage-facts)确认它是 agentsview 用量缓存,文件名匹配不算数probeUsageCache的 fail-closed 探测逻辑);
  • v8 及以后的代次携带退役协议标记,并在每个打开它的 SQLite 连接池的整个生命周期内持有一个跨进程共享租约flockpath + ".lease")。一个打开者只有在拿到独占租约、按精确文件名重新核对格式版本与源数据库 ID、并关闭自己的句柄之后,才能移除另一个被识别的代次;随后主库、WAL、共享内存文件一起删除,而极小的租约文件被保留,让竞争中的打开者不可能锁到被替换的 inode 上;
  • 协议之前的代次、身份不匹配的文件保持不动(旧进程可能还开着它们);比当前运行格式更新的代次也保留,避免降级后的二进制强迫新版代次重建。
  • 若兄弟目录不可写,进程退化为在临时数据库中使用相同 schema 与读取路径,并告警「重启后会重建」——功能不中断,只是失去持久性(openTemporaryUsageCache分支)。

时区身份:规则指纹防 zoneinfo 更新

时区身份在请求窗口之间保持稳定:命名时区用 IANA 名 +1970 至 2100 年时区规则的指纹组合成 key,任何 zoneinfo 更新只要在该范围内增删或移动了转换点,就会换一个 key,从而绝不会复用过期的日分桶(usage_rollup.go 的usageTimezoneRuleFingerprint逐条哈希每个规则区间及其精确结束时刻)。当进程本地时区只报告Local时,会先尝试解析/etc/localtime的 zoneinfo 符号链接(usageLocationName),解析不出就用同样的规则指纹作为 key——防止懒加载的time.Local初始化或不同请求范围把同一时区裂成多个代次。

五、后台维护:latest-first 回填与数据库卫生

HTTP 就绪之后,可写的 daemon 进程按「最新会话优先」回填:

  • 每批最多 256 个会话(usageFillInstallBatchSize = 256,usage_cache_fill.go;usage_cache_background.go 中回填批大小与其对齐,以便前台等待者随每个已提交批次尽早释放);
  • 构建进程本地 rollup 与最近请求过的最多 8 个命名时区
  • 变更工作只在归档提交之后入队,缓存绝不在归档写事务内填充
  • 每批之间执行PRAGMA optimize,freelist 较大时做有界增量 vacuum(阈值为 4096 页、每次最多 256 页,见usageCacheVacuumFreelistThreshold/usageCacheVacuumPages),代次创建后与完整回填后执行全量ANALYZE
  • 删除日志清扫只是卫生工作——聚合读取要求当前归档候选,本来就不可能暴露孤儿缓存行;
  • 一轮过程中源数据若移动,worker 重新捕获整个快照重试,最多 3 次;已安装指纹让未变更的批次在多次尝试间可复用。

回填入口是 usage_cache_background.go 的StartUsageCacheBackfill(要求可写归档,且以已安装会话版本为准——重启一轮会自然跳过已完成的工作),WaitUsageCacheBackfill供 daemon 生命周期等待;restartUsageCacheBackfillIfEnabled的注释还点明了一个克制的设计:CLI resync 或 compaction 重开归档时不会自行拉起整库后台扫描,只有显式启用的 daemon 生命周期才会。

六、验证与性能门禁

正确性验证的策略是「拿永久 live 路径当 oracle」:

  • 随机化对拍:公开 rollup 结果与 live oracle 在种子随机窗口、时区、过滤器、DST 边界、定价档位、上报成本、Cursor 行、空时间戳、跨会话去重组上逐一比对;
  • 变更测试:覆盖转录替换、resync 指纹、定价、agentstarted_at、删除、schema 代次、数据库 ID 变化等每一类失效源;
  • 受保护克隆发布检查:7 天 / 30 天 / 全历史结果逐字节比对;性能报告把冷构建与 warm 读取分开,报 1 天、7 天、30 天与全历史;warm 路径不得扫描规范化事实

成本模型的门禁表述非常明确:warm 读取必须随日行数量与真实重复组数量增长,而不是随带 token 的消息数量增长;发布检查会连同耗时一起报告异常基数(exception cardinality)。前台慢请求(超过 usage_cache_consumers.go 中usageRollupSlowRequestThreshold = 2s的阈值)会输出隐私安全的分阶段耗时与行计数日志(capture / deletion sweep / fact fill / rollup build / rollup install / cache read),warm 请求则保持静默。冷构建与未缓存候选会话的历史成正比,刻意放在后台、按最新优先消化。

实施计划文档 的「Measured result」一节记录了受保护克隆上的观测值:warm CLI 路径约0.19 秒(1 天)、0.35 秒(7 天)、0.85 秒(30 天),全部远低于 2 秒门禁;冷构建与全历史则作为后台规模工作单独报告。需要注意,设计文档同时声明受保护克隆的耗时会针对每次发布重新测量,而不是把某个记录值当长期承诺。

七、对读者的启示

这套缓存实现里可迁移的 engineering 决策有四点:

  1. 派生数据与权威数据物理分离,让「删除缓存零数据损失」成为架构承诺而非运维约定;
  2. 失效判断只信内容指纹(来源指纹、定价摘要、烘焙元数据精确值),不信任何可能回退的 marker 或时间戳;
  3. 把复杂语义(去重)收敛到「可证明与窗口无关」的子集,其余走窄异常行 + 读取期 Go 侧排序,使热路径成本与真实复杂度而非数据规模成正比;
  4. 竞态处理选择「重试到一致,失败到明确」,绝不静默返回过期结果。

进一步阅读可以从这几个入口开始:schema 与代次协议见 internal/db/usage_cache_schema.go,rollup 构建/安装与重试见 internal/db/usage_rollup.go 和 internal/db/usage_rollup_build.go,事实填充见 internal/db/usage_cache_fill.go,后台回填见 internal/db/usage_cache_background.go,消费端与慢请求日志见 internal/db/usage_cache_consumers.go;PostgreSQL 侧的完整结果对拍夹具位于 internal/postgres/usage_facts_parity_pgtest_test.go,设计全貌另有 实施计划 可对照阅读。

【免费下载链接】agentsviewLocal-first session search, analytics, insights, and token use statistics for coding agents, supporting Claude Code, Codex, and more than 20 other agents.项目地址: https://gitcode.com/GitHub_Trending/ag/agentsview

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询