Gaudiy Tech Blog

Gaudiyの技術、開発組織、カルチャーについてお伝えするブログです

【技術選定/OSS編】LLMプロダクト開発にLangSmithを使って評価と実験を効率化した話

こんにちは。ファンと共に時代を進める、Web3スタートアップ Gaudiy の seya (@sekikazu01)と申します。

この度 Gaudiy では LangSmith を使った評価の体験をいい感じにするライブラリ、langsmith-evaluation-helper を公開しました。

github.com

大まかな機能としては次のように config と、詳細は後で載せますが、LLMを実行する関数 or プロンプトテンプレートと評価を実行する関数を書いて

description: Testing evaluations 

prompt:
  entry_function: toxic_example_prompts

providers:
  - id: TURBO
    config:
      temperature: 0.7
  - id: GEMINI_PRO
    config:
      temperature: 0.7

tests:
  dataset_name: Toxic Queries
  experiment_prefix: toxic_queries_multi_provider

あとは次のように、CLIからこのライブラリのコマンドを実行するとLLMを使った処理たちが実行されて、結果をLangSmithのUI上で確認できる、という仕組みです。

langsmith-evaluation-helper evaluate path/to/config.yml

LangSmith実験履歴ページ

LangSmith実験結果詳細ページ

この仕組みはいわゆる実験管理を目的に作ったものです。この記事ではこのツールを作った背景や、他に比較した選択肢などについて解説していきたいと思います。

実験管理環境が欲しい!

まずはじめに、実験管理環境が欲しかった背景を説明します。

我々Gaudiyでは昨年からLLMを使ったプロダクト開発に力を入れておりましたが、プロンプトチューニングに対して毎度場当たり的な立ち向かい方をしており、疲弊していました。

具体的には、あるLLMを使った機能があるとして、至極愚直に次のようにチューニングをしていました。

  • チームのメンバーたちがその機能を使った体験をたくさんプロダクト上で触ってNotionで課題リストを作る。
  • チューニング担当者がその課題が出なくなるまでプロンプトを変更

これのよくないところとしては色々ありまして

  • インプットのパターンが網羅的でない
    • そもそもリリース前に網羅とかは不可能なのですが、プロダクトを触ってもらう人の感覚に依存しすぎなところがあり、見たい観点の設計ができていない状態でした
  • 実験の履歴を作っていないので、繰り返しプロンプトを改変していると「あれ?前の方が良くね?」と思ってもそこまで戻るのが手間
  • リグレッションが確認できていない
    • プロンプトは複雑になる程に性能が落ちる傾向、あると思います。様々な問題に対処するための場当たり的な変更を積み重ねた結果「あれ?前の方が良くね?」と特定の観点でなったり、気づいたら問題が再燃していたり…
    • 評価観点が積み上がってないので、以前他の人が書いたプロンプトで何を守りたくて追加されたルールなのかが分からない場面が何度ありました。なんとなくで消すとデグレの可能性が…
  • 単純にプロセスとして「複数のインプットで一気に実行」「問題が再現しないかを確認するために繰り返し実行」などが手間で、お手製スクリプトで都度対処していたがちょっと原始的過ぎる
  • 最終的に満たすべきアウトプットの合意がステークホルダー間で握れてないので、その時々のチューニング担当者が何をもってよしとするかに依存し、品質にブレが出る

我ながら書いてて中々アレだなという気持ちになってきましたが、上記のような色々が積み重なって、まあ色々辛い感じになっておりました。 という訳で「一回ちゃんと何かしら仕組み整えないと一生闇だぞここ…」と考え、そんな折に世の中には”実験管理”という概念があることも学んだため、その周辺のツールを色々触り始めました。

要件を考える

まずはツールをリサーチしつつ実現したい要件を整理します。 大まかには以下あたりが必要だろうと考えました。

  • チューニング・評価にまつわる成果物をチームの共有資産にして育てていけるようにする
    • データセット(インプット・期待するアウトプット)
    • 実験結果
    • 評価基準
  • 実験を楽にする
    • 複数のインプットに対して並列実行できる
    • 同じインプットで繰り返し実行できる
    • 実行に対して評価が自動で走る
    • 複数のプロバイダーで並列実行できる

チューニング・評価にまつわる成果物をチームの共有資産にして育てていけるようにする

