13년차의 서버실

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

[태그:] LLM 에이전트

  • [AI] LangChain Agent SDK 활용 사례: 복잡한 워크플로우 자동화 구현기

    [AI] LangChain Agent SDK 활용 사례: 복잡한 워크플로우 자동화 구현기

    LangChain Agent로 복잡한 워크플로우 자동화를 구현하려는 분들이 요즘 정말 많아요. 저도 홈랩(Home Lab, 개인 실험 환경)에서 여러 자동화 파이프라인을 굴리다가, 단순한 스크립트 몇 개로는 더 이상 감당이 안 되는 시점이 왔거든요. 알림은 Slack으로 보내고, 장애 징후는 로그에서 뽑고, 사용자 요청은 요약하고, 필요한 경우엔 외부 API까지 호출해야 했습니다. 처음엔 Python으로 if 문 덕지덕지 붙이면 되겠지 했었는데, 금방 한계가 왔어요. 그때 정리해서 도입한 방식이 바로 LangChain Agent 기반 워크플로우 자동화였습니다.

    이 글에서는 Agent SDK 활용 관점에서, 실제로 LangChain의 에이전트 구성 요소와 Tool(에이전트가 호출하는 기능 단위)을 조합해 어떻게 복잡한 흐름을 관리했는지 풀어보겠습니다. 거창한 데모보다, 제가 직접 해보면서 부딪힌 포인트를 중심으로 썼어요. 혹시 자동화가 점점 커지면서 스크립트가 엉키기 시작했다면 꽤 공감되실 겁니다.

    왜 LangChain Agent가 워크플로우 자동화에 잘 맞을까

    쉽게 말해 LangChain Agent는 LLM 에이전트(대형 언어 모델 기반 작업 판단기)가 상황을 읽고, 필요한 Tool을 골라서 순서대로 실행하게 만드는 구조입니다. 예전엔 제가 직접 분기 로직을 다 짰어요. 예를 들면 “에러 로그가 있으면 요약하고, 장애 패턴이면 티켓 만들고, 아니면 보고서만 저장” 같은 흐름이었죠. 이걸 전부 코드로 박아두면 처음엔 단순한데, 조건이 늘어날수록 유지보수가 지옥이 됩니다.

    근데 LangChain Agent를 붙이면 판단 레이어를 어느 정도 유연하게 분리할 수 있어요. 물론 모든 걸 LLM에게 맡기면 안 됩니다. 여기서 중요한 포인트! 결정은 에이전트가 하되, 실행은 검증된 Tool이 하도록 분리해야 합니다. 저는 이 구조를 쓰고 나서 자동화가 훨씬 읽기 쉬워졌고, 장애 원인 추적도 편해졌더라고요.

    LangChain Agent 기반 워크플로우 자동화 전체 아키텍처 이미지

    LangChain Agent가 요청을 받아 분석하고, 여러 Tool과 검증 단계를 거쳐 결과를 반환하는 전체 흐름을 보여주는 이미지입니다.

    LangChain 사례로 보는 기본 구조

    제가 실제로 많이 쓴 패턴은 아래 4단계였습니다.

    1. 입력 수집: 사용자 요청, 로그, 이벤트, 티켓 내용을 받습니다.
    2. 의도 분류: 요약인지, 분석인지, 외부 시스템 호출이 필요한지 구분합니다.
    3. Tool 실행: 검색, 저장, 알림, 티켓 생성 같은 실제 작업을 수행합니다.
    4. 결과 검증: 응답 포맷과 필수 필드가 맞는지 확인합니다.

    이 구조가 좋은 이유는 명확해요. LLM은 텍스트 해석과 판단에 강하고, 실제 시스템 변경은 함수나 API가 담당하니까요. 다시 말해 자유도는 높이고, 위험도는 낮추는 방식입니다. 저도 처음엔 “에이전트가 너무 똑똑한 척하다가 이상한 Tool 고르면 어쩌지?” 싶었는데, Tool 설계를 보수적으로 하면 꽤 안정적으로 돌아가더라고요.

    구성 요소 역할 실무 포인트
    LLM 질문 해석, 다음 행동 판단 자유 서술은 허용하되 출력 형식은 제한
    Tool 실제 API 호출, 파일 저장, 알림 전송 입력값 검증을 꼭 넣기
    Prompt 행동 기준과 제약 정의 하면 안 되는 작업도 명시
    Executor 에이전트 실행 흐름 관리 타임아웃, 재시도, 로깅 필요
    Validator 결과 검증 JSON 스키마나 필수 필드 검사 추천

    실전 구현 1: 워크플로우 자동화 시나리오 정의

    제가 예제로 자주 설명하는 시나리오는 이렇습니다. 운영 중인 서비스에서 장애 의심 이벤트가 들어오면, 에이전트가 로그를 요약하고, 중요도를 분류하고, 필요한 경우 운영 채널에 알림을 보내고, 마지막으로 리포트를 저장하는 거예요. 말로 하면 쉬운데, 이걸 사람이 수동으로 하려면 은근 반복 작업이 많거든요.

    저는 먼저 Tool 목록부터 아주 보수적으로 잘랐습니다. 이게 진짜 중요합니다.

    • search_logs: 최근 로그 검색
    • classify_severity: 중요도 분류
    • notify_slack: Slack 알림 전송
    • save_report: 결과 저장

    처음엔 Tool을 이것저것 많이 붙였는데 오히려 에이전트가 헷갈리더라고요. 실제로 써보니까 Tool 수를 줄이고, 각 Tool 책임을 명확히 나누는 것이 훨씬 낫습니다.

    환경 준비

    python -m venv .venv
    source .venv/bin/activate
    pip install langchain langchain-openai

    여기서는 가장 단순한 형태로만 가져갑니다. 패키지 버전은 시점에 따라 바뀌기 때문에, 프로젝트에서는 lock file로 고정하는 걸 추천드립니다. 저도 처음엔 로컬에선 되는데 서버에선 안 되는 문제를 몇 번 겪었습니다 ㅎㅎ

    기본 Tool 정의

    from typing import Literal
    from langchain.tools import tool
    
    @tool
    def search_logs(query: str) -> str:
        """Search recent logs by keyword and return a short summary."""
        sample = [
            "api-gateway timeout after 30s",
            "db connection pool exhausted",
            "retry succeeded for payment worker"
        ]
        matched = [line for line in sample if query.lower() in line.lower()]
        return "\n".join(matched) if matched else "no relevant logs"
    
    @tool
    def classify_severity(log_summary: str) -> Literal["low", "medium", "high"]:
        """Classify operational severity from log summary."""
        text = log_summary.lower()
        if "pool exhausted" in text or "timeout" in text:
            return "high"
        if "retry" in text:
            return "medium"
        return "low"
    
    @tool
    def notify_slack(message: str) -> str:
        """Send a message to an operations channel."""
        return f"sent to slack: {message}"
    
    @tool
    def save_report(content: str) -> str:
        """Save an incident report."""
        return "report saved"

    실무에서는 당연히 실제 로그 시스템이나 메시징 API를 붙이겠죠. 다만 구조는 크게 다르지 않습니다. LLM은 실행 주체가 아니라 오케스트레이터(Orchestrator, 작업 조율자)라고 생각하시면 이해가 쉽습니다.

    LangChain Agent 툴 호출 흐름과 설정 구성을 설명하는 이미지

    에이전트가 어떤 기준으로 Tool을 고르고, 각 Tool이 어떤 입력과 출력을 갖는지 보여주는 구성 이미지입니다.

    실전 구현 2: LangChain Agent 연결

    이제 Tool을 에이전트에 연결해보겠습니다. 제가 처음 이 부분에서 좀 헤맸던 이유가, 프롬프트에 역할과 제약을 충분히 안 적어서였습니다. 에이전트는 생각보다 지시를 문자 그대로 받아들이거든요.

    from langchain.agents import AgentExecutor, create_openai_functions_agent
    from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
    from langchain_openai import ChatOpenAI
    
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    tools = [search_logs, classify_severity, notify_slack, save_report]
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """
    You are an operations automation agent.
    Use tools to investigate incidents.
    If severity is high, notify slack.
    Always save a final report.
    Do not invent log data.
    Return a concise final summary.
    """),
        ("human", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad")
    ])
    
    agent = create_openai_functions_agent(llm, tools, prompt)
    executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
    
    result = executor.invoke({
        "input": "Investigate database timeout symptoms and create an incident summary."
    })
    
    print(result["output"])

    여기서 핵심은 세 가지예요.

    1. Do not invent log data처럼 금지 조건을 명시합니다.
    2. Always save a final report처럼 필수 행동을 적습니다.
    3. 최종 응답은 사람이 읽기 쉬운 형태로 짧게 제한합니다.

    이렇게 해두면 LLM 에이전트가 지나치게 산으로 가는 걸 많이 줄일 수 있어요. 저도 처음엔 프롬프트를 너무 추상적으로 써서 Tool 호출 순서가 들쭉날쭉했는데, 운영 기준을 문장으로 박아두니까 꽤 안정됐습니다.

    실전 구현 3: 다단계 워크플로우 자동화로 확장하기

    한 단계 더 나가면 조건부 분기와 검증 로직을 붙일 수 있습니다. 예를 들어 중요도가 high일 때만 Slack 알림을 보내고, 결과는 반드시 JSON으로 저장하도록 강제하는 식이죠. 저는 운영 자동화에서 이 검증 단계를 빼먹었다가 나중에 리포트 형식이 제각각이라 한참 정리했던 적이 있습니다. 삽질 좀 했습니다 ㅎㅎ

    import json
    from pydantic import BaseModel, ValidationError
    
    class Report(BaseModel):
        summary: str
        severity: str
        action_taken: str
    
    
    def validate_report(payload: str) -> str:
        data = json.loads(payload)
        report = Report(**data)
        return report.model_dump_json(ensure_ascii=False)
    
    final_payload = json.dumps({
        "summary": "Database timeout patterns detected from recent logs.",
        "severity": "high",
        "action_taken": "Slack notification sent and report saved."
    }, ensure_ascii=False)
    
    print(validate_report(final_payload))

    이건 에이전트 바깥에서 검증하는 예시입니다. 제 경험상 에이전트 결과를 바로 믿지 말고, 마지막엔 애플리케이션 레벨에서 검증하는 게 안전합니다. 특히 워크플로우 자동화가 외부 시스템을 건드릴수록 더 그렇습니다.

    ⚠️ 트러블슈팅: 제가 실제로 겪었던 문제들

    여기부터가 진짜 실전입니다. 데모는 잘 돌아가는데 운영에 붙이면 꼭 이상한 부분이 생기거든요.

    1. Tool 설명이 모호하면 엉뚱한 함수가 호출됩니다

    예전에 notify와 save 관련 Tool 설명을 둘 다 “store result” 비슷하게 써둔 적이 있었는데, 에이전트가 자꾸 저장만 하고 알림은 안 보내더라고요. 그 뒤로는 Tool docstring을 아주 구체적으로 씁니다.

    • notify_slack: 운영 채널에 즉시 알림을 보냄
    • save_report: 최종 리포트를 파일 또는 저장소에 기록

    설명이 겹치면 안 됩니다. 정말 사소해 보이는데 체감 차이가 커요.

    2. 프롬프트만으로 제어하려고 하면 흔들립니다

    처음엔 “high면 알림 보내”를 프롬프트에만 적어뒀습니다. 그런데 상황에 따라 누락되는 경우가 있더라고요. 이후에는 애플리케이션 쪽에도 한 번 더 조건문을 넣었습니다. 즉, 프롬프트 제약 + 코드 제약 이중으로 묶는 방식입니다.

    3. 로그 요약이 길어지면 판단 품질이 떨어집니다

    이건 꽤 자주 겪습니다. 입력 컨텍스트가 너무 길면 핵심 장애 패턴이 묻혀버리거든요. 그래서 저는 최근 로그를 전부 넣지 않고, 사전 필터링을 거친 뒤 요약본만 넣습니다. 쉽게 말해 검색(Search)과 추출(Extraction)을 먼저 하고, 에이전트 판단은 그 다음입니다.

    4. 실패 경로를 안 만들면 운영에서 곤란합니다

    API 호출 실패, 응답 지연, 빈 결과 같은 케이스를 빼먹기 쉽습니다. 근데 실제 운영은 정상보다 예외가 더 기억에 남습니다. 그래서 아래처럼 실패 응답도 표준화해두는 걸 추천드립니다.

    workflow:
      on_error:
        notify: true
        save_fallback_report: true
        retry_policy:
          enabled: true
          max_attempts: 3

    이거 해두면 밤에 훨씬 편합니다. 진짜로요.

    LangChain Agent를 활용한 장애 대응 자동화 예외 처리 흐름 이미지

    Tool 실패, 재시도, 대체 보고서 저장 같은 예외 처리 경로를 시각적으로 설명하는 이미지입니다.

    검증과 결과: 자동화가 실제로 편해졌는지 확인하는 방법

    자동화는 “돌아간다”보다 “믿고 맡길 수 있다”가 중요합니다. 저는 보통 아래 항목으로 검증합니다.

    1. 같은 입력에 대해 비슷한 결과가 나오는가
    2. high severity에서 알림 누락이 없는가
    3. 결과 저장 포맷이 항상 일정한가
    4. 실패 시 fallback 경로가 작동하는가

    간단한 테스트 스크립트도 붙여봅니다.

    test_inputs = [
        "database timeout",
        "retry succeeded",
        "unknown warning"
    ]
    
    for item in test_inputs:
        output = executor.invoke({"input": f"Investigate: {item}"})
        print(item)
        print(output["output"])
        print("-" * 40)

    이 단계에서 중요한 건 벤치마크 숫자를 화려하게 만드는 게 아닙니다. 오히려 재현성(reproducibility, 같은 조건에서 비슷한 결과가 나오는 성질)과 예측 가능성이 더 중요합니다. 제가 직접 해보니, 사람 손을 완전히 없애는 자동화보다 “1차 분석과 정리까지 맡기는 자동화”가 훨씬 현실적이고 만족도도 높았습니다.

    검증 항목 자동화 전 자동화 후
    로그 확인 사람이 수동 검색 Tool로 일관되게 검색
    중요도 판단 담당자마다 기준 차이 프롬프트와 규칙으로 통일
    알림 발송 가끔 누락 조건 기반 자동 전송
    리포트 저장 형식 제각각 검증 후 동일 포맷 저장

    자동화 실행 결과, 중요도 분류, 알림 여부, 리포트 저장 상태를 한눈에 보여주는 대시보드 이미지입니다.

    LangChain Agent 도입 전 체크리스트

    무조건 Agent부터 붙이기보다, 아래를 먼저 확인하시면 시행착오를 많이 줄일 수 있습니다.

    • Tool이 충분히 분리돼 있는가
    • 실패했을 때 사람이 이어받을 수 있는가
    • 출력 검증 로직이 있는가
    • LLM에게 맡길 판단과 코드로 고정할 규칙이 구분돼 있는가
    • 로그와 실행 기록을 남기고 있는가

    특히 마지막은 꼭 챙기세요. 나중에 “왜 이런 판단을 했지?”를 보려면 실행 흔적이 있어야 합니다. 이전 글에서 다뤘던 운영 로그 정리 방식이 있다면 그 구조를 그대로 재활용해도 좋습니다. 다음 글에서는 이런 흐름을 더 확장해서 멀티스텝 승인(approval) 자동화까지 다뤄볼 예정입니다.

    자주 묻는 질문

    Q1. LangChain Agent는 모든 자동화에 필요한가요?

    아닙니다. 분기가 거의 없고 입력과 출력이 고정돼 있다면 일반 스크립트가 더 단순하고 좋습니다. 에이전트는 판단이 필요한 자동화에서 빛이 납니다.

    Q2. LLM 에이전트가 실수하면 위험하지 않나요?

    맞습니다. 그래서 실제 시스템 변경은 제한된 Tool만 허용하고, 검증 단계를 별도로 둬야 합니다. 저는 읽기와 분류는 넓게, 쓰기와 변경은 좁게 가져갑니다.

    Q3. Agent SDK 활용 포인트를 한 줄로 정리하면요?

    판단은 유연하게, 실행은 보수적으로입니다. 이 원칙 하나만 지켜도 워크플로우 자동화 품질이 꽤 달라집니다.

    LangChain Agent 도입 전후 비교와 핵심 원칙 요약 이미지

    도입 전후 차이, Tool 설계 원칙, 검증 전략을 요약한 마무리 인포그래픽 이미지입니다.

    마무리: 복잡한 워크플로우 자동화, 결국 설계가 반입니다

    LangChain Agent를 써보면 처음엔 되게 마법처럼 느껴져요. 자연어로 시키면 알아서 Tool을 고르니까요. 근데 실제로 오래 굴려보면, 잘 되는 이유는 모델이 똑똑해서라기보다 Tool 경계, 프롬프트 제약, 검증 로직을 얼마나 잘 설계했느냐에 달려 있더라고요. 저도 처음엔 이게 뭔가 싶었는데, 구조를 한 번 제대로 잡고 나니 워크플로우 자동화가 한결 차분해졌습니다.

    정리하면 이렇습니다. LangChain Agent는 복잡한 워크플로우 자동화에 꽤 강력한 도구예요. 하지만 무턱대고 붙이기보다, 에이전트가 판단할 영역과 코드가 통제할 영역을 분리해야 진짜 실무형으로 살아남습니다. 혹시 지금 자동화 스크립트가 점점 괴물이 되어가고 있다면, 작은 Tool 몇 개부터 분리해서 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 에이전트 구축 경험이나, 특정 도구를 연동하는 방법에 대해 이야기해볼게요.

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