LLM 앱 개발 실전 #13 실전 프로젝트: 사내 문서 Q&A 봇

4 분 소요

마지막 글입니다. 지금까지 다룬 조각들을 하나로 묶어, 사내 문서에 답하는 Q&A 봇을 만들어 보겠습니다. 회사 규정이나 제품 매뉴얼을 넣어 두면, 직원이 자연어로 묻고 문서에 근거한 답을 받을 수 있는 앱입니다. 이 한 앱에 시리즈의 핵심이 거의 다 들어갑니다.

무엇을 묶는가 #

이 봇은 앞서 만든 조각들의 조합입니다.

  • 검색 — 문서를 조각으로 쪼개 임베딩하고, 질문과 가까운 조각을 찾습니다(7편, 8편).
  • 근거 답변 — 찾은 조각만 근거로 답하게 하고, 없으면 모른다고 답하게 합니다(4편).
  • 스트리밍 — 답을 실시간으로 흘려보냅니다(3편).
  • 대화 메모리 — 이어지는 질문을 위해 히스토리를 유지합니다(2편, 9편).

준비: 문서를 인덱싱하기 #

먼저 문서를 조각으로 쪼개 임베딩해 둡니다. 이 인덱싱은 문서가 바뀔 때만 하면 되고, 질문할 때마다 다시 하지 않습니다.

qa_index.py
import numpy as np
from sentence_transformers import SentenceTransformer

embedder = SentenceTransformer("all-MiniLM-L6-v2")

def chunk_text(text: str, size: int = 300, overlap: int = 50):
    chunks, start = [], 0
    while start < len(text):
        chunks.append(text[start:start + size])
        start += size - overlap
    return chunks

# 사내 문서들을 조각으로 쪼개 임베딩한다
documents = [open(f).read() for f in ["policy.txt", "manual.txt"]]
chunks = [c for doc in documents for c in chunk_text(doc)]
chunk_vectors = embedder.encode(chunks)

def retrieve(query: str, top_k: int = 3):
    q = embedder.encode([query])[0]
    scores = chunk_vectors @ q
    ranked = np.argsort(scores)[::-1][:top_k]
    return [chunks[i] for i in ranked]

봇 만들기 #

이제 검색, 근거 프롬프트, 스트리밍, 메모리를 하나의 함수에 묶습니다.

qa_bot.py
import anthropic

client = anthropic.Anthropic()
messages = []  # 대화 메모리

SYSTEM = "너는 사내 문서를 안내하는 도우미야. 제공된 자료만 근거로 답하고, 자료에 없으면 '자료에서 찾을 수 없습니다'라고 답해."

def ask(question: str) -> None:
    # 1) 검색: 질문과 관련된 조각을 찾는다
    found = retrieve(question)
    context = "\n\n".join(f"<doc>{c}</doc>" for c in found)

    # 2) 근거 프롬프트: 자료와 질문을 함께 담는다
    user_content = f"<자료>\n{context}\n</자료>\n\n질문: {question}"
    messages.append({"role": "user", "content": user_content})

    # 3) 스트리밍: 답을 실시간으로 출력하며 모은다
    answer = ""
    with client.messages.stream(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        system=SYSTEM,
        messages=messages,
    ) as stream:
        for text in stream.text_stream:
            print(text, end="", flush=True)
            answer += text
    print()

    # 4) 메모리: 답을 히스토리에 더한다
    messages.append({"role": "assistant", "content": answer})

# 대화형으로 사용
ask("연차는 며칠인가요?")
ask("그걸 반차로 나눠 쓸 수 있나요?")  # 이전 맥락을 기억해 이어서 답한다

흐름을 따라가 보겠습니다. 질문이 들어오면 먼저 관련 조각을 검색하고(1), 그 자료와 질문을 함께 프롬프트에 담습니다(2). system 프롬프트로 “자료만 근거로, 없으면 모른다"는 규칙을 고정해 환각을 막습니다. 답은 스트리밍으로 실시간 출력하면서 동시에 모으고(3), 끝나면 히스토리에 더해 다음 질문이 맥락을 잇게 합니다(4). 두 번째 질문 “그걸 반차로"의 “그걸"이 연차를 가리킨다는 걸 봇이 알 수 있는 이유는 이 메모리 덕분입니다.

여기서 더 나아가려면 #

이 봇은 핵심 구조를 모두 갖췄지만, 실제 서비스로 가려면 앞서 다룬 것들을 더 붙입니다.

  • 비용 — system과 자료가 반복되면 프롬프트 캐싱으로 비용을 줄입니다(12편). 대화가 길어지면 히스토리를 요약해 압축합니다(9편).
  • 규모 — 문서가 많아지면 전체 비교 대신 벡터 데이터베이스를 씁니다(7편).
  • 품질 — 질문과 기대 답의 평가셋으로 프롬프트 변경의 효과를 측정합니다(12편).
  • — 콘솔 출력 대신 StreamingResponse로 브라우저까지 답을 흘려보냅니다(3편).
  • 행동 확장 — 문서 검색을 넘어 외부 시스템을 조작해야 한다면 툴 콜링과 에이전트로 넓힙니다(6편, 10편).

시리즈를 마치며 #

처음 시작할 때는 API 키 하나로 한 문장을 주고받는 것이 전부였습니다. 거기서 메시지 구조와 파라미터, 스트리밍, 프롬프트, 구조화된 출력으로 기초를 다졌고, 툴 콜링과 RAG, 메모리, 에이전트, MCP로 앱이 바깥 세상과 연결되게 했으며, 비용과 평가와 관측으로 운영의 토대를 갖췄습니다. 그리고 이번 글에서 그 모두를 하나의 동작하는 앱으로 묶었습니다.

LLM 앱 개발의 큰 그림은 사실 단순합니다. 좋은 맥락을 모아 프롬프트에 담고, 모델의 답을 받아 코드에 안전하게 잇는 것. 검색도, 메모리도, 구조화된 출력도 결국 이 큰 흐름의 한 부분이었습니다. 이 시리즈에서 익힌 조각들을 조합하면, 여러분의 문제에 맞는 LLM 앱을 직접 설계할 수 있습니다.

이 시리즈는 Claude를 기준으로 설명했지만, 다뤄 온 개념은 대부분 다른 LLM 제공자에도 그대로 적용할 수 있습니다. 이제 만들고 싶은 앱을 정하고, 가장 작은 형태부터 한 조각씩 붙여 나가 보세요. 그동안 함께해 주셔서 감사합니다.

X