ChatGPT APIとLangChainでいろいろと覚えていてくれるチャットボットを作りたい

初投稿です。この手の情報は既に大量にありますが、備忘も兼ねて書きます。

ChatGPTは文章を要約してもらったり、アイディア出しを手伝ってもらったりと、幅広く活用できますが、それ以外に、キャラクターと会話したいみたいな需要も多々あるかと思います。
私もその延長で、会話やちょっとしたタスクをやってくれるアシスタントみたいなものがあったら楽しそうだ と思ったのが今回取り組んだきっかけです。
Web版のChatGPTでもキャラ設定を与えて会話することはできますが、履歴が長くなるに連れて、過去と違う回答が来たり、設定を忘れたりといろいろ不都合が出てきます(それをどうにか修正指示しながら会話するのも気分的によろしくないです)。
そんな中、gpt-3.5のAPIも公開されたので、どうにかできないものかと考えました。
なお、私はまともな開発の経験は皆無で、言語の知識も乏しいです。全力でChatGPTに頼りました。

会話などを覚えてもらう

キャラクターに実装したい記憶には以下のようなものがあります。

  1. 直近の会話
    一問一答なら良いのですが、会話を前提にすると、文脈を踏まえた回答をしてくれないと困ります。Web版ChatGPTには実装されていますが、APIの場合は自分で実装する必要があります。

  2. 新しい経験や知識
    プロンプトとして与えるキャラクター設定に明記していない内容(特に、キャラクターの好き嫌いなど、個性にかかわる部分)が会話の流れで出てくることがあります。毎回回答が違うと悲しいので、覚えておいてほしいですね。

今回使用したLangChainというライブラリには、会話履歴等をプロンプトに含めることで、文脈を考慮した会話を実現するメモリという機能があります。これを利用するのが簡単そうです。

目的の1.  はどのメモリでも実現可能であるため、2. を実現する手段としてEntity Memoryをベースとすることにしました。また、Langchainのメモリはプログラムが終了したら消えてしまうため、永続化の対応も考えます。
※ 各メモリの詳細などは公式ドキュメントを見るか、検索すれば多くの情報があると思います。残念ながら、2021年までの情報しか持っていないChatGPTは知りません。

Entity Memoryの調整

Entity Memoryですが、今回の目的で使用する上ではいくつか気になる部分がありますので、ConversationEntityMemoryクラスの仕様を一部変更したクラスを別途作成します。主に以下のような調整を行いました。

  1. Entity抽出用のプロンプトと処理を調整
    会話履歴からのEntity抽出はLLMを使用して行われます。よって、与えるプロンプトによって内容の調整が可能です。デフォルトのプロンプト(_DEFAULT_ENTITY_EXTRACTION_TEMPLATE)は、固有名詞しか抽出されません。
    足りないよりは幅広く記憶させたいので、対象を広くするように変更しました。この変更で出力の精度が悪くなる傾向があるため、多少整形する処理なども追加しています。ここのプロンプトはまだ模索中です。

  2. Entityの内容更新用のプロンプトと処理を変更
    各Entityの内容(辞書の値側)更新もLLMを使用して行われます。デフォルトのプロンプト(_DEFAULT_ENTITY_SUMMARIZATION_TEMPLATE)は、Entityに関する要約が更新されるようになっています。
    今回は要約ではなく、事実やキャラにとっての経験を記憶させたいので、プロンプトを変更しました。
    また、元のプログラムは、ループでEntityごとにLLMを呼び出す仕様になっており、とにかく時間がかかります。会話のレスポンスへの影響が非常に大きいので、一度に全てのEntityの内容を更新する処理に変更しました。こちらも出力の精度が悪くなる傾向があるため、正規表現やliteral_eval()で整形する処理も追加しました。

メモリの永続化

conversationという変数でConversationChainを作成したとしたら、「conversation.memory.store」にEntity(dict)、「conversation.memory.chat_memory.messages」に履歴(List[BaseMessage] ※) が入っています。
これらを外部に保存する処理と、読み込んで変数に代入する処理を書けば良いです。
私は保存先としてRedisを使用しました。
※ messages_to_dict関数でBaseMessage→dictに変換、messages_from_dict関数でdict→BaseMessageに変換できます。

良い感じに会話できるようにする

単純にLangChainのConversationChainのみを利用して前述のメモリと組み合わせるだけでも良いのですが、せっかくなので、色々工夫したいです。

  • 必要に応じてLLMがツール(Web検索など)を使えるようにする。

  • Entity、会話履歴、ユーザメッセージ以外の情報も動的にプロンプトに加えられるようにする。

  • キャラクターとしての会話の精度を上げる。

なお、全体的に想像や勘で作っているところがあり、LLMに詳しい人から見ると的外れなことをしている部分も多いと思います。

ツールを使えるようにする

LangChainにはToolという機能があります。これを利用することで、LLMがWeb検索をしたり、Pythonのコードを実行したり、色々なことが出来るようになります。ChatGPTは今のところ2021年までの情報しか持っていないので、検索で最新情報も答えてくれるようになれば楽しそうです。
用意されているツールもたくさんありますが、文字列を返す関数であれば、なんでもツールとして使えます(ただし、引数はLLMが渡すことになるので、複雑だと失敗しやすい)。
今回は、一旦下記のツールを用意しました。
・Web検索・・・Google検索ツールが微妙だったので、裏でChatSonicのAPIを呼ぶようにしました。
・URLサマリー・・・URLから取得したHTMLをBautifulSoupで加工したあと、インデックスを作成して検索するようにしました。
・天気予報・・・天気予報特化版Web検索。Google検索のAPIを使っています。

