Next.js 애플리케이션에서 LlamaIndex.TS 배포하기

Next.js 애플리케이션에서 LlamaIndex.TS 배포하기

LlamaIndex.TS 에이전트를 Next.js 애플리케이션에 통합하는 방법을 다루는 가이드예요. API 라우트, 서버 컴포넌트, 엣지 런타임까지 어떤 식으로 배치할 수 있는지 하나씩 볼게요.

출처: 공식문서

필수 설정

Next.js 설정

withLlamaIndex를 써서 호환성을 보장하세요.

import withLlamaIndex from "llamaindex/next";


/** @type {import('next').NextConfig} */
const nextConfig = {
  // Your existing config
};


export default withLlamaIndex(nextConfig);

API 라우트

App Router (권장)

app/api/chat/route.ts:

import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";


// Initialize agent once (consider using a singleton pattern)
let myAgent: any = null;


async function initializeAgent() {
  if (myAgent) return myAgent;


  try {
    const greetTool = tool({
      name: "greet",
      description: "Greets a user with their name",
      parameters: z.object({
        name: z.string(),
      }),
      execute: ({ name }) => `Hello, ${name}! How can I help you today?`,
    });


    myAgent = agent({
      tools: [greetTool],
      llm: openai({ model: "gpt-4o-mini" }),
    });


    return myAgent;
  } catch (error) {
    console.error("Failed to initialize agent:", error);
    throw error;
  }
}


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


    if (!message || typeof message !== 'string') {
      return NextResponse.json(
        { error: "Message is required and must be a string" },
        { status: 400 }
      );
    }


    const agent = await initializeAgent();
    const result = await agent.run(message);


    return NextResponse.json({ response: result.data });
  } catch (error) {
    console.error("Chat error:", error);
    return NextResponse.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

여기서 주의할 점은 에이전트를 매 요청마다 만들지 않고 싱글턴 패턴으로 한 번만 초기화한다는 거예요. initializeAgent가 이미 만들어진 에이전트가 있으면 그대로 반환하죠.

Pages Router (레거시)

pages/api/chat.ts:

import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { z } from "zod";
import type { NextApiRequest, NextApiResponse } from "next";


let myAgent: any = null;


async function initializeAgent() {
  if (myAgent) return myAgent;


  const timeTool = tool({
    name: "getCurrentTime",
    description: "Gets the current time",
    parameters: z.object({}),
    execute: () => new Date().toISOString(),
  });


  myAgent = agent({
    tools: [timeTool],
    llm: openai({ model: "gpt-4o-mini" }),
  });


  return myAgent;
}


export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== "POST") {
    return res.status(405).json({ error: "Method not allowed" });
  }

  try {
    const { message } = req.body;


    const agent = await initializeAgent();
    const result = await agent.run(message);


    res.json({ response: result.data });
  } catch (error) {
    console.error("Chat error:", error);
    res.status(500).json({ error: "Internal server error" });
  }
}

서버 컴포넌트

서버 컴포넌트에서도 에이전트를 초기화할 수 있어요.

app/chat/page.tsx:

import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { z } from "zod";


async function initializeAgent() {
  const helpTool = tool({
    name: "getHelp",
    description: "Provides help information",
    parameters: z.object({
      topic: z.string().optional(),
    }),
    execute: ({ topic }) => {
      if (topic) {
        return `Here's help for ${topic}: This is a helpful resource about ${topic}.`;
      }
      return "Available topics: general, troubleshooting, api, deployment";
    },
  });


  return agent({
    tools: [helpTool],
    llm: openai({ model: "gpt-4o-mini" }),
  });
}


export default async function ChatPage() {
  const chatAgent = await initializeAgent();


  return (
    <div>
      <h1>Chat Interface</h1>
      <p>Agent initialized and ready to help!</p>
      {/* Your chat UI components */}
    </div>
  );
}

엣지 런타임

엣지 런타임은 Node.js API 접근이 제한돼요. 그래서 관련 모듈을 await import(...)로 동적 로드해서 엣지 번들에 포함시키지 않도록 하는 것이 핵심이에요.

app/api/chat-edge/route.ts:

import { NextRequest, NextResponse } from "next/server";


export const runtime = "edge";


