LangChainの特徴と実際にLLMアプリケーションを作成するまで
こんにちは、shibamです。
ChatGPTの登場以降、LLM(大規模言語モデル)を活用したアプリケーション開発が活発になっています。
今回は、LLMアプリケーション開発のためのフレームワーク「LangChain」について、その特徴と基本的な使い方を紹介します。
目次
1. TL;DR
- LangChainは、LLMアプリケーション開発のための統合フレームワーク
- Chains、Agents、Memory、Promptsなどの機能を提供
- OpenAI、Anthropic(Claude 3.7 Sonnet)、Google等、様々なLLMプロバイダーに対応
- Python/TypeScriptで利用可能で、豊富なドキュメントとコミュニティを持つ
2. LangChainとは
概要
LangChainは、LLMを活用したアプリケーション開発を効率化するためのフレームワークです。
LangChainを使うことで、以下のような課題を解決できるようになります。
- 複数のLLMを統一的なインターフェースで扱いたい
- プロンプトを再利用可能な形で管理したい
- LLMの出力を構造化されたデータとして扱いたい
- 外部データソースと連携させたい
LangChainの基本概念
大規模言語モデル(LLM)を活用する際、単に質問を投げて回答を得るだけでなく、「情報の取得→分析→適切な形での回答生成」といった複数のステップを組み合わせることがよくあります。
例えば、ユーザーが「最近の東京の天気傾向を要約して」と質問した場合、天気データを取得し、それを基にLLMに要約を生成させ、最後に読みやすい形で提示するというプロセスが必要です。LangChainは、こうした複雑なワークフローを簡単に構築できるフレームワークなのです。
3. LangChainの特徴
特徴的な機能
1. Chains(チェーン)
チェーンは、複数の処理を連結して一連の流れを作る機能です。例えば「ユーザーからの質問→情報検索→回答生成→回答の翻訳」といった流れを1つのチェーンとして定義できます。
日常的な例えでいうと、料理のレシピのようなものです。材料(入力)に対して、「切る→炒める→味付け→盛り付け」という一連の手順(処理)を順番に適用することで、最終的な料理(出力)が完成します。LangChainのチェーンも同様に、入力データに対して複数の処理を順に適用し、最終的な出力を得るための仕組みです。
以下のような処理を連結して実行できます:
- プロンプトの作成 → LLMへの入力 → 結果の整形
- 文書の読み込み → ベクトル化 → 類似度検索 → 要約
以下は、chainの例です。パイプ(“|”)を使って、プロンプト、モデル、出力パーサーを連結しています。
# チェーンの例
chain = prompt | chat | StrOutputParser()
response = chain.invoke({"input": "質問内容"})
2. Agents(エージェント)
LLMに「考える能力」を与え、複数のツールを組み合わせて目的を達成する機能です:
- 天気APIやweb検索など、外部ツールの利用
- タスクの分解と実行順序の決定
- 結果の統合と最適な回答の生成
以下はAgentの実装例です。
# Agentの例
tools = [
Tool(name="Weather", func=get_weather, description="天気を取得します"),
Tool(name="Search", func=search_web, description="web検索を実行します")
]
agent = create_react_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
response = agent_executor.invoke({"input": "質問内容"})
事前に定義したtoolsをAgentに渡して、Agentが適切なツールを選択して実行します。
Agentは以下のような場面で特に効果的です:
- 複数のステップが必要なタスク(例:情報収集→分析→要約)
- 条件分岐を含む処理(例:ユーザーの質問内容に応じて適切なツールを選択)
- 反復的な処理(例:最適な結果が得られるまで検索を繰り返す)
3. Memory(メモリ)
会話の文脈を保持する機能です。
- 直前の会話履歴の保持
- 重要な情報の長期保存
- 文脈に応じた回答の生成
Memoryを使用することで、会話の文脈を保持することができます。
以下は実装例です。
from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.schema.runnable import RunnablePassthrough
from langchain.schema.output_parser import StrOutputParser
from dotenv import load_dotenv
import os
# 環境変数の読み込み
load_dotenv()
def memory_example():
# メモリの設定
memory = ConversationBufferMemory(return_messages=True)
chat = ChatOpenAI(model="gpt-4o")
# プロンプトテンプレートの作成
prompt = ChatPromptTemplate.from_messages([
("system", "あなたは親切なアシスタントです。会話の履歴を参照して回答してください。"),
MessagesPlaceholder(variable_name="history"),
("user", "{input}")
])
# チェーンの作成
chain = (
RunnablePassthrough.assign(
history=lambda x: memory.load_memory_variables({})["history"]
)
| prompt
| chat
| StrOutputParser()
)
# 会話の例
print("会話1: 自己紹介")
response1 = chain.invoke({"input": "私の名前はshibamです。"})
memory.save_context({"input": "私の名前はshibamです。"}, {"output": response1})
print(f"応答: {response1}\n")
print("会話2: 趣味を尋ねる")
response2 = chain.invoke({"input": "私の趣味は何でしたか?"})
memory.save_context({"input": "私の趣味は何でしたか?"}, {"output": response2})
print(f"応答: {response2}\n")
print("会話3: 趣味を伝える")
response3 = chain.invoke({"input": "私は読書が好きです。"})
memory.save_context({"input": "私は読書が好きです。"}, {"output": response3})
print(f"応答: {response3}\n")
print("会話4: 趣味を確認")
response4 = chain.invoke({"input": "私の趣味は何ですか?"})
memory.save_context({"input": "私の趣味は何ですか?"}, {"output": response4})
print(f"応答: {response4}")
if __name__ == "__main__":
memory_example()
以下は実行例です。
会話1: 自己紹介
応答: こんにちは、Shibamさん!今日はどのようにお手伝いできますか?
会話2: 趣味を尋ねる
応答: 以前の会話でShibamさんの趣味についてはお聞きしていないようです。もしよろしければ、Shibamさんの趣味について教えていただけますか?それに基づいてお話を進められます。
会話3: 趣味を伝える
応答: 読書が好きなんですね!素晴らしい趣味ですね。どんなジャンルの本を読むのが好きですか?お気に入りの本や作家がいれば教えてください。
会話4: 趣味を確認
応答: あなたの趣味は読書ですね。どんなジャンルの本を読むのが好きなのか、またお気に入りの本や作家についてもぜひ教えてください。
4. Prompts(プロンプト)
再利用可能なプロンプトテンプレートを作成・管理する機能です。
- 変数の埋め込み
- プロンプトの構造化
- プロンプトの版管理
以下は、プロンプトの実装例です:
from langchain.prompts import ChatPromptTemplate
# カスタマーサポート用のプロンプト
customer_support_prompt = ChatPromptTemplate.from_messages([
("system", """あなたはECサイトのカスタマーサポート担当です。
以下のガイドラインに従って対応してください:
- 丁寧な言葉遣いを心がける
- 具体的な解決策を提案する
- 必要に応じて返品・交換の案内をする"""),
("user", "商品名: {product}\n問い合わせ内容: {inquiry}")
])
# 技術文書作成用のプロンプト
technical_doc_prompt = ChatPromptTemplate.from_messages([
("system", """技術文書を作成するアシスタントとして、以下の形式で文書を作成してください:
1. 概要
2. 前提条件
3. 実装手順
4. 注意事項
5. トラブルシューティング"""),
("user", "対象機能: {feature}\n実装内容: {implementation}")
])
# カスタマーサポートプロンプトの使用例
chain = customer_support_prompt | ChatOpenAI(model="gpt-4o") | StrOutputParser()
response = chain.invoke({
"product": "ワイヤレスイヤホン",
"inquiry": "購入した商品の片側から音が出なくなりました。"
})
# 技術文書プロンプトの使用例
chain = technical_doc_prompt | ChatOpenAI(model="gpt-4o") | StrOutputParser()
response = chain.invoke({
"feature": "ユーザー認証機能",
"implementation": "JWTを使用したトークンベースの認証システム"
})
以下は実行例です。
カスタマーサポートの例:
応答:
お問い合わせいただきありがとうございます。お客様が購入されたワイヤレスイヤホンの片側から音が出なくなってしまったとのこと、大変申し訳ございません。
まず、お試しいただける対処方法として、以下の手順をお試しください:
1. イヤホンを完全に充電します。
2. ペアリング(Bluetooth接続)を解除し、再度ペアリングを行います。
3. スマートフォンや他のデバイスで音楽を再生してみて、再度左右の音が出るか確認してみてください。
それでも問題が解決しない場合は、お手数ですがご購入いただいたECサイトの注文履歴から「返品・交換」の手続きをお進めいたします。手続きについてご不明点があれば、いつでもお知らせください。
カスタマーサポートの例:
応答:
この度はワイヤレスイヤホンの不具合によりご不便をおかけしており、大変申し訳ございません。
まずは、以下の点をご確認いただけますでしょうか。
1. **充電の確認**: イヤホンが十分に充電されているかご確認ください。片側が充電切れの場合、音が出ないことがあります。
2. **接続設定の確認**: お使いのデバイスとのBluetooth接続を一度解除し、再度ペアリングをお試しください。接続が不安定な場合、片側から音が出ないことがあります。
3. **イヤホンのリセット**: イヤホンのリセット方法が取扱説明書に記載されている場合、その手順に従ってリセットをお試しください。
上記の方法を試しても問題が解決しない場合、商品の初期不良の可能性がございます。その際は、返品または交換の手続きをさせていただきますので、購入証明書(レシートや注文確認メール)をご用意の上、当店のサポートセンターまでご連絡ください。
お手数をおかけいたしますが、何卒よろしくお願い申し上げます。
技術文書の例:
応答:
# JWTを使用したトークンベースの認証システム
## 1. 概要
JWT(JSON Web Token)を使用したトークンベースの認証システムは、ユーザーの認証を行う際に、ユーザー情報を含むトークンを生成し、そのトークンを用いてユーザーの認証とリクエストの検証を行う仕組みです。このシステムは、セッションベースの認証と比較してスケーラブルで、特に分散型アプリケーションにおいて有効です。
## 2. 前提条件
- 開発環境としてNode.jsとExpressがインストールされていること。
- データベース(例:MongoDBやPostgreSQL)がセットアップされていること。
- JWT関連のライブラリ(例:jsonwebtoken)がインストールされていること。
- HTTP通信を行うためのクライアントツール(例:Postman)が利用可能であること。
## 3. 実装手順
### 3.1 ライブラリのインストール
```
npm install express jsonwebtoken body-parser bcryptjs
```
### 3.2 ユーザーモデルの作成
使用するデータベースに応じて、ユーザー情報を保存するためのモデルを作成します。以下はMongoDBを使用した例です。
```javascript
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
module.exports = mongoose.model('User', UserSchema);
```
### 3.3 パスワードのハッシュ化
ユーザー登録時にパスワードをハッシュ化して保存します。
```javascript
const bcrypt = require('bcryptjs');
const hashPassword = async (password) => {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(password, salt);
};
```
### 3.4 トークンの生成
JWTを使用してユーザー認証時にトークンを生成します。
```javascript
const jwt = require('jsonwebtoken');
const generateToken = (user) => {
return jwt.sign({ id: user._id, username: user.username }, 'your_secret_key', { expiresIn: '1h' });
};
```
### 3.5 ルートの設定
Expressを使用してユーザー登録、ログイン、認証を行うルートを設定します。
```javascript
const express = require('express');
const bodyParser = require('body-parser');
const User = require('./models/User'); // ユーザーモデル
const app = express();
app.use(bodyParser.json());
// ユーザー登録
app.post('/register', async (req, res) => {
const { username, password } = req.body;
const hashedPassword = await hashPassword(password);
const user = new User({ username, password: hashedPassword });
await user.save();
res.status(201).send('User registered');
});
// ログイン
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await User.findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).send('Invalid credentials');
}
const token = generateToken(user);
res.json({ token });
});
// 認証ミドルウェア
const authenticate = (req, res, next) => {
const token = req.header('Authorization').replace('Bearer ', '');
try {
const decoded = jwt.verify(token, 'your_secret_key');
req.user = decoded;
next();
} catch (error) {
res.status(401).send('Invalid token');
}
};
// 保護されたルート
app.get('/protected', authenticate, (req, res) => {
res.send('Protected data');
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
```
## 4. 注意事項
- JWTの秘密鍵は環境変数として管理し、コードにハードコーディングしないようにします。
- トークンの有効期限を適切に設定し、長すぎないように注意します。
- HTTPSプロトコルを使用して通信の安全性を確保します。
## 5. トラブルシューティング
- **トークンが無効エラーが発生する:** トークンが正しく生成されているか、署名が正しいかを確認します。クライアント側でトークンが正しく送信されているかも確認します。
- **ユーザーが見つからないエラー:** データベースへの接続設定が正しく行われているか、ユーザーモデルが正しくマップされているかを確認します。
- **bcryptのエラー:** パスワードのハッシュ化と比較処理でエラーが発生する場合、bcryptのバージョンが互換性のあるものであるかを確認します。
プロンプトテンプレートを使用することで、以下のようなメリットがあります。
- 一貫性のある応答を維持できる
- プロンプトの再利用が容易になる
- ビジネスルールや要件を明確に組み込める
5. Retrievers(検索機能)
外部データソースから関連情報を検索・取得する機能です。RAG(Retrieval Augmented Generation)パターンの実装に不可欠です。
- ベクトルデータベースからの類似度検索
- 複数のデータソースを組み合わせた検索
- 検索結果のランキングと絞り込み
Retrieverを使った質問応答の実装例
Retrieverを使って関連文書を取得し、LLMに質問応答させる例を見てみましょう。
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.schema.document import Document
from langchain.schema.runnable import RunnablePassthrough
from langchain.prompts import ChatPromptTemplate
from dotenv import load_dotenv
import os
# 環境変数の読み込み
load_dotenv()
def retriever_example():
# 埋め込みモデルの初期化
embeddings = OpenAIEmbeddings()
# サンプルドキュメントを作成
documents = [
Document(
page_content="東京の人気観光スポット情報:\n"
"1. 東京スカイツリー - 高さ634mの電波塔。展望台からは東京の絶景が楽しめます。\n"
"2. 浅草寺 - 東京最古の寺院で、雷門と仲見世通りが有名です。\n"
"3. 新宿御苑 - 都心にある広大な公園で、春には桜が美しい場所です。",
metadata={"category": "観光", "location": "東京"}
),
Document(
page_content="東京の観光名所ベスト10:\n"
"- 渋谷スクランブル交差点:世界で最も忙しい交差点の一つ。\n"
"- 明治神宮:東京の中心部にある広大な森の中の神社。\n"
"- 上野公園:美術館や動物園がある文化の中心地。",
metadata={"category": "観光", "location": "東京"}
),
Document(
page_content="東京ディズニーリゾート訪問ガイド:\n"
"東京ディズニーランドと東京ディズニーシーの2つのパークからなる、日本を代表する観光地です。",
metadata={"category": "エンターテイメント", "location": "千葉"}
),
Document(
page_content="大阪の観光スポット:\n"
"- 大阪城:豊臣秀吉が建てた壮大なお城\n"
"- 道頓堀:グリコの看板で有名な繁華街\n"
"- ユニバーサル・スタジオ・ジャパン:人気テーマパーク",
metadata={"category": "観光", "location": "大阪"}
)
]
# ベクターストアの作成
vectorstore = Chroma.from_documents(documents, embeddings)
# 検索機能の取得
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 検索を実行
docs = retriever.invoke("東京の観光スポット")
print(f"検索結果: {len(docs)}件の関連ドキュメントが見つかりました")
for i, doc in enumerate(docs):
print(f"\n検索結果 {i+1}:")
print(f"内容: {doc.page_content[:300]}...")
print(f"メタデータ: {doc.metadata}")
# 質問応答の例を追加
print("\n" + "="*50)
print("Retrieverを使った質問応答の例")
print("="*50)
# ChatGPT APIを使用
llm = ChatOpenAI(model="gpt-3.5-turbo")
# プロンプトテンプレートの作成
prompt_template = """次の情報を参考にして、質問に答えてください。
情報:
{context}
質問: {question}
答え:"""
prompt = ChatPromptTemplate.from_template(prompt_template)
# 質問応答チェーンの構築
def format_docs(docs):
return "\n\n".join([doc.page_content for doc in docs])
qa_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
)
# 質問応答の実行
question = "東京で最も高い観光スポットはどこですか?"
print(f"\n質問: {question}")
response = qa_chain.invoke(question)
print(f"回答: {response.content}")
if __name__ == "__main__":
retriever_example()
実行例:
検索結果: 3件の関連ドキュメントが見つかりました
検索結果 1:
内容: 東京の人気観光スポット情報:
1. 東京スカイツリー - 高さ634mの電波塔。展望台からは東京の絶景が楽しめます。
2. 浅草寺 - 東京最古の寺院で、雷門と仲見世通りが有名です。
3. 新宿御苑 - 都心にある広大な公園で、春には桜が美しい場所です。...
メタデータ: {'category': '観光', 'location': '東京'}
検索結果 2:
内容: 大阪の観光スポット:
- 大阪城:豊臣秀吉が建てた壮大なお城
- 道頓堀:グリコの看板で有名な繁華街
- ユニバーサル・スタジオ・ジャパン:人気テーマパーク...
メタデータ: {'category': '観光', 'location': '大阪'}
検索結果 3:
内容: 東京の観光名所ベスト10:
- 渋谷スクランブル交差点:世界で最も忙しい交差点の一つ。
- 明治神宮:東京の中心部にある広大な森の中の神社。
- 上野公園:美術館や動物園がある文化の中心地。...
メタデータ: {'category': '観光', 'location': '東京'}
==================================================
Retrieverを使った質問応答の例
==================================================
質問: 東京で最も高い観光スポットはどこですか?
回答: 東京の最も高い観光スポットは東京スカイツリーです。
このように、Retrieverを使うことで、関連する情報を検索し、その情報をもとにLLMが質問に答えることができます。これはRAG(Retrieval Augmented Generation)と呼ばれる手法の基本形です。
6. Document Loaders(ドキュメントローダー)
さまざまな形式のデータを読み込んでLangChainで利用できる形に変換する機能です。
- PDFやWord文書などの読み込み
- CSVやエクセルファイルの処理
- webページやHTMLの解析
- Slackやメールなどのコミュニケーションデータの取り込み
以下は、Document Loadersの実装例です。(ここでは、webページの読み込みを実験しています。実行にはBeautiful Soup 4が必要です。)
from langchain_community.document_loaders import PyPDFLoader, CSVLoader, WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
import os
def document_loader_example():
# webページの読み込み例
try:
web_loader = WebBaseLoader("https://www.isoroot.jp/isoroot/wp-admin/post.php?post=8301&action=edit")
web_documents = web_loader.load()
print(f"webページから{len(web_documents)}ドキュメントを読み込みました")
# 最初の100文字を表示
if web_documents:
print(f"内容の一部: {web_documents[0].page_content[:50]}...")
except Exception as e:
print(f"webページの読み込みに失敗しました: {e}")
if __name__ == "__main__":
document_loader_example()
以下は実行例です。
webページから1ドキュメントを読み込みました
内容の一部:
ログイン ‹ 株式会社アイソルート — WordPress
Document Loadersを使用することで、以下のようなメリットがあります。
- 異なる形式のデータを統一的に扱える
- フォーマットの違いを気にせずに処理できる
- データを適切なサイズに分割して、LLMやベクトルDBに最適な形で渡せる
4. インストール方法
LangChainを使い始めるには、以下の手順で環境を準備します。
pip install langchain langchain-openai python-dotenv
プロジェクト直下に .env
ファイルを作成し、以下のようにAPIキーを設定してください
OPENAI_API_KEY=your_openai_api_key
5. LangChain使用時のよくある問題と解決策
- APIキーが読み込まれない
.env に正しいキーを設定し、load_dotenv()
をスクリプトの先頭で呼び出してください。 - モジュールが見つからない
依存パッケージがインストールされているかpip list
で確認してください。 - SyntaxError: invalid syntax
使用している Python のバージョンと構文が合っているか確認してください。 - モデル指定エラー
ChatOpenAI のmodel
に指定する名前(gpt-4o や gpt-3.5-turbo)が正しいか確認してください。 - RateLimitError が発生する
リクエスト頻度を下げるか、time.sleep()
でウェイトを入れると回避できます。 - PromptTemplate の変数とプレースホルダーが一致しない
input_variables
に定義した名前とテンプレート内の {…} が同じかチェックしてください。 - 依存パッケージのバージョン不整合
pip install --upgrade langchain langchain-openai
などでバージョンを揃えましょう。 - 会話履歴が保持されない
Memory 機能を使う場合はConversationBufferMemory
等を正しく設定し、memory.load_memory_variables
を活用してください。
6. 最後に
LangChainは、LLMアプリケーション開発において非常に強力なツールとなります。
環境構築などを自身の環境で行う必要がありますが、ベンダーの提供するツールを利用する場合と比較してより柔軟なアプリケーションを作成することができます。
是非、LLMアプリケーション開発にLangChainを活用してみてください。