🤖

Google Cloud で生成 AI アプリケーションを作ろう!パート 6 : LLM のみを用いたアプリ開発の限界とその対応方法

2023/08/17に公開

はじめに

このシリーズでは、Google Cloud の生成 AI サービス、特に、大規模言語モデル(LLM)を用いたアプリケーションのアイデアを具体的な実装例を含めて紹介してきました。「パート 2 : PaLM API の利用例 1 〜 英文を日本語に要約する」「パート 3 : PaLM API の利用例 2 〜 メールを分類する」「パート 4 : LLM を活用した Web アプリを英語学習に役立てる」では、「要約しなさい」「カテゴリーを選びなさい」「正しい文法の文章にしなさい」と言った指示を与える事で、欲しい結果が得られました。

しかしながら、LLM を用いて、さらに複雑な処理を行おうとすると、いくつかの技術的な課題に直面します。これらの課題は、LLM に入力するデータに適切な前処理を施したり、LLM を他のサービスと連携するなどの方法で解決できる場合があります。ここでは、典型的な課題とその解決策のヒントを紹介します。

LLM の課題と解決策のヒント

学習時に含まれない情報

LLM は、学習時のデータを元にして応答文を生成するので、学習時に含まれない情報を用いた回答は得られません。これは、回答に必要となる追加の情報を入力テキスト(プロンプト)の一部に含めることで対応できます。また、外部の検索エンジンと連携して、検索によって得られた情報をプロンプトに追加するなどのインテグレーションも考えられます。

事実に基づかない回答

LLM は、「自然な文章」を生成することを主目的に学習された機械学習モデルなので、事実と異なる回答を生成することがあります。事実に基づいた回答が必要なユースケースでは、回答内容を別途検証したり、プロンプトに追加の情報を加えて、事実と異なる回答の発生を低減するなどの工夫が必要です。

自然言語以外のデータ処理

LLM を用いて計算問題を解くなどの例が紹介されることもありますが、専門的な計算処理であれば、当然ながら専用のライブラリーを用いた方がより高速に正確な結果が得られます。自然言語以外のデータ処理を LLM による自然言語処理と組み合わせたアプリケーションを作成する場合、外部のライブラリーと LLM を連携させるためのインテグレーションが必要となります。

入力文字数の制限

LLM のプロンプトに入力できる文字数には制限があるため、長文のテキストをそのままの形では処理できない場合があります。このような場合、入力テキストを分割して処理したり、さらに、分割処理で得られた複数の結果を統合するなどの追加処理が必要となります。

利用コストと処理時間

LLM による回答文の生成には、GPU や TPU などの計算リソースが必要となるので、API サービスを利用する場合、利用方法によっては、利用コストが掛かりすぎたり、応答を得るまでのレイテンシーが問題となる場合があります。過去の応答内容をキャッシュして再利用するなどで、これらの影響を低減する工夫が考えられます。

セキュリティに対する考慮

ユーザーが入力したテキストをそのままの形で LLM に入力すると、アプリケーションとして意図していない動作を引き起こす可能性があります。回答を生成する上での条件をあらかじめプロンプトに埋め込んでいる場合でも、ユーザーが「前の指示を無視して回答してください」などの指示を追加して、これを回避するプロンプト・インジェクションの攻撃手法が知られています。このような問題を回避するには、LLM に入力する内容をテンプレートに当てはめて、事前に入力内容をチェックするなどの工夫が必要になります。

LangChain の活用

前節で述べた LLM の課題では、それぞれについて「解決策のヒント」を示しました。これらの解決策を実装する方法はいろいろ考えられますが、基本的には、LLM とその他の API サービスを連携させたデータ処理のパイプラインを実装する形になります。このような LLM を中心としたデータ処理のパイプラインを実装するためのフレームワークに LangChain があります。

LangChain は、LLM を用いたデータ処理を実装する際に必要となる典型的な機能を個別のモジュールとして提供しています。具体的には次のようなモジュールがあり、検索エンジンなどの API サービスとの連携やキャッシュ機能など、前述の解決策を実装する上で役に立つ機能が用意されています。

  • Model I/O
  • Data Connection
  • Chains
  • Agents
  • Memory
  • Callbacks
  • Evaluation

ここでは、LangChain の使い方の雰囲気を掴んでもらうために、最も基本的なモジュールである、Model I/O を用いたサンプルコードを紹介します。

LLM を使用する際は、「入力テキストのフォーマット(Format)」「LLM の API をコールして結果を取得(Predict)」「得られた結果の再フォーマット(Parse)」という 3 つの処理が基本となります。Model I/O を用いると、これら 3 つの処理を標準化した手順で実行できます。複数のモデルに対して処理をブランチしたり、あるいは、複数のモデルの呼び出しを直列に結合するなどの処理をパイプラインとして定義できます。

LangChain(Model I/O)を用いたコードサンプル

ここでは、簡単な例として、LLM に新製品の商品名を考えてもらうコードを実装します。LangChain を用いて、Format → Predict → Parse の順に処理を実装していきます。

