yoi/tickets/token-counter.review.md
2026-04-13 20:21:26 +09:00

3.0 KiB
Raw Blame History

token-counter レビュー

要件の充足

チケットが定義した 3 API・型・アルゴリズムは全て実装されている:

  • Pod::total_tokens()TokenEstimate
  • Pod::split_for_retained(retained)SplitPoint
  • Pod::savings_for_drop(range)TokenEstimate
  • EstimateSource: Measured / Interpolated / Extrapolated / NoData

設計方針(状態を持たない pure 関数、provider 非依存、ローカルトークナイザ不要)も 満たされている。_impl 関数群は (&[Item], &[UsageRecord]) だけを受け取り、 Pod メソッドは history と usage_history を渡すだけの薄いラッパー。

アーキテクチャ

レイヤー 変更内容
pod::token_counter pure な計算関数 + Pod のメソッドとして公開
pod::Pod usage_history: Arc<Mutex<Vec<UsageRecord>>> を追加。restore で復元、persist_turn で追記、compact で clear
pod::PruneHook min_savings 判定を savings_for_drop_impl に委譲。usage_history の shared handle を保持
llm-worker::prune prune()prunable_indices() + apply_prune() に分解。min_savings 判定とトークン会計への依存を除去

prune の責務分離が適切。llm-worker 側は pure な候補抽出と適用のみ、 トークン会計への依存は pod 層に閉じている。

指摘と対処

1. split_for_retained_impl の O(n²) シリアライズ(非ブロッカー、未対処)

tokens_at1..=history.len() で毎回呼び、内部で prefix_byteshistory 全体の JSON シリアライズ)を都度計算。長大セッションでは item 数に対して二乗になる。 prefix_bytes をループ外で 1 回だけ計算して渡す形にリファクタリングすべきだが、 現時点の history サイズでは実害なし。パフォーマンスが問題になった段階で対処。

2. PruneHook の savings 過大評価(認識済み、未対処)

prune は content を None にするだけで item を消さないため、savings_for_drop (範囲全体の drop を仮定)は実際の節約量より大きい値を返す。閾値判定としては prune を発動しやすい方向=安全側。ログの estimated_savings_tokens が過大になる 点はチューニング時に注意。

3. compact 後の usage_history.clear()(後続チケットで対処)

compact 直後は measurement が空になり total_tokens()NoData を返す。 compact-improvements で last_input_tokens を撤去して閾値判定を usage 経由に 一本化する際、この NoData 期間の扱いを設計する必要がある。

テスト

token_counter: 13 件NoData / Measured / Extrapolated / Interpolated 各ケース、 split の境界、savings の measurement 差分、空 range、out-of-range

prune (llm-worker): prunable_indices + apply_prune に分解後のテスト 5 件。 候補抽出、適用、冪等性、既 prune 済み除外、境界。

判定

承認。