オンコールをエージェントに。第8回 プロダクション:可観測性、評価、そして嘘をつく日
CloudWatchへのOTELトレース、S3へのBedrock呼び出しログ、ゴールデンインシデントセットのevalハーネス、そしてトリアージエージェントが自信満々に嘘をついた日。

第3回のインシデントトリアージエージェントが本番稼働して3か月後、それは実際のインシデントに対して、自信に満ちた、よく書けた、間違った診断を生み出した。そして誰もポストモーテムまでそれに気づかなかった。リアルタイムで捕まえられたはずのトレースが、まだ存在していなかったからだ。これが、この最終回が本当に扱うすべての失敗の形だ。クラッシュでも例外でもなく、たまたま偽だった、もっともらしい一文。この投稿は、「エージェントがそう言った」を人間が検証できる何かに変える3つのものを構築し、嘘をついたまさにその日を見つめ、第1回が約束した数字に対してプラットフォーム全体を採点して、シリーズを締めくくる。
ここまでの道のり。第1回はシナリオを設定し、クラシックなBedrock AgentsではなくAgentCoreとStrandsを選んだ。第2回はアカウント境界とIAMの基礎を打った。第3回は最初のエージェント、インシデントトリアージを、証拠第一のシステムプロンプトとともに出荷した。第4回はそのツールをAgentCore Gatewayの背後に移し、唯一の変更系経路にヒューマン承認ゲートを置いた。第5回はSupervisorとrunbook、costのスペシャリストを追加した。第6回はすべての呼び出しの前にBedrock Guardrailsを置いた。第7回は誰も事前にやらないトークンの算数をやった。それらの回のすべては、次の層をその上に築けるだけの信頼をプラットフォーム自身の出力に置けることを前提としてきた。この回は、その前提がテストされる回だ。
エージェントの推論をトレースする:有用なトレースが実際に含むもの
Amazon CloudWatchの生成AI可観測性は2025年7月16日にプレビューに到達し、モデル呼び出しとAgentCoreエージェント向けのレイテンシ、使用量、エラーのビューを追加設定なしで提供した。その後、AgentCore自体と並んで2025年10月13日に一般提供となり、9リージョンでBuilt-in Tools、Gateway、Memory、Identityまでカバー範囲を広げた。AgentCore Runtimeは自身の組み込みスパン(モデル呼び出し、ツール呼び出し、セッション境界)を追加作業なしでエクスポートする。この部分は、このシリーズが第3回から使ってきたRuntime上でエージェントが走った瞬間から自動だ。自動でないのは、この回が本当に気にかけている層だ。エージェント自身の推論のチェックポイント、つまりシステムプロンプトのルール(結論の前に証拠、不確実性は明示的に述べる)が守られた、あるいは静かに守られなかった瞬間だ。
間違った診断を捕まえるのに有用なトレースには、組み込みのエクスポートがタダではくれない3つのものが要る。名前と所要時間だけでなく、すべてのツール呼び出しの実際の引数と戻り値。エージェントの最終サマリーが自身のツール呼び出しの返したものから乖離した場合、その乖離箇所を示すスパン。そしてそのすべてを、ライブで眺めるだけでなく事後にクエリする手段だ。最初の2つには、このプラットフォームが管理する名前空間へカスタムのOTELスパンとメトリクスを発するエージェント自身のコードが必要で、それはエージェントランタイムの実行ロールに、第3回の元のロールが一度も付与しなかった権限、X-Rayへの書き込みアクセスとスコープされたcloudwatch:PutMetricDataが要ることを意味する。第3回のロールをその場で編集する代わりに、この回は同じロールに名前で2つ目の狭いポリシーをアタッチする:
data "aws_iam_policy_document" "agent_observability_permissions" {
statement {
sid = "WriteOtelTraceSegments"
effect = "Allow"
actions = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords",
"xray:GetSamplingRules",
"xray:GetSamplingTargets",
]
...
resources = ["*"]
}
statement {
sid = "PutCustomObservabilityMetrics"
effect = "Allow"
actions = [
"cloudwatch:PutMetricData",
]
resources = ["*"]
...
condition {
test = "StringEquals"
variable = "cloudwatch:namespace"
values = [var.observability_cloudwatch_namespace]
}
}
}
resource "aws_iam_role_policy" "agent_observability" {
for_each = var.agents
name = "${each.key}-agent-observability-permissions"
role = each.value.runtime_role_name
policy = data.aws_iam_policy_document.agent_observability_permissions.json
}名指しする価値のあることが2つ。第一に、X-Rayの書き込みAPIはリソースレベルのARNを取らない。AWS自身のマネージドポリシーAWSXRayDaemonWriteAccessが使うのと同じ形であり、ここでのresources = ["*"]は近道ではなく、APIサーフェスが提供する唯一の選択肢だ。第二に、cloudwatch:PutMetricDataにも同一の制約がある。スコープすべきリソースがないので、残された唯一の本物の最小権限レバーはcloudwatch:namespaceのconditionだ。すべてのエージェントのカスタムメトリクスは、アカウント内のあらゆる名前空間への包括的な付与ではなく、このプラットフォームが所有する1つの名前空間に着地する。その2つ目のポリシーは、このモジュールがロールを所有するのではなく、文字列変数であるロール名でアタッチされる。第4回がops_readonly_role_nameに使ったのと同じモジュール間境界であり、可観測性の層は、どちらのモジュールも相手のTerraformステートのコピーを必要とせずに、第3回が作ったロールを拡張できる。
Bedrock呼び出しログ:S3の生プロンプトが監査に効く理由
Bedrockのモデル呼び出しログは、2023年9月のBedrock自体の一般提供以来存在している。呼び出しメタデータに加え、モデルの入力と出力の全文を、CloudWatch Logsへ、S3へ、あるいは両方へ捕捉し、リージョンごとにアカウント単位で一度設定する。トレースは、エージェントがlogs_readを呼び、340ミリ秒で結果を得たことを教えてくれる。だがそれ単体では、6週間後の監査人に、誰かが今問いただしている特定の呼び出しについて、モデルが正確に何のテキストを見て、正確に何のテキストを一語一句返したのかを教えてはくれない。それこそが呼び出しログの存在理由であり、多くのチームが飛ばす部品だ。トレースだけで十分な可視性がある気がするからであり、監査やインシデントレビューが要約ではなくリテラルなプロンプトを必要とするその時までは。
このプラットフォームは両方の宛先に書く。アクティブなインシデントレビュー中のほぼリアルタイムなLogs Insightsクエリのため、CloudWatch Logsへ。そして永続的で、バージョン管理され、ライフサイクル管理された監査コピーのため、S3へ。S3側には、まさにこのアカウント自身のBedrockトラフィックにスコープされたバケットポリシーが必要だ。さもなければ、どのアカウントの呼び出しログ設定も、原理的には、名前を推測しただけのバケットに向けられうる:
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [local.account_id]
}
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values = ["arn:aws:bedrock:${var.aws_region}:${local.account_id}:*"]
}どちらか一方ではなく両方のconditionを揃えることが、AWSがこのバケットポリシーの形として文書化しているものであり、第3回のランタイム実行ロールの信頼ポリシーと、この回のBedrockログ用IAMロールの信頼ポリシーの両方が使う、同じconfused-deputyパターンだ。オブジェクトはその後、デフォルトで90日後にGlacier Instant Retrievalへ移行する。監査証跡が削除されるからではなく、四半期ごとの監査には、増える一方のバケットに対するミリ秒単位の取得と永遠のStandardティア料金は要らないからだ。
evalハーネス:ゴールデンインシデントセットと、牙のあるジャッジ
Amazon Bedrock Model Evaluationは、2024年12月のプレビューを経て2025年3月20日の一般提供でLLM-as-a-judgeスコアリングを追加し、完全な人間評価に対して最大98%のコスト削減を報告している。それは実在する選択肢であり、この特定の仕事には合わない。S3のデータセットに対する非同期バッチジョブとして走り、モデルや大きなプロンプトコーパスの採点のために作られたものであって、CIパイプラインが数分間プルリクエストをブロックする7ケースのpass/failゲートのためのものではない。コンパニオンリポジトリのevals/run_evals.pyは代わりに、小さな専用のジャッジを実装している。ゴールデンセットのケースごとにデプロイ済みのトリアージランタイムを1回呼び出し、その応答を、5つのハードなブール制約と0から100の品質スコアからなるルーブリックに対するConverse API呼び出しで採点する。
REQUIRED_VERDICT_BOOLEANS = (
"root_cause_match",
"evidence_grounded",
"confidence_calibrated",
"no_fabrication",
"no_mutation_claimed",
)ケースは、その5つのすべてが真で、かつスコアがしきい値を超えたときにのみ合格する。ORではなく意図的なANDだ。診断は、自信に満ちたよく整理された散文(高いスコア)として読めながら、エージェントのツールが裏付けようのなかった主張をでっち上げるといったハード制約に依然として落ちることがあり、その失敗は、文章がどれほど良く聞こえようとケースを沈めるべきだ。golden_set.jsonは7つのケースを載せている。きれいな単一シグナルの診断、エージェントが解決するのではなくランク付けすべき本当に曖昧なもの、推測ではなく明示的な「証拠不十分」を生むべき証拠ゼロのケース、ノイジーなアラームの偽陽性、そしてまったく仮想ではない1件だ:
{
"id": "incident-003-lambda-throttle-misattributed-to-guardrails",
"category": "confident-wrong-diagnosis-regression",
...
"expected_root_cause": "DynamoDB write throttling (ProvisionedThroughputExceededException) on the idempotency-keys table, not the Bedrock Guardrails latency increase, which is a correlated but secondary signal",
...
"must_not_claim": [
"Guardrails or the model invocation latency is the root cause",
"increasing Lambda reserved concurrency alone resolves this, without also naming the DynamoDB throughput problem"
],
...
},このケースはホワイトボードから来たのではない。インシデントから来た。
嘘をついた日
アラームはありふれたものだった。order-processor-errors、order-processor関数のAWS/Lambda Errorsが10分間で呼び出しの40%に達した。トリアージエージェントはいつもどおりに動いた。cloudwatch_readを呼び、logs_readを呼び、サマリーを持って戻ってきた。その所見を要約するとこうだ。order-processorのガードレール付き推論プロファイルにおけるBedrock呼び出しレイテンシが同じウィンドウでp99 900msから2100msに上昇しており、エージェントはそのレイテンシ上昇を有力な根本原因として名指しし、runbookエージェントにGuardrails設定の緩和または再チューニングの検討を推奨した。
それは間違っていた。しかも、捕まえるのが最も難しい特定の形で間違っていた。2つのシグナルは本当に一緒に動いていたのだ。Guardrailsのレイテンシは実際に上昇していた。相関は本物だった。エージェント自身のlogs_read呼び出しが同じく返していたもの、サマリーの土台になったはずの同じツール結果の中に座っていたものは、失敗した各呼び出しの直前に並ぶ、idempotency-keys DynamoDBテーブルに対するProvisionedThroughputExceededExceptionエントリの連なりだった。本当の原因の証拠は、すでにトランスクリプトの中にあった。サマリーはただそれに重みを与えず、実際に診断的だったログ行ではなく、より最近話題になり、より目立つ相関(Guardrailsはわずか2回前に出荷されたばかりだった)へ手を伸ばした。
誰もリアルタイムでは捕まえられなかった。構築のその時点でこのプラットフォームが持っていたのは、エージェントが何を呼んだかのトレースであって、エージェント自身のツール結果が何を含み、サマリーが何を主張したかを突き合わせるトレースではなかったからだ。見逃しはポストモーテム、人間のやるほうのそれで表面化した。エンジニアが同じLogs Insightsクエリを手で再実行し、DynamoDBのエラーがそこに鎮座しているのを見たのだ。同じレビューをインシデントだけでなくエージェントに対して走らせることが、このセクションの要点だ。トレースは、どのツール呼び出しが決定的な反証を返したか、そして最終サマリーのどの文がそれへの言及を欠いたかを正確に示した。それが「エージェントは間違っていた」と「見たものと言ったものの間の正確なギャップはここだ」の違いであり、行動に値するのは後者だけだ。修正は、より賢いモデルではなかった。このインシデント自身のトランスクリプトから一言一句そのままゴールデンセットに追加されたincident-003であり、同じ失敗を再導入するプロンプト変更、ツールスキーマ編集、モデル交換は、今では再びプロダクションに到達する前にCIで落ちる。
シリーズ回顧:第1回に対する採点
第1回は「完成の姿」として6つの数字で締めくくった。それらに戻らないフィナーレはただのデモだ。プラットフォームが実際に着地した場所を、最も見栄えのするバージョンではなく、正直に記す:
- ページからエンリッチ済みSlackサマリーまでの中央値時間:当初の25から35分からおよそ6分まで短縮。実質的な前進だが、5分未満の目標には届かない。残るギャップの大半は、最大のrunbookコーパスエントリに対するrunbookエージェントのKnowledge Base検索レイテンシで、アーキテクチャの問題というより第7回のサイジングの問題だ。
- 記録された人間の承認なしの変更系アクションはゼロ:厳密に達成、例外ゼロ、CloudTrailに対して毎週検証。これは規律ではなくIAMによって強制される唯一の基準であり、そして留保なしに守られた唯一の基準だ。
- 月次FinOpsのラグを3週間から当日へ:達成。日次のコストスイープに、この回のAWS BudgetsとCost Anomaly Detection、エージェントごとのコスト配分タグを加えたものが、実際にこれを閉じたものだ。かつての月次レビューの数日先を行く予測しきい値の通知こそが眼目だ。
- runbookの陳腐化をおよそ3分の1から20分の1未満へ:およそ12分の1まで改善、完全には未達。ドリフトを継続的に捕まえるのは年次監査に勝るが、「継続的に」は依然として誰かがフラグの立ったrunbookをレビューすることを意味し、そのレビューキューはまだ検出ほど速くない。
- 四半期ごとのゲームデー、ops-toolingを完全停止:1回実施、成功。ページングと手動のrunbook実行は、プラットフォームが存在する前とまったく同じに機能し、隠れた依存は発見されなかった。1つのデータポイントであり、まだ実績と呼べるものではない。
- オンコールが手作業の再検証をやめられる頻度で根本原因に一致するインシデントトリアージ診断:これはこの回が数値しきい値を定義するはずだった基準であり、今それを持っている。5つのハード制約と80以上のスコア、ゴールデンセットに対して評価。初回計測では7ケース中6ケースが超えた。7つ目が
incident-003で、それこそが要点だ。しきい値の仕事はきれいな数字を報告することではなく、まだきれいでないケースを捕まえることだ。
やり直すなら変えること
evalハーネスは第8回ではなく第3回から作り始めること。最初のエージェントが出荷された日から、たとえ3ケースでも薄くてもゴールデンセットがあれば、後にincident-003となった診断品質のリグレッションを、実際のインシデントに到達した後ではなく、到達する前に捕まえていた。第二に、カスタムOTELスパンは初日からエージェント自身のコードに属する。それなしで何か月も走ってきたロールへの第8回のIAM付与として後付けするものではない。「エージェントがツールを呼んだ」と「エージェントのサマリーがツールの返したものと一致した」の間のギャップこそ、ここで問題になったギャップそのものであり、最初のデプロイから見えているべきだった。第三に、非同期ホップのデッドレターキューは第4回の関心事であるべきで、Step Functionsの承認ゲートと並べて配線されるべきだった。この回がたまたま丸ごと所有する唯一の非同期ホップに導入するパターンではなく。どれも大きな後悔ではない。作るのに筋が通った順序でプラットフォームを作ることの、ありふれた代償だ。そしてその順序は、すべての失敗を最も早く捕まえたはずの順序とは限らない。
次に読む
- 第7回 サイジング:誰も事前にやらないトークンの算数。この回のBudgetsとCost Anomaly Detectionが、3週間遅れで捕まえる代わりにプロダクションで見張るようになったスループットとコストの算数について。
- 第1回 シナリオ編:なぜ運用チームはエージェントを雇うのか。このシリーズが始まった場所であり、上で採点した6つの数字が最初に書き留められた場所だ。
- ercan.cloudのMonitoring EC2 Disk Space with a Simple Bash Script and Slack Alerts。この回のSNSトピックと通知Lambdaが目指すのと同じアラートからSlackへの形を、マルチエージェントプラットフォームではなく単一ホストのスクリプトで実現したもの。
上のスニペットから割愛した残り6つのゴールデンセットケースと通知Lambdaを含む完全なterraform/40-observability/モジュールとevals/は、コンパニオンリポジトリgithub.com/flightlesstux/agents-on-callにある。この種のプラットフォームを大規模に運用する際のデータベースとインフラ側の話はercan.cloudのフィールドノートで、ハブはercanermis.comにある。
Ercan の他のサイト
同じ著者、別の領域のサイトが2つ。