🐙

Gemini 1.5 のロングコンテキストを活かして AI を育てるアプローチ 〜 RAG の限界を軽やかに突破するために

2024/07/26に公開

はじめに

この記事では、Gemini 1.5 のロングコンテキストを活かして LLM を用いた AI システムを段階的に育てるアプローチを説明します。後半では、RAG システムの導入ハードルを下げるためにこのアプローチを適用するイメージをサンプルコードとあわせて紹介します。

ここではまず、前提知識となるグラウンディングや RAG の仕組みを説明します。

グラウンディングと RAG の違いについて

LLM の業務活用に向けて勉強していると、かならず耳にするのが「グラウンディング」や「RAG」というキーワードです。グラウンディングは、LLM の基盤モデル自身が保有していない(学習していない)追加の参考情報をプロンプトに埋め込む事で、参考情報に基づいた回答を生成させるテクニックです。次の図のようなプロンプトを想像するとわかりやすいでしょう。


プロンプトに参考情報を埋め込む例

——「ん?これは RAG とは違うの?」と思ったあなたのために、くわしく説明しておきましょう。

この図では、どのような質問に対しても同じ参考情報を利用して回答することが想定されています。このように、特定の情報に基づいて回答を生成することは、あくまでもグラウンディングであり、RAG ではありません。言い換えれば、与えられた参考情報と無関係な質問は、回答の範疇外ということになります。

一方、より広い範囲の質問に答えられるように、「質問の内容に応じて、必要な参考情報を探し出して用意する」というアプローチも考えられます。この場合は、質問をプロンプトに入力する前に、まず、「質問の回答の参考になりそうな情報を検索して探し出す」というステップが必要になります。そして、得られた情報をさきほどの図のように、質問とセットでプロンプトに埋め込んで、回答を生成します。このように、「検索」と「グラウンディング」を組み合わせた仕組みRAG(Retrieval-Augmented Generation)と呼ばれます。


グラウンディング、検索、RAG の関係

したがって、RAG システムを実現するには、次の処理を行う仕組みが必要になります。

  • 利用者が質問を入力する
     ↓
  • 質問の回答に必要な情報を検索する
     ↓
  • 検索で得られた情報と質問を LLM に入力して回答を生成する

この際、「質問の回答に必要な情報を検索する」という部分は、LLM とは別に独立したシステムとして用意することになります。そのため、独自の技術を使った新しい検索システムを構築する事で、「自分だけの RAG システムを作って悦に入る」というエンジニアならではの高度な趣味を楽しむことも可能です。

RAG システム構築の難しさ

「RAG システムの構築を趣味として楽しむ?なにそれ?」と思った方は、インターネット検索の歴史に思いを馳せてください。—— カテゴリーごとに人力で編纂したリンク集を提供するディレクトリサービスからはじまり、キーワードにマッチする文書を探し出すキーワード検索、そして、マッチした文書の中からより重要な情報を選択するランキングシステムの導入、さらには、検索文の「意味」を理解して適切な情報を選び出すセマンティック検索とさまざま技術が開発されてきました。また、大量の情報を高速に検索するためのインデックス技術や分散処理技術など、「検索システム」の構築は、情報処理技術の総合格闘技とも言える世界です。商用の検索システムには及ばずとも、自分なりの工夫で新しい検索システムを作ることは、エンジニアとして十分に挑戦しがいのあるテーマと言えるでしょう。

ただし、インターネット検索と RAG のための検索システムは、目的が異なる点に注意してください。RAG システムを構築する際は、そのシステムの利用目的(ユースケース)によって、検索対象のデータが変わります。多くの場合、検索対象とするデータを収集・保存したデータベースを構築するところから作業がはじまります。


インターネット検索と RAG における検索の違い

そして、「質問の回答に役立つ情報」を探し出すことが RAG における検索の役割ですので、想定される質問に応じてどのような情報をどのような形態で保存するかなど、工夫やチューニングのポイントはたくさんあります。最近よく耳にする「ベクトル検索」は、RAG の検索システムでも用いられる検索技術の1つです。—— このように、検索システム構築の難しさが、RAG システム構築の難しさに直結することになります。

