はじめに
LLMを利用する場合、ブラウザ上でチャット形式のUIが提供されるサービスを利用するのが手っ取り早く、広く使われている手法だと思いますが、ここではアプリケーション(主にPython)からLLMを利用する方法を見ていきます。
LLMを活用したアプリ開発を効率化するためのフレームワーク(OSS)としてLangChainというものがあるので、今回はLangChainについて整理していきます。
関連記事
生成AI Study - (1) 基礎知識の整理
生成AI Study - (2) LangChain入門
生成AI Study - (3) RAG入門
LangChain概要
LangChainとは
LangChainとは、LLMを活用したアプリケーション開発を行う際に様々な支援機能を提供するフレームワークで、OSSとして提供されています。公式にはPython、JavaScript/TypeScriptのライブラリが提供されています。
LangChainの主要な特徴としては3つ挙げられています。
- Standardized component interfaces: LLMや関連コンポーネントの種類が多様化しており、各コンポーネントごとの利用方法を個別に学習するのが困難です。LangChainではLLM関連コンポーネントにアクセスするための標準的なインターフェースを提供しているため、コンポーネントごとの学習コストが軽減されます。
- Orchestration: LLMを活用したコーディングでは、複数のコンポーネントを利用して組み合わせて利用することが多々あります。そのようなコーディングをサポートする機能が提供されます(後述のLCELという機能もその一つです)。
- Observability and evaluation: トレースやパフォーマンス指標のモニタリング機能を提供してくれます。この機能を提供するコンポーネントは"LangSmith"と呼ばれるものですが、基本的にこれは有償のようです(Freeプランも一応あるみたいですが)。
参考:
Why LangChain?
Architecture
LangSmith Pricing
LangChain Expression Language(LCEL)
LangChain提供のライブラリは多数提供されていますが、比較的新しい機能としてLCELというインターフェースを提供しています。これを使用することで、"宣言的な"コーディングスタイルでLLMのオペレーションを記述することができ、シンプルな記述で効率的な処理が行われることになるようです。
特徴的なのは、Linux/Unixシェルでのパイプライン処理に似た表記が行えることです。"|
"(パイプ)演算子を使って前の処理結果を後続のインプットに渡されるという流れを記述することができます。
開発効率やパフォーマンス等の観点から、LangChainでアプリ開発を行う際はLCELの利用が推奨となっているようです。
参考:
LangChain Expression Language (LCEL)
LangChainを使う前に...
LangChainはLLMを扱ったコーディングをする際の標準的なインターフェースを提供していることが特徴の一つです。このメリットを理解するために、各LLM固有の作法がどうなっているのかを見ておくのもよいかと思います。
例えば、watsonx.aiを使う場合のコーディングのイメージがどうなるかは、別の記事で試してみています。
参考:
Pythonからwatsonx.aiのサービスを使ってみる - サンプル実行
Use watsonx, and Meta llama-3-70b-instruct to answer question about an article
LangChainを使ってみる
基本的に以下のTutorialをベースにシンプルなコードを作って動かしてみます。
Build a simple LLM application with chat models and prompt templates
環境セットアップ
LLMのサービスとしてはwatsonx.aiを使用します。
VS CodeのJupyter Notebookからアクセスすることにします。
この辺りの基本セットアップは以下の記事を参考に。
Pythonからwatsonx.aiのサービスを使ってみる
※watsonx.aiのサービスを使用するために必要なセットアップは済んでいて、アクセス時に必要なAPIKey, Project ID, EndpointURLの情報は入手済みの前提とします。
LangChainを使用するにあたっては、langchain
というパッケージをPython環境にインストールしておきます。
例: pip install langchain
また、今回はwatsonx.aiのサービスを使用するので、IBM watsonx用のlangchainパッケージもインストールする必要があります。
例: pip install langchain-ibm
コーディング例(1): シンプルケース
モデル作成
まず利用するLLMについての準備を行います。ここは使用するLLMのプロバイダーごとに指定するパラメーターなどの作法が異なりますが、LLMごとの差分はmodelオブジェクトに隠蔽されることになります。
ここでは、watsonx.aiのサービスを使用してmodelオブジェクトを作成していきます。
import getpass
import os
if not os.environ.get("WATSONX_APIKEY"):
os.environ["WATSONX_APIKEY"] = getpass.getpass("Enter API key for IBM watsonx: ")
from langchain_ibm import ChatWatsonx
model = ChatWatsonx(
model_id="ibm/granite-34b-code-instruct",
url="https://us-south.ml.cloud.ibm.com",
project_id="<WATSONX PROJECT_ID>"
)
watsonx.aiのサービスを利用する際に必要な環境依存の情報としては、APIKey, ProjectID, EndpointURLがありますが、いずれもChatWatsonxクラスのパラメータで指定できます。
チュートリアルの例だと実行時にAPIKeyを都度入力させるようなコード例になっていますが、それだと毎回鬱陶しいですし、コード中にAPIKeyをハードコードするのはよくないので、環境依存の情報はまとめて環境変数として与えるようにしたいと思います。
環境変数の取り込みはpython-dotenvパッケージのload_dotenv()
を利用すると良さそうです。
MY_WATSONX_APIKEY='xxxxx'
MY_WATSONX_ENDPOINT="https://us-south.ml.cloud.ibm.com"
MY_WATSONX_PROJECTID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
import os
from dotenv import load_dotenv
load_dotenv()
myURL = os.getenv("MY_WATSONX_ENDPOINT")
myProjectID = os.getenv("MY_WATSONX_PROJECTID")
myAPIKey = os.getenv("MY_WATSONX_APIKEY")
from langchain_ibm import ChatWatsonx
model = ChatWatsonx(
model_id = "mistralai/mistral-large",
url = myURL,
project_id = myProjectID,
apikey = myAPIKey
)
これで、環境依存の情報はまとめて環境変数ファイル(.env
)で管理できるようになります。
watsonx.ai用のモデル作成のためにはChatWatsonxを使用します。
ChatWatsonxについての詳細はこちら:
ChatWatsonx
LangChain Python API Reference - ChatWatsonx
LangChain - Chat Models
上のサンプルコードの例では、LLMの種類としてmistral/mistral-large
を指定しています。watsonx.aiで指定できるモデルについては以下のドキュメントに記載があります。(Deprecated/Withdrawnとなっているモデルもあるので注意)
参考:
IBM watsonx - Supported foundation models in watsonx.ai
IBM watsonx - Foundation model deprecation
プロンプト作成、LLM呼び出し
上で作成したモデルを使って、プロンプトを与えてLLM呼び出しを行い結果を取得してみます。
from langchain_core.messages import HumanMessage, SystemMessage
messages = [
SystemMessage("""次の日本語を英語に翻訳してください。"""),
HumanMessage("""私の名前は山田太郎です。千葉県出身です。""")
]
result1 = model.invoke(messages)
さて、この例では、SystemMessage, HumanMessageという2種類のクラスでプロンプトを指定しています。LangChainにおいて、一般的に利用されるチャットモデルでの入出力データは"メッセージ"として扱われます。とりあえずは、SystemMessage、HumanMessageはそれぞれ、System Prompt、User Promptに該当すると解釈しておけばよさそうです。これらのオブジェクトをList型にしてmessagesというオブジェクトに格納しています。
messagesというオブジェクトとして作成されたプロンプトを引数にして、先に作成されたmodelのinvoke()
メソッドにてLLMのサービスを呼び出して結果を取得しています。
上のコード例のmodelはChatWatsonxクラスをインスタンス化したオブジェクトですが、ChatWatsonxクラスは langchain_core.runnables.base.Runnable
インターフェースを実装したクラスで、invoke()
メソッドはRunnableインターフェースで定義されているメソッドです。
ここでは、ChatWatsonxというモデルに対して、メッセージのリスト(System Prompt と User Prompt)を与えることで、その内容に応じたメッセージ(AIMessage)が返されるということになります。
参考:
LangChain - Runnable interface
LangChain - Chat Models
このLLM呼び出し部分がLangChainの肝になる部分だと思われますが、それだけになかなかこの構造が奥深いものがあります。一旦ここではinvoke()
メソッドでLLM呼び出しが行えると捉えておけばよさそうです。
結果の解釈
上で返されたresult1
はAIMessage型のオブジェクトになっています。
これをそのまま出力させてみるとこんな感じになります。
print(result1)
content=' 訳:\n\nMy name is Tarou Yamada. I am from Chiba Prefecture.' additional_kwargs={} response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 46, 'total_tokens': 72}, 'model_name': 'mistralai/mistral-large', 'system_fingerprint': '', 'finish_reason': 'stop'} id='chatcmpl-6e6b11e2-23a1-4306-9544-db038ae6b327---e76d5cc963c77b8ce3466b6a91cc1861' usage_metadata={'input_tokens': 46, 'output_tokens': 26, 'total_tokens': 72}
結果にはメタ・データなどの情報も含まれます。生成されたテキスト情報だけ抽出するには、content
フィールド部分を抜き出す必要があります。
print(result1.content)
訳:
My name is Tarou Yamada. I am from Chiba Prefecture.
メタ・データを抽出してみるとこんな感じ
print(result1.usage_metadata)
{'input_tokens': 46, 'output_tokens': 26, 'total_tokens': 72}
コーディング例(2): Prompt Templateの利用
Promptを作成する際に、ある程度雛形(テンプレート)のようなものを作っておいて一部を変数で置き換えて使いまわししたいということはよくあると思います。
そういう場合にPrompt Templateを利用できます。
参考:
Build a simple LLM application with chat models and prompt templates - Prompt Templates
LangChain - Prompt Templates
モデル作成部分はコーディング例(1)と同様なので、ここでは割愛します。
テンプレートからプロンプト作成
# create a template
system_template = "以下の{language1}を{language2}に翻訳してください。 "
user_template = "私の名前は{name}です。出身地は{birthplace}"
prompt_template = ChatPromptTemplate.from_messages(
[("system", system_template),
("user", user_template)]
)
# create a prompt from a template
prompt = prompt_template.invoke({"language1": "日本語",
"language2": "英語",
"name": "山田太郎",
"birthplace": "千葉県"})
ChatPromptTemplate.from_messages()
メソッドで、system prompt、user promptに与えるテンプレートとなる文字列を設定します。テンプレートとなる文字列には{xxx}
という形式で一部を変数として設定しておくことができます。
このテンプレートを元にinvoke()
メソッドにて実際のプロンプトを作成することができます。この時各変数に対して実際の値をdictionary型で引数として与えることができます。
LLM呼び出し、結果の解釈
先に作成したprompt
を引数として、model
に対してinvoke()
を実行します。
# get a response from an LLM using a prompt
response = model.invoke(prompt)
print(response.content)
My name is Taro Yamada. I come from Chiba Prefecture.
結果の解釈についてはコーディング例(1)と同様です。
コーディング例(3): LCELの利用
LangChain Expression Language(LCEL)のコーディング作法を試してみます。ここでは、コーディング例(2)の内容を、LCEL作法で書き換えてみます。
参考:
LangChain - LangChain Expression Language(LCEL)
モデル、プロンプト・テンプレートの作成
from langchain_ibm import ChatWatsonx
from langchain_core.runnables import RunnableSequence
# create a model
model = ChatWatsonx(
model_id = "mistralai/mistral-large",
url = myURL,
project_id = myProjectID
)
# create a template
system_template = "以下の{language1}を{language2}に翻訳してください。 "
user_template_ja = "私の名前は{name}です。出身地は{birthplace}"
prompt_template_ja = ChatPromptTemplate.from_messages(
[("system", system_template),
("user", user_template_ja)]
)
チェーンの作成
# create a chain
chain_ja = prompt_template_ja | model
先に作成した、プロンプト・テンプレートprompt_template_ja
の結果(生成されたプロンプト)をインプットとしてモデルmodel
を呼び出す、という処理の流れをパイプ演算子|
を用いることで表現し、それをchain_ja
というRunnableSequenceクラスのオブジェクトとして保持できます。
※ここでは、まだプロンプト・テンプレートに対して具体的な変数の値を与えていないことに注意してください。
LLM呼び出し、結果の解釈
先に作成したchain_ja
のチェーン(プロンプト生成 → LLM呼び出し)を実行します。chain_ja
に対してinvoke()
メソッドを発行することでこのチェーンに含まれる処理がシーケンシャルに実行されます。
ここで、プロンプト生成に必要な変数の値をdictionary型で引数として与えます。
# get a response from an LLM using a prompt
response_ja2en = chain_ja.invoke({"language1": "日本語",
"language2": "英語",
"name": "山田太郎",
"birthplace": "千葉県"})
print(response_ja2en.content)
My name is Yamada Taro. I am from Chiba Prefecture.
結果の解釈についてはコーディング例(1)と同様です。
コーディング例(4): output parserの利用
LLMの処理を行う過程の各種処理で、出力結果の形式をコントロールしたい場合があります。結果を期待する形式で出力させるためのoutput parserという機能があります。
output parserにも出力させたい構造によって様々な種類がありますが、ここでは単純なStrOutputParserを試してみます。
参考:
LangChain - Output parsers
ここでは、コーディング例(3)で作成したLCEL形式のチェーンにoutput parserを組み込んでみたいと思います。
モデル、プロンプト・テンプレートの作成
ここは前回のコーディング例(3)と全く同じです。
from langchain_ibm import ChatWatsonx
from langchain_core.runnables import RunnableSequence
# create a model
model = ChatWatsonx(
model_id = "mistralai/mistral-large",
url = myURL,
project_id = myProjectID
)
# create a template
system_template = "以下の{language1}を{language2}に翻訳してください。 "
user_template_ja = "私の名前は{name}です。出身地は{birthplace}"
prompt_template_ja = ChatPromptTemplate.from_messages(
[("system", system_template),
("user", user_template_ja)]
)
output parser作成
from langchain_core.output_parsers import StrOutputParser
# create a parser
output_parser = StrOutputParser()
チェーンの作成
# create a chain
chain_ja = prompt_template_ja | model | output_parser
ここで、チェーンの最後のoutput_parser
を繋げます。これによって、LLM処理結果をoutput_parserで処理するという流れが定義されました。
LLM呼び出し、結果の解釈
上で作成したチェーンchain_ja
をinvoke()
メソッドで実行します。ここの書き方は前回のサンプルと同様です。
# get a response from an LLM using a prompt
response_ja2en = chain_ja.invoke({"language1": "日本語",
"language2": "英語",
"name": "山田次郎",
"birthplace": "東京都"})
print(response_ja2en)
My name is Jiro Yamada. I am from Tokyo.
ただし、output_parserが介在することにより最終的に生成される結果、つまりresponse_ja2enに格納される結果は前回と変わっています。
今回StrOutputParserを使っていますので、結果は純粋なテキストのみとなり、メタ・データなどの情報は排除されています。従って、単純にresponse_ja2en
をprintすることでLLMから返された結果部分のみが出力されています。
コーディング例(5): with_structured_output()メソッドの利用
Chatモデルの場合LLM呼び出し結果は、通常、任意の形式のテキストデータとして返されます。それだと後続の処理で不都合が生じることがあるので、特定のスキーマに準拠した形で結果を返してくれると有用な場合があります。
LLMからの結果を特定のスキーマに当てはめて返すような仕組みが提供されていますので、ここではその仕組みを試してみます。
参考:
LangChain - How to return structured data from a model
API Reference - ChatWatsonx - with_structured_output()
モデル作成
ここは基本的にこれまでのコーディング例と同じです。watsonx.aiのモデルを使用します。
# create a model
model = ChatWatsonx(
model_id = "mistralai/mistral-medium-2505",
url = myURL,
project_id = myProjectID
)
スキーマ設定
結果として取得したいデータ構造を表すスキーマを設定します。今回はJSONデータで取得する想定で、その構造をJSON Schemaとして指定しています。今回は、クイズを生成させてその内容をJSONデータ形式で出力させることを想定し、その結果の構造をJSON Schemaとして組み立てています。
※JSON schemaのdescription部分に各フィールドに格納する値の説明を記述するのですが、どうもここに日本語が含まれているとうまく動作しないようだったので、一旦JSON Schemaには日本語は含めないようにしています。
json_schema = {
"title": "CatQuiz",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "quizzes about cats",
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "statement of question",
},
"answer": {
"type": "string",
"description": "statement of answer",
},
"difficulty": {
"type": "integer",
"description": "How difficult the question is, from 1 to 10"
}
},
"required": ["question","answer","difficulty"]
}
structured_model = model.with_structured_output(json_schema)
先に作成したモデルに対してwith_structured_output()
メソッドを使用してjson_schemaを設定します。
プロンプト指定、LLM呼び出し
先に作成したstructured_model
(json_schemaを設定したモデル)に対してプロンプトを指定してLLM呼び出しを行います。
result = structured_model.invoke("猫に関するクイズを日本語で生成してください。生成結果には質問と回答と難易度を含めてください。")
このように、with_structured_outputでJSON Schemaを設定したモデルで、出力構造体を指定するやり方は、使用するモデルの種類によってかなり挙動にばらつきがあるようです。最初、mistral-largeで試していましたが、特に日本語指示の場合、挙動が安定しませんでした。同じコードでも成功するケースと失敗するケースが出てくる状況でした(体感的には10回に1回は成功し、9回はエラーになる感じ)。
今回指定したような、mistral-medium-2505だと比較的安定して日本語指示でも動かせました。それでも複雑なJSON Schemaを設定するとうまく動かせませんでした。
このwith_structured_output()
メソッドによるスキーマ指定はモデルによっては使い物にならない可能性がありそうです。まだまだ成熟していない感じでしょうか。
(プロンプトをちょっと変えたりスキーマちょっと変えただけで挙動が安定しないので怖くて使いにくい...)
結果解釈
print(result)
{'answer': '約15年', 'difficulty': 5, 'question': '一般的に猫はどのぐらいの寿命でしょうか?'}
それっぽく出力はされていますが...
結果のresult
の型を確認するとdict型(辞書型)になっていましたが、これは正しいのだろうか?(Str型でJSON形式のテキストが返されることを期待していたのだが...)
おまけ
上で最終的にやりたかったことを、with_structured_output()
メソッドを使わずに、system promptで与えてあげるとどうなるか試してみました。
from langchain_core.messages import HumanMessage, SystemMessage
model = ChatWatsonx(
model_id = "mistralai/mistral-large",
url = myURL,
project_id = myProjectID
)
messages = [
SystemMessage("""以下のJSON Schemaの形式に従ったJSON形式で出力してください。JSONデータのみを出力してください。
```json-schema
"title": "CatQuiz",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "Quizzes about cats",
"type": "array",
"items":{
"type":"object",
"required": ["question", "answer", "difficulty"],
"properties": {
"question": {
"type": "string",
"description": "statement of question",
},
"answer": {
"type": "string",
"description": "statement of answer",
},
"difficulty": {
"type": "integer",
"description": "How difficult the question is, from 1 to 10"
}
}
}
"""),
HumanMessage("""猫に関するクイズをいくつか日本語で生成してください。生成結果にはそれぞれ質問と回答と難易度を含めてください。""")
]
result = model.invoke(messages)
print(result.content)
```json
[
{
"question": "猫の寿命は約何年ですか?",
"answer": "猫の寿命は約15年から20年です。",
"difficulty": 3
},
{
"question": "猫が一番好きな睡眠場所はどこですか?",
"answer": "猫が一番好きな睡眠場所は暖かく、静かで暗い場所です。",
"difficulty": 4
},
{
"question": "猫が爪を削ることをなんと呼びますか?",
"answer": "猫が爪を削ることを爪とぎと呼びます。",
"difficulty": 2
},
{
"question": "猫はどのようにして感情を表現しますか?",
"answer": "猫は尻尾や耳の動き、声、目つきで感情を表現します。",
"difficulty": 5
},
{
"question": "猫の耳はどのように動かすことができますか?",
"answer": "猫の耳は30個以上の筋肉を持ち、180度回転させることができます。",
"difficulty": 6
}
]
```
これで割と安定して求める形式で結果が取得できています。最初と最後の行は省いてデータをハンドリングする必要がありそうですが、こっちの方がよっぽど扱いやすい気がします。
LangChainのデメリット
LangChainのコンセプトはすばらしいと思いますが、色々試していて何か問題が発生した場合に内部的にどのような動きになっていてどこに原因があるのかが分かりにくいなぁと感じることが多々ありました。確かにコードとしてはきれいに書けそうなんですけど、非常に問題判別がしにくい印象でした。
その後、ある有識者の話を聞くに、どうも最近ではLangChainの利用には懐疑的な見方をする人もいるようです。主な理由としては、以下のようなことがあるようです。
- ブラックボックス化: 様々な生成AIやツールに対応するためLangChain内で処理している部分が多く何をやっているのかブラックボックスになってしまっている。プロンプト・エンジニアリングの部分もLangChain内部で実装されている処理もあり、最終的にLLMにどのようなプロンプトが投げられているのか分かりにくい。
- 破壊的変更が多い: 様々なツールに対応しようとしているせいか、LLM技術のトレンドの進化に合わせた対応を重視し、互換性が損なわれている傾向が強い。(※ただし、これは2024年くらいまでの傾向のようで、2025年7月時点では比較的安定してきているとのことです。)
- 個別最適化されたより使いやすい技術の台頭: RAGならコレ、エージェント的な振る舞いに特化したフレームワークならコレ、のように、利用したい技術に特化した扱いやすい技術が次々提供されている状況がある。
LangChainは汎用的なフレームワークたることを目指していると思いますが、その汎用性ゆえにブラックボックス化などの課題もあるようです。標準化を目指しているので学習コストが低いということを謳っていますが、大規模に多様なLLMサービスやツールを使う場合でなければ、かえって学習コストが高くなる場合もあるのではないかという気もします。
生成AIのエリアは技術の進歩が著しい分野であり、新しい技術が出ては消え、というのが繰り返されるのは過渡期においては必然であると言えると思います。なのでLangChainも今後流行るか廃れるかは未知数であると思います。
ただ、少なくとも2025年7月時点ではLangChainはLLMを使用するためのフレームワークとしてはデファクト・スタンダートと言えるくらい広く使われているようですし、機能やサポート範囲もかなり充実しているようです。
一度一つのフレームワークを使って試行錯誤してみることは他の技術を使う場合にも応用が利くので、その意味でもLangChainを使ってみるのは有用なのだと思います。