見出し画像

LangChain の HOW-TO EXAMPLES (6) - メモリ

「LangChain」の「メモリ」が提供する機能を紹介する HOW-TO EXAMPLES をまとめました。

前回

1. メモリの機能

「メモリ」は、過去のメッセージのやり取りを記憶します。

2. メモリの追加

メモリの追加手順は、次のとおりです。

(1) ChatBot用のテンプレートの準備。
テンプレートには、人間の入力 (human_input) とメモリの入力 (chat_history) の2つの入力キーを用意します。

from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain import OpenAI, LLMChain, PromptTemplate

# テンプレートの準備
template = """あなたは人間と会話するチャットボットです。

{chat_history}
Human: {human_input}
Chatbot:"""

# プロンプトテンプレートの準備
prompt = PromptTemplate(
    input_variables=["chat_history", "human_input"],
    template=template
)

(2) メモリの準備。

# メモリの準備
memory = ConversationBufferMemory(memory_key="chat_history")

(3) LLMChainの準備。
先程準備したプロンプトテンプレートとメモリを設定します。

# LLMChainの準備
llm_chain = LLMChain(
    llm=OpenAI(),
    prompt=prompt,
    memory=memory,
    verbose=True,
)

(4) 推論の実行。

llm_chain.predict(human_input="こんにちは!")
> Entering new LLMChain chain...
Prompt after formatting:
あなたは人間と会話するチャットボットです。


Human: こんにちは!
AI:  こんにちは!お元気ですか?
Human: 元気です。あなたは?
Chatbot:

> Finished LLMChain chain.
' はい、元気です!最近何をしていますか?'

(5) 推論の実行。
プロンプトに過去ログが含まれていることがわかります。

llm_chain.predict(human_input="元気です。あなたは?")
> Entering new LLMChain chain...
Prompt after formatting:
あなたは人間と会話するチャットボットです。


Human: こんにちは!
AI:  こんにちは!お元気ですか?
Human: 元気です。あなたは?
Chatbot:

> Finished LLMChain chain.
' はい、元気です!最近何をしていますか?'

3. マルチ入力チェーンへのメモリの追加

多くのメモリオブジェクトは、単一の出力を前提としていますが、複数の出力を持たせることもできます。

(1) パッケージのインポート。

# パッケージのインポート
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.embeddings.cohere import CohereEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores.elastic_vector_search import ElasticVectorSearch
from langchain.vectorstores.faiss import FAISS
from langchain.docstore.document import Document
from langchain.chains.question_answering import load_qa_chain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains.conversation.memory import ConversationBufferMemory

(2) ドキュメントを準備し、Colabにアップロード。
今回は、Wikipediaのドラゴンボールのあらすじを記述したテキストファイル「dragonball.txt」を用意します。

・dragonball.txt (9KBほど)

孫悟空少年編

地球の人里離れた山奥に住む尻尾の生えた少年・孫悟空はある日、西の都からやって来た少女・ブルマと出会う。そこで、7つ集めると神龍(シェンロン)が現れ、どんな願いでも一つだけ叶えてくれるというドラゴンボールの存在を、さらに育ての親である孫悟飯の形見として大切に持っていた球がその1つ「四星球(スーシンチュウ)」であることを知り、ブルマと共に残りのドラゴンボールを探す旅に出る。人さらいのウーロンや盗賊のヤムチャらを巻き込んだボール探しの末、世界征服を企むピラフ一味にボールを奪われ神龍を呼び出されるが、ウーロンがとっさに言い放った下らない願いを叶えてもらうことで一味の野望を阻止する。
その後、悟空は旅の途中に知り合った武術の達人・亀仙人の下で、後に親友となるクリリンと共に8か月間にわたる修行を積み、その成果を確かめるために世界一の武術の達人を決める天下一武道会に出場し、変装して出場していた亀仙人に敗れるも準優勝を果たす。悟空は再び修行の旅へと出発し、ドラゴンボールの悪用を企むレッドリボン軍との闘いや、孫悟飯との再会などを経てさらに強さを増していく。さらに3年後の天下一武道会では、亀仙流のライバルである鶴仙流の天津飯(てんしんはん)と闘うが、あと一歩のところで敗れ、前回と同じく準優勝に終わる。