苦労をしたくない場合のアプローチ

当然ながら、「いや、俺/私はそんな苦労はしたくないんだ。業務に役立つ RAG システムを手っ取り早く導入したいだけなんだ…」という方もいるでしょう。そのような際は、RAG に対応した検索システムのマネージドサービスを利用するという選択もあります。次の記事では、RAG 用のチャンク取得を実行する例も紹介しているので、参考になるでしょう。

ただし、検索システムそのものはマネージドサービスで用意できても、そこに蓄積する情報を集める仕事は残ります。業務用途を考えた場合、社内の各所にちらばった情報をどうやって集めて検索システムに貯めていけばよいのでしょうか? そもそも文書化されていない、担当者だけが知っている「暗黙知」はどうすればよいのでしょうか? —— 悩みはつきなさそうです。

このような時に参考となるのが、「Human work」→「AI Assisted」→「AI Automation」という 3 段階で AI を育てる発想です。

Many-shot learning の例で考える AI の育て方

ここでは、一旦 RAG システムを離れて、Many-shot learning を例に「AI の育て方」を説明します。

Few-shot learning とは?

Many-shot learning を説明する準備として、まずは、Few-shot learning を復習しておきましょう。

あくまで架空の例ですが、EC サイトの商品レビューコメントをポジティブ/ネガティブに分類する業務を考えます。ポジティブなコメントを目につくところに表示して宣伝効果を高めるといった目的を想定した作業です。現状は専任の担当者が人力で分類を行っていますが、この担当者は、LLM を活用して自動化することを考えているようです。次の図のように、プロンプトにレビューコメントを入力して分類を依頼すると、分類結果とその理由が返ってくると期待していましたが…


理想の AI Automation の世界

残念ながら期待通りにはいかないようです。図の例のレビューコメントは、さきほどの目的を考えると担当者的にはポジティブに分類するべきですが、実際には「ポジティブとネガティブな要素が混ざっています」とあくまで客観的な結果が返ります。


「暗黙知」を理解しない AI

これは、担当者独自の判断基準、すなわち、「暗黙知」を LLM が理解しないことに起因します。「プログラムは思った通りには動かない。書いた通りに動く」というソフトウェアエンジニア業界の格言がありますが、AI の世界も変わりありません。LLM には、担当者の気持ちを読み取るエスパー能力はありませんので、何らかの方法で暗黙知を伝える必要があります。

このような時に用いるテクニックの1つが Few-shot learning です。「入力内容」と「期待する出力内容」の具体例をいくつか用意してプロンプトに埋め込んでおくと、LLM はこれらの例を参考にして、担当者が期待する判断基準を類推します。さきほどの例であれば、次の図のような具体例を与えるとよさそうです。


Few-shot learning による対応例

Few-shot learning から Many-shot learning へ

ただし、この担当者が扱う膨大な量と種類のレビューコメントを考えると、この例だけではあらゆるパターンに対応することは難しくなります。担当者の暗黙知を反映した、より多くの例を追加していく必要があります。

幸いなことに、Gemini 1.5 Pro / Flash はロングコンテキストに対応しており、それぞれ、2M、および、1M トークンという膨大な量のテキストをプロンプトに入力できるので、あらゆるパターンに対応するための膨大な例を詰め込むことも可能です。このように、具体例の数をどんどん増やすことで、より多くのパターンに対応させるアプローチが Many-shot learning です。

特に、具体例として入力する内容(さきほどの図の [examples] 以降に相当する部分)は固定的な内容なので、Gemini API の Context caching を利用してキャッシュに保存しておくという使い方も可能です。

AI Assisted を経由した AI Automation の実現

とは言え…、Many-shot learning の場合でも、RAG システム構築と同様に、データ収集のプロセスは残ります。さきほどのレビューコメントの分類業務であれば、現状行っている日々の分類作業の結果をログに記録して、Many-shot learning の例に追加していく方法が考えられます。しかし、この場合でも、「どれだけの例を追加すれば十分なのか?」「どのような例がより効果的なのか?」と言った検証が必要です。データ収集と結果検証の長い日々を過ごさない限り、AI の恩恵は受けられないのでしょうか?

