본문으로 건너뛰기

AI 챗봇 만들기

AI 챗봇 만들기서버리스 함수와 LLM을 연결해 AI 챗봇을 만드는 가이드

중급AI16초읽기 약 4분업데이트 2026-04-14

서버리스 함수와 LLM을 연결해 AI 챗봇을 만드는 가이드

이 튜토리얼에서 배울 내용
서버리스 함수에서 LLM API 호출
대화 컨텍스트 관리
DB에 대화 기록 저장
챗봇 UI 구현

RAG AI 챗봇 만들기

ConnectBase 의 AI 스트리밍 + Knowledge Base (RAG) 를 결합해 PDF / DOCX / text 문서를 컨텍스트로 답변하는 챗봇을 만듭니다. 외부 LLM API 키를 코드에서 직접 다룰 필요 없이 콘솔에서 AI 프로바이더(Gemini / OpenAI / Claude)를 등록하고 cb.ai.chatStream({ knowledgeBaseId }) 한 줄로 호출합니다.

완성된 기능

  • AI 스트리밍 응답 (token-by-token 표시)
  • Knowledge Base 검색 결과를 컨텍스트로 자동 주입 (RAG)
  • PDF / DOCX / text 파일을 그대로 업로드 → 청킹 + 인덱싱
  • 한국어 nori 형태소 분석기 자동 적용

아키텍처

프론트엔드 → cb.ai.chatStream({ knowledgeBaseId })
                ↓
       AI Server (BM25 검색 → 컨텍스트 조립 → LLM 스트리밍)
                ↑
            Knowledge Base (문서 → 청크)

사전 준비

  1. AI 프로바이더 등록: 콘솔 > AI 탭에서 Gemini / OpenAI / Claude 중 하나의 API 키 등록 (BYOK)
  2. Knowledge Base 생성: 콘솔 > 지식베이스 > 새 KB 생성 → KB ID 복사 (chunk_size: 500, chunk_overlap: 50 권장)

1. 프로젝트 설정

bash
npm create vite@latest rag-chatbot -- --template react-ts
cd rag-chatbot
npm install connectbase-client

.env:

VITE_CONNECT_BASE_PUBLIC_KEY=cb_pk_여기에_퍼블릭키_입력
VITE_KB_ID=콘솔에서_복사한_KB_UUID

2. SDK 설정

src/lib/connectbase.ts:

typescript
import { ConnectBase } from 'connectbase-client'

export const cb = new ConnectBase({
  publicKey: import.meta.env.VITE_CONNECT_BASE_PUBLIC_KEY,
})

export const KB_ID = import.meta.env.VITE_KB_ID as string

3. 문서 업로드 컴포넌트

PDF / DOCX / text 파일을 그대로 드롭하면 addDocumentFromFile 헬퍼가 base64 인코딩 + MIME 추출 + 50MB 검증을 자동 처리합니다.

src/components/KbUploader.tsx:

tsx
import { useState } from 'react'
import { cb, KB_ID } from '../lib/connectbase'

export function KbUploader() {
  const [uploading, setUploading] = useState(false)
  const [status, setStatus] = useState<string>('')

  const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0]
    if (!file) return
    setUploading(true)
    setStatus(`업로드 중: ${file.name}`)
    try {
      const doc = await cb.knowledge.addDocumentFromFile(KB_ID, file, {
        metadata: { source: 'user-upload' },
      })
      // 백그라운드 청킹 폴링
      setStatus(`인덱싱 중... (${doc.status})`)
      while (true) {
        const { documents } = await cb.knowledge.listDocuments(KB_ID)
        const target = documents.find((d) => d.id === doc.id)
        if (target?.status === 'ready') { setStatus(`완료: ${file.name}`); break }
        if (target?.status === 'failed') {
          setStatus(`실패: ${target.error_message ?? '알 수 없음'}`)
          break
        }
        await new Promise((r) => setTimeout(r, 2000))
      }
    } catch (err) {
      setStatus(`에러: ${err instanceof Error ? err.message : String(err)}`)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div>
      <input type="file" accept=".pdf,.docx,.txt,.md,.csv,.json" onChange={handleFile} disabled={uploading} />
      <span style={{ marginLeft: 8 }}>{status}</span>
    </div>
  )
}