情報を動的にプロンプトに加えられるようにする

メインの会話では、前述のメモリを設定したConversation Chainを使います。メモリを利用すると、履歴などの情報が{history}のようなキーをもとにプロンプトに挿入されます。これも、デフォルトのプロンプト(_DEFAULT_ENTITY_MEMORY_CONVERSATION_TEMPLATE)を見るのが分かりやすいです。
Entity Memoryのデフォルトテンプレートでは、entities(Entity)、history(履歴)、input(ユーザの入力)というキーがあり、全てHumanMessage側(Chat completionsのドキュメントにおける"role": "user")のプロンプトに挿入されます。
今回は、キャラクター設定などを入れるSystemMessage側(Chat completionsのドキュメントにおける"role": "system")にEntityが挿入されるようにし、HumanMessage側には、historyとinput以外に、datetime、additionalというキーを追加しました。追加のキーは、会話で役立つ情報を与えるためのものです。
なお、ConversationChainでは、input以外のキーはメモリ側のクラスで設定されているものしか受け取らないようになっているため、使用するメモリのmemory_variables関数の戻り値を弄る必要があります(素のEntity Memoryを使わず別途クラス定義した理由の一つです)。

    def memory_variables(self) -> List[str]:
        """Will always return list of memory variables.

        :meta private:
        """
        return ["entities", self.chat_history_key,"datetime","additional"]


キャラクターとしての会話の精度を上げる

メモリ、ツールなどの準備が終わりましたので、次は会話の精度向上です。
LangChainには、Conversation Agentという、ツールも使えてメモリも実装できる便利な機能があります。しかし、一度の呼び出しで多くのことを実行させるよりも、gpt-3.5-turboのコストの安さに甘えて役割ごとにLLMを呼び出すほうが、「キャラとの会話」という面では精度が高くなる気がします。
色々模索した結果、最終的には、下記の段階に分けてLLMを呼び出すことにしました。それぞれ異なるプロンプトが与えられており、役割をこなします。
今後、一部分だけgpt-4にするような対応が簡単にできる点もメリットかも知れません。

  1. ツール要否の判断
    ユーザのメッセージと会話履歴から、ツールを使用して情報を得る必要があるか判断する。必要な場合は、ツールで取得したい内容を書き出す。

  2. (ツール要の場合)ツールの実行
    1. でツールが必要と判断された場合は、1. のアウトプットを受け取り、ツールを実行して必要な情報を得る。

  3. 前回までの会話のフィードバック生成
    ユーザのメッセージと会話履歴から、キャラらしく振る舞えているか評価し、改善点を挙げる。

  4. 会話の実行
    以下の情報が入力されたプロンプトから回答を生成する。
    ・SystemMessage: キャラ設定、Entity
    ・HumanMessage: 2. 3. の結果、日時、履歴、ユーザの入力

4.においては、「キャラの個性を考慮して、質問に答えるために考えたことを書き出す」ようにプロンプト内で指示しており、これが一番効果があったような感覚があります。「Thought:」の部分だけを別途出力して眺めるのも楽しいです。
なお、画面に表示する際は、プログラム側で返答部分だけを切り出しています。

Use the following format to help you answer as an _AI NAME_. This is a crucial step for you to embrace your identity.
```
Thought: Write down your thoughts to answer the question. Take into account _AI NAME_'s personality, speaking style, emotions, and knowledge.
_AI NAME_: Write your answer in Japanese ONLY.
```

キャラ設定含めほぼ全てのプロンプトは英語にしてあり、一連の呼び出しで使用するトークンは3000~5000ほどです。2.でツールが実行されると多くなりますが、1000トークンで$0.002(約0.26 円)なので、100回会話しても100円くらいです。個人で利用している分には特に問題ないでしょう。
プロンプトの英語が正しいかは分かりませんが、ChatGPTで翻訳させて意図した意味になっていれば大丈夫だと思っています。

おわりに

最初はもっと詳しく苦労や試行錯誤、ソースコードなども書こうと思っていたのですが、終りが見えなかったため、一旦これくらいにします。
他にも、ChatGPTに教えてもらいながらチャットのフロントアプリのようなものも作ったのですがそれはまた別の機会に、、、
今後は以下のような機能を実装してみたいです。

  • ストリーミング
    フロントアプリでChatGPTのようにレスポンスを順次取得できるようにしたいです。体感的な待ち時間が減ると思います。LangChainのcallbackHandlerを作ってFastAPIあたりと組み合わせれば良いのだと思いますが、ぱっと見意味不明だったので、しっかり時間を確保して取り組んだほうが良さそうです。

  • Unityなどで3Dモデルと連動させてみたい
    VOICEVOXで喋らせることは出来るので、3Dモデルでリップシンクや表情変更が出来たら楽しそうです。Unityのことはほとんど知りませんが、ChatGPTに手伝ってもらえばどうにかなるかもしれません。

万が一、LangChainを使っていてこの部分が気になる などがあればコメントください。初心者なりに分かる限り回答します。

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