ここには、「理想の AI Automation を実現しないと役に立たない」という思い込みの落とし穴があります。

この例であれば、次の図のステップで活用を進める方法があります。


AI Assisted を経由した AI Automation の実現

担当者の夢は「すべてを AI にまかせる完全自動化」ですが、その前のステップとして、「AI の出力を参考に人間が判定する」という使い方が考えられます。Many-shot learning に使用するデータが不十分な段階では、AI の判定は必ずしも正しいとは限りません。それでも、「AI の判定を見て、間違っていれば修正する」という作業に置き換えれば、レビューコメントを読み込んで自力で判断する既存の作業よりは、作業負荷は軽減されるでしょう。

また、AI の判定が誤っていた場合は、担当者が考える正しい判定結果を新しいデータとして追加することで、AI の判定を改善することができます。これは担当者の「暗黙知」をデータ化して「形式知」に変換するプロセスに他なりません。上の図では、このステップを「AI Assisted」と書いていますが、人間からのフィードバックで AI を改善するという側面も考えると「AI-Human Collaboration」と言ってもよいでしょう。

また、この段階の仕組みを実現するのは、技術的にもそれほど難しいものではありません。新しいデータを追加すると言っても、システム的にはプロンプトに入力するテキストを変更するだけですので、簡単な仕組みで自動化が可能です。やる気のある担当者であれば、さまざまなデータを追加して、結果がどう変化するかを自主的に検証することもできるでしょう。

「やってみないとわからない」のが生成 AI をはじめとする先端技術の世界です。「こうすればうまくいくはず」という思い込みで複雑なシステムを設計・構築する前に、まずは、いろいろなアイデアを手早く試せる簡単な仕組みを見つけ出すことが成功の鍵と言えます。その点において、Gemini 1.5 Pro / Flash のロングコンテキスト対応は、いろいろな使い所がありそうです。

そして、良質なデータが十分に集まって、すべてを AI に任せられる「AI Automation」(もしくは、それに近い段階)に到達した段階で、システム全体の設計を見直して、より高度なチューニングへと進むとよいでしょう。

RAG システムの限界突破に向けて

ここまでに説明した手法は、RAG システムの構築で苦労しているケースでも参考になるかも知れません。「質問内容に応じた適切な情報を検索することで、幅広い質問に対応すること」が RAG の目的です。とは言え、あらゆる質問に完璧に回答する「理想の AI Automation」をはじめから求めているとうまくいきません。まずは、質問の種類を限定して、それに必要なデータを収集するところから始めるとよいでしょう。

さらに、質問の種類が限定的であれば、データを検索するシステムをわざわざ用意しなくても、集めたデータをすべてプロンプトに入力するという大胆なアプローチも可能です。これは、前述の「いろいろなアイデアを手早く試せる簡単な仕組み」にあたります。RAG システム構築の難所であるデータ検索システムを持つことなく、データ収集や暗黙知のデータ化を進めることが可能になります。たとえば、質問の回答が間違っていれば、「何が間違っているのか」「期待する回答は何だったのか」というフィードバックをもらって、その情報をさらにプロンプトに追加するといった方法が考えられるでしょう。

良質なデータが十分に集まった段階で、プロンプトに入力するデータを整理したり、質問ごとに必要な情報だけを取り出す検索システムとの連携をあらためて検討すれば、「検索精度が悪いのか、データが足りないのか、それとも、データの質がよくないのか」という RAG システムのチューニングに伴う苦労を軽減することもできるでしょう。

サンプルコードで理解を深める

ここからは、「RAG の限界突破」に向けた前述のアプローチを適用するイメージを具体化するために、公開データを利用して、「ロングコンテキストにデータを詰め込むアプローチ」を実際に行うサンプルコードを紹介します。

事前準備

