Vercel環境でGoogle AIを使用したNext.js 15でのLangchainとLangraphのセットアップ

langchainlangraphnextjsvercelgoogle-aigeminitypescriptai-agents
By sko X opus 4.19/20/202510 min read

このガイドでは、TypeScriptでAIエージェントを構築するために、GoogleのGeminiモデルを使用してNext.js 15プロジェクトでLangchainとLangraphを設定し、Vercelにデプロイする方法を説明します。

メンタルモデル:ビルディングブロック アーキテクチャ

このセットアップを、特殊なミドルウェアを備えた最新のWeb APIの組み立てと考えてください。Next.js 15がサーバーフレームワークを提供し、LangchainがAIオーケストレーション層(LLM用のExpressミドルウェアのような)として機能し、Langraphがステートフルなワークフロー機能(AIワークフロー用のRedux Sagaのような)を追加します。GoogleのGeminiモデルがインテリジェンス層として機能し、Vercelのプラットフォームがサーバーレスデプロイメントを処理し、Google Cloud Functionsのようにしかしより良いDXでAIエンドポイントを自動スケーリングします。

基本セットアップ:プロジェクトの初期化

1. Next.js 15プロジェクトの作成

npx create-next-app@latest my-ai-agent --typescript --tailwind --app --no-src-dir
cd my-ai-agent

TypeScript、Tailwind CSS v4、App Router、ルートディレクトリにファイルを配置した新しいNext.js 15プロジェクトを作成します。

2. コア依存関係のインストール

npm install langchain @langchain/core @langchain/langgraph @langchain/google-genai
npm install @langchain/community ai @ai-sdk/google
npm install daisyui@latest

Langchainコアライブラリ、ワークフロー用のLangraph、Google Gemini統合、コミュニティツール、Vercel AI SDK、UI コンポーネント用のDaisyUI v5をインストールします。

3. TypeScript設定

Next.js 15では、--typescriptフラグでプロジェクトを作成すると、TypeScriptが自動的に設定されます。デフォルトのtsconfig.jsonはすでにLangchain使用に最適化されており、変更は不要です。

4. 環境変数

# .env.local
GOOGLE_API_KEY=your-google-ai-api-key
LANGCHAIN_TRACING_V2=true  # オプション:LangSmithトレースを有効化
LANGCHAIN_API_KEY=your-langsmith-key  # オプション:LangSmithデバッグ用
LANGCHAIN_CALLBACKS_BACKGROUND=false  # 必須:Vercelサーバーレス用

重要:LANGCHAIN_CALLBACKS_BACKGROUNDの説明

true(デフォルト)に設定すると、Langchainはレスポンス送信後にコールバックをバックグラウンドで非同期実行します。これは従来のサーバーでは問題ありませんが、Vercelのようなサーバーレス環境では次の理由で問題が発生します:

  1. サーバーレス関数が即座に終了する、レスポンス返却後に
  2. バックグラウンドコールバックが強制終了される、完了前に、これにより待機リクエストやタイムアウトが発生
  3. Vercelがエラーを報告する場合がある、シンプルなリクエストでも「関数実行が長すぎます」のように

これをfalseに設定すると、すべてのコールバックがレスポンス送信前に同期的に完了することが保証され、サーバーレス関数の終了問題を防ぎます。これはVercel、AWS Lambda、および類似のプラットフォームでは必須です。

5. Tailwind v4とDaisyUI v5用のglobals.css更新

/* app/globals.css */
@import "tailwindcss";
@plugin "daisyui";

新しいCSSベース設定を使用してTailwind CSS v4をインポートし、DaisyUI v5をプラグインとして読み込みます。

6. Google GeminiでのベーシックLangchain APIルート

// app/api/chat/route.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { HumanMessage, AIMessage } from '@langchain/core/messages';
import { NextResponse } from 'next/server';

export const runtime = 'nodejs';
export const maxDuration = 300; // Proプランで5分

export async function POST(req: Request) {
  try {
    const { message } = await req.json();

    const model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0.7,
      streaming: false,
      maxOutputTokens: 2048,
    });

    const response = await model.invoke([
      new HumanMessage(message)
    ]);

    return NextResponse.json({
      content: response.content
    });
  } catch (error) {
    console.error('Chat API error:', error);
    return NextResponse.json(
      { error: 'Failed to process chat' },
      { status: 500 }
    );
  }
}

メッセージを受け取り、Gemini Proで処理し、完全なLangchainサポートのためのNode.jsランタイムでAIレスポンスを返すPOSTエンドポイントを作成します。

