【鍵はLambda】Amazon Bedrock Agentsのカスタムオーケストレーション機能を理解する

# はじめに
当記事はAWS Lambda と Serverless Advent Calendar 2024 3日目の記事です！

https://qiita.com/advent-calendar/2024/lambda

# Amazon Bedrock Agentsのアップデート
[2024年11月28日、Amazon Bedrock Agents がカスタムオーケストレーションをサポートするようになりました](https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-bedrock-agents-custom-orchestration/)というアップデートがありました！

このカスタムオーケストレーションを使用するには、オーケストレーションロジックの概要を示す**Lambda関数**が必要になる、とのこと。

であれば、良いアドベントカレンダーのネタになりそうだ！ということでキャッチアップしてみることにしました！

# カスタムオーケストレーションとは？
公式ドキュメントによると、このカスタムオーケストレーションを実装することで
>**エージェントが複数のステップのタスクを処理し、決定を下し、ワークフローを実行する方法を完全に制御できる**

ようになるそうです。

ざっくり言うと、**エージェントの行動順序や判断プロセスをカスタマイズできるようになった**、ということです。

***


以下のドキュメントに、カスタムオーケストレーションについて詳しく書かれていたのでこちらを読み進めていきます。

https://aws.amazon.com/blogs/machine-learning/getting-started-with-amazon-bedrock-agents-custom-orchestrator/

### 従来のBedrock Agent
従来のBedrock Agentでは、いわゆるReAct(Reasoning&Acting)によってオーケストレーションされていました。

ReActとは、LLMを用いて推論と行動を同時に行うための手法です。

具体的に言うと、以下の3つを繰り返すフローがReActです。
- **Reasoning**：自身の行動と理由を推論すること
→推論は、人間で言う「考える」という行動に似ています。
- **Acting**：その推論に基づいて行動すること
- **Observation**：行動結果を観察し推論と行動を繰り返して、最終回答を出力する

ということで、LLM自身が考え、行動し、最終的に何かを決定して回答を出力してくれる、というようなものになっています。

Bedrock Agentでも同じことが行われています。
LLMが各ステップを推論し、そのステップごとに収集された情報に基づいて次のアクションを決定する、という反復的な意思決定プロセスです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/06e86c2e-1b48-5dba-a103-d0556f89e13e.png)

この手法は、各ステップごとに推論が行われます。
したがって、段階的に行動を考えていくようなワークフローに適しています。
ステップ後の結果を鑑みて計画を変更したり、行動が変化したり、というようなこともあるようです。

その一方で、かなり逐次的な構造であるため、複雑な計画を実行する場合に遅延を引き起こす可能性もあります。
1回1回の行動ごとに思案する時間が入るようなものなので、それは確かに時間がかかりますよね。

ReActの個人的なイメージとしては、Googleマップで調べながら自転車を漕いでいるようなイメージを持ちました。

>**行き先を決定してGoogleマップに登録する(実現したいことを決定する)
→自転車を漕ぐ(Acting)
→曲がり角でマップを確認する(Observation)
→別ルートのほうが近道そうだと考える(Reasoning)
→マップとは異なる道を選んで自転車を漕ぐ(Acting)
→曲がり角でマップを確認する(Observation)
→間違えていそうなので道を戻る(Reasoning)**

曲がり角が来る度にマップを確認(Observation)していては、確かに時間はかかってしまいそうですよね。
ただし、エージェントが継続的に判断を見直しながら進められるという柔軟性が備わっています。

このReActが、これまでのBedrock Agentでは用いられていました。

参考：

[Amazon Bedrock 生成AIアプリ開発入門 [AWS深掘りガイド]](https://www.amazon.co.jp/Amazon-Bedrock-%E7%94%9F%E6%88%90AI%E3%82%A2%E3%83%97%E3%83%AA%E9%96%8B%E7%99%BA%E5%85%A5%E9%96%80-AWS%E6%B7%B1%E6%8E%98%E3%82%8A%E3%82%AC%E3%82%A4%E3%83%89-%E5%BE%A1%E7%94%B0/dp/4815626448)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/958428fa-b4fa-ed83-c9b0-c9ad04388773.png)

https://leagence.co.jp/what_is_gpt_react/

### カスタムオーケストレーション機能
それに対してカスタムオーケストレーションを用いると、ReWOO(Reasoning WithOut Observations)によってオーケストレーションされることになります。

ReWOOでは、ReActとは異なり、**タスク実行後にLLMによるObservationが行われません**。
タスク計画を事前に作成しておき、推論や行動後の出力はチェックせずに一連のワークフローを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/6cf85757-0497-7e48-c2b9-e78818c92c3f.png)

