はじめに
LangChainの安定版V0.1.0がリリースされたのを記念して、
LangChainゆる勉強会を開催します。
LangChainとは?
LangChain is a framework for developing applications powered by language models. It enables applications that:
Are context-aware: connect a language model to sources of context (prompt instructions, few shot examples, content to ground its response in, etc.)
Reason: rely on a language model to reason (about how to answer based on provided context, what actions to take, etc.)
LangChainは言語モデルを利用したアプリケーション開発のためのフレームワークです。
以下のようなアプリケーションを可能にします:
コンテキストを認識する:言語モデルをコンテキストのソース(プロンプトの指示、いくつかの例、応答の根拠となるコンテンツなど)に接続する。
推論する:言語モデルに依存して推論する(提供されたコンテキストに基づいてどのように答えるか、どのようなアクションを取るか、など)。
LangChainに用意されている6つのモジュール
-
Model I/O 言語モデルを扱いやすくする
- プロンプト準備、言語モデルの呼び出し、結果の受け取りの3つのステップを簡単に実装するための機能
-
Retrieval 未知のデータを扱えるようにする
- LLMは未知のデータ(学習していないデータ)を扱えません
- この問題に対応するのがRetrieval-Augmented Generation(RAG)です
-
Memory 過去の対話を短期・長期で記憶する
- 前の文脈を踏まえた対話形式で言語モデルに回答させるには、それまでの会話をすべて送信する必要がある
- 会話履歴の保存と、読み込みを簡単にする機能を提供
-
Chains 複数の処理をまとめる
- LangChainでは多数のモジュールや別の機能を組み合わせつつ1つのアプリケーションを作成
- このモジュールは組み合わせを簡単にする機能を提供します
-
Agents 自律的に外部と干渉して言語モデルの限界を超える
- ReActやOpenAI Function Callingという手法を用い、言語モデルでは対応できないタスクをおこなう
-
Callbacks さまざまなイベント発生時に処理を行う
- ログ出力や外部ライブラリとの連携
参考書
以下の書籍を参考にしています。
LangChain完全入門 生成AIアプリケーション開発がはかどる大規模言語モデルの操り方
書籍のソースコードはここにあります。
https://github.com/harukaxq/langchain-book
今回の環境の特徴
多くの場合、LangChainから接続するLLMは、OpenAI社のAPIを使うと思います。(そのような記事が圧倒的に多いです)
しかし、今回は、私の環境の都合でAmazon Bedrockを利用します。
なお、以下の書籍ではLangChainのバージョンは、0.0.261を利用していますが、せっかくなので、最新安定版の0.1.0を利用します。
環境のセットアップ
今回は以下の環境を使っています。
- MacOS
- 複数バージョンの管理:https://asdf-vm.com/
- Pythonのパッケージマネージャー:https://python-poetry.org/
なお、poetry、Pythonともにasdfからインストールできます。
ここでは、asdfがインストールされている前提で、Python、Poetryのインストールから説明します。
Pythonインストール(asdfから)
基本的には以下のサイトの手順でOKです。
https://dev.classmethod.jp/articles/asdf-python-introduction/
# Python のプラグインとリポジトリを確認します
% asdf plugin list all | grep python
(出力↓)
python *https://github.com/danhper/asdf-python.git
# プラグインをインストールします。
% asdf plugin add python https://github.com/danhper/asdf-python.git
# プラグインがインストールできたか確認します。
% asdf plugin list
(出力↓)
python
# インストール可能なバージョン一覧を確認
% asdf list all python
(出力↓)
ズラズラと出てきます。
# 今回はPython3.11.6をローカルにインストールします。
% asdf install python 3.11.6
% asdf local python 3.11.6
% python --version
(出力↓)
Python 3.11.6
poetryインストール(asdfから)
poetryのインストールもPythonと基本的な手順は一緒です。
% asdf plugin-add poetry
% asdf list-all poetry
(出力↓)
(中略)
1.7.1
% asdf install poetry 1.7.1
% asdf local poetry 1.7.1
% poetry --version
(出力↓)
Poetry (version 1.7.1)
poetryのプロジェクト初期化
# poetry初期化(基本的に全部デフォルトで良い)
% poetry init
# 仮想環境の使用
% poetry config virtualenvs.in-project true --local
# pythonバージョン確認
% poetry run python --version
% poetry env list
LangChainのインストール
以下のようにしてv0.1.0を指定します。
poetry add langchain==0.1.0
この結果は、以下のようにpyproject.tomlに書き込まれます。
[tool.poetry]
name = "impress-book-v0-1-0"
version = "0.1.0"
description = ""
authors = ["Akira Abe"]
readme = "README.md"
packages = [{include = "impress_book_v0"}]
[tool.poetry.dependencies]
python = "^3.11"
langchain = "0.1.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
環境変数などの定義
.envを作成しAWSのアクセスキーなどを登録します。
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=YYYYYYYYYYYYYYYYYYYY
AWS_DEFAULT_REGION=us-east-1
MODEL_ID=anthropic.claude-v2
警告
.envはgitにコミットしないでください!
Model I/Oのサンプル実装
このタイミングで他に必要なライブラリをインストールします。
poetry add python-dotenv==1.0.0
poetry add boto3==1.34.16
poetry add langchain-community==0.0.11
まずは非常に単純な、LLMを呼び出すケース
import os
from dotenv import load_dotenv
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
import langchain
langchain.verbose = True
load_dotenv()
chat = BedrockChat(
model_id=os.getenv("MODEL_ID"),
streaming=True,
model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
)
result = chat([HumanMessage(content="Hello, how are you?")])
print(result.content)
プロンプトテンプレートを使ったケース
import os
from dotenv import load_dotenv
from langchain_community.chat_models import BedrockChat
from langchain_core.messages import HumanMessage
from langchain_core.prompts import PromptTemplate
import langchain
langchain.verbose = True
load_dotenv()
# BedrockChatを初期化する
chat = BedrockChat(
model_id=os.getenv("MODEL_ID"),
streaming=True,
model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
)
# PromptTemplateを使って、文章を生成する
prompt = PromptTemplate(
template="{product}はどこの会社が開発した製品ですか?",
input_variables=[
"product"
]
)
result = chat(
[
HumanMessage(content=prompt.format(product="iPhone")),
]
)
print(result.content)
OutputParserを用いてLLMからの出力をパースするケース
import os
from dotenv import load_dotenv
from langchain_community.chat_models import BedrockChat
# from langchain_core.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field, ValidationInfo, field_validator
import langchain
langchain.verbose = True
load_dotenv()
# BedrockChatを初期化する
chat = BedrockChat(
model_id=os.getenv("MODEL_ID"),
streaming=True,
model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
)
# Pydanticのモデルを定義する
class Smartphone(BaseModel):
release_date: str= Field(description="スマートフォンの発売日") #← Fieldを使って説明を追加する
screen_inches: float = Field(description="スマートフォンの画面サイズ(インチ)")
os_installed: str = Field(description="スマートフォンにインストールされているOS")
model_name: str = Field(description="スマートフォンのモデル名")
@field_validator("screen_inches") #← validatorを使って値を検証する
def validate_screen_inches(cls, field: int, info: ValidationInfo): #← validatorの引数には、検証するフィールドと値が渡される
assert info.config is not None #← configがNoneでないことを確認する
if field <= 0: #← screen_inchesが0以下の場合はエラーを返す
raise ValueError("Screen inches must be a positive number")
return field
# PydanticOutputParserを定義する
parser = OutputFixingParser.from_llm(
parser = PydanticOutputParser(pydantic_object=Smartphone),
llm=chat
) #← PydanticOutputParserをSmartPhoneモデルで初期化する
result = chat([ #← Chat modelsにHumanMessageを渡して、文章を生成する
HumanMessage(content="Androidで最近リリースしたスマートフォンを1個挙げて"),
HumanMessage(content=parser.get_format_instructions())
])
parsed_result = parser.parse(result.content) #← PydanticOutputParserを使って、文章をパースする
print(f"モデル名: {parsed_result.model_name}")
print(f"画面サイズ: {parsed_result.screen_inches}")
print(f"OS: {parsed_result.os_installed}")
print(f"発売日: {parsed_result.release_date}")
Retrievalのサンプル実装
このタイミングでchainlitなどのライブラリをインストールします。
poetry add spacy==3.7.2
poetry add pymupdf==1.23.8
poetry add tiktoken==0.5.2
poetry add chromadb==0.4.22
poetry add chainlit==0.6.402
# spacyの日本語対応モデルをダウンロードする
poetry run python -m spacy download ja_core_news_sm
PDFの内容を元に回答するケース
import os
from dotenv import load_dotenv
import chainlit as cl
from langchain_community.chat_models import BedrockChat
from langchain.document_loaders import PyMuPDFLoader
from langchain.embeddings import BedrockEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_core.messages import HumanMessage
from langchain.text_splitter import SpacyTextSplitter
from langchain_community.vectorstores import Chroma
import langchain
langchain.verbose = True
load_dotenv()
# Chat開始時に検索対象のPDFをアップロードするようにする。
embeddings = BedrockEmbeddings()
chat = BedrockChat(
model_id=os.getenv("MODEL_ID"),
streaming=True,
model_kwargs={"temperature": 0.5, "max_tokens_to_sample" : 2048}
)
prompt = PromptTemplate(template="""文章を元に質問に答えてください。
文章:
{document}
質問: {query}
""", input_variables=["document", "query"])
text_splitter = SpacyTextSplitter(chunk_size=300, pipeline="ja_core_news_sm")
@cl.on_chat_start
async def on_chat_start():
files = None #← ファイルが選択されているか確認する変数
while files is None: #← ファイルが選択されるまで繰り返す
files = await cl.AskFileMessage(
max_size_mb=20,
content="PDFを選択してください",
accept=["application/pdf"],
raise_on_timeout=False,
).send()
file = files[0]
if not os.path.exists("tmp"): #← tmpディレクトリが存在するか確認
os.mkdir("tmp") #← 存在しなければ作成する
with open(f"tmp/{file.name}", "wb") as f: #← PDFファイルを保存する
f.write(file.content) #← ファイルの内容を書き込む
documents = PyMuPDFLoader(f"tmp/{file.name}").load() #← 保存したPDFファイルを読み込む
splitted_documents = text_splitter.split_documents(documents) #← ドキュメントを分割する
database = Chroma( #← データベースを初期化する
embedding_function=embeddings,
# 今回はpersist_directoryを指定しないことでデータベースの永続化を行わない
)
database.add_documents(splitted_documents) #← ドキュメントをデータベースに追加する
cl.user_session.set( #← データベースをセッションに保存する
"database", #← セッションに保存する名前
database #← セッションに保存する値
)
await cl.Message(content=f"`{file.name}`の読み込みが完了しました。質問を入力してください。").send() #← 読み込み完了を通知する
@cl.on_message
async def on_message(input_message):
print("入力されたメッセージ: " + input_message)
database = cl.user_session.get("database") #← セッションからデータベースを取得する
documents = database.similarity_search(input_message)
documents_string = ""
for document in documents:
documents_string += f"""
---------------------------
{document.page_content}
"""
result = chat([
HumanMessage(content=prompt.format(document=documents_string,
query=input_message)) #← input_messageに変更
])
await cl.Message(content=result.content).send()