AI に任せすぎない設計:Step Functions × AgentCore で人間承認ワークフローを作る

記事タイトルとURLをコピーする

はじめに

こんにちは。高橋 (ポインコ兄) です。

AIエージェントに「全部任せる」のは、まだ怖い。でも、AIを一切使わないのももったいない——そんなジレンマを解決する新しい統合が登場しました。

AWS Step Functions × Amazon Bedrock AgentCore の統合です (2026年6月、プレビュー) 。以降、Amazon Bedrock AgentCore を「AgentCore」と略します。

この組み合わせにより、ワークフローの中に「AIが考えるステップ」を埋め込み、さらに重要な判断の前だけ人間が承認する、という構成がシンプルに実現できます。

本記事では、経費申請の AI 審査 + 人間承認フロー をハンズオンで構築しながら、この統合の使い方と実ユースケースを解説します。

こんな人におすすめ

  • ✅ Step Functions を使ったことがある (基本操作は理解済み)
  • ✅ AIエージェントをワークフローに組み込む方法を知りたい
  • ✅ 「AI全自動」ではなく「AI + 人間の協調」設計に興味がある

Step Functions × AgentCore とは

AgentCore Harness (マネージドハーネス)

Amazon Bedrock AgentCore の マネージドハーネス は、AIエージェントの実行基盤です。モデル・システムプロンプト・ツールを設定として宣言するだけで、オーケストレーションコードなしに動作します。

従来のエージェント実装:

自分でループを書く → モデル呼び出し → ツール実行 → 再度モデル呼び出し → ...

ハーネスを使うと:

モデル・ツール・プロンプトを設定 → InvokeHarness を呼ぶだけ

Step Functions との統合

Step Functions から Harness を呼び出すには、以下の Task 定義を使います:

{
  "Type": "Task",
  "Resource": "arn:aws:states:::bedrockagentcore:invokeHarness",
  "Arguments": {
    "HarnessArn": "arn:aws:bedrock-agentcore:us-east-1:123456789012:harness/my-harness",
    "RuntimeSessionId": "{% $uuid() %}",
    "Messages": [
      {
        "Content": [{ "Text": "{% $states.input.userMessage %}" }],
        "Role": "user"
      }
    ],
    "SystemPrompt": [{ "Text": "You are a helpful assistant." }],
    "Model": {
      "BedrockModelConfig": {
        "ModelId": "global.anthropic.claude-sonnet-4-6"
      }
    }
  },
  "End": true
}

主な制約 (重要)

項目 内容
統合パターン Request Response のみ (.sync・コールバックは非対応)
最大実行時間 15分
レスポンス 最終ターンのテキストのみ (途中のツール使用・推論ブロックは含まれない)
セッション継続 RuntimeSessionId を同一にすることで複数呼び出し間でコンテキスト継続可能
利用可能リージョン us-east-1, us-west-2, eu-central-1, ap-southeast-2 (プレビュー)

この統合がなかった場合の実装

従来の構成

AgentCore 統合がない場合、Step Functions から AI エージェントを使うには Lambda を中継する必要がありました。

[Step Functions]
  → Task: Lambda 呼び出し
      → Lambda 内で agent loop を自前実装
          → Bedrock Converse API 呼び出し
          → ツール使用があれば実行して結果をフィードバック
          → end_turn になるまでループ
      → 最終応答を返す
  → 次の State へ

Lambda のコードはおよそこうなります:

import boto3, json

bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')

def handler(event, context):
    messages = [{"role": "user", "content": [{"text": event["userMessage"]}]}]
    system = [{"text": "あなたは経費審査担当者です..."}]

    for _ in range(10):  # 無限ループ防止
        response = bedrock.converse(
            modelId="us.anthropic.claude-sonnet-4-6",  # クロスリージョン推論プロファイル
            messages=messages,
            system=system,
        )
        stop_reason = response["stopReason"]
        assistant_msg = response["output"]["message"]
        messages.append(assistant_msg)

        if stop_reason == "end_turn":
            for block in assistant_msg["content"]:
                if "text" in block:
                    return {"result": block["text"]}

        elif stop_reason == "tool_use":
            tool_results = []
            for block in assistant_msg["content"]:
                if "toolUse" in block:
                    result = execute_tool(block["toolUse"]) # ツール実行
                    tool_results.append({
                        "toolResult": {
                            "toolUseId": block["toolUse"]["toolUseId"],
                            "content": [{"text": json.dumps(result)}]
                        }
                    })
            messages.append({"role": "user", "content": tool_results})

    raise Exception("Max iterations reached")

def execute_tool(tool_use):
    # ツール実行ロジックを自前実装...
    pass

そして Step Functions 側は Lambda を呼ぶだけ:

{
  "Type": "Task",
  "Resource": "arn:aws:lambda:us-east-1:ACCOUNT_ID:function:expense-review-agent",
  "Parameters": {
    "userMessage.$": "States.Format('申請者: {}\n金額: {}円\n目的: {}', $.applicant, $.amount, $.purpose)"
  },
  "Next": "判定分岐"
}

何が変わったか

項目 従来 (Lambda 中継) AgentCore 統合
agent loop の実装 自前で書く (数十〜数百行) ハーネスが管理 (不要)
ツール実行のフィードバック 自前実装 ハーネスが処理
セッション継続 (多ターン) DynamoDB 等でメッセージ履歴を自前保存 RuntimeSessionId を指定するだけ
モデル・プロンプトの変更 Lambda コードを修正してデプロイ 設定変更のみ (コード変更なし)
可観測性 (ターン詳細) CloudWatch への書き込みを自前実装 自動で CloudWatch に記録
実行時間の上限 Lambda の最大 15 分 ハーネス側の設定に依存 (最大 15 分)

AgentCore 統合の本質は「Lambda で書いていた agent loop のボイラープレートをなくす」ことです。Step Functions 側からは、複雑なエージェントも「ひとつの Task ステート」として扱えるようになります。

Lambda 版のデプロイ手順

実際に動かしてみましょう。コードは hands-on/lambda/index.py、State Machine 定義は hands-on/statemachine-lambda.json です。

1. Lambda をパッケージ化 & デプロイ

cd hands-on/lambda
zip function.zip index.py

# IAM ロール作成
aws iam create-role \
  --role-name expense-review-lambda-role \
  --assume-role-policy-document file://../iam-lambda-trust.json \
  --region us-east-1

aws iam put-role-policy \
  --role-name expense-review-lambda-role \
  --policy-name bedrock-converse \
  --policy-document file://../iam-lambda-policy.json
# ※ iam-lambda-policy.json の Resource には foundation-model/* に加えて
#    inference-profile/* も必要 (クロスリージョン推論プロファイル使用のため) 

# Lambda 関数作成 (ACCOUNT_ID を自分のIDに差し替え) 
aws lambda create-function \
  --function-name expense-review-agent \
  --runtime python3.12 \
  --role arn:aws:iam::ACCOUNT_ID:role/expense-review-lambda-role \
  --handler index.handler \
  --zip-file fileb://function.zip \
  --timeout 120 \
  --region us-east-1

2. State Machine 作成

# lambda/ から一段上に戻る
cd ..

# statemachine-lambda.json の ACCOUNT_ID を差し替えてから実行
aws stepfunctions create-state-machine \
  --name expense-review-lambda \
  --definition file://statemachine-lambda.json \
  --role-arn arn:aws:iam::ACCOUNT_ID:role/StepFunctionsRole \
  --region us-east-1

3. テスト実行

aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT_ID:stateMachine:expense-review-lambda \
  --input '{"applicant":"山田太郎","amount":30000,"purpose":"書籍購入費 (AWS認定試験対策) "}' \
  --region us-east-1


ハンズオン:経費申請 AI 審査ワークフロー

ワークフロー概要

[申請入力]
    ↓
[AgentCore: 経費審査]  ← AI が申請を審査し APPROVE / REJECT / REVIEW を判定
    ↓
[Choice: 判定分岐]
  ├─ APPROVE → [承認処理 (完了) ]
  ├─ REJECT  → [却下通知 (完了) ]
  └─ REVIEW  → [SNS 通知 + 人間承認待ち]
                    ↓ (承認 / 却下コールバック)
               [AgentCore: 最終回答生成]
                    ↓
               [完了]

ポイント:AgentCore は Request Response のみなので、人間承認は SNS + .waitForTaskToken の別 State で実装します。

Step 1:AgentCore Harness の作成

  1. AWS コンソール → Amazon Bedrock → AgentCore → ハーネス
  2. 「ハーネスを作成 (高度なハーネス作成)」をクリック
  3. 以下を設定:
項目 設定値
ハーネス名 expense-review-harness
モデル Claude Sonnet 4 (claude-sonnet-4-6)
システムプロンプト 後述

システムプロンプト (経費審査エージェント) :

あなたは経費申請の審査担当者です。以下のルールに従って申請を審査してください。

審査ルール:
- 5万円以下かつ明確な業務目的がある → APPROVE
- 金額に関わらず不審な内容・ルール違反がある → REJECT
- 5万円超または判断が難しい場合 → REVIEW

レスポンス形式 (必ずこの形式で返答) :
DECISION: [APPROVE|REJECT|REVIEW]
REASON: [判定理由を1〜2文で]

Step 2:SNS トピックの作成 (人間承認用)

aws sns create-topic --name expense-approval-topic --region us-east-1

承認リンクを受け取るメールアドレスをサブスクリプション登録します。