別サイトには、こんな図も載っていました。こちらの方がイメージしやすいかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/06361f3a-9831-f8dd-cda1-0fe4c447b7b4.png)

引用：

https://tech.algomatic.jp/entry/2024/03/19/183857

このReWOOのメリットとしては、大きく2つあります。
- LLMの呼び出しを最小限にできる
- タスク完了までの遅延を削減できる

ReActでは、各ステップごとにObservationが行われていました。
このObservationはLLMが行っているため、1ステップごとにLLMを呼び出す必要があります。
最終的には、Nステップに対して、N+1回のLLM呼び出しが必要になります。

その一方で、ReWOOではObservationを行いません。
したがって、　1ステップごとにLLMを呼び出す必要がありません。

その結果、**ReWOOではLLMの呼び出しを最大2回にまで削減**できます。
ここまで呼び出し回数を減らせるとなると、タスクの実行時間も大きく減らすことが出来ますね。

では、なぜそんなことが可能なのか。なぜObservationが不要なのか。

それは、**タスク開始前に、タスクの実行計画を作成しているから**です。その計画に則って作業を実施するだけなので、LLMを逐一呼び出す必要がなくなっています。

先程の図でも、最初に `CreatePlan` と記載されています。ここで計画を作成しているというわけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/6cf85757-0497-7e48-c2b9-e78818c92c3f.png)

<font color="red">**そのタスク実行計画にあたるのが、Lambda関数です。**

このLambda関数を元に、Bedrock Agentの行動順序や判断プロセスをワークフロー化できます。
その分、トレードオフとして、プロンプトの指定が難しくなるようです。

何はともあれ、**Lambda関数を用いることで、Bedrock Agentの機能や動作を調整し、精度・適応性・効率を向上させる**ことができるようになります。

それが、カスタムオーケストレーション機能です。

# カスタムオーケストレーションLambda関数の設定
Bedrock AgentとLambda関数間のやり取りフローとしては以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3616210/aad88b16-f986-724d-1bde-7830f3755104.png)

このLambda関数は、Bedrockのランタイムプロセスに対してモデルやアクションツールを呼び出すタイミングや方法を指示し、最終的な応答を決定することによって、エージェントが入力に応答する方法を制御します。

と、言葉でだけ説明されてもよくわかりませんよね。

ドキュメントに記載されていたサンプルコードを例に、どんなことを行っているのか確認しましょう。

```sample.js
function react_chain_of_thought_orchestration(event) {
    const incomingState = event.state;

    let payloadData = '';
    let responseEvent = '';
    let responseTrace = '';
    let responseAttribution = '';

    if (incomingState == 'START') {
        // 1. Invoke model in start
        responseEvent = 'INVOKE_MODEL';
        payloadData = JSON.stringify(intermediatePayload(event));
    } else if (incomingState == 'MODEL_INVOKED') {
        const stopReason = modelInvocationStopReason(event);
        if (stopReason == "tool_use") {
            // 2.a. If invoke model predicts tool call, then we send INVOKE_TOOL event
            responseEvent = 'INVOKE_TOOL';
            payloadData = toolUsePayload(event);
        } else if (stopReason == "end_turn") {
            // 2.b. If invoke model predicts an end turn, then we send FINISH event
            responseEvent = 'FINISH';
            payloadData = getEndTurnPayload(event);
        }
    } else if (incomingState == 'TOOL_INVOKED') {
        // 3. After a tool invocation, we again ask LLM to predict what should be the next step
        responseEvent = 'INVOKE_MODEL';
        payloadData = intermediatePayload(event);
    } else {
        // Invalid incoming state
        throw new Error('Invalid state provided!');
    }
                    
    // 4. Create the final payload to send back to BedrockAgent
    const payload = createPayload(payloadData, responseEvent, responseTrace, ...);
    return JSON.stringify(payload);
}              
```

### 1.ステート管理
この関数内では、3つの主要な状態(state)を管理しています。
1. `START`：会話の開始状態
1. `MODEL_INVOKED`：LLMが呼び出された後の状態
→恐らくですが、ここで言う「LLM」はエージェントを指しているのだと思います
→以降、LLMはエージェントのことだとして進めます
1. `TOOL_INVOKED`：ツールが呼び出された後の状態
→ここで言うツールとは、Knowledge baseやアクショングループを指します

### 2.イベント実行
この関数内では、3つのイベントを実行しています。
1. `INVOKE_MODEL`：LLMを呼び出す
1. `INVOKE_TOOL`：特定のツールを使用する
1. `FINISH`：会話を終了する

