Pengaturan

Bahasa

Bangun Chatbot AI dengan Satu API Key: Dari Nol hingga Produksi dalam 30 Menit

L
LemonData
ยท26 Februari 2026ยท1 tampilan
#chatbot#tutorial#python#fastapi#streaming
Bangun Chatbot AI dengan Satu API Key: Dari Nol hingga Produksi dalam 30 Menit

ํ•˜๋‚˜์˜ API ํ‚ค๋กœ AI ์ฑ—๋ด‡ ๋งŒ๋“ค๊ธฐ: 30๋ถ„ ๋งŒ์— ์ œ๋กœ์—์„œ ํ”„๋กœ๋•์…˜๊นŒ์ง€

์ด ํŠœํ† ๋ฆฌ์–ผ์—์„œ๋Š” ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต, ๋Œ€ํ™” ๊ธฐ๋ก, ๋ชจ๋ธ ์ „ํ™˜, ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์™„๋ฃŒ AI ์ฑ—๋ด‡ ๋ฐฑ์—”๋“œ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. Python, FastAPI, OpenAI SDK๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ, API ์ง‘๊ณ„๊ธฐ๋ฅผ ํ†ตํ•ด ์–ด๋–ค ๋ชจ๋ธ์ด๋“  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•„์ˆ˜ ์กฐ๊ฑด

pip install fastapi uvicorn openai

1๋‹จ๊ณ„: ๊ธฐ๋ณธ ์ฑ„ํŒ… ์—”๋“œํฌ์ธํŠธ

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import OpenAI
from pydantic import BaseModel

app = FastAPI()

client = OpenAI(
    api_key="sk-lemon-xxx",
    base_url="https://api.lemondata.cc/v1"
)

class ChatRequest(BaseModel):
    message: str
    model: str = "gpt-4.1-mini"
    conversation_id: str | None = None

@app.post("/chat")
async def chat(req: ChatRequest):
    response = client.chat.completions.create(
        model=req.model,
        messages=[{"role": "user", "content": req.message}]
    )
    return {"reply": response.choices[0].message.content}

์ด ์ฝ”๋“œ๋Š” ์ž‘๋™ํ•˜์ง€๋งŒ ์ŠคํŠธ๋ฆฌ๋ฐ, ๊ธฐ๋ก, ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด์ œ ์ด๋ฅผ ๊ฐœ์„ ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

2๋‹จ๊ณ„: ์ŠคํŠธ๋ฆฌ๋ฐ ์ถ”๊ฐ€

์ŠคํŠธ๋ฆฌ๋ฐ์€ ์ „์ฒด ์‘๋‹ต์„ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ํ† ํฐ์ด ์ƒ์„ฑ๋˜๋Š” ์ฆ‰์‹œ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋‹ต๋ณ€์ด ํ˜•์„ฑ๋˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    def generate():
        stream = client.chat.completions.create(
            model=req.model,
            messages=[{"role": "user", "content": req.message}],
            stream=True
        )
        for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                yield f"data: {delta.content}\n\n"
        yield "data: [DONE]\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream"
    )

3๋‹จ๊ณ„: ๋Œ€ํ™” ๊ธฐ๋ก

๋Œ€ํ™” ๊ธฐ๋ก์„ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค(ํ”„๋กœ๋•์…˜์—์„œ๋Š” Redis๋‚˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ๊ต์ฒด ๊ฐ€๋Šฅ).

from collections import defaultdict
import uuid

conversations: dict[str, list] = defaultdict(list)

