analyzing-expensive-users について
AI オブザーバビリティで高コストのユーザーを調べる分析担当者向けのスキルです。ユーザー別ランキング、プロジェクト基準値、モデルやトークンの内訳、代表的なトレースを組み合わせ、コスト要因を明らかにします。
主な機能
- 01生成コストによるユーザーランキング
- 02プロジェクト基準値との指標比較
- 03モデル・トークン・キャッシュコストの分析
- 04高コストトレースの確認
- 05発見したカスタムディメンションでの集計
活用シーン
- 01AI 支出が最も多いユーザーの特定
- 02利用量と単価のどちらが要因かを判定
- 03トレースから再試行やコンテキスト肥大を調査
- 04プロダクトのディメンション別に高コストなワークフローを特定
MCP 対応
SQL、LLM トレース検索、スキーマ確認、アプリリンク生成に PostHog MCP ツールを使う手順を示します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posthog/ ai-plugin --skill analyzing-expensive-usersインストール後、Claude Code で/analyzing-expensive-usersを入力して読み込まれたか確認します。
詳細
高コストユーザーの分析
AI のオブザーバビリティにおいて、最もコストの高いユーザーを把握したい場合にこのスキルを使用します。作業は単にユーザーをコスト順に並べるだけではありません。有用な回答では、上位ユーザーのコストが高い理由を説明します。たとえば、使用量、モデルの選択、プロンプトのサイズ、出力のサイズ、キャッシュの動作、再試行やエラー、トレースの種類、機能またはテナントのディメンション、代表的なトレースの例などです。
一般的なコスト集計には、exploring-llm-costs も使用してください。個々のトレースを読む場合は、exploring-llm-traces も使用してください。
ツール
| ツール | 目的 |
|---|---|
posthog:execute-sql | ユーザーを順位付けし、指標をプロジェクトのベースラインと比較する |
posthog:query-llm-traces-list | 特定のユーザーの高コストなトレースを見つける |
posthog:query-llm-trace | 何が起きたのかを説明するため、代表的なトレースを読む |
posthog:read-data-schema | グループ化する前に、カスタムイベントまたはユーザープロパティを確認する |
posthog:generate-app-url | リージョンとプロジェクトを指定した UI へのリンクを作成する |
基本ルール
- 期間を限定して開始します。 ユーザーが期間を指定しない場合は、過去 30 日間を使用し、その旨を伝えます。ユーザーがリンクや既存のフィルターを提示した場合は、日付範囲、テストアカウントのフィルター、プロパティのフィルターを維持します。
- 生成呼び出しの支出から始めます。 ユーザーごとの順位付けクエリでは、
$ai_generationの行をdistinct_idでグループ化し、traces、generations、errors、total_cost、first_seen、last_seenを集計します。高コストユーザーを見つける最初の方法として、これが最適です。 - ユーザーごとの支出全体を把握するには、埋め込みを意図的に含めます。 より広範なコスト集計には、
event IN ('$ai_generation', '$ai_embedding')を含めます。ただし、イベントの対象範囲を変更した場合は、その旨を明示します。 - ユーザーを解釈する際は、デフォルトのトレース ID を除外します。 一部の SDK では、ユーザーが設定されていない場合、
$ai_trace_idがdistinct_idとして使用されます。識別済みユーザーについては、distinct_id = properties.$ai_trace_idを除外し、どれだけの支出が未帰属になるかを明示します。 - カスタムディメンションを推測しません。
feature、tenant_id、plan、workflow_name、または同様の顧客固有フィールドでグループ化する前に、イベントとユーザーのプロパティを確認します。 - 因果関係を説明する前にトレースを読みます。 集計によって疑わしい対象は特定できますが、代表的なトレースを読むことで、そのユーザーのコストが高い理由が、実際のワークフロー、再試行、ループ、大きなコンテキスト、ツールを多用する生成、その他の動作のどれなのかを把握できます。
ワークフロー
1. 生成呼び出しの支出でユーザーを順位付けする
コストの高いユーザーを知りたいという質問には、まずこれを使用します。
posthog:execute-sql
SELECT
distinct_id,
argMax(email, timestamp) AS email,
argMax(name, timestamp) AS name,
countDistinctIf(ai_trace_id, notEmpty(ai_trace_id)) AS traces,
count() AS generations,
countIf(notEmpty(ai_error) OR ai_is_error = 'true') AS errors,
round(sum(ai_total_cost_usd), 4) AS total_cost,
round(avg(ai_total_cost_usd), 6) AS avg_cost_per_generation,
sum(ai_input_tokens) AS input_tokens,
sum(ai_output_tokens) AS output_tokens,
min(timestamp) AS first_seen,
max(timestamp) AS last_seen
FROM (
SELECT
distinct_id,
timestamp,
toString(properties.$ai_trace_id) AS ai_trace_id,
toFloat(properties.$ai_total_cost_usd) AS ai_total_cost_usd,
toString(properties.$ai_error) AS ai_error,
toString(properties.$ai_is_error) AS ai_is_error,
toInt(properties.$ai_input_tokens) AS ai_input_tokens,
toInt(properties.$ai_output_tokens) AS ai_output_tokens,
toString(person.properties.email) AS email,
toString(person.properties.name) AS name
FROM events
WHERE event = '$ai_generation'
AND timestamp >= now() - INTERVAL 30 DAY
)
GROUP BY distinct_id
ORDER BY total_cost DESC
LIMIT 25
識別済みユーザーについて質問されている場合は、内側の WHERE 句にこれを追加します。
AND (
properties.$ai_trace_id IS NULL
OR distinct_id != properties.$ai_trace_id
)
email や name など、必要な明示的ラベル列のみを抽出します。未加工の person.properties オブジェクトや、それを含むタプルは決して抽出しないでください。結果にプロパティの全データがシリアル化され、ラベルに必要な範囲を大きく超えて個人データが漏えいします。ユーザーにメールアドレスも名前もない場合は、distinct_id を代わりに使用します。
2. ベースラインを確立する
最上位のユーザーは、他のユーザーと比較して初めて意味を持ちます。ユーザーごとのベースラインを算出し、そのユーザーのコストが高い理由が、生成回数の多さ、トレース数の多さ、生成あたりのコストの高さ、プロンプトの長さ、出力の長さ、エラー率の高さのどれなのかを判断できるようにします。
posthog:execute-sql
WITH per_user AS (
SELECT
distinct_id,
count() AS generations,
countDistinctIf(toString(properties.$ai_trace_id), notEmpty(toString(properties.$ai_trace_id))) AS traces,
countIf(notEmpty(toString(properties.$ai_error)) OR toString(properties.$ai_is_error) = 'true') AS errors,
sum(toFloat(properties.$ai_total_cost_usd)) AS total_cost,
avg(toFloat(properties.$ai_total_cost_usd)) AS avg_cost_per_generation,
avg(toInt(properties.$ai_input_tokens)) AS avg_input_tokens,
avg(toInt(properties.$ai_output_tokens)) AS avg_output_tokens
FROM events
WHERE event = '$ai_generation'
AND timestamp >= now() - INTERVAL 30 DAY
GROUP BY distinct_id
)
SELECT
count() AS users,
round(sum(total_cost), 4) AS project_total_cost,
round(avg(total_cost), 4) AS avg_cost_per_user,
round(quantile(0.5)(total_cost), 4) AS p50_user_cost,
round(quantile(0.9)(total_cost), 4) AS p90_user_cost,
round(quantile(0.99)(total_cost), 4) AS p99_user_cost,
round(avg(avg_cost_per_generation), 6) AS mean_user_cost_per_generation,
round(avg(avg_input_tokens), 0) AS mean_user_input_tokens,
round(avg(avg_output_tokens), 0) AS mean_user_output_tokens,
round(sum(errors) / nullIf(sum(generations), 0), 4) AS error_rate
FROM per_user
外側の集計列の名前は、集計対象となる CTE 列の名前とは異なります(total_cost ではなく project_total_cost)。HogQL では、外側の sum()/avg() 内にある単独の total_cost は、同じ名前の出力エイリアスに解決されます。これにより、集計関数が別の集計関数の中にネストされ、Aggregate function sum(per_user.total_cost) is found inside another aggregate function というエラーでクエリが失敗します。2 つの階層で異なる名前を使ってください。
ベースラインのクエリがまだエラーになる場合は、ベースラインを取得できなかったと報告し、回答でもその旨を明記してください。p50/p90/p99 の数値を捏造したり、それらの数値なしにユーザーが「中央値の Nx 倍」だと主張したりしないでください。代わりに絶対コストと支出全体に占める割合で順位付けし、ユーザーごとの分布を算出できなかったことを記載してください。
上位ユーザーを報告する際は、各ユーザーの支出全体に占める割合と、p50/p90 の何倍かを含めてください。そうすることで、偏りが明確になります。
3. 上位ユーザーのコスト要因を分解する
説明する価値のある各上位ユーザーについて、モデル別およびトークンコストの内訳に分けて支出を分析します。
posthog:execute-sql
SELECT
toString(properties.$ai_provider) AS provider,
toString(properties.$ai_model) AS model,
count() AS generations,
countDistinctIf(toString(properties.$ai_trace_id), notEmpty(toString(properties.$ai_trace_id))) AS traces,
round(sum(toFloat(properties.$ai_total_cost_usd)), 4) AS total_cost,
round(avg(toFloat(properties.$ai_total_cost_usd)), 6) AS avg_cost_per_generation,
sum(toInt(properties.$ai_input_tokens)) AS input_tokens,
sum(toInt(properties.$ai_output_tokens)) AS output_tokens,
sum(toInt(properties.$ai_reasoning_tokens)) AS reasoning_tokens,
sum(toInt(properties.$ai_cache_read_input_tokens)) AS cache_read_tokens,
sum(toInt(properties.$ai_cache_creation_input_tokens)) AS cache_write_tokens,
round(sum(toFloat(properties.$ai_input_cost_usd)), 4) AS input_cost,
round(sum(toFloat(properties.$ai_output_cost_usd)), 4) AS output_cost,
round(sum(toFloat(properties.$ai_request_cost_usd)), 4) AS request_cost,
round(sum(toFloat(properties.$ai_web_search_cost_usd)), 4) AS web_search_cost,
countIf(notEmpty(toString(properties.$ai_error)) OR toString(properties.$ai_is_error) = 'true') AS errors
FROM events
WHERE event = '$ai_generation'
AND timestamp >= now() - INTERVAL 30 DAY
AND distinct_id = '<distinct_id>'
GROUP BY provider, model
ORDER BY total_cost DESC
次の判断基準に沿って結果を解釈してください。
- 生成回数が多く、生成あたりのコストは通常の場合、主な要因は利用量です。
- 生成あたりのコストが高く、利用量は通常の場合、高額なモデル、長いコンテキスト、長い出力、推論トークン、ウェブ検索料金、リクエスト料金が主な要因です。
- 入力トークンが多い場合、通常はコンテキストの肥大化、会話履歴の繰り返し、大きな取得ドキュメント、または切り詰め処理の欠如が原因です。
- 出力トークンまたは推論トークンが多い場合、冗長な回答、Chain of Thought 形式の推論モデル、出力制限の欠如、またはツールのループが原因です。
- 入力が繰り返されているのにキャッシュ再利用率が低い場合、プロンプトキャッシュが活用されていない可能性があります。
exploring-llm-costs/references/cache-accounting.mdのキャッシュ計算式を使用してください。 - エラーが多い、または高コストのトレースが多数ある場合、再試行、失敗したツール呼び出し、またはループが考えられます。どれが原因か述べる前に、トレースを確認してください。
- リクエスト料金またはウェブ検索料金が高い場合、主な要因はプロバイダーの固定料金、またはツールを多用する生成であり、トークン量だけではありません。
4. 上位ユーザーを他の全ユーザーと比較する
プロジェクト全体について同じモデル別またはトークン別の内訳を算出し、比較します。合計値だけに頼らないでください。「このユーザーは他のユーザーと同じモデルを使っているが、生成回数は 9 倍だった」や「利用量は通常だったが、支出の 82% が他ではあまり使われていない高コストモデルによるものだった」といった説明ができるようにします。
比較に役立つ指標:
- プロジェクト全体のコストに占める上位ユーザーの割合
- 上位ユーザーの生成回数とトレース数を、ユーザーの p50/p90 と比較
- 生成あたりの平均コストとプロジェクト平均
- 生成あたりの入力トークン数とプロジェクト平均
- 生成あたりの出力トークン数または推論トークン数とプロジェクト平均
- エラー率とプロジェクト平均
- モデル構成と全体のモデル構成
- 同じモデルにおけるキャッシュヒット率と全体のキャッシュヒット率
5. ユーザーの高コストなトレースを特定する
SQL で順位付けしたトレース一覧を取得し、posthog:query-llm-trace で代表的なトレースを確認します。
posthog:execute-sql
SELECT
toString(properties.$ai_trace_id) AS trace_id,
count() AS generations,
round(sum(toFloat(properties.$ai_total_cost_usd)), 4) AS total_cost,
round(avg(toFloat(properties.$ai_total_cost_usd)), 6) AS avg_cost_per_generation,
sum(toInt(properties.$ai_input_tokens)) AS input_tokens,
sum(toInt(properties.$ai_output_tokens)) AS output_tokens,
countIf(notEmpty(toString(properties.$ai_error)) OR toString(properties.$ai_is_error) = 'true') AS errors,
min(timestamp) AS started_at,
max(timestamp) AS ended_at
FROM events
WHERE event = '$ai_generation'
AND timestamp >= now() - INTERVAL 30 DAY
AND distinct_id = '<distinct_id>'
AND notEmpty(toString(properties.$ai_trace_id))
GROUP BY trace_id
ORDER BY total_cost DESC
LIMIT 10
そのユーザーの上位 2~3 件以上のトレースを開きます。
posthog:query-llm-trace
{
"traceId": "<trace_id>",
"dateRange": { "date_from": "-30d" }
}
集計結果を説明できる、最初の具体的なパターンを探します。
- ツール呼び出しの繰り返し、または再試行ループ
- 大きなコンテキストウィンドウ、または取得ドキュメントの繰り返し
- 長いマルチターンセッション
- 通常のタスクに対して選択された高コストモデル
- 同じワークフローからの多数の小さな呼び出し
- 冗長な出力、または制約のない推論
- ウェブ検索料金やリクエスト料金が高い呼び出し
- モデルのコストが発生したエラー
6. 集計結果だけでは原因が分からない場合は、カスタムディメンションを確認する
上位のユーザーに多くのコストがかかっているように見える一方、モデルやトークンの内訳から理由が分からない場合は、$ai_generation のカスタムイベントプロパティを調べ、関連するプロダクトディメンションでグループ化します。一般的な例として、feature、tenant_id、organization_id、workflow_name、agent、route、environment がありますが、推測で決めないでください。
kind: "event_properties"とevent_name: "$ai_generation"を指定して、posthog:read-data-schemaを呼び出します。- 有望なフィールドについて、
kind: "event_property_values"を指定してposthog:read-data-schemaを呼び出し、実際の値を確認します。 - 見つかったプロパティごとに、上位ユーザーのコストをグループ化します。
posthog:execute-sql
SELECT
toString(properties.<property_name>) AS dimension,
count() AS generations,
countDistinctIf(toString(properties.$ai_trace_id), notEmpty(toString(properties.$ai_trace_id))) AS traces,
round(sum(toFloat(properties.$ai_total_cost_usd)), 4) AS total_cost,
round(avg(toFloat(properties.$ai_total_cost_usd)), 6) AS avg_cost_per_generation
FROM events
WHERE event = '$ai_generation'
AND timestamp >= now() - INTERVAL 30 DAY
AND distinct_id = '<distinct_id>'
AND isNotNull(properties.<property_name>)
GROUP BY dimension
ORDER BY total_cost DESC
LIMIT 20
この手順によって、「ユーザー 123 はコストが高い」というだけの状態から、「契約レビューのワークフローでは、実行のたびに 90k トークンのドキュメントを最も高コストなモデルに入力するため、コストが高い」という具体的な説明が得られることがよくあります。
UI リンクの作成
リンクの作成には posthog:generate-app-url を使用してください。プロジェクトのリージョンが異なる場合があるため、ホスト名をハードコードしないでください。
- トレース一覧:
generate-app-url { "url": "/ai-observability/traces" } - 単一のトレース:
generate-app-url { "url": "/ai-observability/traces/{id}", "params": { "id": "<trace_id>" } }
単一のトレースについて、トレースのタイムスタンプが分かる場合は、?timestamp=<url_encoded_started_at> を付加すると、UI で正しい時間範囲が開きます。
回答の形式
クエリではなく、回答を最初に示します。適切な回答には、次の内容が含まれます。
- 上位ユーザー - 総コスト順に並べ、総コスト、支出に占める割合、生成数、トレース数、生成あたりの平均コスト、エラー率を示します。各ユーザーはラベルのみ(メールアドレス、名前、または
distinct_id)で識別してください。ユーザーが求めていない生のperson.propertiesオブジェクトやその他の個人情報フィールドは表示しないでください。 - コストが高い理由 - ユーザーごとに、ベースラインと比較した具体的な要因を 1 つまたは 2 つ示します。
- 根拠 - モデル、トークン、キャッシュ、カスタムディメンションの内訳と、読み取ったトレースの例へのリンクを示します。
- 有効そうな対策 - 観測された要因に基づいて、具体的な最適化案を示します。コンテキストの削減、出力の上限設定、ワークフローへの低コストモデルの採用、キャッシュの改善、リトライループの修正、機能ごとのトラフィックの分割などが考えられます。
- 注意事項 - 結果に埋め込みが含まれるか、トレース ID のデフォルト値が除外されているか、最初のランキングとは異なるイベントセットを使用しているかを示します。
一般論は避けてください。データからモデル構成が要因だと分からない限り、「より安価なモデルを使う」という助言は役に立ちません。入力トークンがベースラインに比べて多くない限り、「プロンプトのサイズを減らす」という助言も役に立ちません。
関連スキル
exploring-llm-costs— プロジェクト全体の支出:合計、内訳、コストの回帰exploring-llm-traces— ユーザーのコストが高い生成の背後にあるトレースを読み取る