Node.js 빠른 시작

Node.js 빠른 시작 (Node.js Quickstart)

AI SDK는 AI 기반 애플리케이션을 만드는 데 도움을 주는 강력한 TypeScript 라이브러리예요. 이 빠른 시작에서는 스트리밍 채팅 UI를 가진 간단한 에이전트를 만들어 보면서, 여러분 프로젝트에서 SDK를 쓰는 데 핵심이 되는 개념과 기법을 배워볼 거예요.

출처: 문서

본문

이 튜토리얼에서는 스트리밍 채팅 사용자 인터페이스를 가진 간단한 에이전트를 구축합니다. 그 과정에서 여러분 프로젝트에서 SDK를 사용할 때 기본이 되는 핵심 개념과 기법을 배우게 됩니다.

프롬프트 엔지니어링과 HTTP 스트리밍 개념이 익숙하지 않다면, 선택적으로 이 문서들을 먼저 읽어도 좋습니다.

전제 조건 (Prerequisites)

이 빠른 시작을 따라 하려면 다음이 필요합니다:

Vercel AI Gateway API 키가 아직 없다면, Vercel 웹사이트에서 가입해서 얻을 수 있습니다.

애플리케이션 설정 (Setup Your Application)

mkdir 명령으로 새 디렉토리를 만드는 것부터 시작합니다. 새 디렉토리로 이동한 다음 pnpm init 명령을 실행합니다. 이렇게 하면 새 디렉토리에 package.json이 생성됩니다.

mkdir my-ai-app
cd my-ai-app
pnpm init

의존성 설치 (Install Dependencies)

ai, 즉 AI SDK와 함께 다른 필요한 의존성들을 설치합니다.

AI SDK는 어떤 대규모 언어 모델(LLM)과도 상호작용할 수 있는 통합 인터페이스로 설계되었습니다. 즉 단 한 줄의 코드로 모델과 프로바이더를 바꿀 수 있다는 뜻이에요! 사용 가능한 프로바이더와 커스텀 프로바이더 구축에 대해 providers 섹션에서 자세히 알아볼 수 있습니다.

pnpm add ai zod dotenv
pnpm add -D @types/node tsx typescript

ai 패키지에는 AI SDK가 들어 있습니다. zod는 대규모 언어 모델(LLM)에 전달할 타입 안전 스키마를 정의하는 데 사용합니다. dotenv는 애플리케이션 안에서 환경 변수(내 Vercel AI Gateway 키)에 접근하는 데 사용합니다. 또한 -D 플래그로 설치되는 개발 의존성 세 가지가 TypeScript 코드를 실행하는 데 필요합니다.

Vercel AI Gateway API 키 구성 (Configure Vercel AI Gateway API key)

프로젝트 루트 디렉토리에 .env 파일을 만들고 Vercel AI Gateway API 키를 추가합니다. 이 키는 Vercel AI Gateway 서비스와 애플리케이션을 인증하는 데 사용됩니다.

touch .env

.env 파일을 편집합니다:

AI_GATEWAY_API_KEY=xxxxxxxxx

xxxxxxxxx를 실제 Vercel AI Gateway API 키로 바꾸세요.

AI SDK는 AI_GATEWAY_API_KEY 환경 변수를 사용하여 Vercel AI Gateway와 인증합니다.

애플리케이션 생성 (Create Your Application)

프로젝트 루트에 index.ts 파일을 만들고 다음 코드를 추가합니다:

import { ModelMessage, streamText } from 'ai';
__PROVIDER_IMPORT__;
import 'dotenv/config';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: __MODEL__,
      messages,
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

