「配送先を会社の住所へ変更してください」
顧客には未発送の注文が二件ありました。問い合わせAIは「変更しました」と返したものの、翌日確認すると、対象ではない注文の住所だけが更新されていました。
ユーザー入力と最終回答しか残っていなければ、原因候補は広がるばかりです。変更対象の解釈を誤ったのか。古い配送ルールを参照したのか。モデルの判断は妥当でもTool Callの引数を組み立てる処理でIDが入れ替わったのか。APIは失敗を返したのに、AIが成功として扱ったのか。
本番の問い合わせAIで必要なObservabilityは、会話を保存することではありません。「どの版の仕組みが、どの根拠で、どの操作を選び、業務側で何が起きたか」を一件単位で再構成できることです。

問い合わせ一件を、一つのTraceとして扱う
最初に決めたいのは、会話IDとは別のtrace_idです。チャットが複数ターンにまたがり、検索や業務APIが非同期で動き、人へ調査を依頼しても、同じ案件の出来事をtrace_idで関連付けます。各処理にはevent_idとparent_event_idを持たせると、並列検索や再試行を含む順序も復元できます。
イベントの粒度は「LLMを一回呼んだ」だけでは足りません。少なくともIntent、Retrieved Context、Model Decision、Tool Call、Tool Result、Final Answerを別イベントにします。人が案件を引き取った場合は、その理由と引き渡した時点も残します。そうすれば、検索は正しかったのに判断で対象注文を取り違えた、といった切り分けが可能になります。
最小構成のイベントSchema
実装方式に依存しない最小例を、次のようなイベント形式にしています。値そのものを無制限に保存するのではなく、調査に必要な参照と要約を残す前提です。
{
"trace_id": "inq_01J...",
"event_id": "evt_006",
"parent_event_id": "evt_005",
"stage": "tool_result",
"observed_at": "2026-09-01T09:31:42.184Z",
"intent": {"name": "change_shipping_address", "entity_count": 2},
"knowledge": {"document_id": "shipping_policy", "version": "v12"},
"decision": {"action": "update_order", "reason_code": "USER_CONFIRMED"},
"tool": {
"name": "update_shipping_address",
"argument_fingerprint": "sha256:...",
"idempotency_key": "inq_01J...:address:1"
},
"result": {"status": "rejected", "code": "ORDER_ID_MISMATCH"},
"model_version": "model_2026_08",
"prompt_version": "support_agent_v27",
"latency_ms": 418
}
puts 'code block.'
trace_idと親子関係は、原因までの経路を組み立てるために使います。model_version、prompt_version、knowledge.versionは、同じ入力を再現した際に結果が変わる理由を探す手がかりです。ナレッジ本文を丸ごとコピーするより、文書ID、版、取得順位、利用した範囲を結び付けた方が、更新後も当時の状態を追いやすくなります。
decisionには、モデルの非公開な思考過程を保存するのではなく、選択した行動、対象エンティティ、確認済み条件、ポリシー上のreason_codeを構造化して残します。必要なのは思考の全文ではなく、「なぜこの操作が許可された扱いになったか」をシステムの言葉で説明できる材料です。
Tool Callではツール名だけでなく、対象ID、重要な引数のマスク済み要約、引数全体のfingerprint、idempotency_keyを残します。Tool ResultもHTTP 200だけで成功にせず、業務上の結果コード、更新対象、更新前後のバージョンを記録します。今回の障害なら、AIの回答より先にORDER_ID_MISMATCHへ到達できます。
ログ量より、失敗を分類できるか
大量のプロンプトとレスポンスを保存しても、調査の入口がなければ運用では使えません。intent_conflict、stale_context、policy_denied、tool_timeout、business_rejected、handoff_requiredのように、発生箇所と性質をreason_codeで揃えると、同じ障害が何件起きているかを集計し、そのまま再現テストへ戻せます。
一方で、会話には氏名、住所、注文情報が含まれます。全文ログ、デバッグ用Trace、監査証跡は保存目的、閲覧権限、保持期間を分けるべきです。通常の調査はマスク済みイベントで行い、原文を開く操作自体も監査対象にする。Observabilityを理由に個人情報を増やし過ぎる設計は、本末転倒です。
本番ログを、次の改善へ戻す
障害対応が終わったら、該当Traceを匿名化し、期待するIntent、許可する操作、想定する結果コードを付けて回帰テストへ追加します。ログは事後説明のためだけでなく、同じ失敗を次のリリースで止めるための入力です。原因がナレッジ、モデル、オーケストレーション、業務APIのどこにあったかで、修正担当も変わります。
Udeskに関わるAIカスタマーサービスの実運用でも、回答だけでなく、検索、判断、業務処理、人への引き渡しを一つのTraceとして追えるかは継続的な論点です。皆さんはAIエージェントのログをどこまで残しますか。障害再現に必要な情報と、保存すべきでない情報の境界をどう決めていますか。