watsonx.ai, langchain 基礎

# 初めに
langchainを使用したwatsonx.aiの基本（LLMとChatLLMの違い、invoke関数）についてまとめて備忘録にしようかと思います。
もしご参考になる場合は、langchainを最新のバージョン（現時点ではv0.3）にしてからお試しください。

始める前にwatsonx側で`APIキー`、`Project id`、`Endpoint url`の3点を用意し、.envファイルに保存しておきます。
それでは本題に入ります。

# 目次

1. ライブラリのインポート
2. modelのインスタンス化
3. modelにpromptを投げる

## 1. ライブラリのインポート
今回はLLMとChatLLMの2つの基本的な操作についてまとめます。
必要なライブラリをインポートしておきます。
ついでにenvファイル情報もロードしておきます。
```python
from langchain_ibm import ChatWatsonx, WatsonxLLM
import os
from dotenv import load_dotenv
load_dotenv()
```
今回は見やすさの為に環境変数をセットしていませんが、もしセットしたい場合は[公式doc](https://python.langchain.com/docs/integrations/llms/ibm_watsonx/)を参考にしながらセットしてください。


## 2. modelのインスタンス化
LLMとChatLLMをインスタンス化します。
パラメーターは辞書形式で引数に入れることができます。
今回、再現性の確保や説明の都合上でgreedyメソッドに、モデルはllama3.1-70bをセットをしていきます。
```python
parameters = {
    "decoding_method": "greedy", # or sample
    "max_new_tokens": 100,
    "min_new_tokens": 1,
    # "stop_sequences": ["。", "."],
    # "temperature": 0.5,
    # "top_k": 50,
    # "top_p": 1,
}

# model id:
## https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-api-model-ids.html?context=wx&locale=ja

chat = ChatWatsonx(
    model_id="meta-llama/llama-3-70b-instruct",
    url=os.getenv("WATSON_URL"),
    project_id=os.getenv("WATSON_ID"),
    params=parameters,
    apikey=os.getenv("WATSON_API")
)

llm = WatsonxLLM(
    model_id="meta-llama/llama-3-70b-instruct",
    url=os.getenv("WATSON_URL"),
    project_id=os.getenv("WATSON_ID"),
    params=parameters,
    apikey=os.getenv("WATSON_API")
)
```
利用可能なModel IDについては途中のコメント部分のURLを参照することで確認できます。
また、別の方法としてわざと適当な文字列を入れるとエラー文で確認することもできます。（こちらの方が普段はよく使っています。汗）

`↓`の様な感じです。
```python
ChatWatsonx(
    model_id="abc", # 適当にいれる
    url=os.getenv("WATSON_URL"),
    project_id=os.getenv("WATSON_ID"),
    params=parameters,
    apikey=os.getenv("WATSON_API")
)
```
![スクリーンショット 2024-09-17 215817.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491855/c682c48d-f388-bd5c-ca92-a9c03a3990f0.png)


## 3. modelにpromptを投げる
LLMとChatLLMのどちらも`model.invoke(prompt)`でmodelに対してpromptを投げることが可能になっています。
ここではもう少しだけ深ぼって、何が違うか？について見ていきたいと思います。

まずは戻り値のデータ型です。
下記を実行していただくとご確認いただけますが、ChatLLMの場合はAIMessage型であり、LLMはstr型になっています。
```python
chat_result1 = chat.invoke("こんにちは！")
print(type(chat_result1))
# langchain_core.messages.ai.AIMessage

llm_result1 = llm.invoke("こんにちは！")
print(type(llm_result1))
# str
```

とは言いつつもこれが大きな違いという訳ではありません、本題は次です。

先ほどのコードを実行していただくと何となくLLMの方がChatLLMより生成が長く感じたのでは無いでしょうか？
greedyで全く同じpromptを投げて時間が長いと感じる場合、ほとんどの場合はoutputされているトークン数が多いことが原因です。（個人的感覚ですが）

実際に中身を見てみましょう。AIMessageの場合は`.content`で中身を確認できます。
```python
print(chat_result1.content)
# こんにちは！お元気ですか？

print(llm_result1)
# * 2019年：《我們的青春》飾演 李小雨

# ### MV

# * 2015年：《小幸運》by 田馥甄
# * 2016年：《我想你》by 李榮耀

# ### 綜藝節目

# * 2015年：《一站到底》第3季
# * 2016年：《我們來了》第2季
# * 2017年：
```

コメントアウトで少し見づらくなっていますが明らかにLLMもoutputがおかしなことに気づかれるかもしれません。
これはChatLLMとLLMのinvoke関数の中身が若干違うことが原因です。

LLM.invoke()の場合はpromptの文字列をそのままLLMに投げています。反対にChatLLMの場合は内部で`_convert_input()`が動き、自動的にHumanMessage型に変換されます。

もう少し簡潔に書くと、「LLMの場合は単純に次の文字を予測していて、Chatの場合は構造化しているからchatの文脈であることを理解している」という事になります。

watsonxのプロンプトラボを利用して検証してみましょう。

まずはフリーフォームで「こんにちは！」と書き実行してみます。（パラメーターやモデルに注意してください。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491855/c453be93-ae92-e21c-58a6-ed13d33029eb.png)
先ほどのLLMと同様の結果が得られたかと思います。

では、ChatLLMの構造化とは何でしょうか？
ChatLLMでは下記の2つどちらかの方法でMessageをリスト化してからinvoke()でmodelに渡すことができます。
```python
# 方法1
messages = [
    ("system", "You are a helpful assistant that translates English to French."),
    (
        "human",
        "I love you for listening to Rock.",
    ),
]

chat.invoke(messages)


# 方法2
from langchain_core.messages import (
    HumanMessage,
    SystemMessage,
)

system_message = SystemMessage(
    content="You are a helpful assistant which telling short-info about provided topic."
)
human_message = HumanMessage(content="horse")

chat.invoke([system_message, human_message])
```

方法1の方が構造化のイメージが湧きやすいのではないでしょうか？
実際にこの形になぞってpromptを作り直しプロンプトラボで試してみましょう。
下記のpromptを投げてみます。
```text
human: こんにちは！
assistant:
```

![スクリーンショット 2024-09-17 223435.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491855/b20a71a8-203e-5891-652c-89d49a5eeeab.png)

ChatLLMと同様の結果を確認できたでしょうか？

このようにLLMとChatLLMでは内部で構造化しているかどうかの違いが生じるため、同じパラメーター、同じpromptを投げても結果が変わることがあるため気を付けてください！😊