SYSTEM_PROMPT = "You are a helpful assistant. Be concise and direct."

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    conv_id = req.conversation_id or str(uuid.uuid4())

    # ๋ฉ”์‹œ์ง€ ๊ธฐ๋ก ๊ตฌ์„ฑ
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    messages.extend(conversations[conv_id])
    messages.append({"role": "user", "content": req.message})

    # ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ €์žฅ
    conversations[conv_id].append(
        {"role": "user", "content": req.message}
    )

    def generate():
        full_response = []
        stream = client.chat.completions.create(
            model=req.model,
            messages=messages,
            stream=True
        )
        for chunk in stream:
            delta = chunk.choices[0].delta
            if delta.content:
                full_response.append(delta.content)
                yield f"data: {delta.content}\n\n"

        # ์–ด์‹œ์Šคํ„ดํŠธ ์‘๋‹ต ์ €์žฅ
        conversations[conv_id].append(
            {"role": "assistant", "content": "".join(full_response)}
        )
        yield f"data: [DONE]\n\n"

    return StreamingResponse(
        generate(),
        media_type="text/event-stream",
        headers={"X-Conversation-ID": conv_id}
    )

4๋‹จ๊ณ„: ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

AI API ํ˜ธ์ถœ์€ ์—ฌ๋Ÿฌ ์ด์œ ๋กœ ์‹คํŒจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: ์†๋„ ์ œํ•œ, ์ž”์•ก ๋ถ€์กฑ, ๋ชจ๋ธ ๋ฏธ์‚ฌ์šฉ ๊ฐ€๋Šฅ ๋“ฑ. ๊ฐ ๊ฒฝ์šฐ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค:

from openai import (
    APIError,
    RateLimitError,
    APIConnectionError
)

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    conv_id = req.conversation_id or str(uuid.uuid4())
    messages = build_messages(conv_id, req.message)

    def generate():
        try:
            full_response = []
            stream = client.chat.completions.create(
                model=req.model,
                messages=messages,
                stream=True
            )
            for chunk in stream:
                delta = chunk.choices[0].delta
                if delta.content:
                    full_response.append(delta.content)
                    yield f"data: {delta.content}\n\n"

            conversations[conv_id].append(
                {"role": "assistant", "content": "".join(full_response)}
            )

        except RateLimitError as e:
            yield f"data: [ERROR] ์†๋„ ์ œํ•œ์— ๊ฑธ๋ ธ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค ์ฃผ์„ธ์š”.\n\n"
        except APIConnectionError:
            yield f"data: [ERROR] ์—ฐ๊ฒฐ ์‹คํŒจ. ์žฌ์‹œ๋„ ์ค‘...\n\n"
        except APIError as e:
            yield f"data: [ERROR] {e.message}\n\n"

        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

def build_messages(conv_id: str, user_msg: str) -> list:
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]
    # ์ปจํ…์ŠคํŠธ ๊ธธ์ด ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ตœ๊ทผ 10ํ„ด ์œ ์ง€
    history = conversations[conv_id][-20:]
    messages.extend(history)
    messages.append({"role": "user", "content": user_msg})
    conversations[conv_id].append({"role": "user", "content": user_msg})
    return messages

5๋‹จ๊ณ„: ๋ชจ๋ธ ์ „ํ™˜

์‚ฌ์šฉ์ž๊ฐ€ ๋Œ€ํ™” ์ค‘์— ๋ชจ๋ธ์„ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ์š”๊ตฌ์— ๋งž๋Š” ์—ฌ๋Ÿฌ ๋ชจ๋ธ:

AVAILABLE_MODELS = {
    "fast": "gpt-4.1-mini",
    "smart": "claude-sonnet-4-6",
    "reasoning": "o3",
    "budget": "deepseek-chat",
    "creative": "claude-sonnet-4-6",
}

@app.get("/models")
async def list_models():
    return {"models": AVAILABLE_MODELS}

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ด ์˜ต์…˜๋“ค์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ชจ๋ธ์ด ์ง‘๊ณ„๊ธฐ๋ฅผ ํ†ตํ•ด ๋™์ผํ•œ OpenAI ํ˜ธํ™˜ ํ˜•์‹์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, model ํŒŒ๋ผ๋ฏธํ„ฐ๋งŒ ๋ณ€๊ฒฝํ•˜๋ฉด ์ „ํ™˜์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

