チュートリアル: LangGraph または Foundry Agent Service を使用して Azure App Service でエージェント Web アプリを構築する (Node.js)

このチュートリアルでは、既存のデータ ドリブン Express.js CRUD アプリケーションにエージェント機能を追加する方法について説明します。 これには、LangGraph と Foundry Agent Service という 2 つの異なる方法を使用します。

Web アプリケーションにショッピング、ホテル予約、データ管理などの便利な機能が既にある場合は、プラグイン (LangGraph の場合) または OpenAPI エンドポイント (Foundry Agent Service の場合) でそれらの機能をラップすることで、Web アプリケーションにエージェント機能を追加するのは比較的簡単です。 このチュートリアルでは、簡単な to-do リスト アプリから始めます。 最終的には、App Service アプリでエージェントを使用してタスクを作成、更新、管理できるようになります。

LangGraph と Foundry Agent Service の両方を使用すると、AI 駆動型機能を使用してエージェント Web アプリケーションを構築できます。 LangGraph は Microsoft セマンティック カーネルに似ていて SDK ですが、セマンティック カーネルは現在 JavaScript をサポートしていません。 次の表に、いくつかの考慮事項とトレードオフを示します。

Consideration LangGraph ファウンドリー エージェント サービス
Performance 高速 (ローカルで実行) 低速 (マネージド、リモート サービス)
Development 完全なコード、最大制御 低いコード、迅速な統合
Testing コードでの手動/単体テスト クイック テスト用の組み込みプレイグラウンド
Scalability アプリで管理 Azure マネージド、自動スケーリング
セキュリティ ガードレール カスタム実装が必要 組み込みのコンテンツの安全性とモデレーション
アイデンティティ カスタム実装が必要 組み込みのエージェント ID と認証
Enterprise カスタム統合が必要 組み込みの Microsoft 365/Teams 展開と Microsoft 365 統合ツール呼び出し。

展開されたアプリでは、App Service認証にはブラウザUIとAPIの両方でMicrosoft Entraのサインインが必要です。 LangGraphはApp Service内で動作し、タスクサービスを直接呼び出します。 Foundry Agent Serviceはリモートで動作し、OpenAPIツールを通じて保護されたタスクAPIを呼び出します。

このチュートリアルでは、以下の内容を学習します。

  • 既存のアプリ機能を LangGraph のプラグインに変換します。
  • プラグインを LangGraph エージェントに追加し、Web アプリで使用します。
  • 既存のアプリ機能を Foundry Agent Service の OpenAPI エンドポイントに変換します。
  • Web アプリで Foundry エージェントを呼び出します。
  • マネージド ID 接続に必要なアクセス許可を割り当てます。
  • Microsoft Entra IDでApp ServiceのウェブアプリとそのAPIを保護します。
  • Foundry OpenAPIツールを設定し、管理されたIDで保護されたApp Service APIを呼び出します。

Prerequisites

Codespaces を使用してサンプルを開く

開始する最も簡単な方法は、GitHub Codespaces を使用することです。GitHub Codespaces を使用すると、必要なすべてのツールがプレインストールされた完全な開発環境が提供されます。

  1. https://github.com/Azure-Samples/app-service-agentic-langgraph-foundry-nodeにある GitHub リポジトリに移動します。

  2. [ コード ] ボタンを選択し、[ Codespaces ] タブを選択し、[ メインでコードスペースを作成] を選択します。

  3. Codespace が初期化されるまでしばらく待ちます。 準備ができたら、ブラウザーに完全に構成された開発環境が表示されます。

  4. アプリケーションをローカルで実行します。

    npm install
    npm run build
    npm start
    
  5. ポート 3000 で実行されているアプリケーションが表示されたら、[ブラウザーで開く] を選択し、いくつかのタスクを追加します。

    エージェントは完全に構成されていないため、まだ機能しません。 後で構成します。

エージェント コードを確認する

どちらの方法でも、アプリケーションの開始時にエージェントが初期化され、POST 要求によってユーザー メッセージに応答する同じ実装パターンが使用されます。

