개발가이드

  • 개발가이드

LLM 멀티챗 가이드

여러 턴에 걸쳐 대화를 이어가는 기본 패턴. 클라이언트가 messages[] 를 소유·전송하고 응답의 compacted_messages 로 통째 교체하는 stateless 방식, sliding_window 로 토큰 비용 관리, 재시도 전략.

이런 상황에 사용하세요

이전 대화의 맥락을 이어가야 할 때입니다. "내 이름 기억해?" 같은 질문에 모델이 "네, 지훈님" 이라고 답하려면 지난 턴의 대화를 함께 전달해야 합니다. "페르소나·UX·업무 자동화" 는 아직 없고, 단순히 여러 턴을 이어가는 기본만 필요한 단계를 다룹니다.

시나리오
왜 멀티챗이 맞는가
코드 리뷰 대화 (설명 → 수정 요청 → 재설명)
이전 코드·피드백을 기억해야 함
번역 후 톤 조정 ("좀 더 공식적으로")
원문·초벌 번역이 맥락
계획 세우기 (여행·운동·학습)
이전 선택을 고려해 다음 제안
문서 Q&A (한 번 컨텍스트 보내고 여러 질문)
매번 긴 컨텍스트 재전송 대신 히스토리 유지

페르소나·말투·제품 정책 같은 고정된 배경지식이 필요하면 챗봇 구축/에이전트 구축 가이드로 가세요.

핵심 개념 — stateless + client-owned history

서버는 대화를 기억하지 않습니다. 각 호출은 독립적이며, 연속성은 전적으로 클라이언트가 만듭니다.

역할
동작
클라이언트
messages[] 배열을 보유하고, 매 턴마다 전체 히스토리를 전송
서버
요청 바디의 messages[] 만 보고 답변 생성. 어떤 세션·ID·캐시도 사용하지 않음
응답의 compacted_messages
"다음 턴 그대로 써도 되는 완성된 히스토리" — 요청 messages + 이번 assistant 응답이 append 된 상태

⚠️ 흔한 실수 금지: 응답 받은 뒤 history.push(result.data.message) 로 직접 이어붙이지 마세요. 서버 compact(트림)가 적용됐을 때 클라이언트·서버 히스토리가 어긋납니다. 항상 history = result.data.compacted_messages 로 통째 교체하세요.

전제 조건

항목
설명
API 키 · 잔액
/dev_guide/start · 토큰 과금 (1포인트 = 1원)
모델
/rest/llm/models 응답의 id 중 선택해 model 필드에 전달

Step 1 — 두 턴 이어가기 (가장 짧은 예시)

클라이언트가 보유하는 history 배열에 사용자 메시지를 push → 전체를 전송 → 응답의 compacted_messages 로 교체. 이 세 단계가 전부입니다.


import axios from 'axios';

let history = []; // messages[] — 클라이언트가 소유

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

  const { data: result } = await axios.post('https://apick.app/rest/llm/chat', {
    model: 'meta-llama/Meta-Llama-3.1-8B-Instruct',
    messages: history,
  }, {
    headers: { 'CL_AUTH_KEY': process.env.API_KEY },
  });

  history = result.data.compacted_messages; // ⚠️ 교체 (push 금지)
  return result.data.message.content;
}

console.log(await ask('내 이름은 지훈이야.'));
console.log(await ask('내 이름 기억해?'));
// → "네, 지훈님이라고 말씀하셨죠."
console.log(await ask('내 이름으로 삼행시 지어줘.'));
            

Step 2 — 긴 대화의 비용 관리: sliding_window

대화가 길어질수록 매 호출마다 보내는 messages[] 가 커지고 input 토큰이 누적 증가합니다 (10턴 → 20턴 → 50턴 ...). 비용이 기하급수적으로 늘 수 있습니다.

compact.strategy: "sliding_window" 옵션을 주면 서버가 최근 N개의 user/assistant 페어만 남기고 앞쪽을 버린 뒤 모델에 전달합니다. window_pairs 는 유지할 페어 수 (기본 10, 최소 1).

