OPENAI API+LangChain+Gradioでキャラクターとお話するアプリを作る

# 概要
初投稿です。

OPENAI API, LangChain, Gradioを使って好きなキャラクターとお話しようという試みです。
以下のようなものが作れます。
![2025-03-26_15h06_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3327109/702cd52b-6701-42b6-948d-2a1c988db278.png)
~~ChatGPTでもできますが、プロンプトをすぐに忘れるので、APIからやった方が楽だと思います。お金かかりますが。~~

# APIキーの取得・設定
[Open AI](https://platform.openai.com)のアカウントを作成し、下記のリンクからAPIキーを取得します。

https://platform.openai.com/account/api-keys

:::note warn
APIキーは発行時にしか見られないので、メモするなどして控えてください。また、漏洩に注意してください。
:::

環境変数に設定します。
```bash
export OPENAI_API_KEY="(取得したAPIキー)"
```

# ライブラリの導入
```
pip install langchain
pip install langchain-community
pip install langchain-openai
pip install gradio
```

# 実装
`YourFavoriteSystemPrompt`をお好みの設定プロンプトで置き換えてください。
```python:app.py
import os

from langchain_core.messages import SystemMessage
from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    HumanMessagePromptTemplate,
)
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory

import gradio as gr

# 会話履歴ストア
store = {}


# セッションIDごとの会話履歴の取得
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


# 設定プロンプト
character_setting = YourFavoriteSystemPrompt

# チャットプロンプトテンプレート
prompt = ChatPromptTemplate.from_messages(
    [
        SystemMessage(content=character_setting),
        MessagesPlaceholder(variable_name="history"),
        HumanMessagePromptTemplate.from_template("{input}"),
    ]
)

# チャットモデル
llm = ChatOpenAI(
    model_name="gpt-4o",
    temperature=0.6,
    streaming=True,
)

# パース用モジュール(レスポンスのJSONからcontentを取り出すパーサー)
parser = StrOutputParser()

# LCEL
runnable = prompt | llm | parser

# RunnableWithMessageHistoryでラップ
runnable_with_history = RunnableWithMessageHistory(
    runnable=runnable,
    get_session_history=get_session_history,
    input_messages_key="input",
    history_messages_key="history",
)


def answer(user_message, history):
    response = runnable_with_history.invoke(
        {"input": user_message},
        config={"configurable": {"session_id": "hoge"}},
    )
    print(response)
    return response


if __name__ == "__main__":
    gr.ChatInterface(fn=answer, type="messages").launch()

```
`python app.py`か`gradio app.py`で動かせます。後者はホットリロード対応です。
# 解説もどき

:::note warn
以下の解説は本記事公開時の古いバージョンのものです。
:::

### チャットモデル
```python
llm = ChatOpenAI(
    model_name="gpt-3.5-turbo", 
    max_tokens=512,
    temperature=0.2,
    streaming=True, 
    callback_manager=CallbackManager([StreamingStdOutCallbackHandler()])
)
```
チャットモデルを準備する部分です。
`model_name`は文字通り使うモデルの名前です。GPT-4を使いたい場合は`gpt-4`に置き換えてください。デフォルトは`gpt-3.5-turbo`です。
`max_tokens`は生成される応答の最大トークン数です。デフォルトは`None`です。
`temperature`はサンプリング温度で、0～1の値を指定します。小さいほど生成文章が一定になり、大きいほど多様な文章が生成されます。デフォルトは`0.7`です。あんまり大きいとキャラがブレるかも。
`streaming`と`callback_manager`はLangChainのストリーミングを使うためのやつです。なんとなくテンポ良くなる気がする。

https://note.com/npaka/n/nf3f9ce18d2f4

曰く
>「ストリーミング」は、一度にすべてではなく、トークン単位で出力を返すことによって、体感レイテンシを減らすのに役立ちます。

らしいです。
### メモリ
```python
memory = ConversationBufferWindowMemory(k=3, return_messages=True)
```
メモリを準備する部分です。
`return_messages`を`True`にすることで、会話の履歴をメッセージのリストとして取得します。
`k`は記憶する会話の数です。ここでは3なので、直前3往復の会話を記憶し、プロンプトに投げ込みます。なお、これを
```
memory = ConversationBufferMemory(return_messages=True)
```
で置き換えるとすべての会話を記憶します。ただし、プロンプトとして渡せるトークン数には制限があるのであまり長い会話はできません。
```
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=256, return_messages=True)
```
で置き換えると、`max_token_limit`分の最新の会話はそのまま、超過した古い会話は要約して記憶します。要約する分、上記のものに比べると処理が遅くなります。`max_token_limit`のデフォルトは`2000`です。
### 会話チェーン
```python
conversation = ConversationChain(memory=memory, prompt=prompt, llm=llm, verbose=True)
```
`verbose`を`True`にすることで、投げ込まれるプロンプトを確認できます。
### フロントエンド
```python
css = """
.message.user{
    background: #06c755 !important;
}"""

with gr.Blocks(css=css) as demo:
    # コンポーネント
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")

    def user(message, history):
        return "", history + [[message, None]]

    def chat(history):
        message = history[-1][0]
        response = conversation.predict(input=message)
        history[-1][1] = response
        return history

    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
        chat, chatbot, chatbot
    )
    clear.click(lambda: None, None, chatbot, queue=False)
```
フロントエンド部分です。
テキストボックスの色は、`color_map`が非推奨になったようなのでCSSで置き換えてLINE風にしました。あとは適当に[Gradioの公式ドキュメント](https://gradio.app/creating-a-chatbot/)をパクってちょっと変えただけです。
送信されたテキストを`chat`関数に直接引数として渡せないのモヤモヤするのでやり方あったら誰か教えてほしい。

# 設定プロンプト
設定プロンプトは、以下のようにしました。
```python
character_setting = """高森藍子は、「アイドルマスター シンデレラガールズ」に登場するアイドルです。これから彼女を相手にした対話のシミュレーションを行います。彼女のプロフィールは以下の通りです。

名前：高森藍子
性別：女性
年齢：16歳
学年：高校1年生
出身：東京都
誕生日：7月25日
髪の色：茶色
髪型：お団子ヘア、ポニーテール(もみあげが長い)
趣味：近所の公園をお散歩
一人称：私

高森藍子は、心優しいゆるふわな女の子です。ファンが優しい気持ちに、笑顔になってくれるようなアイドルを目指しています。お散歩したり、トイカメラで写真を撮ったりすることが好きです。控えめですが、一度決めたことは最後までやり通す意志の強さをもっています。一人称は私です。ユーザのことはプロデューサーさんと呼びます。

高森藍子のセリフの例を以下に示します。

(略)

上記例を参考に、高森藍子の性格や口調、言葉の作り方を模倣し、回答を構築してください。回答は、高森藍子の発言のみを出力してください。
では、シミュレーションを開始します。"""

```

セリフの例は、公式(モバマス)のセリフを25個列記しました。本当はもっと例示したいんですが、お金がないので泣く泣く削りました。

# 最後に
前から構想は抱えてたのに音声やイラストにかまけて全然GPT関連に触れてなかったんですが

https://twitter.com/yakionigiri765/status/1644863737252421632?s=20

を見て「こんなことやってる場合じゃねえ！」と急いでChatGPT Plusに加入してGPT-4を体験しました。あれやばいですね。チャットしながら顔ずっとニヤニヤしてました。でも制限があるので、そこで現実に戻されちゃうんですよね。ChatGPT APIでなんとかできないかなっていうのと、TTSと組み合わせたいなとずっと思ってたので、今回雑に作ってみました。賢い対話システムが素人でも簡単に作れるので、すごい時代ですよね。

# 参考
https://python.langchain.com/en/latest/getting_started/getting_started.html

https://qiita.com/hideki/items/b26154ab503fd3394a0b

https://note.com/npaka/n/n403fc29a02c7

https://qiita.com/sakasegawa/items/db2cff79bd14faf2c8e0

https://blog.shikoan.com/langchain_buffer_token/

https://anond.hatelabo.jp/20230317165032

https://github.com/gradio-app/gradio/pull/3370#issuecomment-1455127401

2025/3/26追記

https://www.gradio.app/guides/creating-a-chatbot-fast

https://zenn.dev/khisa/articles/7f56f4e66cae43