まずLLMを使った機能を育てていくにあたって鍵となるのはデータセット評価基準を育てていくことです。LLMを使った機能がプロダクト要件の中で求められている品質を満たしているかを確認するかには評価基準が必要ですし、その評価基準が満たされているかを確認するためには実際にLLMに推論を行なっていただく必要があるので、その観点が見られるようなインプットのリスト、すなわちデータセットが必要です。

そして大事なのは育てていくという考え方です。 というのも、プロダクト要件から全てを網羅的に検証する評価基準やデータセットを初手で生み出すのは人間には不可能だからです。

実験をしていく過程で色んなアウトプットを見ていった結果「あ、こういう観点もあるな」と気づいて評価基準が変わっていったり、またリリース後も実際の使われ方を見ても変わっていくものでしょう。

余談ですがこのシステムを触っていく中で評価基準などが変わっていくことはCriteria Driftと名付けられていたりします。

arxiv.org

なので何らかの形で「最終的に品質保証を行なったデータセット」と「その時の評価基準(できれば実行可能なコード)」が誰かのローカルの作業環境に残るだけでなく、組織の共有資産として積み上がっていくと嬉しいだろうなと考えていました。

実験を楽にする

ここはシンプルに作業効率の話なのですが、実験をしていく過程では色々並列で実行したくなってきます。

  • 複数のモデルの結果を確認して、そのタスクにおいてどれがコスト/精度のバランスがいいかを見る
  • 課題が解決されているかを見るために同じインプットでも繰り返し実行したい
  • 複数のインプットで並列実行したい

などなど。 単純な話ではあるのですが、本当にチューニングする過程ではプロンプトを数十どころか数百回実行することなんてザラです。なのでここはシンプルなプログラミングで解決できる割に効果もすぐ出やすいので満たしていきたい観点でした。

辛い時は自分でお手製Pythonスクリプトで効率化してたりもしたのですが、それを秘伝のタレが如く育てていくのも非生産的だし、世の中には既にソリューションを実装してくれているところもあるので、一度ちゃんと調べて整えていこうと考えていました。

最初要件に考えてたけど外した要件: プロンプトをコード外管理

余談なのですが、最初はプロンプトの管理も一緒にできるといいかなと考えていましたが、これはとりあえず初期要件からは外しまして、多分今後もそんなに必要にならなそうかな?と考えています。

現状弊社のコードベース内では、プロンプトテンプレートの文字列をPythonのコード内で管理しており、それを変数として読み取る形で扱っています。

▼ イメージ

TOXIC_EXAMPLE_PROMPT = """
Given the following user query,
assess whether it contains toxic content.
 Please provide a simple 'Toxic' or 'Not toxic'
 response based on your assessment.

User content : {text}
"""

llm.invoke(TOXIC_EXAMPLE_PROMPT, **kwargs)

これの課題に感じていた点としては以下あたりでしょうか

  • エンジニアしか触れないところにプロンプトがある
  • プロンプトの変更を反映するために都度デプロイされる必要がある

ただ、実験管理環境ができた折に「UIを通して繰り返し動作確認をする」というユースケースはバックエンドの挙動をテストするのにフロントエンドと結合させて都度確認するような非効率さになるので、まずこの課題に関しては優先度が下がるなと思いました。

エンジニア以外がプロンプトを触れる件に関しても、コード管理外に置くために対応する必要があるタスクもそれなりにあり、そのデメリットを打ち消すほど強い要望も無さそうなのでマスト要件ではないかなと判断しました。

ちなみに対応することの具体例としては以下あたりです。

  • プロンプトテンプレート内で必要なインプットがコードから渡されるように担保する必要があったり(ないとエラーが出てしまう)
  • プロンプトを別途管理する方でもprd, stg, devで環境を分けるみたいな概念を登場させたり

ただめちゃくちゃヘビー要件というほどでもないですし、テンプレートのみならずコンフィグ(temperature や使うモデルなど)をセットで管理できるといいかもなみたいな気持ちもあったりするので、将来的に外部管理も始める機運が高まったりするかもな〜とは感じています。

ツール選定 最初はpromptfooから

上記のような要件を満たすツールを探しまして(あるいは自作も一応検討)、以下あたりを試してみました。

  • promptfoo
  • LangSmith
  • PromptLayer
  • MLFlow

他にも色々、本当にすごい数のツールがあるのですが、とても全部見てられないので一旦上記だけ素振りして確認しました。下記はObservability Toolリストなのですが、実験管理機能も兼ねてることが多いのでご興味あれば見てみてください。