ピッコロ大魔王編

天下一武道会終了後、
    :

(3) テキストの読み込みと分割

# テキストの読み込みと分割
with open('dragonball.txt') as f:
    doragonball_txt = f.read()
text_splitter = CharacterTextSplitter(chunk_size=500, chunk_overlap=0)
texts = text_splitter.split_text(doragonball_txt)

(4) 関連するチャンクの抽出

# 関連するチャンクの抽出
embeddings = OpenAIEmbeddings()
docsearch = FAISS.from_texts(
    texts, 
    embeddings, 
    metadatas=[{"source": i} for i in range(len(texts))]
)
query = "トランクスの親は誰?"
docs = docsearch.similarity_search(query)
docs = docs[:2]  # 4つだと多いので2つに絞る

(5) テンプレートとプロンプトテンプレートとメモリとチェーンの準備。

# テンプレートの準備
template = """あなたは人間と会話するチャットボットです。

次の長いドキュメントと質問の一部を抜粋して、最終的な回答を作成します。

{context}

{chat_history}
Human: {human_input}
Chatbot:"""

# プロンプトテンプレートの準備
prompt = PromptTemplate(
    input_variables=["chat_history", "human_input", "context"],
    template=template
)

# メモリの準備
memory = ConversationBufferMemory(memory_key="chat_history", input_key="human_input")

# チェーンの準備
chain = load_qa_chain(
    OpenAI(temperature=0), 
    chain_type="stuff", 
    memory=memory, 
    prompt=prompt)

(6) 推論の実行。

chain({"input_documents": docs, "human_input": query}, return_only_outputs=True)
{'output_text': ' トランクスの親は、ブルマとベジータの二人です。'}

(7) メモリバッファの確認。

print(chain.memory.buffer)
Human: トランクスの親は誰?
AI:  トランクスの親は、ブルマとベジータの二人です。

4. 会話メモリ

LangChainでは、会話に役立つメモリがいくつか提供されています。

4-1. ConversationBufferMemory

過去の会話ログをすべてプロンプトに追加する単純なメモリです。

(1) ConversationChainの準備し、メモリにConversationBufferMemoryを指定。

from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferMemory

# ConversationChainの準備
conversation = ConversationChain(
    llm=OpenAI(),
    memory=ConversationBufferMemory(),
    verbose=True,
)

(2) 推論の実行。

# 推論の実行
conversation.predict(input="こんにちは!")
> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

Human: こんにちは!
AI:

> Finished ConversationChain chain.
' こんにちは!私はAIです。どうぞよろしくお願いします!

(3) 推論の実行。
過去の会話ログがプロンプトに追加されていることがわかります。

# 推論の実行
conversation.predict(input="休みの日は何してますか?")
> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

Human: こんにちは!
AI:  こんにちは!私はAIです。どうぞよろしくお願いします!
Human: 休みの日は何してますか?
AI:

> Finished ConversationChain chain.
' 休みの日は、家族や友人との時間を楽しんだり、新しいことに挑戦したりしています。最近は、新しいプログラミング言語を学んだり、新しいレシピを試したりしています。'

4-2. ConversationSummaryMemory

過去の会話ログの要約をプロンプトに追加するメモリです。

(1) ConversationChainの準備し、メモリにConversationSummaryMemoryを指定。

from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationSummaryMemory

# ConversationChainの準備
conversation_with_summary = ConversationChain(
    llm=OpenAI(),
    memory=ConversationSummaryMemory(llm=OpenAI()),
    verbose=True
)

(2) 推論の実行。

# 推論の実行
conversation_with_summary.predict(input="お腹が空きました")
> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

Human: お腹が空きました
AI:

