딥러닝에서 챗봇의 발전
1. 챗봇이란?
사용자의 자연어 질문을 분석하여 응답을 생성하는 프로그램이다.
규칙기반 - 검색기반 - 생성기반 - 지능형 에이전트 순서로 발전했다.
챗봇은 초기(규칙/검색기반)에는 입력문–패턴–매칭–출력문 구조로 동작했으며, 이후 생성 기반과 지능형 에이전트로 발전하면서 내부 처리 방식이 고도화되었다.
챗봇은 사용자의 입력을 받아 답변을 하는데
아래와 같이 4개 순서대로 발전했다.
규칙기반 챗봇 : 미리 정해진 패턴에만 반응하여 조금이라도 달라져도 실패
검색기반 챗봇 : 가장 유사한 문서나 문장을 찾아서 그대로 반환
생성기반 챗봇 : 새로운 문장을 직접 생성해서 반환
지능형 에이전트 챗봇 : 문장 생성과 더불어 실제 행동을 수행
정해진 답변만을 해야 하는 사내 FAQ에는 검색기반 챗봇이 적합하며,
실무에서는 질문 표현이 다양하기 때문에 검색 결과를 바탕으로 답변을 생성하는
RAG 기반 생성기반 챗봇이 가장 적합하다.
지능형 에이전트 챗봇의 예시로는 커서(Cursor)나 클로드(Claude)등이 있고,
사용자의 입력으로 API 호출, DB 저장, 통계 계산, 리포트(문서) 생성 등의 "행동"들을 할 수 있다.
즉 생성 기반 챗봇에 RAG 가 사용됐다면,
지능형 에이전트는 RAG에 더해 Tool 활용 + Memory(DB, 기록)를 통해 실제 행동까지 수행한다고 볼 수 있다.
2. RAG란?
RAG(Retrieval-Augmented Generation)은 외부 문서를 먼저 검색한 뒤, 그 결과를 참고해
모델이 답변을 만드는 구조다. 생성기반 챗봇부터 사용된다.
3. 로컬 AI란?
클라우드 LLM과 달리 로컬 환경에서 다운로드하여 사용할 수 있는 AI를 말한다.
개인별로 원하는 파일만을 학습시켜 커스터마이징을 할 수도 있고,
학습된 내용이 클라우드에 기록되지 않기에 보안 유지에도 좋다.
4. 로컬 AI의 활용
로컬 AI는 Ollama와 같은 서버플랫폼을 통해 PC에서 직접 다운로드하고 실행할 수 있다.
Ollama는 아래 경로에서 설치할 수 있다.
https://ollama.com/download
Download Ollama on macOS
Download Ollama for macOS
ollama.com

