LLMをコードに使うとき、すべてのエンジニアがぶつかる瞬間があります。出力は完璧に見えるのに……実はそうではありません。関数はコンパイルでき、構造も正しそうに見える。しかし、実運用の下では微妙な何かが壊れてしまうのです。「正しく見える」と「正しく動く」のギャップこそが、ほとんどの評価が失敗するまさにその場所です。
魔法のコード生成器としてLLMを扱うのではなく、分散システムのように扱うほうが役に立ちます。つまり非決定的で、レイテンシに敏感で、エッジケースだらけです。この記事では、精度・レイテンシ・失敗の挙動という、より現実に根ざした評価方法を探りつつ、実際に本番環境で使える実用的なフレームワークを紹介します。
Why Most LLM Evaluations Feel Misleading
現在の多くの評価手法は、現実というよりデモ向けに最適化されています。HumanEvalのようなベンチマークは価値がありますが、しばしば正しさを「いくつかのユニットテストに通るかどうか」に還元してしまいます。これはおもちゃのような問題には機能しますが、状態管理、外部依存、あいまいな要件といった現実的な複雑さを導入するとすぐに破綻します。
欠けているのは「文脈」です。
実際のエンジニアリングのワークフローでは、コードが単独で存在することはめったにありません。コードはシステムの中に生き、APIとやり取りし、時間とともに進化します。静的な問題ではうまく動くLLMでも、既存のコードベースを修正するよう求められたり、複数ファイルにまたがって推論する必要が生じたりすると失敗することがあります。
そこで問いは、「コードを生成できるか?」から、より実用的な「現実にぶつかっても生き残るコードを生成できるか?」へと移ります。
Accuracy Is a Spectrum, Not a Score
精度を二値の結果に還元したくなるのは自然です。テストが通るか、通らないか。ですが、それでは役に立つ情報が隠れてしまいます。
実際には、LLMが生成したコードはだいたい3つのカテゴリに分かれます。完全に正しいこともあります。ほとんど正しいが、エッジケースが抜けていたり、制約を誤解していたりすることもあります。そして、見た目だけでは気づきにくい形で、自信満々に間違っていることもあります。
より役に立つ考え方は、精度を「グラデーション」として扱うことです。
ある社内評価では、テストが通ったかどうかだけでなく、どのように失敗したかも追跡し始めました。エッジケースで実装が壊れるのか? 問題を誤解しているのか? それとも、構造的には正しいが不完全な解決になっているのか?
その結果、よりニュアンスのある指標が生まれました。
def weighted_accuracy(results):
score = 0
for test in results:
if test.passed:
score += 1
elif test.edge_case:
score -= 0.5
else:
score -= 1
return score / len(results)
このような採点は重要な何かを浮き彫りにします。すべての失敗が同じではないのです。エッジケースを見逃すのと、そもそも問題を誤解してしまうのでは大きな違いがあります。
Latency Changes How Developers Think
レイテンシはパフォーマンスに影響するだけではありません。挙動を変えてしまいます。
レスポンスが瞬時なら、開発者はより多く反復します。探索し、実験します。しかしレイテンシがじわじわと増えてくると、利用のパターンが変わります。プロンプトはより慎重になり、反復は遅くなり、ツールは「役に立つ」よりも「重い」と感じられ始めます。
面白いのは、レイテンシが単にモデルのサイズだけに関係するわけではない点です。プロンプトの作り方によって大きく左右されます。
たとえば、構造化された推論や複数ステップの指示を追加すると、出力の品質が向上することがよくあります。しかし、その分トークン生成時間も増えます。ある一連の実験では、明示的な推論ステップを追加すると正しさが目に見えて改善しましたが、システムがもっさり感じられるほどになり、開発者が素早いタスクに対してそれを使うのをやめてしまうほどでした。
これにより、微妙なトレードオフが生まれます。「最良」のモデルが必ずしも最も正確とは限らず、ユーザーの対話ループに合っているモデルが「最良」になり得るのです。
Failure Is Where the Real Signal Lives
成功だけを測定していると、最も価値のある洞察を見逃します。
失敗モードは、モデルがどう考えているか(より正確には、どう壊れるか)を教えてくれます。そして、失敗を分類し始めると、すぐにパターンが見えてきます。
繰り返し現れる課題のひとつは、私が「もっともらしいハルシネーション」と呼ぶものです。モデルは、慣用的でよく構造化されたコードを生成しますが、実在しない関数や前提に依存してしまいます。これらのエラーは、目視で通ってしまうため危険です。
もうひとつよくあるパターンは「コンテキストのドリフト」です。モデルは最初は正しく始めるのに、特に長い生成になるほど、元の要件から徐々にずれていきます。そして最後には、少し別の問題を解いてしまいます。
さらに、境界での失敗もあります。幸せな経路(happy path)は完璧に動くのに、その外側――ヌル値、大きな入力、並行性――といった条件が入ると解決策が壊れます。
これらを体系的に追跡すると、モデル評価の仕方そのものが変わります。「どのモデルが最良か?」ではなく、「どのモデルが、許容できるやり方で失敗するのか?」という問いに切り替わっていきます。
A Lightweight Evaluation System That Actually Works
LLMを適切に評価するのに、大規模なインフラ投資は必要ありません。単純なレイヤード構成で、意味のある結果を得られます。
中核となるのは4つの要素です。タスク定義、生成インターフェース、実行環境、そして分析レイヤーです。
以下は簡略化した流れです。
for task in task_suite:
prompt = format_prompt(task)
for model in models:
output = model.generate(prompt)
test_results = run_in_sandbox(output, task.tests)
analysis = analyze(test_results, output)
store(task, model, analysis)
重要なのは複雑さではなく一貫性です。すべてのモデルは、同じ条件のもとで評価されるべきです。同じプロンプトで、同じテストスイートで。
返却形式: {"translated": "翻訳されたHTML"}
The Trade-offs Nobody Talks About
ここに「無料の昼食」はありません。
精度を上げることは多くの場合レイテンシを増やします。レイテンシを下げると推論の深さが損なわれることがあります。より多くのコンテキストを追加すれば正確さは向上し得ますが、同時にノイズも持ち込むことになります。
プロンプトエンジニアリングですらコストがかかります。高度に最適化されたプロンプトはパフォーマンスを大きく押し上げられる一方で、脆くなりがちです。課題の構造を少し変えるだけで、品質が大きく落ちることがあります。
私自身の実験で意外だったのは、「完璧なプロンプト」がどれほど脆いかという点でした。あるデータセットで非常に優れた性能を示したプロンプトでも、問題の分布がわずかに変わるだけで、すぐに劣化しました。
これは重要な示唆です。ローカスト性は、ピーク性能よりも重要なのです。
Rethinking "Good Enough"
ある時点で、評価は指標を最大化することよりも、許容できるリスクを定義することへと比重が移ります。
LLMを社内のツールに使っているなら、たまに不正確なことがあっても問題ない場合があります。自動で本番のコードを生成するなら、ハードルははるかに高くなります。
目指すのは完璧さではありません。予測可能性です。
失敗のモードが明確で、常に85%の正確さを保てるモデルのほうが、95%の正確さでも予測不能に失敗するモデルより価値が高いことがよくあります。
Final Thought
LLMは静的なツールではありません。使い方によって振る舞いが変わっていく、進化するシステムです。評価にはベンチマークだけでは不十分で、実際の制約の中でどのように振る舞うかを観察することが必要です。
精度を「一点」ではなくスペクトラムとして捉え、レイテンシをユーザー体験の要素として捉え、失敗を洞察の源として捉え始めると、何かが変わります。「最良」のモデルを追いかけるのをやめて、実際に頼れるシステムを構築し始められるのです。
そして、そこでLLMは「すごい」ところから離れ、そして「役に立つ」存在になっていきます。