Step 3:State Machine の作成

Workflow Studio で以下の ASL を定義します。

{
  "Comment": "経費申請 AI 審査 + 人間承認ワークフロー",
  "StartAt": "AI経費審査",
  "States": {
    "AI経費審査": {
      "Type": "Task",
      "Resource": "arn:aws:states:::bedrockagentcore:invokeHarness",
      "Arguments": {
        "HarnessArn": "arn:aws:bedrock-agentcore:us-east-1:ACCOUNT_ID:harness/expense-review-harness",
        "RuntimeSessionId": "{% $uuid() %}",
        "Messages": [
          {
            "Content": [{
              "Text": "{% '申請者: ' & $states.input.applicant & '\n金額: ' & $string($states.input.amount) & '円\n目的: ' & $states.input.purpose %}"
            }],
            "Role": "user"
          }
        ]
      },
      "Next": "判定分岐",
      "ResultPath": "$.agentResult"
    },
    "判定分岐": {
      "Type": "Choice",
      "Choices": [
        {
          "StringMatches": "DECISION: APPROVE*",
          "Variable": "$.agentResult.Output.Message.Content[0].Text",
          "Next": "承認処理"
        },
        {
          "StringMatches": "DECISION: REJECT*",
          "Variable": "$.agentResult.Output.Message.Content[0].Text",
          "Next": "却下通知"
        }
      ],
      "Default": "人間承認待ち"
    },
    "承認処理": {
      "Type": "Pass",
      "Result": { "status": "APPROVED" },
      "End": true
    },
    "却下通知": {
      "Type": "Pass",
      "Result": { "status": "REJECTED" },
      "End": true
    },
    "人間承認待ち": {
      "Type": "Task",
      "Resource": "arn:aws:states:::sns:publish.waitForTaskToken",
      "Arguments": {
        "TopicArn": "arn:aws:sns:us-east-1:ACCOUNT_ID:expense-approval-topic",
        "Message": {
          "taskToken": "{% $states.context.Task.Token %}",
          "agentJudgement": "{% $states.input.agentResult.Output.Message.Content[0].Text %}",
          "application": "{% $states.input %}"
        }
      },
      "Next": "最終回答生成",
      "TimeoutSeconds": 86400
    },
    "最終回答生成": {
      "Type": "Task",
      "Resource": "arn:aws:states:::bedrockagentcore:invokeHarness",
      "Arguments": {
        "HarnessArn": "arn:aws:bedrock-agentcore:us-east-1:ACCOUNT_ID:harness/expense-review-harness",
        "RuntimeSessionId": "{% $uuid() %}",
        "Messages": [
          {
            "Content": [{
              "Text": "{% '人間の最終判断: ' & $states.input.humanDecision & '\n申請内容: ' & $string($states.input.application) & '\n申請者への最終通知メッセージを丁寧に作成してください。' %}"
            }],
            "Role": "user"
          }
        ]
      },
      "End": true
    }
  }
}

Step 4:実行してみる

3パターンの入力でテストします:

# APPROVE ケース (3万円・業務目的明確) 
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT_ID:stateMachine:expense-review-agentcore \
  --input '{"applicant":"山田太郎","amount":30000,"purpose":"書籍購入費 (AWS認定試験対策) "}' \
  --region us-east-1

# REJECT ケース (業務外) 
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT_ID:stateMachine:expense-review-agentcore \
  --input '{"applicant":"山田太郎","amount":3000,"purpose":"個人的な飲み会の代金"}' \
  --region us-east-1

# REVIEW ケース (5万円超) 
aws stepfunctions start-execution \
  --state-machine-arn arn:aws:states:us-east-1:ACCOUNT_ID:stateMachine:expense-review-agentcore \
  --input '{"applicant":"山田太郎","amount":80000,"purpose":"クラウド系カンファレンス参加費 (re:Invent 2026) "}' \
  --region us-east-1

実行結果 (実際の出力) :

APPROVE:

{
  "status": "APPROVED",
  "reason": "DECISION: APPROVE\nREASON: 申請金額30,000円は上限の5万円以下であり、AWS認定試験対策の書籍購入費として業務上の目的が明確です。不審な点も見当たらないため、承認といたします。"
}

REJECT:

{
  "status": "REJECTED",
  "reason": "DECISION: REJECT\nREASON: 申請目的が「個人的な飲み会」であり、業務との関連性が認められないため否認です。経費申請は業務上必要な支出に限られます。"
}

REVIEW → 人間が承認した場合:

{
  "status": "HUMAN_DECIDED",
  "humanDecision": { "humanDecision": "APPROVED" },
  "agentJudgement": "DECISION: REVIEW\nREASON: 申請金額80,000円が審査基準の上限 (50,000円) を超過しているため、上長または経理部門による追加承認が必要です。"
}

