見出し画像

「LangChain完全入門」を寄り道写経で”ほぼ”完全入門③MemoryとChains

書籍の著者 田村悠 先生


この記事は、書籍「LangChain完全入門」の第4章「Memory - 過去の対話を短期・長期で記憶する」と第5章「Chains - 複数の処理をまとめる」の通称「寄り道写経」を取り扱います。
寄り道の狙いは「最近のライブラリで動かすこと」です。
したがいまして、記事に掲載する内容は「コード」に焦点を絞ります。

Memory は、会話履歴を保存することによって、過去の会話の文脈を考慮した回答を得るための機能です。
Chains は、複数の処理をまとめるための機能です。
チャットbotの経験値がどんどん上がっています!

しかし今回、遂にエラー解消できない事象に突き当たってしまいました。
言語モデルとlangchainの世界は奥深いです。。。
それはさておきさておき・・・
さあ書籍を開いて言語モデル活用の旅に出発です🚀

深海救難艇のイラスト:「いらすとや」さんより

はじめに


書籍「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}
$$

免責事項

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

第4章 Memory 関連


chat_sample_1.py

### HumanMessageとAIMessageが繰り返し会話する

# インポート
from langchain_openai import ChatOpenAI                    # ★_openai
from langchain_core.messages import HumanMessage           # ★_core

# 質問文の設定
query = '茶碗蒸しを作るのに必要な食材を教えて'

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

# messageの準備                                            # ★追加
message = [
    HumanMessage(content=query)
]

# chatの実行
result = chat.invoke(message)                              # ★invoke
print(result.content)

chat_sample_2.py

### 会話履歴をコードに持たせる

from langchain_openai import ChatOpenAI                      # ★_openai
from langchain_core.messages import HumanMessage, AIMessage  # ★_core

# 質問文の設定
query = '茶碗蒸しを作るのに必要な食材を教えて'

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

# messageの準備                                               # ★追加
message = [
    HumanMessage(content=query),
    AIMessage(
        content='''茶碗蒸しを作るためには、以下の食材が必要です。

1. 卵
2. 鶏肉や豚肉などの具材
3. しいたけやにんじんなどの野菜
4. だし汁
5. 醤油
6. みりん
7. 塩
8. ごま油

これらの食材を用意して、茶碗蒸しを作る手順に従って調理してください。'''),
    HumanMessage(content='前の回答を英語に翻訳して'),
]

# chatの実行
result = chat.invoke(message)                                    # ★invoke
print(result.content)

sanple.py
変更不要

chat_memory_1.py

### 会話履歴を保存できるチャットbot

# インポート
import chainlit as cl
from langchain_openai import ChatOpenAI                    # ★_opanai
from langchain.memory import ConversationBufferMemory
from langchain_core.messages import HumanMessage           # ★_core

## 設定と準備
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# memoryの準備
memory = ConversationBufferMemory(return_messages=True)

## チャットボット処理
# チャット起動時の処理:メッセージを画面表示
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='私は会話の文脈を考慮した返答ができるチャットボットです。'
                'メッセージを入力してください'
    ).send()

# チャットメッセージ受取時の処理
@cl.on_message
async def on_message(message):
    # memoryの内容を取得
    memory_message_result = memory.load_memory_variables({})
    # messagesに会話履歴を設定
    messages = memory_message_result['history']
    # 受け取ったメッセージをHumanMessageの形式でmessagesに追加
    messages.append(HumanMessage(content=message.content))  # ★.content
    # chatの実行(全会話履歴messagesを送信)
    result = chat.invoke(messages)                          # ★ invoke
    # ユーザの質問messageとLLMの回答resultをmemoryに追加
    memory.save_context(
        {
            'input': message.content,                       # ★.content
        },
        {
            'output': result.content,
        },
    )
    # chatの回答を画面表示
    await cl.Message(content=result.content).send()

chat_memory_2.py

### 会話履歴を保存できるチャットbot, CanversationChain使用(短いコードになる)

# インポート
import chainlit as cl
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI                  # ★_opanai
from langchain.memory import ConversationBufferMemory

## 設定と準備
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# memoryの準備
memory = ConversationBufferMemory(return_messages=True)
# ConversationChainの準備
chain = ConversationChain(
    memory=memory,
    llm=chat,
)