JavaScript (axios) — sliding_window 적용

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

  const { data: result } = await axios.post('https://apick.app/rest/llm/chat', {
    model: 'meta-llama/Meta-Llama-3.1-8B-Instruct',
    messages: history,
    compact: { strategy: 'sliding_window', window_pairs: 10 }, // 최근 10페어만 유지
  }, {
    headers: { 'CL_AUTH_KEY': process.env.API_KEY },
  });

  history = result.data.compacted_messages; // 이미 트림된 상태
  console.log('누적 페어:', history.length / 2, '/ prompt 토큰:', result.data.usage.prompt_tokens);
  return result.data.message.content;
}
            

window_pairs 얼마로? 짧은 컨텍스트(최근 2~3턴만 중요)는 5, 일반 대화는 10, 긴 상담·튜터링은 20. 커질수록 맥락↑ 비용↑. 대부분 10 이면 충분합니다.

Step 3 — 에러 대응 + 재시도

네트워크 또는 업스트림 일시 오류(502) 는 지수 백오프로 1회만 재시도. 잔액(402)·입력(400) 오류는 재시도 금지.


async function askRobust(userText, retryLeft = 1) {
  history.push({ role: 'user', content: userText });
  try {
    const { data: result } = await axios.post('https://apick.app/rest/llm/chat', {
      model: 'meta-llama/Meta-Llama-3.1-8B-Instruct',
      messages: history,
      compact: { strategy: 'sliding_window', window_pairs: 10 },
    }, {
      headers: { 'CL_AUTH_KEY': process.env.API_KEY },
      timeout: 60000,
    });
    history = result.data.compacted_messages;
    return { text: result.data.message.content, cost: result.api.cost };
  } catch (e) {
    const status = e.response?.status;
    // 재시도 금지
    if (status === 400) throw new Error('입력이 올바르지 않습니다');
    if (status === 402) throw new Error('포인트가 부족합니다 — 충전 필요');
    // 502/503/네트워크: 1회 재시도 (지수 백오프)
    if (retryLeft > 0 && (!status || status >= 500)) {
      history.pop(); // 방금 push 한 user 제거 후 재시도
      await new Promise(r => setTimeout(r, 500 + Math.random() * 500));
      return askRobust(userText, retryLeft - 1);
    }
    throw e;
  }
}
            

전체 통합 예시 — axios

멀티챗의 가장 간결한 완성형 코드입니다. 이 30줄이면 "여러 턴 이어가며 대화하는" 기능은 완결됩니다.


import axios from 'axios';

const API_KEY = process.env.API_KEY;
const MODEL = 'meta-llama/Meta-Llama-3.1-8B-Instruct';

async function multiChat() {
  let history = [];
  const userTurns = [
    '내 이름은 지훈이야.',
    '내 이름 기억해?',
    '그럼 나랑 역할극 하자. 넌 조선시대 학자고 난 네 제자야.',
    '스승님, 오늘 날씨가 참 좋습니다.',
  ];
  let totalCost = 0;

  for (const user of userTurns) {
    history.push({ role: 'user', content: user });
    const { data: result } = await axios.post('https://apick.app/rest/llm/chat', {
      model: MODEL,
      messages: history,
      compact: { strategy: 'sliding_window', window_pairs: 10 },
    }, { headers: { 'CL_AUTH_KEY': API_KEY } });
    history = result.data.compacted_messages;
    totalCost += result.api.cost;
    console.log('🙋', user);
    console.log('🤖', result.data.message.content, '\n');
  }
  console.log('누적 비용:', totalCost, '포인트');
}

multiChat();
            

자주 묻는 질문

질문
histroy 를 브라우저 새로고침해도 유지하려면?
localStorage/sessionStorage/indexedDB 에 직렬화해 저장. 서버는 도와주지 않습니다 — stateless.
대화가 100턴 넘어가면?
sliding_window 로는 부족할 수 있음. 오래된 부분을 한 번 요약 호출 후 system 메시지로 치환. (llm_chat 참고)
여러 사용자를 동시에 서빙하려면?
각 사용자별로 독립된 history 배열을 유지. 서버는 구분하지 않으므로 사용자 단위는 앱이 책임.
같은 모델·같은 입력이면 항상 같은 답?
아니오. temperature 가 기본 > 0 이라 변주 있음. 재현성 높이려면 temperature: 0.1.
현재 페이지 북마크