高度な例:ストリーミング付きLangraphエージェント

1. 追加依存関係のインストール

npm install @langchain/google-genai @langchain/langgraph zod
npm install @vercel/kv uuid @google/generative-ai
npm install @tanstack/react-query

Google AI埋め込み、Langraphエージェント、Zodでのスキーマ検証、キャッシュ用のVercel KV、データフェッチ用のTanStack Queryの依存関係を追加します。

2. ツール付きエージェントの作成

// lib/agent.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { Calculator } from '@langchain/community/tools/calculator';
import { WebBrowser } from '@langchain/community/tools/webbrowser';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { HumanMessage } from '@langchain/core/messages';
import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';

export function createAgent() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0,
    streaming: true,
    maxOutputTokens: 8192,
  });

  const embeddings = new GoogleGenerativeAIEmbeddings({
    modelName: "embedding-001",
  });

  const tools = [
    new Calculator(),
    new WebBrowser({ model, embeddings }),
  ];

  return createReactAgent({
    llm: model,
    tools,
  });
}

高速なレスポンスのためのGemini Flashを使用し、強化された機能のためのCalculatorとWebBrowserツールを装備したReActエージェントをセットアップします。

3. Langraphでのストリーミング APIルート

// app/api/agent/route.ts
import { createAgent } from '@/lib/agent';
import { HumanMessage } from '@langchain/core/messages';

export const runtime = 'nodejs';
export const maxDuration = 300;

export async function POST(req: Request) {
  const { message } = await req.json();

  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();

  const agent = createAgent();

  // バックグラウンドでストリーミング開始
  (async () => {
    try {
      const eventStream = await agent.stream({
        messages: [new HumanMessage(message)],
      });

      for await (const event of eventStream) {
        if (event.agent?.messages?.[0]?.content) {
          await writer.write(
            encoder.encode(`data: ${JSON.stringify({
              content: event.agent.messages[0].content
            })}\n\n`)
          );
        }
      }
    } catch (error) {
      console.error('Streaming error:', error);
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

生成されたエージェントレスポンスをリアルタイムで送信するServer-Sent Events(SSE)ストリーミングを実装し、体感パフォーマンスを向上させます。

4. ストリーミング付きクライアントコンポーネント

// app/providers.tsx
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState } from 'react';

export default function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000, // 1分
        retry: 1,
      },
    },
  }));

  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

AIアプリケーション用に最適化されたデフォルト設定でTanStack Queryプロバイダーをセットアップします。

// app/layout.tsx
import './globals.css';
import Providers from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

グローバルアクセスのためにTanStack Queryプロバイダーでアプリケーションをラップします。

// components/ChatInterface.tsx
'use client';

import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';

export default function ChatInterface() {
  const [message, setMessage] = useState('');
  const [response, setResponse] = useState('');

  const streamChat = useMutation({
    mutationFn: async (userMessage: string) => {
      setResponse('');

      const res = await fetch('/api/agent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: userMessage }),
      });

      if (!res.ok) throw new Error('Failed to send message');

      const reader = res.body?.getReader();
      const decoder = new TextDecoder();

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));
              setResponse(prev => prev + data.content);
            } catch {}
          }
        }
      }

      return response;
    },
    onError: (error) => {
      console.error('Chat error:', error);
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim()) {
      streamChat.mutate(message);
    }
  };

  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">Google Gemini AIエージェント</h2>

        <form onSubmit={handleSubmit}>
          <div className="form-control">
            <textarea
              className="textarea textarea-bordered"
              placeholder="質問を入力してください..."
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              rows={3}
              disabled={streamChat.isPending}
            />
          </div>

          <div className="card-actions justify-end mt-4">
            <button
              type="submit"
              className="btn btn-primary"
              disabled={streamChat.isPending || !message.trim()}
            >
              {streamChat.isPending ? (
                <>
                  <span className="loading loading-spinner"></span>
                  思考中...
                </>
              ) : '送信'}
            </button>
          </div>
        </form>

        {streamChat.isError && (
          <div className="alert alert-error mt-4">
            <span>メッセージの送信に失敗しました。もう一度お試しください。</span>
          </div>
        )}

        {response && (
          <div className="alert mt-4">
            <span>{response}</span>
          </div>
        )}
      </div>
    </div>
  );
}

TanStack QueryのuseMutationフックを使用してストリーミングAPI呼び出しを管理し、ビルトインのローディングとエラー状態を持つReactコンポーネント。