LangGraphTaskAgent、src/agents/LangGraphTaskAgent.ts のコンストラクターで初期化されます。 初期化コードでは、次の処理が行われます。

    constructor(taskService: TaskService) {
        this.taskService = taskService;
        this.memory = new MemorySaver();
        try {
            const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
            const deploymentName = process.env.AZURE_OPENAI_DEPLOYMENT_NAME;

            if (!endpoint || !deploymentName) {
                console.warn('Azure OpenAI configuration missing for LangGraph agent');
                return;
            }
            // Initialize Azure OpenAI client
            const credential = new DefaultAzureCredential();
            const azureADTokenProvider = getBearerTokenProvider(credential, "https://cognitiveservices.azure.com/.default");
            
            this.llm = new AzureChatOpenAI({
                azureOpenAIEndpoint: endpoint,
                azureOpenAIApiDeploymentName: deploymentName,
                azureADTokenProvider: azureADTokenProvider,
                azureOpenAIApiVersion: "2024-10-21"
            });
            // Define tools directly in the array
            const tools = [
                tool(
                    async ({ title, isComplete = false }) => {
                        const task = await this.taskService.addTask(title, isComplete);
                        return `Task created successfully: "${task.title}" (ID: ${task.id})`;
                    },
                    {
                        name: 'createTask',
                        description: 'Create a new task',
                        schema: z.object({
                            title: z.string(),
                            isComplete: z.boolean().optional()
                        }) as any
                    }
                ),
                tool(
                    async () => {
                        const tasks = await this.taskService.getAllTasks();
                        if (tasks.length === 0) {
                            return 'No tasks found.';
                        }
                        return `Found ${tasks.length} tasks:\n` + 
                               tasks.map(t => `- ${t.id}: ${t.title} (${t.isComplete ? 'Complete' : 'Incomplete'})`).join('\n');
                    },
                    {
                        name: 'getTasks',
                        description: 'Get all tasks',
                        schema: z.object({}) as any
                    }
                ),
                tool(
                    async ({ id }) => {
                        const task = await this.taskService.getTaskById(id);
                        if (!task) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${task.id}: "${task.title}" - Status: ${task.isComplete ? 'Complete' : 'Incomplete'}`;
                    },
                    {
                        name: 'getTask',
                        description: 'Get a specific task by ID',
                        schema: z.object({
                            id: z.number()
                        }) as any
                    }
                ),
                tool(
                    async ({ id, title, isComplete }) => {
                        const updated = await this.taskService.updateTask(id, title, isComplete);
                        if (!updated) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${id} updated successfully.`;
                    },
                    {
                        name: 'updateTask',
                        description: 'Update an existing task',
                        schema: z.object({
                            id: z.number(),
                            title: z.string().optional(),
                            isComplete: z.boolean().optional()
                        }) as any
                    }
                ),
                tool(
                    async ({ id }) => {
                        const deleted = await this.taskService.deleteTask(id);
                        if (!deleted) {
                            return `Task with ID ${id} not found.`;
                        }
                        return `Task ${id} deleted successfully.`;
                    },
                    {
                        name: 'deleteTask',
                        description: 'Delete a task',
                        schema: z.object({
                            id: z.number()
                        }) as any
                    }
                )
            ];

            // Create the ReAct agent with memory
            this.agent = createReactAgent({
                llm: this.llm,
                tools,
                checkpointSaver: this.memory,
                stateModifier: `You are an AI assistant that manages tasks using CRUD operations.
                
You have access to tools for creating, reading, updating, and deleting tasks.
Always use the appropriate tool for any task management request.
Be helpful and provide clear responses about the actions you take.

If you need more information to complete a request, ask the user for it.`
            });
        } catch (error) {
            console.error('Error initializing LangGraph agent:', error);
        }
    }

展開されたサンプルはApp Service認証で保護されており、サーバーが選択した1つのLangGraphスレッドを使用します。 ユーザーメッセージを処理する際、エージェントはユーザーのメッセージとサーバー管理スレッドIDで invoke() を呼び出します。

private readonly conversationThreadId = 'authenticated-conversation';

