Amazon BedrockからClaudeを使う

# 業務で使えるAIチャットボットを立ち上げたい。

世の中に大規模言語モデル(LLM)を利用する事例が増えてくるにつれ、社内でも生成AIを使って問い合わせ回答のメールテンプレートを作ったりプログラムコードの生成とかができるような環境を作って生産性を上げようということでRAG環境の構築を始めた。

AWSのドキュメントやワークショップ環境を参考にしながら進めつつ、いったん想定としてはSlackからSlackアプリに質問チャットするのをトリガーとして【Slack Bolt(JS) → Lambda → Bedrock】の流れで構築してみようとしている。

まずはいったんAWS SDKからbedrockを使う土台を作るところから始めたので、その時のメモ。

# まずは必要なモジュールをインストール

`npm install @aws-sdk/client-bedrock-runtime`

※ credentialを使った認証が必要な場合は`@aws-sdk/credential-providers`も一緒に。

# プログラム(Node.js 20.xランタイムで実装)

### Bedrockのクライアントインスタンスを生成

段階的に表示する様子を見てみたかったので、
今回は`InvokeModelWithResponseStreamCommand`で実装してみた。

```js
import {
  BedrockRuntimeClient,
  InvokeModelWithResponseStreamCommand
} from '@aws-sdk/client-bedrock-runtime';
const client = new BedrockRuntimeClient({ region: 'ap-northeast-1' });

/*
// credentialsを使う場合
import { fromIni } from '@aws-sdk/credential-providers';
const credentials = fromIni({ profile: 'bedrockAccessAcount' });
const client = new BedrockRuntimeClient({ region: 'ap-northeast-1', credentials });
*/
```

### 質問プロンプト

- システムプロンプト(`systemPrompt`): Claudeに役割を指定する。
- ユーザープロンプト(`userPrompt`): Claudeに質問する内容を指定する。

```js
const systemPrompt = `
あなたはJavascriptの関数を作成するアシスタントです。
<ユーザーの要件></ユーザーの要件>に従ったプログラムを提供します。
`;

const userPrompt = `
<ユーザーの要件>
以下の<code></code>をaxiosからビルトインのfetchを使う方法に置き換えたjavascriptのコード
<code>
import axios from 'axios';
const response = await axios({
  url: 'https://example.com/api/handle', method: 'POST',
  headers: { 'Content-Type': 'application/json', Authorization: \`Bearer token/str\`, },
  data: { channel, ts },
});
</code>
</ユーザーの要件>
`;
```

### Claudeのパラメーター

Claudeはテキスト補完APIとメッセージAPIの2種類がある。
- テキスト補完API
  - 一往復のやり取りで、テキスト生成に向いている。
- メッセージAPI
  - messagesパラメーターに会話のやり取りを順番に格納して、会話をするような実装に向いている。

今回はメッセージAPIのパラメーターを採用する。ちなみにテキスト補完APIでも`Human: `や`Assistant: `のキーワードを使ってメッセージAPIと同じようなことは可能。


以下の例では、まず最初にユーザーが「こんにちは」と入力した後、Claudeから「こんにちは、Claudeです。どうされました？」という回答が返ってきたことを前提にして、それに続く会話を変数`userPrompt`から入力するという要領。

```js
const anthropicClaudeParams = {
  mssagesApi: {
    anthropic_version: 'bedrock-2023-05-31',
    system: systemPrompt,
    messages: [
      { role: 'user', content: [{ type: 'text', text: 'こんにちは。' }] },
      { role: 'assistant', content: [{ type: 'text', text: 'こんにちは、Claudeです。どうされました？' }] },
      { role: 'user', content: [{ type: 'text', text: userPrompt }] },
    ],
    /*
    // テキストだけのやりとりの場合は、以下のような省略形の書き方もできる。
    messages: [
      { role: 'user', content: 'こんにちは。' },
      { role: 'assistant', content: 'こんにちは、Claudeです。どうされました？' },
      { role: 'user', content: userPrompt },
    ],
    */
    max_tokens: 3e3,
    temperature: 0.9,
    top_p: 0.9,
    top_k: 300,
    stop_sequences: ['Human:'],
  },
};
```

### BedrockのAPIを実行(Claudeに質問)する

※ `accept`や`contentType`はデフォルト値が`'application/json'`なので省略可。

```js
const command = new InvokeModelWithResponseStreamCommand({
  accept: 'application/json',
  body: JSON.stringify(anthropicClaudeParams.mssagesApi),
  contentType: 'application/json',
  modelId: 'anthropic.claude-v2:1',
});

const response = await client.send(command);
```

### Claudeの回答をコンソール上に表示する。

`for await...of`を使ってResponseStreamを読み込むごとに段階的に表示する。

```js
for await (const streamChunk of response.body) {
  if (!streamChunk.chunk?.bytes) { break; }

  const body = JSON.parse(new TextDecoder('utf-8').decode(streamChunk.chunk?.bytes));

  if (body.type === 'content_block_delta' && body.delta.type === 'text_delta') {
    // 読み込んだResponseStreamから回答の文字を標準出力に表示。
    process.stdout.write(body.delta.text);
  } else if (body.type === 'message_delta') {
    // 回答の出力が終わったら回答に関する情報を表示。
    console.log(''); // 無理やり改行を入れる。
    console.log('body.delta.stop_reason:', body.delta.stop_reason);
    console.log('body.delta.stop_sequence:', body.delta.stop_sequence);
    console.log('body.usage.output_tokens:', body.usage.output_tokens);
  }
}
```

ちなみに読み込んだストリームは、`console.log()`だと末尾に改行が入って見栄えが悪くなるので、`process.stdout.write()`を使って出力するようにした。
今回は開発者コンソールやLambdaなどではなく、nodeコマンドで土台プログラムを実行してるので、いったん標準出力を制御するオブジェクトを使った。

```js
// console.log(body.delta.text); だと中途半端に改行が入る。
/*
はい
、
axios
から
fetch
に
変換する
コード例
を
示します
。
*/

// process.stdout.write(body.delta.text); でいい感じの見栄えにした。
/*
はい、axiosからfetchに変換するコード例を示します。
*/
```

### 出力例

~~~text
はい、axiosからfetchに変換するコード例を示します。

```js
async function request() {

  const response = await fetch('https://example.com/api/handle', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer token/str`,
    },
    body: JSON.stringify({
      channel,
      ts 
    })
  });

  const data = await response.json();

  return data;
}
```

主な変更点は以下の通りです。

- fetch関数を使うように変更
- request bodyをJSON.stringifyで文字列に変換する必要がある
- responseのデータはjsonメソッドでパースする必要がある

axiosはPromiseベースのHTTPクライアントで、fetchよりも使いやすいインターフェースを提供していますが、fetchはブラウザやNode.jsの標準APIなので、このように変換できます。

ご不明な点があれば遠慮なくご質問ください。
body.delta.stop_reason: end_turn
body.delta.stop_sequence: null
body.usage.output_tokens: 280
~~~

# 最後に

イイ感じに土台ができたので、次はこれをAmazon Kendraと組み合わせて

1. Slackのチャットからbedrockを使ってクエリテキストを生成する。
1. クエリテキストを使ってKendraのRetrieve APIを使って検索リクエストする。
1. 検索リクエスト結果を参考にさらにbedrockで自然言語の回答文を生成する。

というところまで、実装してみる。

# 参考情報
https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html