こんにちは、広野です。
本記事はシリーズもので、以下の記事の続編です。
以前、以下の記事で Amazon Bedrock や Agents for Amazon Bedrock を使用した最小構成 RAG 環境構築を紹介しておりました。当時はAmazon Bedrock 関連のリソースを一部 AWS CloudFormation ではデプロイできなかったのですが、今はサポートされたためにできるようになりました。
当時の構成を現在は変更しており、Knowledge Base に使用するデータベースを Amazon OpenSearch Serverless から Aurora Serverless v2 Postgresql に変更したり、モデルを Claude 3.5 Sonnet に変更したりしています。
本シリーズ記事では、環境構築用の AWS CloudFormation のサンプルテンプレートを 3 記事に分けて紹介します。説明を分割するため、テンプレートを3つに分けていますのでご了承ください。
3回目は Amazon Bedrock 編です。
本記事で取り扱う構成
RAG 環境全体の構成
以下のアーキテクチャで RAG アプリケーションを構築しています。このうち、赤枠の部分が本シリーズ記事で取り扱う箇所です。すみません、当初 Amazon SQS まで含んでおりましたが、コード量が多くなってしまうため今回から割愛いたしました。
series 3 Bedrock 編では、series 2 で構築した Amazon Aurora Serverless v2 Postgresql を Agents for Amazon Bedrock に Knowledge Base として登録し、RAG を使用しない問い合わせ (一般検索) 用の AWS Lambda 関数、および Agents for Amazon Bedrock に問い合わせるための AWS Lambda 関数 URL をデプロイします。
Agents for Amazon Bedrock の構成
Agents for Amazon Bedrock は、ユーザーからの問い合わせの内容から、裏にあるどのナレッジから回答を得るべきか仕分けの判断をしてくれます。ただし、判断基準となる情報は提供してあげないといけないので、それを自然言語で設定します。
- 「SCSK」に関する問い合わせであった場合は、Knowledge Base を確認するようにします。(RAG 検索ルート)
- それ以外の問い合わせであれば、AWS Lambda 関数を呼び出します。この関数は Amazon Bedrock Claude モデルに問い合わせます。(一般検索ルート)
- アプリから Agents for Amazon Bedrock に問い合わせるためには、それ用の AWS Lambda 関数が必要です。ただし関数だけでは API から問い合わせを受け付けられないので、Lambda 関数 URL で公開しています。代わりに API Gateway でもかまいませんが、ストリームレスポンスに対応していなかったため Node.js の関数 URL にしました。
- AWS Lambda 関数 URL には、CORS 設定のためドメイン情報を設定しています。ドメイン名などの情報は、AWS CloudFormation テンプレートのパラメータとして入力するようにしています。
- AWS Lambda 関数 URL の認証については本記事では割愛します。以下の参考記事をご覧ください。
AWS CloudFormation テンプレート
図に掲載している赤字部分を今回のテンプレートで作成しています。一部、前回のテンプレートで作成したスタックからの情報をインポートしている箇所があります。
使用する Amazon Bedrock (一般検索用) のリージョンや Anthropic Claude モデルのバージョンは簡単に変更可能にするため、パラメータ化しています。技術の進歩が著しいので。
AWSTemplateFormatVersion: 2010-09-09 Description: The CloudFormation template that creates an Agent for Amazon Bedrock, a Bedrock Knowledge base, and a Lambda function URL. # ------------------------------------------------------------# # Input Parameters # ------------------------------------------------------------# Parameters: SubName: Type: String Description: System sub name of sample. (e.g. test) Default: test MaxLength: 10 MinLength: 1 DomainName: Type: String Description: Domain name for URL. Default: scskexample.com MaxLength: 40 MinLength: 5 AllowedPattern: "[^\\s@]+\\.[^\\s@]+" SubDomainName: Type: String Description: Sub domain name for URL. (e.g. xxx of xxx.scskexample.com) Default: xxx MaxLength: 20 MinLength: 1 BedrockAgentAliasName: Type: String Description: The Alias name of Agents for Amazon Bedrock. Default: Default MaxLength: 20 MinLength: 1 BedrockRegion: Type: String Description: The region name you use for Amazon Bedrock Claude 3 model. (e.g. ap-northeast-1) Default: ap-northeast-1 MaxLength: 50 MinLength: 1 ClaudeModelId: Type: String Description: The Claude 3 model ID. (e.g. anthropic.claude-3-5-sonnet-20240620-v1:0) Default: anthropic.claude-3-5-sonnet-20240620-v1:0 MaxLength: 100 MinLength: 1 Resources: # ------------------------------------------------------------# # Bedrock Knowledge Base # ------------------------------------------------------------# BedrockKnowledgeBase: Type: AWS::Bedrock::KnowledgeBase Properties: Name: !Sub sample-${SubName}-kb Description: !Sub RAG Knowledge Base for sample-${SubName} KnowledgeBaseConfiguration: Type: VECTOR VectorKnowledgeBaseConfiguration: EmbeddingModelArn: !Sub arn:aws:bedrock:${AWS::Region}::foundation-model/amazon.titan-embed-text-v1 RoleArn: Fn::ImportValue: !Sub sample-${SubName}-IAMRoleBedrockKbArn StorageConfiguration: Type: RDS RdsConfiguration: CredentialsSecretArn: Fn::ImportValue: !Sub sample-${SubName}-SecretAurora DatabaseName: bedrockragkb FieldMapping: MetadataField: metadata PrimaryKeyField: id TextField: chunks VectorField: embedding ResourceArn: Fn::ImportValue: !Sub sample-${SubName}-AuroraDBClusterArn TableName: bedrock_integration.bedrock_kb Tags: Cost: !Sub sample-${SubName} BedrockKnowledgeBaseDataSource: Type: AWS::Bedrock::DataSource Properties: Name: !Sub sample-${SubName}-kb-datasource Description: !Sub RAG Knowledge Base Data Source for sample-${SubName} KnowledgeBaseId: !Ref BedrockKnowledgeBase DataDeletionPolicy: RETAIN DataSourceConfiguration: Type: S3 S3Configuration: BucketArn: Fn::ImportValue: !Sub sample-${SubName}-S3BucketKbDatasourceArn # ------------------------------------------------------------# # Agents for Amazon Bedrock # ------------------------------------------------------------# BedrockAgent: Type: AWS::Bedrock::Agent Properties: AgentName: !Sub sample-${SubName} Description : !Sub The agent for sample-${SubName} to assign the appropriate knowledge base or action group. AgentResourceRoleArn: !GetAtt BedrockAgentRole.Arn FoundationModel: "anthropic.claude-v2:1" Instruction: | あなたは優秀なAIアシスタントです。ユーザーの指示には日本語で回答してください。SCSKに関する情報が必要な場合はナレッジベースから情報を取得してください。それ以外の問い合わせには、Action Group に設定している Claude foundation model を使用して回答してください。 KnowledgeBases: - KnowledgeBaseId: !Ref BedrockKnowledgeBase KnowledgeBaseState: ENABLED Description: Knowledge Base for the information related to SCSK ActionGroups: - ActionGroupName: UserInputAction ActionGroupState: ENABLED ParentActionGroupSignature: AMAZON.UserInput - ActionGroupName: LambdaBedrockAgentAgClaude ActionGroupState: ENABLED ActionGroupExecutor: Lambda: !GetAtt LambdaBedrockAgentAgClaude.Arn FunctionSchema: Functions: - Name: LambdaBedrockAgentAgClaude Description: "Lambda Function to invoke Bedrock Claude foundation model triggered from Bedrock Agent" Parameters: url: Description: "Invoke the Claude foundation model to answer for a general query except for the related to SCSK." Required: false Type: string Tags: Cost: !Sub sample-${SubName} DependsOn: - LambdaBedrockAgentAgClaude - BedrockAgentRole BedrockAgentAlias: Type: AWS::Bedrock::AgentAlias Properties: AgentAliasName: !Ref BedrockAgentAliasName AgentId: !Ref BedrockAgent Description: Default alias 2024-07-13 1 Tags: Cost: !Sub sample-${SubName} DependsOn: - BedrockAgent # ------------------------------------------------------------# # Bedrock Agent Role (IAM) # ------------------------------------------------------------# BedrockAgentRole: Type: AWS::IAM::Role Properties: RoleName: !Sub AmazonBedrockExecutionRoleForAgents_sample-${SubName} AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: "sts:AssumeRole" Principal: Service: bedrock.amazonaws.com Condition: StringEquals: "aws:SourceAccount": !Sub ${AWS::AccountId} ArnLike: "aws:SourceArn": !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/*" Policies: - PolicyName: !Sub AmazonBedrockExecutionPolicyForAgents_sample-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "bedrock:InvokeModel" Resource: - !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/anthropic.claude*" - Effect: Allow Action: - "bedrock:Retrieve" - "bedrock:RetrieveAndGenerate" Resource: - !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:knowledge-base/${BedrockKnowledgeBase}" DependsOn: - BedrockKnowledgeBase # ------------------------------------------------------------# # Lambda Execution Role (IAM) # ------------------------------------------------------------# LambdaBedrockInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub sample-LambdaBedrockInvocationRole-${SubName} Description: This role allows Lambda functions to invoke Bedrock. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Policies: - PolicyName: !Sub sample-LambdaBedrockInvocationPolicy-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "bedrock:InvokeModel" - "bedrock:InvokeModelWithResponseStream" Resource: - !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/anthropic.claude*" LambdaBedrockAgentInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub sample-LambdaBedrockAgentInvocationRole-${SubName} Description: This role allows Lambda functions to invoke Bedrock Agent. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Policies: - PolicyName: !Sub sample-LambdaBedrockAgentInvocationPolicy-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "bedrock:InvokeAgent" Resource: - !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent-alias/${BedrockAgent}/*" DependsOn: - BedrockAgent LambdaBedrockAgentAgClaudeInvocationRole: Type: AWS::IAM::Role Properties: RoleName: !Sub sample-LambdaBedrockAgentAgClaudeInvocationRole-${SubName} Description: This role allows Lambda functions to invoke Bedrock Claude FM. AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: / ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess Policies: - PolicyName: !Sub sample-LambdaBedrockAgentAgClaudeInvocationPolicy-${SubName} PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "bedrock:InvokeModel" - "bedrock:InvokeModelWithResponseStream" Resource: - !Sub "arn:aws:bedrock:${AWS::Region}::foundation-model/anthropic.claude*" # ------------------------------------------------------------# # Lambda # ------------------------------------------------------------# LambdaBedrock: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sample-Bedrock-${SubName} Description: !Sub Lambda Function to invoke Bedrock for sample-${SubName} Architectures: - x86_64 Runtime: nodejs20.x Timeout: 180 MemorySize: 128 Role: !GetAtt LambdaBedrockInvocationRole.Arn Handler: index.handler Tags: - Key: Cost Value: !Sub sample-${SubName} Code: ZipFile: !Sub | const { BedrockRuntimeClient, InvokeModelWithResponseStreamCommand } = require("@aws-sdk/client-bedrock-runtime"); const bedrock = new BedrockRuntimeClient({region: "${BedrockRegion}"}); exports.handler = awslambda.streamifyResponse(async (event, responseStream, _context) => { try { const args = JSON.parse(event.body); if (args.prompt == '') { responseStream.write("No prompt provided."); responseStream.end(); } const body = { "max_tokens": 3000, "temperature": 0.5, "top_k": 250, "top_p": 1, "anthropic_version": "bedrock-2023-05-31", "system": "質問文に対して適切な回答をしてください。", "messages": [ { "role": "user", "content": [ { "type": "text", "text": args.prompt } ] } ] }; const input = { modelId: '${ClaudeModelId}', accept: 'application/json', contentType: 'application/json', body: JSON.stringify(body) }; const command = new InvokeModelWithResponseStreamCommand(input); const apiResponse = await bedrock.send(command); let completeMessage = ""; for await (const item of apiResponse.body) { const chunk = JSON.parse(new TextDecoder().decode(item.chunk.bytes)); const chunk_type = chunk.type; if (chunk_type === "content_block_delta") { const text = chunk.delta.text; completeMessage = completeMessage + text; responseStream.write(text); } } responseStream.end(); } catch (error) { console.error(error); responseStream.write('error'); responseStream.end(); } }); DependsOn: - LambdaBedrockInvocationRole LambdaUrlBedrock: Type: AWS::Lambda::Url Properties: AuthType: AWS_IAM Cors: AllowCredentials: false AllowHeaders: - "*" AllowMethods: - POST AllowOrigins: - !Sub https://${SubDomainName}.${DomainName} ExposeHeaders: - "*" MaxAge: 0 InvokeMode: RESPONSE_STREAM TargetFunctionArn: !GetAtt LambdaBedrock.Arn DependsOn: - LambdaBedrock LambdaBedrockAgent: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sample-BedrockAgent-${SubName} Description: !Sub Lambda Function to invoke Bedrock Agent for sample-${SubName} Architectures: - x86_64 Runtime: nodejs20.x Timeout: 600 MemorySize: 128 Role: !GetAtt LambdaBedrockAgentInvocationRole.Arn Handler: index.handler Tags: - Key: Cost Value: !Sub sample-${SubName} Code: ZipFile: !Sub | const { BedrockAgentRuntimeClient, InvokeAgentCommand } = require("@aws-sdk/client-bedrock-agent-runtime"); const bedrockagent = new BedrockAgentRuntimeClient({region: "${AWS::Region}"}); exports.handler = awslambda.streamifyResponse(async (event, responseStream, _context) => { try { // Query Bedrock Agent const args = JSON.parse(event.body); if (args.prompt == '') { responseStream.write("No prompt provided."); responseStream.end(); } const agentInput = { "agentId": "${BedrockAgent}", "agentAliasId": "${BedrockAgentAlias.AgentAliasId}", "sessionId": args.jobid, "enableTrace": false, "endSession": false, "inputText": args.prompt, "sessionState": { "promptSessionAttributes": { "serviceid": args.serviceid, "user": args.username, "datetime": args.datetime } } }; const command = new InvokeAgentCommand(agentInput); const res = await bedrockagent.send(command); const actualStream = res.completion.options.messageStream; const chunks = []; for await (const value of actualStream) { const jsonString = new TextDecoder().decode(value.body); const base64encoded = JSON.parse(jsonString).bytes; const decodedString = Buffer.from(base64encoded,'base64').toString(); try { chunks.push(decodedString); responseStream.write(decodedString); } catch (error) { console.error(error); responseStream.write(null); responseStream.end(); } } responseStream.end(); } catch (error) { console.error(error); responseStream.write('error'); responseStream.end(); } }); DependsOn: - LambdaBedrockAgentInvocationRole - BedrockAgentAlias LambdaUrlBedrockAgent: Type: AWS::Lambda::Url Properties: AuthType: AWS_IAM Cors: AllowCredentials: false AllowHeaders: - "*" AllowMethods: - POST AllowOrigins: - !Sub https://${SubDomainName}.${DomainName} ExposeHeaders: - "*" MaxAge: 0 InvokeMode: RESPONSE_STREAM TargetFunctionArn: !GetAtt LambdaBedrockAgent.Arn DependsOn: - LambdaBedrockAgent LambdaBedrockAgentAgClaude: Type: AWS::Lambda::Function Properties: FunctionName: !Sub sample-BedrockAgentAgClaude-${SubName} Description: !Sub Lambda Function to invoke Bedrock Claude model triggered from Bedrock Agent action group for sample-${SubName} Architectures: - x86_64 Runtime: python3.12 Timeout: 300 MemorySize: 128 Role: !GetAtt LambdaBedrockAgentAgClaudeInvocationRole.Arn Handler: index.lambda_handler Tags: - Key: Cost Value: !Sub sample-${SubName} Code: ZipFile: !Sub | import boto3 import json bedrock = boto3.client('bedrock-runtime', region_name='${BedrockRegion}') def lambda_handler(event, context): print(event) # Invoke Bedrock body = { "max_tokens": 3000, "temperature": 0.5, "top_k": 250, "top_p": 1, "anthropic_version": "bedrock-2023-05-31", "system": "質問文に対して適切な回答をしてください。", "messages": [ { "role": "user", "content": [ { "type": "text", "text": event['inputText'] } ] } ] } res = bedrock.invoke_model( body=json.dumps(body), contentType='application/json', accept='application/json', modelId='${ClaudeModelId}' ) resbody = json.loads(res['body'].read())['content'][0].get('text', '適切な回答が見つかりませんでした。') return { "messageVersion": "1.0", "response": { "actionGroup": event["actionGroup"], "function": event["function"], "functionResponse": { "responseBody": { "TEXT": { "body": resbody } } } }, "sessionAttributes": event["sessionAttributes"], "promptSessionAttributes": event["promptSessionAttributes"] } DependsOn: - LambdaBedrockAgentAgClaudeInvocationRole LambdaBedrockAgentAgClaudePermission: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt LambdaBedrockAgentAgClaude.Arn Action: lambda:InvokeFunction Principal: bedrock.amazonaws.com SourceAccount: !Sub ${AWS::AccountId} SourceArn: !Sub "arn:aws:bedrock:${AWS::Region}:${AWS::AccountId}:agent/${BedrockAgent}" DependsOn: - BedrockAgent # ------------------------------------------------------------# # Output Parameters # ------------------------------------------------------------# Outputs: # Lambda LambdaBedrockArn: Value: !GetAtt LambdaBedrock.Arn Export: Name: !Sub sample-${SubName}-LambdaBedrockArn LambdaBedrockUrl: Value: !GetAtt LambdaUrlBedrock.FunctionUrl Export: Name: !Sub sample-${SubName}-LambdaBedrockUrl LambdaBedrockAgentArn: Value: !GetAtt LambdaBedrockAgent.Arn Export: Name: !Sub sample-${SubName}-LambdaBedrockAgentArn LambdaBedrockAgentUrl: Value: !GetAtt LambdaUrlBedrockAgent.FunctionUrl Export: Name: !Sub sample-${SubName}-LambdaBedrockAgentUrl
Agents for Amazon Bedrock 変更時の作業
上述の AWS CloudFormation テンプレートを流しただけで一旦 Agents for Amazon Bedrock の一連の構成が出来上がりますが、何か構成を変更したときには、バージョン管理の機能があるためにエイリアスの情報も更新しておく必要があります。以下の Description の部分を、何か記述ルールを決めて同時に変えていきましょう。
BedrockAgentAlias: Type: AWS::Bedrock::AgentAlias Properties: AgentAliasName: !Ref BedrockAgentAliasName AgentId: !Ref BedrockAgent Description: Default alias 2024-07-13 1
本記事の範囲はこれで終了です。
まとめ
いかがでしたでしょうか?
あまり説明はなく AWS CloudFormation テンプレートを読んで下さい的な内容になっていますが、そもそもテンプレート化したい人でないとこの記事は読まないと思いますので、ある程度読める方がいらっしゃっているのかな、と思います。その他、AWS Lambda 関数のつくりもなにげに参考になるかと思っております。
本記事が皆様のお役に立てれば幸いです。