drdroid.io

結論だけ先に述べると次のように進めました。

  • 最初 promptfoo が柔軟性高くていい感じだな〜、となる
  • ただ足りていない点がいくつかあり、そこをカバーできるLangSmithを検討
  • LangSmithが良さそうだとなったが、promptfooで一部恋しい体験もあったので、そこを埋めるためにちょっとしたライブラリを作成

ちなみにMLFlowはちょっとUIがこなれてなかったり機能も他選択肢と比較して足りていないように感じたので断念。PromptLayerもよくできていたのですが、LangSmith等と比較して有意な点がプロンプトテンプレートの管理のみに感じ、先ほど触れた通りそこは今回重要な観点として捉えていなかったのと、既に長らくLangSmithをログ基盤として採用していたのでこちらも断念しました。 (余談ですがログ基盤みたいな根っこを握っているとこういうところでもセットとして技術選定されやすくなっていくんだなという感想と、今後人々の目が肥えていくとLLMプロダクト開発のプロセス支援を全般的にできることが選定の基準になっていくんだろうなと思いました)

最初は promptfoo を触ってみました。他の同僚もちょうど実験するタスクを持っていたので試してもらっていたところ評判も上々で「なんかもうこれでいいんじゃね〜?」と思っていました。

大まかには次のような利点がありました。

  • データセットを用意すると並列で実行できる
  • プリセットの評価関数も多数あり実行できる
  • 複数のモデルの並列実行や繰り返し実行もできる
  • CSVとしてアウトプットしたりURLを発行して共有もできる

ただ以下のような欠点もありました。(2024/4月時点での情報なのでもしかしたら今はアップデートで解消されている部分もあるかもしれません🙏)

  • promptのテンプレートを返さないといけない = LLMの実行はpromptfooに委ねられている
    • これが割と致命的で、例えばChainであったりLangGraphなど複数ステップを持つようなもののアウトプットの評価を回したい時に活用できませんでした
  • CSVでテストデータセット管理する場合に評価の書き方が書きづらい記法になる

    • __expected1__expected2__expected3 などのカラムを生やしていく書き方

    www.promptfoo.dev

  • 実験結果の共有が手動で行われる必要がある

    • CSVで出力してNotionなりSlackなりにアップロードする必要性
    • shareオプションもありはしますが、リンクさえあれば誰でも見られる状態にはなってしまう
    • 一応self-hostのオプションもあるのですが、そこのデプロイ準備するのもちょっと面倒

という訳で、「シンプルなプロンプティング」「チューニングタスクが個人に閉じてそんなに問題ない」場合であれば promptfoo で問題ないかな〜〜〜という所感でしたが、せっかくならもう一声!なソリューションが欲しいなと思いました。

ただ繰り返しにはなりますが、プロンプトをいじっていく時の様々な"めんどくさい"を解消してくれることは間違い無いので、凄く良いツールだなと感じています。

そして LangSmith へ〜便利ライブラリの開発〜

大分欲しいオプションも充実していて良かったのですが、上記の微妙な点を解消できないかなとLangSmithのevaluateを使った仕組みを検討してみました。

参考までにLangSmithでは次のようにデータセットを作って

LangSmithのデータセット例

コードで evaluate という関数をLangSmithのSDKからインポートして実行します。

from langsmith import Client
from langsmith.schemas import Run, Example
from langsmith.evaluation import evaluate

client = Client()

# LLMの実行
def predict(inputs: dict) -> dict:
    messages = [{"role": "user", "content": inputs["question"]}]
    response = openai_client.chat.completions.create(messages=messages, model="gpt-3.5-turbo")
    return {"output": response}

# スコアをつける評価関数
def must_mention(run: Run, example: Example) -> dict:
    prediction = run.outputs.get("output") or ""
    required = example.outputs.get("must_mention") or []
    score = all(phrase in prediction for phrase in required)
    return {"key":"must_mention", "score": score}

experiment_results = evaluate(
    predict,
    data=dataset_name,
    evaluators=[must_mention],
)

https://docs.smith.langchain.com/old/evaluation/quickstart からコードを引用しつつ、概念理解に重要じゃない部分を削りました(⚠️そのままでは動かないコードになってます)