Ollama를 설치하고 나면 CLI나 터미널에서 명령어를 통해 모델을 설치할 수 있다.
예시)
C:\WINDOWS\system32>ollama pull 모델명
글에서는 메모리가 8GB ~ 16GB 사이일 때 적합한 gemma3:4 b 모델을 사용했다.
Ollama의 API 경로)
# 1단계) 텍스트 생성 요청 : 단일 질문, 단일 응답
http://localhost:11434/api/generate
# 2단계) 대화형 생성 요청 : 제한적으로 이전의 대화내용을 기억
http://localhost:11434/api/chat
2단계로 표시된 /chat 경로로 사용할 경우,
응답 데이터의 message 객체를 chat_history 배열에 저장해서
대화 문맥을 기억하게 하여 더 자연스러운 대답을 만들어낼 수 있다.
TF-IDF로 텍스트를 수치벡터로 변환)
# 3단계) TfIdf 기반 검색 활용
# 텍스트를 수치 벡터로 변환하기 위한 TF-IDF 벡터라이저를 생성한다
local_vectorizer = TfidfVectorizer(
# 텍스트가 벡터화되기 전에 자동으로 실행되는 전처리 함수
preprocessor=normalize_korean_text,
# 단어가 아닌 문자(char) 단위로 토큰을 생성하여
# 짧고 다양한 한국어 질의에도 대응할 수 있도록 한다
analyzer="char",
# 2~4글자 길이의 문자 n-gram을 사용해
# 문맥과 의미를 더 잘 반영하도록 설정한다
ngram_range=(2, 4),
)
TF-IDF 사용 전체 코드 예시) 아래 더보기 클릭
import requests
import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# Ollama에서 실행할 모델 이름
model_name = "gemma3:4b"
# Ollama Chat API 엔드포인트 (messages 기반)
api_url = "http://localhost:11434/api/chat"
# 이전 대화 기록을 저장해 대화 맥락을 유지
chat_history: list[dict] = []
# TF-IDF 벡터 검색을 위한 더미 문서 목록
# {"text": 질문, "intent": 답변}
documents: list[dict] = [
{
"text": "우리 회사 연차 신청 규정이 어떻게 되나요?",
"intent": "그룹웨어를 통해 신청해야 하며 연차 사용일 기준 최소 3일 전에 신청서를 작성하고 팀장의 승인을 받아야 한다."
},
{
"text": "연차는 얼마나 발생하나요?",
"intent": "연차는 입사 첫해 11일이 발생하고 근속 연수에 따라 매년 1일씩 증가한다."
},
]
# 한글 텍스트 정규화 함수
def normalize_korean_text(text: str) -> str:
if not text:
return ""
return " ".join(text.strip().split())
# TF-IDF 인덱스 생성
def build_tfidf_index(docs):
local_corpus = []
for doc in docs:
text = (doc["text"] + "\n" + doc["intent"]).strip()
local_corpus.append(text)
local_vectorizer = TfidfVectorizer(
preprocessor=normalize_korean_text,
analyzer="char",
ngram_range=(2, 4),
)
local_doc_vectors = local_vectorizer.fit_transform(local_corpus)
return local_corpus, local_vectorizer, local_doc_vectors
# TF-IDF 인덱스 준비
corpus, vectorizer, doc_vectors = build_tfidf_index(documents)
# 사용자 질문과 가장 유사한 문서 top-k 검색
def retrieve_top_docs(query: str, top_k: int = 3):
if not vectorizer or doc_vectors is None:
return []
query_vec = vectorizer.transform([query])
sims = cosine_similarity(query_vec, doc_vectors)[0]
ranked_indices = np.argsort(-sims)
results = []
for idx in ranked_indices[:top_k]:
results.append((documents[idx], float(sims[idx])))
return results
# 검색 결과를 프롬프트용 컨텍스트 문자열로 변환
def format_context(docs_with_scores, min_score: float = 0.0) -> str:
if not docs_with_scores:
return "관련된 회사 문서를 찾지 못했다."
lines = []
for i, (doc, score) in enumerate(docs_with_scores, start=1):
if score < min_score:
continue
text = doc.get("text", f"문서 {i}")
intent = doc.get("intent", "")
lines.append(f"[{i}번 질문] : {text}")
lines.append(f"내용 : {intent}")
lines.append("")
if not lines:
return "관련된 회사 문서를 찾지 못했다."
return "\n".join(lines)
# 사용자 질문 → 벡터 검색 → 컨텍스트 생성
def build_context_text(user_message: str) -> str:
top_docs = retrieve_top_docs(user_message, top_k=3)
return format_context(top_docs)
# Ollama 호출 함수
def ask_ollama(prompt: str) -> str:
context_text = build_context_text(prompt)
messages = []
messages.append({
"role": "system",
"content": (
"다음은 회사 규정과 내부 안내에 대한 문서들이다.\n"
f"{context_text}\n\n"
"가능한 한 위 문서 내용에 근거해 한국어로 답변한다.\n"
"문서에 없는 내용은 추측하지 말고 "
"'관련된 회사 문서를 찾지 못했다'라고 말한다."
)
})
if chat_history:
messages.extend(chat_history)
messages.append({"role": "user", "content": prompt})
payload = {
"model": model_name,
"messages": messages,
"stream": False,
}
resp = requests.post(api_url, json=payload)
resp.raise_for_status()
data = resp.json()
answer = data.get("message", {}).get("content", "").strip()
chat_history.append({"role": "user", "content": prompt})
chat_history.append({"role": "assistant", "content": answer})
return answer
# 메인 실행부
if __name__ == "__main__":
print("=== Ollama gemma3:4b TF-IDF RAG 챗봇 ===")
print("CSV 없이 더미 문서(text, intent)로 테스트 중")
print("종료 명령어 : quit, exit")
while True:
user = input("사용자 : ").strip()
if user.lower() in ["quit", "exit"]:
print("챗봇을 종료한다.")
break
if not user:
continue
try:
print("봇 :", ask_ollama(user))
except Exception as e:
print("오류 :", e)
- CSV 문서 → 벡터화
- 코사인 유사도 기반 검색
- 검색 결과를 system prompt에 삽입
- 근거 기반 응답 생성
위 코드에서 중요한 부분은
1. top_k 를 몇으로 세팅할지,
2. n-gram()의 범위
와 같이 모델을 세팅할 때 변화를 줄 수 있는 부분일 것이다.
document 변수대신
csv 파일로 변수를 만든다면
많은 질문, 응답 리스트를 만들 수 있다.
그 후 TF-IDF로 만들어진 벡터와 코사인 유사도를 조합해 답변 리스트를 만들 수 있다.
ollama의 messages 객체를 조작해서 의도에 맞게 답변을 조정할 수 있다.
ollama의 답변을 세팅하는 함수 예시)
def ask_ollama(prompt: str) -> str:
messages = [
{
"role": "system",
"content": (
"너는 company_docs.csv만 참고해서 답변하는 인사·총무 챗봇이다. "
"회사 문서에 없으면 '업무에 관련된 내용이 아니라 답변을 드릴 수 없습니다.'라고만 말한다."
),
},
{"role": "user", "content": prompt},
]
payload = {
"model": model_name,
"messages": messages,
"stream": False,
# "options": {
# "temperature": 0
# },
}
resp = requests.post(api_url, json=payload, timeout=120)
resp.raise_for_status()
data = resp.json()
message = data.get("message", {}) or {}
return (message.get("content", "") or "").strip()
참고로 리액트에서 지원하지 않는 파이썬의 라이브러리들이 있기 때문에,
파이썬의 라이브러리를 그대로 사용할 수 있는 플라스크를 사용하여 서버를 구성하는 것이 좋다.
TfidfVectorizer 나 consine_similarity는 리액트에서 완전히 동일한 기능을 지원하는 라이브러리는 없다.
그래서 예를 들어
프론트는 리액트
백앤드는 스프링
챗봇 API 서버는 플라스크로 나눠서 사용하는 것이 좋다.