지원 파일: PDF (텍스트), DOCX, text/*, JSON. 미지원: 스캔 이미지 PDF / OCR / HWP / XLSX → unsupported mime type for text extraction 에러. 상한: 50MB.

4. 스트리밍 챗봇 훅

cb.ai.chatStreamknowledgeBaseId 만 전달하면 서버가 자동으로 검색 → 컨텍스트 주입 → LLM 호출까지 처리합니다. 토큰이 도착할 때마다 onToken 콜백이 호출되어 실시간 타이핑 효과를 구현할 수 있습니다.

src/hooks/useRagChatbot.ts:

typescript
import { useState, useCallback } from 'react'
import { cb, KB_ID } from '../lib/connectbase'

interface ChatMessage { role: 'user' | 'assistant'; content: string }

export function useRagChatbot() {
  const [messages, setMessages] = useState<ChatMessage[]>([])
  const [streaming, setStreaming] = useState(false)
  const [sources, setSources] = useState<Array<{ document_name: string; content: string }>>([])

  const sendMessage = useCallback(async (content: string) => {
    const nextMessages: ChatMessage[] = [...messages, { role: 'user', content }, { role: 'assistant', content: '' }]
    setMessages(nextMessages)
    setSources([])
    setStreaming(true)

    try {
      await cb.ai.chatStream(
        {
          messages: nextMessages.slice(0, -1),  // assistant placeholder 제외
          knowledgeBaseId: KB_ID,               // ← RAG 활성화
          topK: 5,
          agentic: true,                         // AI 가 검색 쿼리 자동 생성 (선택)
        },
        {
          onToken: (token, metadata) => {
            if (metadata?.type === 'sources') {
              setSources(metadata.sources)
              return
            }
            // 일반 토큰: 마지막 assistant 메시지에 누적
            setMessages((prev) => {
              const copy = [...prev]
              copy[copy.length - 1] = { ...copy[copy.length - 1], content: copy[copy.length - 1].content + token }
              return copy
            })
          },
          onDone: () => setStreaming(false),
          onError: (err) => {
            setStreaming(false)
            setMessages((prev) => [...prev.slice(0, -1), { role: 'assistant', content: `에러: ${err}` }])
          },
        }
      )
    } catch (err) {
      setStreaming(false)
    }
  }, [messages])

  return { messages, sendMessage, streaming, sources }
}

5. 챗봇 UI

src/components/RagChatbot.tsx:

tsx
import { useState } from 'react'
import { useRagChatbot } from '../hooks/useRagChatbot'
import { KbUploader } from './KbUploader'

export function RagChatbot() {
  const [input, setInput] = useState('')
  const { messages, sendMessage, streaming, sources } = useRagChatbot()

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    if (!input.trim() || streaming) return
    sendMessage(input.trim())
    setInput('')
  }

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4 gap-4">
      <KbUploader />

      <div className="flex-1 overflow-y-auto space-y-3">
        {messages.map((msg, i) => (
          <div key={i} className={`p-3 rounded-lg ${msg.role === 'user' ? 'bg-blue-100 ml-12' : 'bg-gray-100 mr-12 whitespace-pre-wrap'}`}>
            {msg.content || (streaming && i === messages.length - 1 ? '검색 중...' : '')}
          </div>
        ))}
      </div>

      {sources.length > 0 && (
        <div className="text-xs text-secondary border-t pt-2">
          <strong>참조 문서:</strong>{' '}
          {sources.map((s) => s.document_name).filter((v, i, a) => a.indexOf(v) === i).join(', ')}
        </div>
      )}

      <form onSubmit={handleSubmit} className="flex gap-2 border-t pt-3">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="문서에 대해 질문하세요..."
          disabled={streaming}
          className="flex-1 px-3 py-2 border rounded"
        />
        <button type="submit" disabled={streaming} className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50">전송</button>
      </form>
    </div>
  )
}

6. 사용자별 격리 (선택)

다중 사용자 RAG 시나리오 (예: 각자 자기 PDF 만 검색) 라면 cb.auth.signInMember(...) 로 AppMember JWT 를 활성화하세요. 서버가 자동으로:

  • addDocumentFromFilemetadata.user_id 자동 태깅
  • chatStream 검색에서 본인 문서로만 제한
  • listDocuments / deleteDocument 도 본인 자료만 노출

추가 코드 변경 불필요 — 토큰이 활성화되면 서버가 알아서 격리합니다.

다음 단계

  • 대화 기록 영속화cb.databaseconversations 테이블에 messages JSON 저장
  • 다중 KB 라우팅 — 카테고리별 KB 를 만들어 질문 키워드에 따라 knowledgeBaseId 선택
  • Agentic 옵션agentic: false 로 단일 쿼리 검색 (응답 빠름, 비용↓), agentic: true 로 AI 가 다중 쿼리 자동 생성 (정확도↑)
  • 출처 표시 UIsources 배열의 chunk_index / score 활용해 신뢰도 시각화
  • 이미지 PDF — OCR 결과를 텍스트로 변환 후 source_type='text' 로 직접 추가 (서버측 OCR 미지원)

이 튜토리얼이 도움이 됐나요?