見出し画像

「LangChain完全入門」を寄り道写経で完全入門②Retrieval

書籍の著者 田村悠 先生


この記事は、書籍「LangChain完全入門」の第3章「Retrieval - 未知のデータを扱えるようにする」の通称「寄り道写経」を取り扱います。
寄り道の狙いは「最近のライブラリで動かすこと」です。
したがいまして、記事に掲載する内容は「コード」に焦点を絞ります。

今回の記事に登場するコードは、Retrieval を用いることで、言語モデルが学習していない情報を外部から「検索・収集」する感じです!
シンプルなRAGチャットbotの実装があります!
では書籍を開いて言語モデル活用の旅に出発です🚀

じゅうたんのイラスト:「いらすとや」さんより

はじめに


書籍「LangChain完全入門」のご紹介

書籍「LangChain完全入門 生成AIアプリケーション開発がはかどる大規模言語モデルの操り方」(以下、テキストと呼びます)は、2023年10月に発売された LangChain の入門書です。

LangChain は大規模言語モデル(LLM)を活用するアプリケーション開発で一般的に用いられる機能を提供する優れたライブラリであり、テキストによると「執筆時点でデファクトスタンダードとなっています」とのことです。
LLM と API でつながるアプリがサクッと出来てしまう魔法のライブラリです。
LangChain公式サイトはこちら。

テキストを実践して「10数行のコードでLLMを使った簡易なチャットボットを作れる!」という衝撃に何度も出会いました。
そして、自分で書いた(テキストから写経した)チャットGPTのようなChatボットの存在がとても愛おしいのです!

テキストは、LangChain の 6つのモジュールの基礎的な利用方法をコードとコード実行結果を用いて「とてもわかりやすく」解説する優れた書籍です。
プログラミング初学者の私にとって、手入力でいいかげん丹念に写経したコードの実行結果を「テキスト掲載の正解」(テキストのコード実行結果)と照らし合わせられることは、「写経コードが正しく動いたんだ!」という安心感と達成感を確保できる「御守り」です。
安心感が「 LangChain 最高~!楽しい~!」に直結するのです。

ただし最近のPythonライブラリを使うとテキストのコードが動かない場合があります。
そこで寄り道写経。
最近のライブラリで動かせるよう、コードに手を加えます!

楽しい飲み会のイラスト(私服):「いらすとや」さんより

引用表記

この記事は、出典に記載の書籍に掲載された文章及びコードを引用し、適宜、掲載文章とコードを改変して書いています。
【出典】
「LangChain完全入門 生成AIアプリケーション開発がはかどる大規模言語モデルの操り方」初版、著者 田村悠、インプレス

記事中のイラストは、「かわいいフリー素材集いらすとや」さんのイラストをお借りしています。
ありがとうございます!


おしながき


記事の書き方

  • テキストの「.pyファイル」ごとに書きます。

  • テキストの掲載順に書きます。

  • インポート文のみの変更で動く場合には、インポート文の箇所のみ書きます。

  • インポート文以外の変更が必要な場合には、コード全文を書きます。
    変更箇所は ★印 をつけるなどして明確にします。

  • テキストのコード行番号とは一致しません。

  • 実行結果は書きません。

コードの変更方針

  • 2024年5月6日から10日までの間に変更後コードを実行して動くことを確認しました。

  • ベストな変更方法では無い可能性があります(可能性が高いです)。

  • テキストのオリジナルコードを実行することで発生するエラーの回避策を試行錯誤して考えます。

  • なるべくテキストのコードを尊重して変更します。
    新しいライブラリが期待する使用方法では無い可能性があります。

  • なるべくワーニング発生を回避します。
    ただし、私の技量でワーニング回避できない部分は文章でお知らせします。
    【緩募】ワーニング対策が分かる方、ぜひ対策を教えて下さい。

  • ところどころにコメント文を入れていますが、私的なメモです。
    気にしないでください。

Pythonと主なライブラリの使用バージョン

$$
\begin{array}{l:l}
ライブラリ等 & バージョン \\
\hline
\text{python} & 3.11.9 \\
\text{langchain} & 0.1.17 \\
\text{langchain-core} & 0.1.50 \\
\text{langchain-community} & 0.0.36 \\
\text{langchain-openai} & 0.1.6 \\
\text{langchain-text-splitters} & 0.0.1 \\
\text{langchainhub} & 0.1.15 \\
\text{openai} & 1.25.2 \\
\text{pymupdf} & 1.24.2 \\
\text{spacy} & 3.7.4 \\
\text{tiktoken} & 0.6.0 \\
\text{chromadb} & 0.5.0 \\
\text{chainlit} & 1.0.506 \\
\text{wikipedia} & 1.4.0 \\
\text{redis} & 5.0.4 \\
\text{bs4} & 0.0.2 \\
\text{google-search-results} & 2.4.2 \\
\end{array}
$$

