Skip to main content

챗봇 개발 프로젝트

LLM API를 활용하여 시스템 프롬프트 설계부터 Function Calling, 대화 히스토리 관리, 안전장치 적용, Gradio ChatInterface 배포까지 완전한 챗봇을 구축하는 프로젝트입니다. 이 프로젝트는 프롬프트 엔지니어링과 LLM 활용 실무 섹션의 내용을 종합적으로 적용합니다.

사전 준비

pip install openai anthropic gradio python-dotenv
# .env 파일
OPENAI_API_KEY=sk-...

실습

1

시스템 프롬프트 설계

챗봇의 성격, 역할, 행동 규칙을 정의하는 시스템 프롬프트를 설계합니다.
# 시스템 프롬프트 설계 - NLP 학습 어시스턴트
SYSTEM_PROMPT = """당신은 "NLP 학습 도우미"입니다. 자연어 처리(NLP)를 학습하는 사람들을 돕는 AI 어시스턴트입니다.

## 역할과 성격
- NLP/딥러닝 분야의 친절한 멘토입니다
- 복잡한 개념을 쉬운 비유로 설명합니다
- 질문자의 수준에 맞춰 답변 깊이를 조절합니다

## 행동 규칙
1. 한국어로 답변하되, 영어 기술 용어는 그대로 사용합니다
2. 코드 예시가 필요하면 Python으로 작성합니다
3. 확실하지 않은 정보는 "확실하지 않습니다"라고 솔직하게 밝힙니다
4. 답변 끝에 관련 추가 질문을 제안합니다

## 제한 사항
- NLP/AI/딥러닝 관련 질문에만 답변합니다
- 의료, 법률, 금융 조언은 하지 않습니다
- 개인정보를 요구하거나 저장하지 않습니다

## 도구
현재 시간 조회와 NLP 용어 검색 도구를 사용할 수 있습니다.
"""

# 프롬프트 품질 체크리스트
checklist = {
    "역할 정의": "NLP 학습 도우미" in SYSTEM_PROMPT,
    "행동 규칙": "행동 규칙" in SYSTEM_PROMPT,
    "제한 사항": "제한 사항" in SYSTEM_PROMPT,
    "출력 형식": "한국어" in SYSTEM_PROMPT,
}
print("프롬프트 품질 체크:")
for item, passed in checklist.items():
    print(f"  {'[O]' if passed else '[X]'} {item}")
좋은 시스템 프롬프트의 구조: 역할 정의행동 규칙제한 사항출력 형식. 이 네 가지 요소를 빠짐없이 포함하면 일관된 챗봇 동작을 기대할 수 있습니다.
2

OpenAI/Claude API 연동

여러 LLM 제공자를 유연하게 전환할 수 있는 통합 인터페이스를 구현합니다.
from openai import OpenAI
from typing import Generator
import os
from dotenv import load_dotenv

load_dotenv()

class LLMClient:
    """LLM API 통합 클라이언트"""

    def __init__(self, provider: str = "openai"):
        self.provider = provider
        if provider == "openai":
            self.client = OpenAI()
            self.model = "gpt-4o-mini"
        else:
            raise ValueError(f"지원하지 않는 제공자: {provider}")

    def chat(
        self,
        messages: list,
        tools: list = None,
        stream: bool = False,
    ):
        """메시지를 전송하고 응답을 받습니다."""
        kwargs = {
            "model": self.model,
            "messages": messages,
            "temperature": 0.7,
            "stream": stream,
        }
        if tools:
            kwargs["tools"] = tools
            kwargs["tool_choice"] = "auto"

        return self.client.chat.completions.create(**kwargs)

    def stream_chat(self, messages: list) -> Generator[str, None, None]:
        """스트리밍으로 응답을 생성합니다."""
        stream = self.chat(messages, stream=True)
        for chunk in stream:
            if chunk.choices[0].delta.content:
                yield chunk.choices[0].delta.content

