개발가이드

  • 개발가이드

LLM 챗봇 구축 가이드

system 페르소나·프로덕션 에러 처리·스트리밍(선택)·비용 관리를 포함한 제품 수준 챗봇 구축. axios 기반 Chatbot 클래스 완성 코드로 복사해 바로 사용.

이 가이드가 다루는 것

멀티챗이 "여러 턴을 기술적으로 이어가는 법" 이었다면, 이 가이드는 사용자 앞에 내놓을 수 있는 제품 수준의 챗봇을 만듭니다.

추가되는 요소
내용
페르소나
system 으로 역할·말투·태도 고정
UX
스트리밍(선택), 응답 대기 인디케이터, 에러 토스트
프로덕션 방어
에러 매핑, 재시도, 비용 캡, 비용 모니터링
통합 코드
복사-붙여넣기 가능한 Chatbot 클래스 (axios 기반)

아직 messages[] 멀티턴 개념이 낯설다면 멀티챗 가이드를 먼저 보세요. "도메인 지식·정책 준수·출력 형식 강제" 같은 고도화된 페르소나가 필요하면 에이전트 구축 가이드로 이어집니다.

전제 조건

항목
설명
API 키 · 잔액
/dev_guide/start — 최소 1포인트
모델
/rest/llm/models 응답의 id 중 선택해 model 필드에 전달
엔드포인트 레퍼런스
/dev_guide/llm_chat — 전체 스펙

Step 1 — system으로 페르소나 고정하기

system에 역할, 답변 언어, 말투와 길이를 직접 지정합니다. 서버는 별도 프리셋을 저장하지 않으므로 호출자가 매 요청에 같은 지침을 전달해야 합니다.

system 작성 기준
요소
예시
역할
당신은 친절한 한국어 요리 도우미입니다.
답변 형식
재료, 조리 순서, 예상 시간을 구분해 답하세요.
제약
확실하지 않은 내용은 추측하지 말고 확인이 필요하다고 안내하세요.
axios — system 적용

import axios from 'axios';

const { data: result } = await axios.post('https://apick.app/rest/llm/chat', {
  model: 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
  system: '당신은 친절한 한국어 요리 도우미입니다. 재료와 조리 순서를 간결하게 구분해 답하세요.',
  content: '오늘 저녁 뭐 먹을지 추천해줘. 집에 계란·밥·김치만 있어.',
}, {
  headers: { 'CL_AUTH_KEY': process.env.API_KEY },
});

console.log(result.data.message.content);
            

시스템 지침은 서버에 저장되지 않습니다. 여러 턴 대화에서도 동일한 system을 매번 보내거나 messages[0]에 system 역할로 포함하세요. 고도화된 커스터마이징은 에이전트 구축 가이드에서 다룹니다.

Step 2 — UX 개선 (선택): 스트리밍

응답이 길 때 사용자는 "답이 다 나올 때까지의 정적 시간" 을 불편해합니다. stream: true 로 타이핑처럼 글자를 흘릴 수 있습니다.

주의: 스트리밍은 구현 복잡도를 높입니다. MVP 단계에서는 건너뛰고, 사용자 피드백이 "응답이 느려서 기다리기 싫다" 일 때 도입하세요. axios 에서 SSE 스트림은 responseType: 'stream' + Node 의 경우, 브라우저에서는 fetch 가 더 간편합니다 (axios 는 브라우저 SSE 를 공식 지원하지 않음). 아래는 Node 서버 프록시 기준 예시.

Node.js (axios 스트림)

import axios from 'axios';

async function askStream(history, userText, onChunk) {
  history.push({ role: 'user', content: userText });

  const res = await axios.post('https://apick.app/rest/llm/chat', {
    model: 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
    system: '당신은 친절한 한국어 상담 챗봇입니다. 핵심 답변과 다음 행동을 짧게 안내하세요.',
    messages: history,
    stream: true,
    compact: { strategy: 'sliding_window', window_pairs: 10 },
  }, {
    headers: { 'CL_AUTH_KEY': process.env.API_KEY },
    responseType: 'stream',
  });

  let buf = '', event = '', full = '', done = null;
  for await (const chunk of res.data) {
    buf += chunk.toString('utf8');
    const frames = buf.split('\n\n'); buf = frames.pop();
    for (const frame of frames) {
      for (const line of frame.split('\n')) {
        if (line.startsWith('event: ')) event = line.slice(7).trim();
        else if (line.startsWith('data: ')) {
          const payload = JSON.parse(line.slice(6));
          if (event === 'data') {
            const delta = payload.delta ?? payload.content ?? '';
            full += delta; onChunk(delta);
          } else if (event === 'done') done = payload;
        }
      }
    }
  }
  return { text: full, next: done?.compacted_messages || history, cost: done?.cost };
}
            