免責事項

私の使用環境で動いたコードをそのまま掲載しており、別の環境で動かない可能性があります。
記事のコードにより生じる直接的または間接的な損害について、私では一切の責任を負いかねます。予めご了承ください。

第3章 Retrieval 関連


sample_vector.py
インポートのみ変更

# インポート
from langchain_openai import OpenAIEmbeddings               # ★_openai

(以下省略)

prepare_1.py
インポートのみ変更

# インポート
from langchain_community.document_loaders import PyMuPDFLoader  # ★_community

(以下省略)

prepare_2.py
インポートのみ変更

# インポート
from langchain_community.document_loaders import PyMuPDFLoader  # ★_community
from langchain.text_splitter import SpacyTextSplitter

(以下省略)

prepare_3.py
インポートのみ変更

# インポート
from langchain_community.document_loaders import PyMuPDFLoader  # ★_community
from langchain.text_splitter import SpacyTextSplitter
from langchain_openai import OpenAIEmbeddings                   # ★_openai
from langchain_community.vectorstores import Chroma             # ★_community

(以下省略)

quary_1.py
インポートのみ変更

# インポート
from langchain_openai import OpenAIEmbeddings           # ★_openai
from langchain_community.vectorstores import Chroma     # ★_community

(以下省略)

quary_2.py

### ベクトルデータベースで検索を実行し、質問の回答を出力する

# インポート
from langchain_openai import ChatOpenAI                # ★_openai
from langchain_openai import OpenAIEmbeddings          # ★_openai
from langchain_core.prompts import PromptTemplate      # ★_core
from langchain_core.messages import HumanMessage       # ★_core
from langchain_community.vectorstores import Chroma    # ★_community

## 質問文の作成
query = '飛行車の最高速度は?'

## 質問文に類似する文書をベクトルデータベースから抽出
# ベクトル埋め込みの設定
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002',)

# データベースの準備
database = Chroma(
    persist_directory='./.data',
    embedding_function=embeddings,
)

# ベクトルデータベースの検索 ◆質問と類似する文章を検索
documents = database.similarity_search(query)

# 検索した文章を格納(chatメッセージに使う)
documents_string = ''
for document in documents:
    documents_string += f'''
---------------------------
{document.page_content}
'''

## chatの実行
# promptの準備
prompt = PromptTemplate(
    template='''文章を元に質問に答えてください。

文章:
{document}

質問: {query}
''',
    input_variables=['document', 'query']
)

# messageの準備                                           ★追加
message = [
    HumanMessage(content=prompt.format(document=documents_string, query=query))
]
print('message:\n', message, '\n\n')                    # ★検証用

# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')

# chatの実行
result = chat.invoke(message)                           # ★invoke
print('result:\n', result, '\n\n')                      # ★検証用

# chatの返答を表示
print(result.content)

chat_1.py

### チャット画面でメッセージを送受信

# インポート
import chainlit as cl

# chat開始時に実行する関数の定義
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='準備ができました!メッセージを入力してください!'
    ).send()    # 初期表示するメッセージを送信

# メッセージ送信時に実行する関数の定義
@cl.on_message
async def on_message(input_message):
    print('入力されたメッセージ: ' + input_message.content)        # ★content
    await cl.Message(content='こんにちは!').send()

chat_2.py

### チャット画面から質問を入力

## インポート
import chainlit as cl
from langchain_openai import ChatOpenAI              # ★_opanai
from langchain_openai import OpenAIEmbeddings        # ★_openai
from langchain_core.prompts import PromptTemplate    # ★_core
from langchain_core.messages import HumanMessage     # ★_core
from langchain_community.vectorstores import Chroma  # ★_community


## 設定と準備
# ベクトル埋め込みの準備
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002',)
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# promptの準備
prompt = PromptTemplate(template='''文章を元に質問に答えてください。

文章:
{document}

質問: {query}
''', input_variables=['document', 'query'])
# データベースの準備
database = Chroma(
    persist_directory='./.data',
    embedding_function=embeddings,
)

## chatの設定

# chat開始時に実行する関数の定義
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='準備ができました!メッセージを入力してください!'
    ).send()    # 初期表示するメッセージを送信