# 클라이언트 생성
llm = LLMClient(provider="openai")

# 기본 테스트
response = llm.chat([
    {"role": "system", "content": SYSTEM_PROMPT},
    {"role": "user", "content": "안녕하세요! Transformer가 뭔가요?"},
])
print(response.choices[0].message.content)
3

대화 히스토리 관리

대화 맥락을 유지하면서 토큰 사용량을 효율적으로 관리합니다.
import tiktoken
from typing import Optional
from dataclasses import dataclass, field

@dataclass
class ConversationManager:
    """대화 히스토리를 관리합니다."""
    system_prompt: str
    max_tokens: int = 4000  # 히스토리 최대 토큰 수
    messages: list = field(default_factory=list)

    def __post_init__(self):
        self.encoding = tiktoken.encoding_for_model("gpt-4o-mini")
        self.messages = [
            {"role": "system", "content": self.system_prompt}
        ]

    def _count_tokens(self, messages: list) -> int:
        """메시지 목록의 토큰 수를 계산합니다."""
        total = 0
        for msg in messages:
            total += 4  # 메시지 구조 오버헤드
            total += len(self.encoding.encode(msg.get("content", "")))
        return total

    def add_user_message(self, content: str):
        """사용자 메시지를 추가합니다."""
        self.messages.append({"role": "user", "content": content})
        self._trim_if_needed()

    def add_assistant_message(self, content: str):
        """어시스턴트 메시지를 추가합니다."""
        self.messages.append({"role": "assistant", "content": content})

    def _trim_if_needed(self):
        """토큰 제한을 초과하면 오래된 메시지를 제거합니다."""
        while self._count_tokens(self.messages) > self.max_tokens:
            if len(self.messages) <= 2:  # system + 최소 1개 메시지
                break
            # 가장 오래된 대화 쌍을 제거 (시스템 메시지 다음부터)
            self.messages.pop(1)

    def get_messages(self) -> list:
        """현재 대화 히스토리를 반환합니다."""
        return self.messages.copy()

    def clear(self):
        """대화를 초기화합니다."""
        self.messages = [
            {"role": "system", "content": self.system_prompt}
        ]

    @property
    def token_count(self) -> int:
        return self._count_tokens(self.messages)

# 사용 예시
conversation = ConversationManager(system_prompt=SYSTEM_PROMPT)

# 대화 진행
conversation.add_user_message("BERT가 뭔가요?")
print(f"현재 토큰 수: {conversation.token_count}")
4

Function Calling으로 기능 확장

챗봇에 외부 도구를 연결하여 기능을 확장합니다.
import json
from datetime import datetime

# 도구 함수 구현
def get_current_time() -> str:
    """현재 시간을 반환합니다."""
    now = datetime.now()
    return now.strftime("%Y년 %m월 %d일 %H시 %M분")

def search_nlp_glossary(term: str) -> dict:
    """NLP 용어를 검색합니다."""
    glossary = {
        "transformer": {
            "korean": "트랜스포머",
            "definition": "Self-Attention 메커니즘 기반의 시퀀스 처리 아키텍처",
            "paper": "Attention Is All You Need (2017)",
            "key_concept": "Query-Key-Value 기반 Attention",
        },
        "bert": {
            "korean": "버트",
            "definition": "양방향 인코더 사전학습 모델 (Bidirectional Encoder Representations from Transformers)",
            "paper": "BERT: Pre-training of Deep Bidirectional Transformers (2018)",
            "key_concept": "Masked Language Model + Next Sentence Prediction",
        },
        "tokenization": {
            "korean": "토큰화",
            "definition": "텍스트를 모델이 처리할 수 있는 최소 단위로 분할하는 과정",
            "methods": "BPE, WordPiece, SentencePiece, Unigram",
            "key_concept": "서브워드 토큰화로 OOV 문제 해결",
        },
        "attention": {
            "korean": "어텐션",
            "definition": "시퀀스 내 중요한 정보에 선택적으로 가중치를 부여하는 메커니즘",
            "types": "Bahdanau, Luong, Self-Attention, Multi-Head",
            "key_concept": "Query와 Key의 유사도로 Value에 가중 합산",
        },
    }

    term_lower = term.lower().replace(" ", "")
    result = glossary.get(term_lower)

    if result:
        return {"found": True, "term": term, **result}
    else:
        return {"found": False, "term": term, "message": f"'{term}'에 대한 정보를 찾을 수 없습니다."}