const result = await this.agent.invoke(
    {
        messages: [
            { role: 'user', content: message }
        ]
    },
    {
        configurable: {
            thread_id: this.conversationThreadId
        }
    }
);

サンプル アプリケーションをデプロイする

サンプルリポジトリにはAzure Developer CLI(AZD)テンプレートが含まれており、これによりApp Serviceアプリが作成され、サンプルアプリケーションをデプロイします。 このテンプレートは、アウトバウンドAzure AI通話のためのシステム割り当てマネージデンティティを有効にし、Microsoft Entra IDでApp Service認証を設定します。 基盤となる認証設定の詳細については、「 Foundry Agent ServiceのSecure OpenAPIエンドポイント」を参照してください。

  1. ターミナルで、Azure Developer CLI を使用して Azure にサインインします:

    azd auth login
    

    手順に従って認証プロセスを完了します。

  2. AZD 模板を使って Azure App Service app をデプロイしてください:

    azd up
    
  3. メッセージが表示されたら、次の回答を入力します。

    Question Answer
    新しい環境名を入力します。 一意の名前を入力します。
    使用する Azure サブスクリプションを選択します。 サブスクリプションを選択します。
    使用するリソース グループを選択します。 [Create a new resource group]\(新しいリソース グループの作成\) を選択します。
    リソース グループを作成する場所を選択します。 [スウェーデン中部] を選択します。
    新しいリソース グループの名前を入力します。 Enter」と入力します。
  4. AZD 出力で、アプリの URL を見つけます。 また、後で使うために Foundry OpenAPIの管理されたアイデンティティオーディエンスの 値をコピーしてください。 出力は次のようになります。

     Deploying services (azd deploy)
    
       (✓) Done: Deploying service web
       - Endpoint: <URL>
    
     Foundry OpenAPI managed identity audience:
     api://<generated-client-id>
     
  5. AZD出力からApp Serviceエンドポイントを開きます。

  6. Microsoftがプロンプトを提示したら、デプロイメントテナント内のアカウントでサインインし、タスクリストが読み込まれているか確認してください。

  7. 同じ認証済みブラウザで、自動生成されたOpenAPIスキーマを https://<app-name>.azurewebsites.net/api/schemaで開いてください。

  8. 生成されたOpenAPIスキーマをコピーまたは保存します。 Foundry Agent Service のピボットでそれを使用します。

    Note

    App Service認証は、未認証のブラウザリクエストに対してHTTP 302リダイレクトを返します。 このサンプルにはブラウザUIとAPIの両方が含まれているため、リダイレクトは使いやすいサインイン体験を提供します。 API専用アプリでは一般的にHTTP 401が使われます。

    これで認証済みのApp Serviceアプリが手に入ります。 システム割り当てのマネージドアイデンティティは、Foundryからの発信通話に使用されます。 別個のユーザー割り当て管理IDは、App Service認証のための秘密なしの認証情報を提供します。

Microsoft Foundry リソースを作成して構成する

  1. Foundryポータルでプロジェクトを作成します。

  2. 任意のモデルをデプロイします ( Microsoft Foundry クイック スタート: リソースの作成を参照)。

  3. モデルプレイグラウンドの上部から、モデル名をコピーします。

  4. ホームページでAzure OpenAIエンドポイントをコピーしておきましょう。

必要なアクセス許可を割り当てる

  1. Foundryポータルの上部メニューで 「管理 」を選択してください。

  2. Projectの詳細で、projectの親リソースを選択し、「Open in Azure portal」を選択します。

    Azureポータルからは、リソースにロールベースのアクセスを割り当てることができます。

  3. App Serviceアプリのマネージデンティティと az loginで使用するユーザーの両方に、以下の役割を追加します。

    ターゲット リソース 必要なロール 必要な理由
    鋳造所 Cognitive Services OpenAI ユーザー Microsoft Agent Framework のチャット完了サービス。

    手順については、「Azure portal を使用して Azure ロールを割り当てる」を参照してください。