Vertex AI Workbench のノートブックでサンプルコードを実行していきます。新しいプロジェクトを作成したら、Cloud Shell のコマンド端末を開いて、Vertex AI Workbench を使用するのに必要な API を有効化します。また、英語の公開データを事前に日本語に翻訳して利用するので、翻訳に使用する Translation API も有効化しておきます。

gcloud services enable \
  aiplatform.googleapis.com \
  notebooks.googleapis.com \
  translate.googleapis.com

また、次のコマンドを実行して Vertex AI Service Agent を作成します。これは、Context caching の利用に必要になります。

PROJECT_ID=$(gcloud config list --format 'value(core.project)')
curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  https://us-central1-aiplatform.googleapis.com/v1/projects/$PROJECT_ID/locations/us-central1/endpoints \
  -d ""

そして、次のコマンドで Workbench インスタンスを作成します。

INSTANCE=long-context-poc
PROJECT_ID=$(gcloud config list --format 'value(core.project)')
gcloud workbench instances create $INSTANCE \
  --project=$PROJECT_ID \
  --location=us-central1-a \
  --machine-type=e2-standard-2

クラウドコンソールのナビゲーションメニューから「Vertex AI」→「ワークベンチ」を選択すると、作成したインスタンス long-context-poc があります。インスタンスの起動が完了するのを待って、「JUPYTERLAB を開く」をクリックしたら、「Python 3(ipykernel)」の新規ノートブックを作成します。

この後は、ノートブックのセルでコードを実行していきます。

サンプルデータの用意

ここでは、Hugging Face で公開されている「レシピデータ」を用いて、料理のレシピに関する質問に答える AI エージェントを作成します。公開データを取得するのに必要なパッケージを、および、Translation API を使用するためのパッケージをインストールします。Context caching は Preview 機能のため、google-cloud-aiplatform パッケージを最新版にアップグレードする必要があります。

!pip install --upgrade \
  git+https://github.com/huggingface/transformers.git datasets \
  google-cloud-translate google-cloud-aiplatform

ERROR: pip's dependency resolver does not currently take into... というエラーメッセージが出ることがありますが、これは無視しても問題ありません。インストールしたパッケージを有効にするために、次のコマンドでノートブックのカーネルを再起動します。

import IPython
app = IPython.Application.instance()
_ = app.kernel.do_shutdown(True)

再起動を確認するポップアップが表示されるので、[Ok] をクリックします。

続いて、必要なモジュールをインポートして、Vertex AI のクライアント SDK を初期化します。今回は Context caching の機能を使用するので、preview 版の SDK をインポートして、使用するリージョンは us-central1 を指定します。

import datetime, hashlib, vertexai
from tqdm.notebook import tqdm
from datasets import load_dataset
from google.cloud import translate_v2
from vertexai.preview.generative_models import GenerativeModel, Part
from vertexai.preview import generative_models, caching

vertexai.init(location='us-central1')

テキストを日本語に翻訳する関数 translate() を定義して、動作を確認します。

def translate(text, target='ja'):
    translate_client = translate_v2.Client()
    translation = translate_client.translate(text, target_language=target)
    return translation['translatedText']

print(translate('hello!'))

[実行結果]

こんにちは!

レシピデータをダウンロードして、日本語に翻訳したデータセットを作成します。ここでは、サンプルとして先頭の 2,000 件のデータを利用します。

def load_recipe(recipe, num):
    result = []
    recipe_list = list(zip(recipe['train']['title'],
                           recipe['train']['directions']))[:num]
    for c, item in enumerate(tqdm(recipe_list)):
        title, directions = item
        h = hashlib.shake_128(title.encode()).hexdigest(4)
        title, directions = translate(title), translate(directions)
        result.append(f'[{h}] {title}: {directions}')
    return result

recipe = load_dataset('Shengtao/recipe')
recipe_data = load_recipe(recipe, 2000)

1つのデータは、[8桁のID] 料理名: 調理手順 という形式の1行のテキストです。先頭のデータを表示すると、次のようになっています。

print(recipe_data[0])

[実行結果]

