クラウドサービス事業部の松浦です。
今回はAWS Summit 2024に向けて生成AIを使用したシステムを開発した際に効果的だった手法やコツがありましたのでご紹介いたします。
目次
はじめに
みなさんもご存知かもしれませんが、生成AIは要求された入力値に対して学習データを基に確率的に次に続く可能性が高い単語・文章を出力する手法をとっています。
そのため生成AIが不正確または完全に架空の情報を出力してしまう現象があります。この問題は、特に信頼性が求められる分野でのAI活用において、大きなリスクとなります。
これはハルシネーションと呼ばれる現象で、生成AIを扱う際には切り離せない現象となっています。
本記事では評価AIを用いた再生成ロジックを導入することで、生成AIのハルシネーションリスクを軽減する手法について解説します。
この手法により、生成されたテキストが適切かどうかをチェックし、必要に応じて再生成を行うことで、出力結果の精度と信頼性を向上させることができます。
使用技術
- AWS SDK for JavaScript (v3)
- AWS Lambda (Node 20)
- AWS Bedrock Claude3 Haiku
- TypeScript
コードの詳細
全体コード
先に完成形をお見せします。
今回は会話文を要約するようAIにリクエストします。
処理の流れとしてはリクエストされた会話文をBedrockを通して要約した後その出力結果が正しいかBedrockを通して評価させます。評価した結果正しくないと判定された場合要約を再生成し再度評価するようループ処理を3回まで実行させます。
本処理では与えられた会話文をテキスト要約するという単純なロジックなので評価AIを挟まなくとも精度の高いものが得られましたが、インプットしたテキストに記載されていない未知の情報をAIに出力させるような要件の際にこのような評価AIを挟む手法はとても有効でした。
index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand, InvokeModelWithResponseStreamCommandOutput, InvokeModelWithResponseStreamCommandInput, } from "@aws-sdk/client-bedrock-runtime"; import { modelId, maxToken, chatSummarySystemPrompt, reviewSummarySystemPrompt, } from "./constants/bedrock"; const client = new BedrockRuntimeClient({ region: "us-east-1", }); type ReviewSummaryOutputText = { result: "success" | "failed"; }; export const handler = async (event: any, context: any) => { const chatSummaryOutputText = await chatSummary(event); console.log(`chatSummaryOutputText: ${chatSummaryOutputText}`); const reviewSummaryOutputText = await reviewSummary( event, chatSummaryOutputText ); console.log( `reviewSummaryOutputText: ${JSON.stringify(reviewSummaryOutputText)}` ); let generateCount = 1; const chatSummaryOutputTextLoopResult = await checkGenerateSummaryLoop( event, generateCount, JSON.parse(reviewSummaryOutputText), chatSummaryOutputText ); if (chatSummaryOutputTextLoopResult === null) { return createRespose(500, "failed to generate summary"); } return createRespose(200, chatSummaryOutputTextLoopResult); }; // レビューした結果、failedが帰ってきたらもう一度生成しなおし、3回までループする const checkGenerateSummaryLoop: Function = async ( event: any, generateCount: number, reviewSummaryOutputText: ReviewSummaryOutputText, chatSummaryOutputText: string ) => { if (reviewSummaryOutputText.result === "success") { return chatSummaryOutputText; } else if (reviewSummaryOutputText.result === "failed" && generateCount < 4) { const reChatSummaryOutputText = await chatSummary(event); const reReviewSummaryOutputText = await reviewSummary( event, chatSummaryOutputText ); return await checkGenerateSummaryLoop( event, generateCount + 1, JSON.parse(reReviewSummaryOutputText), reChatSummaryOutputText ); } else if ( reviewSummaryOutputText.result === "failed" && generateCount >= 4 ) { return null; } }; // レビューAI実行 const reviewSummary = async (event: any, outputText: string) => { const userPrompt = ` 生成AIには以下のように指示しました <systemPrompt> ${chatSummarySystemPrompt} </systemPrompt> HumanMessageは以下のように渡しました。 <HumanMessage> ${JSON.stringify(event.body) || JSON.stringify(dummyMessages)} </HumanMessage> その結果AIが出力したテキストは以下でした。 <generatedText> ${outputText} </generatedText> 生成AIの出力テキストは正しいですか? JSONフォーマットで「result」キーにfailedかsuccessを入れて答えてください。 `; const requestBody = { anthropic_version: "bedrock-2023-05-31", system: reviewSummarySystemPrompt, max_tokens: maxToken, temperature: 0.1, messages: [ { role: "user", content: [{ type: "text", text: userPrompt }], }, ], }; const params: InvokeModelWithResponseStreamCommandInput = { modelId: modelId, contentType: "application/json", accept: "application/json", body: JSON.stringify(requestBody), }; const command = new InvokeModelWithResponseStreamCommand(params); const response: InvokeModelWithResponseStreamCommandOutput = await client.send(command); console.log(`response: ${JSON.stringify(response)}`); return await getOutputTextFromInvokeModelWithResponseStreamCommandOutput( response ); }; // ユーザーからインプットされた会話文の要約をBedrockに投げる const chatSummary = async (event: any) => { const userPrompt: string = JSON.stringify(event.body) || JSON.stringify(dummyMessages); const requestBody = { anthropic_version: "bedrock-2023-05-31", system: chatSummarySystemPrompt, max_tokens: maxToken, temperature: 0.1, messages: [ { role: "user", content: [{ type: "text", text: userPrompt }], }, ], }; const params: InvokeModelWithResponseStreamCommandInput = { modelId: modelId, contentType: "application/json", accept: "application/json", body: JSON.stringify(requestBody), }; const command = new InvokeModelWithResponseStreamCommand(params); const response: InvokeModelWithResponseStreamCommandOutput = await client.send(command); console.log(`response: ${JSON.stringify(response)}`); return await getOutputTextFromInvokeModelWithResponseStreamCommandOutput( response ); }; // Bedrockからのレスポンスを文字列に整形 const getOutputTextFromInvokeModelWithResponseStreamCommandOutput = async ( response: InvokeModelWithResponseStreamCommandOutput ) => { let res = []; try { for await (const body of response.body!) { if (body.chunk && body.chunk.bytes) { const chunk = JSON.parse( Buffer.from(body.chunk.bytes).toString("utf-8") ); // console.log(`chunk: ${JSON.stringify(chunk)}`); if (chunk.delta && chunk.delta.text) { res.push(chunk.delta.text); } } } } catch (err) { console.log( `getOutputTextFromInvokeModelWithResponseStreamCommandOutputError: ${err}` ); } return res.join(""); }; const createRespose = (statusCode: number, body: any) => { return { statusCode: statusCode, body: { data: body, }, }; }; |
./constants/bedrock.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
export const modelId = "anthropic.claude-3-haiku-20240307-v1:0"; export const maxToken = 4096; export const chatSummarySystemPrompt: string = ` あなたは会話文を要約する生成AIです。 maxTokenは${maxToken}なので超えないように要約してください。 また、出力する文章が会話文の要約に適しているかよく考えた上で出力してください。 `; export const reviewSummarySystemPrompt: string = ` あなたは生成AIの出力テキストが正しいか判定する評価AIです。 生成AIに渡されたsystemPrompt、HumanMessageと生成AIが出力したテキストを渡すので正しければsuccessを、間違っていたり確証が取れないことを述べていたらfailedを出力してください。 なお、あなたが返す出力データはJSONフォーマットで「result」キーのみを返し、その他のフィラーや相槌などは返さないでください。 以下はあなたが評価した出力が正しかった場合の出力例です。 <example> { result: "success", } </example> では、この後にHumanから生成AIに渡されたsystemPrompt、HumanMessageと生成AIが出力したテキストを渡すので厳密に評価してください。 `; |
実際の処理結果
コードの実際の処理結果はこちらです。
リクエストする会話文
要約された結果
フロントUIはAWS Summit用に用意したものですが、バックエンドのロジックは本記事のコードを使用しています。
それでは本コードについて解説します。
必要なインポート
まずは必要なパッケージと定数をインポートします。
1 2 3 4 5 6 7 8 9 10 11 12 |
import { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand, InvokeModelWithResponseStreamCommandOutput, InvokeModelWithResponseStreamCommandInput, } from "@aws-sdk/client-bedrock-runtime"; import { modelId, maxToken, chatSummarySystemPrompt, reviewSummarySystemPrompt, } from "./constants/bedrock"; |
定数フォルダには以下のように生成AIのモデルとシステムプロンプトを定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
export const modelId = "anthropic.claude-3-haiku-20240307-v1:0"; export const maxToken = 4096; export const chatSummarySystemPrompt: string = ` あなたは会話文を要約する生成AIです。 maxTokenは${maxToken}なので超えないように要約してください。 また、出力する文章が会話文の要約に適しているかよく考えた上で出力してください。 `; export const reviewSummarySystemPrompt: string = ` あなたは生成AIの出力テキストが正しいか判定する評価AIです。 生成AIに渡されたsystemPrompt、HumanMessageと生成AIが出力したテキストを渡すので正しければsuccessを、間違っていたり確証が取れないことを述べていたらfailedを出力してください。 なお、あなたが返す出力データはJSONフォーマットで「result」キーのみを返し、その他のフィラーや相槌などは返さないでください。 以下はあなたが評価した出力が正しかった場合の出力例です。 <example> { result: "success", } </example> では、この後にHumanから生成AIに渡されたsystemPrompt、HumanMessageと生成AIが出力したテキストを渡すので厳密に評価してください。 `; |
reviewSummarySystemPromptは評価AIのシステムプロンプトを定義しており、JSON形式で生成するように指示をしています。
XMLタグを使用することで出力の精度を上げています。詳しくはAnthropicのプロンプトエンジニアリング XMLタグを使用するに詳しく書いておりますのでそちらをご参照ください。
また、生成AIにJSON形式で出力させる手法についてはAnthropicの プロンプトエンジニアリング 出力フォーマットの制御 (JSONモード)に詳しく書いておりますのでそちらをご参照ください。
クライアントの設定
次に、AWS Bedrockのクライアントを設定します。ここでは、us-east-1
リージョンを使用しています。
1 2 3 |
const client = new BedrockRuntimeClient({ region: "us-east-1", }); |
ハンドラ関数の定義
イベントハンドラ関数では、入力イベントを元に要約を生成し、その要約が正しいかを評価します。評価が失敗した場合、最大3回まで再度要約を生成し評価を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
export const handler = async (event: any, context: any) => { const chatSummaryOutputText = await chatSummary(event); console.log(`chatSummaryOutputText: ${chatSummaryOutputText}`); const reviewSummaryOutputText = await reviewSummary( event, chatSummaryOutputText ); console.log( `reviewSummaryOutputText: ${JSON.stringify(reviewSummaryOutputText)}` ); let generateCount = 1; const chatSummaryOutputTextLoopResult = await checkGenerateSummaryLoop( event, generateCount, JSON.parse(reviewSummaryOutputText), chatSummaryOutputText ); if (chatSummaryOutputTextLoopResult === null) { return createRespose(500, "failed to generate summary"); } return createRespose(200, chatSummaryOutputTextLoopResult); }; |
要約生成と評価のループ処理
評価が失敗した場合に再度要約を生成し、評価するループ処理を定義します。
ループ処理には再帰関数を使用しており、無限ループを防ぐため3回までのループと設定しています。。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
const checkGenerateSummaryLoop: Function = async ( event: any, generateCount: number, reviewSummaryOutputText: ReviewSummaryOutputText, chatSummaryOutputText: string ) => { if (reviewSummaryOutputText.result === "success") { return chatSummaryOutputText; } else if (reviewSummaryOutputText.result === "failed" && generateCount < 4) { const reChatSummaryOutputText = await chatSummary(event); const reReviewSummaryOutputText = await reviewSummary( event, chatSummaryOutputText ); return await checkGenerateSummaryLoop( event, generateCount + 1, JSON.parse(reReviewSummaryOutputText), reChatSummaryOutputText ); } else if ( reviewSummaryOutputText.result === "failed" && generateCount >= 4 ) { return null; } }; |
要約生成関数
ユーザーの入力を元にAWS Bedrockを使用し要約を生成する関数を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
const chatSummary = async (event: any) => { const userPrompt: string = JSON.stringify(event.body) || JSON.stringify(dummyMessages); const requestBody = { anthropic_version: "bedrock-2023-05-31", system: chatSummarySystemPrompt, max_tokens: maxToken, temperature: 0.1, messages: [ { role: "user", content: [{ type: "text", text: userPrompt }], }, ], }; const params: InvokeModelWithResponseStreamCommandInput = { modelId: modelId, contentType: "application/json", accept: "application/json", body: JSON.stringify(requestBody), }; const command = new InvokeModelWithResponseStreamCommand(params); const response: InvokeModelWithResponseStreamCommandOutput = await client.send(command); console.log(`response: ${JSON.stringify(response)}`); return await getOutputTextFromInvokeModelWithResponseStreamCommandOutput( response ); }; |
要約評価関数
生成された要約が正しいかを評価する関数を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
const reviewSummary = async (event: any, outputText: string) => { const userPrompt = ` 生成AIには以下のように指示しました <systemPrompt> ${chatSummarySystemPrompt} </systemPrompt> HumanMessageは以下のように渡しました。 <HumanMessage> ${JSON.stringify(event.body) || JSON.stringify(dummyMessages)} </HumanMessage> その結果AIが出力したテキストは以下でした。 <generatedText> ${outputText} </generatedText> 生成AIの出力テキストは正しいですか? JSONフォーマットで「result」キーにfailedかsuccessを入れて答えてください。 `; const requestBody = { anthropic_version: "bedrock-2023-05-31", system: reviewSummarySystemPrompt, max_tokens: maxToken, temperature: 0.1, messages: [ { role: "user", content: [{ type: "text", text: userPrompt }], }, ], }; const params: InvokeModelWithResponseStreamCommandInput = { modelId: modelId, contentType: "application/json", accept: "application/json", body: JSON.stringify(requestBody), }; const command = new InvokeModelWithResponseStreamCommand(params); const response: InvokeModelWithResponseStreamCommandOutput = await client.send(command); console.log(`response: ${JSON.stringify(response)}`); return await getOutputTextFromInvokeModelWithResponseStreamCommandOutput( response ); }; |
レスポンスの生成
最終的なレスポンスを生成する関数を定義します。
1 2 3 4 5 6 7 8 |
const createRespose = (statusCode: number, body: any) => { return { statusCode: statusCode, body: { data: body, }, }; }; |
終わりに
AWS Bedrockを利用することで、システムの中に生成AIを使用することが容易になります。
しかし、生成AIを使用する場合は生成AIのリスクについても向き合わねばなりません。
本ブログの内容が、皆様の開発に役立つことを願っています。興味のある方は、ぜひAWS Bedrockのドキュメントもご覧ください。
- AWS Bedrockを活用したAI生成テキスト評価と再生成の実装技法 - 2024-06-17
- AWSから公開されたJavaScriptランタイム「LLRT」を使ったLambdaをAWS CDKで構築する方法 - 2024-02-19
- 【Bun】JavaScriptでシェルスクリプトを書けると噂のBun Shell使ってみた - 2024-02-08
- 【Next.js】Next.js 14をAmplify V6でデプロイ・ホスティングする方法【Amplify】 - 2024-02-06
- JavaScript最新の動向【JavaScript Rising Stars2023】 - 2024-02-01