13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] Haystack

  • [AI] Haystack으로 사내 지식 검색 챗봇 RAG 파이프라인 구축 사례

    [AI] Haystack으로 사내 지식 검색 챗봇 RAG 파이프라인 구축 사례

    [LLM 애플리케이션 개발] Haystack으로 사내 지식 검색 챗봇 구축 사례: RAG 파이프라인 개발기

    안녕하세요, 13년차 인프라 엔지니어입니다. 요즘 제가 홈랩에서 몰두하고 있는 프로젝트가 하나 있는데, 바로 Haystack을 활용한 사내 지식 검색 챗봇이거든요. 회사에서 일하다 보면 “그 정보 어디 있지?”라며 헤매는 시간이 정말 많지 않으신가요? 저도 처음엔 그랬습니다. 수많은 문서와 슬랙 채널을 뒤지며 시간을 낭비하는 게 비효율적이라고 늘 생각했었거든요.

    그러다 “이걸 LLM(Large Language Model, 거대 언어 모델)으로 해결할 수 있지 않을까?”라는 생각이 자꾸만 들었고, 결국 직접 만들어보기로 결심했습니다. 특히 RAG(Retrieval Augmented Generation, 검색 증강 생성) 기법을 쓰면 환각(Hallucination) 없이 정확한 답변을 얻을 수 있다는 점이 마음에 들었죠. 제가 선택한 도구는 Haystack입니다. 지금부터 제가 어떻게 이 프로젝트를 진행했는지, 그리고 어떤 삽질들을 겪었는지 자세히 풀어보겠습니다.

    사내 지식 검색 챗봇의 RAG 파이프라인 아키텍처 개념도입니다. 질문이 들어오면 Haystack이 문서를 뒤져서 관련성 높은 정보를 가져오고, 그걸 LLM에 넘겨서 답변을 만들어내는 흐름이죠.

    RAG와 Haystack: 왜 이 조합일까요?

    먼저 RAG와 Haystack이 정확히 뭔지 짚고 넘어갈게요. 이미 잘 아시는 분도 계시겠지만, 저도 처음엔 개념을 제대로 이해하는 데 시간이 걸렸거든요. LLM 애플리케이션 개발은 빠르게 진화하고 있어서, 기본기를 탄탄히 하는 게 정말 중요하더라고요.

    RAG (Retrieval Augmented Generation, 검색 증강 생성)

    LLM이 똑똑하긴 하지만, 학습 데이터 시점 이후의 정보나 우리 회사 같은 특정 도메인 지식은 알 수가 없습니다. 그리고 가끔은 환각(Hallucination), 즉 없는 이야기를 지어내기도 하죠. 이 문제를 해결하기 위해 나온 게 RAG입니다. 간단히 말해서, 질문이 들어오면 LLM이 바로 답변하는 게 아니라, 먼저 외부 지식 베이스(우리 회사 문서)에서 관련성 높은 정보를 ‘검색(Retrieval)’해서 가져온 다음, 그 정보를 ‘참고해서(Augmented)’ 답변을 ‘생성(Generation)’하는 방식입니다. 이렇게 하면 LLM이 최신 정보나 특정 도메인 지식에 기반한 정확한 답변을 할 수 있게 되더라고요.

    Haystack: RAG 파이프라인을 쉽게 구축하는 프레임워크

    RAG 파이프라인을 밑바닥부터 직접 구현하려면 생각보다 복잡합니다. 문서 로딩(Document Loading), 임베딩(Embedding), 검색기(Retriever), 생성기(Generator) 등 여러 컴포넌트를 일일이 연결해야 하거든요. Haystack은 이런 복잡한 과정을 쉽고 유연하게 만들어주는 프레임워크입니다. 다양한 DocumentStore(문서 저장소), Retriever(검색기), Generator(생성기) 컴포넌트를 모듈식으로 제공해서, 마치 레고 블록처럼 조립하듯이 파이프라인을 구축할 수 있어요. 파이썬 기반이라 저 같은 개발자에게는 접근성도 정말 좋습니다.

    실전 구현: Haystack RAG 파이프라인 만들기

    이제 제가 어떻게 사내 지식 검색 챗봇을 만들었는지 단계별로 보여드리겠습니다. 홈랩에서 직접 해보면서 시행착오도 많았는데, 핵심만 쏙쏙 뽑아서 공유해드릴게요.

    1. 환경 구성 및 Haystack 설치

    가장 먼저 개발 환경을 설정하고 Haystack을 설치해야겠죠. 저는 Python 가상 환경을 사용해서 의존성 충돌을 방지합니다. 깔끔하게 시작해야 나중에 트러블슈팅이 쉬워거든요.

    
    # 가상 환경 생성 및 활성화
    python3 -m venv haystack_env
    source haystack_env/bin/activate
    
    # Haystack 및 필요한 라이브러리 설치
    # PDF 문서를 처리할 예정이므로 pypdf도 설치합니다.
    pip install farm-haystack[pdf,faiss]
    # LLM 연동을 위해 OpenAI 라이브러리를 설치합니다.
    # 저는 테스트용으로 OpenAI API를 사용했습니다. (실제 운영 시에는 보안 고려 필수!)
    pip install openai
    

    참고로 <code>farm-haystack[pdf,faiss]처럼 대괄호 안에 옵션을 넣으면 필요한 컴포넌트들을 한 번에 설치할 수 있어요. FAISS(Facebook AI Similarity Search)를 사용해 빠른 검색을 구현했고, 나중에 스케일 아웃을 고려해서 Elasticsearch도 고려했습니다.

    2. 사내 문서 로딩 및 인덱싱

    이제 챗봇이 참고할 지식 베이스를 구축해야 합니다. 회사 내부 문서(회의록, 기술 문서, FAQ 등)를 Haystack이 이해할 수 있는 형태로 변환하고 저장하는 과정이죠. 저는 주로 PDF나 텍스트 파일 형태의 문서를 사용했습니다. 여기서 중요한 건 문서 청킹(Document Chunking) 전략입니다. 너무 길면 LLM의 컨텍스트 윈도우(Context Window)를 초과하고, 너무 짧으면 문맥을 놓칠 수 있거든요. 이 부분에서 저도 삽질 좀 했습니다. 처음엔 문서를 통째로 넣었다가 “너무 길어요”라는 LLM의 반응에 깜짝 놀랐죠. ㅎㅎ

    
    import os
    from haystack.document_stores import FAISSDocumentStore
    from haystack.nodes import TextConverter, PreProcessor
    
    # 문서 저장소 초기화 (FAISS 사용)
    # FAISS는 메모리 기반으로 빠르게 유사성 검색을 할 수 있게 해줍니다.
    document_store = FAISSDocumentStore()
    
    # 문서 변환 및 전처리 설정
    text_converter = TextConverter(remove_numeric_tables=True, valid_languages=["ko"])
    preprocessor = PreProcessor(
        clean_empty_lines=True,
        clean_whitespace=True,
        split_by="word",
        split_length=500,
        split_overlap=50,
        split_respect_sentence_boundary=True,
        language="ko"
    )
    
    # 문서 로딩
    from pathlib import Path
    DOC_DIR = "data"
    if not os.path.exists(DOC_DIR):
        os.makedirs(DOC_DIR)
    
    # PDF 파일들을 로드하고 전처리합니다.
    doc_files = list(Path(DOC_DIR).glob("*.pdf"))
    for file in doc_files:
        docs = text_converter.run(file_paths=[str(file)])["documents"]
        processed_docs = preprocessor.run(documents=docs)["documents"]
        document_store.write_documents(processed_docs)
    
    print(f"총 {document_store.get_document_count()}개의 문서(청크)가 인덱싱되었습니다.")
    

    여기서 중요한 건 PreProcessor의 split_length와 split_overlap 파라미터예요. 이 값을 어떻게 설정하느냐에 따라 검색 결과의 품질이 크게 달라지더라고요. 회사 문서의 스타일이나 평균 문장 길이를 고려해서 여러 번 테스트하고 최적값을 찾는 게 중요합니다. 저도 이 부분에서 여러 번 값을 조절하며 시행착오를 겪었습니다.

    Haystack 문서 인덱싱 과정을 시각화한 다이어그램

    Haystack에서 문서들을 로딩하고 인덱싱하는 과정입니다. 원본 문서가 텍스트로 변환되고, 의미 있는 조각(청크)으로 나뉘어 검색 가능한 형태로 저장되는 과정이죠.

    3. RAG 파이프라인 구축: 검색기와 생성기의 조립

    문서가 준비되었으니, 이제 질문이 들어왔을 때 관련 문서를 찾아주고 답변을 생성해주는 핵심 파이프라인을 만들 차례입니다. Haystack에서는 Pipeline 클래스를 사용해서 컴포넌트들을 연결해요. 저는 BM25Retriever와 PromptNode를 사용했습니다. BM25는 키워드 기반 검색에 강력하고, PromptNode는 LLM을 통합해 강력한 언어 생성 능력을 제공합니다.

    
    from haystack.nodes import BM25Retriever, PromptNode, PromptTemplate
    from haystack.pipelines import Pipeline
    import os
    
    # 1. Retriever (검색기) 설정
    # DocumentStore에서 가장 관련성 높은 문서를 찾아옵니다.
    retriever = BM25Retriever(document_store=document_store)
    
    # 2. Generator (생성기) 설정 - LLM 연동
    # PromptTemplate에 RAG를 위한 지시사항(프롬프트 엔지니어링)을 추가합니다.
    rag_prompt_template = PromptTemplate(
        prompt="""Given the following documents, answer the question in Korean.
        If you don't know the answer, just say that you don't know, don't make up an answer.
        Documents:
        {% for doc in documents %}
        {{ doc.content }}
        {% endfor %}
        Question: {{query}}
        Answer:"""
    )
    
    # PromptNode를 사용하여 LLM을 통합합니다.
    # model_name은 사용하려는 LLM 모델명입니다.
    # api_key는 OpenAI API 키를 환경 변수에서 가져옵니다.
    prompt_node = PromptNode(
        model_name_or_path="gpt-3.5-turbo",
        api_key=os.environ.get("OPENAI_API_KEY"),
        default_prompt_template=rag_prompt_template,
        max_length=500,
        model_kwargs={"temperature": 0.1}
    )
    
    # 3. RAG 파이프라인 조립
    rag_pipeline = Pipeline()
    rag_pipeline.add_node(component=retriever, name="Retriever", inputs=["Query"])
    rag_pipeline.add_node(component=prompt_node, name="PromptNode", inputs=["Retriever"])
    
    print("RAG 파이프라인이 준비되었습니다!")
    

    PromptTemplate이 정말 중요한데요. LLM이 가져온 문서를 바탕으로 어떤 답변을 해야 할지 지시하는 역할을 하거든요. “주어진 문서에서만 답변해라”, “모르면 모른다고 해라” 같은 지시를 명확히 주면 환각을 최소화할 수 있습니다. temperature 값도 중요한데, 낮을수록 정해진 사실에 기반한 답변을 유도합니다.

    ⚠️ 삽질 경험과 트러블슈팅: 제가 겪은 문제들

    이 과정을 진행하면서 겪었던 몇 가지 삽질과 해결책을 공유합니다. 여러분은 저처럼 같은 실수를 반복하지 않으시길 바라요!

    1. 문제: “Context window exceeded” 오류
      상황: 처음에는 PreProcessor에서 문서를 너무 크게 청킹하거나, 청킹을 아예 하지 않고 통째로 LLM에 넘기려 했습니다. 그랬더니 “Context window exceeded” 에러가 나더군요. LLM마다 한 번에 처리할 수 있는 토큰(단어) 수가 정해져 있는데, 이 한도를 초과한 거죠.
      해결: PreProcessor의 split_length와 split_overlap 파라미터를 조절했습니다. split_length를 LLM의 컨텍스트 윈도우 한계와 검색할 문서의 양을 고려해서 적절히 줄이고, split_overlap을 설정해 청크 간의 문맥 연결성을 유지했어요. 특히 split_respect_sentence_boundary=True 옵션을 주면 문장 중간에 잘리는 것을 방지할 수 있습니다.
    2. 문제: “관련 없는 문서 검색” 또는 “빈약한 답변”
      상황: 질문에 대한 답변이 엉뚱하거나, 문서에 분명히 내용이 있는데도 “모르겠습니다”라고 하는 경우가 있었습니다. 검색기(Retriever)가 관련성 높은 문서를 제대로 찾아오지 못했거나, 찾아온 문서가 충분히 상세하지 않았기 때문이었죠.
      해결:

      • 검색기 튜닝: BM25Retriever 외에 DensePassageRetriever(DPR)나 EmbeddingRetriever 같은 임베딩 기반 검색기도 시도해봤어요. 특히 DPR은 문맥을 더 잘 이해해서 유사성 높은 문서를 찾아주는 데 효과적이더라고요. 다만, 임베딩 모델 선택과 GPU 자원이 필요할 수 있습니다.
      • 프롬프트 엔지니어링 강화: PromptTemplate에 “주어진 문서 외의 정보는 사용하지 마세요”, “간결하게 답변해주세요” 등의 지시를 더 명확하게 추가했어요.
      • 문서 품질 개선: 결국 지식 베이스가 튼튼해야 합니다. 문서의 내용이 너무 두루뭉술하거나 핵심 정보가 부족하면 아무리 좋은 RAG 파이프라인이라도 좋은 답변을 내기 어렵거든요. 회사 내 문서들을 정리하고 표준화하는 작업도 병행했습니다.

    검증 및 결과: 드디어 챗봇과 대화하다!

    여러 번의 시행착오 끝에 드디어 만족스러운 답변을 내놓는 챗봇을 만들었어요! 파이프라인이 제대로 작동하는지 확인하는 순간은 정말 짜릿했습니다. 간단한 질문으로 테스트해볼 차례입니다.

    
    # 챗봇에 질문하기
    question = "2023년 하반기 사내 워크샵은 언제 어디서 개최되나요?"
    result = rag_pipeline.run(query=question, params={"Retriever": {"top_k": 3}})
    
    # 결과 출력
    print(f"질문: {question}\n")
    print(f"답변: {result['answers'][0].answer}\n")
    print("-" * 30)
    print("참조 문서:")
    for doc in result.get("documents", []):
        print(f"- {doc.meta.get('name', '제목 없음')} (ID: {doc.id[:8]}...)")
    

    결과를 보면, LLM이 단순히 질문에 답하는 것을 넘어, 어떤 문서에서 정보를 가져왔는지 참조 문서(Source Documents)까지 알려주더라고요. 이게 바로 RAG의 강력한 장점입니다. 사용자는 답변의 근거를 확인할 수 있어 신뢰도가 높아져요. 실제로 회사 회의록이나 프로젝트 보고서 같은 문서들을 넣어 테스트해보니, 담당자를 일일이 찾지 않아도 필요한 정보를 빠르게 얻을 수 있어서 업무 효율성이 크게 향상될 거 같다는 확신이 들었습니다.

    완성된 Haystack 기반 사내 지식 검색 챗봇 사용자 인터페이스 스크린샷

    제가 개발한 사내 지식 검색 챗봇의 실제 동작 화면입니다. 질문에 대한 명확한 답변과 함께 어떤 문서에서 정보를 가져왔는지 출처까지 알려줘서 신뢰할 수 있죠.

    Retriever 선택 가이드

    제가 여러 검색기를 사용해보면서 느낀 점을 바탕으로, 어떤 상황에서 어떤 검색기가 유리한지 정리해봤습니다. 여러분의 환경과 필요에 맞춰 선택하시면 됩니다.

    검색기 (Retriever) 장점 단점 추천 사용 사례
    BM25Retriever 설치 및 사용이 간편
    키워드 매칭에 강력
    적은 컴퓨팅 자원
    의미론적 유사성 파악 어려움
    동의어/유의어 처리 한계
    초기 프로토타입 개발
    명확한 키워드 기반 문서
    제한된 컴퓨팅 자원 환경
    EmbeddingRetriever (DPR 포함) 의미론적 유사성 검색 가능
    문맥 이해도가 높음
    질문 의도를 더 잘 파악
    임베딩 모델 필요 (추가 설치/학습)
    GPU 또는 고성능 CPU 요구
    초기 설정 복잡성
    고품질 검색 결과 요구
    의미가 복잡한 문서
    충분한 컴퓨팅 자원 보유

    저 같은 경우는 초기에는 BM25로 빠르게 시작하고, 성능 개선이 필요할 때 EmbeddingRetriever로 전환하는 전략을 사용했어요. 홈랩에서는 자원 제약이 있을 수 있으니, 이 표를 참고해서 현명한 선택을 하시길 바랍니다.

    마무리: RAG 챗봇, 가능성을 엿보다

    이번 Haystack 기반 RAG 파이프라인 개발은 저에게 LLM 애플리케이션 개발의 무한한 가능성을 보여준 값진 경험이었습니다. 처음엔 낯설고 복잡하게 느껴졌지만, 하나하나 직접 부딪히고 해결해나가면서 많은 걸 배울 수 있었어요. 특히 사내 지식 검색 챗봇은 업무 효율성을 혁신적으로 높일 수 있는 강력한 도구가 될 거라는 확신이 들었습니다. 물론 아직 개선할 부분은 많습니다. 사용자 인터페이스(UI)를 더 친숙하게 만들고, 검색 정확도를 높이기 위한 지속적인 모델 튜닝, 그리고 보안 강화 같은 과제들이 남아있거든요.

    만약 여러분도 회사 내부의 비효율적인 정보 탐색 문제로 고민하고 계신다면, 저처럼 Haystack과 RAG를 활용해 사내 지식 검색 챗봇 구축에 도전해보시길 강력히 추천합니다. 처음엔 삽질 좀 하겠지만, 그 과정에서 얻는 경험과 지식은 분명 여러분의 엔지니어링 역량을 한 단계 끌어올려 줄 겁니다. 다음 글에서는 이 Haystack 챗봇을 Kubernetes 환경에 배포하는 과정이나, 더 다양한 검색기를 통합하는 방법에 대해 다뤄볼 예정이니 기대해주세요!

    RAG 파이프라인과 Haystack의 주요 장점들을 요약한 인포그래픽

    RAG 파이프라인과 Haystack의 주요 장점들을 요약한 이미지입니다. LLM의 한계를 극복하고 효율적인 애플리케이션 개발을 가능하게 하죠.

  • [AI] Haystack 기반 AI 에이전트 구축, 실패 사례로 배우는 설계 함정

    [AI] Haystack 기반 AI 에이전트 구축, 실패 사례로 배우는 설계 함정

    안녕하세요, 13년차 서버실 지킴이, ’13년차의 서버실’ 블로그 주인장입니다. 오늘은 제가 최근 홈랩에서 Haystack 기반 AI 에이전트(AI Agent)를 구축하다가 겪었던 뼈아픈 실패 경험과 거기서 배운 설계 함정들에 대해 솔직하게 이야기해보려고 합니다. 😅

    요즘 LLM(Large Language Model)이 워낙 핫하잖아요? 저도 이 친구들을 그냥 채팅만 시킬 게 아니라, 좀 더 능동적으로 제 업무를 도와주는 ‘에이전트’로 만들어보고 싶다는 욕심이 생기더라고요. 그래서 오픈소스 프레임워크 중 하나인 Haystack을 선택해서 무작정 뛰어들었죠. 처음엔 “와, 이거 진짜 대박인데?” 싶었는데, 막상 실전에 들어가 보니 생각보다 만만치 않더군요. 삽질 좀 했습니다 ㅎㅎ

    혹시 여러분도 AI 에이전트 개발에 관심 있으신가요? 아니면 이미 도전 중이신데 뭔가 잘 안 풀리는 부분이 있으신가요? 제 경험담이 여러분의 시간과 노력을 아끼는 데 조금이나마 도움이 되기를 바랍니다. 💡

    Haystack 기반 AI 에이전트 아키텍처 다이어그램

    그림 1: Haystack 기반 AI 에이전트의 일반적인 아키텍처 개요.

    AI 에이전트, 그리고 Haystack (쉽게 말해~)

    먼저, AI 에이전트(AI Agent)가 정확히 뭔지부터 짚고 넘어갈까요? 쉽게 말해, 스스로 목표를 세우고, 필요한 도구(Tool)들을 활용해서 그 목표를 달성해 나가는 인공지능 시스템이라고 보시면 됩니다. 마치 비서처럼 “오늘 뉴스 요약해 줘”라고 하면, 에이전트가 알아서 뉴스 검색 도구를 쓰고, 내용을 요약해서 저에게 알려주는 식이죠. 🤖

    그럼 Haystack은 뭘까요? Haystack은 오픈소스 LLM 프레임워크(Open-source LLM Framework) 중 하나인데요, LLM 기반의 애플리케이션, 특히 검색 증강 생성(RAG, Retrieval Augmented Generation)이나 AI 에이전트 같은 복잡한 시스템을 쉽게 구축할 수 있도록 도와주는 도구들의 모음이라고 생각하면 돼요. 파이프라인(Pipelines), 문서 저장소(Document Stores), 생성기(Generators), 검색기(Retrievers) 등 다양한 컴포넌트(Component)들을 조합해서 원하는 기능을 만들 수 있게 되어 있거든요. 마치 레고 블록처럼요!

    제가 Haystack을 선택한 이유는 유연한 모듈 구조와 활발한 커뮤니티 덕분이었습니다. 다양한 LLM과 벡터 DB를 플러그인(Plug-in) 방식으로 연결할 수 있다는 점도 매력적이었고요. 🚀

    실전 구현, 그리고 기대와 현실의 괴리

    제가 처음 목표했던 건 ‘주어진 질문에 대해 최신 정보를 스스로 찾아 답변하고, 필요하면 특정 API를 호출해 액션을 취하는 에이전트’였습니다. 예를 들어, “오늘 날씨는 어때?”라고 물으면 날씨 API를 호출하고, “최근 AI 트렌드는?”이라고 물으면 웹 검색 후 요약해주는 식이었죠.

    기본적인 Haystack 에이전트 구성은 다음과 같았습니다.

    1. LLM 연결: OpenAI GPT-4나 로컬에서 돌리는 Llama 2 같은 대규모 언어 모델을 연결합니다.
    2. 도구(Tools) 정의: 웹 검색 도구, 날씨 API 호출 도구, 데이터베이스 조회 도구 등을 만듭니다. 각 도구는 특정 기능을 수행하는 함수로 구현됩니다.
    3. 에이전트 설정: LLM이 어떤 도구들을 사용할 수 있는지 알려주고, 어떤 방식으로 추론(Reasoning)하고 행동(Acting)할지 지시합니다.

    간단한 파이썬 코드로 이 구성의 뼈대를 잡을 수 있었죠. 대략 이런 느낌이랄까요?

    from haystack.agents import Agent, Tool
    from haystack.components.generators import OpenAIChatGenerator
    # ... 다른 컴포넌트 임포트
    
    # 예시 Tool 정의 (실제로는 더 복잡합니다)
    def web_search(query: str):
        """Performs a web search for the given query and returns relevant results."""
        print(f"DEBUG: Performing web search for: {query}")
        # 실제 웹 검색 로직 (e.g., Google Search API 호출)
        return f"Search results for '{query}': AI 에이전트 관련 최신 뉴스 요약..."
    
    def get_weather(location: str):
        """Retrieves the current weather for a specified location."""
        print(f"DEBUG: Getting weather for: {location}")
        # 실제 날씨 API 호출 로직
        return f"Current weather in {location}: 맑음, 25도"
    
    # Tool 인스턴스 생성
    web_search_tool = Tool(name="web_search", function=web_search, description="웹 검색을 수행하여 최신 정보를 찾습니다.")
    weather_tool = Tool(name="get_weather", function=get_weather, description="특정 지역의 현재 날씨 정보를 가져옵니다.")
    
    # LLM 생성기 설정 (예시)
    llm_generator = OpenAIChatGenerator(model="gpt-4", api_key="YOUR_API_KEY")
    
    # Agent 생성
    my_agent = Agent(llm=llm_generator, tools=[web_search_tool, weather_tool])
    
    # 에이전트 실행 (이 부분부터 삽질이 시작됩니다...)
    # result = my_agent.run("오늘 서울 날씨는 어때?")
    # result = my_agent.run("최근 AI 에이전트 개발 트렌드에 대해 알려줘")
    

    이렇게 코드를 짜놓고 “이제 알아서 척척 해주겠지?” 하는 기대감에 부풀어 있었죠. 하지만 현실은… 제 에이전트는 제가 의도했던 대로 동작하지 않는 경우가 태반이었습니다. 😭

    AI 에이전트의 복잡한 로직 처리 실패 다이어그램

    그림 2: AI 에이전트가 복잡한 로직에서 혼란을 겪는 모습.

    ⚠️ 실패 사례로 배우는 설계 함정들

    제 삽질 경험을 통해 얻은 가장 중요한 교훈은 “LLM은 만능이 아니다”라는 겁니다. 그리고 “Haystack 에이전트 설계는 프롬프트 엔지니어링 그 이상”이라는 사실이었죠. 몇 가지 주요 실패 사례와 해결법을 공유합니다.

    1. LLM에 과도한 로직 의존 (The “Do Everything” LLM Fallacy)

    • 문제점: 처음엔 모든 판단과 로직을 LLM에게 맡기려고 했습니다. “이런 상황에선 저 도구를 쓰고, 저런 상황에선 이렇게 판단해” 식으로요. 하지만 LLM은 복잡한 다단계 로직이나 정교한 조건 분기(Conditional Branching)를 정확히 수행하기 어렵더라고요. 추론 과정이 길어질수록 오류 확률이 높아지고, 엉뚱한 방향으로 빠지기 일쑤였습니다. 🤯
    • 해결법: 명확한 비즈니스 로직은 코드로 구현하고, LLM은 ‘언어 이해’와 ‘도구 선택’에 집중하도록 역할을 분담했습니다. 예를 들어, 특정 조건에서는 무조건 특정 도구를 호출하도록 파이프라인(Pipeline) 자체를 설계하고, LLM은 그 도구에 넘겨줄 인자(Argument)를 추출하는 역할만 맡기는 식이죠.

    2. 어설픈 도구(Tool) 설계와 오용

    • 문제점:
      • 너무 많은 도구: 에이전트에게 너무 많은 도구를 한꺼번에 주니, LLM이 어떤 도구를 써야 할지 혼란스러워했습니다. 마치 초보 운전자가 복잡한 계기판을 보고 당황하는 것과 같았죠.
      • 모호한 도구 설명: 각 도구의 <code>description(설명)이 불분명하거나, 입력/출력 형식이 모호하면 LLM이 올바르게 사용하지 못했습니다. 예를 들어, ‘데이터 조회’ 도구가 있는데, 어떤 데이터를 어떻게 조회하는지 명확하지 않으니 LLM이 엉뚱한 쿼리를 날리는 경우가 많았습니다.
      • 도구 간 충돌: 기능이 겹치는 도구들이 있으면, LLM이 어떤 도구를 선택해야 할지 갈팡질팡했습니다.
    • 해결법:
      • 도구 개수 최소화: 꼭 필요한 도구만 제공하고, 복잡한 기능은 여러 도구가 아닌 하나의 도구 내부에서 처리하도록 했습니다.
      • 명확하고 간결한 설명: 각 도구의 name과 description, 그리고 기대하는 input(입력)과 output(출력) 형식을 매우 구체적으로 정의했습니다. “이 도구는 언제, 무엇을 위해 사용하며, 어떤 정보를 입력해야 가장 좋은 결과를 얻을 수 있다”는 점을 명시했죠.
      • 도구 체이닝(Tool Chaining): 복잡한 작업은 에이전트가 아닌, 미리 정의된 파이프라인 안에서 여러 도구를 순차적으로 호출하도록 설계했습니다.

    3. 컨텍스트 윈도우(Context Window) 관리 실패

    • 문제점: 에이전트가 이전 대화나 작업 이력을 계속 기억하게 하려니 컨텍스트 윈도우가 빠르게 꽉 차버렸습니다. LLM의 토큰(Token) 제한에 걸려 중요한 정보를 놓치거나, 비용이 폭증하는 문제가 발생했죠. 💸 특히 RAG를 적용했을 때, 검색된 문서들이 컨텍스트를 과도하게 차지하는 경우가 많았습니다.
    • 해결법:
      • 요약(Summarization): 이전 대화 이력을 주기적으로 요약해서 컨텍스트에 포함시켰습니다. Haystack의 요약 컴포넌트(Summarizer Component)를 활용했죠.
      • 관련성 필터링: 모든 이력을 넣는 대신, 현재 태스크와 가장 관련성 높은 정보만 선별적으로 컨텍스트에 주입했습니다.
      • 토큰 예산 설정: 각 단계별로 사용할 토큰의 최대치를 정해두고, 넘어가면 요약을 강제하거나 오래된 정보를 제거하는 로직을 추가했습니다.

    4. 평가(Evaluation)와 반복(Iteration)의 부재

    • 문제점: 처음에 “잘 되겠지” 하고 대충 만들고는, 몇 번 테스트해보고 안 되면 그냥 갈아엎는 식으로 개발했습니다. 어떤 부분에서 실패했는지, 왜 실패했는지에 대한 체계적인 기록이나 평가 없이 주먹구구식으로 접근했죠. 결국 똑같은 실수를 반복하고, 개선 속도가 매우 느렸습니다. 🐢
    • 해결법:
      • 테스트 케이스 작성: 다양한 시나리오에 대한 테스트 케이스(Test Case)를 만들고, 각 케이스별 에이전트의 응답을 기록했습니다.
      • 평가 지표 정의: ‘정확도’, ‘관련성’, ‘도구 사용의 적절성’ 등 평가 지표를 정의하고, 결과를 수치화해서 개선점을 명확히 파악했습니다. Haystack은 자체적으로 평가 도구를 제공하기도 합니다.
      • 디버깅(Debugging) 환경 구축: 에이전트의 추론 과정(Thought Process), 사용된 도구, LLM의 최종 출력 등을 쉽게 확인할 수 있는 로깅(Logging) 및 모니터링(Monitoring) 시스템을 구축했습니다.
    AI 에이전트 성능 모니터링 대시보드

    그림 3: AI 에이전트 성능 모니터링 대시보드 예시.

    그래서, 어떻게 개선했고 어떤 결과를 얻었나?

    위에서 언급한 설계 함정들을 하나씩 고쳐나가면서 제 Haystack 에이전트는 훨씬 더 견고해질 수 있었습니다. 특히 LLM의 역할을 명확히 하고, 도구 설계를 정교하게 가져간 것이 주효했어요. 삽질 끝에 드디어 에이전트가 제가 의도한 대로 동작하는 모습을 봤을 때의 희열이란! 🎉

    물론 아직 완벽하진 않지만, 이전처럼 엉뚱한 답변을 하거나 무한 루프에 빠지는 일은 현저히 줄었습니다. 특히 복잡한 정보 검색과 요약 작업에서 높은 효율을 보여주고 있어요. 이젠 단순한 질문 답변을 넘어, 특정 스케줄에 맞춰 필요한 정보를 자동으로 가져와 요약해주는 수준까지 발전시켰답니다.

    가장 크게 배운 점은 “LLM 에이전트 개발은 일반적인 소프트웨어 개발과 다르지 않다”는 것입니다. 단순히 프롬프트만 잘 쓴다고 되는 게 아니라, 아키텍처(Architecture) 설계, 모듈화(Modularization), 테스트(Testing), 디버깅(Debugging) 등 소프트웨어 엔지니어링의 기본 원칙들이 그대로 적용된다는 사실을 다시 한번 깨달았습니다.

    # 개선된 에이전트의 동작 예시 (Haystack 2.x 기반 개념적 코드)
    # 최신 API는 공식 문서를 참고하세요.
    # 핵심은 LLM의 역할은 '이해'와 '도구 인자 추출'에 집중하고,
    # 복잡한 로직은 파이프라인이나 외부 함수로 분리하는 것.
    
    from haystack.agents import Agent, Tool
    from haystack.components.generators import OpenAIChatGenerator
    from haystack.components.retrievers import InMemoryBM25Retriever
    from haystack.components.document_stores import InMemoryDocumentStore
    from haystack.components.builders.answer_builder import AnswerBuilder
    from haystack import Pipeline
    
    # 1. DocumentStore와 Retriever 설정 (RAG 예시)
    document_store = InMemoryDocumentStore()
    # document_store.write_documents(...) # 실제 문서 로딩
    retriever = InMemoryBM25Retriever(document_store=document_store)
    
    # 2. Tools 정의 (명확한 설명과 입력/출력)
    def perform_complex_analytics(data_query: str):
        """
        고급 분석을 수행하고 보고서를 생성합니다.
        입력: 'data_query' - 분석할 데이터에 대한 구체적인 쿼리 문자열 (예: "지난달 판매량 추이").
        출력: 분석 결과 요약 텍스트.
        """
        print(f"DEBUG: Performing complex analytics for: {data_query}")
        # 실제 복잡한 분석 로직 (외부 시스템 연동 등)
        return f"분석 결과: {data_query}에 대한 심층 분석 보고서가 생성되었습니다."
    
    analytics_tool = Tool(
        name="complex_analytics_tool",
        function=perform_complex_analytics,
        description="주어진 데이터 쿼리에 따라 복잡한 데이터 분석을 수행하고 요약 보고서를 생성합니다."
    )
    
    # 3. LLM Generator
    llm_generator = OpenAIChatGenerator(model="gpt-4", api_key="YOUR_API_KEY")
    
    # 4. Agent 생성 (이제 에이전트는 Tool과 LLM에 의존)
    my_improved_agent = Agent(llm=llm_generator, tools=[analytics_tool])
    
    # 5. Pipeline 구축 (에이전트가 복잡한 파이프라인의 한 단계로 동작할 수도 있습니다)
    # 이 예시에서는 에이전트 자체가 메인 역할을 하도록 단순화.
    # 실제로는 RAG Pipeline -> Agent -> Answer Builder 등으로 구성될 수 있습니다.
    
    # 예시: 에이전트 실행 및 결과 확인
    # print(my_improved_agent.run("지난달 서울 지역 판매량 추이에 대한 분석 보고서를 만들어줘."))
    # 결과는 이전보다 훨씬 의도에 맞게, 적절한 도구를 사용하며 나옵니다.
    

    마무리하며: 멘토로서 드리는 조언

    AI 에이전트 개발은 정말 흥미로운 분야입니다. 하지만 제가 겪었던 것처럼 많은 시행착오가 따를 수밖에 없습니다. 13년차 인프라 엔지니어로서, 그리고 홈랩에서 직접 삽질하며 배운 점을 정리하자면 이렇습니다. ✅

    항목 실패 사례 (Bad Practice) 성공 사례 (Good Practice)
    LLM 역할 모든 복잡한 로직을 LLM에 맡김 LLM은 ‘이해’와 ‘도구 선택’에 집중, 복잡한 로직은 코드로 분리
    도구(Tool) 설계 너무 많거나 모호한 설명, 기능 중복 최소한의 도구, 명확하고 구체적인 설명, 단일 책임 원칙
    컨텍스트 관리 모든 이력 저장, 토큰 제한 무시 요약, 관련성 필터링, 토큰 예산 설정
    개발 프로세스 주먹구구식 개발, 체계적인 평가 부재 테스트 케이스, 평가 지표, 디버깅 환경 구축
    실패를 통해 성장한 인프라 엔지니어

    그림 4: 실패를 딛고 성장한 인프라 엔지니어의 모습.

    AI 에이전트는 앞으로 우리 업무 방식에 큰 변화를 가져올 잠재력을 가지고 있습니다. 여러분도 저처럼 포기하지 않고 꾸준히 실험하고 개선해나간다면 분명 멋진 결과물을 만들어낼 수 있을 겁니다. 저도 다음번에는 더 고도화된 Haystack 에이전트 구축 경험이나, 특정 도구를 연동하는 방법에 대해 이야기해볼게요.

    궁금한 점이나 함께 나눌 경험이 있다면 언제든지 댓글로 남겨주세요! 다음 글에서 만나요! 👋