[ea31dd9b] シンプルなマカロニとチーズ: 大きめの鍋に軽く塩を入れた水を沸騰させます。
エルボーマカロニを沸騰したお湯で、時々かき混ぜながら、中まで火が通って歯ごたえの
ある状態になるまで 8 分間茹でます。水を切ります。鍋にバターを入れ、中火で溶かします。
小麦粉、塩、こしょうを加えて滑らかになるまで 5 分間ほどかき混ぜます。バターと小麦粉
の混合物に牛乳をゆっくりと注ぎ、滑らかで泡立つまで 5 分間ほどかき混ぜ続けます。牛乳
の混合物にチェダーチーズを加え、チーズが溶けるまで 2 ~ 4 分間かき混ぜます。マカロニ
をチーズソースに混ぜ込み、ソースに絡めます。

ここでは、実行結果を見やすくするために折り返して表示していますが、実際には1行に繋がったテキストデータです。

ID から対応するレシピを取得する関数 get_recipe() も用意しておきます。

def get_recipe(id):
    for line in recipe_data:
        if line.startswith(f'[{id}]'):
            return line

Context caching の利用

用意したレシピデータをキャッシュに保存します。はじめに、トークン数を確認しておきましょう。

model = GenerativeModel('gemini-1.5-pro-001')
model.count_tokens(recipe_data)

[実行結果]

total_tokens: 376867
total_billable_characters: 595687

約37万トークンであることがわかります。これは、Context caching を利用してコストを抑えることができます。次のコマンドで、このデータをキャッシュに保存して、これを利用するモデルを取得します。ここでは、基盤モデルとして Gemini 1.5 Pro を使用します。

prompt_cached = ['[Recipes]'] + recipe_data
cached_content = caching.CachedContent.create(
    model_name='gemini-1.5-pro-001',
    contents=prompt_cached,
    ttl=datetime.timedelta(minutes=60)
)

model_cached = GenerativeModel.from_cached_content(cached_content=cached_content)

続いて、モデルにプロンプトを入力して応答を得る汎用的な関数 generate() を定義します。

def generate(model, prompt, temperature=0.1, top_p=0.4, show=True):
    responses = model.generate_content(
        prompt,
        generation_config={
            'max_output_tokens': 8192,
            'temperature': temperature,
            'top_p': top_p
        },
        safety_settings={
            generative_models.HarmCategory.HARM_CATEGORY_HATE_SPEECH: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
            generative_models.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
            generative_models.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
            generative_models.HarmCategory.HARM_CATEGORY_HARASSMENT: generative_models.HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
        },
        stream=True,
    )

    result = ''
    for response in responses:
        try:
            if show:
                print(response.text, end='')
            result += response.text
        except:
            break

    return result

レシピ QA エージェントの作成

キャッシュに保存したレシピデータに基づいて、レシピに関する QA を行うエージェントを作成します。と言っても、回答に必要なデータはキャッシュに保存されているので、これを参照して回答するようにプロンプトで指示をするだけです。

具体的には、次のようなプロンプトのテンプレートを使用します。キャッシュに保存したレシピのテキストデータが先頭にあって、その後にこのテンプレートの内容を追記したものが実際に使用されるプロンプトになります。

prompt_template = '''\
{}

[Additional instructions]
{}

-----
You are a data analyst specialized in cooking recipes.
The texts in [Recipes] is a list of recipes for various dishes.
The format of [Recipes] is one recipe per one line where each line consist of "[<ID>] <dish name>: <recipe description>".

[Task]
A. Write an answer to the [Question] that cites individual sources from [Recipes] as comprehensively as possible.
B. Show a list of "[ID] <dish name>" part in the source [Recipe] for each citation used in the output of Task A.

[Condition]
A. Each source is independent and might repeat or contradict content from other sources.
A. The response should be directly supported by the given sources and cited appropriately with a [ID] notation.
A. The response should be as simple as possible. Avoid adding any information that's not explicitly asked.
A. If no relevant source, the entire output is only "情報がありません。"
A. If you can't create an answer satisfying all conditions, the entire output is only "条件を満たす回答はできません。"
A. The answer contains 10 citations at most.
A. You must also follow the [Additional instructions].

[Format instraction]
A. In plain text, no markdowns. Show the answer in Japanese.
A. A dish name in the answer is based on the <dish name> part in the source [Recipes].
B. The list of "[ID] <dish name>" part is a literal copy from the source [Recipes].
B. Avoid adding "[ID] <dish name>" that's not explicitly contained in the answer.

[Example1]
<Question>
小麦粉を使った肉料理の代表例を2つ教えて。
<Answer>
小麦粉を使った肉料理の代表例は、チキンパルメザン[7815c351]、ミートローフ[a9735973]です。

- [7815c351] チキンパルメザン
- [a9735973] ミートローフ

[Example2]
<Question>
ハバネロを使った甘い料理を教えて。
<Answer>
情報がありません。

[Question]
{}
'''