## チャットボット処理

# チャット起動時の処理:メッセージを画面表示
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='私は会話の文脈を考慮した返答ができるチャットボットです。'
                'メッセージを入力してください。'
    ).send()

# チャットメッセージ受取時の処理
@cl.on_message
async def on_message(message: str):
    # ConversationChainを用いて言語モデルを呼び出し、chatを実行
    result = chain.invoke(message.content)        # ★ invoke, .content
    # chatの回答を画面表示
    await cl.Message(content=result['response']).send()

chat_memory_3.py
エラーの発生を解消できず、掲載を省略します。
on_message関数にて redis にアクセスする時にエラーが発生します。

Error while reading from **API_KEYです** : (100**, '既存の接続はリモートホストに強制的に切断されました。', None, 100**, None)

chat_memory_4.py
エラーの発生を解消できず、掲載を省略します。
on_message関数にて redis にアクセスする時にエラーが発生します。

Error while reading from **API_KEYです** : (100**, '既存の接続はリモートホストに強制的に切断されました。', None, 100**, None)

【緩募】
redis の接続エラーの解消策をご存じの方、ぜひ教えて下さい!

いろいろなお辞儀をする人と「よろしくお願いします」のイラスト:「いらすとや」さんより

language_models.py

### 会話履歴をクリアしないためコンテキスト長の限界を超えてエラー発生する例
# ただしエラー発生しませんでした

# インポート
from langchain_openai import ChatOpenAI                        # ★_openai
from langchain_core.messages import HumanMessage, AIMessage    # ★_core

# chatの準備
chat = ChatOpenAI()

# messageの準備                                                # ★追加
message = [
    HumanMessage(content='茶碗蒸しの作り方を教えて'),
    AIMessage(content='{ChatModelからの返答である茶碗蒸しの作り方}'),
    HumanMessage(content='餃子の作り方を教えて'),
    AIMessage(content='{ChatModelからの返答である餃子の作り方}'),
    HumanMessage(content='チャーハンの作り方を教えて'),
]

result = chat.invoke(message)                                  # ★invoke
print(result.content)

custom_memory_1.py

### 古い会話を削除する

# インポート
import chainlit as cl
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI                     # ★_openai
from langchain.memory import ConversationBufferWindowMemory

## 設定と準備
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# memoryの準備
memory = ConversationBufferWindowMemory(
    return_messages=True,
    k=3,
)
# chainの準備
chain = ConversationChain(
    memory=memory,
    llm=chat,
)

## chat画面の設定

# chat画面起動時の処理
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='私は会話の文脈を考慮した返答ができるチャットボットです。'
                'メッセージを入力してください。'
    ).send()

# chatメッセージ受取時の処理
@cl.on_message
async def on_message(message: str):
    # 保存されているメッセージを取得
    messages = chain.memory.load_memory_variables({})['history']
    # 保存されているメッセージを標準出力に表示
    print(f'保存されているメッセージの数: {len(messages)}')
    for saved_massage in messages:
        print(saved_massage.content)
    # OpenAIチャットにメッセージを送信して回答を取得
    result = chain.invoke(message.content)      # ★invoke, .content
    # OpenAIチャットの回答をchat画面に表示
    await cl.Message(content=result['response']).send()

custom_memory_2.py

### 会話を要約することでトークン数制限に対策する

# インポート
import chainlit as cl
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI                     # ★_openai
from langchain.memory import ConversationSummaryMemory

## 設定と準備
# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# memoryの準備
memory = ConversationSummaryMemory(
    llm=chat,
    return_messages=True,
)
# chainの準備
chain = ConversationChain(
    memory=memory,
    llm=chat,
)

## chat画面の設定

# chat画面起動時の処理
@cl.on_chat_start
async def on_chat_start():
    await cl.Message(
        content='私は会話の文脈を考慮した返答をできるチャットボットです。\n'
                'メッセージを入力してください。'
    ).send()

