Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Vol.04 LLMOps に取り組み始めた話

技術本部Strategic Products Engineering Unit Contract One Devグループの伊藤です。契約データベース「Contract One」の開発に携わっています。
Contract Oneでは、GPTを活用した機能をいくつか提供しています。
今回は、Contract OneのGPTを活用した機能開発のために、LLMOpsの取り組みの一環としてLangfuseを導入し始めた話をします。

なお、本記事は【Strategic Products Engineering Unitブログリレー】という連載記事のひとつです。
buildersbox.corp-sansan.com

はじめに

Contract Oneでは、GPTを活用した文書内検索 *1 と要約機能 *2 を約1年前にリリースし、現在も提供しています。
GPTは自然言語形式の入力をAPI形式で処理できるため、さまざまなプロダクトで比較的容易に取り入れやすいです。しかし、多くの場合で問題となるのはアウトプットの品質です。GPTにはハルシネーションと呼ばれる、一見それらしい出力を返してしまうという問題があるため、明確に指標を定めて評価していく必要があります。
それに加え、GPTやLLMの界隈は驚異的なスピードで進化しており、モデルやツールの数も日々増えています。その進化をプロダクトに反映し続けていくためには、効率的に評価が行える仕組み作りが重要です。大規模言語モデルの運用管理についてのプラクティス・技術・ツールなどがLLMOpsなどと呼ばれます。
Contract Oneでも、新しい技術が出るたびに検証はしていたものの、仕組み化されていないために効率的な評価が難しい状態でした。今回はそこについて向き合ってきた過程や、今向き合っていることについて共有します。

問題の整理

まず、起きている問題を整理します。主に次の3点が問題であると定義しました。

  1. 研究開発部と共同で開発を行っているが、検証用データが共有しやすく、かつ実行可能な状態になっていない
  2. どの検証用データに対して、どのモデルを使用してどのような結果が出たか管理されていない
  3. 検証する際の評価指標などが一元管理できていない。また、良しあしが曖昧なことが多いために手動での評価を行っている部分があるために自動で評価できない

LLMOpsの領域としては本番環境でのモニタリングやリリースフローなども含まれますが、スモールスタートとして今回は重要視しないことにしました。
上の3点について、どういったソリューションがあるか調査しました。

LLMOpsのためのツール選定

LLMOpsのためのツールはいくつか既存サービスがありましたが、結果的にLangfuseを採用することにしました。主な採用理由は大きく分けて2点です。

self hostが柔軟に行えるため

お客さまからお預かりしているデータを利用した検証を行いたい場合があるため、self hostが利用できることはマストでした。
Langfuseはself hostが利用でき、コンテナ形式で提供されているためデプロイの選択肢が多いです。データ管理もPostgreSQLにつなげるだけでよいため、Cloud Run / Cloud SQLなどで簡単に運用できる点が大きな魅力でした。
また、一部の機能を除いたすべてがMITライセンスで使用できるため、費用がかからないことも大きいです。
一点、self hostであってもTelemetryがデフォルトで有効 *3 になっている点に注意です。defaultだとLangfuseが管理するサーバーに利用時の統計データ(ログが何件あるかなど)が送信されてしまいます。Telemetry部分のソースコードも公開されており、コードの内容的にも問題なさそうではありますが、企業で利用する際には気をつけた方が良さそうです。

欲しい機能がそろっているため

「問題の整理」の項で挙げた3つを解決できる機能がそろっていました。

  • 1のソリューション: Datasetsという機能で、入力とそれによって期待する出力のデータをセットで定義、管理でき、SDKで配信もできる
  • 2のソリューション: Datasetsに対応したRunという概念があり、その時点でのアプリにdatasetを通した結果が保存できる
  • 3のソリューション: Runに対応したScoreという概念があり、valueという抽象的な数値のinterfaceで定義できる。これにより、単純な文字列比較からLLMでの評価まで、自前の評価用関数の結果を格納できる

3について、手動での比較を完全になくせるわけではないと思っていますが、自前の関数での評価で一定対応できると考えています。結果の良しあしが言語化できれば、評価が曖昧になってしまう機能であっても、LLMを用いた評価で対応できると考えたためです。
LLMを用いた評価は他の記事でも多く紹介されており、要約などの曖昧な表現の評価として有用な手段だと考えています。次のような論文でも言及されています。

その他

そのほかにも、権限制御が簡単に行えたり、SSOでのログインが可能だったり、データの設計がシンプルなので他ツールとのつなぎこみが容易という点もありました。この辺は今後運用していく中で知見などがあれば記事にします。

他のツールとの比較に当たって

最初にLangSmithを検討しましたが、データを基本的にLangSmith Cloudにおくことが前提となり、それを避けるためにself hostにする場合はEnterpriseプランに入らなければならなかったことが主な理由で不採用としました。他にも、Enterpriseプランが高額なこと、モニタリングなどは現在必要ないためにオーバースペックであるという点も理由の一つです。
必要な機能は簡素であるために全て自前で評価用アプリを開発することも検討しましたが、Langfuseがマッチしたために行いませんでした。
検討したツールは以下です。評価用のツールだけでもいろいろあるんだなと思いましたが、コアなコンセプトは似ている印象がありました。いろいろツールはあれど一番重要なのはDatasetや評価指標をちゃんと定義して運用していくことで、それができていれば他のツールに乗り換えることも可能だと考えています。

Langfuseの機能紹介