5. Geminiでのプロンプトチェーン実装

// lib/chains/prompt-chain.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { PromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { NextResponse } from 'next/server';

export function createPromptChain() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
    temperature: 0
  });

  // ステップ1:仕様の抽出
  const extractPrompt = PromptTemplate.fromTemplate(
    "以下から技術仕様を抽出してください:{input}"
  );

  // ステップ2:JSONに変換
  const transformPrompt = PromptTemplate.fromTemplate(
    "'cpu'、'memory'、'storage'キーを持つJSONに変換してください:{specs}"
  );

  // チェーンの作成
  const chain = RunnableSequence.from([
    extractPrompt,
    model,
    new StringOutputParser(),
    { specs: (prev: string) => prev },
    transformPrompt,
    model,
    new StringOutputParser(),
  ]);

  return chain;
}

// APIルートでの使用法
export async function POST(req: Request) {
  const { text } = await req.json();
  const chain = createPromptChain();

  const result = await chain.invoke({ input: text });
  return NextResponse.json({ result });
}

複雑なタスクを順次ステップに分割するプロンプトチェーンパターンを実装します - まずデータを抽出し、次にJSON形式に変換します。

6. Google Geminiでのコンテキストエンジニアリング

// lib/context-engineer.ts
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { SystemMessage, HumanMessage } from '@langchain/core/messages';
import { Document } from '@langchain/core/documents';

interface ContextConfig {
  systemPrompt: string;
  retrievedDocs?: Document[];
  userHistory?: string[];
  environmentContext?: Record<string, any>;
}

export class ContextEngineer {
  private model: ChatGoogleGenerativeAI;

  constructor() {
    this.model = new ChatGoogleGenerativeAI({
      modelName: 'gemini-2.5-pro',
      temperature: 0.3,
      maxOutputTokens: 8192,
    });
  }

  async processWithContext(
    userQuery: string,
    config: ContextConfig
  ) {
    const messages = [];

    // システムコンテキストを追加
    messages.push(new SystemMessage(config.systemPrompt));

    // 取得されたドキュメントをコンテキストとして追加
    if (config.retrievedDocs?.length) {
      const docContext = config.retrievedDocs
        .map(doc => doc.pageContent)
        .join('\n\n');
      messages.push(new SystemMessage(
        `関連するコンテキスト:\n${docContext}`
      ));
    }

    // ユーザー履歴を追加
    if (config.userHistory?.length) {
      messages.push(new SystemMessage(
        `過去の会話:\n${config.userHistory.join('\n')}`
      ));
    }

    // 環境コンテキストを追加
    if (config.environmentContext) {
      messages.push(new SystemMessage(
        `環境:${JSON.stringify(config.environmentContext)}`
      ));
    }

    // 実際のユーザークエリを追加
    messages.push(new HumanMessage(userQuery));

    return await this.model.invoke(messages);
  }
}

システムプロンプト、取得されたドキュメント、会話履歴、環境データを組み合わせてクエリを処理する前に包括的なコンテキスト層を構築します。

7. Vercelデプロイメント設定

// vercel.json
{
  "functions": {
    "app/api/agent/route.ts": {
      "maxDuration": 300
    },
    "app/api/chat/route.ts": {
      "maxDuration": 60
    }
  }
}

AI操作により長い実行時間を許可するようにVercelを設定します。環境変数はNext.jsとVercelによって自動的に処理されます。

8. メインページ統合

// app/page.tsx
import ChatInterface from '@/components/ChatInterface';

export default function Home() {
  return (
    <main className="min-h-screen bg-base-200">
      <div className="container mx-auto p-4">
        <div className="text-center mb-8">
          <h1 className="text-5xl font-bold">AIエージェントプラットフォーム</h1>
          <p className="py-6">Google Gemini & Langraph によって駆動</p>
        </div>

        <div className="flex justify-center">
          <div className="w-full max-w-2xl">
            <ChatInterface />
          </div>
        </div>
      </div>
    </main>
  );
}

レスポンシブレイアウトとDaisyUIスタイリングを使用してチャットインターフェースコンポーネントを中央配置するメインランディングページ。

9. 高度なLangraphワークフロー

// lib/workflows/stateful-workflow.ts
import { StateGraph, END } from '@langchain/langgraph';
import { ChatGoogleGenerativeAI } from '@langchain/google-genai';
import { BaseMessage } from '@langchain/core/messages';