# chatメッセージ受取時の処理
@cl.on_message
async def on_message(message: str):
    # 保存されているメッセージを取得
    messages = chain.memory.load_memory_variables({})['history']
    # 保存されているメッセージを標準出力に表示
    print(f'保存されているメッセージの数: {len(messages)}')
    for saved_massage in messages:
        print(saved_massage.content)
    # OpenAIチャットにメッセージを送信して回答を取得
    result = chain.invoke(message.content)      # ★invoke, .content
    # OpenAIチャットの回答をchat画面に表示
    await cl.Message(content=result['response']).send()

第5章 Chains 関連


LangChain の バージョンアップによって、Chains 関連の使い方がガラリと変わっているように感じました。
コードスタイルなども大きく変わっています。
一方で機能変更が発展途上であり、古い機能と新しい機能の混在がまだ必要な状況であるとも感じました。
この記事では、テキストのコードを活かす方向、つまり、新機能の意図と異なる場合であってもテキストのコードを活かす方向で、コード変更をしました。

llmchain.py

### 複数のモジュールをまとめる

# インポート
from langchain_core.prompts import PromptTemplate # ★_core
from langchain_openai import ChatOpenAI           # ★_openai

# chatの準備
chat = ChatOpenAI(model='gpt-3.5-turbo')
# プロンプトの生成
prompt = PromptTemplate(
    template='{product}はどこの会社が開発した製品ですか?',
    input_variables=['product'],
)

# chainの準備 (RunnableSequence)
chain = prompt | chat                             # ★RunnableSequence

# chatを実行して回答を取得
result = chain.invoke('iPhone')                   # ★invoke

# 回答結果の表示
print(result)           # 全文                    # ★検証用
print(result.content)   # 回答文のみ               # ★.content

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

# インポート
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI                 # ★_openai
from langchain.memory import ConversationBufferMemory

(以下省略)

request_chain.py

### 指定したURLから取得した情報を元に回答を生成する
# LLMChainは非推奨だが、LLMRequestsChain併用時の代替案が見つからないため使用した

# インポート
from langchain.chains import LLMChain, LLMRequestsChain
from langchain_openai import ChatOpenAI                  # ★_openai
from langchain_core.prompts import PromptTemplate        # ★_core

## 設定と準備
# chatモデルの準備
chat = ChatOpenAI()

# プロンプトの準備
prompt = PromptTemplate(
    input_variables=['query',
                     'requests_result'],
    template='''以下の文章を元に質問に答えてください。
文章: {requests_result}
質問: {query}''',
)

# llm_chainの準備
# LLMChainは非推奨だが、LLMRequestsChain併用の代替案が見つからないため使用
llm_chain = LLMChain(
    llm=chat,
    prompt=prompt,
    verbose=True,
)

# chainの準備
chain = LLMRequestsChain(
    llm_chain=llm_chain,
)

## chatの実行
result = chain.invoke({                                   # ★.invoke
    'query': '東京の天気について教えて',
    'url': 'https://www.jma.go.jp/bosai/forecast/data/overview_forecast/130000.json',
})

print('result\n', result['output'])                       # ★['output']

sequential_chain.py

### LLMに記事執筆と英語翻訳を依頼して回答を得る
# LLMChainは非推奨だが、LLMRequestsChain併用時の代替案が見つからないため使用した

# インポート
from langchain.chains import LLMChain, SimpleSequentialChain
from langchain_openai import ChatOpenAI              # ★_openai
from langchain_core.prompts import PromptTemplate    # ★_core

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

# 機能1:記事を書く機能のchainの作成
write_article_chain = LLMChain(      # LLMChainは非推奨
    llm=chat,
    prompt=PromptTemplate(
       template='{input}についての記事を書いてください。',
        input_variables=['input']),
)

# 機能2:翻訳する機能のchainの作成
translate_chain = LLMChain(          # LLMChainは非推奨
    llm=chat,
    prompt=PromptTemplate(
        template='以下の文章を英語に翻訳してください。\n{input}',
        input_variables=['input']),
)

# 2つの機能を順番に処理するchainの作成
sequential_chain = SimpleSequentialChain(
    chains=[write_article_chain, translate_chain],
)

# chatの実行
result = sequential_chain.invoke('エレキギターの選び方')  # ★.invoke
print(result['output'])                                  # ★output


第4章、第5章の寄り道写経は以上です。


シリーズの記事

次の記事

前の記事

目次

ブログの紹介


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の教科書です。
よかったらぜひ、お試しくださいませ。

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

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

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