LangChain + ClaudeV3 + Streamlit サンプルコード

## やりたいこと

以前、練習目的で、あちらこちらのサイトを参考に ClaudeV2 + LangChain + Streamlitのサンプルコードを書いてみたことがある。
が、時代はあっという間にClaudeV3（半年もすればV4とかV5とかなってそうだが...）になり、LangChainも日進月歩で変わっているようなので、2024年4月時点での魚拓として作成してみた。

## コード

いきなりだがコード。
なお下記コードを動作させるにあたっては、同僚M.O.氏に多大なるご助力をいただいた。この場を借りて感謝申し上げます。

```python:streamlit-claudev3.py
import langchain
import streamlit as st
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.chat_message_histories import StreamlitChatMessageHistory
from boto3 import client
from botocore.config import Config
# これ要らなくなったかも
# from langchain.memory import ConversationBufferMemory

# BedrockChatは今後はChatBedrockに置き換わる模様
# 参考：　https://python.langchain.com/docs/integrations/chat/bedrock/
from langchain_aws import ChatBedrock

# デバッグ用
langchain.verbose = True
langchain.debug = True

# 1. Streamlitセッションステートを使ってチャット履歴を格納
history = StreamlitChatMessageHistory(key="chat_messages")

# 2. Streamlitセッションステートを元にLangChainのチャット履歴を格納
# これ要らなくなったかも
# memory = ConversationBufferMemory(memory_key="history", chat_memory=history)

# 3. メッセージ履歴が空の場合、システムメッセージを追加
if len(history.messages) == 0:
    history.add_ai_message("ようこそ。どのようなご用件ですか？")

# 4. ClaudeV3から追加されたシステムプロンプトに基本的な指示を記述
# ここでは投資に関する質問には回答しないよう指示している
system_prompt = """You are an AI chatbot having a conversation with a human.
You answer always in japanese.
You must not answer questions on investment with the aim of making a profit.
"""

# 5. 最近のLangChainではPromptTemplateではなくChatPromptTemplateが使用される模様
# 前半にシステムプロンプト、後半にチャット履歴とヒューマンプロンプトを記載
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system_prompt),
        (
            "human",
            """
            Current conversation:
            <conversation_history>
            {history}
            </conversation_history>

            Here is the human's next reply:
            <human_reply>
            {human_input}
            </human_reply>
            """,
        ),
    ]
)

# 6. ChatBedrockでboto3を使えるようにしておく（必須ではない）
bedrock_client = client(
    service_name="bedrock-runtime",
    region_name="us-east-1",
    config=Config(read_timeout=300, retries={"max_attempts": 1}),
)

# 7. LLMをインスタンス化
# 最近はBedrockやBedrockChatの代わりにChatBedrockを使用するのがよいらしい
llm = ChatBedrock(
    model_id = "anthropic.claude-3-sonnet-20240229-v1:0",
    region_name = "us-east-1",
    verbose = True,
    model_kwargs = {
        "anthropic_version": "bedrock-2023-05-31",
        "temperature": 0.1,
        "top_p": 0.9,
        "top_k": 50,
        "max_tokens": 1000,
    },
#    client = bedrock_client,  # ここはオプション
)

# 8. チェインを作成し、以下を実行させる
# 8-1. ヒューマンプロンプトを処理
# 8-2. LLMに渡す
# 8-3. アウトプットをパースしAIレスポンスからcontent部を抽出
chain = prompt | llm | StrOutputParser()

for msg in history.messages:
    st.chat_message(msg.type).write(msg.content)

# 9. Streamlitを使ってUIを構築
if human_input := st.chat_input(key="input"):
    st.chat_message("human").write(human_input)

    # 9-1. LLMを実行
    # 最近はLLMChain()ではなくinvoke()を使う模様
    response = chain.invoke(
        {
            "history": history,
            "human_input": human_input
        }
    )

    # 9-2. ヒューマンプロンプト、AIレスポンスをチャット履歴に追加
    history.add_user_message(human_input)
    history.add_ai_message(response)

    # 9-3. AIレスポンスを表示
    st.chat_message("ai").write(response)
```

## 要点解説

### モジュール変更
```python
from langchain_aws import ChatBedrock
```

以前は`from langchain.llms import Bedrock`を使用していたが、これが`BedrockChat`に変わり、さらに個別モジュールに分離された上で`ChatBedrock`に変わった。今後（しばらく）はこれが推奨値になると考えてよさそう。

### システムプロンプト

```python
system_prompt = """You are an AI chatbot having a conversation with a human.
You answer always in japanese.
You must not answer questions on investment with the aim of making a profit.
"""
```

LLMへの基本的な指示は、これまではコンテキスト内に入れてきたが、現在は**システムプロンプト**として分離する形が取れるようになっている。わかりやすくてよい。

### メモリ作成

```python
# memory = ConversationBufferMemory(memory_key="history", chat_memory=history)
```

従来は`ConversationBufferMemory()`に会話履歴を代入していたが、試した限り`StreamlitChatMessageHistory()`を直接渡せているので、要らなくなった可能性が高い。

### チェイン記載

```python
chain = prompt | llm | StrOutputParser()

...

    response = chain.invoke(
        {
            "history": history,
            "human_input": human_input
        }
    )
```

従来は以下のようにチェインを記載していたが、パイプで繋いでパーサーに渡すし、その後`invoke()`メソッドを呼び出す形に変わった（もしくはそちらが主流となった）。

```python
llm_chain = LLMChain(
    llm=Bedrock(
        model_id='anthropic.claude-v2:1',
        model_kwargs={
            "temperature":0.0,
            "max_tokens_to_sample": 500
        }
    ),
    prompt=prompt,
    memory=memory
)
```

## まとめ

この短期間で`Bedrock`→`BedrockChat`→`ChatBedrock`と推奨の関数がめまぐるしく変遷していることからも分かるとおり、非常に変化が速い。このポストも時を置かず過去のものとなりそうだが、その時々に、同様のハマリに苦しんでいる方の一助となるであろうと信じて。