ローカルLLMで簡単にチャットアプリを作る

# はじめに

LLMの進化が著しく、ついていくのもやっとです。
最近、ローカルLLMデビューをしたので、さっそくStreamlitでチャットアプリを作って遊んでみようと思います。

# 必要なパッケージ

- streamlit
- llama_cpp

なければインストールしましょう

```
pip install streamlit llama_cpp
```

llama_cppはcmakeなどのC++フレームワークが入っていないと動かないので、適宜インストールしましょう。

# Streamlitについて

Streamlitは簡単にWebアプリが作れるPythonのフレームワークです。
以前にチャットエレメントを使った時の記事はこちら↓

https://qiita.com/sugimochi_1019/items/ede15dcacddfde36df4e

# llamaモデルについて
> LLaMA（Large Language Model Meta AI）は、Meta AI が2023年2月に発表した大規模言語モデル[1][2]。70億パラメータから650億パラメータまで、さまざまなサイズのモデルが学習された。
(引用：https://ja.wikipedia.org/wiki/LLaMA)

Metaの開発した大規模言語モデルで、いろんなところでオープンソースとして使われていますね。
今回は以下の記事を参考にして構築していきます。

https://qiita.com/sald_ra/items/24b571ea2b9c0fa04c56#cpu%E6%8E%A8%E8%AB%96%E3%81%B8%E3%81%AE%E9%81%93

# コードの構造
1. ユーザ入力の受付 - `input_prompt`ユーザからの入力を受け付けます
1. チャット履歴の表示 - `write_chats`関数で渡したチャット履歴を画面に表示
1. Llamaモデルを使用した返答の生成 - `LlamaAdapter`クラスを通じて、ユーザの入力に対するボットの返答を生成する。

# 実装

実際のコードを紹介していきます。

## input_prompt
```python
def input_prompt(chats):
    """ユーザからの入力を受け取る

    Args:
        chats ([dict]): 元チャット履歴

    Returns:
        [dict]: 更新後のチャット履歴
    """
    # ユーザーからの入力を受け取る
    prompt = st.chat_input("Say something")
    # ユーザーからの入力があれば、チャット履歴に追加
    if prompt:
        chats.append({"role": "user", "message": prompt})
        
    return chats
```

ユーザからのinputを受け取り、chatsに記録していきます。

## write_chats

```python
def write_chats(chats):
    """チャット履歴を表示する

    Args:
        chats ([dict]): チャット履歴
    """
    # チャット履歴を表示
    for chat in chats:
        # ユーザーの発言
        if chat["role"] == "user":
            with st.chat_message("user"):
                st.write(f"{chat['message']}")
        # ボットの発言
        else:
            with st.chat_message("ai"):
                st.write(f"{chat['message']}")
```

chatsの内容を表示します。少し無駄のある実装かもしれませんw

## LlamaAdapter

```python
class LlamaAdapter:
    def __init__(self):
        # モデルを初期化
        self.llm = Llama(model_path='./japanese-stablelm-3b-4e1t-instruct-q4_K_M.gguf')

    def infer(self, prompt):
        """返答を生成する

        Args:
            prompt (str): ユーザーの入力

        Returns:
            str: ボットの返答
        """
        # ボットの返答を生成
        output = self.llm(
            prompt,
            max_tokens=64,
        )["choices"][0]["text"]
        return output
```

こちらは[参考サイト](https://qiita.com/sald_ra/items/24b571ea2b9c0fa04c56#cpu%E6%8E%A8%E8%AB%96%E3%81%B8%E3%81%AE%E9%81%93)のコードを少しいじって、好きなプロンプトを入力できるようにしました。

## メイン部分

動作する部分を書いていきます。

```python
# モデルを初期化
if "model" not in st.session_state:
    st.session_state["model"] = LlamaAdapter()
# 使いやすい形にする
model = st.session_state["model"]

# チャット履歴を初期化
if "chat_history" not in st.session_state:
    st.session_state["chat_history"] = []
# 使いやすい形にする
chats = st.session_state["chat_history"]

# promptの入力を受け付ける
chats = input_prompt(chats)

# チャット履歴を出力
if len(chats)!=0:
    write_chats(chats)

if len(chats)!=0 and chats[-1]["role"] == "user":
    # ボットの返答を予測
    with st.spinner("Thinking..."):
        output = model.infer(chats[-1]["message"])
        chats.append({"role": "bot", 
                    "message": f"{output}"})

# ボットのチャットを出力
if len(chats)!=0 and chats[-1]["role"] == "bot":
    write_chats([chats[-1]])
    
```

# 実行
以下のコマンドで実行することができます。

```
streamlit run app.py
```

# 実際の使用例

入力すると、こんな感じで考え始めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/b2da38d9-4d70-ccc9-3d76-c4032d7c090a.png)

すると、以下のように返答が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/d71ae58e-ccdb-c047-3f38-22025ea706f0.png)

また、以下のようにちゃんと下に続いていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/e68dcd36-9791-3f3e-108a-2a23d31ca1b3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/456646/6c3e2e03-f825-3dc8-6d38-74d0ae6a2215.png)

# まとめ
Streamlitとllama_cppパッケージを使用して、簡単にローカルで動作するチャットアプリを作成する方法を紹介しました。これができるといろんなアイデアが湧いてきますね。