サンプル アプリケーションで接続変数を構成する

  1. .env を開きます。 Foundry ポータルから前にコピーした値を使用して、次の変数を構成します。

    Variable Description
    AZURE_OPENAI_ENDPOINT Azure OpenAIエンドポイント(Foundryポータルのホームページからコピー)。
    AZURE_OPENAI_DEPLOYMENT_NAME デプロイのモデル名 (新しい Foundry ポータルのモデル プレイグラウンドからコピーされます)。

    Note

    チュートリアルをシンプルにするために、App Service のアプリ設定で上書きするのではなく 、.env でこれらの変数を使用します。

    Note

    チュートリアルをシンプルにするために、App Service のアプリ設定で上書きするのではなく 、.env でこれらの変数を使用します。

    .envの値はアプリのFoundryへのアウトバウンド接続を設定します。 AZURE_AI_FOUNDRY_ACCOUNT_CLIENT_ID は、Foundry から App Service への OpenAPI の個別の受信接続を構成し、AZD 環境に格納されます。

App Service認証はローカルのExpressプロセスではなくAzure上で動作するため、ローカルテストのワークフローは変更されません。

  1. Azure CLI を使用して Azure にサインインします。

    az login
    

    これにより、サンプル コード内の Azure ID クライアント ライブラリは、ログインしているユーザーの認証トークンを受け取ることができます。 前にこのユーザーに必要なロールを追加したことを思い出してください。

  2. アプリケーションをローカルで実行します。

    npm run build
    npm start
    
  3. ポート 3000 で実行されているアプリケーションが表示されたら、[ブラウザーで開く] を選択します。

  4. 両方のピボットを別々に検証します:

    • ランググラフ:LangGraph Agentを選択し、エージェントにタスク作成を依頼します。 LangGraphは進行中のタスクツールを呼び出します。
    • ファウンドリーエージェントサービス:Foundry Agentを選択し、エージェントにタスク作成を依頼します。 リモートのFoundryエージェントは、管理IDを持つ展開された保護された /api/tasks エンドポイントを呼び出します。

    Foundryエージェントが作成するタスクは、ローカルのメモリ内データベースではなく、デプロイされたApp Serviceインスタンスに現れます。 Foundry OpenAPIツールは常にOpenAPIスキーマに埋め込まれたサーバーURLを使用します。

  5. GitHub コードスペースに戻り、アプリの変更をデプロイします。

    azd up
    
  6. デプロイ済みアプリケーションに移動し、サインインして両方のピボットをテストします。 LangGraph Agentでタスクを作成・リストし、その後Foundry Agentでタスクを作成・リストアップします。 両方のピボットがタスクリストを更新しているか確認してください。

よく寄せられる質問

Foundryエージェントに検索拡張生成(RAG)を追加するにはどうすればよいですか?

このガイダンスは、このチュートリアルの Foundry Agent Service パス にも適用されます。 他のタブに示されているLangGraph、Semantic Kernel、Microsoft Agent Frameworkの実装は変更されません。

Foundry IQナレッジベースを作成または選択し、 そのナレッジベースをFoundry Agent Serviceエージェントに接続します。 接続は管理されたMCP知識ツールとしてエージェントに露出されます。

App Serviceコードは、既存のFoundryクライアントおよび agent_referenceを通じて同じエージェントを名前で呼び出し続けています。 ウェブアプリは直接のAzure AI 検索連携や独自のMCPクライアントを必要としません。 UIにソースが表示された場合は、エージェントから返される引用注釈を処理します。

各接続はどの管理IDを使っていますか?

方向 アイデンティティ
App Service が Foundry を呼び出します App Service システム割り当て ID
Foundry OpenAPIツール呼び出し /api/tasks 親 Foundry リソース システムが割り当てた ID

プロジェクトエンドポイントはプロジェクトとエージェントを選択します。 ホストされたOpenAPIツールが使う識別元を特定するわけではありません。

リソースをクリーンアップする

アプリケーションの使用が完了したら、App Service リソースを削除して、追加のコストが発生しないようにすることができます。

azd down --purge

そして、別に作成したFoundryリソースを削除してください。

その他のリソース