こんにちは。ファンと共に時代を進める、Web3スタートアップ Gaudiy のエンジニアの Namiki ( @ruwatana ) です。
ここ1〜2年くらいで、生成AI / LLM界隈の盛り上がりは非常に加速してきており、それをいかに活用して新たな価値を提供するかということに集中している方も少なくないことかと思います。
弊社Gaudiyも比較的早期からこの分野に可能性を見出し、積極的に挑戦してきました。そんなLLMプロダクト開発を行なっていく中で、発生した課題に対して蓄積されたナレッジを活かして日々改善できるよう昇華しています。
今回はこの分野の開発に切っても切れないプロンプトチューニングの業務プロセスにフォーカスし、よく起こりうるであろう課題に対してどのように効率化・解消していっているのか、その一端をユースケースとともにご紹介できればと思います。
※なお、本稿は「技術選定/OSS編」と「開発プロセス/検証編」の2部構成となっており、後編の内容となります。
前編の seya さんの記事も合わせてお読みいただけますと幸いです 🙏
Gaudiyが取り組むLLMプロダクト
Gaudiyでは、複数のIP(知的財産)に関するコミュニティプラットフォーム「Gaudiy Fanlink」を提供しています。
まだベータ版ではあり、あまり詳細をお伝えできないのですが、現在特定のIP様と協業しながらFanlinkに芸能人やアニメキャラといった特定キャラクターのAIアバターとのチャット(以下AIトークと呼びます)を楽しめる機能を絶賛PDCAしながら開発しております。
この機能において、AIのメッセージの生成にはLLMを利用しています。LLMは欠かせない存在であり、LLMがそのクオリティを大きく左右すると言っても過言ではないといえます。
ここで、現段階でのAIトーク機能を簡単にご紹介します。
ユーザーはFanlink上にて、特定キャラクターのふるまいをするAIアバター(以下キャラクターAIとします)のメッセージに対してメッセージを送ることができるようになっており、キャラクターAIはその固有の特徴や記憶などの情報を保持し、常にユーザーからのメッセージや会話の流れに対して適切なふるまいをするようにプロンプトされています。
また、テキストメッセージだけでなく、デジタルアイテムのギフトを渡すなどの付加的な体験も提供しており、今後音声などのマルチモーダルなインプットを与えることができるような機能も検討しています。
プロンプトには、ユーザーからのメッセージに対して返信をするものだけでなく、会話のし始めの話題提起、自己紹介、会話の流れを終了させるものなど、非常にさまざまなシチュエーションが存在し、さまざまなコンテキストによって使用するプロンプトをロジックの中で使い分けるようにしています。これによって、1区切りの会話の開始から終了までをより自然に振る舞ったり、次の話題にシームレスに移行するといったことができるような仕組みを実現しています。
次に、アーキテクチャを簡単にご紹介します。
ユーザーからのメッセージが送信されてくると、リプライ生成キュー(Cloud Pub/Subを利用しています)にタスクをスタックし、処理が開始されるとどのキャラクターAIの情報なのかを表す一意のIDをもとにキャラクターの特徴が格納されているDBから情報を取得します。
※ちなみにキャラクターAI自体はそれぞれ個別に複数人を構築できる構成を取っており、現在は実際に2名のキャラクターAIを運用して検証しています
さらに、Vector Store(Vertex AIを利用しています)に事前に格納されたベースとなるキャラクターAIの記憶データやユーザーとキャラクターAIの過去の会話サマリー情報をベクトル検索によって、ユーザーからのメッセージクエリなどから近傍する情報を取り出します(RAGベース)。
そして、会話のし始めや何回目の会話なのかといったコンテキスト情報をもとに使用するプロンプトのテンプレートを決定します。上記で得たキャラクター情報と関連記憶の情報といったさまざまなデータの中からプロンプトに必要なインプットのみを抽出し、LangChainのインターフェースを用いてLLMをinvokeしてメッセージを生成します。
この手法をとることで、一つのプロンプトテンプレートにおいて複数のキャラクターAIの振る舞いを実現することを可能にしています。
また、非常にシンプルなタスクなどはシングルプロンプトで構成されていますが、より複雑な文脈の認識や低コスト化などを目的として、LangGraphを用いてマルチステップにプロンプトをチェーン実行するようなパターンも存在します(この辺りの詳しい技術内容は今回は割愛します)。
こうしたLLMの実行はLangChainより提供されるLangSmithにて、GUI上でトレーシングされた結果を閲覧できるので、ハルシネーションを起こした際などに非常に簡単にデバッグができるような機構を実現しています。
プロンプトチューニング業務で直面した課題
前述したように、キャラクターAIとのトーク機能の実現には複数のプロンプトを使用しており、その一つ一つの精度を上げるためのプロンプトチューニングの重要性は非常に高くなっていました。
しかし、我々はプロンプトチューニングの効率的なやり方を確立できておらず、場当たり的に行なってしまうなどコストが膨らみ、さまざまな課題を抱えていました。
最も大きな課題は、チューニングを担当するエンジニアとステークホルダーとなる弊社Bizメンバーとの間の共通言語がアプリケーションの最終挙動をもとにした定性結果のみで、抽象的な要求を満たしていない理由をエンジニアが実装レベルまで分解・分析するためのコストやロスが大きく生じていたことでした。
従来は、一通りの機能が実装完了した後、作成したAIの特徴などをよく知っていてIPとも密に連携している弊社のBizメンバーが実質のステークホルダーとなってクオリティーのチェックを依頼するような運用体制を取っていました。 チェック後の結果の観点リストは、キャラクターAIのベースとなっている対象の話口調などの特徴を満たしているかという無限大にも広がる観点からなる項目が定性的にまとまっている状態でした。
クオリティーチェックの結果としてはもちろん正しいのですが、その一個一個の定性的な観点をエンジニアが読み取って全てを満たしながら(時には優先度立てて)プロンプトという実装に落とし込む作業の負荷は非常に高い状態でした。
終わりも完了条件も見えにくいこの作業において、作業自体が自然消滅したり、満足にチューニングされずに開発が進んでしまうということも起きました。しまいには、LLMモデルを廉価モデルに変更したせいで品質が低下したなどという全く根拠のない結論づけをされてしまうこともありました。(これではエンジニアは困ってしまいます・・・)
プロンプトチューニングをしていると、こっちを立てればあっちが立たずなどの問題は往々にして生じます。最終的にはどれがマストで実現しないといけない要件なのかといった判断は逐一質問をしなければいけないといったコミュニケーションコストも大きくなっていました。
こういったプロンプトの動作をLangSmithのPlaygroundを使って確認・チューニングすることもありましたが、この場合履歴なども残らず試行自体も手動で逐一行わなければならないため非常にコストがかかっていました。
また、当該のプロンプトにどのLLMモデルが最適なのかをモデル間で比較する際にも、このやり方では非常に大きなコストがかかってしまうといえるでしょう。
プロンプトチューニングの効率化
検討フェーズ
AIチームも発足しこの分野に詳しいメンバーも増えつつありましたが、AIチームではR&Dがメインでこうした泥臭いプロセス課題に関してはほとんどアプローチできていない現状でした。プロダクト開発チーム側の自分としてはここに大きな課題感を持ったため、まずはこれを解消するために以下が最低限実現できると良いと考えました。
- 複数の想定インプットとその正解の一例となる期待値を記したデータセットを何らかのドキュメントで事前作成する
- 用意したデータセットに対して自動でLLM実行ができる環境を用意し実行結果を自動生成できる
- チューニング担当(エンジニア)とステークホルダー(Biz)が実行結果をもとに改善のための会話をできる共通言語となる永続データを用意する
- 任意のLLMモデルへの差し替えや結果の比較が簡単にできる
実現フェーズ
まず、複数の想定インプットとその正解ケースからなるデータセットをGoogleスプレッドシートにて作成することを決めました。 採用理由としては、オンラインでリアルタイムに複数人が編集可能であることと、シート/セル参照を使うことで共通定義の変更を簡単にデータセットに一括反映できるスプレッドシートの特性がマッチしていると考えたからです。
スプレッドシートの構成は、以下のような形にしました。
- キャラクターAIの特徴情報自体のチューニングもしやすいようにDBのデータを複製した共通定義シート
- 各プロンプトごとに必要なインプットとアウトプットからなるケースを1行にまとめそれを必要なパターン分作成したシート(複数)
各プロンプトのシートにはどのキャラクターAIに関するチューニングケースなのかを設定できるセレクトボックスや、その特徴が共通定義シート側からセル参照で動的に入るようにセルの値に数式を用いてauto fillする構成にしました。
それ以外のケースごとに必要なユーザーからのメッセージ入力やOutputとなる期待値などはBizメンバーにパターン網羅されるように作成依頼しました。
続いて、LLM実行と実行結果の抽出の効率化ですが、ここの技術知見がほとんどなかったのでこれから掘ろうと思った矢先に、AIチームより連携を買って出てくださり、課題感と実現したい世界観を改めてsyncすると、LangSmithのDatasets & Testing 機能を用いてデータセットごとの一括実行や実行検証結果をLangSmith上に永続化できる仕組みを提案してくださいました!
LangSmithのDatasets & Testing機能を効果的に使うためのAPIはLangChainより公開されているため、これらを我々のユースケースで利用できるようにラップしてあげることで、要望を実現できました。
※このHelperは、現在OSSライブラリとして公開もしています。詳しい内容に関しては、下記のリポジトリや冒頭で紹介した seya さんの記事をお読みいただければと思います。
ここからは簡単に全体の実現フローを図示し、Helperの使い方とともにプロンプトのEvaluationを効率的に行うための実装の一部を示します(現在のHelper OSSのインターフェースとは少々異なる可能性もあるのでご了承ください)。
まず、Googleスプレッドシートの情報を読み込み、LangSmithのDatasetsのExampleの形式に変換・登録するためのPythonスクリプト(dataset.py)を実装し実行します。
読み込みには、普段Gaudiyがインフラとしても利用しているGCPの中で提供されているGoogle Sheet APIを利用しました。 スクリプト上からGoogle Sheet APIを利用するためには、事前に作ったサービスアカウントに対して、読み込みたいスプレッドシート側の共有設定で閲覧権限を付与することで実現ができました(権限の割り当て方がIAMとは異なるため注意です)。
読み込んだdictionaryデータを必要に応じてマッピングや整形処理を行い、Helperを呼び出してLangSmith上の任意のDataset Exampleに登録/上書きします。
# LangSmith Dataset Name LANGSMITH_DATASET_NAME = "Test Dataset" # Google Spreadsheet ID SPREADSHEET_ID = "hogehoge" # Google Spreadsheet name SHEET_RANGE = "test_prompt_sheet" # path to SA json for Google Sheet API SERVICE_ACCOUNT_KEY_PATH = os.getenv("SERVICE_ACCOUNT_KEY_PATH") async def dataset() -> None: # fetch Spreadsheet values = get_sheet_values(SHEET_RANGE) # map and create input data header_row = values[0] header_columns_count = len(header_row) data_rows = values[1:] dict_list: list[dict[str, Any]] = [] for row in data_rows: name = row_dict.get("name", "") bio = row_dict.get("bio", "") attitude = row_dict.get("attitude", "") last_message = row_dict.get("last_message", "") output = row_dict.get("output", "") dict_list.append({ **TestPromptInput( name=name, bio=bio, attitude=attitude, last_message=last_message, ).model_dump(), "output": output, }) # Call helper library create_examples(dict_list, dataset_name=LANGSMITH_DATASET_NAME) def get_sheet_values(sheet_range: str) -> list[Any]: credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_KEY_PATH, scopes=["https://www.googleapis.com/auth/spreadsheets.readonly"], ) service = build("sheets", "v4", credentials=credentials) sheet = service.spreadsheets() result = sheet.values().get(spreadsheetId=SPREADSHEET_ID, range=sheet_range).execute() values = result.get("values") return values if __name__ == "__main__": asyncio.run(dataset())
このスクリプトを実行 ( python -m dataset.py ) することで、任意のDatasetのExampleにスプレッドシートで定義したインプットとアウトプットが全て反映できるようになりました。
次に、Helperを用いたExperimentに必要なコンフィグを定義するYAMLファイル(config.yaml)に、実行したいDatasets、LLMモデル、利用したいbuiltinのチェック関数などの指定を記述します。
description: test prompt prompt: name: prompt.py type: python entry_function: prompt evaluators_file_path: evaluations.py providers: - id: TURBO tests: type: langsmith_db dataset_name: Test Dataset experiment_prefix: test_
ちなみに、カスタムでチェックを施したい場合は、evaluations.pyに評価ロジックを書いてコンフィグでそれを指定することでその評価を回すことも簡単にできます。 例えば正負を含むスコアを返すプロンプトの正負の精度判定用のチェック関数などを独自に実装して評価に用いることができました。
def same_sign(run: Run, example: Example) -> dict[str, bool]: if run.outputs is None or example.outputs is None or run.outputs.get("output") is None: return {"score": False} example_output = example.outputs.get("output") if example_output is None: raise ValueError("example.outputs is None") example_score = int(example_output) output_score = int(run.outputs["output"].score) if example_score == 0 or output_score == 0: return {"score": example_score == output_score} example_sign = example_score // abs(example_score) output_sign = output_score // abs(output_score) return {"score": example_sign == output_sign}
この辺りは初期要望にはありませんでしたがExperimentsの機能としてこういうものも使えるよとご教示いただきました。 なお、実装したチェック関数のスコア結果はLangSmithのExperiments上でChartで視覚的に表示できるため、これも評価の指標としては非常にわかりやすかったです。
最後に、実際のプロダクション側のプロンプトテンプレート定義を参照して、実行するプロンプトテンプレートの実装を別ファイル(prompt.py)に定義します。(直接プロンプト文をこのファイル上に定義することも可能です)
@traceable async def prompt(inputs: dict[str, Any]) -> TestPromptTemplate: return TestPromptTemplate( input=TestPromptInput( name=inputs.get("name", ""), bio=inputs.get("bio", ""), attitude=inputs.get("attitude", ""), ) )
あとは、configを参照したaliasコマンドを実行すると、登録したデータセットの数だけLLM実行が行われ、実行完了後にLangSmith上のExperimentsに全ての実行結果が保存されます。
make evaluate <path/to/config.yml>
このExperimentsの個々の結果はURLリンクも存在するので、このリンクを共有することでチューニング担当とステークホルダーが実行結果や修正方針に対しての議論が簡単にできるようになりました 🎉
運用フェーズ
実際のプロンプトチューニング業務は、先述の通り、複数のプロンプトが対象だったため、エンジニアメンバーで上記の実装も含めて作業を分担することにしました(基本的に1人1プロンプトずつ担当しました)。
エンジニアがチューニング可能な明らかに期待値に対して間違っているとわかる事象をどのようなロジックで修正を施したかを、プロンプトごとにNotionにまとめ、時系列でチューニングの内容を追えるようにしました。
また、ある程度良い結果が出たタイミングで、OutputとなるLangSmithのExperiments URLをBizメンバーに共有してレビューをもらい、指摘を続けて修正したり、問題なければ完了といったフローで行いました。
結果と考察
従来、何度もプロンプト改善のレビューと修正を繰り返し、LLM開発に精通したエンジニアやPdMが2週間ほどプロンプトチューニングに張り付いていた状況だったり、時にはチューニング自体がおざなりになってしまっていたというよろしくない状況から、プロンプトチューニングの効率化によって、Biz中心に行うスプレッドシートのデータ作成作業を数人日程度、エンジニアが行うDatasetsとEvaluation Scriptの実装を0.5〜1人日程度、チューニングとレビュー実施をプロンプトの複雑度にもよりますが数人日程度で完了できるという実績を得ることができるようになりました(実際チューニング実施期間は1週間程度で完了することができました)。
この実績は、今後同様の作業をやる上での目安としても、かなり良い情報になることが予想されます。
また、従来は特定のメンバーしか行っていなかったこの業務ですが、チームに所属する全エンジニアがこの工程を分担することでプロンプトチューニングのコンテキストやナレッジの習得、相互レビューなどの副次効果もありました。
今まではBizメンバーに開発完了したアプリケーションを実際に触ってもらい、定性的な品質評価をして修正観点をリスト化いただいてましたが、今回はGround truthとなるアウトプットの期待値も事前にデータセットとして登録してもらうことで、チューニング担当側の心理的な負担も大きく軽減することができました。
プロンプトの定義ファイルへのGitリンクなども事前にBizメンバーに渡し、実際のプロンプト文自体やどういう仕組みやどういうシチュエーションで該当のプロンプトが実行されるということを説明し把握いただいた上でチューニングを行いました。LangSmithのExperiments結果からChainのトレースデータも簡単に閲覧できるようになったことで、具体的にこのプロンプトのこの表記をこうしたらいいのではないか?といったような修正方針をBizメンバーからも提案いただけるようになりました。エンジニアからするとこういった詳細観点をいただけるのは本当に助かります。
プロンプトチューニングの作業はクオリティーに直結するため、エンジニアだけで完結することができません。周辺のメンバーも含めてコンテキストに相互に染み出すことが非常に重要であるという気づきも得られました。
今後の課題
我々の効率化は、まだまだかなり粗く、場当たり的な改善となっているのが現状です。
今後も、LLMプロダクト開発を行なっていく上で、さまざまな課題が発生すると考えておりますが、今後の課題と検討しているその改善策についても一例を示しておきます。
特に、今後キャラクターAIを量産していくとなった際には、Bizメンバーの人力コストという面が非常に大きな課題となっているため、次はここにアプローチができればと考えております。
- キャラクターAIを作成するための特徴や記憶データの人力作成コスト
- 生成AIを用いて、人格定義データから自動で特徴や記憶データを生成する
- Outputの期待値となるGround truthデータの人力作成コスト
- 既存のデータセットと人格定義データをもとに新たなAI用の正解データを類推し生成AIによって自動生成する
- プロンプト精度のEvaluationやチューニングを人力で行なっている部分の品質課題やコスト課題
- LLM as a Judgeを用いてAIが評価やチューニングをする仕組みの検討
まとめ
いかがでしたでしょうか?
割と起こりがちなプロンプトチューニングという業務の中で生じる課題に対して、GoogleスプレッドシートとLangSmithのHelperを用いた効率化のアプローチを紹介しました。いかに人力かつ時間のかかる作業を効率化していくか、そこに生成AIという新たな技術を適用して解消していくかという一つの事例を作ることができたのではないかと感じています。
今後も改善を続けていき、LLMプロダクト開発をより良いものにできたらと思います。 以上です、お読みいただきありがとうございました。