LLMプロダクトは"モデル進化"に追随できているか ― 継続的品質評価基盤を設計した話

はじめに

こんにちは、Acsim 開発チームの池田です。

LLM は数ヶ月単位で進化しています。年に何度かフロンティアモデルが入れ替わり、そのたびに旧来のプロンプトが効かなくなったり、逆に新しい能力が解放されたりします。プロダクト側がこの進化に追随できるかどうかは、もはや「どのモデルを選ぶか」の問題ではなく、「品質をどう測り続けるか」の問題に変わってきていると感じています。

LangChain による業界調査でも、本番運用エージェントの最大の阻害要因として「品質」が挙げられており、回答者の3分の1が品質を主要なブロッカーとして報告しています 1 。一方で観測可能性 (observability) の導入率は89% と高水準なのに対し、評価 (evals) の導入率は52% にとどまるという結果になっています。「監視はしているが、評価まで手が回っていない」チームが多いのが実情のようです。

Acsim は多数のエージェントがシステム内部で稼働しておりますが、私たちもしばらく同じ問題を抱えていました。本記事では、モデル更新時の品質劣化を即時検知できる継続的評価基盤を、どう設計したかを書きます。

結果としては、2026年4月の claude-sonnet-4-6 リリース時には、この基盤によりモデル更新対応を 2日でリリース Ready な状態へ短縮できました。リリース後も品質劣化や LLM 起因の不具合は発生していません。これまでモデル更新対応に2週間〜1ヶ月かかっていたことを考えると、おおよそ 1/7〜1/15 の短縮になります。


背景:モデル更新のたびに2週間〜1ヶ月かけていた

Acsim は業務フロー分析、画面・機能の自動生成、編集提案、ナレッジ検索、画像解析など、役割の異なる複数のエージェントが連携して動くプロダクトです。それぞれのエージェントは異なる構造化出力を持ち、それぞれに「正しい振る舞い」の定義が違います。

LLM 評価基盤を整備する以前、私たちはモデルが更新されるたびに目視確認でこれを検証していました。各エージェントの出力を一つひとつ確認し、業務フローが正しく分析されているか、画面構造が崩れていないか、内部用語がユーザーに漏洩していないか、といった観点を人間がチェックしていく作業です。

このやり方には3つの限界がありました。

  • 時間がかかる — 27エージェント (本記事執筆時点) × 各複数のシナリオを目視で確認すると、1サイクルで 2週間〜1ヶ月を要する
  • 観点が属人化する — 確認者によって着眼点が違い、抜けが出る
  • CI/CD に組み込めない — 手動アノテーションは本質的にスケールしない

Langfuse の自動評価ガイドにも、プロンプトを微修正したりモデルを差し替えたりするたびに各変更が品質に与える影響を理解する必要があるが、毎回手動でアノテーションするのは遅くコストが高い、特に CI/CD に組み込みたい場合は厳しい、という話が出てきます 2。私たちもまさに同じ壁にぶつかっていました。

モデル更新のリリースを見ても、「すぐ試したいが、QA に2週間かかる」という状況が続いていました。


目指したこと

評価基盤を作るにあたって、最初に「何を測るか」を整理することから始めました。

基本のアプローチとしては LLM as a Judge ― LLM 自身に出力の品質を判定させる方式 ― を採用しています。エージェントが扱うのは構造化 JSON だけでなく、自然言語のテキスト、業務フロー分析、画面・機能の生成など多岐にわたるため、最終的には意味的な判定ができる LLM の力が必要だと判断したからです。

ただし、「この出力は良いですか」と漠然と LLM に聞くと、判定者の暗黙の好みに左右されて結果がぶれます。そこで、評価をぶれさせないために、出力を以下の 2 つの軸で測る形にしました。

  1. 入力指示への準拠 ― 出力がプロンプトの指示に忠実か
  2. 壊してはいけない観点のチェックリスト ― 「ここは絶対に維持してほしい」観点を満たしているか

順に説明します。

1. 入力指示への準拠

各エージェントには、システムプロンプトとユーザープロンプトでタスクの指示が与えられます。出力がそれらの指示にどれだけ忠実かを判定する軸です。Mastra の組み込みである prompt-alignment scorer を利用しています。

「指定されたフォーマットで返しているか」「セクション構成は指示通りか」「不要な情報を勝手に追加していないか」など、指示遵守に関する観点が広く対象になります。エージェントが共通して守るべきベースラインを、この軸で押さえます。

2. 壊してはいけない観点のチェックリスト

