10,847件の意思決定イベント。3つの意外な洞察。そして、エージェントの可観測性について考え方を変えた1つの23ドルの目覚まし代。
23ドルの謎
私は市場調査のためのマルチエージェントシステムを運用しています。3つのエージェント、1つの目的:
- Scout:APIとWebソースからデータを収集
- Analyst:生データを洞察に変換
- Writer:最終レポートを作成
うまくいっていました。ですが、ある日うまくいかなくなりました。
ある月曜の朝、私は48時間遅れ、そして$23のAPIクレジットがかかったレポートを見つけました。通常の実行は2時間で、だいたい$4かかります。
何が起きた?
私はすべて確認しました。APIのレート制限?いいえ。モデルのダウンタイム?いいえ。LangSmithのトレースではチェーンは正常に完了していることが示されました。各エージェントは「タスク完了」と報告。ログの各行はすべて緑でした。
しかし「タスク完了」から「レポートが用意できる」までのどこかで、46時間が消えていました。
そこで気づきました。私は自分のエージェントが実際に何を決めているのか、まったく分かっていなかったのです。分かっていたのは、彼らが何をしたかだけでした。
そこで実験をしました。
実験:50行のコード、1週間、すべての意思決定
私はエージェントのオーケストレータに軽量な意思決定ロガーを追加しました。API呼び出しのトレースはしません—それはすでにあります。記録したかったのは意思決定です:
-
J(Judge):エージェントが新しいタスクを開始する、または判断を下す -
D(Delegate):エージェントが別のエージェントへ仕事を引き渡す -
T(Terminate):エージェントがタスクを終了する(成功/失敗は問わない) -
V(Verify):エージェントが他者の出力を検証する
これが中核となるコード(簡略版—完全版はGitHub):
import json
import hashlib
import time
import asyncio
from uuid import uuid4
def hash_content(content: str) -> str:
"""意思決定ペイロード用のコンテンツアドレス指定ハッシュを作成する。"""
return f"sha256:{hashlib.sha256(content.encode()).hexdigest()[:16]}"
async def log_decision(verb: str, who: str, what: str, ref: str = None):
event = {
"jep": "1",
"verb": verb,
"who": who,
"what": hash_content(what) if what else None,
"when": int(time.time()),
"nonce": str(uuid4()),
"ref": ref,
"aud": "research-pipeline-v1"
}
# 非同期書き込み—エージェントをブロックしない
await write_to_ndjson(event)
私はこれを火曜日にデプロイしました。1週間後、私は10,847件の意思決定イベントを得ていました。
そこで分かったことは以下です。
発見#1:委任(Delegate)の35%が循環していた
私のエージェントたちは、常に互いに仕事を委任します。Scoutは生データをAnalystへ渡します。Analystは洞察をWriterへ渡します。WriterはScoutに明確化を求めます。普通ですね。
しかし、D(Delegate)イベントをrefチェーンごとにグラフ化すると、予想外のものが見えました:
Scout → Analyst → Scout → Analyst → Scout(終了)
1,203回、1週間の間に、エージェントは長さ≥2の委任ループを作っていました。各ループで次のものが消費されていました:
- 約2秒の計算時間
- 引き継ぎ理由付けのためのLLM呼び出しが1回追加
- 委任メッセージそのもののトークンコスト
合計の無駄:約40分の計算と、APIコストで3.20ドル。致命的ではありません。ですが、Dイベントをそれぞれのrefチェーンとともにログに残すまで、完全に見えませんでした。
修正:私は単純なルールを追加しました。あるエージェントが、現在のチェーンの中で「すでに委任した相手」から委任を受け取った場合は、そこで中断してエスカレーションする。するとループはほぼゼロまで減りました。
発見#2:失敗したツール呼び出しは、あきらめるまで7回リトライされていた
Scoutの仕事の1つは、公開Webサイトから競合の価格をスクレイピングすることです。ときどき、サイトがタイムアウトします。これは普通です。
普通ではなかったのはリトライ挙動です。
ツール呼び出しが失敗すると、エージェントは平均で7回リトライしてから終了していました。最もひどかったのはそのスクレイピング用ツールです。23:23のタイムアウトが次のように連鎖していました:
23:23 - ツール呼び出しが失敗(タイムアウト)
23:24 - リトライ1が失敗
23:26 - リトライ2が失敗
23:29 - リトライ3が失敗
...
03:17 - リトライ11が失敗し、エージェントはようやく終了
4時間。11回の再試行。それぞれが、新しいブラウザインスタンスを使った新規のAPI呼び出しです。この失敗の連鎖1本あたりのコスト: $1.87。
週を通して、過剰な再試行によって ~$9.40 が無駄になりました。
修正: 重要でないツールについては再試行回数を最大3に制限しました。まだ失敗する場合、エージェントは reason: "tool_unavailable" とともに T をログし、部分的なデータで次へ進みます。レポートの完成度は少し下がるかもしれませんが、時間どおりに、予算内に収まります。
Discovery #3: The 3 AM Termination Storm
水曜日の3:14 AMに、ログの中で妙なものを見つけました:
90秒以内に T(Terminate)イベントが47回発生。
通常のレートは1時間あたり~10回です。
それらはすべて reason: "empty_response" を持っていました。判明したのは、データ提供者のAPIが一時的に停止しており、200 OK を返しつつ本文が空だったことです。並列の各エージェントが同時に叩き、何も受け取れず、即座に終了しました。
アラートは一度も鳴りませんでした。オーケストレータの観点では、すべてのタスクは「正常に完了」していましたが、実際にはデータがゼロのまま完了していたのです。当日の朝の最終レポートは通常より40%短く、それがなぜか意思決定ログを掘り下げるまで分かりませんでした。
修正: シンプルな監視を追加しました。1分あたり reason: "empty_response" の T イベントが5回を超えたら、パイプラインを一時停止して通知します。次にそのAPIが不安定になったとき、60秒以内に気づけました。
Discovery #4: Verification Was Silently Slowing Down
V(Verify)イベントは、あるエージェントが別のエージェントの出力を確認するときに発生します。アナリストが洞察を作り、ライターはそれらを含める前に筋が通っているかを検証します。
タイムスタンプのところで、次のようなものに気づきました:
| Day | Avg Time Between J and V
|
|---|---|
| Tuesday | 1.2 seconds |
| Wednesday | 1.8 seconds |
| Thursday | 2.9 seconds |
| Friday | 4.1 seconds |
| Monday | 4.7 seconds |
検証サービスが じわじわと 低速化していました。まだ何かを壊すほどではありませんが、明確な傾向です。判明したのは、ファクトチェックに使うベクトルDBに6か月分の古い埋め込み(embedding)が溜まっており、クエリが遅くなっていたことでした。
意思決定レベルのタイムスタンプがなければ、タイムアウトし始めてパイプラインを壊し始めてからしか気づけなかったでしょう。代わりに、週末に再インデックスのジョブをスケジュールしました。火曜の朝: 1.3秒に戻りました。
What I Changed (And What You Can Steal)
私は複雑な可観測性(observability)プラットフォームを作りませんでした。意思決定ログから分かったことに基づいて、オーケストレータに 3つのルール を追加しただけです:
| Rule | Trigger | Action |
|---|---|---|
| 円環(循環)委任ガード |
D チェーンに重複した who
|
ループを断ち、エスカレート |
| 再試行上限 | ツール呼び出しが> 3回失敗 | T をログし、部分データで継続 |
| 終了(termination)嵐のアラート | 1分あたり同一理由の T が> 5 |
パイプラインを一時停止し、通知 |
次週の実行: 1.8時間。$3.70。 そして、APIの障害がレポートを静かに(気づかないうちに)壊してしまう前に捕まえられました。
Why This Matters (And Why Most Agent Logs Are Useless)
私が学んだのは、「アクションをログすること」と「意思決定をログすること」の間には違いがあるということです。
| Action Log | Decision Log |
|---|---|
| "Called API X" | "confidence < 0.7 なのでAnalystに委任" |
| "Task completed" | "ツールのタイムアウトにより部分データで終了" |
| "Received response" | "検証済み—ハッシュ一致、コヒーレンススコア0.82" |
アクションログは、何が起きたかを教えてくれます。意思決定ログは、なぜ起きたのかを教えてくれます。
「なぜ」がなければ、マルチエージェントシステムのデバッグはただの当て推量になります。
How to Get Started (Without Adopting a New Protocol)
スタック全体を作り直す必要はありません。まずはシンプルに始めましょう:
レベル1: 構造化された引き継ぎログ
あるエージェントが別のエージェントに仕事を渡すたびに、1行のJSONを追加します。from、to、reason、そしてペイロードのhashを含めてください。これだけで委任ループを検知できます。
レベル2: 意思決定の動詞を追加する
各ログに、それが表す意思決定の種類をタグ付けします: initiated、delegated、terminated、verified。これにより検索やグラフ化が容易になります。
レベル3: それらをつなぎ合わせる
イベントをリンクするために ref フィールドを使います。これで、個別のイベントだけでなく、意思決定チェーン全体のトレースが手に入ります。
レベル4: サイン(否認防止が必要なら)を追加する
監査証跡が重要になるものを作っているなら—コンプライアンス、会計、複数当事者のシステムなど—暗号学的な署名が欲しくなるはずです。上で使ったフォーマットは JEP(Judgment Event Protocol)と互換性があり、署名とリプレイ防止が標準で追加されます。ただし、プレーンなJSONとrefフィールドだけでも価値の80%は得られます。
I Open-Sourced the Logger
この実験で使ったロガーは、現在オープンソースになっています。200行のPythonで、どんなエージェントフレームワークでも動作し、NDJSONに書き出すので、1999年みたいに cat や grep ができます。
含まれています:
- 4つすべての意思決定動詞を備えたコアロガー
- Mermaidの可視化スクリプト(エージェントの意思決定チェーンをフローチャートとして参照できます)
- 委任ループや終了嵐を検出する分析ツール
- 完全な例とテスト
Your Turn
私のエージェントは壊れていませんでした。見えていなかった、割高な意思決定をしていただけです。
あなたのエージェントのログで見つけた、いちばん奇妙だったことは何ですか? それとも、手探りで飛んでいますか?
コメントを残してください。あなたのシステムで(見えているのか/見えていないのか)何が起きているのかぜひ聞かせてください。



