こんにちは!Eureka AI Teamで、Pairs(ペアーズ)のMLOps Engineerをしているnariです。

こちらは、Eureka(Pairs) Advent Calendar 2024 の10日目の記事です。

本日は、私たちが構築した「LLMOps基盤」と、その中核を担うツールLangfuseを活用した「評価ドリブンなリリースサイクル」についてご紹介します。

対象読者

この記事は以下の方々に向けています:

  • LLMアプリケーションの運用に課題を感じている方
  • LLM-as-a-Judgeとかよく聞くけど、そういう評価システム/プロセスって何から始めればいいかわからない方
  • 評価ドリブンなリリースサイクルの全体像がまずは知りたい方
  • LLMOps基盤の設計/運用、そちらを用いた継続評価フローの設計に興味がある方

TL;DR

  1. LLMアプリケーションの運用は従来のMLOpsの手法が通じず、かつ出力の評価が難しいことなどが起因して、非常に難しい
  2. 上記の課題を解決するために、Langfuseを中枢に据えたLLMOps基盤を用いて、オンライン評価とオフライン評価でリリースを挟み込んだ評価ドリブンなリリースサイクルを回していくのがおすすめ
  3. 上記を実践するために
  • まずはアプリケーションのログ・トレースを保存するところから始める
  • 次にプロンプトマネジメント導入と、評価データセット作りを数件からでよいので始める
  • そこからプロンプト実験と、LLM-as-a-JudgeなどのLLM Evaluatorの仕組みを、評価基準など不完全で良いので導入してみる(ドメインエキスパートやユーザーのアノテーションの仕組みを導入できるならそちらも並行して検討する)
  • これらをまずは実践することで、評価ドリブンなリリースライフサイクルが、評価データセットと評価基準を育てながら回せるようになる

背景:LLMアプリケーション運用の課題

ここでいうLLMアプリケーションは、基本的に自社でLLMモデルを継続学習しホスティングして運用する形ではなく、ベンダーの提供するLLM APIを利用したアプリケーションを対象とします。

LLM APIを活用したアプリケーションの運用では、以下のような課題が顕著です:

  • 出力の評価が難しい:モデルの出力が自然言語などで品質評価が難しく、最初から理想的な出力を完全に言語化するのも困難で、運用しながら評価基準を改善していく必要があります。
  • 従来のMLOps手法がそのままでは通じない:従来の機械学習のように継続的学習(Continuous Training)をベースにした改善が難しいため、LLMアプリケーション運用のノウハウが必要となります。
  • モデル/プロンプトの出力精度低下(デグレ)の検知の重要性:様々なベンダーから新しいLLM Model APIが急速に進化してリリースされるため、その都度モデル/プロンプト検証が必要になります。しかし、モデルやプロンプトの変更が思わぬ形で出力の品質に影響を与えるリスクがあり、事前の検知と発生した場合は迅速な検出と対応が求められます。

また、弊社ではLLMアプリケーションを開発運用しているのがAI Teamだけでなく、SRE/Platform Teamも開発者の生産性向上のためのLLM活用を戦略の一部として実施しており、全社的に使えるこういった課題の解決を支援する基盤を必要としていました。

これらの課題を解決するため、Langfuseを中核に据えたLLMOps基盤を設計・実装して提供し、評価ドリブンなリリースサイクルの実現を目指しました。

LLMOps基盤のシステムアーキテクチャ

LLMOps基盤のシステムアーキテクチャ

主なコンポーネントと役割

Langfuse(Self-hosted)

LLMアプリケーションのログ・トレースを一元管理し、評価データセットやカスタムスコアを用いた継続的な評価を実現します。Amazon EKS(Kubernetes)上にHelmでホストし、ArgoCDでリリース管理をしています。Langfuse v2 を使用して構築しています。

Amazon Aurora PostgreSQL

Langfuseで収集されたログ・トレースデータや評価結果を格納します。

GitHub Actions(GHA)

プロンプト更新や実験フローを自動化するためのCI/CDパイプラインを構築しています。

Langfuseを採用した理由

LangfuseをLLMOps基盤のメインツールとして採用した背景には、次のような理由があります:

1.LLMOpsに必要な機能を網羅

LangfuseはSelf-hostするパターンでも、ログ・トレース管理、プロンプトマネジメント、評価データセット、実験管理、カスタムスコアによる評価など、LLMOpsに必要な機能を網羅的に提供しています。(現在はFree Planを使用中)

2.Self-hostしやすさ

LLMOps系のSaaSソリューションは、大規模トラフィックのログ・トレースデータ量によるコストが課題で、ある程度の規模のtoCサービスだと採用が難しいことが多いです。