export async function POST(request: NextRequest) {
  const { setEnvs } = await import("@llamaindex/env");
  setEnvs(process.env);


  try {
    const { message } = await request.json();


    const { agent } = await import("@llamaindex/workflow");
    const { tool } = await import("llamaindex");
    const { openai } = await import("@llamaindex/openai");
    const { z } = await import("zod");


    const timeTool = tool({
      name: "time",
      description: "Gets current time",
      parameters: z.object({}),
      execute: () => new Date().toISOString(),
    });


    const myAgent = agent({
      tools: [timeTool],
      llm: openai({ model: "gpt-4o-mini" }),
    });


    const result = await myAgent.run(message);
    return NextResponse.json({ response: result.data });
  } catch (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

첫 줄의 setEnvs(process.env)는 엣지 환경에서 환경 변수를 LlamaIndex에 넘겨주는 초기화 단계예요.

스트리밍 응답

더 나은 사용자 경험을 위해 스트리밍을 구현할 수 있어요.

app/api/chat-stream/route.ts:

import { agent } from "@llamaindex/workflow";
import { tool } from "llamaindex";
import { openai } from "@llamaindex/openai";
import { agentStreamEvent } from "@llamaindex/workflow";
import { NextRequest } from "next/server";
import { z } from "zod";


// Initialize agent once (consider using a singleton pattern)
let myAgent: any = null;


async function initializeAgent() {
  if (myAgent) return myAgent;


  try {
    const greetTool = tool({
      name: "greet",
      description: "Greets a user with their name",
      parameters: z.object({
        name: z.string(),
      }),
      execute: ({ name }) => `Hello, ${name}! How can I help you today?`,
    });


    myAgent = agent({
      tools: [greetTool],
      llm: openai({ model: "gpt-4o-mini" }),
    });


    return myAgent;
  } catch (error) {
    console.error("Failed to initialize agent:", error);
    throw error;
  }
}


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


  const stream = new ReadableStream({
    async start(controller) {
      try {
        const agent = await initializeAgent();
        const events = agent.runStream(message);


        for await (const event of events) {
          if (agentStreamEvent.include(event)) {
            controller.enqueue(new TextEncoder().encode(event.data.delta));
          }
        }


        controller.close();
      } catch (error) {
        controller.error(error);
      }
    },
  });


  return new Response(stream, {
    headers: {
      "Content-Type": "text/plain",
      "Transfer-Encoding": "chunked",
    },
  });
}

클라이언트 통합

API 호출용 React 훅

hooks/useAgentChat.ts:

import { useState } from "react";


export function useAgentChat() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [response, setResponse] = useState<string | null>(null);


  const chat = async (message: string) => {
    setLoading(true);
    setError(null);


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


      if (!res.ok) {
        throw new Error(`HTTP error! status: ${res.status}`);
      }


      const data = await res.json();
      setResponse(data.response);
    } catch (err) {
      setError(err instanceof Error ? err.message : "An error occurred");
    } finally {
      setLoading(false);
    }
  };


  return { chat, loading, error, response };
}

채팅 컴포넌트

components/ChatInterface.tsx:

"use client";


import { useState } from "react";
import { useAgentChat } from "@/hooks/useAgentChat";


export default function ChatInterface() {
  const [message, setMessage] = useState("");
  const { chat, loading, error, response } = useAgentChat();


  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!message.trim()) return;

    await chat(message);
    setMessage("");
  };


  return (
    <div className="max-w-2xl mx-auto p-4">
      <form onSubmit={handleSubmit} className="mb-4">
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          placeholder="Send a message..."
          className="w-full p-2 border rounded"
          disabled={loading}
        />
        <button
          type="submit"
          disabled={loading || !message.trim()}
          className="mt-2 px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
        >
          {loading ? "Thinking..." : "Send"}
        </button>
      </form>

      {error && (
        <div className="p-3 mb-4 bg-red-100 border border-red-400 text-red-700 rounded">
          Error: {error}
        </div>
      )}

      {response && (
        <div className="p-3 bg-gray-100 border rounded">
          <strong>Agent:</strong>
          <p>{response}</p>
        </div>
      )}
    </div>
  );
}

다음 단계

더 알아보기