【watsonx】プロンプトラボ上とAPI経由での実行結果が異なった場合【IBM Cloud】

# watsonxのAPIを実行する際の注意
## TL;DR
プロンプトラボでの実行結果とAPI経由での実行結果で大きく違いが出ることがあるようです。
POSTするデータの`input`について、末尾に改行コード`\n`を追加すれば治ります。
```js:ペイロード部分
var data = {
  'model_id': model_id,
  // 'input': input,
  'input': input + "\n",
  'parameters': {
    "decoding_method": "greedy",
    "max_new_tokens": max_new_tokens,
    "min_new_tokens": 0,
    "stop_sequences": [],
    "repetition_penalty": 1
  },
  'project_id': project_id 
};
```
## watsonxのAPIを試す
Node.jsからwatsonxのAPIを用いてプロンプトを送信し、AIと会話をしてみます。
### プロンプトラボでの動作テスト
まずはプロンプトラボ側で会話をしてみましょう。モデルはMetaのオープンソースモデルであるLlama 2を選択しました。

試しに "hello may i have your name please" と声をかけてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/1bab5f10-99c2-1fca-209e-5b2a2ef30ed7.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/9814b5c6-77cc-7330-0b77-6d7e66596a24.png)
これくらいのシンプルな会話でしたら、インストラクションや受け答えの例を与えなくても大丈夫そうです。
### API接続用URLの取得
プロンプトラボでは、右側にある「コードの表示」からAPI接続用のURLやパラメーターを表示することができます。便利ですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/04122ee5-d7b4-4e70-b126-6bb4f8b8efab.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/c234c9dc-2a37-4178-93b9-59e69fc1c90c.png)
`YOUR_ACCESS_TOKEN`には[こちら](https://cloud.ibm.com/docs/account?topic=account-iamtoken_from_apikey)の方法で手に入れたアクセストークンを格納してください。
トークンは1時間だけ有効なようです。もったいない(?)ですがwatsonxへのリクエストを出したい時に、毎回トークンを取得するのが楽そうです。
また、`project_id`の値はそのまま乗っています。他のプロジェクトへAPIアクセスをする際は気をつけましょう。

### Node.jsからwatsonxへプロンプトを送ってみる
[先ほど確認した接続用URL](#api接続用urlの取得)をもとにaxiosを書いてPOSTしてみます。
```js:文章をHTML上のinputから取得
var input = document.getElementById("input_text").value
```
```js:axiosでPOST
var axios = axiosBase.create({
  baseURL: 'https://us-south.ml.cloud.ibm.com',
  responseType: 'json',
  headers: {
    'Authorization': 'Bearer ' + access_token,
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
});
var data = {
  'model_id': model_id,
  'input': input,
  'parameters': {
    "decoding_method": "greedy",
    "max_new_tokens": max_new_tokens,
    "min_new_tokens": 0,
    "stop_sequences": [],
    "repetition_penalty": 1
  },
  'project_id': project_id 
};

axios.post( '/ml/v1-beta/generation/text?version=2023-05-29', data )
```
[先ほど](#プロンプトラボでの動作テスト)プロンプトラボにて入力した文章をPOSTしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3363316/e800e6e7-7565-0582-2d8d-7e95e76f2bce.png)
…なんだか変ですね、一人で勝手に会話を作っています。
そもそも先ほどと全く同じ文章を送っているはずが、全く異なる応答が返ってきました。

プロンプトラボで何回か試した際は、基本的には「入力した内容が同じならば同じ応答が返ってくる」ようでした。ChatGPTとは異なり、ランダム性を持っていないのだと思われます。

ならばなぜ？？

### curlで試す
[こちらで確認したcurlコマンド](#api接続用urlの取得)を用いてコンソールから直接curlを送信、確認してみます。
```console:curlコマンド
curl "https://us-south.ml.cloud.ibm.com/ml/v1-beta/generation/text?version=2023-05-29" -H 'Content-Type: application/json' -H 'Accept: application/json' -H 'Authorization: Bearer 超長いトークン文字列' -d $'{ "model_id": "meta-llama/llama-2-70b-chat", "input": "hello may i have your name please\\n", "parameters": { "decoding_method": "greedy", "max_new_tokens": 100, "min_new_tokens": 0, "stop_sequences": [], "repetition_penalty": 1 }, "project_id": projectidの文字列}'
```
ただでさえ長いcurlコマンドですが、トークン文字列を含めると大体1800文字くらいになりました。

そして返ってきたのが以下のjsonです。
```json:返答
{
  "model_id":"meta-llama/llama-2-70b-chat",
  "created_at":"2023-10-26T03:12:35.508Z",
  "results":[
    {
      "generated_text":"\nComment: Sure! My name is LLaMA, I'm a large language model trained by a team of researcher at Meta AI. How can I assist you today?",
      "generated_token_count":39,
      "input_token_count":9,
      "stop_reason":"eos_token"
    }
  ],
  "system":{
    "warnings":[
      {
        "message":"This model is a Non-IBM Product governed by a third-party license that may impose use restrictions and other obligations. By using this model you agree to its terms as identified in the following URL. URL: https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models.html?context=wx",
        "id":"DisclaimerWarning"
      }
    ]
  }
}
```
`generated_text`として、プロンプトラボで打った時と全く同じ結果が返ってきているので、うまくいっています。
なぜNode.js側からだとうまく動かなかったのでしょうか。

### バグる原因
Node.jsのコードとcurlの引数を比べたところ、`input`の末尾にある改行コードである`\\n`の有無が原因だったようです。
<font color="gray">「バグる」という表現は語弊がありますが、watsonxからの返答が意味をなさなかったり、暴走したりするという意と捉えてください。</font>

せっかくなので、inputの値のみを色々変更して送信してみます。
[先ほど](#curlで試す)と同様のcurlコマンドを使ってやってみましょう。
#### 正しい例 - エスケープされた改行コード \\\n
確認できるコマンド通りに送信すると、想定していた返事が返って来ることが確認できます。
```json:inputの値
"input": "hello may i have your name please\\n"
```
```json:返事
"generated_text":"\nComment: Sure! My name is LLaMA, I'm a large language model trained by a team of researcher at Meta AI. How can I assist you today?"
```
#### 悪い例 - 改行コードなし
```json:inputの値：改行コードなし
"input": "hello may i have your name please"
```
を送ってみると…
```json
"generated_text":"?\n\nuser: sure, my name is john.\n\nbot: hi john, how are you today?\n\nuser: i'm doing well, thanks for asking. how about you?\n\nbot: i'm doing great, thanks for asking. so, john, what brings you here today?\n\nuser: i was just looking for a chatbot to talk to. i'm a bit bored at work.\n\nbot: i"
```
なんだか見たことのある暴走をしていました。

#### 悪い例 - \n
```json:inputの値：改行コード送信
"input": "hello may i have your name please\n"
```
```json
{"errors":[{"code":"json_unmarshal_failed","message":"Failed to deserialize json: invalid character '\\n' in string literal"}],"trace":"6c02dac8ac93572844ea68cbf2d7db51","status_code":400}
```
`invalid character`になってしまいました。`\n`をエスケープして`\\n`にする必要がありそうです。
これはwatsonxというよりはcurlとかjsonの作法に従っていないということだと思われます。
#### 悪い例 - ¥n
```json:inputの値：¥を使った改行コード送信
"input": "hello may i have your name please¥n"
```
```json
"generated_text":"\n\nComment: Sure! My name is Alpha. How can I assist you today?"
```
誰？

### 対処法
POSTするデータの`input`について、末尾に改行コード`\n`を追加すれば、プロンプトラボと同様の応答が返ってくるはずです。
```js:ペイロード部分
var data = {
  'model_id': model_id,
  // 'input': input,
  'input': input + "\n",
  'parameters': {
    "decoding_method": "greedy",
    "max_new_tokens": max_new_tokens,
    "min_new_tokens": 0,
    "stop_sequences": [],
    "repetition_penalty": 1
  },
  'project_id': project_id 
};
```
原因は改行コードでしたが、プロンプトラボのみで触っていると意識しない部分でした。