사내 AI 활용 가이드

문서 챗봇 만들기
LangChain과 FastAPI

LangChain FastAPI 구축
연구소 기술노트 · 2026년 8월 · 읽는 데 약 16분
4/5 시리즈 사내 문서 AI · 기초 — 심화

이 글의 목표 — ① 흩어진 부품을 조립하는 프레임워크 ② 화면과 연결되는 API 서버 만들기 ③ 사내에서 쓸 수 있는 모델 고르기 ④ 챗봇이 지어내지 않게 하는 방법 ⑤ 운영하면서 개선하는 구조를 이해합니다.

지금까지 필요한 개념은 모두 나왔습니다. 정리하면 이렇습니다.

문서 로드 → 청킹 → 임베딩 → 저장 → 검색 → 리랭킹 → 프롬프트 조합 → LLM 호출

이것을 전부 직접 만들면 수백 줄이 됩니다. 다행히 대부분은 이미 만들어진 도구가 있습니다. 이번 편은 그 도구들을 어떻게 엮는가에 관한 내용입니다.

시작 전 안내 (보안)

이 글에는 직접 따라 해 보실 수 있는 코드가 들어 있습니다. 실습하실 때 회사 문서·도면·소스코드·개인정보가 포함된 자료를 외부 AI 서비스나 상용 API에 입력하지 마세요. 예시 코드에 등장하는 외부 API는 동작 원리를 설명하기 위한 것이며, 사내 자료에는 사용할 수 없습니다.

연습하실 때는 뉴스 기사나 공개된 매뉴얼처럼 공개 자료를 쓰시고, 실제 사내 자료를 다룰 때는 이 글 4장에서 설명하는 사내 서버 설치 방식으로 구성해 주시기 바랍니다.

1. LangChain — 부품을 조립하는 도구

LangChain은 LLM을 사용하는 프로그램을 만들 때 반복적으로 필요한 것들을 미리 만들어 놓은 도구 모음입니다. 오픈소스이며 무료입니다.

레고에 비유하면 이해가 쉽습니다. 개별 블록에 해당하는 것이 문서 로더, 청킹 도구, 임베딩 연결, 데이터베이스 연결이고, 이 블록들을 이어 붙여 완성품을 만듭니다. 블록을 갈아 끼우기도 쉽습니다. 데이터베이스를 바꾸고 싶으면 그 부분 한 줄만 바꾸면 됩니다.

단계LangChain 도구하는 일
문서 로드PyPDFLoader, WebBaseLoaderPDF·웹·CSV에서 글자를 뽑아냅니다
청킹RecursiveCharacterTextSplitter2편에서 설명한 재귀적 청킹입니다
청킹CharacterTextSplitter고정 크기 청킹입니다
청킹SemanticChunker시맨틱 청킹입니다
임베딩OpenAIEmbeddings, HuggingFaceEmbeddings글자를 벡터로 바꿉니다
저장Chroma, FAISS, Milvus벡터와 원문을 저장합니다
검색·답변RetrievalQA검색부터 LLM 호출까지 한 번에 처리합니다

2편에서 다룬 재귀적 청킹을 직접 구현할 필요가 없다는 점을 확인하실 수 있습니다. RecursiveCharacterTextSplitter가 바로 그 알고리즘이며, 설정 두 줄이면 동작합니다.

다른 선택지

도구강점고르는 기준
LangChain연동 가능한 대상이 가장 많고 자료가 풍부합니다일반적인 문서 챗봇
LlamaIndex복잡한 문서 구조를 잘 다룹니다대용량 문서, 복잡한 검색
LangGraph조건 분기와 반복을 표현하기 좋습니다여러 갈래로 나뉘는 복잡한 흐름
Haystack운영 환경에서의 안정성이 검증되어 있습니다기업 내부 검색 시스템

처음 만드신다면 자료가 가장 많은 LangChain으로 시작하시기를 권합니다.

2. 열 줄로 만드는 RAG