もうひとつの軸は、「ここは絶対に維持してほしい」観点をリストとして書き出し、項目ごとに独立に true/false で判定するものです。

const businessFlowAnalyzerChecklistScorer = createChecklistScorer({
  checkItems: [
    "成果物セクションに成果物名・説明・作成者・承認者の4列を持つ表が存在するか、成果物が存在しない場合は「該当なし」と明記されていること",
    "メール一覧セクションに件名・送信者・受信者・目的の4列を持つ表が存在するか、メールが存在しない場合は「該当なし」と明記されていること",
    "成果物セクションとメール一覧セクションでテーブルが存在する場合、各行が1件のみ記載していること",
    "出力に読み手にとって理解不可能なシステム内部用語(businessPatterns, judgementCriteria, operationMode 等のJSONフィールド名やcamelCase表記の英単語)が含まれていないこと",
  ],
  model: scorerLanguageModel,
});

最終スコアは「合格項目の confidence 合計 / 項目数」で算出しています。観点を項目リストとして書き下しておくことで、(a) どこで落ちたかがレポートから直接読み取れる、(b) 判定基準が言語化されてチームで共有できる、というメリットがあります。

Langfuse の自動評価ガイドでも "Pick one Failure Mode" — 一つの失敗モードに焦点を絞り、複数を一度にカバーしようとしない — という原則が紹介されていますが 2、私たちはこれを項目の粒度に落とし込んだかたちです。


実装上の工夫

ここからは、原則を具体的なコードに落としていく上で工夫した点を紹介します。

カテゴリ別にスコアラーを切り替える

入力の性質によって「正しい振る舞い」が変わるエージェントをどう評価するか、というのは最初に悩んだポイントでした。たとえばユーザーから来る入力が雑談なのか、データ参照のリクエストなのか、編集操作の依頼なのか、あるいは攻撃的な指示なのかによって、エージェントが取るべき行動はまったく異なります。

これに対しては、入力ケースをカテゴリでグループ分けし、グループごとに別のスコアラーセットを適用する設計にしました。雑談には「テキストとして自然な応答かどうか」、データ参照には「テキスト品質に加えて、正しいツール呼び出しが行われたか」、攻撃的入力には「適切に拒否されたか」を、それぞれ別の物差しで測ります。同じエージェントを評価していても、入力カテゴリごとに見るべき観点を切り替えるイメージです。

これによって「同じ物差しで全部測ろうとして失敗するパターン」を回避できました。LangChain の Deep Agents 評価記事でも、各データポイントごとに独自の成功基準が必要、という同じ問題意識が触れられています 3

具体的には、テストケースをカテゴリごとにフィルタしてからグループ単位で評価を実行しています。

const sections = [
  {
    title: "直接応答",
    cases: cases.filter((c) => c.category === "greeting" || c.category === "simple_question"),
    scorers: [directResponseScorer], // テキスト品質のみ
  },
  {
    title: "データ参照ルーティング",
    cases: cases.filter((c) => c.category === "data_query"),
    scorers: [routingScorer, toolCallAccuracyScorer], // LLM + Code
  },
  {
    title: "write操作",
    cases: cases.filter((c) => c.category === "write_request"),
    scorers: [writeRequestScorer],
  },
  {
    title: "インジェクション耐性",
    cases: cases.filter((c) => c.category === "context_injection_resilience"),
    scorers: [contextInjectionResilienceScorer],
  },
  // ...
];
for (const { title, cases: sectionCases, scorers } of sections) {
  await runExperimentWithLogging({
    cases: sectionCases,
    scorers,
    target: agent,
    // ...
  });
}

Checklist scorer の判定根拠を先に出させる

軸2で使うチェックリスト方式のスコアラーは、LLM 自身にチェック項目を1つずつ「合格 or 不合格」で判定させる仕組みです。便利な一方で、LLM judge は同じ入力でも結果がぶれることがあるため、判定精度を上げる工夫を入れています。

ポイントは、LLM に答えを出させる順番です。多くの実装では「判定結果 → 理由」の順で出力させますが、これだと LLM は先に結論を出してから後付けで理由をこじつける動きになりがちで、判定の根拠が結論に引きずられます。逆に「理由 → 確信度 → 判定結果」の順で出させると、LLM は根拠を組み立ててから結論を導く流れになります。

なぜこれが効くかというと、LLM は autoregressive な性質を持つため、先に出力したトークンに後続の出力が引きずられるからです。先に結論を出してしまうと、後付けで理由を結論に合わせてしまうバイアスが生じます。これは Chain-of-Thought プロンプティングと同じ原理を、構造化出力のスキーマ設計に応用したものと言えます。