# 도구 정의
chatbot_tools = [
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "현재 날짜와 시간을 조회합니다.",
            "parameters": {"type": "object", "properties": {}, "required": []},
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_nlp_glossary",
            "description": "NLP 용어의 정의, 관련 논문, 핵심 개념을 검색합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "term": {
                        "type": "string",
                        "description": "검색할 NLP 용어 (예: transformer, bert, attention)"
                    }
                },
                "required": ["term"],
            }
        }
    }
]

# 도구 매핑
tool_functions = {
    "get_current_time": get_current_time,
    "search_nlp_glossary": search_nlp_glossary,
}

def process_tool_calls(response, messages: list) -> list:
    """도구 호출을 처리하고 결과를 메시지에 추가합니다."""
    assistant_message = response.choices[0].message

    if not assistant_message.tool_calls:
        return messages

    messages.append(assistant_message)

    for tool_call in assistant_message.tool_calls:
        func_name = tool_call.function.name
        func_args = json.loads(tool_call.function.arguments)

        # 도구 실행
        try:
            result = tool_functions[func_name](**func_args)
        except Exception as e:
            result = {"error": str(e)}

        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result, ensure_ascii=False),
        })

    return messages
5

Gradio ChatInterface UI

대화형 UI를 Gradio ChatInterface로 구현합니다.
import gradio as gr
import json
from openai import OpenAI

client = OpenAI()

def chat_with_bot(message: str, history: list) -> str:
    """Gradio ChatInterface용 챗봇 응답 함수"""
    # 대화 히스토리 구성
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    # Gradio history 형식 변환
    for user_msg, bot_msg in history:
        messages.append({"role": "user", "content": user_msg})
        if bot_msg:
            messages.append({"role": "assistant", "content": bot_msg})

    messages.append({"role": "user", "content": message})

    # 1차 호출 (도구 포함)
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        tools=chatbot_tools,
        tool_choice="auto",
        temperature=0.7,
    )

    # 도구 호출 처리
    assistant_message = response.choices[0].message
    if assistant_message.tool_calls:
        messages = process_tool_calls(response, messages)

        # 도구 결과를 포함하여 최종 응답
        final_response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            temperature=0.7,
        )
        return final_response.choices[0].message.content
    else:
        return assistant_message.content

def chat_with_bot_streaming(message: str, history: list):
    """스트리밍 버전의 챗봇 응답 함수"""
    messages = [{"role": "system", "content": SYSTEM_PROMPT}]

    for user_msg, bot_msg in history:
        messages.append({"role": "user", "content": user_msg})
        if bot_msg:
            messages.append({"role": "assistant", "content": bot_msg})

    messages.append({"role": "user", "content": message})

    # 스트리밍 호출
    stream = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.7,
        stream=True,
    )

    partial_response = ""
    for chunk in stream:
        if chunk.choices[0].delta.content:
            partial_response += chunk.choices[0].delta.content
            yield partial_response

# Gradio ChatInterface
demo = gr.ChatInterface(
    fn=chat_with_bot_streaming,
    title="NLP 학습 도우미",
    description=(
        "NLP/딥러닝 학습을 도와주는 AI 어시스턴트입니다. "
        "Transformer, BERT, 토큰화 등 NLP 관련 질문을 해보세요!"
    ),
    examples=[
        "Transformer의 Self-Attention이 뭔가요?",
        "BERT와 GPT의 차이점을 알려주세요",
        "한국어 NLP에서 형태소 분석이 왜 중요한가요?",
        "지금 몇 시인가요?",
    ],
    theme=gr.themes.Soft(),
    retry_btn="다시 생성",
    undo_btn="이전으로",
    clear_btn="대화 초기화",
)

