Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Strands Agents + AG-UIで Human-in-the-Loop付きのAIエージェントを実現する

こんにちは。データ分析エンジニアの木介です。

AIエージェントを導入し、業務の自動化を進めていくと、「重要な操作には人による承認を挟みたい」というケースが出てきます。

今回は、Strands Agentsのinterrupt機能とAG-UIプロトコルを組み合わせて、Human-in-the-Loop(HITL)付きの調査レポート作成エージェントを実現してみます。

github.com

strandsagents.com

1. はじめに

AIエージェントは自律的にタスクを遂行する能力を持つ一方で、外部サービスへのアクセスやデータの永続化といった重要な操作を無条件に実行させるのはリスクがあります。

今回は以下の2つの技術を組み合わせることで、Human-in-the-LoopなUXをユーザーに提供するための一例について紹介します。

技術 役割
Strands Agents の interrupt Agentの処理を一時停止し、ユーザー承認を要求する
AG-UI プロトコル Agentとフロントエンド間の通信を標準化し、状態同期・承認フローを実現する

2. AG-UI × HITL

2.1. AG-UIとは

AG-UI(Agent-User Interaction Protocol)は、AIエージェントとフロントエンドアプリケーション間の通信を標準化する オープンで軽量なイベントベースのプロトコル です。

AG-UI概要

従来のREST/GraphQL APIは単純なリクエスト/レスポンス型を前提としていますが、AIエージェントは長時間実行・ストリーミング・非決定論的という特性を持ちます。
AG-UIはこの特性に対応し、AgentとUI間の通信を行うために作成されました。

主な機能は以下のとおりです。

機能 説明
ストリーミングチャット トークン単位でのリアルタイム配信
共有状態 AgentとUI間で型指定された状態をリアルタイム同期
ツールコール Agentのツール実行をイベントとしてフロントエンドに通知
Human-in-the-Loop フロー中途での一時停止・承認・編集が可能

AG-UIには16種類の標準イベントが定義されており、Agentはこれらのイベントを発行するだけでフロントエンドとの連携が実現されます。

AG-UIプロトコルの全体像を以下に示します。

AG-UI

AG-UIを採用することで、従来のREST/GraphQL APIでは実現しにくかった以下の利点が得られます。

利点 説明
フレームワーク非依存 Strands Agents、LangGraph、CrewAI、Google ADKなど、多様なAgentフレームワークと接続可能
トランスポート非依存 SSE、WebSocket、webhooksなど、アーキテクチャに最適な配信方法を選択できる
リアルタイム中間結果 Agentの処理状況をストリーミングでUIに配信し、長時間実行でもユーザーに進捗を提示できる
共有状態の双方向同期 StateSnapshot / StateDeltaにより、AgentとUI間の状態を型安全にリアルタイム同期できる
HITLのネイティブサポート interruptイベントを通じて、承認フローをプロトコルレベルでサポートしている
UIとAgentの疎結合 Agentはイベントを発行するだけでよく、UI実装の詳細を知る必要がない

2.2. Human-in-the-Loopとは

Human-in-the-Loop(HITL)は、AIの自動処理の途中で人間の判断を介入させるワークフローパターンです。
以下の記事でも解説をしているので、参考にしていただければと思います。

acro-engineer.hatenablog.com

具体的には、エージェントが外部検索やファイル保存などの操作を実行する直前に処理を一時停止し、ユーザーに承認・否認を求めます。
承認されれば処理を続行し、否認されれば処理をキャンセルまたは修正します。

これにより、エージェントの自律性を活かしつつ、意図しない操作や後戻りできない処理のリスクを軽減できます。

2.3. 今回実現するアプリイメージ

今回作成するのは、調査テーマを入力すると、エージェントが 計画立案 → 検索 → レポート作成 まで自動で進める調査レポートアプリです。

以下の2箇所でHITLの承認を挟みます。

  1. 外部検索の実行前: 調査計画を確認し、検索してよいかをユーザーに確認
  2. レポート保存の実行前: 作成したレポートの内容を確認し、保存してよいかをユーザーに確認

AG-UIを採用する利点として は、Agent開発者がフロントエンドのUI実装を意識しなくてよい点です。
AgentがAG-UIプロトコルに準拠したイベントを発行するだけで、対応するフロントエンド(今回はCopilotKit)が状態表示や承認モーダルを自動的に構築します。

AgentのロジックとUIが疎結合に保たれるため、フロントエンドの差し替えも容易です。

構成

以下にアプリの構成を示します。

アプリの構成

レイヤー 技術スタック
フロントエンド Next.js + CopilotKit
プロトコル AG-UI (HTTP / SSE)
バックエンド Strands Agents + ag-ui-strands
外部サービス Tavily MCP(Web検索)

