<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Prompt Engineering on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/prompt-engineering/</link>
    <description>Recent content in Prompt Engineering on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Fri, 27 Feb 2026 09:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/prompt-engineering/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>LLM運用の可観測性を実装する：OpenTelemetryでつくるPrompt/Token/Latency監視の実践</title>
      <link>https://www.ai2core.com/posts/2026-02-27-llm-observability-opentelemetry/</link>
      <pubDate>Fri, 27 Feb 2026 09:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-27-llm-observability-opentelemetry/</guid>
      <description>OpenTelemetryを使ってLLMアプリのレイテンシ、トークン、品質劣化を追跡する実装手順を具体例付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="llm運用の可観測性を実装するopentelemetryでつくるprompttokenlatency監視の実践">LLM運用の可観測性を実装する：OpenTelemetryでつくるPrompt/Token/Latency監視の実践</h1>
<p>LLMアプリは「動く」だけでは本番品質になりません。運用を始めると、次のような問題が必ず発生します。</p>
<ul>
<li>昨日まで 1.2 秒だった応答が突然 4 秒台になる</li>
<li>コストが月末に急増したが、どの機能が原因かわからない</li>
<li>回答品質が落ちたと言われるが、どのプロンプト変更が影響したか追えない</li>
<li>リトライ回数や外部API待ちの偏りが可視化されていない</li>
</ul>
<p>この課題を解く鍵が「可観測性（Observability）」です。本記事では OpenTelemetry を軸に、LLM アプリの監視をゼロから構築する実装を、実際に運用で使える粒度で説明します。</p>
<h2 id="なぜ-apm-だけでは-llm-を見切れないのか">なぜ APM だけでは LLM を見切れないのか</h2>
<p>従来の Web アプリ監視（CPU、HTTP レイテンシ、エラーレート）だけでは、LLM 特有の故障点が見えません。理由は、LLM の品質とコストが「入力テキスト」と「推論設定」に強く依存するためです。</p>
<p>少なくとも次の軸が必要です。</p>
<ol>
<li><strong>Prompt 可視化</strong>: システム/ユーザー/ツール呼び出しの構成</li>
<li><strong>Token 可視化</strong>: input/output token、モデル別単価、キャッシュヒット率</li>
<li><strong>推論経路可視化</strong>: retrieval → rerank → generation の各ステップ時間</li>
<li><strong>品質シグナル</strong>: hallucination 率、参照文書一致率、ユーザー評価</li>
</ol>
<p>つまり、HTTP 1 本のログでは不十分で、<strong>トレース単位で LLM 実行を分解</strong>する必要があります。</p>
<h2 id="アーキテクチャの全体像">アーキテクチャの全体像</h2>
<p>最初に、実装対象を次の構成とします。</p>
<ul>
<li>API: FastAPI</li>
<li>LLM: OpenAI / Azure OpenAI（抽象化）</li>
<li>RAG: pgvector + reranker</li>
<li>Observability: OpenTelemetry SDK + OTLP Exporter + Grafana Tempo/Loki/Prometheus</li>
</ul>
<p>処理フローは次の通りです。</p>
<ol>
<li>リクエスト受信時に <code>trace_id</code> を生成</li>
<li>Retrieval、Rerank、Generate をそれぞれ span 化</li>
<li>各 span に token、model、temperature、cache_hit を attribute として記録</li>
<li>失敗時は exception をイベントとして保存</li>
<li>レスポンス時にコスト推定を metrics として送信</li>
</ol>
<h2 id="ステップ1opentelemetryの初期設定">ステップ1：OpenTelemetryの初期設定</h2>
<p>まずは Python で最小セットを導入します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>uv add opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp opentelemetry-instrumentation-fastapi
</span></span></code></pre></td></tr></table>
</div>
</div><p>次に初期化コードを用意します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#75715e"># observability.py</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry <span style="color:#f92672">import</span> trace, metrics
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.trace <span style="color:#f92672">import</span> TracerProvider
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.trace.export <span style="color:#f92672">import</span> BatchSpanProcessor
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.exporter.otlp.proto.grpc.trace_exporter <span style="color:#f92672">import</span> OTLPSpanExporter
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry.sdk.resources <span style="color:#f92672">import</span> Resource
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>resource <span style="color:#f92672">=</span> Resource<span style="color:#f92672">.</span>create({
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;service.name&#34;</span>: <span style="color:#e6db74">&#34;tech-blog-autopilot-api&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;service.version&#34;</span>: <span style="color:#e6db74">&#34;1.3.0&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;deployment.environment&#34;</span>: <span style="color:#e6db74">&#34;production&#34;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>provider <span style="color:#f92672">=</span> TracerProvider(resource<span style="color:#f92672">=</span>resource)
</span></span><span style="display:flex;"><span>provider<span style="color:#f92672">.</span>add_span_processor(
</span></span><span style="display:flex;"><span>    BatchSpanProcessor(OTLPSpanExporter(endpoint<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;http://otel-collector:4317&#34;</span>, insecure<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>trace<span style="color:#f92672">.</span>set_tracer_provider(provider)
</span></span><span style="display:flex;"><span>tracer <span style="color:#f92672">=</span> trace<span style="color:#f92672">.</span>get_tracer(<span style="color:#e6db74">&#34;llm-pipeline&#34;</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>ここで重要なのは、<strong>service.name を固定すること</strong>です。デプロイごとに揺れるとダッシュボードが分断され、比較分析ができません。</p>
<h2 id="ステップ2llm処理を-span-で分割する">ステップ2：LLM処理を span で分割する</h2>
<p>実運用では「遅い」の原因が retrieval なのか generation なのかで対応が変わります。そこで、処理を細かく span 化します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> opentelemetry <span style="color:#f92672">import</span> trace
</span></span><span style="display:flex;"><span>tracer <span style="color:#f92672">=</span> trace<span style="color:#f92672">.</span>get_tracer(<span style="color:#e6db74">&#34;llm-pipeline&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">generate_answer</span>(query: str, user_id: str):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">with</span> tracer<span style="color:#f92672">.</span>start_as_current_span(<span style="color:#e6db74">&#34;rag.pipeline&#34;</span>) <span style="color:#66d9ef">as</span> root:
</span></span><span style="display:flex;"><span>        root<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;user.id&#34;</span>, user_id)
</span></span><span style="display:flex;"><span>        root<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;feature&#34;</span>, <span style="color:#e6db74">&#34;support-chat&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> tracer<span style="color:#f92672">.</span>start_as_current_span(<span style="color:#e6db74">&#34;rag.retrieve&#34;</span>) <span style="color:#66d9ef">as</span> span_retrieve:
</span></span><span style="display:flex;"><span>            docs <span style="color:#f92672">=</span> retrieve_docs(query)
</span></span><span style="display:flex;"><span>            span_retrieve<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;retrieved.count&#34;</span>, len(docs))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> tracer<span style="color:#f92672">.</span>start_as_current_span(<span style="color:#e6db74">&#34;rag.rerank&#34;</span>) <span style="color:#66d9ef">as</span> span_rerank:
</span></span><span style="display:flex;"><span>            ranked <span style="color:#f92672">=</span> rerank_docs(query, docs)
</span></span><span style="display:flex;"><span>            span_rerank<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;rerank.top_k&#34;</span>, <span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> tracer<span style="color:#f92672">.</span>start_as_current_span(<span style="color:#e6db74">&#34;llm.generate&#34;</span>) <span style="color:#66d9ef">as</span> span_gen:
</span></span><span style="display:flex;"><span>            response <span style="color:#f92672">=</span> call_llm(query, ranked)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.model&#34;</span>, response<span style="color:#f92672">.</span>model)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.input_tokens&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>input_tokens)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.output_tokens&#34;</span>, response<span style="color:#f92672">.</span>usage<span style="color:#f92672">.</span>output_tokens)
</span></span><span style="display:flex;"><span>            span_gen<span style="color:#f92672">.</span>set_attribute(<span style="color:#e6db74">&#34;llm.temperature&#34;</span>, <span style="color:#ae81ff">0.2</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> response<span style="color:#f92672">.</span>text
</span></span></code></pre></td></tr></table>
</div>
</div><p>この分割で、「retrieval が中央値 70ms → 280ms に悪化」「特定モデルだけ output token が急増」など、運用判断に直結する情報が取得できます。</p>
<h2 id="ステップ3コストをメトリクス化する">ステップ3：コストをメトリクス化する</h2>
<p>運用現場で最も効くのは、<strong>推定コストをリアルタイムに可視化</strong>することです。モデル単価表をコードに持ち、1リクエストごとに計算して metrics に送ります。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">8
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>MODEL_PRICE <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;gpt-4.1-mini&#34;</span>: {<span style="color:#e6db74">&#34;in&#34;</span>: <span style="color:#ae81ff">0.0000003</span>, <span style="color:#e6db74">&#34;out&#34;</span>: <span style="color:#ae81ff">0.0000012</span>},
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;gpt-4.1&#34;</span>: {<span style="color:#e6db74">&#34;in&#34;</span>: <span style="color:#ae81ff">0.000003</span>, <span style="color:#e6db74">&#34;out&#34;</span>: <span style="color:#ae81ff">0.000012</span>},
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">estimate_cost</span>(model: str, in_tokens: int, out_tokens: int) <span style="color:#f92672">-&gt;</span> float:
</span></span><span style="display:flex;"><span>    p <span style="color:#f92672">=</span> MODEL_PRICE[model]
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> in_tokens <span style="color:#f92672">*</span> p[<span style="color:#e6db74">&#34;in&#34;</span>] <span style="color:#f92672">+</span> out_tokens <span style="color:#f92672">*</span> p[<span style="color:#e6db74">&#34;out&#34;</span>]
</span></span></code></pre></td></tr></table>
</div>
</div><p>推奨は次の3指標です。</p>
<ul>
<li><code>llm_cost_usd_total</code>（counter）</li>
<li><code>llm_tokens_input_total</code> / <code>llm_tokens_output_total</code>（counter）</li>
<li><code>llm_latency_ms</code>（histogram）</li>
</ul>
<p>これを feature、tenant、model のラベルで集計すると、予算統制が一気に楽になります。</p>
<h2 id="ステップ4品質低下を検知する仕組みを入れる">ステップ4：品質低下を検知する仕組みを入れる</h2>
<p>レイテンシとコストだけでは不十分です。品質監視を最低限でも導入します。</p>
<h3 id="4-1-自動評価ジョブ">4-1. 自動評価ジョブ</h3>
<p>夜間バッチで固定データセット（100問程度）を流し、次を記録します。</p>
<ul>
<li>正答率（正解文との semantic similarity）</li>
<li>出典一致率（回答が引用した文書IDの妥当性）</li>
<li>禁止事項違反率（PII、コンプラNG）</li>
</ul>
<h3 id="4-2-本番フィードバック">4-2. 本番フィードバック</h3>
<p>UI で 👍 / 👎 を取り、trace_id と紐づけます。こうすると「悪評の大半が temperature=0.9 の実験フラグ経由」など、根因分析が可能です。</p>
<h2 id="ステップ5運用で効くダッシュボードを作る">ステップ5：運用で効くダッシュボードを作る</h2>
<p>実際に使われるダッシュボードは、項目を欲張らない方が強いです。最初は次の 6 つに絞ってください。</p>
<ol>
<li>P50/P95 レイテンシ（全体 + モデル別）</li>
<li>リクエスト数とエラー率（HTTP + LLM例外）</li>
<li>日次コスト（全体 + feature別）</li>
<li>input/output token 推移</li>
<li>retrieval 件数と空振り率</li>
<li>ユーザー評価（👍率）</li>
</ol>
<p>特に P95 とコストは同一画面に置くのがポイントです。高速化で品質が落ちた、または品質改善でコストが跳ねた、というトレードオフが即時に見えます。</p>
<h2 id="よくある失敗と回避策">よくある失敗と回避策</h2>
<h3 id="失敗1prompt全文を生で保存して個人情報を漏らす">失敗1：Prompt全文を生で保存して個人情報を漏らす</h3>
<p>対策は、PII マスキングを export 前に必ず実行することです。メール、電話番号、住所は正規表現だけでなく、NER ベースで二重防御すると安全です。</p>
<h3 id="失敗2span属性の命名がバラバラ">失敗2：span属性の命名がバラバラ</h3>
<p><code>llm.input_tokens</code> と <code>input_token_count</code> が混在すると集計不能になります。命名規約をリポジトリに固定し、CI で lint してください。</p>
<h3 id="失敗3高カーディナリティ地獄">失敗3：高カーディナリティ地獄</h3>
<p><code>user_id</code> をそのままメトリクスラベルに入れると TSDB が破綻します。ユーザー軸は trace/log に置き、metrics は tenant や plan 程度に抑えます。</p>
<h2 id="導入ロードマップ2週間">導入ロードマップ（2週間）</h2>
<ul>
<li><strong>Day 1-2</strong>: FastAPI + LLM呼び出しに trace 埋め込み</li>
<li><strong>Day 3-4</strong>: token/cost メトリクス送信</li>
<li><strong>Day 5-6</strong>: Grafana ダッシュボード構築</li>
<li><strong>Day 7-9</strong>: しきい値アラート設計（P95、error、cost）</li>
<li><strong>Day 10-12</strong>: 品質評価バッチ導入</li>
<li><strong>Day 13-14</strong>: インシデント演習（意図的劣化を検知できるか）</li>
</ul>
<p>2週間で「見える化」は十分達成できます。完璧を目指すより、まず計測可能にすることが重要です。</p>
<h2 id="まとめ">まとめ</h2>
<p>LLM運用で本当に困るのは、失敗そのものではなく「失敗の理由が見えない」状態です。OpenTelemetry を使って retrieval、generation、token、cost、品質を一貫して観測できるようにすると、改善サイクルが回り始めます。</p>
<p>可観測性は守りではなく、開発速度を上げるための攻めの基盤です。まずは span を3つに分けるところから始めてください。それだけで、LLM運用の景色が大きく変わります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>LLM</category>
      <category>OpenTelemetry</category>
      <category>Observability</category>
      <category>Prompt Engineering</category>
    </item>
  </channel>
</rss>
