ENECHANGE Developer Blog

ENECHANGE開発者ブログ

定期実行AIエージェントをLambdaからAgentCore Runtimeに移行した理由と方法

VPoTの岩本 (iwamot) です。

本ブログの新着記事をレビューしてくれるAIエージェント「ブログほめ太郎」について、実行環境をAWS LambdaからAmazon Bedrock AgentCore Runtimeに移行しました。

今回の記事では、なぜ移行したのか、どのように移行したのかをお伝えします。

なぜ移行したのか

ブログほめ太郎をAgentCore Runtimeに移行した理由は大きく2つあります。

  • 汎用的なLambdaより、AIエージェントに特化したAgentCore Runtimeのほうが実行環境として好ましい(餅は餅屋)と以前から考えていた
  • Amazon EventBridgeスケジューラからAgentCore Runtimeが呼び出せるようになった(図1)

図1:EventBridgeスケジューラのターゲット選択画面

つまり、AWSの機能改善によって、理想的な運用が可能になったからというわけです。

どのように移行したのか

移行は以下の手順で進めました。

  1. agentcore create で初期コードを生成
  2. コードを調整
  3. terraform apply でデプロイ

1. agentcore create で初期コードを生成

まず、Bedrock AgentCore Starter Toolkitを使って、初期コードを生成しました。

agentcore create --project-name bloghometaro \
  --template production \
  --iac Terraform \
  --non-interactive

IaCをCDKでなくTerraformにしたのは、ENECHANGEでは全社的にTerraformを活用しているためです。

2. コードを調整

続いて、コードを調整しました。調整のポイントは以下の通りです。

  • ブログほめ太郎では不要なファイルやTerraformリソースを削除(おもにMCPやAgentCore Memory関連)
  • 足りないTerraformリソースを追加(EventBridgeスケジューラなど)
  • Dockerビルドコマンドを調整 (terraform/bedrock_agentcore.tf)
  • src/main.py を差し替え

terraform/bedrock_agentcore.tf で定義されているDockerビルドコマンドについては、AgentCore RuntimeがARM64のみサポートしているため、以下のように調整しました。

docker build -t ...
↓
docker build --platform linux/arm64 -t ...

また、src/main.py は以下の内容で差し替えました。Lambda向けのハンドラ関数を定義しなくて済むので楽です。

# src/main.py
import gc
import os
from typing import List

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from pydantic import BaseModel, Field
from strands import Agent
from strands.models import BedrockModel
from strands.session.s3_session_manager import S3SessionManager

# /var/task のままだと、slackツールのimport時にディレクトリ作成エラーになる
# https://github.com/strands-agents/tools/issues/199
os.chdir("/tmp")

from strands_tools import http_request, rss, slack

app = BedrockAgentCoreApp()

# 環境変数から設定を取得
RSS_URL = os.environ.get("BHT_RSS_URL", "https://tech.enechange.co.jp/feed")
SLACK_CHANNEL_ID = os.environ.get("BHT_SLACK_CHANNEL_ID", "")
SESSION_BUCKET = os.environ.get("BHT_SESSION_BUCKET", "")

NEW_ARTICLE_FINDER_PROMPT = f"""
あなたは新着記事のURLリストを取得する専門エージェントです。

1. RSSフィード {RSS_URL} を参照する

2. 最後にレビューした記事のURL(システムメッセージで提供)より後に公開された記事を取得する
  - 最後にレビューした記事のURLが提供されていない場合は、最新記事1件を取得する

3. 取得した記事のURLのみをシンプルなリストとして返す(古い順)
  - 例:["https://example.com/article1", "https://example.com/article2"]
  - 新着記事がない場合は空のリスト [] を返す
  - 説明は不要、URLリストのみを出力すること
"""