demo.launch(share=False)
스트리밍 모드에서는 Function Calling이 자동으로 처리되지 않습니다. 스트리밍과 도구 호출을 모두 지원하려면, 도구 호출이 필요한 경우 먼저 비스트리밍으로 도구를 처리한 뒤, 최종 응답만 스트리밍하는 하이브리드 방식을 사용하세요.
6

안전장치 적용

이전 섹션에서 학습한 안전장치를 챗봇에 통합합니다.
import re

class ChatbotGuardrails:
    """챗봇 전용 안전장치"""

    # 주제 이탈 키워드 (NLP/AI와 무관한 주제)
    OFF_TOPIC_PATTERNS = [
        r"(?i)(주식|투자|비트코인|부동산)",
        r"(?i)(처방|진단||병원|치료)",
        r"(?i)(법률|소송|재판|변호사)",
    ]

    # 프롬프트 인젝션 패턴
    INJECTION_PATTERNS = [
        r"(?i)ignore\s+(all\s+)?previous",
        r"(?i)시스템\s*프롬프트",
        r"(?i)(너의|당신의)\s*규칙",
    ]

    def check_input(self, user_input: str) -> dict:
        """입력을 검사합니다."""
        # 길이 제한
        if len(user_input) > 2000:
            return {"allowed": False, "reason": "메시지가 너무 깁니다 (최대 2000자)."}

        # 프롬프트 인젝션 검사
        for pattern in self.INJECTION_PATTERNS:
            if re.search(pattern, user_input):
                return {"allowed": False, "reason": "보안상 처리할 수 없는 요청입니다."}

        # 주제 이탈 검사
        for pattern in self.OFF_TOPIC_PATTERNS:
            if re.search(pattern, user_input):
                return {
                    "allowed": False,
                    "reason": "NLP/AI 관련 질문에만 답변 가능합니다. 다른 질문을 해주세요."
                }

        return {"allowed": True, "reason": None}

# 안전장치가 적용된 최종 챗봇 함수
guardrails = ChatbotGuardrails()

def safe_chat(message: str, history: list) -> str:
    """안전장치가 적용된 챗봇 응답 함수"""
    # 입력 안전장치
    check = guardrails.check_input(message)
    if not check["allowed"]:
        return check["reason"]

    # 정상 처리
    return chat_with_bot(message, history)

# 안전한 Gradio ChatInterface
safe_demo = gr.ChatInterface(
    fn=safe_chat,
    title="NLP 학습 도우미 (안전장치 적용)",
    description="NLP/딥러닝 학습을 도와주는 AI 어시스턴트입니다.",
    theme=gr.themes.Soft(),
)

safe_demo.launch(share=False)

트러블슈팅

Gradio 버전에 따라 history 형식이 다를 수 있습니다.
  • Gradio 4.x: historylist[list[str, str]] (사용자, 봇 쌍)
  • Gradio 5.x: historylist[dict] 형태일 수 있습니다
  • type="messages"를 ChatInterface에 지정하면 OpenAI 호환 형식으로 사용 가능합니다
정상적인 동작입니다. LLM은 토큰 단위로 생성하며, 한국어 한 글자가 하나의 토큰인 경우가 많습니다. 사용자 경험을 위해 버퍼링은 Gradio가 자동으로 처리합니다.
도구의 description이 충분히 구체적인지 확인하세요. 시스템 프롬프트에 “도구를 사용할 수 있습니다”라는 안내를 포함하면 도구 호출률이 높아집니다.
대화 히스토리의 토큰 수가 누적되기 때문입니다. ConversationManagermax_tokens를 조절하거나, 오래된 대화를 요약하는 전략을 적용하세요.

다음 단계