なんということでしょう、先ほどのpromptfooの問題の数々はLangSmithのevaluateで全て解けるではありませんか!

  • promptのテンプレートを返さないといけない = LLMの実行はpromptfooに委ねられている
    • ただの関数なのでなんでも書ける
  • CSVでテストデータセット管理する場合に評価の書き方が書きづらい記法になる
    • ただの関数なのでなんでも書ける
  • 実験結果の共有が手動で行われる必要がある
    • LangSmithにログインできる人にだけURLで共有できる

という訳でこの辺の要件はLangSmithでクリアできそうだなとなりました、しかしpromptfooの様々な便利オプションも捨てがたい…。

という訳で作ってみたのが、LangSmithのevaluateを活用しつつ、promptfooみたいないい感じのインターフェイスでプロンプトや処理を実行できる、こちらのライブラリです。

github.com

冒頭でも紹介したことの再掲になってしまうのですが、config とプロンプトテンプレートを渡す関数、評価を実行する関数を書いて実行できるようになっています。

▼ Config

description: Testing evaluations 

prompt:
  entry_function: toxic_example_prompts

providers:
  - id: TURBO
    config:
      temperature: 0.7
  - id: GEMINI_PRO
    config:
      temperature: 0.7

tests:
  dataset_name: Toxic Queries
  experiment_prefix: toxic_queries_multi_provider

▼ プロンプトのテンプレートを返す関数

TOXIC_EXAMPLE_PROMPT = """
Given the following user query,
assess whether it contains toxic content.
 Please provide a simple 'Toxic' or 'Not toxic'
 response based on your assessment.
User content : {text}
"""

def toxic_example_prompts() -> str:
    return TOXIC_EXAMPLE_PROMPT

プロンプトではなく任意の関数を実行することも可能です。(関数の引数にコンフィグで定義したLLM ProviderのIDが入ってきます)

from langsmith_evaluation_helper.schema import Provider
from langsmith_evaluation_helper.llm.model import ChatModel, ChatModelName
from langchain.prompts import PromptTemplate

def custom_run_example(inputs: dict, provider: Provider) -> str:
    # replace with your favorite way of calling LLM or RAG or anything!
    id = provider.get("id")
    if id is None:
        raise ValueError("Provider ID is required.")

    llm = ChatModel(default_model_name=ChatModelName[id])
    prompt_template = PromptTemplate(
        input_variables=["text"], template="Is this sentence toxic? {text}."
    )
    messages = prompt_template.format(**inputs)
    formatted_messages = PromptTemplate.from_template(messages)

    result = llm.invoke(formatted_messages)

    return result

▼ 評価を実行する関数

from typing import Any
from langsmith.schemas import Example, Run

def correct_label(run: Run, example: Example) -> dict:
    score = # 何かしらの評価関数
    return {"score": score}

evaluators: list[Any] = [correct_label]
summary_evaluators: list[Any] = []

これらを用意して実行したい対象のコンフィグへのパスを指定して以下コマンドを実行すると、データセットに対して定義したプロンプトや関数を用いてLangSmithのevaluateが実行されます。

langsmith-evaluation-helper evaluate path/to/config.yml

これにてLangSmithが持つ種々の恩恵を得られつつも、promptfooのようなインターフェイスで、プロンプトケース毎固有の情報だけを記述すればプロンプト・関数が一気に実行できるようになりました。やったね!

せっかくなのでOSSにしてみたので、皆様もLangSmithを活用する場合にはぜひ使ってみてください!

また、これを実際に使ってプロンプトチューニングに取り組んだ、より実務的な話を同僚の namicky さんが記事に書いてくれています。 ツールはあっても結局どうやってデータセットを作って評価を行なっていくか、という部分がプロダクト内でLLMを活用していくにあたって非常に大事でタメになると思うのでぜひ併せて読んでみてください。

techblog.gaudiy.com

おわりに

以上、実験管理環境の技術選定とライブラリを作った話を書いてきました。

データセットや評価などはしっかり資産として積み上がっていくようにしたいなという気持ちはありつつ、実験管理環境の技術は割と後からでも取り替えが効くものだと思うので、初めてのLLMプロダクト開発においては、とりあえず良さそうなものスッと選んで活用して肌感得るのがいいのではないかなと思います。

もし、LangSmithを活用していく際には我々のライブラリも使ってみてください! それでは!👋

site.gaudiy.com

site.gaudiy.com