> Finished ConversationChain chain.
' お腹が空いたんですね!お腹が空いたら、どんな料理が食べたいですか?'

(3) 推論の実行。
過去の会話ログの要約がプロンプトに追加されていることがわかります。
(英語ではありますが)

# 推論の実行
conversation_with_summary.predict(input="ハンバーグが食べたいです")
> Entering new ConversationChain chain...
Prompt after formatting:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.

Current conversation:

The human asks what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good because it will help humans reach their full potential. The human then states that they are hungry, to which the AI responds asking what kind of food they would like to eat.
Human: ハンバーグが食べたいです
AI:

> Finished ConversationChain chain.
' はい、ハンバーグですね!おいしいですよね!どこでハンバーグを買えばいいですか?'

4-3. ConversationBufferWindowMemory

最後のK回分の対話をプロンプトに追加するメモリです。

(1) ConversationChainの準備し、メモリにConversationBufferWindowMemoryを指定。

from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationBufferWindowMemory

# ConversationChainの準備
conversation_with_summary = ConversationChain(
    llm=OpenAI(),
    memory=ConversationBufferWindowMemory(k=2),
    verbose=True
)

4-4. ConversationSummaryBufferMemory

過去の会話ログの要約と最後のKトークン分の対話をプロンプトに追加するメモリです。

(1) ConversationChainの準備し、メモリにConversationBufferWindowMemoryを指定。

from langchain.llms import OpenAI
from langchain.chains import ConversationChain
from langchain.chains.conversation.memory import ConversationSummaryBufferMemory

# ConversationChainの準備
conversation_with_summary = ConversationChain(
    llm=OpenAI(),
    memory=ConversationSummaryBufferMemory(llm=OpenAI(), max_token_limit=40),
    verbose=True
)

5. 会話メモリのカスタマイズ

会話メモリのカスタマイズとして、「ap_prefix」を変更することができます。デフォルトでは「AI」に設定されていますが、任意に設定できます。

(1) ConversationChainの準備。
今回は、AIプレフィックスを「AI Assistant」にしてみました。

from langchain.prompts.prompt import PromptTemplate

# テンプレートの準備
template = """以下は、HumanとAIの間の友好的な会話です。 
AIはおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AIが質問に対する答えを知らない場合、正直に"知らない"と言います。

Current conversation:
{history}
Human: {input}
AI Assistant:"""

# プロンプトテンプレートの準備
prompt = PromptTemplate(
    input_variables=["history", "input"], template=template
)

# ConversationChainの準備
conversation = ConversationChain(
    prompt=prompt,
    llm=OpenAI(),
    verbose=True,
    memory=ConversationBufferMemory(ai_prefix="AI Assistant")
)

(2) 推論の実行。

# 推論の実行。
conversation.predict(input="こんにちは")
> Entering new ConversationChain chain...
Prompt after formatting:
以下は、HumanとAIの間の友好的な会話です。 
AIはおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AIが質問に対する答えを知らない場合、正直に"知らない"と言います。

Current conversation:

Human: こんにちは
AI Assistant:

> Finished ConversationChain chain.
' こんにちは!私はあなたのAIアシスタントです。今日は何をしますか?'

(3) 推論の実行。
AIプレフィックスが「AI Assistant」になっていることがわかります。

conversation.predict(input="今日の魚座の運勢は?")
> Entering new ConversationChain chain...
Prompt after formatting:
以下は、HumanとAIの間の友好的な会話です。 
AIはおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AIが質問に対する答えを知らない場合、正直に"知らない"と言います。

Current conversation:

Human: こんにちは
AI Assistant:  こんにちは!私はあなたのAIアシスタントです。今日は何をしますか?
Human: 今日の魚座の運勢は?
AI Assistant:

> Finished ConversationChain chain.
' 今日の魚座の運勢は、今日はあなたが自分の夢を実現するために努力していることを示しています。あなたは、自分の能力を最大限に活用して、自分の夢を実現するために努力していることを示しています。'