最初の {} の部分には、キャッシュにはない新しいレシピを追加で記載する想定です。また、 [Additional instructions] の部分には、ユーザーからのフィードバックで得られた追加の指示が入ります。これらのデータに基づいて回答を生成した上で、参照したレシピ(IDと料理名)のリストを付与するように指示しています。

ユーザーからの質問をテンプレートに埋め込んで、回答を生成する関数 answer_with_citations() を次のように定義します。

def answer_with_citations(question, new_recipes='', instructions='None'):
    prompt = prompt_template.format(new_recipes, instructions, question)
    response = generate(model_cached, prompt)
    return response

それでは、簡単な質問で動作確認を行いましょう。

question = '''\
桃を使った甘いデザートを1つ教えて。
'''
answer = answer_with_citations(question)

[実行結果]

桃を使った甘いデザートは、ピーチコブラーダンプケーキI[55a9c25f]です。

- [55a9c25f] ピーチコブラーダンプケーキI 

桃を使ったデザートが、レシピの ID と共に得られました。どのような料理なのか具体的に聞いてみましょう。

question = '''\
[55a9c25f] ピーチコブラーダンプケーキI はどのような料理か詳しく説明して。
'''
answer = answer_with_citations(question)

[実行結果]

ピーチコブラーダンプケーキI [55a9c25f]は、オーブンを190℃に予熱し、9 x 13インチのパンの
底に桃を敷き詰め、乾燥したケーキミックスで覆い、バターを細かく切ってケーキミックスの上に
置き、シナモンを振りかけて190℃で45分間焼いた料理です。

- [55a9c25f] ピーチコブラーダンプケーキI 

実際のレシピを表示して、回答があっているか確認します。

print(get_recipe('55a9c25f'))

[実行結果]

[55a9c25f] ピーチコブラーダンプケーキI: オーブンを 375 度 F (190 度 C) に予熱します。
9 x 13 インチのパンの底に桃を空けます。乾燥したケーキミックスで覆い、しっかりと押します。
バターを細かく切り、ケーキミックスの上に置きます。上からシナモンを振りかけます。
375 度 F (190 度 C) で 45 分間焼きます。

問題なさそうです!

新しいレシピを追加する例

既存のレシピにはなさそうな質問をしてみましょう。

question = '''\
朝鮮人参を使ったデザートを1つ教えて。
'''
answer = answer_with_citations(question)

[実行結果]

情報がありません。

与えられたレシピだけに基づいて回答するように指示しているので、これは期待通りの結果と言えます。ですが、朝鮮人参を使ったユニークなデザートのレシピを追加するのは悪いことではありません。このような場合は、関数 answer_with_citations()new_recipes オプションで新しいレシピの情報が追加できます。

new_recipes = '''\
[0b6d8e5a] 朝鮮人参と蜂蜜のプリン: 鍋に牛乳、生クリーム、砂糖、刻んだ朝鮮人参を入れ、\
弱火で加熱します。沸騰直前で火を止め、10分ほど蒸らして朝鮮人参の風味を移します。茶こしで\
朝鮮人参を取り除き、溶き卵と蜂蜜を加えて混ぜ合わせます。耐熱容器に流し入れ、160℃に予熱\
したオーブンで30分湯煎焼きします。粗熱が取れたら冷蔵庫で冷やし固めます。お好みでホイップ\
クリームやフルーツを添えてお召し上がりください。
'''

