LangChainを用いて、昨日や一週間前のことを会話するチャットボットを作る

もし自分専用のチャットボットがいたら、
当然、数日前に会話した内容を覚えていてほしいですよね？

LangChainを用いて、過去の会話内容に基づいて会話をするチャットボットを作りたいなと思いトライしてみましたので、記事にしました。

今回の完成物は、以下のようなイメージです。

```:過去の会話
【一週間前の会話 （2023-06-28 19:03:42） 】
Human: 最近暑くなってきたからさっぱりしたモノが食べたくて、冷麺を食べたよ。
AI: いいね！ちなみに、盛岡冷麺、韓国冷麺、どっち…？

【一昨日の会話 (2023-07-03 21:53:02)】
Human: 今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。
AI: いいなぁ。僕もイタリアン食べたい！！

【昨日の会話 (2023-07-04 20:11:56)】
Human: 今日の昼は中華レストランでよだれ鶏を食べんたんだ。最近暑いからか、あの酸味と辛味を欲してしまうんだよね
AI: 暑いと酸味と辛味を欲するの？なんで？
```
```:今日の会話 (2023-07-05 19:17:28)
Human: 僕、一週間前って何食べたっけ？
AI: 一週間前は、冷麺を食べたようですね。暑くなってきたからさっぱりしたモノが食べたくて、冷麺を選んだんですよ。
```

# 概要
前述の通り、LangChainを用いて、昨日や一週間前のことを会話できるチャットボットの作っていきます。