# メッセージ送信時に実行する関数の定義 
@cl.on_message
async def on_message(input_message):
    
    ## 外部データの検索
    # ベクトルデータベースの検索 ◆chat質問メッセージと類似する文章を検索
    documents = database.similarity_search(input_message.content)

    # 検索した文章を格納
    documents_string = ''
    for document in documents:
        documents_string += f'''
    ----------------------------
    {document.page_content}
    '''
    
    ## LLMと会話
    # メッセージの編集 ◆chat質問メッセージと検索結果をまとめる
    message = [                                          # ★追加
        HumanMessage(
            content=prompt.format(
                document=documents_string,
                query=input_message.content)             # ★.content
        )
    ]

    # chatの実行、LLMからの返答を格納
    result = chat.invoke(message)                        # ★invoke

    # LLMからの返答をchat画面に表示
    await cl.Message(content=result.content).send()

chat_3.py

### RAGチャット:チャット開始時にアップロードしたPDFファイルベースにしたRAG

## インポート
import chainlit as cl
from langchain_openai import ChatOpenAI                # ★_openai
from langchain_community.document_loaders import PyMuPDFLoader  # ★_community
from langchain.text_splitter import SpacyTextSplitter
from langchain_openai import OpenAIEmbeddings          # ★_openai
from langchain_community.vectorstores import Chroma    # ★_community
from langchain_core.prompts import PromptTemplate      # ★_core
from langchain_core.messages import HumanMessage       # ★_core


## 設定と準備
# ベクトル埋め込みの準備
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002',)
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# promptの準備
prompt = PromptTemplate(template='''文章を元に質問に答えてください。

文章:
{document}

質問: {query}
''', input_variables=['document', 'query'])
# テキスト分割の準備
text_splitter = SpacyTextSplitter(chunk_size=300, pipeline='ja_core_news_sm')


## chat開始時にPDFファイルをアップロードする処理
# chat開始時に実行する関数の定義
@cl.on_chat_start
async def on_chat_start():

    ## 設定と準備
    # ファイルが選択されているかを確認する変数の初期化
    files = None
    
    ## PDFファイル取得処理
    # PDFファイルがドラッグ・アンド・ドロップされるまで待つ
    while files is None:
        files = await cl.AskFileMessage(
            max_size_mb=20,
            content='PDFファイルを選択してください',
            accept=['application/pdf'],
            raise_on_timeout=False,
        ).send()    
    # 取得したPDFファイル情報の代入
    file = files[0]
    
    ## PDFファイルをベクトルデータベースに格納する処理
    # PDFファイルの読み込み
    documents = PyMuPDFLoader(file.path).load()      # ★filr.path
    # テキストの分割
    splitted_documents = text_splitter.split_documents(documents)
    # データベースのインスタンスの作成 ※永続化しない
    database = Chroma(embedding_function=embeddings)
    # データベースに分割したテキストを追加
    database.add_documents(splitted_documents)
    # データベースをセッションに保存
    cl.user_session.set('database', database)

    ## chatに読み込み終了のメッセージを送信
    await cl.Message(
        content=f'`{file.name}`の読み込みが完了しました。\n質問を入力してください。'
    ).send()


## 質問を受取り、ベクトルデータベースを検索して、LLMの回答をchatに表示
# メッセージ送信時に実行する関数の定義 
@cl.on_message
async def on_message(input_message):
    
    ## 質問に類似するテキストをベクトルデータベースから取得
    # セッションからデータベースの取得
    database = cl.user_session.get('database')
    # ベクトルデータベースの検索 ◆chat質問メッセージと類似する文章を検索
    documents = database.similarity_search(input_message.content) # ★.content
    # 検索した文章を格納
    documents_string = ''
    for document in documents:
        documents_string += f'''
    ----------------------------
    {document.page_content}
    '''
    
    ## LLMと会話
    # メッセージの編集 ◆chat質問メッセージと検索結果をまとめる
    message = [                                      # ★追加
        HumanMessage(
            content=prompt.format(
                document=documents_string,
                query=input_message.content)         # ★.content
        )
    ]

    # chatの実行、LLMからの返答を格納
    result = chat.invoke(message)                    # ★invoke

    # LLMからの返答をchat画面に表示
    await cl.Message(content=result.content).send()

quary_3.py

### ベクトルデータベースで検索を実行し、質問の回答を出力する
# ★RetrievalQAはDeprecatedになった。代替案は複雑である
# https://api.python.langchain.com/en/latest/chains/langchain.chains.retrieval_qa.base.RetrievalQA.html