실제 코드를 보겠습니다. 프로그래밍을 모르셔도 어느 줄이 어느 단계에 해당하는지만 보시면 충분합니다.

준비 단계 — 문서를 넣어 두기

from langchain.document_loaders import PyPDFLoader
from langchain.text_splitter  import RecursiveCharacterTextSplitter
from langchain.embeddings     import HuggingFaceEmbeddings
from langchain.vectorstores   import Chroma

# ① 문서 로드
문서 = PyPDFLoader("이용안내.pdf").load()

# ② 청킹 — 2편에서 설명한 재귀적 청킹
splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 조각 최대 크기
    chunk_overlap=50)      # 겹치는 분량
조각들 = splitter.split_documents(문서)

# ③ 임베딩 + ④ 저장 — 이 한 줄로 둘 다 처리됩니다
db = Chroma.from_documents(
    documents=조각들,
    embedding=HuggingFaceEmbeddings(model_name="..."),
    persist_directory="./db")

마지막 한 줄이 조각 하나하나를 임베딩 모델에 통과시키고, 나온 벡터를 원문과 함께 저장합니다. 2편에서 설명한 여섯 단계가 전부 이 안에서 일어납니다.

질문 단계 — 답변 받기

from langchain.chains import RetrievalQA

chain = RetrievalQA.from_chain_type(
    llm=llm,
    retriever=db.as_retriever(search_kwargs={"k": 5}),   # 상위 5개 검색
    return_source_documents=True)                        # 출처도 함께 받기

결과 = chain({"query": "이용 수수료가 어떻게 되나요?"})

print(결과["result"])              # 답변 문장
print(결과["source_documents"])    # 근거가 된 조각들

여기까지가 전부입니다. 직접 만들면 수백 줄이 걸릴 작업이 열 줄 남짓으로 끝납니다.

다만 이 기본 구성에는 3편에서 다룬 하이브리드 검색과 리랭킹이 빠져 있습니다. 처음에는 이렇게 시작해서 동작을 확인한 뒤, 정확도가 부족하면 그때 붙이는 순서를 권합니다.

3. FastAPI로 서비스 만들기

파이프라인이 완성되면 사용자가 쓸 수 있도록 서버로 감싸야 합니다. 웹 화면이나 사내 메신저가 이 서버에 질문을 보내고 답변을 받아 갑니다.

파이썬에서는 FastAPI가 사실상 표준입니다.

특징왜 유용한가
동시 처리한 사람의 답변을 기다리는 동안 다른 사람의 요청을 처리합니다
자동 문서API 사용법 문서가 자동으로 만들어집니다
입력 검증잘못된 형식의 요청을 자동으로 걸러 냅니다
실시간 전송답변을 한 글자씩 내보낼 수 있습니다
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel

app = FastAPI()

class 질문요청(BaseModel):
    query: str
    session_id: str

@app.post("/chat")
async def chat(req: 질문요청):
    경로 = await 라우터(req.query)                # 3편의 라우터

    if 경로 == "문서검색":
        결과 = await 하이브리드검색(req.query)     # 3편
        결과 = 리랭킹(req.query, 결과)             # 3편
    elif 경로 == "함수실행":
        결과 = await 함수호출(req.query)           # 3편

    return StreamingResponse(
        답변생성(req.query, 결과),
        media_type="text/event-stream")

3편에서 다룬 라우터, 하이브리드 검색, 리랭킹이 어디에 들어가는지 보이실 것입니다. 개념을 이해하고 나면 조립 자체는 단순합니다.

한 글자씩 내보내기

LLM이 답변을 다 만들 때까지 기다리면 몇 초 동안 빈 화면을 보게 됩니다. 사용자는 고장 났다고 생각합니다.

생성되는 대로 즉시 내보내면 글자가 하나씩 나타나므로 체감 속도가 크게 달라집니다. 실제 소요 시간은 같은데도 그렇습니다. ChatGPT에서 답변이 타이핑되듯 나오는 것이 바로 이 방식입니다.

async def 답변생성(질문, 참고문서):
    프롬프트 = 프롬프트조합(질문, 참고문서)
    async for 조각 in llm.astream(프롬프트):
        yield f"data: {조각}\n\n"      # 생성되는 대로 즉시 전송
    yield "data: [DONE]\n\n"
// 화면 쪽 코드
const source = new EventSource('/chat');
source.onmessage = (event) => {
  if (event.data === '[DONE]') { source.close(); return; }
  chatBox.textContent += event.data;    // 받는 대로 화면에 추가
};

4. 어떤 LLM을 쓸 것인가

1편의 보안 안내를 다시 떠올려 보겠습니다. 내부 자료를 다루는 시스템에서는 외부 API를 쓸 수 없습니다. 질문과 문서가 함께 외부로 전송되기 때문입니다.

따라서 사내 서버에 설치해서 쓸 수 있는 모델을 골라야 합니다. 다행히 성능이 충분한 공개 모델이 여럿 있습니다.

모델크기한 번에 읽는 양라이선스강점
EXAONE 3.52.4B / 7.8B32K 토큰비상업 무료, 상업은 협의한국어 문서 이해
Qwen2.57B~72B128K 토큰상업 이용 가능한국어·영어 혼용, 복잡한 추론

모델 크기 뒤의 B는 파라미터가 몇 십억 개인지를 뜻합니다. 1편에서 설명한 가중치의 개수입니다. 클수록 똑똑하지만 그만큼 큰 장비가 필요합니다.

모델필요한 GPU 메모리
EXAONE-3.5-2.4B24GB 1장
EXAONE-3.5-7.8B40GB 1장
Qwen2.5-7B24GB 1장
Qwen2.5-72B80GB 2장

예산과 장비가 제한적이라면 7B 안팎의 모델이 현실적인 선택입니다. 사내 안내 챗봇 용도로는 충분한 성능이 나옵니다.

모델을 서버로 띄우기

모델 파일을 받았다고 바로 쓸 수 있는 것은 아닙니다. 요청을 받아 처리하는 서버로 띄워야 합니다. 이때 쓰는 것이 vLLM입니다.

vLLM은 여러 사람의 요청을 묶어서 한 번에 처리하고, GPU 메모리를 효율적으로 나눠 씁니다. 그냥 모델을 돌리는 것보다 훨씬 많은 사람을 감당할 수 있습니다.

$ python -m vllm.entrypoints.openai.api_server \
    --model LGAI-EXAONE/EXAONE-3.5-7.8B-Instruct \
    --port 8000 \
    --tensor-parallel-size 2        # GPU 2장 사용

편리한 점이 하나 있습니다. vLLM은 OpenAI와 똑같은 형식의 API를 제공합니다. 그래서 예제 코드에서 주소만 바꾸면 그대로 동작합니다.

from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1")   # 사내 서버 주소

공개 자료로 연습할 때는 외부 API로 만들어 보고, 사내 자료를 붙일 때 이 한 줄만 바꾸는 방식으로 진행하실 수 있습니다.

5. 시스템 프롬프트 — 챗봇의 행동 규칙

같은 모델이라도 어떤 지시를 미리 넣어 두느냐에 따라 완전히 다르게 행동합니다. 이 사전 지시를 시스템 프롬프트라고 합니다.

사내 안내 챗봇에서 가장 중요한 규칙은 모르면 모른다고 답하게 하는 것입니다. 아무 지시가 없으면 LLM은 1편에서 설명한 습성대로 그럴듯하게 지어냅니다.

시스템_프롬프트 = """
당신은 사내 문서 안내 AI입니다.

## 필수 규칙
1. 출처 명시 — 답변 마지막에 [출처: 문서명, 쪽] 형식으로 반드시 표기합니다.
2. 근거 없으면 답하지 않음 — 아래 참고 문서에 없는 내용은
   "해당 정보를 찾을 수 없습니다. 담당자에게 문의해 주세요."로만 답합니다.
3. 개인정보 조회 불가 — 개인별 신청 현황이나 심의 결과는 답하지 않습니다.
4. 실시간 정보 제외 — 처리 현황, 대기 순번 등은 답하지 않습니다.

## 참고 문서
{검색된_조각들}

## 사용자 질문
{질문}
"""

2번 규칙이 핵심입니다. 이 한 줄이 있고 없고에 따라 챗봇의 신뢰도가 완전히 달라집니다.

원하는 답변 형식이 있다면 예시를 두세 개 넣어 주는 것이 효과적입니다. 설명하는 것보다 보여 주는 편이 잘 통합니다.

지어내지 않게 하는 네 가지 장치

장치방법
출처 강제프롬프트에 출처 표기를 의무로 명시합니다
모른다고 말하기 허용"모르면 모른다고 하라"를 명시적으로 적습니다
점수로 걸러내기3편의 리랭킹 점수가 낮으면 아예 답변하지 않게 합니다
기록 남기기답변과 근거 문서가 어긋나는 경우를 기록해 둡니다

세 번째가 특히 효과가 큽니다. 애초에 관련 없는 문서가 전달되지 않으면 지어낼 재료 자체가 없어집니다.

6. 문서가 바뀌면 다시 반영하기

매뉴얼이 개정되면 데이터베이스도 갱신해야 합니다. 그러지 않으면 챗봇이 옛날 규정을 근거로 자신 있게 답합니다. 겉보기에는 정상 동작하므로 발견하기도 어렵습니다.

방식설명적합한 경우
폴더 감시지정한 폴더에 파일이 바뀌면 자동 실행구성이 간단하고 사내망에 적합합니다
정기 실행매주 정해진 시각에 실행변경이 잦지 않은 경우
수동 실행관리자가 버튼을 눌러 실행즉시 반영이 필요한 경우
@app.post("/admin/reindex")
async def 다시반영(문서경로):
    문서   = PyPDFLoader(문서경로).load()
    조각들 = splitter.split_documents(문서)
    벡터들 = 임베딩모델.encode([c.page_content for c in 조각들])

    collection.upsert(...)     # 같은 문서의 기존 조각을 교체

마지막 줄에서 추가가 아니라 교체를 하는 점이 중요합니다. 옛 조각을 남겨 두면 새 규정과 옛 규정이 함께 검색되어, 어느 쪽이 나올지 알 수 없게 됩니다.

7. 완성된 구조와 운영 지표

시리즈에서 다룬 내용을 모두 합치면 다음과 같습니다.

[준비 단계 — 문서가 바뀔 때만]

  문서 ──▶ 청킹 ──▶ 임베딩 ──▶ 벡터 DB 저장
          (2편)     (2편)       (3편)


[질문 단계 — 질문할 때마다]

  질문 ──▶ 서버 수신 ──▶ 라우터
                          │
            ┌─────────────┴─────────────┐
            ▼                           ▼
      문서 검색 (3편)              함수 실행 (3편)
      하이브리드 검색                  계산 결과
      리랭킹 → 상위 5개                    │
            └─────────────┬─────────────┘
                          ▼
                   프롬프트 조합 (4편)
                          ▼
                   LLM 답변 생성
                          ▼
                   한 글자씩 화면 전송
구간역할사용 기술
준비문서 읽기LangChain 문서 로더
조각내기재귀적 청킹
저장Milvus 등 벡터 DB
검색의미 + 키워드 검색임베딩 모델 + BM25, RRF로 결합
순위 재정렬리랭커
분기질문 종류 판단키워드 규칙 + LLM
계산수수료·추천 등직접 작성한 함수
생성답변 문장 작성사내 설치 LLM
서비스요청 처리·전송FastAPI + vLLM

운영하며 지켜볼 것

지표목표무엇을 알려 주는가
답변 못 한 비율10% 미만문서에 빠진 내용이 얼마나 되는가
평균 응답 시간3초 미만검색·리랭킹·생성 전체 소요 시간
1위 정확도85% 이상가장 관련 있는 문서를 1위로 올렸는가
함수 실행 성공률95% 이상계산 요청을 제대로 처리했는가