フロントエンドはCopilotKitの useCoAgent hookでAgent状態を共有し、AG-UIの HttpAgent でバックエンドに接続します。バックエンドはStrands Agentsを ag-ui-strands パッケージでAG-UI互換エンドポイントとして公開しています。

3. 実装ポイント

3.1. Strands Agents interruptによるHITL制御

Strands Agentsでは HookProvider を使い、ツール実行の直前に割り込み(interrupt)を発生させることができます。

strandsagents.com

以下の図に、interruptによるHITL制御の流れを示します。

interruptによるHITL制御

処理の流れは以下の4ステップです。

  1. Agentがツールを呼び出す — 検索や保存などのツール実行がトリガーされる
  2. HookProviderのBeforeToolCallが発火する — ツール実行前にコールバックが呼ばれる
  3. event.interrupt() で処理を一時停止する — Agentの実行が中断され、ユーザーの判断を待つ
  4. ユーザーが承認・否認を返す — 承認ならツールが実行され、否認なら cancel_tool でスキップされる

以下の ResearchApprovalHook により、run_search(外部検索)と save_report(レポート保存)の実行前にユーザー承認を要求します。

class ResearchApprovalHook(HookProvider):
    """検索実行とレポート保存の直前にユーザー承認を要求するHook。

    Strands Agentsのinterrupt機能を使い、特定のツール実行前に
    処理を一時停止してユーザーの判断を仰ぐ。
    """

    def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
        """BeforeToolCallEventにコールバックを登録する。"""
        registry.add_callback(BeforeToolCallEvent, self._before_tool_call)

    def _before_tool_call(self, event: BeforeToolCallEvent) -> None:
        """ツール実行前に呼ばれ、対象ツールであれば承認を要求する。"""
        tool_name = event.tool_use.get("name")
        tool_input = event.tool_use.get("input", {})

        if tool_name == "run_search":
            search_plan = tool_input.get("search_plan", {})
            # interruptで処理を一時停止し、ユーザーの承認を待つ
            approval = event.interrupt(
                "research-search-approval",
                reason={
                    "title": "検索実行の承認",
                    "message": "以下の調査計画で外部検索を開始します。",
                    "summary_text": f"テーマ: {search_plan.get('topic', '')}",
                },
            )
            # 承認されなければツール実行をキャンセル
            if not self._is_approved(approval):
                event.cancel_tool = "ユーザーが検索実行を否認しました。"
            return

        if tool_name == "save_report":
            title = tool_input.get("title", "")
            # レポート保存前にも同様にinterruptで承認を要求
            approval = event.interrupt(
                "research-save-report-approval",
                reason={
                    "title": "レポート保存の承認",
                    "message": "生成したレポートをローカルに保存します。",
                    "summary_text": f"タイトル: {title}",
                },
            )
            if not self._is_approved(approval):
                event.cancel_tool = "ユーザーがレポート保存を否認しました。"

ポイントは以下のとおりです。

  • event.interrupt() を呼ぶとAgentの処理が一時停止し、ユーザーの応答を待つ
  • reason に承認モーダルに表示する情報(タイトル・メッセージ・要約)を渡す
  • 否認された場合は event.cancel_tool にメッセージを設定するとツール実行がスキップされる
  • Hook方式のため、ツール実装自体にHITLロジックを混入させずに済む

3.2. AG-UIによるAgent-UIの連携

AG-UIプロトコルを介して、バックエンドのStrands AgentsとフロントエンドのCopilotKitが連携します。

strandsagents.com

以下の図に、AG-UIイベントを介したバックエンド-フロントエンド間の連携の全体像を示します。

AG-UIによるAgent-UIの連携

図に示している通り、以下のイベントを実行することでAG-UIによるAgent-UI間の連携を実現します。

  1. StateSnapshot / StateDelta — Agent状態をフロントエンドにリアルタイム同期する
  2. TextMessage (stream) — チャットメッセージをトークン単位でストリーミング配信する
  3. ToolCall Start/End/Result — ツールの実行状況をフロントエンドに通知する
  4. interrupt (pending) — 承認が必要な操作が発生したことをフロントエンドに送信する
  5. interrupt_response — ユーザーの承認・否認をバックエンドに返送する

バックエンド: AG-UIエンドポイントの公開

ag-ui-strands パッケージを使い、Strands AgentsをAG-UI互換のFastAPIエンドポイントとして公開します。

from ag_ui_strands import (
    StrandsAgent, StrandsAgentConfig, ToolBehavior, create_strands_app,
)
from strands import Agent

