3回目の記事、というより、いちばん地味な話
1回目で RAG とオントロジー、2回目でセキュリティを書いた。今回で最終回だ。テーマは「作って終わり」にしない仕組み、つまり運用の話である。正直に言うと、シリーズの中でいちばん地味だ。派手なデモも、目を引くアーキ図もない。ただ、契約を1年、2年と続けてもらえるかどうかは、ほぼここで決まる。
先週、あるクライアントの情シス責任者と話していて、こんな一言があった。「動いたのは3ヶ月前だが、最近みんな使わなくなった」。理由を掘ると、社内規程が改訂されたのに RAG のインデックスが古いままで、AI が旧規程を根拠に回答していた、というものだった。技術的には難しい問題ではない。しかし「誰が、いつ、何をトリガーに、再取り込みを走らせるのか」が決まっていなかった。それだけで、社内 LLM は静かに死んでいく。

なぜ「作って終わり」で崩れるのか
崩壊のパターンは、経験上ほぼ3つに分類できる。1つ目は回帰。プロンプトやモデル、reranker のパラメータを少しいじった瞬間、以前は正しく答えられていた質問が壊れる。人間はすべての質問を覚えていないので、気づいた頃には「なんとなく最近ダメになった」という感想だけが残る。
2つ目はデータドリフト。社内文書は生き物だ。規程改訂、組織変更、商品ラインナップの差し替え、業務フローの見直し。3ヶ月放置すればソースの3割が現実と乖離する。3つ目は業務変更。ユーザーが最初に想定していた質問とは違う使い方を始める。営業向けに作ったはずが、現場作業者が「この設備の点検頻度は?」と聞き始める。retrieval が想定外の chunk を引き、精度が下がる。
いずれも、コードだけを見ていても検知できない。運用の設計、つまり「何を継続的に測るか」を決めておかないと、崩れているのに気づけない。
評価パイプライン ― まず「壊れたか」を測れるように
継続運用の出発点は、リリース後も同じ物差しで品質を測り続けられる状態を作ることだ。筆者は Ragas と DeepEval を組み合わせて使うことが多い。Ragas は RAG 特有の指標 (context precision / recall、faithfulness、answer relevancy) を数値化するのに向いている。DeepEval は LLM の単体テストとして CI に組み込みやすい。
肝はデータセットだ。本番のトレース (プロダクションでの実問い合わせと引かれた context、返答) を貯めておき、そこから golden dataset を作る。経験則として、最終的に評価に使う件数の 5 倍を raw で抜き、業務カテゴリごとにダウンサンプルして偏りを均す。300件で評価したいなら、raw で 1500件を集め、そこからバランスをとる。
from ragas import evaluate
from ragas.metrics import (
context_precision,
context_recall,
faithfulness,
answer_relevancy,
)
from datasets import Dataset
golden_records: List[Dict]
question / ground_truth / answer / contexts (List[str])
dataset = Dataset.from_list(golden_records)
result = evaluate(
dataset,
metrics=[
context_precision,
context_recall,
faithfulness,
answer_relevancy,
],
)
しきい値割れがあれば非ゼロで終了 (CI から呼ぶ想定)
thresholds = {
"context_precision": 0.70,
"context_recall": 0.65,
"faithfulness": 0.80,
"answer_relevancy": 0.75,
}
failed = [k for k, v in thresholds.items() if result[k] < v]
if failed:
raise SystemExit(f"quality gate failed: {failed}")
ここで一つ注意しておきたいのは、評価対象は embedding 単体ではなくパイプライン全体 (chunking → retriever → reranker → LLM → prompt) だという点だ。embedding モデルだけを新しいものに差し替えて「スコアが下がった」と嘆く事故を、筆者は何度か見てきた。chunking を変えれば retriever の挙動が変わるし、reranker を挟めば context precision は跳ねる。切り分けたければ、変更する軸を1つに絞って比較するしかない。
社内データ更新への追従
再取り込みの設計は、地味だが効く。基本方針はシンプルで、ソースの URL (もしくは path) と hash を持ち、差分だけを再インデックスする。全件走査は高コストなので日次バッチ、hash 差分の反映は 15 分おき、というように分けている現場が多い。
import hashlib
from typing import Iterable
def sha256_of(text: str) -> str:
return hashlib.sha256(text.encode("utf-8")).hexdigest()
def sync_documents(sources: Iterable[dict], store) -> dict:
added, updated, skipped = 0, 0, 0
for src in sources:
digest = sha256_of(src["text"])
existing = store.get(src["url"])
if existing is None:
store.upsert(src["url"], digest, src["text"])
added += 1
elif existing["hash"] != digest:
store.upsert(src["url"], digest, src["text"])
updated += 1
else:
skipped += 1
return {"added": added, "updated": updated, "skipped": skipped}
ここで一つ、はまりやすい罠がある。embedding モデルを更新するときは、必ず全文書を再インデックスする。ベクトル空間が変わるので、旧モデルで作ったベクトルと新モデルのクエリベクトルを混ぜてはいけない。「差分だけでいいだろう」と思って混在させると、retriever が近い順を全く違うものと判定する。前回のセキュリティ側の話で言えば、権限分離されたインデックスがテナントごとに増えるほど、この一括再インデックスは重い作業になる。日次のオフピーク時間帯にジョブを分割して回すのが現実解だ。
検索基盤自体 (Vertex AI Search、Azure AI Search、OpenSearch など) はマネージドに寄せ、hash 差分の管理や再取り込みのオーケストレーションだけを自前で持つ、という切り分けが実装量と障害面積のバランスがよかった。
CI と評価ゲート ― PR のタイミングで崩れを止める
プロンプト、chunking パラメータ、retriever の設定、reranker の閾値。どれを触っても品質は動く。だからこそ、PR の段階で回帰を検知したい。GitHub Actions で、golden dataset の一部 (fast subset) を走らせて、しきい値を下回ったらマージを止める、というのがミニマムだ。
name: llm-eval-gate
on:
pull_request:
paths:
- "prompts/"
- "retriever/"
- "eval/**"
- "pipeline.yaml"
jobs:
eval:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install -r requirements.txt
- name: run fast eval subset
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: python -m eval.run --dataset golden/fast.jsonl --report out/report.json
- name: enforce thresholds
run: python -m eval.gate --report out/report.json --config eval/thresholds.yaml
- uses: actions/upload-artifact@v4
if: always()
with:
name: eval-report
path: out/report.json
fast subset は 30〜50 件、5〜10 分で終わる規模にしている。全量 (数百件) は夜間の定期ジョブで別に走らせ、翌朝レポートを見る運用だ。PR のたびに 30 分待たされると、開発が止まる。
もう一つ、評価用データセットにもバージョンを付けておくこと。golden v1.3 と v1.4 でスコアを比較しても意味がないので、しきい値の議論をするときは「どのバージョンで測ったか」を必ずセットにする。前回の記事で扱った PII マスキングや権限分離と同じで、監査可能性の観点からも、評価データそのものをバージョン管理しておくと後で楽になる。
週次レポートの自動化
評価スコアは、貯めておかないと意味を持たない。週次で、指標の推移を1枚のレポートにまとめて Slack か社内 wiki に流す。筆者が最低限並べているのは以下だ。
context precision / recall、faithfulness、answer relevancy の週次平均と前週比
カテゴリ別 (規程、商品、業務手順など) スコア分布
低スコア質問トップ 10 と、その context が何だったか
p50 / p95 レイテンシと、コスト (1000 トークンあたり単価 × 週間トークン数)
データ再取り込みの added / updated 件数と、失敗した URL のリスト
Langfuse のようなトレーシング基盤を入れておくと、この集計は素直に書ける。1回目で書いたオントロジー側のカテゴリと、この週次レポートのカテゴリ軸を揃えておくのが地味なコツで、後から「営業カテゴリだけが下がっている」といった読み方ができるようになる。
「継続運用は組織設計だ」という一言
ある製造業の情シス部長が、雑談の中でこう言った。「技術的に動かせることと、組織として続けられることの間には、想像より広い川がある」。この一言は、そのままメモに残してある。誰が golden dataset を更新するのか、誰が週次レポートを読んで判断するのか、誰が再取り込みの失敗にオンコールで対応するのか。これが決まっていない状態でシステムだけ渡すと、半年後にはただの古いチャットボットになる。
MLOps や LLMOps という言葉はどうしても技術寄りに聞こえるが、うまく回っている現場はほぼ例外なく、この「誰が」を先に決めていた。ツールは後からでも足せる。
まとめ
3回にわたる社内 LLM 構築·運用のメモは、これで一区切りだ。1回目で RAG とオントロジーで業務構造を扱う設計、2回目で PII マスキング·監査ログ·権限分離のセキュリティ、そして今回で評価·再学習·追加開発の運用サイクル。書きながら改めて思ったのは、この3つはどれ一つ欠けても長続きしない、ということだった。同じ悩みを持つ人の参考になれば嬉しい。
筆者は 5years+ で日本·韓国の中小企業向け AI/LLM 業務応用を担当している。