본문 바로가기

콩's AI

📄 사내 문서 Q&A 챗봇, 파이썬으로 구축 (feat. Gemini) - 1단계

반응형

 

📄 사내 문서 Q&A 봇, 파이썬으로 10분 컷! (feat. Gemini)

회사 생활하다 보면 "규정집 어디 있어요?" 혹은 "이거 절차가 어떻게 되나요?" 같은 질문을 정말 많이 받게 되죠. 매번 파일 찾아서 알려주기도 번거롭고, 문서 검색도 쉽지 않습니다.

그래서 오늘은 복잡한 프론트엔드 개발 없이, Python과 Gemini API만으로 빠르게 사내 문서 Q&A 봇을 만드는 방법을 정리해 봤습니다. IT 관리자분들이라면 프로토타입으로 딱 쓰기 좋은 구조입니다.

1. 프로토타입 기술 스택

거창한 서버나 DB 구축 없이, 로컬 환경에서 바로 돌려볼 수 있는 가장 가벼운 조합입니다.

  • Python 3.9 이상: 기본 언어 환경  / 본인의 경우 3.13.3 버전에서 정상 작동함.
  • LangChain: LLM과 데이터를 연결하는 RAG 파이프라인의 핵심 프레임워크
  • Streamlit: HTML/CSS 몰라도 파이썬 코드로 웹 UI를 뚝딱 만들어주는 도구
  • Google Gemini API: 가성비와 성능이 뛰어난 gemini-2.5-flash 모델과 임베딩 text-embedding-004 사용
  • ChromaDB: 별도 설치가 필요 없는 파일 저장 방식의 벡터 데이터베이스

 

2. 환경 설정 (라이브러리 설치)

먼저 터미널(CMD 또는 PowerShell)을 열고 필요한 패키지들을 설치해 주세요. 한 줄이면 끝납니다.

pip install langchain langchain-community langchain-core langchain-google-genai langchain-text-splitters chromadb pypdf

3. 전체 코드 (app.py)

아래 코드를 복사해서 app.py라는 이름으로 저장하세요. 이 코드는 최신 LCEL(LangChain Expression Language) 문법이 적용되어 있어 코드가 훨씬 간결하고 유지 보수가 쉽습니다.

import streamlit as st
import os
import tempfile
import base64
import fitz  # PyMuPDF
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.messages import HumanMessage
from langchain_core.documents import Document

# 로더 및 스플리터
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma

# 페이지 설정
st.set_page_config(page_title="기업 문서 하이브리드 분석 봇", layout="wide")
st.title("📄 기업 문서(IM/제안서) 하이브리드 분석 봇")

# --- [사이드바 설정] ---
with st.sidebar:
    st.header("⚙️ 설정")
    api_key = st.text_input("Google API Key", type="password")
    
    st.divider()
    
    # [핵심 기능] 분석 모드 선택 기능 추가
    st.subheader("🔍 분석 모드 선택")
    analysis_mode = st.radio(
        "문서 처리 방식을 선택하세요:",
        ("A. 텍스트 고속 분석 (기본)", "B. 이미지 정밀 분석 (멀티모달)"),
        index=0,
        help="A: 텍스트 위주 문서에 적합 (빠름)\nB: 차트, 표, 이미지가 많은 문서에 적합 (느림, 고품질)"
    )
    
    uploaded_file = st.file_uploader("PDF 파일 업로드", type=["pdf"])
    process_button = st.button("문서 분석 시작")

# --- [함수 1] 텍스트 모드 처리 (기존 방식) ---
def process_text_mode(file_path):
    loader = PyMuPDFLoader(file_path)
    docs = loader.load()
    
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=200
    )
    splits = text_splitter.split_documents(docs)
    return splits

# --- [함수 2] 이미지 모드 처리 (새로운 방식) ---
def process_image_mode(file_path, api_key):
    # PDF를 이미지로 변환하기 위해 PyMuPDF(fitz) 사용
    doc = fitz.open(file_path)
    text_descriptions = []
    
    # 이미지 분석을 위한 Vision 모델 호출 (Flash가 가성비 좋음)
    vision_model = ChatGoogleGenerativeAI(model="gemini-2.5-flash-preview-09-2025", google_api_key=api_key)
    
    total_pages = len(doc)
    progress_bar = st.progress(0)
    status_text = st.empty()
    
    for i, page in enumerate(doc):
        # 1. PDF 페이지를 이미지(PNG)로 변환
        pix = page.get_pixmap(dpi=150) # DPI 조절로 속도/품질 타협
        img_data = pix.tobytes("png")
        b64_data = base64.b64encode(img_data).decode("utf-8")
        
        # 2. Gemini에게 이미지를 보여주고 설명을 요청 (OCR + 해석)
        prompt_text = """
        이 문서는 기업 분석 보고서의 한 페이지입니다. 
        이 이미지를 자세히 분석해서 텍스트로 변환해주세요.
        
        [필수 포함 내용]
        1. 페이지에 있는 모든 텍스트 내용을 빠짐없이 추출하세요.
        2. 표(Table)가 있다면 마크다운(Markdown) 형식으로 정확하게 변환하세요.
        3. 차트나 그래프가 있다면 그 수치와 추세(Trend)를 텍스트로 설명하세요.
        4. 페이지 번호나 머리말/꼬리말은 제외하세요.
        """
        
        message = HumanMessage(
            content=[
                {"type": "text", "text": prompt_text},
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64_data}"}}
            ]
        )
        
        # 3. 분석 결과 받기
        status_text.text(f"📸 페이지 정밀 분석 중... ({i+1}/{total_pages})")
        response = vision_model.invoke([message])
        
        # 4. Document 객체로 변환 (RAG용)
        text_descriptions.append(Document(
            page_content=response.content,
            metadata={"page": i+1, "source": "image_analysis"}
        ))
        
        progress_bar.progress((i + 1) / total_pages)
        
    status_text.empty()
    progress_bar.empty()
    
    return text_descriptions

# --- [공통] 포맷팅 헬퍼 ---
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# --- [메인 로직] ---
if process_button and uploaded_file and api_key:
    os.environ["GOOGLE_API_KEY"] = api_key
    
    with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
        tmp_file.write(uploaded_file.getvalue())
        tmp_path = tmp_file.name

    try:
        splits = []
        
        # 사용자가 선택한 모드에 따라 분기 처리
        if analysis_mode == "A. 텍스트 고속 분석 (기본)":
            with st.spinner("📖 텍스트 추출 모드로 분석 중입니다..."):
                splits = process_text_mode(tmp_path)
                
        else: # B. 이미지 정밀 분석
            # 이미지 모드는 텍스트 스플리팅을 굳이 잘게 할 필요가 적음 (페이지 단위 해석이므로)
            # 하지만 검색 정확도를 위해 설명이 너무 길면 자름
            with st.spinner("👁️ 이미지 비전 모드로 분석 중입니다... (시간이 소요됩니다)"):
                raw_docs = process_image_mode(tmp_path, api_key)
                text_splitter = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=300)
                splits = text_splitter.split_documents(raw_docs)

        # 공통: 임베딩 및 저장
        with st.spinner("💾 분석 데이터를 저장소(Vector DB)에 적재 중..."):
            embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
            # 새로운 파일 업로드 시 DB 초기화 (프로토타입용)
            st.session_state.vectorstore = Chroma.from_documents(documents=splits, embedding=embeddings)
            
        st.success(f"✅ 학습 완료! ({analysis_mode}) 질문을 입력하세요.")
        
    except Exception as e:
        st.error(f"오류 발생: {e}")

# --- [질의응답 인터페이스] ---
if "vectorstore" in st.session_state:
    st.divider()
    question = st.text_input("💬 질문을 입력하세요 (예: 이 문서의 핵심 투자 포인트는?)")
    
    if question:
        llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash-preview-09-2025", temperature=0)
        
        # 검색 범위 설정 (이미지 모드는 정보 밀도가 높으므로 k를 적절히 조절)
        k_val = 10 if "이미지" in analysis_mode else 50

        retriever = st.session_state.vectorstore.as_retriever(
            search_type="similarity",
            search_kwargs={"k": k_val}
        )
        
        template = """당신은 유능한 비즈니스 분석가입니다.
        아래 [참고 문서]는 PDF 문서를 분석하여 추출된 내용입니다.
        특히 표나 차트에 대한 설명이 포함되어 있을 수 있습니다.
        이를 바탕으로 질문에 명확하게 답변해 주세요.
        
        [참고 문서]:
        {context}
        
        질문: {question}
        """
        prompt = ChatPromptTemplate.from_template(template)
        
        rag_chain = (
            {"context": retriever | format_docs, "question": RunnablePassthrough()}
            | prompt
            | llm
            | StrOutputParser()
        )
        
        with st.spinner("🤖 답변 생성 중..."):
            response = rag_chain.invoke(question)
            st.markdown("### 💡 답변")
            st.write(response)
            
            with st.expander("참고한 문서 조각(Source) 보기"):
                docs = retriever.invoke(question)
                for i, doc in enumerate(docs):
                    st.caption(f"Source {i+1} (Page {doc.metadata.get('page', '?')})")
                    st.text(doc.page_content[:300] + "...")

4. 실행 방법 및 체크 포인트

코드가 준비되었다면 터미널에서 아래 명령어를 입력해 보세요. 브라우저가 자동으로 열립니다.

streamlit run app.py

💡 IT 관리자 체크 포인트

이 프로토타입을 테스트할 때 다음 세 가지를 중점적으로 확인해 보세요.

  • 답변 속도: Gemini 2.5 Flash 모델이 사내망 환경에서도 충분히 빠른 응답 속도를 보여주는지 확인이 필요합니다.
  • 청크(Chunk) 크기: 코드의 chunk_size=2000 설정이 우리 회사 문서 스타일에 맞는지 보세요. 문맥이 잘리면 이 숫자를 조절해야 합니다.
  • 환각(Hallucination) 여부: 엉뚱한 문서를 보고 답하지 않는지, 문서에 없는 내용은 "모르겠습니다"라고 정확히 말하는지 테스트해 봐야 합니다.

이제 복잡한 구축 과정 없이, 가벼운 프로토타입으로 사내 봇 도입 가능성을 빠르게 검증해 보세요!

반응형

⚠️ 광고 차단 프로그램 감지

애드블록, 유니콘 등 광고 차단 확장 프로그램을 해제하거나
화이트리스트에 추가해주세요.