← 블로그 · Projects
실험 노트: 블로그 챗봇과 OpenAI API — 무엇을 검증하려 했고 무엇을 배웠나
#Projects #LLM #OpenAI #CaseStudy
실험 노트: 블로그 챗봇과 OpenAI API
이 글은 완성된 제품의 소개가 아니라 실험 기록입니다. 개인 블로그에 OpenAI API 기반 챗봇을 붙이면서 무엇을 검증하려 했고, 어떤 설계를 택했으며, 어디서 방향을 접었고, 그 과정에서 무엇을 배웠는지를 정리합니다.
문제 정의 — 무엇을 검증하려 했나
검증하고 싶었던 질문은 크게 두 가지였습니다.
- 개인 블로그 규모의 서비스에 LLM 챗봇을 붙이는 데 필요한 최소 구성은 무엇인가. 별도 백엔드 없이, 블로그가 이미 올라가 있는 Next.js 안에서 해결할 수 있는가.
- 블로그 콘텐츠를 답변에 반영하려면 어디까지의 장치가 필요한가. 프롬프트만으로 충분한가, 아니면 RAG 같은 검색 계층이 필요한가.
즉 이 프로젝트의 목표는 “좋은 챗봇”이 아니라, 개인 프로젝트 규모에서 LLM 기능의 적정 복잡도를 찾는 것이었습니다.
배경 지식 정리 — LLM은 어떻게 답을 만드나
설계에 들어가기 전에, API 뒤에서 일어나는 일을 스스로 정리했습니다. 입력과 출력 사이의 동작을 모르면 비용과 한계를 추정할 수 없기 때문입니다.
생성형 AI와 LLM
- 생성형 AI는 대화, 이야기, 이미지, 동영상, 음악 등 새로운 콘텐츠를 만들어내는 AI의 한 갈래입니다.
- **LLM(대규모 언어 모델)**은 그중 텍스트의 이해와 생성을 담당하는 기술입니다.
입력에서 출력까지
- 자연어 입력을 받는다
- Context를 토큰 단위로 쪼갠다
- 다음에 올 토큰을 고른다
- 3번을 계속 반복한다
- 적절한 시점에 생성을 끊는다
- 자연어로 출력한다
핵심은 3번, “다음 토큰 고르기”입니다. 내부적으로는 다음 과정을 거칩니다.
토큰 배열 → 임베딩 벡터 배열 → (복잡한 연산) → Context 벡터
→ 다음 토큰의 확률 분포 → 다음 토큰 선택
학습한다는 것의 의미
- 입력 언어를 하나의 Context 벡터로 치환하는 방법을 학습합니다. 토큰별로 적절한 임베딩 벡터를 매핑하고, 연산 과정의 무수한 가중치를 업데이트하는 일입니다.
- 그 Context 벡터로부터 다음 토큰의 확률 분포를 계산하는 방법을 학습합니다. Context 벡터를 토큰별 확률 분포로 바꾸는 행렬을 업데이트하는 일입니다.
ChatGPT의 3단 구성
- Pre-Training — 자연어를 이해하고 토큰을 잘 예측하도록 학습
- Fine-Tuning & RLHF — 사람의 지시를 잘 따르도록 조정
- Prompting — 누구나 쉽게 쓸 수 있도록 기본 Context를 작성
이 정리에서 얻은 실용적 결론은 하나였습니다. API 호출 비용은 결국 토큰 수에 비례하고, 컨텍스트에 무엇을 넣느냐가 곧 비용 설계라는 점입니다. 이 관점이 뒤의 RAG 판단에 그대로 이어집니다.
설계 — Next.js API Route 하나로 시작
첫 번째 질문(최소 구성)에 대한 답은 단순했습니다. 블로그가 Next.js(Pages Router) 위에 있으므로, /api 핸들러 하나가 챗봇 백엔드의 전부가 될 수 있었습니다.
- 클라이언트는 대화 메시지 배열을 POST로 보낸다
- 서버 핸들러가 system 프롬프트를 앞에 붙여 OpenAI Chat Completions API를 호출한다
- 응답 메시지를 배열에 덧붙여 돌려준다
당시 작성한 핸들러는 다음과 같습니다.
import type { NextApiRequest, NextApiResponse } from 'next';
import OpenAI from 'openai';
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs';
const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_API,
organization: process.env.NEXT_PUBLIC_ORGANIZATION_API
});
type CompletionsResponse = {
messages: ChatCompletionMessageParam[];
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<CompletionsResponse>
) {
if (req.method !== 'POST') return res.status(405).end();
const messages = req.body.messages as ChatCompletionMessageParam[];
const response = await openai.chat.completions.create({
messages: [
{
role: 'system',
content:
'이 챗봇은 해당 블로그의 개발 질문에 성실하게 대답하는 전용 챗봇입니다.'
},
...messages
],
model: 'gpt-3.5-turbo'
});
messages.push(response.choices[0].message);
res.status(200).json({ messages });
}
구조 자체는 의도대로 최소한이었습니다. 다만 지금 다시 보면 명백한 결함이 하나 있습니다. API 키 환경 변수에 NEXT_PUBLIC_ 접두사를 쓴 것입니다. Next.js에서 이 접두사는 값을 클라이언트 번들에 노출시키므로, 서버 전용 시크릿에는 절대 쓰면 안 되는 이름입니다. 서버 핸들러 안에서만 사용했더라도 명명 자체가 사고의 씨앗이며, 이 실험에서 코드보다 값진 교훈 중 하나로 남았습니다.
두 번째 질문 — RAG 도입 시도와 철회
블로그 콘텐츠를 답변에 반영하기 위해, 처음에는 RAG(Retrieval-Augmented Generation)를 도입하려 했습니다. 블로그 포스트를 임베딩해 벡터 데이터베이스에 저장하고, 사용자 질문과 관련된 콘텐츠를 검색해 컨텍스트로 제공하는 설계였습니다.
진행하며 드러난 문제
1. API 비용
- 블로그 콘텐츠 전체를 임베딩하는 과정에서 상당한 API 비용이 발생했습니다.
- 사용자 질문마다 검색 결과를 컨텍스트로 밀어 넣으면서 호출당 토큰이 늘었고, 비용이 예상보다 높아졌습니다.
- 개인 프로젝트 수준의 트래픽에서 이 추가 비용은 부담이었고, 비용 대비 답변 품질 개선은 크지 않았습니다.
2. 오버엔지니어링
- 개인 블로그 챗봇의 사용량과 요구 복잡도를 놓고 보면, RAG는 문제 크기보다 큰 해법이었습니다.
- 단순한 프롬프트 엔지니어링만으로도 목적에 충분한 답변 품질이 나왔습니다.
- 벡터 DB와 검색 파이프라인이 들어오면서 시스템 복잡도와 유지보수 부담이 함께 커졌습니다.
철회 결정
비용과 복잡도, 두 축 모두에서 RAG는 이 프로젝트의 실제 필요를 넘어섰다고 판단해 도입을 철회했고, 기본적인 프롬프트 엔지니어링 방식으로 단순화했습니다. 실패라기보다는, 두 번째 검증 질문에 대한 답을 얻은 것에 가깝습니다. 개인 블로그 규모에서는 프롬프트만으로 충분하다.
한계
이 실험이 다루지 못한 것도 분명히 적어 둡니다.
- 답변이 모델의 사전 학습 지식과 system 프롬프트에 의존하므로, 블로그의 최신 글을 정확히 인용하는 수준의 응답은 보장하지 못합니다. RAG를 철회하며 의도적으로 받아들인 한계입니다.
- 요청 인증·사용량 제한 같은 운영 장치는 실험 범위 밖이었습니다. 공개 서비스로 키우려면 가장 먼저 필요한 부분입니다.
- 위에서 언급한 환경 변수 명명 문제처럼, 최소 구성이라는 목표가 보안 관행의 점검을 건너뛰게 만든 지점이 있었습니다.
배운 것
- 기술 도입은 비용과 복잡도 대비 실제 필요로 판단해야 합니다. 특히 LLM에서는 컨텍스트 설계가 곧 비용 설계이고, “더 정확해질 것 같다”는 기대만으로 검색 계층을 얹으면 비용이 먼저 도착합니다.
- 작은 프로젝트에는 단순한 해법이 더 적합할 수 있습니다. API Route 하나 + system 프롬프트라는 최소 구성이 이 규모에서는 정답이었습니다.
- 오버엔지니어링을 피하고, 실제 필요에 맞는 최소한의 솔루션에서 시작해 필요가 증명될 때 확장하는 편이 낫습니다. RAG를 먼저 만들고 철회한 순서가 아니라, 프롬프트로 시작해 부족함이 증명되면 RAG를 검토하는 순서였다면 임베딩 비용을 아꼈을 것입니다.
- 시크릿 관리는 최소 구성에서도 최소가 아닙니다. 환경 변수 이름 하나가 노출 사고로 이어질 수 있다는 점을 코드 리뷰 항목으로 남겼습니다.
이 실험의 결론을 한 문장으로 줄이면 이렇습니다. 기술의 복잡도와 비용은 프로젝트의 실제 필요와 균형을 이뤄야 하며, 그 균형점은 생각보다 단순한 쪽에 있다.