DevLog/LangChain

LangChain #5 – 챗봇 구현

archive-log 2025. 4. 17. 19:30

이전 글 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. 로그인 후 마이페이지 > 회원정보 수정에서 주소, 연락처 등 정보를 변경할 수 있습니다.
abc_faq.md
0.00MB

 
벡터 저장소 구축

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 챗봇 구현을 마무리하겠다.