Arize AI のブログでも、LLM judge の設計において「説明とラベルどちらを先に出すかという順序」が判定品質に影響する、という整理がされています 4。説明を先に要求することで繰り返し判定における分散が減り、人間アノテーターとの一致度が上がる、という研究結果が示されています。実装としてはスキーマ上のフィールド順序を意識的に並べるだけですが、効果は実感としても大きいです。

具体的には、スコアラーの出力スキーマで reason → confidence → passed の順にフィールドを並べ、判定プロンプトでも同じ順序を明示しています。

const evaluationItemSchema = z.object({
  item: z.string(),       // ① チェック項目の内容
  reason: z.string(),     // ② まず判定根拠を出力させる
  confidence: z.number(), // ③ 次に確信度
  passed: z.boolean(),    // ④ 最後に結論
});
# 回答フォーマット
各チェック項目について個別に判定し、items 配列として出力してください。
- item: チェック項目の内容
- reason: 判定根拠
- confidence: 判定の確信度(0.0〜1.0)
- passed: 根拠と確信度を踏まえた上での判定結果(true/false)

Code Judge の実装

軸1・軸2 はどちらも LLM judge による意味的な判定が中心ですが、すべてを LLM に投げる必要はありません。「ツールが期待通りの引数で呼ばれたか」「禁止語がユーザー向け出力に混入していないか」のような、決まったロジックで判定できる観点については、コードで決定的にチェックしています。具体例を2つ挙げます。

ツール引数の値を見るスコアラー

エージェントが呼び出すツールには、ふるまいを切り替えるためのフラグ引数があることがあります。たとえば「データを全部作り直す or 部分編集する」を切り替えるフラグや、「上書きを許可するかどうか」を切り替えるフラグなどです。

入力ケースに応じて「このケースではフラグは true で呼ばれるべき」「このケースでは false で呼ばれるべき」というのが決まっているなら、これは LLM に意味判定させるまでもなく、ツール呼び出しの記録を見ればコードで一発でわかります。判定がブレないため、LLM judge より遥かに安定して動きます。

内部用語の漏洩を検知するスコアラー

エージェントがユーザー向けに出力するテキストには、JSON のフィールド名や enum の内部値といったシステム用語が漏れてしまうことがあります。たとえば、内部でステータスを表すために英語のキー名を使っていたら、ユーザー向け文面にもそのまま英単語が書かれてしまう、という類のミスです。そのような値が露出したところでセキュリティ的な懸念はありませんが、ユーザーから見ると意味のわからない用語が突然出てくることにより体験が悪くなります。

これも LLM に意味判定させるまでもなく、禁止語リストとの照合で機械的に検知できます。ポイントは、禁止語リストをアプリ本体で使われている定数からそのまま取り込むことです。新しい内部用語が増えても禁止語のリストが自動で追従するため、本体側と禁止語のリストがずれる余地をなくせます。

これらの Code Judge 群があることで、LLM Judge は意味的な判定だけに集中できるようになります。Anthropic の "Demystifying evals for AI agents" でも、コードで機械的に判定できる部分はコードに任せ、必要な場合のみ LLM に判定させ、人間のレビューは校正に使う、というアプローチが推奨されています 5

チェック項目はボトムアップで育てる

スコアラーをどんなチェック項目で構成するかは、最初の段階ではある程度予測で書きました。エージェントの仕様や出力スキーマを見て、「ここはミスしそうだな」という勘所をベースに書き出していくスタイルです。

ただ、最初から完璧なチェックリストにできるわけではありません。実際にスコアラーが育っていったのは、これまでの手動 QA で蓄積していた経験 ―「以前このパターンで劣化を見つけた」「本番でこういうエラーに遭遇した」― を、ひとつずつチェック項目や Code Judge に落とし込んでいったプロセスでした。例えば前述の内部用語の漏洩を見る Code Judge も、過去に「これに気をつけて見ていた」観点を後から実装に反映したものです。

評価基盤を一気に整える必要はなく、現場で気づいた失敗モードをそのつどスコアラーに足していく、という運用がチームにも馴染みやすかったです。

プロンプトとスキーマを実装と実験で共通化する

エージェントに渡すプロンプトや、出力のスキーマは、本番コードと実験コードの両方から参照する必要があります。これらを二重管理にすると、実装側を直したときに実験側を直し忘れて、実験は古い定義のまま回って通ってしまう、という事故が起きます。実験結果が本番の挙動を反映していなければ、そもそも実験する意味がありません。

そこでプロンプト生成関数とスキーマは、実装側で定義したものを実験側からインポートして使う運用に統一しました。プロンプトがエージェント定義に直接埋め込まれていたり、スキーマがエージェントファイルに含まれている場合は、これらだけを別ファイルに切り出してから両方で参照する形に整えています。

具体的には、実装側でエクスポートした関数を、実験側からそのままインポートして使うだけです。

// 実装側
export const buildGenerateScreensPrompt = (initData, sections) => { /* ... */ };

// 実験側
import { buildGenerateScreensPrompt } from "../../path/to/prompts";

const buildPrompt = (input) => {
  return buildGenerateScreensPrompt(input.initData, input.sections);
};

やってみてどうだったか

claude-sonnet-4-6 がリリースされたとき、この基盤の効果が分かりました。

指標BeforeAfter
モデル更新対応〜リリース Ready まで2週間〜1ヶ月2日

数字以外にも、いくつか良かったことがあります。

ひとつは、モデル更新が定型作業になり、属人性がなくなったことです。これまでは誰がやるか・何を観点に確認するかで結果がぶれる作業でしたが、「実験を回す → スコアを読む」というシンプルな流れに落ちました。担当者が変わってもアウトプットがぶれないので、運用としても安心感がありますし、エンジニアの負荷もかなり下がりました。

もうひとつは、デグレも改善も、両方が見えるようになったことです。モデル更新でスコアが下がっている箇所を発見しやすくなったのはもちろんですが、逆に上がっている箇所も明確になります。チームにモデル移行のコストをかけてもらうときに、「このエージェントは精度が上がる」「このエージェントは現状維持」と数字で示せるようになったので、移行後のメリットを定性的にも定量的にも説明しやすくなりました。

似たアプローチを取っている事例として、CodeRabbit の Claude Opus 4.7 評価記事が参考になります 6。彼らも新しいフロンティアモデルが登場するたびに、現行アンサンブルの全モデルとベンチマークし、どこで上回りどこでそうでないかを定量評価しているそうです。100個の Error Pattern というキュレートされたベンチマークセットで Opus 4.7 が従来比 24% 改善、といった具体的な数字も公開されています。モデルを育てるのではなく、評価基盤を育てる、というアプローチは方向性として共通しているなと感じました。


まとめ

Acsim での継続的品質評価基盤の設計と運用について紹介しました。

技術的なポイントを整理すると以下のようになります。

  1. 2軸で評価を制御する — LLM as a Judge を基本に、(a) 入力指示への準拠、(b) 壊してはいけない観点のチェックリスト、の2軸で出力を測る
  2. 判定根拠を先に出させるreason → confidence → passed のスキーマ順序で、LLM judge のブレを抑える
  3. コードで判定できる部分はコードで — ツール引数や禁止語の漏洩など、決まったロジックで判定できるものは LLM ではなく機械的にチェックする
  4. チェック項目はボトムアップで育てる — 最初は予測ベースで書き、手動QAや本番エラーで遭遇した観点を少しずつスコアラーに反映する
  5. プロンプトとスキーマを実装と実験で共通化する — 実装側で定義したものを実験側からインポートし、二重管理を避ける

最初の一歩として「モデル進化に追随できる体制」を作れたことの意味は、自分たちの中では大きいと感じています。今後はスコアラー自体の判定精度の校正、セキュリティ系評価の整備、本番トラフィックを使ったオンライン評価への拡張など、やりたいことはまだ残っています。

同じような課題を感じている方の参考になれば幸いです。


Footnotes

Footnotes

  1. State of AI Agents — LangChain — 本番運用エージェントの最大の阻害要因として品質が挙げられている調査結果。observability の導入率89%に対し、評価の導入率は52%。

  2. Automated Evaluations of LLM Applications — Langfuse — 自動評価の必要性と "Pick one Failure Mode" 原則について。 2

  3. Evaluating Deep Agents: Our Learnings — LangChain — エージェント評価における bespoke なテストロジックの必要性について。

  4. Evidence-Based Prompting Strategies for LLM-as-a-Judge — Arize AI — 説明とラベルの出力順序が判定品質に与える影響について。

  5. Demystifying evals for AI agents — Anthropic — コードによる機械的な判定を優先し、LLMによる判定で補完するアプローチについて。

  6. What Claude Opus 4.7 means for AI code review — CodeRabbit — モデル更新時のベンチマーク評価のアプローチ事例。