interface WorkflowState {
  messages: BaseMessage[];
  currentStep: string;
  metadata: Record<string, any>;
}

export function createStatefulWorkflow() {
  const model = new ChatGoogleGenerativeAI({
    modelName: 'gemini-2.5-flash',
  });

  const workflow = new StateGraph<WorkflowState>({
    channels: {
      messages: {
        value: (x: BaseMessage[], y: BaseMessage[]) => [...x, ...y],
        default: () => [],
      },
      currentStep: {
        value: (x: string, y: string) => y || x,
        default: () => 'start',
      },
      metadata: {
        value: (x: Record<string, any>, y: Record<string, any>) => ({...x, ...y}),
        default: () => ({}),
      },
    },
  });

  // ノードを定義
  workflow.addNode('analyze', async (state) => {
    const response = await model.invoke(state.messages);
    return {
      messages: [response],
      currentStep: 'process',
    };
  });

  workflow.addNode('process', async (state) => {
    // 分析を処理
    return {
      currentStep: 'complete',
      metadata: { processed: true },
    };
  });

  // エッジを定義
  workflow.addEdge('analyze', 'process');
  workflow.addEdge('process', END);
  workflow.setEntryPoint('analyze');

  return workflow.compile();
}

LangraphのStateGraphを使用してメッセージ履歴を管理し、実行ステップを追跡し、定義されたノードとエッジを通してデータを処理するステートフルワークフローを作成します。

10. フロントエンドとのステートフルワークフロー統合

// app/api/workflow/route.ts
import { createStatefulWorkflow } from '@/lib/workflows/stateful-workflow';
import { HumanMessage } from '@langchain/core/messages';

export const runtime = 'nodejs';
export const maxDuration = 300;