しかし、LangfuseはOSSとして提供され、Self-hostすることが可能であり、しかもhelm chartまで提供されているので、弊社のメインホスティング先であるAWS EKSを用いて構築できることも大きかったです。

評価ドリブンなリリースライフサイクルの全体像

(引用元:https://langfuse.com/docs/datasets/overview

Langfuseが推奨する、上記のようなフローを実現していくことをベースに進めました。

ざっくり説明すると、ログ・トレースと評価データセットを中心に、オンライン評価プロセスとオフライン評価プロセスでリリースを挟み込んでこのサイクルを繰り返していくイメージです。

ここからは、Langfuseで実現するオンライン評価プロセスとオフライン評価プロセスについて順番に説明していきます。

オンライン評価プロセス

オンライン評価プロセス全体のシーケンスは以下のようになっています。

オンライン評価プロセス シーケンスフロー
  1. ユーザーが使用してログ・トレースデータを保存を保存する
  2. そのログトレースに対してユーザーやドメインエキスパート、開発者、LLMが評価しアノテーションする

という流れを、それぞれ詳しく説明していきます。

1. ログ・トレースデータの保存

ユーザーの利用で発生したログ・トレースデータをLangfuseに送信し、後続の評価や分析などで活用します。すべてのベースとなる重要なデータです。

Python SDKを使用すると、@observe()のアノテーションを対象メソッドに付与して、呼び出すだけでTraceとそれに紐づくObservationがネストされた状態まで表現されます。

(引用元:https://langfuse.com/docs/tracing

その中で、langfuse_contextを用いTrace/Observationそれぞれの状態を更新し、さまざまな情報を付与することが可能になっています。

from langfuse.decorators import langfuse_context, observe

@observe()
def fn():
langfuse_context.update_current_observation(
level="WARNING",
status_message="This is a warning"
)
# outermost function becomes the trace, level and status message are only available on observations
@observe()
def main():
fn()
main()

ここで貯めたログ・トレースデータは、後述する評価アノテーションなどを参考にしながら、オフライン評価で使用する評価データセットにもワンクリックで追加することができます。とても便利です。

トレース詳細画面のAdd to datasetボタン

2. 評価とアノテーション

2.1 人(ユーザー、ドメインエキスパート、開発者)による評価

ユーザーによるフィードバック評価の仕組みをアプリケーションに組み込める場合、ユーザーに回答品質などを評価してもらい、それをLangfuseのカスタムスコア機能でトレースにもアプリケーションなどで自動マッピングします。

カスタマーケアやリサーチャーなど、そのタスクのドメインエキスパートにサンプリング評価してもらう機構が作れる場合も、こちらもLangfuseのカスタムスコア機能でトレースにもアプリケーションなどで自動マッピングします(理由が添えられるなら理由もあると望ましい)。ドメインエキスパートと定例を組んで一緒にアノテーションの結果を眺めながら対話していって、プロンプト/システムのチューニングの参考にするのも非常に有用です。

LangfuseはUI上からも直接カスタムスコアをつけることができるので、開発者自ら地道に評価してアノテーションするパターンもありますが、継続的に高精度で行うのは非常に難しいです。

上記それぞれのデータを分析しながら、評価データセットに追加したり後述するLLMによる評価の評価基準を改善したりプロンプトを改善していくことになります。

2.2 LLMによる評価(LLM-as-a-Judge)

人による評価をすべてに実施してアノテーションできればいいですが、現実問題それはコストや工数の問題で難しいことが多いです。アノテーションのリードタイムやコスト的に、LLMによる評価を選ぶ必要が出た場合に導入を検討します。

弊社では、AI Teamが管理するAmazon EKS ClusterにホスティングしているLLM Evaluator Batchが、非同期に評価タスクを処理して、Langfuseのカスタムスコア機能を用いてアノテーションを行なっています。

こちらで使う評価用プロンプト、評価基準を育てていく必要がありますが、前述の通りドメインエキスパートのラベリングを参考にしたり、ユーザー評価を分析しながらだんだん育てていけばいいので最初から作り込む必要はありません。(RagasなどのLLM-as-a-Judgeライブラリで既存の指標について評価しアノテーションすることを始めるというのも手)

具体的な実装内容は、対象のタグがついたトレースをフェッチしてきて、評価の処理を実行して評価スコアをアノテーションを繰り返しているだけです。

def evaluate_and_score_traces(target_tag: List[str], env: str, task_type: str) -> None:
to_ts = datetime.now()
from_ts = to_ts - timedelta(minutes=3)
traces = langfuse.fetch_traces(from_timestamp=from_ts, to_timestamp=to_ts, tags=target_tag).data
for trace in traces:
if ALREADY_LLM_EVALUATED_TAG in trace.dict().get("tags", []):
continue
expected_output = ""
if env == "stage" and trace.dict().get("metadata"):
expected_output = trace.dict()["metadata"].get("expected_output", "")
evaluation = evaluate(
task_type,
trace.dict().get("input"),
trace.dict().get("output"),
expected_output
)
langfuse.score(
trace_id=trace.id,
name=f"llm_eval_score_{task_type}",
value=evaluation.get("score"),
data_type="NUMERIC"
)
langfuse.score(
trace_id=trace.id,
name=f"llm_eval_reason_{task_type}",
value=evaluation.get("reason")
)
(引用元:https://langfuse.com/docs/scores/external-evaluation-pipelines

こちらのアノテーション結果も、定期的に整理して分析し、プロンプト/システム/評価データセット/評価プロンプトのチューニングを行なっていきます。

また、人/LLMそれぞれの評価スコア平均の時系列推移についてもLangfuseで可視化できるので、そこでデグレ検知や改善を確認することも可能になっています。

評価スコア平均の時系列推移

より具体的なLLM-as-a-Judgeの作り方は、以下のブログが非常に参考になります。

オフライン評価プロセス

オフライン評価実験は大きく分けると2つ存在します。

プロンプト単体実験と、LLMアプリケーション統合実験です。まずは前者の導入を検討し、前処理やルールベース、複数のモデルでの処理を含めて統合評価したいケースは後者を検討するのが良いと思います。

今回はプロンプト実験の方をメインで説明していきます。なお、今後プロンプトと称する時は、プロンプトテキストだけでなく対象モデルIDや推論パラメータなどのConfigも含むものとします。

プロンプト実験

プロンプト実験のシーケンスフローは以下のようになっています。

プロンプト実験のシーケンスフロー

それぞれの工程を説明していきます。

1.プロンプトの更新をBranch Push トリガーでGHAで実行する

プロンプトも、LangfuseのPrompt Management機能を用いて管理しています。前述の通り、プロンプトテキストだけでなく、使用するモデルIDや推論パラメーターもセットで登録し、バージョン管理できます。

プロンプト登録/更新のオペレーションとしては、GitHub上のプロンプトを修正し、そちらからLangfuse側にGHA Pipelineで同期するような設計にしています。

langfuse.create_prompt(
name=name,
type=prompt_type,
prompt=prompt_text,
labels=labels,
config=prompt_config_local,
)

このような運用にしているのは、今のところプロンプトをいじるのは開発者のみで、コード管理してプロンプト変更に対してもレビューを行いたいからです。

2.評価データセットを取得し、新バージョンのプロンプトへ実験を実行する

GHA Pipelineがトリガーされ、Langfuseのプロンプトが登録・更新された後、Langfuseから評価データセットを取得し、そちらを用いてその新バージョンのプロンプトへの実験を実行します。

入力と期待する出力のセットを1アイテムとして扱い、その集合体をLangfuseではDatasetと呼び、この記事では評価データセットと呼んでいます。

Langfuseなら評価データセット(Dataset)はワンクリックで作れます。アイテムは事前に数件で良いので追加しておきます。こちらは前述した通り、運用する中でオンラインのログ・トレースから良さそうなものを追加しながら件数を増やしていったり改善していくことができるので、最初は数件程度から小さく始める形で構いません。

Langfuseでは、評価データセット(Dataset)と、実験(DatasetRun)と評価データセットアイテム(DatasetItem)の関係は以下のようになっています。

(引用元:https://langfuse.com/docs/datasets/overview

Python SDKを使用している場合、評価データセット配列をforで回して、評価データセットアイテムそれぞれに対してitem.observeを実行すると、自動でそれぞれの評価データセットアイテムに紐づく形でTraceを生成してくれるので、そこにプロンプトの実行結果のInput/Outputなどの情報を付与して実験結果を保存することが可能になっています。

def run_experiment(experiment_name, system_prompt):
dataset = langfuse.get_dataset("capital_cities")
for item in dataset.items:
# item.observe() returns a trace_id that can be used to add custom evaluations later
# it also automatically links the trace to the experiment run
with item.observe(run_name=experiment_name) as trace_id:
# run prompt, pass input and system prompt
output = run_my_prompt(item.input, system_prompt)
# optional: add custom evaluation results to the experiment trace
# we use the previously created example evaluation function
langfuse.score(
trace_id=trace_id,
name="exact_match",
value=simple_evaluation(output, item.expected_output)
)

必要な場合は、Ground Truth(期待する出力)との一致や距離などの簡単な評価についてはここで一緒に実行してカスタムスコアとしてアノテーションします。

3.LLM Evaluator Batchによる非同期トレース評価とアノテーション

非同期的にトレースを対象タグでフィルタして取得し、評価後にLangfuse のカスタムスコアの機能を用いてアノテーションを実施します。

実験に紐づいているトレースが、アプリケーションを実行した際のトレースと全く同じ構造であるため、前述のオンライン評価でのLLM Evaluator Batchによる評価とアノテーションの仕組みを用いて、ほぼ同じ方式できています。(違いは、評価プロンプトに期待するアウトプットを渡して追加の判断材料にしてもらうことくらい)

4.実験結果URLを付与した、ステージング環境への新プロンプト反映のPull Requestを作成

実験の実行が終わったら、新プロンプトをステージング環境に適用するPull Requestを作成。実験結果のURLが付与されているのでそちらを確認して、LLM評価スコアの平均やその他指標が改善していた場合のみマージして反映します。(実行にかかるコストやレイテンシーについても比較できるのも嬉しい)

直近の例だと、Claude 3.5 HaikuやAmazon Nova micro/lite/proなどの新しくリリースされたモデルに対して、さまざまなユースケースに対して独自の評価データセットを用いて性能(LLM評価スコアベース)、コスト、レイテンシーがどのように変化するのかさくっとオフライン評価して分析できるのは非常に便利でした。

作成されるPull Requestサンプル
実験結果は、Prompt Versionで比較することができる
実験結果は、評価データセットアイテム毎にもPrompt Versionで比較することができる

LLMアプリケーション統合実験

LLMアプリケーション統合実験 シーケンスフロー

ここでは詳しく説明しません。プロンプト実験との差分は、トリガーをBranch PushではなくWorkflow Dispatchに変更し、統合実験用の評価データセットを用いて実験で実行する対象がプロンプトからステージング環境のアプリケーションになるだけです。(その際、実験で発生するTraceとアプリケーションで発生するTraceが同一コンテキストで処理できない場合、Session IDによるグルーピングが役立つかもしれません)

今後の展望

Langfuse v2からv3への移行

弊社の大規模なトラフィックの時系列トレースデータを、さまざまなフィルターや期間で分析しようとすると、処理パフォーマンスが悪化し、PostgreSQL DBインスタンスも高負荷な状態になるケースが度々起こっていました。

昨日(12月9日)に正式リリースされたv3は、現在使用しているv2からアーキテクチャを大きく変更し、OLAP DBとしてClickHouseを採用しています。これにより、上記の課題を解決できる可能性が高いため、近々移行を計画しています。

(なお、Langfuseの公式ドキュメントでも、ClickHouseを採用するアーキテクチャへの変更理由として、PostgreSQLの行ベースのストレージモデルでは複雑な集計や時系列データを扱う際にパフォーマンスのボトルネックが生じていたことを挙げています。)

v3のアーキテクチャ(引用元: https://langfuse.com/self-hosting/upgrade-guides/upgrade-v2-to-v3

Langfuse OSSの有償プラン(Pro/Enterprise)の導入検証

有償プランでしか使えない機能に魅力的なものは多く(Managed Prompt Experiment、Managed LLM-as-a-Judge Evaluator、etc)、そちらを使用するためのコストに見合うと判断できればFree Planからの変更を検討したいです。プランごとの料金や機能紹介はこちら

複数のモデル/エージェントなアプリケーションの評価プロセスの制定

今後、複数のモデルやエージェントの採用によりどんどんLLMアプリケーションのシステムデザインは複雑になっていきます。それに合わせて、オフライン/オンライン評価方法も適宜修正していく必要があります。

その際、以下の論文が非常に参考になりそうです:

終わりに

Langfuseをフル活用してLLMOps基盤を構築することで、LLMアプリケーションの品質向上のための評価ドリブンなリリースサイクルを実現することができました。

この記事のメインの目的は、巷でよく話されているLLM-as-a-Judgeや評価システムについて見聞きはするけれど、始め方も全体像もわからない人にそれらを提供することです。

まずはアプリケーションのログ・トレースを貯めることから始めて、プロンプトマネジメントを導入し評価データセットを数件でいいから作ってみて、評価はプロンプト実験から始めていき、そこからLLM-as-a-JudgeのEvaluator、ドメインエキスパート/ユーザーによる評価とアノテーションを徐々に導入して、リリースサイクルを回しながらそれぞれどんどん改善していけばいいと思っています!

まずは小さく始めて、徐々に良いものにしていくのにLangfuseは非常にお勧めできるLLMOps系のツールだと思うので、ぜひご検討ください。

参考文献

--

--

Pairs Engineering
Pairs Engineering

Published in Pairs Engineering

Learn about Pairs’ engineering efforts, product developments and more.

TAKASHI NARIKAWA
TAKASHI NARIKAWA

Written by TAKASHI NARIKAWA

SWE(MLOps/SRE) in Tokyo. MLOps/LLM/SRE/devops/Go/Python/AWS/Terraform/Kubernetes/Docker

No responses yet