数週間前、私のリサーチAgentが前週の4倍のコストをかけ始めた。同じ機能、同じトラフィック、同じモデル。出力は問題なく見えた。請求書はそうではなかった。
明らかなことを認めるまで、午後中 print 文を追加した。問題は明白だった:特定の実行でAgentが実際に何をしているか、まったく把握できていなかったのだ。Agentは可変数のツール呼び出しを行い、一度も記録していない失敗で黙ってリトライし、時には同じドキュメントを3回読み返してから諦めるループに陥っていた。そのいずれも請求書以外のどこにも現れなかった。
犯人はリトライパスだった。不安定なツールがタイムアウトし、Agentはそのたびに全コンテキストを再送して黙ってリトライしていた。「成功した」1回の実行が、2回ではなく9回のモデル呼び出しをしていたのだ。それを発見できたのは、ようやく各ステップの動作を書き留めたからだ。あの午後は、どんなトレーシングツールの1ヶ月分よりも高くついた。
要するに:Agentは完全にはコントロールできないループであり、観察できないループは改善できない。実行ごとに、各ステップで何が起きたか、何トークン消費したか、いくらかかったか、どれくらい時間がかかったか、失敗したかどうかを記録する必要がある。開始するのにプラットフォームは不要だ。ログが必要なだけだ。
実際に何をキャプチャするか
ツールのことはしばらく忘れよう。問題は:実行が失敗したとき、何を知りたいか?そこから逆算すれば、リストが得られる。
各Agentの実行につき、1つのトレース——実行全体——が欲しい。それはスパンで構成され、各ステップに1つずつ対応する。各スパンには以下が含まれるべきだ:
- ステップの種類 — モデル呼び出し、ツール呼び出し、検索、最上位Agentの実行。
- トークン使用量 — 入力トークンと出力トークンを別々に。コストが異なり、教えてくれることも異なる。
- コスト — トークンとモデルの価格から算出。1回計算してスパンに保存する。
- レイテンシ — 開始時刻と終了時刻。遅い実行と高コストの実行は、通常は別の問題だ。
- ツール呼び出し — どのツールか、引数は何か、何を返したか(または何がどう失敗したか)。
- 結果 — ステップが成功、失敗、リトライしたか、そしてその理由。
- Evalスコア — 実行を採点するなら(すべきだ)、品質をコストと関連付けられるようスコアを添付する。
最後の点は人々がスキップするものだが、最も報われる。トレースは何が起きたかを教えてくれる。Evalスコアはそれが重要だったかどうかを教えてくれる。両者をつなぐことで、唯一重要な問いが立てられる:高コストなバージョンは本当に優れているか? たいていそうではない。
リストにないものに注意してほしい:デフォルトですべてのスパンに完全なプロンプトとレスポンステキストをキャプチャすること。メッセージコンテンツのキャプチャはデバッグに有用だが、プライバシーとストレージコストのリスクがある。特定の実行でオンにできるフラグの後ろにキャプチャし、常時全体に適用しない。
ベースラインとしてのOpenTelemetryのGenAI規約
独自のフィールド名を発明する前に、すでに存在するものを見てほしい。OpenTelemetry——あらゆるバックエンドトレーシングで使う同じベンダー中立の標準——にはGenAIセマンティック規約がある:先ほど列挙したものを命名するための合意された語彙だ。
有用な部分は属性名だ。モデル呼び出しスパンは gen_ai.request.model(どのモデルか)、gen_ai.usage.input_tokens と gen_ai.usage.output_tokens(トークンの内訳)、gen_ai.response.finish_reasons(なぜ止まったか — stop、tool_calls など)、そして gen_ai.provider.name(プロバイダーの識別)を持つ。プロンプトキャッシュの会計用の属性もある——gen_ai.usage.cache_read.input_tokens と gen_ai.usage.cache_creation.input_tokens——キャッシュが使われると非常に重要になる。キャッシュされた入力は桁違いに安くなりうるからだ。スパン名は {gen_ai.operation.name} {gen_ai.request.model} というフォーマットに従い、Agentレベルの規約は invoke_agent(実行全体)、chat(単一のモデル呼び出し)、execute_tool(ツール呼び出し)などの操作を定義する。(OTel GenAI spans, OTel GenAI observability blog)
正直な注意点が2つある。第1に、2026年半ばの時点でこれらの規約はまだDevelopmentステータスのままだ——安定しつつあるが固まっておらず、属性名はまだ変わりうる。最近独自のリポジトリに移行したことは、真剣に取り組まれていることと、まだ進化中であることの両方を示している。第2に、OTelの仕組みを採用しなくても恩恵を受けられる。たとえプレーンなJSONログを書く場合でも、これらのフィールド名を使ってほしい。自作ログを卒業して本格的なバックエンドに移行する日が来たとき、データはすでにその言語を話している。これほど安価な将来への互換性はない。
80%をカバーする自作JSONLログ
小規模プロジェクトで実際に運用しているセットアップを紹介する。追記専用のJSONLファイル1つ、スパンごとに1オブジェクト、OTelスタイルのフィールド名。サービスなし、SDKなし、アカウントなし。
import json, time, uuid
PRICES = { # USD per token; see your provider's pricing page
"claude-haiku-4.5": {"in": 1.00/1e6, "out": 5.00/1e6},
"claude-sonnet-4.6": {"in": 3.00/1e6, "out": 15.00/1e6},
}
def log_span(trace_id, kind, model, usage, t0, status, **extra):
p = PRICES.get(model, {"in": 0, "out": 0})
cost = usage["in"] * p["in"] + usage["out"] * p["out"]
record = {
"trace_id": trace_id,
"span_id": uuid.uuid4().hex[:12],
"gen_ai.operation.name": kind, # invoke_agent | chat | execute_tool
"gen_ai.request.model": model,
"gen_ai.usage.input_tokens": usage["in"],
"gen_ai.usage.output_tokens": usage["out"],
"cost_usd": round(cost, 6),
"latency_ms": int((time.time() - t0) * 1000),
"status": status, # ok | error | retry
**extra, # feature, tool_name, eval_score...
}
with open("traces.jsonl", "a") as f:
f.write(json.dumps(record) + "\n")
Agentループの各ステップが共通の trace_id で log_span を呼ぶ。各実行にそれが提供した機能のタグをつける(feature="research"、feature="summarize")。これでファイルへの一行コマンドで実際の問いに答えられる:
# cost per feature, last run-set
jq -s 'group_by(.feature)[] | {feature: .[0].feature,
usd: (map(.cost_usd) | add)}' traces.jsonl
以上だ。実行ごとのコスト、機能ごとの帰属、レイテンシ、リトライ回数、そして——Evalスコアをスパンに書いていれば——品質対コスト、すべてが jq、grep、またはDuckDBで読めるファイルから得られる。私の請求を4倍にしたリトライループは、ここでは1つの trace_id の下に積み重なった status: "retry" の行として現れたはずだ。1午後の推測ではなく、3分間の確認で済む。
価格表は正確に保つべき唯一のものだ。現在のClaudeの料金体系では出力トークンのコストは入力の約5倍で、私が使った参考価格は確認時点でHaiku 4.5が100万トークンあたり$1/$5、Sonnet 4.6が$3/$15だった——ただし料金は変わるし、キャッシュやバッチ割引で実際の数値は大きく動く。どんな表もスナップショットとして扱い、プロバイダーのページを確認してほしい。(Claude API pricing breakdown)
トークンコストの帰属:本当に価値を生む問い
ここで規律が真価を発揮する。すべてのスパンがコストと機能タグを持てば、ユニットエコノミクスについて推測するのをやめられる。
自分のプロジェクトで実行したところ、「無料」の機能——ドキュメントアップロードのたびに起動するオート要約——がモデル費用の60%を占めており、ユーザーの約10分の1しか使っていないことがわかった。バグではなかった。盲目的に下した製品の意思決定だった。機能ごとのコストが見えることで、意識的に下せる意思決定になった:オプトインにしたところ、請求は半減し、クレームはゼロだった。
これが帰属の全てだ。総支出は行動できない数字だ。「機能Xは実行あたり$0.40、機能Yは$0.02」は行動できる数字だ。Evalスコアと組み合わせると最も鋭い版になる:良い実行1回あたりのコスト。高コストなパスが安価なパスと同スコアなら、高コストなパスは洒落た説明のついた漏れに過ぎない。
構築か購入か:JSONLログが不十分になるとき
自作ログは長期にわたって本当に十分だ。ファイルでは安く提供できないものが欲しくなったとき、不十分になる:1つの複雑なトレースをクリックして辿るUI、log_span をあちこちで手動呼び出しせずに済む自動計装、CIに組み込まれたEvalの実行、またはターミナルに住んでいない誰かとトレースを共有すること。
その時点でホスト型ツールを検討する価値があり、正直な要約はこうだ:ツールは「最高」ではなく、それぞれの得意なことでグループ化される:
- Langfuse はトレースごとにプロンプト、レスポンス、トークン使用量、レイテンシ、コストをキャプチャし、MITライセンスでセルフホスト可能——UIが欲しいがデータは手元に置きたい場合のJSONLファイルからの自然な次のステップ。(2026年初頭にClickHouseに買収されたことは、ベンダーの安定性が重要なら知っておくべきだ。)
- Arize Phoenix はOpenTelemetry-ネイティブ——OTelとOpenInference計装に基づく——でDockerを通じてローカルで動作し、Evalメトリクスの強力なオープンソースライブラリを持つ。上記のOTel規約を受け入れたなら、これが最も摩擦の少ないバックエンドだ。
- LangSmith はLangChain/LangGraph上にある場合が最も深く、そうでない場合は劣る。
- Braintrust はEval-firstに傾き、品質が後退したときマージをブロックできるCIゲートを持つ——ダッシュボードよりもリリース規律が重要なときに有用だ。
どれかを優勝させるつもりはない。私が使う判断基準:ダッシュボードが欲しければLangfuseかPhoenix;CIのEvalゲートが欲しければBraintrust;LangChainに深く入っているならLangSmith。まだどちらも不要なら、JSONLファイルのままだ。ファイルは今日持っている問いに既に答えているから。また主要なコーディングAgent——Claude Code、Codex、Copilot——が自らOTelテレメトリをエクスポートするようになったことも触れておく価値がある。グルーコードを書かずともその恩恵の一部を得られる。
これが過剰になるとき
私が言おう:3人のユーザーと1つの機能しかない真新しいプロトタイプには、フルトレーシングは厳密さの衣をまとった先延ばしだ。すべての実行を目で読めて、月次請求が端数誤差の範囲なら、トークン数とコストをログして、残りはスキップして、製品を作りにいけ。
正直な閾値は2つのシグナルだ:請求書が驚かせたとき、または失敗が隠れたとき。コストの急増を説明できない初めての瞬間、または再現できない方法でAgentが失敗する初めての瞬間、あなたはその線を超えた——解決策は log_span を追加する1つの午後であって、プラットフォームを立ち上げる1週間ではない。
ファイルから始めよう。OTelのフィールド名を使って、今日のハックが明日の移行パスになるようにしよう。すべてのスパンに機能とコストのタグをつけよう。ファイルが問いに答えなくなった日にツールを買おう——それより前ではなく。目標はObservability自体のためのObservabilityではなかった。目標は、うまくいかなかった実行を見て、3分でお金がどこに消えたかを正確に知れることだった。