# Strands Agentの定義(ツールとHookを登録)
strands_agent = Agent(
    model=model,
    system_prompt="あなたは調査レポート作成エージェントです。",
    tools=[
        create_search_plan, run_search, save_report,
        present_search_plan, present_report_markdown,
    ],
    hooks=[ResearchApprovalHook()],  # HITL用のHookを登録
)

# AG-UI互換のAgentとしてラップ
agui_agent = StrandsAgent(
    agent=strands_agent,
    name="research_report_agent",
    config=StrandsAgentConfig(
        tool_behaviors={
            # ツール結果をフロントエンドの共有状態に変換するルールを定義
            "present_search_plan": ToolBehavior(
                state_from_result=lambda ctx: {
                    "view_mode": "planning",
                    "search_plan_card": ctx.result_data,
                }
            ),
            "save_report": ToolBehavior(
                state_from_result=lambda ctx: {
                    "view_mode": "completed",
                    "save_result": ctx.result_data,
                }
            ),
        }
    ),
)

# FastAPIアプリとしてAG-UIエンドポイントを公開
app = create_strands_app(agui_agent, "/invocations")

ToolBehaviorstate_from_result で、各ツールの結果をフロントエンドの共有状態に変換するルールを定義しています。
これにより、ツール実行結果が自動的にAG-UIの StateSnapshot イベントとしてフロントエンドに配信されます。

フロントエンド: AG-UIクライアント接続

Next.jsのAPIルートで、AG-UIの HttpAgent を使ってバックエンドのAgentに接続します。

import { HttpAgent } from "@ag-ui/client";
import {
  CopilotRuntime,
  ExperimentalEmptyAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";

// AG-UIのHttpAgentでバックエンドAgentに接続
const runtime = new CopilotRuntime({
  agents: {
    research_report_agent: new HttpAgent({
      url: "http://127.0.0.1:8100/invocations",
    }),
  },
});

export const POST = async (req: NextRequest) => {
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter: new ExperimentalEmptyAdapter(),
    endpoint: "/api/copilotkit",
  });
  return handleRequest(req);
};

フロントエンド: interrupt応答のハンドリング

ページコンポーネントでは useCoAgent hookでAgent状態を購読し、pending_interrupt が設定されたら承認モーダルを表示します。

// useCoAgentでバックエンドAgentの状態を共有
const { state, setState } = useCoAgent<AgentState>({
  name: "research_report_agent",
  initialState: {
    pending_interrupt: null,
    search_plan_card: null,
    report_card: null,
  },
});

// ユーザーの承認・否認をAgentに送信
const submitInterruptResponse = async (approved: boolean) => {
  const pending = state.pending_interrupt;
  // AG-UIプロトコルに従い、interrupt_responseメッセージを送信
  await appendMessage(
    new TextMessage({
      role: Role.User,
      content: `interrupt_response: ${pending.id} ${approved ? "approve" : "deny"}`,
    }),
    { followUp: true },
  );
};

バックエンド側の StrandsAgentinterrupt_response: <id> approve 形式のメッセージをパースし、Strands Agentsのinterrupt応答として処理を再開します。

この仕組みにより、Agent開発者はinterruptの reason にフロントエンドに伝えたい情報を載せるだけで、承認モーダルの表示からユーザー操作の送受信までがプロトコルレベルで自動的に処理されます。

4. 動かしてみる

実際にアプリを動かした画面を紹介します。

1 初期画面

調査テーマを入力するチャットUIが表示されます。

初期画面

2 調査計画の作成

テーマを入力すると、エージェントが調査計画を自動生成します。
検索クエリ、スコープ、最大取得件数などが提案されています。

調査計画

3 検索実行の承認

調査計画の確認後、外部検索の実行前にHITLの承認モーダルが表示されます。
テーマ、検索観点数、調査スコープ、最大取得件数が要約されています。

検索実行の承認

4 検索結果とレポート作成

承認すると外部検索が実行され、検索結果をもとにレポート草案が自動生成されます。

検索結果とレポート作成

5 レポート保存の承認

レポートの保存前にも承認モーダルが表示されます。
タイトル、保存先、ファイル名、内容要約が確認できます。

レポート保存の承認

承認するとレポートがローカルにMarkdownファイルとして保存されます。

5. まとめ

Strands Agentsのinterrupt機能とAG-UIプロトコルを組み合わせることで、Human-in-the-Loop付きの調査レポートエージェントを構築しました。

Strands AgentsのHookProviderにより、ツール実装に手を加えることなくHITLの承認フローを追加でき、AG-UIプロトコルによりAgent側はUI実装を意識せずにフロントエンドとの状態同期・承認フローを実現できます。

エージェントの自律性と人間による制御のバランスを、宣言的に設計できる構成として、実務でも活用できそうです。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com