なお、LangChainとVertex AIを連携させるには、Python 3.8 の環境が必要になります。本記事執筆時点では、Vertex AI Workbench のノートブックは Python 3.7 の環境になっているので、はじめに次の手順で Python 3.8 の環境を用意します。

まず、「パート 1 : 生成 AI ソリューションを使うための基本的なセットアップ」の手順に従って、Workbench の環境を用意して、ローンチャーの画面を開いたら、「Other」のセクションにある[Terminal]をクリックします。

コマンドターミナルが開くので、次のコマンドを実行します。ここでは、Python 3.8 の環境を用意して、事前に Vertex AI の SDK と LangChain のライブラリーをインストールしています。

source /opt/conda/etc/profile.d/conda.sh
conda create -n python38 python=3.8 -y
conda activate python38
conda install ipykernel -y
ipython kernel install --user --name=python38
pip install google-cloud-aiplatform==1.28.1 --upgrade --user
pip install langchain==0.0.250 --user
conda deactivate

ここで、ブラウザーのリロードボタンを押して、画面全体をリロードします。その後、画面上部のタブの右端にある[+]ボタンをクリックすると、ローンチャーの画面が開きます。すると、「Notebook」のセクションに[Python38(Local)]のボタンが追加されているので、これをクリックして新しいノートブックを開きます。

この後は、このノートブック上でコードを実行していきます。すでにコードを書き込んだノートブックが GitHub で公開されていますので、そちらも参考にしてください。

ステップ 1:Format

Format のステップでは、PromptTemplate モジュールを使用して、LLM に入力するプロンプトを作成します。このモジュールでは、事前に定義したテンプレートに動的な内容を埋め込むことができます。

まず、プロンプトのテンプレートを作成します。この例では、{description} の部分に記述された製品について、商品名のアイデアを 3 種類出してもらいます。

from langchain import PromptTemplate

template = """\
You are a naming consultant for new products.
Give me three examples of good names for a product.
Output only names in a comma-separated list, nothing else.

Here's an example.
product: cute pens
output: Scribble, Ink-it, Write-On

Here's the real request to you.
product: {description}
output:
"""

prompt = PromptTemplate(template=template, input_variables=['description'])

テンプレートの動作を確認します。次のコマンドを実行すると、{description} の部分が指定の文字列に置き換えられた結果が得られます。

print(prompt.format(description='Colorful cute smartphone covers for teenagers'))

結果は次のようになります。

You are a naming consultant for new products.
Give me three examples of good names for a product.
Output only names in a comma-separated list, nothing else.

Here's an example.
product: cute pens
output: Scribble, Ink-it, Write-On

Here's the real request to you.
product: Colorful cute smartphone covers for teenagers
output:

ステップ 2:Predict

Predict のステップでは、langchain.llms パッケージの VertexAI モジュールを使用して、Vertex AI の PaLM API にリクエストを送信します。langchain.llms パッケージは、複数ベンダーの LLM の API に対応しており、これらを統一的な形式で利用できます。

次のコマンドを実行すると、PaLM API を呼び出すクラインアントオブジェクトが得られます。オプションで、使用するモデル model_name と温度パラメーター temperature を指定します。クリエイティブな内容が欲しいので、温度パラメーターはすこし大きめの 0.4 にしています。

from langchain.llms import vertexai
llm = vertexai.VertexAI(model_name='text-bison', temperature=0.4)

LLMChain モジュールを用いると、先ほど用意したテンプレートの適用と API の呼び出しをパイプラインとして結合できます。

from langchain import LLMChain
llm_chain = LLMChain(prompt=prompt, llm=llm)

得られたパイプラインは、次のように実行します。ここでは、「ティーンエイジャー向けのカラフルでキュートなスマホカバー」の商品名を提案してもらいます。

description = 'Colorful cute smartphone covers for teenagers'
print(llm_chain.run(description))

実行ごとに異なる結果になりますが、例えば、次のような結果が得られます。

Color Blast, Bright-N-Shiny, Cover-Me

ステップ 3:Parse

最後に、得られた結果をパースして、Python から利用可能な構造化データに変換します。ここでは、CommaSeparatedListOutputParser モジュールを用いて、カンマ区切りのテキストを Python のリストに変換します。はじめに、パーサーのオブジェクトを用意します。

from langchain.output_parsers import CommaSeparatedListOutputParser
output_parser = CommaSeparatedListOutputParser()

このオブジェクトを用いて、次のように LLM からの応答を変換できます。

output_parser.parse(llm_chain.run(description))

結果は次のようになります。

['ColorPop', 'PhoneCasey', 'CaseyCover']

ここでは想定通りの結果が得られましたが、このコマンドを何度か繰り返し実行すると問題が起きることがあります。LLM がプロンプトの指示を無視して、カンマ区切りのテキスト以外の形式で応答を返すことがあるため、意図通りに動作しないのです。LLM からの出力形式をより確実に指定する方法はないのでしょうか?

これは、pydanitc ライブラリーと LangChain の PydanticOutputParser モジュールを組み合わせることで解決できます。この次に説明するように、事前に定義したクラスのオブジェクトとして確実に結果が得られます。

オブジェクト形式で結果を取得