[LangChain](https://langchain.com/)とは、ChatGPTのような大規模言語モデルを拡張するためのライブラリです。使用するクラスや関数については簡単に説明していますが、LangChain自体の詳細は説明しません。ご了承ください。

LangChainに関しては、[公式ページのConceptual Guides](https://docs.langchain.com/docs/)や、[こちらの日本語サイト](https://book.st-hakky.com/docs/langcain-intro/)がわかりやすいと思いますので、必要に応じてご参照ください。

なお、LangChain勉強中ため、誤った記載や非推奨・非効率な実装があるかもしれません。お気づきの点がございましたら、コメントにてご指摘いただけますと幸いです。

## 本記事の内容
以下の３部構成です。
1. 会話をJSON形式で読み書きする
2. 【シンプル方式】 過去の会話を、文脈情報として渡す
3. 【Agent方式】 過去の会話を、AI自身で呼び出す

### 1. 会話をJSON形式で読み書きする
LangChainには、会話内容を記憶する`Memory`という機能があります。
[LangChain公式 (Concepts) - Memory](https://docs.langchain.com/docs/components/memory/)
[LangChain公式 (Python Guides) - Memory](https://python.langchain.com/docs/modules/memory/)

本記事では、会話毎に`Memory`の内容をJSON形式に保存しておき、それを新たに会話する際に読み出すことで、以前の会話内容をチャットボットに与えます。

以下では、２種類「シンプル方式」「Agent方式」を試していますが、JSONファイルに保存して読み出す点は共通です。違いは、過去の会話情報へのアクセス方法です。

### 2. 【シンプル方式】 過去の会話を、文脈情報として渡す
LangChainで特定の情報に基づいて会話させる場合の、シンプルかつ主流の方法です。

「過去の会話内容」と「ユーザの発言」を一緒に渡してやり、それに基づいてチャットボットに発言させます。
```:イメージ
過去の会話内容：
（ここに冒頭に示したような、過去の会話内容を記載）

これまでの会話内容：
Human: こんにちは
AI: こんにちは。何かお手伝いすることはありますか？
Human: 僕、一週間前って何食べたっけ？
AI: （過去の会話内容や文脈に応じて、発言してください）
```

ユーザの発言に対して、いつの記憶が必要かはわかりませんので、__過去の全ての会話内容を渡してやる__ 必要があります。

- メリット
    - シンプルでわかりやすい
- デメリット
    - 過去の会話が蓄積された場合、発言する度に過去の全ての会話内容をチャットボットに伝えるため、__トークン数が増え課金金額が高くなる__ ※
    - 似たような会話内容が連なった場合、__複数の会話内容を混同して正確な回答ができない__ 可能性も考えられる

※ChatGPT APIは、入出力のトークン数に基づく従量課金制

### 3. 【Agent方式】 過去の会話を、AI自身で呼び出す
上記の、シンプル方式の課題を解消する試みが、このAgent方式です。
ユーザの発言を受けて、必要な過去の会話内容を、チャットボット自身に考え・呼び出させます。

```:イメージ
Human: 僕、一週間前って何食べたっけ？

--- AIの頭の中 ---
AI: 一週間前の記憶が必要だ。記憶を取り出そう
（一週間前の会話内容をJSONファイルから読み出す）
AI: ふむふむ。冷麺を食べたのか。
----------------
AI: 一週間前は、冷麺を食べたようですね。
```

- メリット
    - 必要な記憶のみ読み出して使用するため、「過去の会話内容」として使用するトークン数を削減できる
- デメリット
    - チャットボットが記憶を呼び出してくれない場合がある
    - （Agent一般の課題として）APIコール数回が増えるので…
        - 応答時間が長くかかる
        - トークン数が多くなる（課金金額が大きくなる）傾向にある

### 結論の頭出し
結論から申しますと、__回答精度は現状50~70点ぐらい__ のイメージです。
具体的には、以下のような誤った回答をする場合があります。
- シンプル方式
    - 過去の会話内容をうまく参照できず、知らないと答える
- Agent方式
    - 過去の会話内容に引きづられて、変な回答をする
    - 記憶アクセスを不要と考え、過去の会話内容を読み込まず、知らないと答える

上記のような課題はありますが、所望の機能を実現すべく __「工夫した点」も複数あります__ 。本記事では、__工夫点（躓いた点と解決法）__ も記載していますので、それらが少しでも参考になりましたら幸いです。

また、もし誤った記載内容があった場合や、
「プロンプトはもっとこう書いた方が良いよ！」
「LangChain（もしくは他ライブラリ）の、この機能使った方が良いよ！」
などのご助言・ご意見がありましたら、ぜひコメントにてお教えいただけると泣いて喜びます！よろしくお願いいたします。

## ソースコード
GitHubにnotebookをあげていますので、よろしければご参照ください。

https://github.com/izaki-shin/notebooks/blob/master/nlp/LangChain_talk_about_past_chats.ipynb

Google Colaboratoryで開く場合はこちらから。

http://colab.research.google.com/github/izaki-shin/notebooks/blob/master/nlp/LangChain_talk_about_past_chats.ipynb

## 参考文献
今回は主に公式ドキュメント（[LangChain (Python Guides)](https://python.langchain.com/docs/get_started/introduction.html)）を参考にしました。
以下の各所にて、参照したページは極力載せるようにしています。

# 実装

## 環境構築
Google Colabを使用していきます。
まずは、必要なPythonパッケージをインストールします。
```
! pip install openai langchain
```
今回はOpenAI APIを使用するので、アクセストークンを指定します。
```python
import os
os.environ['OPENAI_API_KEY'] = 'ご自身のアクセストークンに書き換えてください'
```

## 1. 会話をJSON形式で読み書きする
前述の通り、LangChainでは、会話の内容は`Memory`クラスに保存されます。
その中には、ユーザやチャットボットの発言が`Message`として格納されています。

以下は、`Memory`から`Message`のリストを取り出して`dict`型に変換する方法です。
参考：[LangChain公式 (Python Guides) - Memory](https://python.langchain.com/docs/modules/memory/#saving-message-history)

```python
from langchain.memory import ConversationBufferMemory
from langchain.schema import messages_from_dict, messages_to_dict

memory = ConversationBufferMemory(return_messages=True)
memory.save_context({"input": "hi!"}, {"output": "whats up?"})  # ダミーデータ

chat_messages = memory.load_memory_variables({})['history']
history_dict = messages_to_dict(chat_messages)
```
[`ConversationBufferMemory`](https://python.langchain.com/docs/modules/memory/how_to/buffer)は、発言をバッファに溜め込んでいくだけの、ベーシックな`Memory`です。
`load_memory_variables({})['history']`で溜め込んだ`Message`のリストを取得して、`messages_to_dict()`で`Message`型から`dict`型に変換しています。

最終的に、以下のように、発言者（`'type'`）と発言内容（`'content'`）が、JSONとして書き出せる形で得られていることがわかります。
```:history_dict
[
  {
    'type': 'human',
    'data': {'content': 'hi!', 'additional_kwargs': {}, 'example': False},
  },
  {
    'type': 'ai',
    'data': {'content': 'whats up?', 'additional_kwargs': {}, 'example': False}
  }
]
```

それでは、会話内容を読み書きする関数を定義します。
```python
# 会話内容の保存・読込の関数
import json

def save_conversation_from_memory(json_filepath, memory, memory_key='history', **kwargs):
    history_messages = memory.load_memory_variables({})[memory_key]
    history_dict = messages_to_dict(history_messages)
    dat = dict({memory_key: history_dict}, **kwargs)  # 任意の情報を保存可能

    with open(json_filepath, 'w') as fout:
        json.dump(dat, fout)
    print('Saved', json_filepath)

def load_conversation_from_json(json_filepath, memory_key='history'):
    with open(json_filepath, 'r') as fin:
        dict_all = json.load(fin)
        ret_dict = {}
        for k, v in dict_all.items():
            if k == memory_key:
                ret_dict[k] = messages_from_dict(v)
            else:
                ret_dict[k] = v
    return ret_dict
```

事前準備の最後として、実験を行うためのサンプルデータ（過去の会話内容）を、ファイル書き出ししておきます。

日別でファイルを分けて保存しています。
また、会話日時も一緒にファイル内に出力しておきます。

```python
# json_file, datetime, chat_history
past_conversations = [
    [
        'chat_history_20230628.json',
        '2023-06-28 19:03:42',
        [
            ({"input": "最近暑くなってきたからさっぱりしたモノが食べたくて、冷麺を食べたよ。"},
             {"output": "いいね！ちなみに、盛岡冷麺、韓国冷麺、どっち…？"}),
        ]
    ],
    [
        'chat_history_20230703.json',
        '2023-07-03 21:53:02',
        [
            ({"input": "今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。"},
             {"output": "いいなぁ。僕もイタリアン食べたい！！"}),
        ]
    ],
    [
        'chat_history_20230704.json',
        '2023-07-04 20:11:56',
        [
            ({"input": "今日の昼は中華レストランでよだれ鶏を食べんたんだ。最近暑いからか、あの酸味と辛味を欲してしまうんだよね"},
             {"output": "暑いと酸味と辛味を欲するの？なんで？"}),
        ]
    ],
]

# 日別に会話内容を保存
for json_file, datetime_str, chat_history in past_conversations:
    memory = ConversationBufferMemory(return_messages=True)
    for input_output_tuple in chat_history:
        memory.save_context(*input_output_tuple)
    # datetimeも一緒に保存
    save_conversation_from_memory(json_file, memory, datetime=datetime_str)
```

## 2. 【シンプル方式】 過去の会話を、文脈情報として渡す
まずは、シンプル方式として、過去の会話内容の全てを、文脈情報として入力データに記載しておく方法を試してみます。

LangChainの基本的な流れとしては、以下の３ステップです。
1. プロンプトを作成
1. `LLMChain`を作成
1. `LLMChain`を実行（文脈とユーザ発言を入力）

まず、プロンプトのうち「過去の会話内容」の部分を作成します。基本的には、先ほど出力したファイルを読み出し、それをテキストとして列挙していくだけです。
```python
# 過去の会話を読み込む
import glob
from langchain.schema import get_buffer_string  # Messageのリストを文字列に変換する関数

def generate_context_from_past_chats(chat_history_files):
    past_chats_text = ''
    for history_file in chat_history_files:
        d = load_conversation_from_json(history_file)
        dt = d['datetime']
        msgs = d['history']
        msgs_str = get_buffer_string(msgs)
        past_chats_text += '【日時】 {:s}\n{:s}\n---\n'.format(dt, msgs_str)

    past_chat_prompt = (
        '以下は、過去の会話です。\n'
        '================\n'
        '{:s}\n'
        '================\n\n'
    ).format(past_chats_text.rstrip('---\n'))

    return past_chat_prompt

past_chats_files = glob.glob('chat_history_*.json')
past_chats_files.sort()
past_chats_context = generate_context_from_past_chats(past_chats_files)
```
生成された`past_chats_context`については、後ほど、プロンプト完成後に他と併せて見ることとします。

続いて、プロンプトの残りの部分のと、`LLMChain`作成になります。
```python
from langchain.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate
)
from langchain.chains import LLMChain
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationBufferMemory

dt_now = '2023-07-05 19:17:28'  # 今の時刻

prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template(
        "あなたはエージェント型チャットボットです。\n"
        "過去の会話を参照しながら対話者（僕）と会話することができます。\n"
        "発言は、100字以内で短く返してください。\n\n"
        f"{past_chats_context}"  # <--------- ここに過去の会話内容を挿入 
        f"現在の日時：{dt_now}\n\nそれでは、会話開始です。"
    ),
    MessagesPlaceholder(variable_name="today_history"),
    HumanMessagePromptTemplate.from_template("{input}")
])

def create_new_chatbot(temperature=0, verbose=True):
    llm = ChatOpenAI(temperature=temperature, model_name="gpt-3.5-turbo")
    memory = ConversationBufferMemory(return_messages=True, memory_key='today_history')
    chatbot = LLMChain(memory=memory, prompt=prompt, llm=llm, verbose=verbose)
    return chatbot
```
実施していること自体は、`LLMChain`の一般的な作成手順です。
参考：[LangChain公式 (Python Guides) - Chains #GetStarted](https://python.langchain.com/docs/modules/chains/#get-started)

`LLMChain`作成の際は、バックエンドで動く大規模言語モデル（`llm=`）、指示や文脈の情報を含むプロンプト（`prompt=`）、会話内容を保存する`Memory`（`memory=`）を与える必要があります。ここで、注意点としては、この`Memory`は『今日』の会話内容を記憶するためのものであり、『昨日以前の過去』の会話内容とは関係がありません。

<details><summary>（補足説明） LangChainにおける ChatModel と LLM</summary><div>

プロンプト定義の`ChatPromptTemplate`のあたりの詳細は割愛しますが、`ChatModel`についてだけ簡単に説明を記載します。

LangChainには、言語モデルのクラスが`LLM`と`ChatModel`の２種類あります。`ChatModel`は`gpt-3.5-turbo`のようなチャット専用のもの、`LLM`はその他一般の`text-davinci-003`のようなものです。

この２つで、プロンプトの書き方も異なっており、今回は`ChatModel`用の書き方となっています。詳しくは、[LangChain公式 (Python Guides) - ModelI/O > Prompts](https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/)）をご参照ください。

</div></details>

続いて、生成されるプロンプトを見ていきます。
```txt:Prompt
System: あなたはエージェント型チャットボットです。
過去の会話を参照しながら対話者（僕）と会話することができます。
発言は、100字以内で短く返してください。

以下は、過去の会話です。
================
【日時】 2023-06-28 19:03:42
Human: 最近暑くなってきたからさっぱりしたモノが食べたくて、冷麺を食べたよ。
AI: いいね！ちなみに、盛岡冷麺、韓国冷麺、どっち…？
---
【日時】 2023-07-03 21:53:02
Human: 今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。
AI: いいなぁ。僕もイタリアン食べたい！！
---
【日時】 2023-07-04 20:11:56
Human: 今日の昼は中華レストランでよだれ鶏を食べんたんだ。最近暑いからか、あの酸味と辛味を欲してしまうんだよね
AI: 暑いと酸味と辛味を欲するの？なんで？
================

現在の日時：2023-07-05 19:17:28

それでは、会話開始です。
Human: 一昨日僕が食べた料理を答えてみて。
```

:::note info
プロンプトにおける工夫点
1. <INS>異なる日の会話は明確に分離する</INS>
    - 過去の各会話を`---`を用いて分割しています。
    - もともとは空行を入れていたのですが、別の日の会話と混同して回答することがありました。`---`で明確に分割することで、発生しづらくなった感じがします。
:::
冒頭の「過去の会話を参照しながら対話者（僕）と会話することができます。」の有効性は不明です。ただ、過去の会話を参照させるための指示文は記載した方がよいとは思います。


それでは、実際に実行した結果を見ていきましょう。

まずは、成功例から。
チャットボット出力はコメントの形で記載しています。
```python: 成功例
chatbot = create_new_chatbot(verbose=False)
chatbot.predict(input="一昨日僕が食べた料理を答えてみて。")
# 一昨日はイタリアンのイカスミパスタと真鯛のカルパッチョを食べたよね。

chatbot = create_new_chatbot(verbose=False)
chatbot.predict(input="7/4に食べた料理は何でしょうか？")
# 7/4に食べた料理は「よだれ鶏」です。

chatbot = create_new_chatbot(verbose=False)
chatbot.predict(input="僕、一週間前って何食べたっけ？")
# 一週間前は、冷麺を食べたようですね。暑くなってきたからさっぱりしたモノが食べたくて、冷麺を選んだんですよ。
```
「昨日」や「一週間前」といった単語表記であっても、「7/4」のような日付表記であっても、正しく回答できています。

__日付を与えただけで、ちゃんと内部で「一昨日」や「一週間前」を計算できているのって、地味にすごくないですか？__
日付を計算する質問（例えば、「今日は7/5です。一昨日は何日ですか？」）であれば、出来そうな感じもするのですが、一昨日食べたものを聞かれて、今日の日付と過去の日付とから、ちゃんと情報を持って来れるのは普通にすごいなと、感動しました。

続いて、失敗例です。
```python: 失敗例
chatbot = create_new_chatbot(verbose=False)
chatbot.predict(input="昨日食べた料理は何でしょうか?")
# すみません、私は過去の会話しか覚えていませんので、昨日の料理についてはわかりません。何か他の話題でお話ししましょうか？
```
成功例と何が違うのか、原因は全くわからないですが、残念ながら失敗しています。

ChatGPTに過去のやりとりについて聞くと、定型文のような文面で「過去の情報は保持していない」的なことを言ってきます。ChatGPTの訓練の過程で、出来ないことはちゃんと出来ないと明確に伝える、ということが教え込まれているのだと思います。勝手な想像ですが、その影響がかなり強く、情報を与えている場合にもその名残がでてしまっているのかな…と考えたりしました。

以上が、シンプル方式です。

シンプル方式の課題は、__過去の会話が蓄積されていった場合に、おそらく太刀打ちできない__ 点です。それを解消すべく、次はAgent方式を見ていきます。

# 3. 【Agent方式】 過去の会話を、AI自身で呼び出す
LangChainで言語モデルを動かす方法に、上述の`LLMChain`とは別に、`Agent`というものがあります。（別と言いましたが、`Agent`の内部では`LLMChain`が動いています。）

`Agent`は、汎用AIを目指した機能です。言語モデルだけでは言葉を操ることしかできませんが、その言葉を操る能力を活かして、`Agent`はさまざまなツール（Web検索や外部APIキック等）を操れるように設計されています。

今回は、そのツールの１つとして、
__「過去の会話内容を呼び出す」ツールを自作し、`Agent`に与えます。__

そうすることで、過去の情報が必要になった際に、`Agent`が自分自身でそのツールを使って過去の会話内容にアクセスし、それに基づいて返答をする、という挙動を期待します。

それでは、実装に入っていきます。
まずは、`Agent`を定義する部分です。
```python
from langchain import OpenAI
from langchain.agents import AgentType, initialize_agent
from langchain.memory import ConversationBufferMemory

def create_new_agent(temparature=0, verbose=True):
    llm_chat = OpenAI(temperature=temparature)
    memory = ConversationBufferMemory(memory_key="chat_history")

    # Agentが使用可能なツールを定義 （ツール自体については後述）
    tools = [get_date_from_keyword, remember_past_tool]

    agent_chain = initialize_agent(
        tools, llm_chat,
        agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
        memory=memory,
        verbose=verbose,
        max_execution_time=3,
        early_stopping_method="generate",
        )
    return agent_chain
```
`initialize_agent`を使って、簡単に`Agent`を作成することができます。その際、`tools`に今回使用可能なツールを入れて、`Agent`に渡してやります（`tools`の中の各ツールについては後述します）。

<details><summary>その他コードの細かな補足（読み飛ばしていただいても問題ないです）</summary><div>

- `llm_chat = OpenAI(temperature=temparature)`
    - 今回は、LLMとして`OpenAI`を使用しています。`text-davinci-003`等の、チャット用途ではない言語モデルを使用する際は、こちらを使用します。`LLM`のインスタンスを返します。
    - 一方で、シンプル方式で使用した`ChatOpenAI`は、`gpt-3.5-turbo`のような、チャット専用の言語モデルを使用する際に使用します。`ChatModel`のインスタンスを返します。
- `ConversationBufferMemory(memory_key="chat_history")`
    - `Agent`を使用する際は、`memory_key="chat_history"`を指定する必要があります。
- `max_execution_time=3`
    - ツールを呼び出すか検討するための言語モデルの実行回数の上限です。
    - 参考：[LangChain公式 (Python Guides) - Agents > How-to > Cap the max number of iterations](https://python.langchain.com/docs/modules/agents/how_to/max_iterations)
- `early_stopping_method="generate"`
    - 上限回数に達して結論に達しなかった場合の対応方法を指定しています。`"generate"`の場合、その旨を言語モデルから伝えてくれます（指定しない場合、上限に達した旨の定型文が出力されます）。
    - 参考：[LangChain公式 (Python Guides) - Agents > How-to > Cap the max number of iterations](https://python.langchain.com/docs/modules/agents/how_to/max_iterations)

</div></details>

続いて、「過去の会話内容を呼び出す」ツールを実装します。
独自ツールの実装方法は、以下の公式ドキュメントが詳しいです。
[LangChain公式 (Python Guides) - Agents > Tools > Defining Dustom Tools](https://python.langchain.com/docs/modules/agents/tools/how_to/custom_tools)
```python
import re
from langchain import OpenAI
from langchain.tools import tool, Tool

# ツール内部で使用するLLM
llm = OpenAI(temperature=0)  # text-davinci-003

# 「昨日」「明日」といった単語を、日付に変更するツール
# 定義方法1. デコレータを使用する方法
@tool("get_date_from_keyword")
def get_date_from_keyword(date_keyward: str) -> str:
    """Get specific date in 'yyyy/mm/dd' format from Japanese keyword (e.g. '昨日', '明後日')."""
    dt_today = '2023/07/05'  # Currently hard coded.
    template1 = 'Translate japanese word "{}" into English.'
    template2 = f'Today is {dt_today}. Tell the date the word "{{}}" means in "2023/01/12" format.'

    word = llm(template1.format(date_keyward)).lstrip('\n')  # 昨日 --> yesterday
    dt_target = llm(template2.format(word)).lstrip('\n')      # yesterday --> 2023/07/04
    print(date_keyward, word, dt_target)
    return dt_target

# 過去の会話内容を呼び出すツール
class RememberPastTool():

    def __init__(self, chat_history_dir='./'):
        self._data_dir = chat_history_dir

    def get_chat_history_text(self, date):
        history_file = os.path.join(self._data_dir, f"chat_history_{date.replace('/', '')}.json")
        if not os.path.isfile(history_file):
            return 'その日は会話していないようです'
        d = load_conversation_from_json(history_file)
        msgs_str = get_buffer_string(d['history'])
        return msgs_str

    def remember_past(self, query: str) -> str:
        query_org = query
        if re.match(r'\d{4}/\d{2}/\d{2}', query) is None:  # is not date format
            query = get_date_from_keyword(query)
        text = self.get_chat_history_text(query)
        return (f'以下は、{query_org}の会話です。\n---\n{text}\n---\n'
                'Please recall "New input". Then, answer to "New input" based on this information.\n')

# 定義方法2. Tool.from_functionで関数を指定する方法
remember_past_tool = Tool.from_function(
    func=RememberPastTool().remember_past,
    name="recall_past_chats",
    description=("Must use this when talking about past experience. You can recall past chat conversations. "
                 "Input can be date (yyyy/mm/dd) or word (e.g. '昨日', '一週間前').")
)
```
ここでは、2つのツールを定義しています。過去の会話内容を呼び出すツール`remember_past_tool`に加えて、「昨日」や「一週間前」といった単語を日付に変換するツール`get_date_from_keyword`を追加で定義しました。

以下、ツール定義における工夫点（苦労した点）です。
追加ツールが必要だった理由も記載しています。

:::note info
Agentツールにおける工夫点
1. <INS>ツールの入力の難易度を下げる</INS>
    - 具体的には、`remember_past_tool`の入力に、『単語』（例えば「昨日」「一週間前」）を許容しています。
    - 当初は入力を『日付』にしていたのですが、文章中の「昨日」を内部的に日付に変換する必要があり、失敗するケースがありました。その点、『単語』は入力文から抜き出すだけで良いので難易度が下がります。
1.  <INS>タスクを分解して簡略化する（1と被りますが）</INS>
    - `get_date_from_keyword`内で、単語を日付に変換する処理を、２ステップに分解しました。
        1. 単語を英訳する（一昨日 → two days ago）
        1. 英単語を日付に変換する
    - 日本語の単語を直接日付に変換させた場合、失敗する（「一昨日」を昨日の日付に変換する）場合があったためです。
1. <INS>ツールの説明(`description`)に命令形にする </INS>
    - `remember_past_tool`の説明を`"Must use ~"`としています
    - 命令形にする前は、過去の話をしてもツールを呼び出してくれないことがありました（その結果「昨日は僕は寿司を食べました！」とか意味不明な発言をしてきたり…）
1. <INS>ツールの出力テキストを工夫する</INS>
    - `remember_past_tool`の出力にて、過去の会話のみを返すのではなく、「これに基づいて回答して」的な指示文も一緒に返す
    - 会話文のみの場合、LLMが会話の続きを出力してしまう場合がありました（かなりの頻度で）。また、コード内では指示文を英語で書いていますが（`Please recall ~`）、日本語だとあまり効果がありませんでした
:::

以上で、`Agent`作成は完了です。

ちなみに、`Agent`の最終的なプロンプトは以下により確認できます。
```python
# agent_chain = create_new_agent()
agent_chain.agent.llm_chain.prompt.template
```
<details><summary>（ご参考）今回の最終的なプロンプトはこちら</summary><div>

指定したツールやその説明が、プロンプト内に記載されています。
```
Assistant is a large language model trained by OpenAI.

Assistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.

Assistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.

Overall, Assistant is a powerful tool that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist.

TOOLS:
------

Assistant has access to the following tools:

> recall_past_chats: MUST use this when talking about past experience. You can recall past chat conversations. Input can be date (yyyy/mm/dd) or word (e.g. '昨日', '一週間前').
> get_date_from_keyword: get_date_from_keyword(date_keyward: str) -> str - Get specific date in 'yyyy/mm/dd' format from Japanese keyword (e.g. '昨日', '明後日').

To use a tool, please use the following format:

'''
Thought: Do I need to use a tool? Yes
Action: the action to take, should be one of [recall_past_chats, get_date_from_keyword]
Action Input: the input to the action
Observation: the result of the action
'''

When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:

'''
Thought: Do I need to use a tool? No
AI: [your response here]
'''

Begin!

Previous conversation history:
{chat_history}

New input: {input}
{agent_scratchpad}
```
</div></details>

それでは、実際に出力を確認していきます。
まずは、動作確認として、簡単な質問からです。
```python:動作確認
agent_chain = create_new_agent()
agent_chain.run(input="こんにちは")
# こんにちは！どうかしましたか？

agent_chain.run(input="昨日は何月何日でしょうか？")
# 昨日は2023年7月4日です。
```
正しく回答できています。`get_date_from_keyword`を使用して、「昨日」から日付を取得できていることが確認できました。（より詳細には、以下の「Agentの思考過程」をご参照ください）

<details><summary>（ご参考）Agentの思考過程</summary><div>

以下のツール選択過程を見ると、「こんにちは」への返答ではツール不要と、日付に関する質問ではツールが必要だと、正しくと判断できていることがわかりますね。
```sh
# agent_chain.run(input="こんにちは")

> Entering new  chain...

Thought: Do I need to use a tool? No
AI: こんにちは！どうかしましたか？

> Finished chain.

# agent_chain.run(input="昨日は何月何日でしょうか？")

> Entering new  chain...
Thought: Do I need to use a tool? Yes
Action: get_date_from_keyword
Action Input: 昨日

Observation: 2023/07/04
Thought: Do I need to use a tool? No
AI: 昨日は2023年7月4日です。

> Finished chain.
```
</div></details>

続いて、過去の会話内容に関する質問です。
```python:成功例
agent_chain = create_new_agent()
agent_chain.run(input="一昨日、僕が何を食べたか、答えてください！")
# 一昨日は友達とイタリアンに行って、イカスミパスタと真鯛のカルパッチョを食べたそうですね！
```
<details><summary>（ご参考）成功例におけるAgentの思考過程</summary><div>

```sh
> Entering new  chain...

Thought: Do I need to use a tool? Yes
Action: recall_past_chats
Action Input: 一昨日

Observation: 以下は、一昨日の会話です。
---
Human: 今日は友達とイタリアンに行ったよ。イカスミパスタと真鯛のカルパッチョを食べたんだけど、どっちも美味しかったぁ。
AI: いいなぁ。僕もイタリアン食べたい！！
---
Please recall "New input". Then, answer to "New input" based on this information.

Thought: Do I need to use a tool? No
AI: 一昨日は友達とイタリアンに行って、イカスミパスタと真鯛のカルパッチョを食べたそうですね！

> Finished chain.
```
</div></details>

```python:微妙な回答
agent_chain = create_new_agent()
agent_chain.run(input="僕、一週間前って何食べたっけ？")
# 冷麺なら、盛岡冷麺と韓国冷麺がありますね！どちらを食べましたか？

agent_chain = create_new_agent()
agent_chain.run(input="昨日僕が食べた料理を答えてみて。")
# 昨日は中華レストランでよだれ鶏を食べましたね。暑いからか、あの酸味と辛味がとても美味しかったです。
```

成功例に加えて、微妙な回答も見てもらいましたが、いずれにおいても、`recall_past_chats`を呼び出して、過去の会話内容を参照した上で返答できていることが見て取れます。

ただ、微妙な回答では、過去の会話内容に引きづられてしまって、質問と直接関係ない内容まで発言してしまっています（最後の例に至っては、「美味しかったです」と、自分も一緒に食べたことになってしまっていますね…）

ツール定義での工夫点に記載した通り、これでも工夫を試みて改善した結果なのですが、依然として過去の会話内容の影響を強く受けてしまっており、今後の課題です…。

最後に、完全な失敗例も見てみます。
```python:失敗例
agent_chain = create_new_agent()
agent_chain.run(input="一昨日の日付は？その日、僕が食べた料理は何？")
# 一昨日は2023年7月3日ですね。その日は、あなたが食べた料理は何でしたか？
```
期待としては、２つのツールを呼び出して欲しかったのですが、残念ながら`get_date_from_keyword`の方しか呼び出してくれませんでした。結果、食べた料理に関する回答ができていません。

<details><summary>（ご参考）失敗例におけるAgentの思考過程</summary><div>

```sh
> Entering new  chain...

Thought: Do I need to use a tool? Yes
Action: get_date_from_keyword
Action Input: 一昨日

Observation: 2023/07/03
Thought: Do I need to use a tool? No
AI: 一昨日は2023年7月3日ですね。その日は、あなたが食べた料理は何でしたか？

> Finished chain.
```
</div></details>

以上が、Agent方式です。

Agent方式は、かなり色々試行錯誤した割に、微妙な結果になってしまって残念です…。まだまだ`Agent`の使いこなし方を学ぶ必要がありそうです。
とはいえ、入力文に応じてツールを自動選択した上で、過去の会話内容を参照できることは確認できたのでよかったです。

# まとめ
本記事では、__過去の会話内容に基づいて会話をするチャットボットを作る__ べく、大規模言語モデルを拡張するライブラリLangChainを用いた実装を試してみました。

「シンプル方式」と「Agent方式」の２種類を試してみました。
1. シンプル方式
    - メリット：シンプルでわかりやすい／API呼び出し回数が少なく応答時間が短い
    - デメリット：過去の会話が増えた場合に対応しきれない
2. Agent方式
    - メリット：過去の会話数に依らず使用可能
    - デメリット：API呼び出し回数が増え、応答時間が長くなる

上記は、方式上の明らかなメリット・デメリットですが、今回実際に試してみて、精度的な比較では以下のようの結果となりました。
- シンプル方式 : ○ (70点?)
    - 正しく回答できるケースが多い（ただし、今回は過去の会話数が少ない）
- Agent方式 : △ (50点?)
    - 過去の会話内容の呼び出し自体は概ね成功
    - なぜか過去の会話に引きづられやすく、質問にうまく回答できていない

__Agent方式がなぜうまく動作していないのか、どう修正すると良いのかは、正直わかっていません。__ もしご知見をお持ちの方がいらっしゃいましたら、コメントにてお教えいただけると幸いです。

今回の記事は以上となります！

最後まで読んでいただき誠にありがとうございます。
少しでも楽しんでいただけたり、参考になる部分があったりしましたら幸いです。

自分専用のチャットボットを作るべく、今回の内容の改良や、その他の実験も今後実施していけたらと思いますので、また記事を書いた際はお読みいただけると嬉しいです。

それでは！(*ˊᗜˋ)ﾉｼ