# インポート ★RetrievalQAは非推奨
from langchain.chains import create_retrieval_chain    # ★ライブラリの変更
from langchain_openai import ChatOpenAI                # ★_openai
from langchain_openai import OpenAIEmbeddings          # ★_openai
from langchain_community.vectorstores import Chroma    # ★_community
# ★追加インポート
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

# 質問文の作成
query = '飛行車の最高速度は?'

# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')

# ベクトル埋め込みの準備
embeddings = OpenAIEmbeddings(model='text-embedding-ada-002',)

# データベースの準備
database = Chroma(
    persist_directory='./.data',
    embedding_function=embeddings,
)

# retrieverの準備
retriever = database.as_retriever()

# システムプロンプトの作成                               # ★追加
system_prompt = (
    '与えられたコンテキストを使って質問に答えてください。'
    '答えが分からない場合には、答えが分からないと答えてください。'
    '回答は3文以内で完結にお願いします。'
    'コンテキスト: {context}'
)

# プロンプトテンプレートの作成                            # ★追加
prompt = ChatPromptTemplate.from_messages(
    [
        ('system', system_prompt),
        ('human', "{input}"),
    ]
)

# 質問と回答のchainの準備                                  # ★追加
question_answer_chain = create_stuff_documents_chain(chat, prompt)
chain = create_retrieval_chain(retriever, question_answer_chain)

# chatの実行
result = chain.invoke({'input': query})                   # ★invoke
print('result:\n', result, '\n\n')     # 検証用

# chatの返答の表示
print(result['answer'], '\n')                             # ★answer
# ソースドキュメントの表示
print(result['context'])                                  # ★context

sample_wikipedia.py

### wikipediaを検索する

# インポート
from langchain_community.retrievers import WikipediaRetriever # ★_community

# retrieverの準備
retriever = WikipediaRetriever(lang='ja')

# 記事の検索・取得
documents = retriever.invoke(input='大規模言語モデル')         # ★invoke

print(f'検索結果: {len(documents)}')
for document in documents:
    print('----------取得したメタデータ----------')
    print(document.metadata)
    print('---------- 取得したテキスト ----------')
    print(document.page_content[:100])

wikipedia_qa_1.py

### Wikipediaをretreiverで検索し、質問の回答を出力する
# ★RetrievalQAはDeprecatedになった。代替案は複雑である
# https://api.python.langchain.com/en/latest/chains/langchain.chains.retrieval_qa.base.RetrievalQA.html

# インポート
from langchain_openai import ChatOpenAI                        # ★_openai
from langchain_community.retrievers import WikipediaRetriever  # ★_community
# ★追加インポート RetrievalQAの代用
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate

# 質問文の作成
query = 'バーボンウイスキーとは?'

# chatの準備
chat = ChatOpenAI()

# Wikipediaのretrieverの準備
retriever = WikipediaRetriever(
    lang='ja',
    doc_content_chars_max=500,
    top_k_results=2,
)

# システムプロンプトの準備                                         ★追加
system_prompt = (
    '与えられたコンテキストを使って質問に答えてください。'
    '答えが分からない場合には、答えが分からないと答えてください。'
    '回答は3文以内で完結にお願いします。'
    'コンテキスト: {context}'
)

# プロンプトテンプレートの準備                                     ★追加
prompt = ChatPromptTemplate.from_messages(
    [
        ('system', system_prompt),
        ('human', '{input}'),
    ]
)

# 質問と回答のchainの準備                                         ★追加
question_answer_chain = create_stuff_documents_chain(chat, prompt)
chain = create_retrieval_chain(retriever, question_answer_chain)

# chatの実行
result = chain.invoke({'input': query})                        # ★invoke
print('result:\n', result, '\n\n')     # 検証用

# Wikipedia検索をして作成した回答結果を表示
source_documents = result['context']                           # ★context
for document in source_documents:
    print('----------取得したメタデータ----------')
    print(document.metadata)
    print('----------取得したテキスト----------')
    print(document.page_content[:100])
print('----------返答----------')
print(result['answer'])                                        # ★answer

sample_wikipedia .py

### wikipediaを検索する

# インポート
from langchain_community.retrievers import WikipediaRetriever # ★_community

# retrieverの準備
retriever = WikipediaRetriever(
    lang='ja',
    doc_content_chars_max=100,
    top_k_results=1
)

# 記事の検索・取得
documents = retriever.invoke(                                 # ★invoke
    input='私はラーメンが好きです。ところでバーボンウイスキーとは何ですか?')
print(documents)