はじめに、pydantic ライブラリーを使用して、結果を格納するクラス ProductNames を定義します。

from pydantic import BaseModel, Field

class ProductNames(BaseModel):
    setup: str = Field(description='product description')
    product_name1: str = Field(description='product name 1')
    product_name2: str = Field(description='product name 2')
    product_name3: str = Field(description='product name 3')

プロパティ setup には、入力した製品情報を格納して、product_name1product_name2
product_name3 のプロパティには、提案された 3 種類の商品名を格納する想定です。LangChain の PydanticOutputParser モジュールを用いて、LLM の出力を ProductNames クラスのオブジェクトに変換するパーサーオブジェクトを生成します。

from langchain.output_parsers import PydanticOutputParser
parser = PydanticOutputParser(pydantic_object=ProductNames)

さらに、このオブジェクトは、LLM に対して、ProductNames クラスに対応した JSON 形式で結果を返すように指示するインストラクションが生成できます。インストラクションの内容は、次のコマンドで確認できます。

print(parser.get_format_instructions())

結果は次のようになります。

The output should be formatted as a JSON instance that conforms to the JSON schema below.

As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.

Here is the output schema:
```
{"properties": {"setup": {"title": "Setup", "description": "product description", "type": "string"}, "product_name1": {"title": "Product Name1", "description": "product name 1", "type": "string"}, "product_name2": {"title": "Product Name2", "description": "product name 2", "type": "string"}, "product_name3": {"title": "Product Name3", "description": "product name 3", "type": "string"}}, "required": ["setup", "product_name1", "product_name2", "product_name3"]}
```

PromptTemplate モジュールを用いて、このインストラクションを含んだプロンプトのテンプレートを用意した上で、LLMChain モジュールによるパイプラインを再構成します。

template="""\
Answer the user query.
{format_instructions}
You are a naming consultant for new products.
Give me examples of good names for a product described as {description}'
"""

prompt = PromptTemplate(
    template=template,
    input_variables=['description'],
    partial_variables={'format_instructions': parser.get_format_instructions()}
)

llm_chain = LLMChain(prompt=prompt, llm=llm)

このあとは、先ほどと同様の方法で、LLM からの回答が得られます。さらに、得られた結果を先ほど用意したパーサーのオブジェクトで ProuductNames クラスのオブジェクトに変換できます。ここでは、「象が踏んでも壊れないスマホカバー」の名前を考えてもらいましょう。

description = description = "Super tough smartphone covers that wouldn't break even if an elephant stamps on"
output = parser.parse(llm_chain.run(description))
output

実行結果は次のようになります。

ProductNames(setup="Super tough smartphone covers that wouldn't break even if an elephant stamps on", product_name1='Rhino Shield', product_name2='Tough Guy', product_name3='Unbreakable')

次のように、プロパティ名を指定して、個々の結果を取り出すことができます。

output.product_name1, output.product_name2, output.product_name3

実行結果は次のようになります。

('Rhino Shield', 'Tough Guy', 'Unbreakable')

1つのパイプラインにまとめる

先ほどの例では、parser.parse(llm_chain.run(description)) のように、パイプラインを実行した後に、さらにパーサーの処理を追加で実行していました。これらの処理を1つにまとめたパイプラインを定義することもできます。次のように、TransformChaim モジュールでパーサーの処理をパイプライン化した上で、SequentialChain モジュールで、LLM の呼び出しとパーサーの処理を結合したパイプラインを定義します。

from langchain.chains import TransformChain, SequentialChain

llm_chain = LLMChain(prompt=prompt, llm=llm, output_key='json_string')

def parse_output(inputs):
    text = inputs['json_string']
    return {'result': parser.parse(text)}

transform_chain = TransformChain(
    input_variables=['description'],
    output_variables=['result'],
    transform=parse_output
)

chain = SequentialChain(
    input_variables=['description'],
    output_variables=['result'],
    chains=[llm_chain, transform_chain],
)

次のように、すべての処理を 1 つのパイプラインとして実行できます。

description = "Super tough smartphone covers that wouldn't break even if an elephant stamps on"
chain.run(description=description)

実行結果は次のようになります。

ProductNames(setup="Super tough smartphone covers that wouldn't break even if an elephant stamps on", product_name1='Toughest Phone Cover', product_name2='Unbreakable Phone Case', product_name3='Elephant-Proof Phone Cover')

まとめ

LLM を活用して実用的なアプリケーションを実装する際は、LLM だけで複雑な処理を行うのではなく、既存のライブラリーや API サービスと組み合わせて活用するという発想が大切です。あるいは、新しいアプリケーションをスクラッチで開発するのではなく、既存のアプリケーションに LLM を用いて機能を追加するなどの活用法も考えられるでしょう。

そして、このようなインテグレーションを支援するツールが LangChain です。この記事では、LangChain が提供するモジュールの中でも最も基本的な Model I/O の使用例を紹介しましたが、この後の「パート 7 : 複数サービスの組み合わせ技で実用的なアプリを作る」では、LangChain を用いて、LLM と他のサービスを連携したアプリケーションの実装例を解説します。

Google Cloud Japan

Discussion