### 3.処理の流れ
やっていることとしては、**ステートごとにif文で処理を指定**しています。これがワークフローとなるわけですね。

(私はあまりキャッチアップできていないのですが、AWS Heroのみのるんさんは「LangGraph風のフローを書く」と仰っていたので、そちらの知見を深めることでも理解が深まりそうです。)

https://x.com/minorun365/status/1861930697059164466

ここからは具体的に、先程のサンプルコードを上から順番に見ていきます。
#### a)開始時のステート(`START`)
```sample.js
if (incomingState == 'START') {
    // 1. Invoke model in start
    responseEvent = 'INVOKE_MODEL';
    payloadData = JSON.stringify(intermediatePayload(event));
}
```
新しい会話が始まると、最初は`START`ステートです。
この時は、`INVOKE_MODEL`イベントでLLMを呼び出します。
そしてユーザーの入力に基づいて、次のアクションを決定するように依頼します。

#### b)LLM呼び出し後のステート(`MODEL_INVOKED`)
```sample.js
else if (incomingState == 'MODEL_INVOKED') {
    const stopReason = modelInvocationStopReason(event);
    if (stopReason == "tool_use") {
        // 2.a. If invoke model predicts tool call, then we send INVOKE_TOOL event
        responseEvent = 'INVOKE_TOOL';
        payloadData = toolUsePayload(event);
    } else if (stopReason == "end_turn") {
        // 2.b. If invoke model predicts an end turn, then we send FINISH event
        responseEvent = 'FINISH';
        payloadData = getEndTurnPayload(event);
    }
}
```
a)の処理において、`INVOKE_MODEL`イベントが実行されたので、ステートは`MODEL_INVOKED`に変化しました。その時の処理が書かれています。

LLMの応答に基づいて、2つのパスがあります。
1. ツールの活用が必要な場合、`INVOKE_TOOL`イベントを実行します
1. 会話を終了する場合、`FINISH`イベントを実行します

1.だった場合は、ステートが`TOOL_INVOKED`に変化し、それに応じた次のアクションへと移ります。

2.だった場合は、最終回等がユーザーに返却されて会話終了です。

#### c)ツール呼び出し後のステート(`TOOL_INVOKED`)
```sample.js
else if (incomingState == 'TOOL_INVOKED') {
    // 3. After a tool invocation, we again ask LLM to predict what should be the next step
    responseEvent = 'INVOKE_MODEL';
    payloadData = intermediatePayload(event);
}
```
b)の処理において、`INVOKE_TOOL`イベントが実行された場合には、`TOOL_INVOKED`ステートへ変化します。その時に実行する処理が記載されています。

ツールの実行結果を受け取った後、再び`INVOKE_MODEL`イベントを実行してLLMを呼び出します。
この時、ステートは`MODEL_INVOKED`に変化します。

したがって、**ツールの実行結果を受け取ったら、b)に戻る**ということですね。
ただ順次実行しているわけではなく、ワークフローが定義されているのだということがここでわかると思います。

#### 具体例
簡単な例をご紹介します。
ユーザーから「明日の東京の天気を教えて」という入力があったとしましょう。

その際の処理フローとしては、
1. `START`ステート → `INVOKE_MODEL`イベントを実行
(LLMが、天気情報が必要と判断する)
1. `INVOKED_MODEL`ステート → `INVOKE_TOOL`イベントを実行
(天気APIツールを呼び出す)
1. `TOOL_INVOKED`ステート → `INVOKE_MODEL`イベントを実行
(天気情報を受け取り、応答を生成する)
1. `INVOKED_MODEL`ステート → `FINISH`イベントを実行
(ユーザーに回答を返す)

という形になります。
ワークフローに則って処理されているのがわかると思います。

# まとめ
このようにフローを作成することで、**各ステップで次のアクションを動的に判断しながらLLMの挙動を制御**できる。それがカスタムオーケストレーションです。

上記は簡単な例でしたが、使い込むためにはかなり大変なLambda関数を作成しないといけなそうな印象を持ちました。

ただ、それでもこの機能がどんなものなのか・ReWOOとはなにか、を知っておくだけでもだいぶ違うのではないかな、と思います！

# 次にやってみること
以下のBedrockサンプルリポジトリにカスタムオーケストレーションを試せるものが追加されていたので、これを試してみます！

https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/features-examples/14-create-agent-with-custom-orchestration

また、LangGraphもインプットして、どのように違うのかについても纏めてみます！