# convert_to_markdown=True を指定しないとHTMLで全文参照され、
# トークン量が増えることがある
ARTICLE_REVIEWER_PROMPT = """
あなたは技術ブログ記事をレビューする専門エージェントです。

1. http_requestツールで当該記事の全文を参照する
  - convert_to_markdown=Trueを必ず指定すること

2. 日本語400字程度で、技術的に価値のあるレビューメッセージを生成する
  - 「<!channel> 新着記事レビューです!」から始める
  - 記事タイトルとURLを紹介する
  - 投稿に感謝する。筆者名が分かれば名前で呼びかける(会社名やチーム名は不要)
  - 絵文字や *アスタリスク1つ* を使って適度に強調する
    - 太字にしたい場合は、 *アスタリスク1つだけ* で囲むこと
    - いかなる場合も、アスタリスクの前後には必ずスペースを入れること
    - アスタリスク2つ(`**`)は絶対に使わないこと
  - レビューメッセージ以外の出力は不要

技術的レビューの観点例:
- 読者が得られる学び
- 技術的な新規性や独自性
- さらに深掘りできる内容の提案
"""

SLACK_PUBLISHER_PROMPT = f"""
あなたはレビューをSlackに投稿する専門エージェントです。

1. slackツールで投稿する
  - Slackチャンネル:{SLACK_CHANNEL_ID}
"""



class NewArticleFinderOutput(BaseModel):
    urls: List[str] = Field(
        default_factory=list, description="新着記事のURLリスト(古い順)"
    )


@app.entrypoint
def invoke(payload):
    """ブログほめ太郎のメインエントリポイント"""
    # セッションマネージャーを初期化(状態を永続化)
    session_manager = S3SessionManager(
        session_id="blog-home-taro",
        bucket=SESSION_BUCKET,
        prefix="sessions",
    )

    # 状態管理用のエージェント(セッションマネージャー付き)
    state_agent = Agent(
        model=BedrockModel(cache_tools="default"),
        session_manager=session_manager,
    )

    # 最後にレビューした記事URLを取得
    last_reviewed_url = state_agent.state.get("last_reviewed_url")
    print(f"Last reviewed URL: {last_reviewed_url}")

    # 新着記事を検索
    last_url_info = (
        f"最後にレビューした記事のURL: {last_reviewed_url}"
        if last_reviewed_url
        else "最後にレビューした記事のURLはありません"
    )
    new_article_finder = Agent(
        model=BedrockModel(cache_tools="default"),
        tools=[rss],
        system_prompt=NEW_ARTICLE_FINDER_PROMPT,
    )
    new_article_finder(f"{last_url_info}\n\n新着記事のURLリストを取得してください")
    finder_output = new_article_finder.structured_output(NewArticleFinderOutput)
    del new_article_finder
    gc.collect()
    urls = finder_output.urls
    print(f"New articles: {urls}")

    if not urls:
        return {"result": "新着記事はありません"}

    for url in urls:
        article_reviewer = Agent(
            model=BedrockModel(cache_tools="default"),
            tools=[http_request],
            system_prompt=ARTICLE_REVIEWER_PROMPT,
        )
        reviewer_output = article_reviewer(f"この記事をレビューしてください:{url}")
        del article_reviewer
        gc.collect()

        slack_publisher = Agent(
            model=BedrockModel(cache_tools="default"),
            tools=[slack],
            system_prompt=SLACK_PUBLISHER_PROMPT,
        )
        slack_publisher(f"以下のレビューを投稿してください:\n\n{reviewer_output}")
        del slack_publisher
        gc.collect()

    # 最後にレビューした記事URLを保存
    state_agent.state.set("last_reviewed_url", urls[-1])
    session_manager.sync_agent(state_agent)

    return {"result": f"レビュー完了: {len(urls)}件の記事を処理しました"}


if __name__ == "__main__":
    app.run()

3. terraform apply でデプロイ

あとは、Terraformでデプロイするだけです。Dockerイメージのビルドも実行してくれるので、簡単に済みました。

cd terraform
terraform init
terraform apply

まとめ

以上、ブログほめ太郎の実行環境をAgentCore Runtimeに移行した理由と方法についてご紹介しました。

今回はLambdaからの移行でしたが、新規でAIエージェントを開発する場合も、同じ手順(agentcore create → コード調整 → terraform apply)で進められます。

AgentCore Runtimeは、まだEventBridgeルールのターゲットとしては指定できませんが、EventBridgeスケジューラで定期実行するだけなら好ましい実行環境だと考えます。ぜひお試しください。