re_phrase_query.py

### 質問を再構築してWikipediaをretreiverで検索、回答を得る
# ワーニングが2つ出る
# ・LangChainDeprecationWarning: Importing GuardrailsOutputParser
# ・The class `LLMChain` was deprecated

# インポート
from langchain_openai import ChatOpenAI                        # ★_openai
from langchain_community.retrievers import WikipediaRetriever  # ★_community
from langchain.retrievers import RePhraseQueryRetriever
from langchain.chains import LLMChain                          # ★.chains
from langchain_core.prompts import PromptTemplate              # ★_core

# 質問文の作成
query = 'バーボンウイスキーとは?'

# chatの準備
chat = ChatOpenAI(temperature=0)

# promptの準備
prompt = PromptTemplate(
    input_variables=['question'],
    template='''以下の質問からwikipediaで検索するべきキーワードを抽出してください。
質問: {question}
'''
)

# llm_chainの準備
llm_chain = LLMChain(llm=chat, prompt=prompt) # ワーニングの原因

# Wikipediaのretrieverの準備
retriever = WikipediaRetriever(
    lang='ja',
    doc_content_chars_max=500,
)

# 質問を再構築するretrieverの準備
re_phrase_query_retriever = RePhraseQueryRetriever(
    llm_chain=llm_chain,  # LLMChainを使う必要がある(ワーニングの原因)
    retriever=retriever,
)

# re_phrase_query_retrieverの実行
documents = re_phrase_query_retriever.invoke(                   # ★invoke
    '私はラーメンが好きです。ところでバーボンウイスキーとは何ですか?'
)
print(documents)

第3章の寄り道写経は以上です。


シリーズの記事

次の記事

前の記事

目次

ブログの紹介


note で7つのシリーズ記事を書いています。
ぜひ覗いていってくださいね!

1.のんびり統計

統計検定2級の問題集を手がかりにして、確率・統計をざっくり掘り下げるブログです。
雑談感覚で大丈夫です。ぜひ覗いていってくださいね。
統計検定2級公式問題集CBT対応版に対応しています。
Python、EXCELのサンプルコードの配布もあります。

2.実験!たのしいベイズモデリング1&2をPyMC Ver.5で

書籍「たのしいベイズモデリング」・「たのしいベイズモデリング2」の心理学研究に用いられたベイズモデルを PyMC Ver.5で描いて分析します。
この書籍をはじめ、多くのベイズモデルは R言語+Stanで書かれています。
PyMCの可能性を探り出し、手軽にベイズモデリングを実践できるように努めます。
身近なテーマ、イメージしやすいテーマですので、ぜひぜひPyMCで動かして、一緒に楽しみましょう!

3.実験!岩波データサイエンス1のベイズモデリングをPyMC Ver.5で

書籍「実験!岩波データサイエンスvol.1」の4人のベイジアンによるベイズモデルを PyMC Ver.5で描いて分析します。
この書籍はベイズプログラミングのイロハをざっくりと学ぶことができる良書です。
楽しくPyMCモデルを動かして、ベイズと仲良しになれた気がします。
みなさんもぜひぜひPyMCで動かして、一緒に遊んで学びましょう!

4.楽しい写経 ベイズ・Python等

ベイズ、Python、その他の「書籍の写経活動」の成果をブログにします。
主にPythonへの翻訳に取り組んでいます。
写経に取り組むお仲間さんのサンプルコードになれば幸いです🍀

5.RとStanではじめる心理学のための時系列分析入門 を PythonとPyMC Ver.5 で

書籍「RとStanではじめる心理学のための時系列分析入門」の時系列分析をPythonとPyMC Ver.5 で実践します。
この書籍には時系列分析のテーマが盛りだくさん!
時系列分析の懐の深さを実感いたしました。
大好きなPythonで楽しく時系列分析を学びます。

6.データサイエンスっぽいことを綴る

統計、データ分析、AI、機械学習、Pythonのコラムを不定期に綴っています。
統計・データサイエンス書籍にまつわる記事が多いです。
「統計」「Python」「数学とPython」「R」のシリーズが生まれています。

7.Python機械学習プログラミング実践記

書籍「Python機械学習プログラミング PyTorch & scikit-learn編」を学んだときのさまざまな思いを記事にしました。
この書籍は、scikit-learnとPyTorchの教科書です。
よかったらぜひ、お試しくださいませ。

最後までお読みいただきまして、ありがとうございました。

この記事が参加している募集

新生活をたのしく

この記事が気に入ったらサポートをしてみませんか?