Lambda 版との出力比較

実際に動かして気づいた面白い点があります。同じシステムプロンプトを渡しているにもかかわらず、AgentCore 版はより豊かな回答を生成しました。

REVIEW ケースの agentJudgement (抜粋) :

📋 **審査サマリー**

| 項目 | 内容 | 判定 |
|------|------|------|
| 申請者 | 山田太郎 | ✅ |
| 金額 | 80,000円 | ⚠️ 上限超過 |
| 業務目的 | カンファレンス参加費 | ✅ 明確 |
| 不審点 | なし | ✅ |

> **次のアクション**: 上長承認 or 経理部門へエスカレーション後、再審査をお願いします。

Lambda 版では「DECISION: REVIEW\nREASON: ...」の1〜2文で終わっていましたが、AgentCore 版はエージェントが自発的に Markdown テーブルの審査サマリーと次のアクションまで生成しました。同じプロンプトでも ハーネスの実行コンテキストがエージェントの推論を豊かにする 可能性を示唆しています。

Step 5:CloudWatch で AI の推論を確認

実行履歴から AgentCore ステップをクリックすると、CloudWatch へのリンクが表示されます。ここでエージェントのターンごとの推論・ツール使用が確認できます。


実ユースケース考察

1. 社内ワークフローの AI 化 (コンプライアンス対応)

購買申請・稟議・契約レビューなど、ルールが明確な審査業務は AI が一次審査を担い、例外ケースのみ人間にエスカレーションする構成が最適です。

効果:審査工数を削減しながら、重要判断の責任は人間が持つ

2. カスタマーサポートの一次対応

問い合わせ受信
  → AgentCore: カテゴリ判定・回答案生成
  → 軽微な質問 → 自動回答
  → クレーム・複雑な案件 → 担当者へエスカレーション

3. データ ETL パイプラインへの AI 組み込み

非構造化データ (PDF・メール・自由記述フォーム) を構造化する処理を Step Functions のパイプラインに組み込めます。既存の ETL ワークフローに InvokeHarness ステップを追加するだけで AI 推論を追加できます。

4. マルチエージェント並列処理 (Map State)

複数ドキュメント → [Map State] → 各ドキュメントで並列 AgentCore 実行 → 結果集約

Step Functions の Map State と組み合わせることで、大量ドキュメントの並列 AI 処理が実現できます。


注意点・ハマりどころ

Request Response のみ

AgentCore の Task では .sync.waitForTaskToken が使えません。人間承認などの非同期処理が必要な場合は、SNS・SQS などの別サービスで実装する必要があります。

15分タイムアウト

TimeoutSeconds を 15分超に設定しても、実際は 15分でタイムアウトします。長時間の推論が必要なタスクには向きません。

レスポンスは最終ターンのみ

途中の推論過程やツール使用結果は Output.Message.Content に含まれません。詳細なデバッグは CloudWatch Transaction Search で確認します。

リージョン制限 (プレビュー期間中)

2026年6月時点では、us-east-1・us-west-2・eu-central-1・ap-southeast-2 の 4 リージョンのみ。アジアパシフィック (東京) リージョン (ap-northeast-1) は対応していません。


まとめ

項目 従来のAIワークフロー Step Functions × AgentCore
エージェントループの実装 自分で書く ハーネスに任せる
人間との協調 別途設計が必要 Choice + waitForTaskToken で自然に実装
可観測性 自前で実装 CloudWatch 連携が組み込み
既存ワークフローとの統合 難しい Task ステートを追加するだけ

Step Functions × AgentCore の組み合わせは、既存のワークフローに AI の判断力を後付けする のに最適なアプローチです。特に「AI が判断し、人間が最終確認する」というヒューマン・イン・ザ・ループ設計は、AI への信頼を段階的に高めていく実践的な方法として、多くの現場で活用できると感じました。


参考リンク


あとがき

本記事では、AWS Step Functions × Amazon Bedrock AgentCore を使って、AI が一次判断し、人間が最終承認するワークフローを構築しました。

AgentCore のハーネスのおかげでエージェントのループ実装が不要になり、Step Functions 側は「AI に渡す → 結果で分岐する」という宣言的な記述だけで済むのが気に入っています。プレビュー段階ですが、これが GA になったとき、どれだけ多くの業務ワークフローが変わるか楽しみです。

兄いわく「ハーネスのシステムプロンプトと Step Functions のワークフロー設計は、Claude Code に相談しながら作ると爆速ですよ〜。このブログを読ませて『こういうフローを作りたい』って言えば大体やってくれます」とのことです。 確かに、ハンズオンをしながら詰まったところをその場で Claude に聞く、というスタイルが一番捗りました。

それではまた、ごきげんよう。

高橋 悠佑 (ポインコ兄) (執筆記事一覧)

健康志向です