6๋‹จ๊ณ„: ์ปจํ…์ŠคํŠธ ์œˆ๋„์šฐ ๊ด€๋ฆฌ

๊ธด ๋Œ€ํ™”๋Š” ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ์ œํ•œ์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

def trim_history(messages: list, max_tokens: int = 8000) -> list:
    """์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ + ์ตœ๊ทผ ๋ฉ”์‹œ์ง€๋ฅผ ํ† ํฐ ์˜ˆ์‚ฐ ๋‚ด๋กœ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค."""
    # ๋Œ€๋žต์ ์ธ ์ถ”์ •: 1 ํ† ํฐ โ‰ˆ 4 ๋ฌธ์ž
    system = messages[0]  # ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋Š” ํ•ญ์ƒ ์œ ์ง€
    history = messages[1:]

    total_chars = len(system["content"])
    trimmed = []

    for msg in reversed(history):
        msg_chars = len(msg["content"])
        if total_chars + msg_chars > max_tokens * 4:
            break
        trimmed.insert(0, msg)
        total_chars += msg_chars

    return [system] + trimmed

์™„์„ฑ๋œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜

# ์‹คํ–‰: uvicorn main:app --reload --port 8000
# ํ…Œ์ŠคํŠธ: curl -N -X POST http://localhost:8000/chat/stream \
#   -H "Content-Type: application/json" \
#   -d '{"message": "Hello!", "model": "gpt-4.1-mini"}'

์ „์ฒด ์ฝ”๋“œ๋Š” 100์ค„ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋‹ค์Œ์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ธ์ฆ (API ํ‚ค ๋˜๋Š” JWT)
  • ์˜์†์  ์ €์žฅ์†Œ (๋Œ€ํ™”์šฉ PostgreSQL ๋˜๋Š” Redis)
  • ์‚ฌ์šฉ์ž๋ณ„ ์†๋„ ์ œํ•œ
  • ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์  ๋ฐ ์ฒญ๊ตฌ
  • ์–‘๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ์„ ์œ„ํ•œ WebSocket ์ง€์›
  • ํ”„๋ก ํŠธ์—”๋“œ (React, Vue ๋˜๋Š” EventSource๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ˆœ์ˆ˜ JS)

๋น„์šฉ ์ถ”์ •

์ผ์ผ 1,000๊ฑด ๋Œ€ํ™”(ํ‰๊ท  5ํ„ด) ์ฒ˜๋ฆฌํ•˜๋Š” ์ฑ—๋ด‡ ๊ธฐ์ค€:

๋ชจ๋ธ ์ผ์ผ ๋น„์šฉ ์›”๊ฐ„ ๋น„์šฉ
GPT-4.1-mini ์•ฝ $2.40 ์•ฝ $72
GPT-4.1 ์•ฝ $12.00 ์•ฝ $360
Claude Sonnet 4.6 ์•ฝ $18.00 ์•ฝ $540
DeepSeek V3 ์•ฝ $1.68 ์•ฝ $50

๋Œ€๋ถ€๋ถ„ ๋Œ€ํ™”๋Š” GPT-4.1-mini๋ฅผ ์‚ฌ์šฉํ•˜๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•  ๋•Œ๋งŒ Claude Sonnet 4.6์œผ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋ฉด ๋Œ€๋ถ€๋ถ„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์›” $100 ์ดํ•˜๋กœ ๋น„์šฉ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


API ํ‚ค ๋ฐ›๊ธฐ: lemondata.cc๋Š” 300๊ฐœ ์ด์ƒ์˜ ๋ชจ๋ธ์„ ํ•˜๋‚˜์˜ ์—”๋“œํฌ์ธํŠธ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. $1 ๋ฌด๋ฃŒ ํฌ๋ ˆ๋”ง์œผ๋กœ ์‹œ์ž‘ํ•˜์„ธ์š”.

Share: