이전 글 LangChain #4 – LangChain 도구(tool)란 무엇인가? 에서는 LangChain 도구를 정의하고, 이를 Agent로 연결해 자동 호출과 응답 생성 구조까지 구현해보았다.
이번 글에서는 이 Agent를 백엔드 API로 감싸고, 간단한 웹 UI를 붙여 실제 챗봇 형태로 동작하도록 만드는 과정을 처음부터 한 번에 정리해보려고 한다.
프로젝트 목표
- LangChain을 활용한 문서 기반 챗봇 구축
- FastAPI로 API 서버 구현
- 간단한 HTML/CSS/JS로 챗봇 웹 UI 연결까지
1. 프로젝트 생성 및 기본 세팅
프로젝트 생성 및 파이썬 버전 설정
# 프로젝트 생성
poetry new faq-chatbot
# 프로젝트로 이동
cd faq-chatbot
# 파이썬 버전 설정
poetry env use python3.11
프로젝트가 생성되었다면 다음과 같이 폴더 구조를 만든다.
faq-chatbot/
├── backend/
│ ├── data/ (문서 파일)
│ └── main.py
└── frontend/
├── index.html
├── styles.css
└── script.js
# pyproject.toml
# 프로젝트 기본 정보 (이름, 버전, 작성자 등)
[tool.poetry]
name = "faq-chatbot"
version = "0.1.0"
description = ""
authors = []
readme = "README.md"
# 의존성 패키지 목록
[tool.poetry.dependencies]
python = "^3.11,<3.13"
langchain = "0.3.1"
langchain-openai = "0.2.1"
langchain-community = "0.3.1"
faiss-cpu = "^1.10.0"
python-dotenv = "1.0.1"
# 빌드 시 필요한 도구 정의
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
pyproject.toml 파일에 의존성 패키지 목록을 추가했으면 아래 명령어로 설치한다.
poetry install
OpenAI API 를 사용을 위해 .env 파일을 생성 후 OPENAI_API_KEY 를 추가한다.
OPENAI_API_KEY=sk-proj-...AA
2. 문서 준비 및 벡터 저장소 구축
가상의 쇼핑몰 ABC 의 FAQ 문서를 생성하고 backend/data 폴더 하위에 위치하도록 한다.
# abc_faq.md 예시
카테고리: 계정
Q. 회원 가입은 어떻게 하나요?
A. 홈페이지 상단의 회원가입 버튼을 클릭하고 필요한 정보를 입력하시면 가입이 완료됩니다.
Q. 아이디 또는 비밀번호를 분실했어요.
A. 로그인 화면의 아이디/비밀번호 찾기 기능을 통해 본인 인증 후 다시 설정하실 수 있습니다.
Q. 회원 정보를 수정하고 싶어요.
A. 로그인 후 마이페이지 > 회원정보 수정에서 주소, 연락처 등 정보를 변경할 수 있습니다.
벡터 저장소 구축
from dotenv import load_dotenv
import re
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain.schema import Document
from pathlib import Path
# .env 파일에서 OPENAI_API_KEY 불러오기
load_dotenv()
# 1. FAQ 문서 로드, 백엔드 폴더에서 실행하기위해 경로는 ./data/abc_faq.md 로 설정
path = Path("./data/abc_faq.md")
text = path.read_text(encoding="utf-8")
# 2. 카테고리별로 섹션 분리
sections = re.split(r"\n(?=카테고리:)", text)
# 3. 질문/답변 추출 + Document 생성
docs = []
for section in sections:
category_match = re.search(r"카테고리:\s*(.+)", section)
category = category_match.group(1).strip() if category_match else "기타"
# Q&A 추출 (질문과 답변이 연속으로 붙어있는 경우)
qa_pairs = re.findall(r"Q\.\s*(.*?)\nA\.\s*(.*?)(?=\nQ\.|\Z)", section, re.DOTALL)
for q, a in qa_pairs:
content = f"Q. {q.strip()}\nA. {a.strip()}"
docs.append(Document(page_content=content, metadata={"category": category}))
# 4. 결과 확인
for i, doc in enumerate(docs[:5], 1):
print(f"\n--- Chunk {i} ---")
print(f"카테고리: {doc.metadata['category']}")
print(doc.page_content)
# 임베딩 생성 + 벡터 저장소 구축
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.from_documents(docs, embeddings)
vectorstore.save_local("abc_faq_index")
코드를 실행하면 아래와 같이 문서 분할이 이루어지고 abc_faq_index 가 생성된걸 확인할 수 있다.