브라우저 프론트엔드는 자체 백엔드 프록시를 두고 거기서 위 Node 코드를 호출한 뒤, 프록시 응답을 SSE 그대로 클라이언트에 흘리는 것이 가장 단순합니다. (API 키 노출도 함께 방지)

Step 3 — 프로덕션 강화

항목
대응
에러 매핑
400 (입력 오류) · 402 (잔액 부족) — 재시도 금지, UX 로 안내
502 (업스트림 일시 오류) — 지수 백오프 1회 재시도
비용 캡
미지정 시 서버가 각 모델의 max_context 까지 자동 허용. 비용·UX 관점에서 더 작은 값(예: 2048, 4096)으로 명시해 답변 길이를 제한하는 것도 권장
비용 모니터링
매 호출 result.api.cost 누적 로깅. 사용자별 일일 한도 초과 시 안내. result.api.pl_id 는 PaymentLog 감사 추적용
응답 속도
챗봇은 대기 민감 — speed: 'fast' 권장. 깊은 추론이 필요한 특수 질문에서만 'slow'
긴 대화
compact.strategy: 'sliding_window' 필수. window_pairs 는 10 권장
API 키 보안
브라우저에서 직접 호출 시 키 노출. 자체 백엔드 프록시 경유 로 키를 서버에 숨기세요

전체 코드 — Chatbot 클래스 (axios · 복사해서 바로 사용)

Step 1~3 를 통합한 클래스입니다. 히스토리 보유 · sliding_window · system 페르소나 · 에러 매핑 · 재시도 · 비용 집계 포함.

Chatbot.js (Node.js / 브라우저 공용)

import axios from 'axios';

const API_BASE = 'https://apick.app/rest/llm/chat';

export class Chatbot {
  constructor({
    apiKey,
    model       = 'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
    system      = '당신은 친절한 한국어 상담 챗봇입니다. 핵심 답변과 다음 행동을 짧게 안내하세요.',
    windowPairs = 10,
    maxTokens   = 512,
    speed       = 'fast',
  } = {}) {
    this.cfg = { apiKey, model, system, windowPairs, maxTokens, speed };
    this.history = [];
    this.totalCost = 0;
  }

  async _post(body, retryLeft = 1) {
    try {
      const { data: result } = await axios.post(API_BASE, body, {
        headers: { 'CL_AUTH_KEY': this.cfg.apiKey },
        timeout: 60000,
      });
      return result;
    } catch (e) {
      const status = e.response?.status;
      if (status === 400) throw new Error('요청 오류 (입력 확인 필요)');
      if (status === 402) throw new Error('포인트 부족 — 충전 필요');
      if (retryLeft > 0 && (!status || status >= 500)) {
        await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
        return this._post(body, retryLeft - 1);
      }
      throw e;
    }
  }

  async ask(userText) {
    this.history.push({ role: 'user', content: userText });
    const result = await this._post({
      model: this.cfg.model,
      system: this.cfg.system,
      messages: this.history,
      compact: { strategy: 'sliding_window', window_pairs: this.cfg.windowPairs },
      max_tokens:  this.cfg.maxTokens,
      speed: this.cfg.speed,
    });
    this.history = result.data.compacted_messages;
    this.totalCost += result.api.cost || 0;
    return result.data.message.content;
  }

  reset() { this.history = []; }
  getCost() { return this.totalCost; }
}

// 사용 예시
const bot = new Chatbot({ apiKey: process.env.API_KEY });
console.log(await bot.ask('내 이름은 지훈이야.'));
console.log(await bot.ask('내 이름 기억해?'));
console.log('누적 비용:', bot.getCost(), '포인트');
            

이 클래스의 system을 바꿔 튜터·번역기·고객센터 등 다른 역할의 챗봇을 만들 수 있습니다. "우리 회사 정책에 따라 답하는" 도메인 특화 챗봇이 필요하면 정책 원문·행동 규칙·출력 형식을 함께 주입하세요. 이것이 에이전트 구축 가이드의 주제입니다.

현재 페이지 북마크