export async function POST(req: Request) {
  const { message, sessionId } = await req.json();

  const encoder = new TextEncoder();
  const stream = new TransformStream();
  const writer = stream.writable.getWriter();

  const workflow = createStatefulWorkflow();

  (async () => {
    try {
      // ワークフローイベントをストリーミング
      const events = await workflow.stream({
        messages: [new HumanMessage(message)],
        currentStep: 'start',
        metadata: { sessionId },
      });

      for await (const event of events) {
        // ステップ更新をフロントエンドに送信
        await writer.write(
          encoder.encode(`data: ${JSON.stringify({
            type: 'step',
            step: event.currentStep || 'processing',
            content: event.messages?.[event.messages.length - 1]?.content,
            metadata: event.metadata
          })}\n\n`)
        );
      }

      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ type: 'complete' })}\n\n`)
      );
    } catch (error) {
      await writer.write(
        encoder.encode(`data: ${JSON.stringify({ type: 'error', error: String(error) })}\n\n`)
      );
    } finally {
      await writer.close();
    }
  })();

  return new Response(stream.readable, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

ステップ遷移と中間結果を含むワークフロー実行イベントをフロントエンドにストリーミングするAPIルート。

// hooks/useWorkflowStream.ts
import { useState } from 'react';
import { useMutation } from '@tanstack/react-query';

interface WorkflowStep {
  name: string;
  status: 'pending' | 'active' | 'complete';
  output?: string;
}

interface WorkflowData {
  message: string;
  sessionId: string;
}

export function useWorkflowStream() {
  const [steps, setSteps] = useState<WorkflowStep[]>([
    { name: 'analyze', status: 'pending' },
    { name: 'process', status: 'pending' },
    { name: 'complete', status: 'pending' }
  ]);

  const mutation = useMutation({
    mutationFn: async (data: WorkflowData) => {
      // ステップをリセット
      setSteps(steps.map(s => ({ ...s, status: 'pending', output: undefined })));

      const res = await fetch('/api/workflow', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (!res.ok) throw new Error('Workflow failed');

      const reader = res.body?.getReader();
      const decoder = new TextDecoder();

      while (reader) {
        const { done, value } = await reader.read();
        if (done) break;

        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');

        for (const line of lines) {
          if (line.startsWith('data: ')) {
            try {
              const data = JSON.parse(line.slice(6));

              if (data.type === 'step') {
                setSteps(prev => prev.map(step => {
                  if (step.name === data.step) {
                    return { ...step, status: 'active', output: data.content };
                  } else if (prev.findIndex(s => s.name === data.step) >
                            prev.findIndex(s => s.name === step.name)) {
                    return { ...step, status: 'complete' };
                  }
                  return step;
                }));
              } else if (data.type === 'complete') {
                setSteps(prev => prev.map(s => ({ ...s, status: 'complete' })));
              } else if (data.type === 'error') {
                throw new Error(data.error);
              }
            } catch (error) {
              if (error instanceof SyntaxError) continue;
              throw error;
            }
          }
        }
      }

      return { success: true };
    },
    onError: (error) => {
      console.error('Workflow error:', error);
      setSteps(prev => prev.map(s => ({ ...s, status: 'pending' })));
    },
  });

  return {
    steps,
    runWorkflow: mutation.mutate,
    isLoading: mutation.isPending,
    isError: mutation.isError,
    error: mutation.error,
    reset: mutation.reset,
  };
}

自動状態更新とエラーハンドリングでワークフローストリーミングを管理するTanStack Queryを使用したカスタムフック。

// components/WorkflowInterface.tsx
'use client';

import { useState } from 'react';
import { useWorkflowStream } from '@/hooks/useWorkflowStream';

export default function WorkflowInterface() {
  const [message, setMessage] = useState('');
  const { steps, runWorkflow, isLoading, isError, error, reset } = useWorkflowStream();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (message.trim()) {
      runWorkflow({
        message,
        sessionId: crypto.randomUUID()
      });
    }
  };

  return (
    <div className="card w-full bg-base-100 shadow-xl">
      <div className="card-body">
        <h2 className="card-title">ワークフロー実行</h2>

        <form onSubmit={handleSubmit}>
          <div className="form-control">
            <input
              type="text"
              className="input input-bordered"
              placeholder="リクエストを入力してください..."
              value={message}
              onChange={(e) => setMessage(e.target.value)}
              disabled={isLoading}
            />
          </div>

          <div className="flex gap-2 mt-4">
            <button
              type="submit"
              className="btn btn-primary"
              disabled={isLoading || !message.trim()}
            >
              {isLoading ? (
                <>
                  <span className="loading loading-spinner"></span>
                  ワークフロー実行中...
                </>
              ) : 'ワークフローを実行'}
            </button>

            {isError && (
              <button type="button" className="btn btn-outline" onClick={() => reset()}>
                リセット
              </button>
            )}
          </div>
        </form>

        {isError && (
          <div className="alert alert-error mt-4">
            <span>エラー:{error?.message || 'ワークフローが失敗しました'}</span>
          </div>
        )}

        {/* ワークフローステップの可視化 */}
        <div className="mt-6">
          <ul className="steps steps-vertical">
            {steps.map((step, idx) => (
              <li
                key={idx}
                className={`step ${
                  step.status === 'complete' ? 'step-success' :
                  step.status === 'active' ? 'step-primary' : ''
                }`}
              >
                <div className="text-left ml-4">
                  <div className="font-semibold capitalize">{step.name}</div>
                  {step.output && (
                    <div className="text-sm opacity-70 mt-1">
                      {step.output.substring(0, 100)}
                      {step.output.length > 100 && '...'}
                    </div>
                  )}
                </div>
              </li>
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
}

クリーンな状態管理とエラーハンドリングのためにカスタムワークフローフックとTanStack Queryを使用するフロントエンドコンポーネント。

// app/workflow/page.tsx
import WorkflowInterface from '@/components/WorkflowInterface';

export default function WorkflowPage() {
  return (
    <main className="min-h-screen bg-base-200">
      <div className="container mx-auto p-4">
        <div className="text-center mb-8">
          <h1 className="text-4xl font-bold">AIワークフロー実行器</h1>
          <p className="py-4">AIエージェントが複雑なタスクをステップバイステップで作業する様子を見守ってください</p>
        </div>

        <div className="flex justify-center">
          <div className="w-full max-w-3xl">
            <WorkflowInterface />
          </div>
        </div>
      </div>
    </main>
  );
}

マルチステップAIエージェント操作の監視のためのクリーンなレイアウトを提供するワークフローインターフェース専用ページ。

結論

このセットアップは、GoogleのGeminiモデルを使用してNext.js 15とVercelでLangchainとLangraphを使ったAIエージェント構築のためのプロダクション対応の基盤を提供します。この設定は、完全なストリーミングサポートと高度なエージェント機能を維持しながら、Geminiの優れたパフォーマンスとコスト効率性を活用します。主要な側面には、適切なGoogle AI統合、Tailwind v4の簡素化された設定とDaisyUI v5、そして Agentic Design Patterns ドキュメントのパターンに従った包括的なプロンプトチェーン実装が含まれます。