question = '''\
朝鮮人参を使ったデザートを1つ教えて。
'''
answer = answer_with_citations(question, new_recipes=new_recipes)

[実行結果]

朝鮮人参を使ったデザートは、朝鮮人参と蜂蜜のプリン[0b6d8e5a]です。

- [0b6d8e5a] 朝鮮人参と蜂蜜のプリン 

今回は、追加のレシピを参照して、朝鮮人参を使ったデザートの情報が得られました。次のように、複数のレシピを簡単に追加していけます。

new_recipes = '''\
[0b6d8e5a] 朝鮮人参と蜂蜜のプリン: 鍋に牛乳、生クリーム、砂糖、刻んだ朝鮮人参を入れ、\
弱火で加熱します。沸騰直前で火を止め、10分ほど蒸らして朝鮮人参の風味を移します。茶こしで\
朝鮮人参を取り除き、溶き卵と蜂蜜を加えて混ぜ合わせます。耐熱容器に流し入れ、160℃に予熱\
したオーブンで30分湯煎焼きします。粗熱が取れたら冷蔵庫で冷やし固めます。お好みでホイップ\
クリームやフルーツを添えてお召し上がりください。
[7f041a32] 大豆ミートの唐揚げ: 大豆ミートをボウルに入れ、ひたひたの水に浸して戻します。\
戻ったら水気をしっかり絞ります。別のボウルに醤油、酒、生姜、ニンニクを混ぜ合わせ、大豆ミート\
を漬け込みます。30分ほど置きます。別のボウルに片栗粉と小麦粉を混ぜ合わせます。漬け込んだ\
大豆ミートに衣をまぶします。170℃の油で揚げます。きつね色になったら油から取り出し、\
油を切ります。お好みでレモンを添えていただきます。'''

question = '''\
肉を使用しない唐揚げのレシピはありますか?肉の代わりの食材は何ですか?
'''
answer = answer_with_citations(question, new_recipes=new_recipes)

[実行結果]

肉を使用しない唐揚げのレシピは、大豆ミートの唐揚げ[7f041a32]です。肉の代わりの食材は
大豆ミートです。

- [7f041a32] 大豆ミートの唐揚げ 

誤った回答をユーザーフィードバックで修正する例

次は、誤った回答が得られる例を探してみます。これはどうでしょうか。

question = '''\
いちごを使ったデザートの代表例を3つ教えて。
'''
answer = answer_with_citations(question)

[実行結果]

いちごを使ったデザートの代表例は、イチゴのショートケーキ[9be2a5ba]、
イチゴプレッツェルサラダ[efffa9b9]、イチゴプレッツェルサラダ[0fa80cf8]です。

- [9be2a5ba] イチゴのショートケーキ
- [efffa9b9] イチゴプレッツェルサラダ
- [0fa80cf8] ジュディのストロベリープレッツェルサラダ 

回答文の中に「イチゴプレッツェルサラダ」が2つありますが、引用を見ると2種類の異なるレシピに対応するようです。でも、これらはサラダであって、デザートではないような気がします。詳しく聞いてみましょう。

question = '''\
イチゴプレッツェルサラダ[efffa9b9]はサラダなのにデザートなのですか?その理由は?
'''
answer = answer_with_citations(question)

[実行結果]

イチゴプレッツェルサラダ[efffa9b9]は、砕いたプレッツェルを砂糖と溶かしバターで固めた
土台の上に、クリームチーズとホイップトッピングを混ぜた層、その上にイチゴの層を重ねて
作ります。デザートのような甘い材料が使われているため、サラダでありながらデザートとして
分類されます。

- [efffa9b9] イチゴプレッツェルサラダ 

これは私の理解が間違っていました。イチゴプレッツェルサラダは、デザートの仲間であっているようです。ちょっと悔しいので、実験として、あえて主観的な判断が入る質問をしてみます。