이 코드에서 어떤 일이 일어나는지 살펴보죠:

  1. 터미널에서 입력을 받기 위한 readline 인터페이스를 설정하여, 명령줄에서 직접 대화형 세션을 가능하게 합니다.
  2. 대화의 기록을 저장할 messages라는 배열을 초기화합니다. 이 기록은 에이전트가 진행 중인 대화에서 맥락을 유지하게 해줍니다.
  3. main 함수에서:
  • 사용자 입력을 요청하고 받아서 userInput에 저장합니다.
  • 사용자 입력을 user 메시지로 messages 배열에 추가합니다.
  • ai 패키지에서 가져온 streamText를 호출합니다. 이 함수는 model 프로바이더와 messages를 포함한 설정 객체를 인자로 받습니다.
  • streamText 함수가 반환한 텍스트 스트림(result.textStream)을 순회하며 스트림 내용을 터미널에 출력합니다.
  • assistant 응답을 messages 배열에 추가합니다.

애플리케이션 실행 (Running Your Application)

이것으로 에이전트를 만드는 데 필요한 모든 것을 구축했습니다! 애플리케이션을 시작하려면 다음 명령을 사용합니다:

pnpm tsx index.ts

터미널에 프롬프트가 나타날 겁니다. 메시지를 입력해보고 AI 에이전트가 실시간으로 응답하는 것을 확인해보세요! AI SDK는 Node.js로 AI 채팅 인터페이스를 빠르고 쉽게 구축하게 해줍니다.

프로바이더 선택 (Choosing a Provider)

AI SDK는 퍼스트파티, OpenAI 호환, 커뮤니티 패키지를 통해 수십 개의 모델 프로바이더를 지원합니다.

이 빠른 시작은 기본 글로벌 프로바이더인 Vercel AI Gateway 프로바이더를 사용합니다. 즉 모델 설정에서 간단한 문자열로 모델에 접근할 수 있다는 뜻입니다:

model: __MODEL__;

게이트웨이 프로바이더를 명시적으로 import해서 사용하는 다른 동등한 두 가지 방법도 있습니다:

// Option 1: Import from 'ai' package (included by default)
import { gateway } from 'ai';
model: gateway('anthropic/claude-sonnet-5');

// Option 2: Install and import from '@ai-sdk/gateway' package
import { gateway } from '@ai-sdk/gateway';
model: gateway('anthropic/claude-sonnet-5');

다른 프로바이더 사용하기 (Using other providers)

다른 프로바이더를 사용하려면 패키지를 설치하고 프로바이더 인스턴스를 만드세요. 예를 들어 OpenAI를 직접 사용하려면:

pnpm add @ai-sdk/openai
import { openai } from '@ai-sdk/openai';

model: openai('gpt-6-astra');

에이전트를 도구로 강화하기 (Enhance Your Agent with Tools)

대규모 언어 모델(LLM)은 놀라운 생성 능력을 갖추고 있지만, 명확한 작업(예: 수학)이나 외부 세계와 상호작용(예: 날씨 가져오기)에서는 어려움을 겪습니다. 여기서 도구(tools)가 등장합니다.

도구는 LLM이 호출할 수 있는 동작입니다. 이 동작의 결과는 다음 응답에서 고려되도록 LLM에 다시 보고될 수 있습니다.

예를 들어 사용자가 현재 날씨를 물어보면, 도구가 없다면 에이전트는 훈련 데이터에 기반한 일반적인 정보만 제공할 수 있습니다. 하지만 날씨 도구가 있다면 최신의 위치별 날씨 정보를 가져와 제공할 수 있습니다.

간단한 날씨 도구를 추가해서 에이전트를 강화해봅시다.

애플리케이션 업데이트 (Update Your Application)

새 날씨 도구를 포함하도록 index.ts 파일을 수정합니다:

import { ModelMessage, streamText, tool } from 'ai';
__PROVIDER_IMPORT__;
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: __MODEL__,
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