6. エージェントへのメモリの追加

エージェントへのメモリの追加を行うには、カスタムエージェントを作成する必要があります。

(1) ツールとプロンプトテンプレートとメモリの準備。

from langchain.agents import ZeroShotAgent, Tool, AgentExecutor
from langchain.chains.conversation.memory import ConversationBufferMemory
from langchain import OpenAI, SerpAPIWrapper, LLMChain

# ツールの準備
search = SerpAPIWrapper()
tools = [
    Tool(
        name = "Search",
        func=search.run,
        description="useful for when you need to answer questions about current events"
    )
]

# プロンプトテンプレートの準備 #prefix  = """Have a conversation with a human, answering the following questions as best you can. You have access to the following tools:""" #suffix  = """Begin!"
prefix = """人間と会話して、次の質問にできる限り答えてください。 次のツールにアクセスできます。"""
suffix = """始め!"


{chat_history}
Question: {input}
{agent_scratchpad}"""
prompt = ZeroShotAgent.create_prompt(
    tools,
    prefix=prefix,
    suffix=suffix,
    input_variables=["input", "chat_history", "agent_scratchpad"]
)

# メモリの準備
memory = ConversationBufferMemory(memory_key="chat_history")

(2) エージェントの準備。

# エージェントの準備
llm_chain = LLMChain(llm=OpenAI(temperature=0), prompt=prompt)
agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True)
agent_chain = AgentExecutor.from_agent_and_tools(
    agent=agent, 
    tools=tools, 
    verbose=True, 
    memory=memory
)

(3) エージェントの実行。

# エージェントの実行
agent_chain.run(input="日本の人口は?")
> Entering new AgentExecutor chain...
Thought: 日本の人口を調べる
Action: Search
Action Input: 日本の人口
Observation: Japan is an island country in East Asia. It is situated in the northwest Pacific Ocean, and is bordered on the west by the Sea of Japan, while extending from the Sea of Okhotsk in the north toward the East China Sea, Philippine Sea, and Taiwan in the south.
Thought: 日本の人口を確認する
Action: Search
Action Input: 日本の人口 2020
Observation: Japan is an island country in East Asia. It is situated in the northwest Pacific Ocean, and is bordered on the west by the Sea of Japan, while extending from the Sea of Okhotsk in the north toward the East China Sea, Philippine Sea, and Taiwan in the south.
Thought: 日本の2020年の人口を確認する
Action: Search
Action Input: 日本の人口 2020
Observation: Japan is an island country in East Asia. It is situated in the northwest Pacific Ocean, and is bordered on the west by the Sea of Japan, while extending from the Sea of Okhotsk in the north toward the East China Sea, Philippine Sea, and Taiwan in the south.
Thought: 日本の2020年の人口を確認した
Final Answer: 2020年の日本の人口は12700万人です。
> Finished AgentExecutor chain.
'2020年の日本の人口は1億2700万人です。

(4) エージェントの実行。
過去の会話を覚えていることを確認します。

# エージェントの実行
agent_chain.run(input="彼らの国歌の名前は?")
> Entering new AgentExecutor chain...
Thought: 日本の国歌の名前を検索する
Action: Search
Action Input: 日本の国歌の名前
Observation: 『君が代』(きみがよ)は、日本国の国歌。 「天皇の治世」を奉祝する歌であり、「祝福を受ける人の寿命」を歌う和歌を元にしている。 歌詞は10世紀初めに編纂された『古今和歌集』の短歌の一つで、曲は1880年(明治13年)に付けられた。
Thought: 日本の国歌の名前を確認した
Final Answer: 日本の国歌の名前は『君が代』(きみがよ)です。
> Finished AgentExecutor chain.
'日本の国歌の名前は『君が代』(きみがよ)です。'

7. カスタムメモリ

カスタムLLMの実装手順は、次のとおりです。
spacyでエンティティを抽出し、それに関する情報をハッシュテーブルで保持するカスタムメモリを作ります。

