📊 agentprof 可视化指南 · 目录 Wiki 10 / 14
Wiki

分析层 rollups

agentprof 的 「花得值不值」信号全来自 agentprof_core::analyzer 模块:它吃 Episodes + SessionMeta + &[ParseWarning] 三组输入,吐 AnalysisReport。每个 rollup(turn_summary / tool_rank / hook_rank)都是独立可测的纯函数,cache 段和 MCP waste 则是 AnalysisReport 上的派生方法(按需算,不入 pipeline),保证 analyzer 本身没有 I/O、没有时间副作用、可以 snapshot test。

CACHE_READ_DISCOUNT
0.9
cache_read 价格 = input × 0.1
CACHE_WRITE_PREMIUM
0.25
cache_creation = input × 1.25
honest hit rate
read / (read + creation)
重用率
naive hit rate
read / (read + input)
整体节省率
🏭 工程类比 — 像 ETL pipeline 的 transform 层
  • Event = raw input(adapter 从 jsonl 解析出的原始记录)。
  • Episode = staged data(一层规整化:tool call 配对、turn 划分、hook 聚合)。
  • AnalysisReport = business view(per-turn / per-tool / per-hook 的 rollup,渲染器直接消费)。

analyzer 模块就是那一层 SQL SELECT ... GROUP BY ... — 输入是 Episodes 这张「staged 表」,输出是若干张 rollup 表(turn_summary / tool_rank / hook_rank)。CacheMetricsWasteReport 是 view-of-view(在 report 上再聚合),不进 transform 层。

3 类 rollup 对比(recon 真实公式)

Rollup关键公式 / 算法输出字段(节选)
turn_summary(episodes)按 turn 分组:iterate episodes.turns,每 turn 收集 tool/hook/skill 调用计数 + 累计 duration + assistant 最后一条 message 的 output_tokensVec<TurnSummaryRow>turn_id, started_at, duration: Option<Duration>, status: TurnStatus, model, mode, output_tokens: Option<u32>, tool_call_count, hook_call_count, skill_call_count
tool_rank(episodes)按 tool name 分组:iterate episodes.tools,统计 success/failure/orphan 计数 + 累计 total_duration + 收集 sorted durations 算 percentile_nearest_rank(50.0) / (95.0);最后按 total_duration 降序Vec<ToolRankRow>name, source: ToolSourceBuiltin/Mcp/Skill/User/Unknown), call_count, success_count, failure_count, orphan_count, total_duration, p50_duration, p95_duration, max_duration, is_user_blocking
CacheMetrics::from_raw(creation, read, input)
(M2.5 派生,非 pipeline)
honest = 100 × read / (read + creation)
naive = 100 × read / (read + input)
saved_net = round(read × 0.9 − creation × 0.25)
Option<CacheMetrics>Nonecreation == 0 && read == 0):creation, read, input, hit_rate_naive_pct, hit_rate_honest_pct, saved_gross: u64, saved_net: i64(可负)

⚠️ Recon 校正:analyze() 本体只算 turn_summary/tool_rank/hook_rank 这 3 个 rollup,model_metricsloaded_mcp_tools 是从 Episodes 直接 clone() 进 report;cache_metricsWasteReport 是 report 上的派生方法,渲染时按需算。

👇 三张卡片:① analyze() 流水线真实顺序(recon 后修正)· ② Cache 段双 hit rate(ADR-0023 的「honest vs naive」)· ③ MCP waste 的 compute_waste + aggregate_waste 4 层精度。

