レッスン3: 構造化ログとメトリクス: 各ステップをデータに変える
学習目標:
- 本番の可観測性が答えなければならない4つの問いを説明し、それが評価メトリクスと同じ数字であり、使い方だけが違うことを認識する
- 自分のハーネス向けに構造化ログを設計する: モデルリクエスト1回につき1レコード、ツール呼び出し1回につき1レコード。フィールドは所要時間、トークン、ツール名、エラーをカバーする
- 診断的な読み方でメトリクスのパターンを具体的な修正に対応づけ、「エラーゼロ」のようなシグナルが記録の仕方によって歪んでいる場面を見抜く
前提: レッスン1と2を完了し、動くハーネスループ(このシリーズの第7コース)が手元にあること | 前: レッスン2 << | 次: レッスン4 >>
1回なら読める、200回は読めない
レッスン2の最後で、あなたは価値のあることをしました: 生のトランスクリプトを最初から最後まで読み、エージェントが一度も口にしなかった問題を3つ捕まえたのです。方法は機能しますし、証拠も確かです。問題は、それが1回の実行だったということです。
同じエージェントを本番に置いてみます: 1日200実行、それぞれループが十数周し、合わせて2000から3000回のツール呼び出しの往復になります。月曜の朝、誰かが「金曜の午後のバッチ、やけに遅かった気がする」と言ったとき、どう応じますか。200本のトランスクリプトを読むのは、どう見ても現実的ではありません。仮に読んだとしても、どこが遅かったのかには答えられません——その判断には実行をまたいだ分布を見る必要があり、1つのサンプルを読むことではないからです。人間の目は「この実行でなぜこうしたのか」には答えられますが、「このバッチが前のバッチとどう違ったか」には答えられません。
だからこのレッスンの仕事は一文で収まります: トランスクリプトを読まなければ答えられない問いを、1本のクエリで答えられる問いに変える。 前者は「なぜ今回、同じ語を繰り返し検索したのか」であり、後者は「過去7日間で最も多く呼ばれたツールはどれで、エラーは全部同じパラメータに乗っているか」です。レッスン2のルールは今も生きています——生のトランスクリプトが一次証拠であり、エージェントの自己申告は数えない。このレッスンは同じ証拠を別の形式で保存するだけです。人が読めて、なおかつ絞り込み、集計し、分布を見られるように。
本番環境が答えなければならない4つの問い
公式ドキュメントは、本番の可観測性で見えていなければならないものを4つ挙げています: どのツールが呼ばれたか、各モデルリクエストにどれだけ時間がかかったか、トークンをどれだけ使ったか、失敗はどこで起きたか1。設計上の判断をするたび、この4つの問いに戻ることになります: このフィールドは、そのうちのどれかに答える助けになるか。ならないなら、それはノイズです。
見覚えがあるかもしれません。このシリーズの第10コースは、評価を論じるときに同じ数字の組を使っていました: 最終状態の正確さに加えて、個々のツール呼び出しとタスク全体の実行時間、ツール呼び出しの総数、トークン消費の総量、そしてツールのエラーを収集することを勧めていました2。同じメトリクスが2度登場し、使い方が違うのです:
違いは数字にではなく、何と比べるかにあります。評価では、固定されたテストセットに対して「変更前 対 変更後」を比べるので、数字は再現可能である必要があります。監視では「今日 対 過去7日間」や「このセッション 対 他のセッション」を比べるので、基準は実行自身の履歴になり、数字は連続していて、タイムスタンプを持ち、次元で切れる必要があります。このレッスンが扱うのは後者です。
構造化ログ: 1ステップにつき1レコード
このセクションはエンジニアリングの実践を述べています。ログのフィールドをどう命名するか、どの形式でディスクに書くかについて、権威ある指針はありません。以下は公式仕様ではなく、動く既定の出発点です。フィールド名は公式資料に実際に現れる語彙(session id、prompt id、tool name、tool_input、tool_response、duration_ms、トークン数、error)から借りているので、あとから公式のテレメトリと統合するときに語彙を対応づけ直す必要がありません。
記録の単位: モデルリクエストに1つ、ツール呼び出しに1つ
エージェントループには、自然に2種類の「ステップ」があります: モデルリクエストが1回、ツール実行が1回。両者の属性はかなり違います——モデルリクエストにはトークン数がありますがツール名はなく、ツール実行はその逆です——が、同じ文脈フィールドの束(どのセッションか、どのプロンプトか、何ミリ秒か)を共有します。
だから: モデルリクエスト1回につき1レコード、ツール呼び出し1回につき1レコードを書き、type フィールドで区別します。ループの1周を丸ごと1レコードに圧縮してはいけません——そうするとモデル時間とツール時間の内訳を永久に計算できなくなります。タスク完了時にサマリーのレコードを1本だけ書くのもだめです——タスクが途中で止まったら、どのステップで止まったかすらわからなくなります。
形式: JSON Lines、1行1オブジェクト
JSON Lines(一般に JSONL と書きます)は名前のとおりです: 1つのファイルの各行が完全な JSON オブジェクトで、行の間にカンマはなく、外側を配列で包むこともしません。
選ぶ理由はどれも地味ですが、どれも妥当です: 追記だけで書けるので、ファイル末尾に戻って ] を足す必要がありません。したがってプロセスが強制終了されても、構文的に壊れたファイルが残ることはありません(最後の1行は書きかけかもしれませんが、それより前の行はすべてパースできます——これが演習の「不正な行は報告するが処理は止めない」という要件の、現実世界での出どころでもあります)。ファイルが数百メガバイトに育っても、1行ずつストリーム処理できます。各行が自己完結しているので、grep で絞り込め、jq で処理でき、人の目でも読めます。
これを、多くのハーネスがすでに持っている散文のログ——[09:12:05] search_docs returned 3 results, took 412ms——と比べてみてください。読み心地はいいのですが、読めるのは人間だけです。「過去7日間の search_docs の平均所要時間は」に答えるには、あの 412ms を取り出す正規表現を書く必要がありますし、誰かが "took" を "elapsed" に変えれば、正規表現は黙ってゼロを返し始めます。散文のログは構造を自然言語にエンコードしており、自然言語は人間がデコードするためのものです。構造化ログはそれを逆にします: 構造はフィールドに宿り、機械が曖昧さなく読みます。絞り込める、集計できる、分布を見られる——この3つの能力こそ、1回から200回へ移るときに実際に必要になるものです。
フィールドの語彙
すべてのレコードに載せる文脈: ts(ミリ秒とタイムゾーンを含む ISO 8601 のタイムスタンプ)、type(model_call か tool_call)、session_id(1つのセッションの識別子。複数ターンにまたがって変わらない)、prompt_id(1つのユーザープロンプトの識別子。それが引き起こしたすべてのモデルリクエストとツール呼び出しがこの値を共有する)、duration_ms。モデルリクエストには model、stop_reason、input_tokens / output_tokens を足します。ツール呼び出しには tool、tool_use_id(レスポンスと対にするため)、error(失敗したときだけ存在)を足します。
prompt_id はここで最も目立たないフィールドですが、あとで最も役立つものになります。いまは全レコードに書き込んでいるだけですが、レッスン4ではこれを使って散らばったレコードを「同じプロンプトから出たイベント」として囲い込み、親子関係で1本の木に束ねます。ツールの入力と戻り値そのもの(tool_input / tool_response)については——既定では全文を書かないでください。長さかバイト数だけを記録します。その理由は最後から2番目のセクションで扱います。
ハーネスへの組み込み
以下のコードは、このシリーズの第7コースの stop_reason 駆動のループの上に載せるものです。まずロガーから:
logRecord() がやることは1つです: 共通の文脈と呼び出し側のフィールドを1行の JSON に統合し、ファイルに追記する。判断もせず、整形もせず、「気の利いた」ことは何もしません——ロガーは愚かであればあるほど良いのです。壊れたら、確認できるログがなくなるのですから。次に、ループの中の2つの計測点:
取り上げておく価値のあるディテールが2つあります。
タイマーをどこで始めてどこで止めるかが、この数字の意味を決めます。 t1 は runTool の前で始まり、戻ってきたあとで止まるので、duration_ms にはツール自身のリトライ、バックオフの待ち、ネットワークの往復が含まれますが、呼び出し前のパラメータ検証は含まれません。この境界を決めるのはあなた自身です。決めたら書き留めてください——半年後に30秒の duration_ms を眺めるとき、それがリトライを含むのかどうかを知る必要があります。
エラーはログとモデルのコンテキストの両方に入れます。 catch ブロックはエラーメッセージを tool_result に押し戻すので、エージェントは次のターンでそれを見ます。公式のガイダンスがここにぴったり当てはまります: ツール呼び出しがエラーを発生させたとき、そのレスポンスは、不透明なエラーコードやトレースバックではなく、具体的で実行可能な改善策を明確に伝えるようにプロンプトエンジニアリングされるべきです2。ログには ETIMEDOUT と書いて構いませんが、モデルに返すのは「リクエストがタイムアウトしました(30秒)。このエンドポイントは範囲の広いクエリでタイムアウトしがちです。date_range を7日以内に絞ってみてください」であるべきです。
メトリクスを診断的に読む
メトリクスの価値は「今日はツール呼び出しが1,283回でした」という数字そのものにはありません——あるパターンがある修正を指し示すところにあります。公式に示されている対応づけは、いずれもまず検証する価値のある手がかりです:
冗長な呼び出しが多い → ページネーションやトークン上限のパラメータを調整すべきかもしれない。 冗長なツール呼び出しが多いことは、ページネーションやトークン上限のパラメータのサイズ調整が妥当であることを示唆しているかもしれません2。モデルはドキュメントの中のある一節を見つける必要があるのに、あなたの search_docs は1ページ5件しか返さないので、28ページめくる羽目になります。この28回の呼び出しはどれも正当で、どれも成功し、メトリクスに「エラー」は現れませんが、全部が無駄です。1ページあたりの件数を25件に上げれば、このパターンは消えます。
不正パラメータのエラーが多い → ツールの説明に明確さか例が必要そう。 不正パラメータによるツールのエラーが多いことは、ツールの説明をより明確にするか、より良い例を足すべきことを示唆しているかもしれません2。これはエラーが同じパラメータに集中しているときに強力です: 7件のエラーがすべて invalid parameter: date_range と言っているなら、まずそのパラメータがどんな形式を期待するのかを説明に書いているかを確認すべきです。調査の方向はツールの説明であって、モデルではありません。
ツール呼び出しの追跡は、他のことも明らかにします。 ツール呼び出しを追跡することは、エージェントが辿るよくあるワークフローを明らかにし、ツールを統合する機会をいくらか提示するのに役立ちます2。たとえば read_file の呼び出しの90%のあとに parse_config が続いているなら、1ステップで済む read_config を提供すべきかもしれません。この種の発見は、どの単一の実行からも決して現れません——集計からしか現れないのです。もう1組の有用な読み方: ツール呼び出しのメトリクスを分析して、最も頻繁に使われるツール、ツールの成功率、平均のツール実行時間、そしてツール種別ごとのエラーパターンを特定する3。
そもそも量の問題である問題もあります——集計を見ずに「どこが多いのか」は言えません。Anthropicはこの種の初期の問題を記録しています: 存在しないソースを求めてウェブを果てしなく漁る4——個々の検索を1つ見ても異常には見えません。数十回の呼び出しを並べて初めて「その場で空回りしている」ことが見えます。
一般的な経験則を1つ(権威ある出典はありません): 平均所要時間はほぼ必ず嘘をつきます。80msの呼び出しが99回に30秒の呼び出しが1回混ざると平均は379msになり、少し遅いが許容範囲に見えます。実態は、速い呼び出しが99回と、完全に固まったものが1回です。所要時間を読むときは、最低でも中央値と高いパーセンタイルを見るか、いっそ最も遅い数件のレコードを直接見てください。
数字を読むときの罠: そのシグナルは実際に何を数えているのか
メトリクスが嘘をつく最も簡単な道は、数え間違えることではありません——数えているものが、あなたが思っているものではないときです。
実際の製品設計を見てみましょう。Claude Code は失敗した API リクエストを内部でリトライし、諦めたあとに初めて api_error イベントを1つだけ発します——このイベントはそのリクエストにとっての終端シグナルであり、途中のリトライ試行は別々のイベントとしては記録されません3。この設計は理にかなっています: リトライのたびにエラーを記録していたら、エラーのグラフは自動回復が処理した一時的なつまずきで溢れ、実際にいくつのリクエストが失敗したのかが見えなくなります。代償は、この意味論を覚えておかなければならないことです——「今日 api_error が3件」は「3件のリクエストが最終的に失敗した」であって「ネットワークのつまずきが3回」ではありませんし、その下に成功したリトライがいくつ隠れているかについては何も語りません。
同じドキュメントページは、非常に実用的な読み方を示しています: セッションがエラーから回復したのか完全に停止したのかを区別するには、イベントを session id でグルーピングし、エラーのあとに後続の API リクエストイベントが存在するかを確認します3。続きがあれば動き続けたということ、なければそこで止まったということです。この判定はグルーピング1回と「エラー以降にレコードがあるかを走査する」1回で済み、費用対効果が極めて高いものです——レベル2の演習で、まさにこれを書いてもらいます。(追記で書かれた JSONL は自然に時刻順なので、単一ファイルの中では明示的なソートは要りません。複数プロセスからログが来る場合は、先に ts でソートしてください。)
この罠から一般的な実践を引き出せます: 各メトリクスについて「これは何を数えているか」を一文で書く。 コードのコメントか、フィールドのドキュメントに書いてください。「ツールのエラー数=すべてのリトライが失敗したあとに1件」と「=例外が投げられるたびに1件」はまったく別のメトリクスですが、名前は同じでありえますし、半年後にダッシュボードを見る人は数字だけからは区別できません。
コストとトークン: 最も見張る価値のある1つの数字
もし1つの数字しか見張れないなら、トークンを見張ってください。
まず桁です。Anthropicのデータでは、エージェントは通常チャットのやり取りの約4倍のトークンを使い、マルチエージェントシステムはチャットの約15倍のトークンを使います4。これは彼らが自分たちのシステムについて観測した値であり、普遍の定数ではありませんが、期待値を定めてくれます: チャット機能をエージェントに作り替えると、請求は「少し」上がるのでは済みません。彼らにはもう1つ統計的な観測があります: トークン使用量それ自体が分散の80%を説明し、ツール呼び出しの数とモデルの選択が他の2つの説明要因である4——これは評価の性能を分析した段落から来ており、「どの量が実行間の差を最もよく説明するか」という意味で、トークンが第1位です。両方を合わせて読んでください: トークンは請求の最大部分であると同時に、実行ごとのばらつきの最大の説明要因でもあります。だから候補となるメトリクスの中で、最初に見張る価値が最も高いのです。
実務上の注意が2つ。コストの数字は近似値です: 公式ドキュメントはコストのメトリクスが近似値であり、公式の請求データはAPIプロバイダーを参照するようにと述べています3。だから用途は「異常を見つける、傾向を比べる」であって、「経理と突き合わせる」ではありません。アトリビューションには次元での切り分けが要ります: 使用量のメトリクスは、チームや個人をまたいだ傾向の追跡、使用量の多いセッションの特定に使えますし、スキル名、プラグイン名、サブエージェントの種類といった具体的なものに支出を帰属させることもできます3。自作ハーネスへの含意は直接的です——それらの次元を最初からログのレコードに書き込み、あとから結合しようとしないこと。事後に次元を結合するのは、要するに再実行と同じです。それから、トークン数はモデルレスポンスの usage フィールドから直接写してください。文字数を4で割るような方法で推定しないこと——日本語と英語の混在、コードが多い場合、画像を含む場合には、目に見えてずれます。
抑制: しきい値を発明しない、全文を記録しない
メトリクスが手に入ると、次に自然に湧く衝動はアラートの設定です: エラー率が5%を超えたらアラート、高パーセンタイルの所要時間が10秒を超えたらアラート。
やめてください。このレッスンはしきい値の数字を一切与えません。権威ある資料にそれが存在しないからです。 公式ドキュメントはアラートを誰かがやるべきことだとは述べていますが、具体的な値を与えたことは一度もありません——エラーバジェット、SLOの目標、アラートのしきい値、どれも数字はゼロです。もしここで「5%を推奨」と書いたら、それは私が作り出したもので、あなたはそれを使ってしまうでしょう。しきい値は自分のベースラインからしか育ちません: まず2週間分のデータを記録し、正常なゆらぎの幅を見て、それから何を異常と呼ぶかを定義します。順序を逆にすると、1日に3回誤報を出し、2週間後には全員にミュートされるルールができあがります。
責任の分担も公式の製品から写す価値があります: Claude Code は生のイベントストリームだけを発し、異常検知、ベースライン化、セッションをまたいだ相関、そしてアラートは、あなたの SIEM または可観測性のバックエンドの責任です3。自作ハーネスにとってこれが意味するのは: 観測される側のシステムは、自分で判断を下さないということです。「ツールのエラーが3回連続したらメールを送る」をハーネスの中に書かないでください——そのロジックはエージェントと一緒にデプロイされ、エージェントと一緒に再起動され、エージェントと一緒に壊れますし、比較対象となる履歴データも持っていません。
最後にもう1つ、これはローンチの3か月後に最もインシデント化しやすいものです: 既定でコンテンツを記録しないでください。 Claude Code は既定でユーザープロンプトの内容を収集せず、記録するのは長さだけです。内容を含めるには環境変数を明示的に設定する必要があります3。Agent SDK のテレメトリも同様に構造優先で、すべてのスパンに所要時間、モデル名、ツール名が記録され、トークン数はAPIリクエストが使用量データを返したときに記録されますが、エージェントが読み書きするコンテンツは既定では記録されません1。
この2つの既定値は、同じ判断を反映しています: 構造的な情報(誰が、いつ、どれだけ、どのツールを、何トークン)は運用上の問いの大多数に答えるのに十分であり、コンテンツはそうではない、と。コンテンツがいったんログに入れば、ログとともにバックアップへ、長期保管へ、そして読み取り権限を持つ全員の視界へ流れていきます。だからあなたのハーネスは、既定では tool_input: {...} ではなく input_bytes: 137 を記録すべきです。特定の呼び出しの正確なパラメータをどうしても調べる必要があるときは、その1回だけ全文記録を有効にします。これはレッスン2の「生のトランスクリプトが一次証拠」と矛盾しません: デバッグ時には往復の全体を絶対に見るべきですが、それは自分が管理する環境で、特定の実行について、読み終わったら終わりです。本番のログは既定で長期保管され、複数人から見えます——それは別の話です。
境界: このレッスンがどこで止まるか
ここまでで、構造化されたレコードの山と、読めるメトリクスの組が手に入りました。このレッスンがやらないことが3つあります。レコードどうしの親子関係を通すこと(1つのプロンプトがどのモデルリクエストを引き起こしたか、どのツール呼び出しがどのサブエージェントの下に入れ子になるか)には、木を組み立てるための相関IDが必要です——それはレッスン4。ハーネスのコードを変えずにライフサイクルの要所にプローブを掛けるのは、レッスン5のフックです。この層をまるごと第7コースのハーネスに載せ、完全なデバッグの実習を通すのがレッスン6です。
💻 演習
まとめ
- 本番の可観測性は4つの問いに答えなければなりません: どのツールが呼ばれたか、各モデルリクエストにどれだけ時間がかかったか、トークンをどれだけ使ったか、失敗はどこで起きたか1。この4つと、このシリーズの第10コースの評価メトリクス(個々のツール呼び出しとタスク全体の実行時間、ツール呼び出しの総数、トークン消費の総量、ツールのエラー)2は同じ数字の組です——評価では変更が改善をもたらしたかの判断に使い、監視では実行の健全性を見張るのに使います。
- ログのフィールド設計と JSONL の選択に権威ある仕様はなく、あなたのエンジニアリング上の判断です。既定の出発点: モデルリクエスト1回につき1レコード、ツール呼び出し1回につき1レコード、1行1つの JSON オブジェクト、session id、prompt id、所要時間、トークン数、ツール名、エラーを含める。散文のログは人間にしか読めませんが、構造化されたものは絞り込め、集計でき、分布を見られます。
- メトリクスの価値は、パターンが修正に直接対応づくところにあります: 冗長な呼び出しが多いことはページネーションやトークン上限のパラメータの調整が必要なことを意味し、不正パラメータのエラーが多いことはツールの説明に明確さか例が必要なことを意味します2。ツール呼び出しの追跡は、エージェントのよくあるワークフローと、ツールを統合する機会も明らかにします2。ツール呼び出しがエラーを発生させたとき、そのレスポンス自体を、不透明なエラーコードではなく具体的で実行可能な指針として書くべきです2。
- シグナルの意味論は、記録の仕方によって定義されます。Claude Code は失敗した API リクエストを内部でリトライし、諦めたあとに初めて
api_error イベントを1つだけ発します——それはそのリクエストの終端シグナルであり、途中のリトライは個別に記録されません3——だから1つの「エラー数」の下に、見えないリトライが大量に隠れていることがあります。セッションが回復したのか停止したのかを区別するには、イベントを session id でグルーピングし、エラーのあとに後続のリクエストイベントがあるかを確認します3。
- トークンは、単独で最も見張る価値のあるメトリクスです: Anthropicのデータでは、エージェントはチャットの約4倍、マルチエージェントシステムは約15倍のトークンを使います4。評価の性能を分析したとき、彼らはトークン使用量それ自体が分散の80%を説明し、ツール呼び出しの数とモデルの選択が他の2つの説明要因であることを見出しました4。コストのメトリクスは近似値で、公式の請求はAPIプロバイダーから来ます3。支出は、スキル名、プラグイン名、サブエージェントの種類といった具体的なものに帰属させられます3。
- 抑制の原則が2つ: 観測される側のシステムは生のイベントストリームだけを発し、異常検知、ベースライン化、アラートはバックエンドの責任です3。ログは既定でコンテンツを記録すべきではありません——公式の製品は既定でプロンプトの内容を収集せず、長さだけを記録します3。テレメトリも既定では構造的な情報だけを記録し、エージェントが読み書きするものは記録しません1。アラートのしきい値と SLO は権威ある資料に数字がありません——発明しないでください。まず2週間のベースラインを記録します。
レッスン4: トレース: 一度の実行を1本の木につなぐ >>