最後に、Langfuseで使っている機能について簡単に紹介します。主に3点で、検証用データの管理、実行の管理、評価の管理です。
架空の機能ですが、「自然言語を検索クエリに変換する」というタスクについての検証用データの作成から実行、評価の管理まで紹介します。

1. 検証用データの準備(Datasets)

InputとExpected Outputをセットで定義して格納します。それぞれ構造化データとして登録できます。このデータはそのまま入力に渡す必要があるわけではなく、評価時に参照したいデータなどもExpected Outputに含めておいて、自前の評価関数などから参照できるため、比較的自由に定義できます。

langfuse = Langfuse(
  secret_key=environ.get("LANGFUSE_SECRET_KEY"),
  public_key=environ.get("LANGFUSE_PUBLIC_KEY"),
  host=environ.get("LANGFUSE_BASEURL"),
  release="v0.0.0",
  timeout=10
)

langfuse.create_dataset("convert_to_search_params")
for row in input_rows:
    input_data = DatasetInput(input=row.input, current_date=row.current_date)
    expected_output = DatasetExpectedOutput(
        title=row.outputs_title, 
        company_name=row.outputs_company_name,
        validity_end_at_from=row.validity_end_at_from,
        validity_end_at_to=row.validity_end_at_to,
        validity_start_at_from=row.validity_start_at_from,
        validity_start_at_to=row.validity_start_at_to,
    )
    
    langfuse.create_dataset_item("convert_to_search_params", input_data, expected_output)
Langfuse - Datasets

2. 実行の作成(Traces)

作成したDatasets をLLMに通した結果をLangfuseに格納してみます。主にTrace - Span - Generationの概念があり、次のコードのような処理で作成が行えます。画面では、それぞれでかかった時間、コスト、アウトプットなどを確認できます。コストの算出は、generationを作成する際に指定したモデルとoutputに渡したtoken数から計算していると思われます。
Promptの作成を飛ばしてしまっていますが、Langfuseの画面でPromptを作成・管理することも可能です。versioning・配信・Traceとのひも付けも行えるので便利です。

# データセットの取得
dataset = langfuse.get_dataset(dataset_name)

client = AzureOpenAI()
# 実行IDの生成
experimentId = f"test-{uuid.uuid4()}"

for item in dataset.items:
    datasetInput = DatasetInput.model_validate(item.input)
    datasetOutput = DatasetExpectedOutput.model_validate(item.expected_output)

    # Trace (実行単位) の開始
    trace = langfuse.trace(
        name="convert-to-search-params-session",
        input=datasetInput,
        metadata={},
        tags=["production"],
    )

    # Span (処理単位) の開始
    span = trace.span(
        name="convert-to-search-params",
        input=datasetInput,
    )

    # Langfuse で管理しているプロンプトの取得
    prompt = langfuse.get_prompt(prompt_name)

    # Generation (LLM 呼び出し用のSpan的なもの) の開始
    generation = span.generation(
        name="convert-to-search-params-chat-completion",
        prompt=prompt,
        model="gpt-35-turbo",
        modelParameters={
            "temperature": 0.0,
        },
        input=prompt.compile(user_input=datasetInput.input),
    )

    # OpenAI APIでChat Completionの取得
    chatCompletionRes = client.chat.completions.create(
        model="gpt-35-turbo",
        messages=prompt.compile(user_input=datasetInput.input),
        temperature=0.0,
    )

    # Generationの終了
    generation.end(
        output=chatCompletionRes,
    )

    completionContent = chatCompletionRes.choices[0].message.content

    # Spanの終了
    span.end(output=completionContent)

    # Traceの終了
    trace.update(output=completionContent)
Langfuse - Traces

3.評価の管理(Datasets - Score)

最後に、Spanの評価と対応するDatasetとのひも付けです。どのデータセット、モデル、処理でどんな結果だったのかをひも付けて管理するために、それぞれのデータをつなげます。

completionContent = chatCompletionRes.choices[0].message.content

# datasetのitemにlink関数が生えているため、それを利用してspanとdataset - Runをひも付ける
item.link(span, experimentId, {
    "description": "テスト",
})

# 自前の評価関数 (正しい JSON 文字列かどうかを判定)
valid_json_score = evaluate_convert_to_search_params_valid_json(completionContent)
span.score(
    name=valid_json_score.get("name"),
    value=valid_json_score.get("value"),
)
# 自前の評価関数 (期待する値と一致しているかどうかを判定)
exact_value_score = evaluate_convert_to_search_params_exact_value(
    output=completionContent,
    expected_output=datasetOutput,
)
span.score(
    name=exact_value_score.get("name"),
    value=exact_value_score.get("value"),
)
Langfuse - Datasets - Score

span.scoreには{ name: string, value: number }で返せればなんでも格納できるので、文字列一致数やLLMでのスコアリングなども計算して記録できます。
今回は、valid-json(正しいJSON文字列かどうか),exact-value(期待したプロパティと一致するかどうか)の2つをスコアリングしてみました。
Runの詳細からSpanを見たり、Promptを見たりなどで、実行の過程もいい感じに見られます。

これで、自前で用意したデータがどの時点でどんな結果だったのかがいつでもわかるようになりました。処理をアップデートした時にデグレしていないかどうか、新しい別のモデルに通した時にどういう結果になるのか、など、評価を効率的に実施していけそうです。

終わりに

Contract OneでのLLMOpsの取り組みについて紹介しました。まだツールは本導入できていませんが、小さく初めて、最終的には開発者・研究者・PdM・法務などが効率的にコラボレーションしながらContract OneのAI機能を成長させていける環境を作れればと思っています。

© Sansan, Inc.