1 analyze() 流水线(recon 真实顺序) 点击展开
📐 真实签名(crates/agentprof-core/src/analyzer/mod.rs:415
pub fn analyze(
    episodes: &Episodes,
    meta: &SessionMeta,
    parse_warnings: &[ParseWarning],
) -> AnalysisReport {
    let report = AnalysisReport {
        meta: meta.clone(),
        turn_summary: turn_summary(episodes),
        tool_rank:    tool_rank(episodes),
        hook_rank:    hook_rank(episodes),
        warnings:        episodes.warnings.clone(),
        parse_warnings:  parse_warnings.to_vec(),
        model_metrics:   episodes.model_metrics.clone(),
        loaded_mcp_tools: episodes.loaded_mcp_tools.clone(),
    };
    // tracing::debug! 记录 tool/hook count 后 return
    report
}

字段构造顺序就是流水线顺序:3 个独立 rollup 函数 + 4 个字段透传(meta / warnings / parse_warnings / model_metrics / loaded_mcp_tools)。没有 cache_metrics 或 waste 字段 — 这俩是 AnalysisReport 上的方法。

🌊 SVG 流程图
Episodes + meta + parse_warnings turn_summary() tool_rank() hook_rank() AnalysisReport(+ clone model_metrics / loaded_mcp_tools)

3 个 rollup 之间互不依赖(pure functions of Episodes),理论上可并行;当前实现单线程顺序执行 —— 因为单 session 通常 < 10 MB,并行开销大于收益。

🤔 为什么单一 entry point
turn_summary / tool_rank / hook_rank 这 3 个 pub fn 都 export 出去当然可以(且确实 export 了 — 便于单元测试),但大多数 caller(cli / serve / storage)需要的是一份完整 AnalysisReport 而非单个 rollup。提供 analyze() 这个 single entry 让调用方少写 5 行 boilerplate,也方便后续加 tracing::instrument 统一观测(name = "analyzer.analyze")。
✅ agentprof 怎么做
analyze()纯函数 + 无 I/O:输入是 borrowed refs,输出 owned AnalysisReport,不读文件、不写 SQLite、不发 OTLP — 全部 side effect 在 caller 那侧。这让它在 doctest / unit test / snapshot test 三个层级都 trivial,也是 ADR-0004 「lenient parsing + pure analyzer」分层决策的体现。
🔀 其他选择
streaming 增量算法(adapter 每读 1 个 event 立刻喂给 analyzer,rollup 在线更新)— 内存占用 O(unique_tools) 而非 O(events),对超大 session 友好;但当前 session 通常 < 10 MB(≤ 数万 events),批量算 + 内存常驻简单且足够快。streaming 版本作为 M4+ 「OTLP live mode」的潜在升级路径在 ADR-0017 留了待办标记,目前不需要。
2 Cache 段双 hit rate(ADR-0023) 点击展开
📐 两个 hit rate 公式
// crates/agentprof-core/src/analyzer/cache.rs:86
pub fn from_raw(creation: u64, read: u64, input: u64) -> Option<Self> {
    if creation == 0 && read == 0 { return None; }
    let naive_denom  = read.saturating_add(input);
    let honest_denom = read.saturating_add(creation);
    let hit_rate_naive_pct  = 100.0 * (read as f64) / (naive_denom  as f64);
    let hit_rate_honest_pct = 100.0 * (read as f64) / (honest_denom as f64);
    let saved_gross = (read as f64 * CACHE_READ_DISCOUNT).round() as u64;
    let saved_net   = (read as f64 * CACHE_READ_DISCOUNT
                     - creation as f64 * CACHE_WRITE_PREMIUM).round() as i64;
    Some(Self { creation, read, input,
                hit_rate_naive_pct, hit_rate_honest_pct,
                saved_gross, saved_net })
}
  • honest_pct = read / (read + creation) — 「我尝试 cache 的 token 里,有几成被 reuse 了」。暴露 over-caching:高 creation + 低 read = cache 策略在烧钱。
  • naive_pct = read / (read + input) — 「我的 prompt 里有几成走了 cache」。直观但不惩罚 over-caching — creation 多了 naive 也好看(因为分母不变)。
  • 两者都报:让用户自己判断 cache 策略是否健康。
💰 净节省公式与常数
// crates/agentprof-core/src/analyzer/cache.rs:19,24
pub const CACHE_READ_DISCOUNT:  f64 = 0.9;   // cache_read = 10% of input price → 节省 90%
pub const CACHE_WRITE_PREMIUM:  f64 = 0.25;  // cache_creation = 125% of input → 多花 25%

saved_gross = round(read × 0.9)                               // 毛节省(input-token equivalent)
saved_net   = round(read × 0.9 − creation × 0.25)             // 净节省,可负

常数对应 Claude Sonnet 4.x(2026-06 价格表):cache read 价格是 input 的 10%(discount 0.9),cache write 价格是 input 的 125%(premium 0.25)。saved_net 类型是 i64 而非 u64可以为负,表示 cache 策略反而花了更多钱(creation 远大于 read 时)。

🚫 关键约束:aggregate 视图显示 cache 列
agentprof aggregate --by tool--by mcp-server 视图故意省略 cache 列。原因(ADR-0023 D-3 条决策):cache_creation / cache_readprompt-level(API request 维度)token 计数,而 tool / MCP server 是 turn-level 维度 —— per-tool cache attribution 在语义上 undefined(一个 prompt 可能触发 5 个 tool call,cache token 怎么分?)。强行均摊会误导用户,所以这两个 aggregate 视图刻意留空 cache 段;--by model--by day 视图保留 cache 列,因为这两个维度的归并是 well-defined 的。
🤔 为什么不只报 honest_pct
honest 是更诚实的指标但不直观:用户问「我有多少 prompt 命中了 cache?」时 honest 不能直接回答。naive 直观但容易自欺欺人。两个都报 + 文档解释差异,让用户自己判断 — 这是 ADR-0023 的核心权衡(informative over prescriptive)。
🔀 其他选择
两条被否决的简化:
只报 saved_net(dollar / token 节省)— 简洁,但用户无法判断 cache 策略本身的健康度。同样的 net 节省,可能来自高效 cache,也可能来自天量 prompt。
只报 cache_creation / cache_read 原始值,不算 rate — 信息无损,但要用户脑补算除法。
当前「两个 rate + 一个净值 + 一个毛值」组合,是「教育成本」与「决策支持」的平衡点。
3 MCP waste — compute_waste + aggregate_waste 与 4 层精度 点击展开
📐 两层 API
// crates/agentprof-core/src/analyzer/waste.rs:402
pub fn compute_waste(report: &AnalysisReport, ctx: &WasteComputeContext) -> WasteReport;

// crates/agentprof-core/src/analyzer/waste.rs:648
pub fn aggregate_waste(per_session: &[(SessionRef, WasteReport)]) -> AggregateWasteReport;
  • compute_waste(report, ctx)单 session:对 report.loaded_mcp_tools 里每个 tool name,看 report.tool_rank 里是否真的被调用过,没调用的就是 wasted context;token 估算精度由 ctx 决定。
  • aggregate_waste(per_session)跨 session:把多份 WasteReport 按 tool / mcp-server 维度合并,输出 AggregateWasteReport,给 aggregate --by mcp-server 渲染用。
🎚️ WasteComputeContext 的 4 层精度(recon: 3 个 builder + 1 默认)
// crates/agentprof-core/src/analyzer/waste.rs:124
pub struct WasteComputeContext<'a> { /* fields ... */ }

impl<'a> WasteComputeContext<'a> {
    pub fn new(wire: &'a BTreeSet<String>) -> Self;          // 层 1:默认(启发式估算)
    pub const fn with_tokenizer(mut self, kind: TokenizerKind) -> Self;   // 层 2
    pub const fn with_config(mut self, cfg: &'a BTreeMap<String, Vec<String>>) -> Self;  // 层 3
    pub fn with_sidecar(mut self, sidecar: &'a dyn SidecarLookup) -> Self;             // 层 4(最准)
}
配置 token 估算来源 精度
1new(wire)heuristic:每 tool ≈ 100 tokens 默认估值⭐ 粗糙
2+ .with_tokenizer(TokenizerKind::O200kBase)用 tiktoken-rs 真算 schema 字符数⭐⭐ 中等
3+ .with_config(mcp.json)读 MCP server 配置,按 declared tool list 算⭐⭐⭐ 较准
4+ .with_sidecar(--tool-descriptions)读用户提供的真实 schema JSON sidecar,每 schema 单独 tokenize⭐⭐⭐⭐ 最准

builder 是可叠加的:WasteComputeContext::new(&wire).with_tokenizer(...).with_sidecar(...) 同时启用层 2 + 层 4。当多源信息可用时取最准的;缺什么 fall back 上一层。

🔧 Tokenizer 选择:infer_tokenizer / build_bpe
// crates/agentprof-core/src/analyzer/waste.rs:297
pub fn infer_tokenizer(model: Option<&str>) -> TokenizerKind;   // 按 dominant model 推断
// crates/agentprof-core/src/analyzer/waste.rs:267
pub fn build_bpe(kind: TokenizerKind) -> Option<CoreBPE>;        // 构造 tiktoken-rs BPE

cli / serve 在调 compute_waste 前用 report.dominant_model() 推断 tokenizer kind(gpt-5 → O200kBase / claude → cl100k_base 近似),自动选最合适的 BPE — 用户不需要手动指定。

🤔 为什么 MCP waste 单独成模块
MCP tool 的规模特性独特:用户配置了 30+ MCP servers、每个 server 暴露 5-50 个 tool,大多数 tool 在一次 session 里从未被调用 — 但 schema 全部塞进 prompt 占 context window。这是 LLM agent 时代最大的浪费来源之一,但传统 token profiler(只算「用了多少」)看不到compute_waste 算的是「加载了但没被调用」的 schema token —— agentprof 的 ROI 卖点核心。
🔀 其他选择
用 char count × 4 估算 token(业界 rule of thumb)— 实现一行,精度 ±30%,对 ROI 排序够用;但 agentprof 已经 depend on tiktoken-rs(cache token 也要算),精算边际成本接近零。不算 waste 只显示 loaded 列表 — 用户得自己脑补,决策支持弱。当前「heuristic 默认 + 4 层精度」是「易用性」与「准确度」的平衡 — 用户随时可以加 --tool-descriptions sidecar.json 升精度,零配置也能用。

下一步

本课讲清了 analyze() pipeline 的真实顺序、CacheMetrics 的双 hit-rate 决策(ADR-0023),以及 MCP waste 的 4 层精度模式。下一课「Tokenizer 与 token 计数」深入 agentprof-core::tokenizertiktoken-rs 集成的细节 — 为什么 cache 段需要 3 列 token、Claude 用什么 encoding、跨 model 比较为什么要慎重。

📂 相关源码: agentprof-core/analyzer/mod.rs  analyze

📂 相关源码: agentprof-core/analyzer/cache.rs  CacheMetrics

📂 相关源码: agentprof-core/analyzer/waste.rs  compute_waste