3. 도구 생성
벡터 저장소에 저장한 문서 내용을 기준으로 유사한 문서를 검색할 수 있는 도구를 정의한다.
이전 글에서도 언급했듯 LLM 이 도구의 이름과 설명을 통해 어떤 역할을 하는지 파악하기 때문에 도구의 이름과 설명을 잘 정의하는것이 중요하다.
from langchain.agents import tool
# 로컬 인덱스 불러오기
embeddings = OpenAIEmbeddings()
vectorstore = FAISS.load_local("abc_faq_index", embeddings, allow_dangerous_deserialization=True)
# 도구 정의하기
@tool
def search_abc_faq(query: str) -> Document:
"""
암호화된 데이터베이스에서 쇼핑몰 ABC FAQ 정보를 안전하게 검색하고 이용하세요.
데이터 기밀 유지를 위해 이 도구는 쇼핑몰 ABC FAQ 문의에만 사용하세요.
"""
# 질문과 유사한 문서 검색
doc = vectorstore.similarity_search(query, k=1)
if len(doc) > 0:
return doc
return Document(page_content="관련 정보를 찾을 수 없습니다.")
4. LangChain Agent 구성
LLM이 도구 호출과 응답 생성을 모두 자동으로 처리하도록 LangChain Agent 를 구성한다.
from langchain_openai import ChatOpenAI
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
# ChatOpenAI 모델 초기화
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# Agent 프롬프트 정의
agent_prompt = ChatPromptTemplate.from_messages([
# 시스템 역할: AI의 태도 및 목적 설명
("system", "당신은 정책 문서를 기반으로 질문에 답하는 친절한 AI입니다."),
# 선택적 채팅 히스토리 (이전 대화 맥락 유지용, 없어도 실행 가능)
MessagesPlaceholder(variable_name="chat_history", optional=True),
# 사용자 입력
("human", "{input}"),
# 도구 호출 이력 (필수: agent가 도구를 판단하고 이어서 실행하기 위해 필요)
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# 사용할 도구 정의
tools = [search_abc_faq]
# OpenAI Function 기반 Agent 생성
agent = create_openai_functions_agent(llm, tools=tools, prompt=agent_prompt)
# 실행기(AgentExecutor)로 감싸 실제 실행 가능하게 구성
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
5. FastAPI
외부에서 API 를 호출할 수 있도록 FastAPI 로 API 를 구현한다.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# API 구현
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
class Query(BaseModel):
input: str
@app.post("/chat")
async def chat(query: Query):
result = executor.invoke({"input": query.input})
return {"answer": result["output"]}
6. 챗봇 UI
질문을 입력하고 답변을 확인할 수 있도록 챗봇 UI 를 간단하게 구현한다.
frontend/index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>LangChain FAQ 챗봇</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="chat-container">
<div id="chat-box" class="chat-box"></div>
<form id="chat-form" class="chat-form">
<input type="text" id="user-input" placeholder="질문을 입력하세요..." autocomplete="off" required />
<button type="submit">전송</button>
</form>
</div>
<script src="script.js"></script>
</body>
</html>
frontend/style.css
body {
font-family: Arial, sans-serif;
background: #f4f4f9;
display: flex;
justify-content: center;
padding: 50px 0;
}
.chat-container {
width: 400px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
}
.chat-box {
max-height: 500px;
padding: 20px;
overflow-y: auto;
}
.message {
margin-bottom: 15px;
}
.user {
text-align: right;
}
.message.user .text {
background: #e0f7fa;
text-align: right;
}
.message.bot .text {
background: #eceff1;
}
.text {
display: inline-block;
padding: 10px 10px;
border-radius: 20px;
max-width: 80%;
}
.chat-form {
display: flex;
border-top: 1px solid #ddd;
}
#user-input {
flex: 1;
padding: 10px;
border: none;
}
.chat-form button {
padding: 0 20px;
border: none;
background: #0288d1;
color: white;
cursor: pointer;
}
frontend/script.js
const form = document.getElementById('chat-form');
const input = document.getElementById('user-input');
const chatBox = document.getElementById('chat-box');
form.addEventListener('submit', async e => {
e.preventDefault();
const query = input.value.trim();
if (!query) return;
appendMessage('user', query);
input.value = '';
try {
const res = await fetch('http://localhost:8000/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: query })
});
const data = await res.json();
appendMessage('bot', data.answer);
} catch (err) {
appendMessage('bot', '오류가 발생했습니다.');
console.error(err);
}
});
function appendMessage(sender, text) {
const msg = document.createElement('div');
msg.className = `message ${sender}`;
const bubble = document.createElement('div');
bubble.className = 'text';
bubble.innerHTML = text;
msg.appendChild(bubble);
chatBox.appendChild(msg);
chatBox.scrollTop = chatBox.scrollHeight;
}
7. 실행
backend 폴더에서 백엔드 서버 실행
uvicorn main:app --reload
frontend 폴더에서 프론트엔드 실행
python3 -m http.server 5500
백엔드와 프론트엔드를 모두 실행했다면 http://localhost:5500 접속 후 테스트가 가능하다.

이번 작업에서는 AI FAQ 챗봇을 위한 문서를 실제로 작성하고, LLM이 잘 이해하고 처리할 수 있도록 문서 구조를 정비해보았다.
특히 중요한 점은 단순히 사람이 읽기 좋은 문서가 아니라, 질문-답변 쌍 단위로 나뉘고, 카테고리 정보까지 포함된 구조로 정리했다는 점이다.
이를 통해 다음과 같은 효과를 기대할 수 있다:
- 문서 기반 챗봇의 검색 정확도를 높일 수 있다.
- 관련 질문에만 집중된 응답이 가능하다.
- 유지보수나 데이터 확장 측면에서도 유리한 구조를 갖출 수 있다.
또한, 정규 표현식을 활용해 문서에서 카테고리 / 질문 / 답변을 자동으로 추출하고, LangChain의 Document 객체로 변환해 벡터 저장소에 바로 삽입할 수 있는 형태로 정리해두었다.
FAQ 챗봇은 단순해 보이지만, 구조화된 문서 설계와 데이터 정제가 잘 되어 있어야 정확하고 유용한 응답을 생성할 수 있다.
이번 글을 끝으로 FAQ 챗봇 구현을 마무리하겠다.
'DevLog > LangChain' 카테고리의 다른 글
LangChain #4 – LangChain 도구(tool)란 무엇인가? (1) | 2025.04.15 |
---|---|
LangChain #3 – 문서 기반 챗봇 구조 이해와 RAG 실습 (0) | 2025.04.15 |
LangChain #2 - 개발 환경 세팅 + LLM + 문서 기반 답변 (1) | 2025.04.12 |
LangChain #1 - 어디에 쓰이는 물건인가 (0) | 2025.04.11 |