trace_id を最初に通しておくと、後で必ず楽になる
CMスポット PoC をやっていて、自分の中で一番「最初から入れてよかった」と思っているのが trace_id の伝播。Main Agent + 6 Sub Agent の構成で、Frontend → Backend → 各 Agent → LLM 呼び出しまで一気通貫で trace_id を通すようにした。これだけで、後の調査速度が桁違いに変わる。
最初に入れないと、絶対に後付けになる
PoC は仕様が動く。Agent が増える。LLM 呼び出しが入れ子になる。動いてから「あれ、どのリクエストが詰まったんだ?」を調べる段階で、trace_id が無いと grep ですら追えない。
trace_id は 後付けがダルい。途中の関数全部に伝播の引数を足し直すことになる。だから「最初に通しておく」だけが現実解。
通す場所
最低限ここを貫通させれば十分。
- Frontend → リクエストに
traceparentヘッダを付ける - Backend (FastAPI) → 受け取った
traceparentをcontextvarsに保持 - Agent パイプライン → Agent 間 I/O のスキーマに
trace_idを必須で入れる - LLM 呼び出し → プロンプト送信時にメタとして添える
- 構造化ログ → 全行に
trace_idを出す
W3C の traceparent フォーマットに乗せておけば、後で OpenTelemetry SDK を入れることになっても破綻しない。PoC のうちは ID を伝播させるだけで十分。
最小実装(FastAPI)
middleware で受け口を作って contextvars に持つ。Agent 間で受け渡しするときは、ログとプロンプトに混ぜるだけ。
import contextvars
from uuid import uuid4
from fastapi import FastAPI, Request
trace_id_ctx = contextvars.ContextVar('trace_id', default='')
app = FastAPI()
@app.middleware('http')
async def trace_middleware(request: Request, call_next):
incoming = request.headers.get('traceparent')
trace_id = (
incoming.split('-')[1] if incoming else uuid4().hex
)
trace_id_ctx.set(trace_id)
return await call_next(request)
@app.get('/agents/run')
async def run():
trace_id = trace_id_ctx.get()
logger.info({
'traceId': trace_id,
'evt': 'agents.start',
})
# ... Main Agent → 6 Sub Agent
これだけで、後の grep が一発で済むようになる。
$ cat app.log | jq 'select(.traceId == "9f2a1c…")'
PoC のうちは Sentry も OTel Collector もいらない。 ID と JSON 構造さえ揃えておけば、jq で十分追える。
「後で楽することに先に投資する」種類のコードがあって、trace_id はその代表例。CMスポット PoC で一番リターンが大きかった設計判断だった気がする。