question = '''\
スパイシーな料理を1つ教えて。
'''
answer = answer_with_citations(question)

[実行結果]

スパイシーな料理の例としては、沖縄風パッタイ[9af388d1]があります。

- [9af388d1] 沖縄風パッタイ 

あくまで個人的な意見ですが、「この料理はスパイシーとは言えない。スパイシーな料理として回答して欲しくない」というフィードバックを返すことにします。具体的には、関数 answer_with_citations()instructions オプションに条件を追加します。これは、プロンプトテンプレートの [Additional instructions] パートに追加されます。

instructions = '''\
[9af388d1] 沖縄風パッタイ はスパイシーな料理として回答しないでください。
'''
question = '''\
スパイシーな料理を1つ教えて。
'''
answer = answer_with_citations(question, instructions= instructions)

[実行結果]

スパイシーな料理の例としては、チャナマサラ[a2d5275c]があります。

- [a2d5275c] チャナマサラ 

フィードバックを反映して、回答が変化したことがわかります。このほかにもフィードバックがある時は、次のように加えていくことができます。

instructions = '''\
[9af388d1] 沖縄風パッタイ はスパイシーな料理として回答しないでください。
[9af388d1] 沖縄風パッタイ は暑い日におすすめの料理として回答してください。
'''
question = '''\
暑い日におすすめの料理をいくつか見繕ってください。
'''
answer = answer_with_citations(question, instructions=instructions)

[実行結果]

暑い日におすすめの料理は、沖縄風パッタイ[9af388d1]、タコスレタスラップ[645dfb76]、
自家製レモネード[30c15764]です。

- [9af388d1] 沖縄風パッタイ
- [645dfb76] タコスレタスラップ
- [30c15764] 自家製レモネード 

これらの例から、Gemini 1.5 Pro / Flash のロングコンテキストを活用して、プロンプトにさまざまな情報を追加する使い方のイメージがつかめたと思います。さきほどのプロンプトのテンプレートはあくまで一例ですので、それぞれのユースケースにおいて、実際の利用形態にあった内容を検討するとよいでしょう。

検証が終わったら、次のコマンドでプロジェクトで使用している Context cache の内容をまとめて削除することができます。

for cached_content in caching.CachedContent.list():
    cached_content.delete()

まとめ

この記事では、Gemini 1.5 のロングコンテキストを活かして LLM を用いた AI システムを段階的に育てるアプローチを説明しました。

AI を活用する際は、用途に応じて適切なデータを集めることが大切です。とは言え、LLM と組み合わせた利用では、そもそもどのようなデータが適切なのかという点で常に試行錯誤が必要です。実際のユースケースに近い内容のテストができる環境を手早く用意すること、そして、AI による恩恵を受けながら適切なデータを効率的に収集できる「AI Assisted」のステップを短期間で実現すること —— これらの点で、ロングコンテキストの活用ポイントがいろいろありそうです。RAG システムの構築に時間がかかって苦労している際は、ロングコンテキストを活用したアプローチもぜひ検討してください。

最後にせっかくなので、今回作成したレシピ QA エージェントに今夜のディナーを提案してもらいましょう。

question = '''\
暑い夏のディナーにおすすめのメイン料理とアルコール飲料をセットで提案して。
理由もあわせて教えてください。
'''
answer = answer_with_citations(question)

[実行結果]

暑い夏のディナーにおすすめのメイン料理とアルコール飲料のセットは、チキンティノーラ[c1e9fe5c]
とモヒート[ca34d0e3]です。チキンティノーラは、鶏肉をショウガや魚醤などで煮込んだ料理なので、
食欲のない時でも食べやすいでしょう。また、チンゲン菜やほうれん草などの野菜も入っているので
栄養満点です[c1e9fe5c]。モヒートは、ラム酒にライムやミントを加えたカクテルなので、暑い夏に
ぴったりの爽やかな飲み物です[ca34d0e3]。

- [c1e9fe5c] チキンティノーラ
- [ca34d0e3] モヒート 

なるほど!

Google Cloud Japan

Discussion