가장 중요한 것은 답하지 못한 질문을 모으는 일입니다. 이것이 곧 다음에 보강해야 할 문서 목록이 됩니다.

async def 미답변_기록(질문, 사유):
    await db.insert("미답변_기록", {
        "질문": 질문, "사유": 사유, "시각": now()})

# 담당자가 기록을 확인 → 해당 내용을 문서에 추가
# → 다시반영 실행 → 즉시 답변 가능해짐

이 순환이 돌기 시작하면 챗봇은 쓸수록 좋아집니다. 모델을 바꾸거나 다시 학습시키지 않고도 성능이 올라가는 것, 이것이 1편에서 말씀드린 RAG 구조의 가장 큰 장점입니다.

8. 자주 나오는 질문

LangChain을 꼭 써야 하나요?

필수는 아닙니다. 각 단계를 직접 구현해도 되고, 실제로 구조가 확정된 뒤에는 걷어 내는 팀도 있습니다. 다만 처음 만들 때는 쓰는 편이 훨씬 빠릅니다. 여러 조합을 시험해 보기 좋고, 도구를 바꿔 끼우기도 쉽습니다.

GPU가 없으면 만들 수 없나요?

공부와 시험 삼아 만드는 것은 GPU 없이도 가능합니다. 가벼운 임베딩 모델은 일반 PC에서도 동작하고, LLM 부분만 외부 API로 대신하면 됩니다. 다만 사내 자료를 다루는 실제 운영에는 GPU가 필요합니다. 모델을 내부에 설치해야 하기 때문입니다.

7B 모델로 충분한가요?

문서 안내 챗봇 용도로는 대체로 충분합니다. RAG에서 LLM이 하는 일은 주어진 문서를 읽고 정리해서 문장으로 만드는 것이지, 스스로 지식을 꺼내는 것이 아니기 때문입니다. 어려운 일은 이미 검색과 리랭킹이 끝내 놓았습니다. 다만 여러 문서를 비교하거나 추론이 필요한 질문이 많다면 더 큰 모델을 검토해 보시기 바랍니다.

답변이 자꾸 틀립니다. 모델을 바꿔야 할까요?

대부분의 경우 모델 문제가 아닙니다. 순서를 권하자면 첫째, 근거로 사용된 문서를 직접 확인해 보세요. 엉뚱한 문서가 들어갔다면 검색 문제이고 3편을 보시면 됩니다. 둘째, 문서는 맞는데 답이 이상하다면 시스템 프롬프트를 손보세요. 모델 교체는 이 둘을 확인한 뒤에 고려할 일입니다.

문서를 몇 개나 넣을 수 있나요?

벡터 데이터베이스는 수백만 건을 다룰 수 있으므로 문서 수는 사실상 제약이 아닙니다. 오히려 문서가 많아질수록 비슷한 내용이 여러 문서에 흩어져 검색 정확도가 떨어지는 것이 문제입니다. 그래서 문서 종류별로 나눠 저장하고, 질문에 따라 필요한 범위만 검색하는 방식을 함께 씁니다.

만드는 데 얼마나 걸리나요?

동작하는 시제품은 며칠이면 만들 수 있습니다. 2장의 코드가 그 수준입니다. 그러나 실제로 쓸 만한 품질에 이르는 데는 그보다 오래 걸리며, 대부분의 시간은 문서를 정리하고 청킹 기준을 잡는 데 들어갑니다. 코드보다 자료 준비가 더 큰 일이라고 생각하시는 편이 정확합니다.

여기까지가 챗봇을 만드는 데 필요한 내용입니다. 다음 편은 애초에 컴퓨터가 어떻게 의미를 파악하는가를 다루는 심화 내용으로, 만드는 일과는 직접 관련이 없으니 관심 있는 분만 읽으셔도 됩니다.