(1) spacyのインストール。

!pip install spacy
!python -m spacy download ja_core_news_lg

(2) パッケージのインポート。

from langchain import OpenAI, ConversationChain
from langchain.chains.base import Memory
from pydantic import BaseModel
from typing import List, Dict, Any

(3) spacyの準備。

import spacy

# Spacyの準備
nlp = spacy.load('ja_core_news_lg')

(4) カスタムメモリの準備。

# カスタムメモリーの準備
class SpacyEntityMemory(Memory, BaseModel):
    entities: dict = {}  # エンティティ情報
    memory_key: str = "entities"  # エンティティ情報をプロンプトに渡すためのキー

    # プロンプトに提供する変数の取得
    @property
    def memory_variables(self) -> List[str]:
        return [self.memory_key]

    # メモリ変数の取得
    def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, str]:
        # 入力テキストにspacyを適用
        doc = nlp(inputs[list(inputs.keys())[0]])

        # エンティティが存在する場合はエンティティ情報を抽出
        entities = [self.entities[str(ent)] for ent in doc.ents if str(ent) in self.entities]

        # メモリ変数を返す
        return {self.memory_key: "\n".join(entities)}

    # 会話のコンテキストをバッファに保存
    def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
        # 入力テキストにspacyを適用
        text = inputs[list(inputs.keys())[0]]
        doc = nlp(text)

        # エンティティごとに情報を辞書に保存
        for ent in doc.ents:
            ent_str = str(ent)
            if ent_str in self.entities:
                self.entities[ent_str] += f"\n{text}"
            else:
                self.entities[ent_str] = text

    # メモリのクリア
    def clear(self) -> None:
        self.entities = {}

(5) プロンプトテンプレートの準備。

from langchain.prompts.prompt import PromptTemplate

# プロンプトテンプレートの準備
template = """以下は、人間と AI の間の友好的な会話です。 
AI はおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AI が質問に対する答えを知らない場合、AI は正直に「知らない」と言います。 
関連する場合、人間が言及したエンティティに関する情報が提供されます。

Relevant entity information:
{entities}

Conversation:
Human: {input}
AI:"""
prompt = PromptTemplate(
    input_variables=["entities", "input"], template=template
)

(6) LLMとConversationChainの準備。

# LLMの準備
llm = OpenAI(temperature=0)

# ConversationChainの準備
conversation = ConversationChain(
    llm=llm, 
    prompt=prompt, 
    verbose=True, 
    memory=SpacyEntityMemory()
)

(7) チェーンの実行。

conversation.predict(input="ハリソンは機械学習が好き")
> Entering new ConversationChain chain...
Prompt after formatting:
以下は、人間と AI の間の友好的な会話です。 
AI はおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AI が質問に対する答えを知らない場合、AI は正直に「知らない」と言います。 
関連する場合、人間が言及したエンティティに関する情報が提供されます。

Relevant entity information:


Conversation:
Human: ハリソンは機械学習が好き
AI:

> Finished ConversationChain chain.
' そうですね!ハリソンは、機械学習を使ったプロジェクトに取り組んでいます。また、彼は、機械学習を使ったソフトウェア開発にも取り組んでいます。

(8) チェーンの実行。

conversation.predict(input="ハリソンが大学で好きだった科目は何だと思いますか?")
> Entering new ConversationChain chain...
Prompt after formatting:
以下は、人間と AI の間の友好的な会話です。 
AI はおしゃべりで、そのコンテキストから多くの具体的な詳細を提供します。 
AI が質問に対する答えを知らない場合、AI は正直に「知らない」と言います。 
関連する場合、人間が言及したエンティティに関する情報が提供されます。

Relevant entity information:
ハリソンは機械学習が好き

Conversation:
Human: ハリソンが大学で好きだった科目は何だと思いますか?
AI:

> Finished ConversationChain chain.
' ハリソンは機械学習が大学で好きだったと聞いています。

次回


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