이 업데이트된 코드에서:

  1. ai 패키지에서 tool 함수를 import합니다.
  2. weather 도구가 있는 tools 객체를 정의합니다. 이 도구는:
    • 에이전트가 언제 사용해야 하는지 이해하는 데 도움이 되는 description을 가집니다.
    • Zod 스키마를 사용하여 inputSchema를 정의하며, 이 도구를 실행하려면 location 문자열이 필요하다고 지정합니다. 에이전트는 대화의 맥락에서 이 입력을 추출하려고 시도합니다. 추출할 수 없으면 누락된 정보를 사용자에게 물어봅니다.
    • 날씨 데이터를 가져오는 것을 시뮬레이션하는(이 경우 무작위 온도를 반환하는) execute 함수를 정의합니다. 이것은 서버에서 실행되는 비동기 함수이므로 외부 API에서 실제 데이터를 가져올 수 있습니다.

이제 에이전트는 사용자가 묻는 어떤 위치에 대해서도 날씨 정보를 "가져올" 수 있습니다. 에이전트가 날씨 도구를 사용해야 한다고 판단하면 필요한 매개변수와 함께 도구 호출을 생성합니다. 그러면 execute 함수가 자동으로 실행되고, 그 결과가 에이전트의 응답 생성에 사용됩니다.

"뉴욕 날씨 어때?" 같은 질문을 해보고 에이전트가 새 도구를 어떻게 사용하는지 확인해보세요.

빈 "assistant" 응답을 보셨나요? 이것은 텍스트 응답을 생성하는 대신 에이전트가 도구 호출을 생성했기 때문입니다. 결과 객체의 toolCall과 toolResult 키에서 도구 호출과 이후의 도구 결과에 접근할 수 있습니다.

import { ModelMessage, streamText, tool } from 'ai';
__PROVIDER_IMPORT__;
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: __MODEL__,
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    console.log(await result.toolCalls);
    console.log(await result.toolResults);
    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

이제 날씨에 대해 물어보면, 채팅 인터페이스에 도구 호출과 그 결과가 표시되는 것을 볼 수 있습니다.

멀티스텝 도구 호출 활성화 (Enabling Multi-Step Tool Calls)

도구 결과가 채팅 인터페이스에 표시되는데도 에이전트가 이 정보를 사용해 원래 질문에 답하지 않는다는 점을 눈치챘을 겁니다. 이것은 에이전트가 도구 호출을 생성하면 기술적으로 생성이 완료된 것이기 때문입니다.

이를 해결하려면 stopWhen을 사용하여 멀티스텝 도구 호출을 활성화할 수 있습니다. 이 기능은 정의한 중지 조건이 충족될 때까지 도구 결과를 자동으로 에이전트에 다시 보내 추가 생성을 트리거합니다. 이 경우 에이전트가 날씨 도구의 결과를 사용해 질문에 답하기를 원합니다.

애플리케이션 업데이트 (Update Your Application)

stopWhen으로 중지 조건을 구성하도록 index.ts 파일을 수정합니다:

import { ModelMessage, streamText, tool, isStepCount } from 'ai';
__PROVIDER_IMPORT__;
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: __MODEL__,
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
      },
      stopWhen: isStepCount(5),
      onStepEnd: async ({ toolResults }) => {
        if (toolResults.length) {
          console.log(JSON.stringify(toolResults, null, 2));
        }
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

이 업데이트된 코드에서:

  1. stopWhen을 isStepCount 5가 될 때로 설정하여, 에이전트가 어떤 주어진 생성에 대해 최대 5개의 "스텝"을 사용할 수 있게 합니다.
  2. 상호작용 각 스텝의 toolResults를 기록하는 onStepEnd 콜백을 추가하여 에이전트의 도구 사용을 이해하는 데 도움을 줍니다. 이 말은 이전 예제의 toolCall과 toolResult console.log 문도 삭제할 수 있다는 뜻입니다.

이제 어떤 위치의 날씨에 대해 물어보면, 에이전트가 날씨 도구 결과를 사용해 질문에 답하는 것을 볼 수 있습니다.

stopWhen: isStepCount(5)를 설정함으로써 에이전트가 어떤 주어진 생성에 대해 최대 5개의 "스텝"을 사용할 수 있게 합니다. 이것은 더 복잡한 상호작용을 가능하게 하고, 필요하다면 여러 스텝에 걸쳐 정보를 수집·처리할 수 있게 해줍니다. 온도를 섭씨에서 화씨로 변환하는 도구를 하나 더 추가해보면 이를 직접 확인할 수 있습니다.

두 번째 도구 추가 (Adding a second tool)

온도를 섭씨에서 화씨로 변환하는 새 도구를 추가하도록 index.ts 파일을 업데이트합니다:

import { ModelMessage, streamText, tool, isStepCount } from 'ai';
__PROVIDER_IMPORT__;
import 'dotenv/config';
import { z } from 'zod';
import * as readline from 'node:readline/promises';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

const messages: ModelMessage[] = [];

async function main() {
  while (true) {
    const userInput = await terminal.question('You: ');

    messages.push({ role: 'user', content: userInput });

    const result = streamText({
      model: __MODEL__,
      messages,
      tools: {
        weather: tool({
          description: 'Get the weather in a location (fahrenheit)',
          inputSchema: z.object({
            location: z
              .string()
              .describe('The location to get the weather for'),
          }),
          execute: async ({ location }) => {
            const temperature = Math.round(Math.random() * (90 - 32) + 32);
            return {
              location,
              temperature,
            };
          },
        }),
        convertFahrenheitToCelsius: tool({
          description: 'Convert a temperature in fahrenheit to celsius',
          inputSchema: z.object({
            temperature: z
              .number()
              .describe('The temperature in fahrenheit to convert'),
          }),
          execute: async ({ temperature }) => {
            const celsius = Math.round((temperature - 32) * (5 / 9));
            return {
              celsius,
            };
          },
        }),
      },
      stopWhen: isStepCount(5),
      onStepEnd: async ({ toolResults }) => {
        if (toolResults.length) {
          console.log(JSON.stringify(toolResults, null, 2));
        }
      },
    });

    let fullResponse = '';
    process.stdout.write('\nAssistant: ');
    for await (const delta of result.textStream) {
      fullResponse += delta;
      process.stdout.write(delta);
    }
    process.stdout.write('\n\n');

    messages.push({ role: 'assistant', content: fullResponse });
  }
}

main().catch(console.error);

이제 "뉴욕 날씨 섭씨로 알려줘"라고 물어보면, 더 완전한 상호작용을 볼 수 있습니다:

  1. 에이전트가 뉴욕에 대해 날씨 도구를 호출합니다.
  2. 도구 결과가 기록되는 것을 볼 수 있습니다.
  3. 그다음 온도를 화씨에서 섭씨로 변환하기 위해 온도 변환 도구를 호출합니다.
  4. 에이전트는 그 정보를 사용해 뉴욕 날씨에 대한 자연어 응답을 제공합니다.

이 멀티스텝 접근 방식은 에이전트가 정보를 수집하고 더 정확하고 맥락에 맞는 응답을 만드는 데 사용할 수 있게 해서, 에이전트를 훨씬 더 유용하게 만듭니다.

이 예제는 도구가 에이전트의 능력을 어떻게 확장하는지 보여줍니다. 실제 API, 데이터베이스 또는 다른 어떤 외부 시스템과도 통합하는 더 복잡한 도구를 만들어, 에이전트가 실시간으로 실제 데이터에 접근·처리하고 외부 세계와 상호작용하는 동작을 수행하게 할 수 있습니다. 도구는 에이전트의 지식 컷오프(knowledge cutoff)와 최신 정보 사이의 간극을 메우면서, 텍스트 응답 생성 너머로 의미 있는 동작을 취할 수 있게 해줍니다.

다음으로 어디로 갈까요? (Where to Next?)

AI SDK를 사용해 AI 에이전트를 구축했습니다! 여기서부터 탐험할 수 있는 몇 가지 경로가 있습니다:

더 알아보기 (Learn more)

전체 사이트맵