🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

Agents & ReAct 패턴 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 2. · 16 Views

Agents & ReAct 패턴 완벽 가이드

LangChain과 LangGraph를 활용한 AI 에이전트 개발의 핵심인 ReAct 패턴을 학습합니다. 추론하고, 행동하고, 관찰하는 지능형 에이전트를 만드는 방법을 초급자도 이해할 수 있도록 설명합니다.


목차

  1. create_agent() 함수 심화
  2. LangGraph 기반 에이전트 구조
  3. ReAct 패턴: 추론→행동→관찰
  4. 시스템 프롬프트 설정
  5. 동적 프롬프트 미들웨어
  6. 에이전트 호출 및 결과 처리

1. create agent() 함수 심화

김개발 씨는 최근 회사에서 AI 챗봇 프로젝트를 맡게 되었습니다. 단순히 질문에 답변만 하는 것이 아니라, 실제로 데이터를 조회하고 계산도 하는 똑똑한 챗봇이 필요했습니다.

"도대체 어떻게 하면 LLM이 스스로 도구를 사용하게 만들 수 있을까요?"

create_agent() 함수는 LangChain에서 도구를 사용할 수 있는 에이전트를 생성하는 핵심 함수입니다. 마치 신입 직원에게 업무 매뉴얼과 필요한 도구들을 한꺼번에 지급하는 것과 같습니다.

이 함수를 제대로 이해하면 LLM이 단순 응답을 넘어 실제 작업을 수행하는 에이전트로 변신시킬 수 있습니다.

다음 코드를 살펴봅시다.

from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

# LLM 모델 초기화
llm = ChatOpenAI(model="gpt-4", temperature=0)

# 에이전트가 사용할 도구 목록 정의
tools = [search_tool, calculator_tool, database_tool]

# 프롬프트 템플릿 구성
prompt = ChatPromptTemplate.from_messages([
    ("system", "당신은 유능한 AI 비서입니다."),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}")
])

# 에이전트 생성 - 핵심 함수
agent = create_tool_calling_agent(llm, tools, prompt)

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 어느 날 팀장님이 다가와 말했습니다.

"개발 씨, 이번에 고객 문의를 자동으로 처리하는 챗봇을 만들어 볼래요? 근데 단순 답변만 하면 안 되고, 실제로 주문 조회도 하고 환불 처리도 할 수 있어야 해요." 김개발 씨는 고민에 빠졌습니다.

지금까지 만들어 본 챗봇은 정해진 질문에 정해진 답변만 하는 수준이었습니다. 어떻게 하면 LLM이 스스로 판단해서 필요한 작업을 수행하게 만들 수 있을까요?

선배 개발자 박시니어 씨가 힌트를 주었습니다. "에이전트라는 개념을 알아봐.

LangChain의 create_agent() 함수를 사용하면 돼." 그렇다면 에이전트란 정확히 무엇일까요? 쉽게 비유하자면, 에이전트는 마치 만능 비서와 같습니다.

일반 LLM이 백과사전처럼 질문에 답변만 한다면, 에이전트는 실제로 전화도 걸고, 이메일도 보내고, 자료도 검색해주는 비서인 것입니다. 이 비서에게 필요한 도구들을 쥐여주는 역할을 하는 것이 바로 create_agent() 함수입니다.

create_agent() 함수가 없던 시절에는 어땠을까요? 개발자들은 LLM의 응답을 일일이 파싱해서 어떤 도구를 호출할지 직접 판단하는 로직을 작성해야 했습니다.

"만약 응답에 '검색'이라는 단어가 있으면 검색 API를 호출하고, '계산'이라는 단어가 있으면 계산기를 호출하라"는 식의 하드코딩이 필요했습니다. 코드가 복잡해지고, 새로운 도구를 추가할 때마다 조건문이 늘어났습니다.

바로 이런 문제를 해결하기 위해 create_agent() 함수가 등장했습니다. 이 함수를 사용하면 LLM이 스스로 어떤 도구를 언제 사용할지 판단합니다.

개발자는 그저 사용 가능한 도구 목록을 전달하기만 하면 됩니다. 마치 신입 직원에게 "이건 전화기, 이건 팩스, 이건 프린터야.

상황에 맞게 알아서 써"라고 알려주는 것과 같습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 ChatOpenAI를 초기화하는 부분에서 temperature를 0으로 설정한 것에 주목해주세요. 에이전트는 일관된 판단을 내려야 하므로 창의성보다 정확성이 중요합니다.

다음으로 tools 리스트에 에이전트가 사용할 도구들을 담습니다. 마지막으로 create_tool_calling_agent() 함수가 이 모든 것을 조합하여 완성된 에이전트를 반환합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 고객 서비스 챗봇을 개발한다고 가정해봅시다.

주문 조회 도구, 환불 처리 도구, FAQ 검색 도구를 tools에 넣어두면, 에이전트가 고객의 질문을 분석하여 적절한 도구를 자동으로 선택합니다. 개발자가 일일이 조건 분기를 작성할 필요가 없어지는 것입니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 많은 도구를 한꺼번에 등록하는 것입니다.

도구가 많아질수록 LLM이 올바른 선택을 하기 어려워집니다. 따라서 용도에 맞게 도구를 분류하고, 하나의 에이전트에는 관련성 높은 도구만 등록하는 것이 좋습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.

"아, 그러니까 저는 도구만 잘 만들어서 등록하면 되는 거군요!" create_agent() 함수를 제대로 이해하면 LLM을 단순한 대화 도구에서 실제 업무를 수행하는 지능형 비서로 탈바꿈시킬 수 있습니다.

실전 팁

💡 - temperature는 0에 가깝게 설정하여 일관된 도구 선택을 유도하세요

  • 도구의 description을 명확하게 작성해야 LLM이 올바른 판단을 내립니다
  • 하나의 에이전트에 도구는 5-7개 이내로 제한하는 것이 효과적입니다

2. LangGraph 기반 에이전트 구조

김개발 씨가 에이전트를 만들어 테스트하던 중 이상한 현상을 발견했습니다. 가끔 에이전트가 같은 도구를 무한 반복 호출하거나, 엉뚱한 순서로 작업을 수행하는 것이었습니다.

"에이전트의 동작 흐름을 좀 더 세밀하게 제어할 수 없을까요?"

LangGraph는 에이전트의 실행 흐름을 그래프 구조로 정의할 수 있게 해주는 프레임워크입니다. 마치 지하철 노선도처럼 에이전트가 어떤 상태에서 어떤 상태로 이동할 수 있는지 명확하게 그려주는 도구입니다.

이를 통해 복잡한 에이전트의 동작을 예측 가능하고 디버깅하기 쉽게 만들 수 있습니다.

다음 코드를 살펴봅시다.

from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated

# 에이전트의 상태를 정의하는 클래스
class AgentState(TypedDict):
    messages: list  # 대화 기록
    next_action: str  # 다음 수행할 액션

# 그래프 생성
workflow = StateGraph(AgentState)

# 노드 추가 - 각각의 처리 단계
workflow.add_node("reasoning", reasoning_node)
workflow.add_node("action", action_node)
workflow.add_node("observation", observation_node)

# 엣지 연결 - 노드 간 흐름 정의
workflow.add_edge("reasoning", "action")
workflow.add_edge("action", "observation")
workflow.add_conditional_edges("observation", should_continue)

# 시작점 설정 및 컴파일
workflow.set_entry_point("reasoning")
app = workflow.compile()

김개발 씨는 에이전트 개발을 진행하면서 점점 머리가 복잡해졌습니다. 처음에는 단순했던 로직이 요구사항이 추가될수록 스파게티처럼 엉켜갔습니다.

"이 상황에서는 검색을 먼저 하고, 저 상황에서는 계산을 먼저 해야 하고..." 박시니어 씨가 김개발 씨의 코드를 보더니 한숨을 쉬었습니다. "개발 씨, 이렇게 조건문으로 흐름을 제어하면 나중에 유지보수가 불가능해요.

LangGraph를 써보는 게 어때요?" 그렇다면 LangGraph란 정확히 무엇일까요? 쉽게 비유하자면, LangGraph는 마치 지하철 노선도와 같습니다.

지하철 노선도를 보면 어느 역에서 어느 역으로 갈 수 있는지, 환승은 어디서 하는지 한눈에 파악할 수 있습니다. LangGraph도 마찬가지로 에이전트가 어떤 상태에서 어떤 상태로 전이할 수 있는지 시각적으로 명확하게 정의합니다.

기존 방식의 문제점은 무엇이었을까요? 일반적인 에이전트는 "루프" 형태로 동작합니다.

생각하고, 행동하고, 결과를 보고, 다시 생각하고... 이 과정이 언제 끝나야 하는지, 어떤 조건에서 다음 단계로 넘어가야 하는지가 코드 곳곳에 흩어져 있었습니다.

버그가 발생해도 어디서 문제가 생겼는지 추적하기 어려웠습니다. LangGraph는 이 문제를 상태 기계 개념으로 해결합니다.

에이전트의 전체 상태를 AgentState라는 클래스로 정의합니다. 여기에는 지금까지의 대화 내용, 다음에 수행할 작업, 중간 결과물 등 에이전트가 알아야 할 모든 정보가 담깁니다.

마치 게임의 세이브 파일처럼, 이 상태만 보면 에이전트가 현재 어떤 상황인지 완벽하게 알 수 있습니다. 코드를 살펴보면 add_node() 메서드로 노드를 추가합니다.

각 노드는 특정 작업을 수행하는 함수입니다. reasoning 노드는 생각하는 단계, action 노드는 행동하는 단계, observation 노드는 결과를 관찰하는 단계입니다.

add_edge() 메서드는 노드 간의 연결을 정의합니다. reasoning에서 action으로, action에서 observation으로 흐름이 이어집니다.

특히 **add_conditional_edges()**는 조건에 따라 다른 노드로 분기할 수 있게 해줍니다. 작업이 완료되었으면 종료하고, 아니면 다시 reasoning으로 돌아가는 식입니다.

실제 현업에서 LangGraph의 진가가 발휘되는 순간이 있습니다. 복잡한 멀티 에이전트 시스템을 구축할 때입니다.

예를 들어 하나의 요청을 처리하기 위해 리서처 에이전트, 분석가 에이전트, 작성자 에이전트가 순차적으로 또는 병렬로 협업해야 한다면, LangGraph 없이는 이 흐름을 관리하기가 매우 어렵습니다. 주의할 점도 있습니다.

LangGraph는 강력하지만 단순한 에이전트에는 과도할 수 있습니다. 도구 2-3개만 사용하는 간단한 에이전트라면 기본 create_agent()로 충분합니다.

복잡도가 정당화될 때만 LangGraph를 도입하세요. 박시니어 씨의 조언을 듣고 김개발 씨는 에이전트 구조를 LangGraph로 재설계했습니다.

이제 에이전트의 동작 흐름이 한눈에 보였습니다. 디버깅도 훨씬 수월해졌습니다.

실전 팁

💡 - 단순한 에이전트에는 LangGraph가 오버엔지니어링일 수 있으니 복잡도를 고려하세요

  • AgentState에는 디버깅에 필요한 정보를 충분히 담아두세요
  • conditional_edges의 분기 조건을 명확하게 정의하여 무한 루프를 방지하세요

3. ReAct 패턴: 추론→행동→관찰

김개발 씨는 에이전트가 때때로 황당한 실수를 하는 것을 발견했습니다. 사용자가 "서울 날씨 알려줘"라고 물었는데, 날씨 API를 호출하지 않고 대충 지어낸 답변을 하는 것이었습니다.

"왜 도구를 사용하지 않고 추측으로 답하는 걸까요?"

ReAct 패턴은 Reasoning(추론)과 Acting(행동)을 결합한 에이전트 동작 방식입니다. 마치 우리가 문제를 풀 때 "음, 이건 이렇게 해야겠다"라고 생각한 후 실제로 행동하고, 그 결과를 보며 다음 단계를 결정하는 것과 같습니다.

이 패턴을 통해 에이전트는 체계적으로 사고하고 행동하게 됩니다.

다음 코드를 살펴봅시다.

# ReAct 패턴의 핵심 구조
def react_loop(query: str, tools: list, llm):
    thought_history = []

    while True:
        # 1. Reasoning: 현재 상황 분석 및 다음 행동 결정
        thought = llm.invoke(f"질문: {query}\n기록: {thought_history}\n생각:")
        thought_history.append({"type": "thought", "content": thought})

        # 2. Acting: 결정된 행동 수행
        action = parse_action(thought)
        if action == "FINISH":
            return extract_answer(thought)

        result = execute_tool(action, tools)

        # 3. Observation: 행동 결과 관찰 및 기록
        thought_history.append({"type": "observation", "content": result})

김개발 씨는 에이전트의 이상한 행동에 당황했습니다. 분명히 날씨 조회 도구를 등록해뒀는데, 왜 사용하지 않고 "서울은 보통 이맘때 20도 정도입니다"라는 엉터리 답변을 하는 걸까요?

박시니어 씨가 설명해주었습니다. "LLM은 기본적으로 그럴듯한 텍스트를 생성하려고 해.

도구를 언제 써야 하는지 명확하게 알려주지 않으면 그냥 아는 척하고 대답해버리는 거야. ReAct 패턴을 적용해봐." ReAct는 Reasoning + Acting의 합성어입니다.

이 패턴은 2022년 구글과 프린스턴 대학 연구진이 발표한 논문에서 처음 소개되었습니다. 핵심 아이디어는 간단합니다.

LLM이 바로 답변하지 않고, 먼저 "생각"을 출력하게 하는 것입니다. 그리고 그 생각을 바탕으로 행동을 결정하고, 행동의 결과를 관찰한 후 다시 생각합니다.

마치 셜록 홈즈가 사건을 해결하는 과정과 비슷합니다. 홈즈는 단서를 보면 바로 범인을 지목하지 않습니다.

"음, 이 진흙은 런던 북부에서만 발견되는 종류군. 그렇다면 용의자의 동선을 확인해봐야겠어." 이렇게 추론하고, 실제로 조사하고, 그 결과를 바탕으로 다음 추론을 이어갑니다.

ReAct 패턴도 정확히 이 방식입니다. 코드에서 while True 루프가 핵심입니다.

첫 번째 단계인 Reasoning에서 에이전트는 현재까지의 정보를 바탕으로 생각합니다. "사용자가 서울 날씨를 물었다.

내가 아는 정보로는 정확한 현재 날씨를 알 수 없다. 날씨 API를 호출해야겠다." 이런 생각이 명시적으로 출력됩니다.

두 번째 단계인 Acting에서는 결정된 행동을 실행합니다. 날씨 API를 호출하고 결과를 받아옵니다.

만약 더 이상 행동이 필요 없다고 판단되면 "FINISH"를 반환하고 루프를 종료합니다. 세 번째 단계인 Observation에서는 행동의 결과를 기록합니다.

"날씨 API 호출 결과: 서울, 맑음, 18도." 이 관찰 결과는 다음 추론의 입력이 됩니다. 이 패턴의 가장 큰 장점은 투명성입니다.

에이전트가 왜 그런 결정을 내렸는지 추론 과정이 모두 기록됩니다. 버그가 발생했을 때 "아, 여기서 잘못 생각했구나"라고 바로 파악할 수 있습니다.

블랙박스처럼 답만 뱉어내는 것이 아니라, 사고의 흐름이 투명하게 드러납니다. 실무에서는 이 패턴이 필수적입니다.

특히 금융이나 의료 같이 결정의 근거가 중요한 도메인에서 ReAct 패턴은 필수입니다. "왜 이 주식을 추천했나요?"라는 질문에 "그냥요"라고 답하면 안 되기 때문입니다.

추론 과정이 기록되어 있으면 감사나 검증이 가능해집니다. 주의할 점은 토큰 사용량입니다.

매번 생각을 출력하므로 일반 LLM 호출보다 토큰을 더 많이 사용합니다. 간단한 질문에도 ReAct 패턴을 적용하면 비용이 늘어날 수 있습니다.

복잡한 작업에만 선택적으로 적용하는 것이 좋습니다. 김개발 씨는 ReAct 패턴을 적용한 후 에이전트가 확실히 달라졌음을 느꼈습니다.

이제 도구를 사용해야 할 때는 정확히 사용하고, 그 과정이 모두 로그에 남았습니다.

실전 팁

💡 - 추론 과정(thought)은 반드시 로깅하여 디버깅에 활용하세요

  • 최대 반복 횟수를 설정하여 무한 루프를 방지하세요
  • 간단한 질문은 ReAct 없이 직접 응답하도록 분기 처리하면 비용을 절약할 수 있습니다

4. 시스템 프롬프트 설정

김개발 씨가 만든 에이전트는 기능적으로는 잘 동작했지만, 뭔가 아쉬웠습니다. 너무 딱딱하게 말하고, 회사의 서비스 톤앤매너와도 맞지 않았습니다.

"에이전트의 성격이나 말투를 어떻게 설정하죠?"

시스템 프롬프트는 에이전트의 기본 성격, 역할, 행동 규칙을 정의하는 지침서입니다. 마치 신입 직원에게 주는 업무 가이드라인과 같습니다.

"당신은 친절한 고객 상담사입니다"라고 알려주면 에이전트는 그에 맞게 행동합니다. 잘 작성된 시스템 프롬프트는 에이전트의 품질을 크게 좌우합니다.

다음 코드를 살펴봅시다.

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

# 상세한 시스템 프롬프트 정의
system_prompt = """당신은 TechMart의 AI 고객 상담사 '테키'입니다.

## 역할
- 고객의 주문, 배송, 환불 문의에 친절하게 응대합니다.
- 정확한 정보 제공을 위해 반드시 도구를 활용합니다.

## 성격
- 밝고 친근한 말투를 사용합니다.
- 공감을 먼저 표현한 후 해결책을 제시합니다.

## 규칙
- 모르는 것은 솔직히 모른다고 말합니다.
- 개인정보 관련 질문에는 답하지 않습니다.
"""

# 프롬프트 템플릿 구성
prompt = ChatPromptTemplate.from_messages([
    ("system", system_prompt),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

김개발 씨는 에이전트를 시연하던 중 난감한 상황을 맞았습니다. 팀장님이 "이 에이전트 말투가 너무 딱딱하네요.

우리 서비스는 좀 더 친근한 이미지인데..."라고 피드백을 주신 것입니다. 박시니어 씨가 해결책을 알려주었습니다.

"에이전트도 사람처럼 성격을 가질 수 있어. 시스템 프롬프트로 정의하면 돼." 시스템 프롬프트는 에이전트의 DNA와 같습니다.

사람이 성장 환경과 교육에 따라 성격이 형성되듯, 에이전트도 시스템 프롬프트에 따라 완전히 다른 모습을 보입니다. 같은 LLM이라도 시스템 프롬프트가 다르면 마치 다른 사람처럼 행동합니다.

좋은 시스템 프롬프트의 구조는 보통 세 가지 요소로 구성됩니다. 첫째, 역할 정의입니다.

"당신은 ~입니다"라는 문장으로 에이전트가 누구인지 명확하게 알려줍니다. 역할이 명확해야 에이전트가 자신이 해야 할 일과 하지 말아야 할 일을 구분할 수 있습니다.

둘째, 성격과 말투입니다. 친근한지, 격식을 차리는지, 유머러스한지 등을 정의합니다.

구체적인 예시를 들어주면 더 효과적입니다. "반갑습니다!

무엇을 도와드릴까요?"처럼 실제 사용할 법한 문장을 포함시키세요. 셋째, 규칙과 제약입니다.

에이전트가 절대 하면 안 되는 것, 반드시 해야 하는 것을 명시합니다. 보안상 민감한 정보를 다루는 에이전트라면 이 부분이 특히 중요합니다.

코드에서 MessagesPlaceholder의 역할도 중요합니다. chat_history는 이전 대화 내용이 들어가는 자리입니다.

이를 통해 에이전트가 맥락을 기억하고 일관된 대화를 이어갈 수 있습니다. agent_scratchpad는 ReAct 패턴에서 추론과 행동의 기록이 쌓이는 곳입니다.

실무에서 시스템 프롬프트를 작성할 때 흔히 하는 실수가 있습니다. 너무 추상적으로 작성하는 것입니다.

"친절하게 응대하세요"보다 "고객의 불만에 먼저 공감을 표현하고, 그 다음 해결책을 제시하세요"가 훨씬 효과적입니다. 구체적인 행동 지침을 줄수록 에이전트가 일관되게 행동합니다.

또 다른 실수는 시스템 프롬프트를 한 번 작성하고 방치하는 것입니다. 에이전트를 운영하다 보면 예상치 못한 상황들이 발생합니다.

그때마다 시스템 프롬프트를 업데이트해야 합니다. 마치 회사의 업무 매뉴얼이 계속 개정되는 것처럼, 시스템 프롬프트도 살아있는 문서입니다.

김개발 씨는 시스템 프롬프트를 다시 작성했습니다. 서비스의 톤앤매너 가이드를 참고하여 구체적인 말투와 응대 방식을 정의했습니다.

결과는 놀라웠습니다. 에이전트가 마치 실제 상담사처럼 자연스럽게 대화하기 시작했습니다.

실전 팁

💡 - 시스템 프롬프트는 마크다운 형식으로 구조화하면 가독성이 좋아집니다

  • 실제 예시 대화를 포함시키면 에이전트가 더 잘 따라합니다
  • 정기적으로 에이전트 응답을 검토하고 시스템 프롬프트를 개선하세요

5. 동적 프롬프트 미들웨어

김개발 씨의 에이전트는 점점 똑똑해졌지만, 새로운 문제가 생겼습니다. VIP 고객에게는 더 정중하게, 일반 고객에게는 캐주얼하게 응대해야 했습니다.

또 시간대에 따라 "좋은 아침입니다", "좋은 저녁입니다"라고 인사도 바꿔야 했습니다. "상황에 따라 프롬프트를 동적으로 바꿀 수 없을까요?"

동적 프롬프트 미들웨어는 실행 시점의 컨텍스트에 따라 프롬프트를 동적으로 생성하는 기법입니다. 마치 상황에 따라 다른 옷을 입는 것처럼, 에이전트도 상황에 맞는 프롬프트를 입을 수 있습니다.

이를 통해 하나의 에이전트가 다양한 상황에 유연하게 대응할 수 있습니다.

다음 코드를 살펴봅시다.

from datetime import datetime
from langchain_core.runnables import RunnableLambda

# 동적 프롬프트 생성 미들웨어
def create_dynamic_prompt(input_data: dict) -> dict:
    user_tier = input_data.get("user_tier", "normal")
    current_hour = datetime.now().hour

    # 시간대별 인사말 설정
    if 5 <= current_hour < 12:
        greeting = "좋은 아침입니다"
    elif 12 <= current_hour < 18:
        greeting = "안녕하세요"
    else:
        greeting = "좋은 저녁입니다"

    # 고객 등급별 톤 설정
    tone = "격식을 갖춘 정중한" if user_tier == "vip" else "친근하고 캐주얼한"

    # 동적으로 시스템 프롬프트 구성
    input_data["system_context"] = f"{greeting}! {tone} 말투로 응대하세요."
    return input_data

# 미들웨어를 체인에 연결
chain = RunnableLambda(create_dynamic_prompt) | agent_executor

김개발 씨는 고민에 빠졌습니다. 요구사항은 계속 늘어나는데, 그때마다 에이전트를 따로 만들 수는 없었습니다.

VIP용 에이전트, 일반용 에이전트, 아침용 에이전트, 저녁용 에이전트... 이렇게 만들다간 관리가 불가능해질 것이 뻔했습니다.

박시니어 씨가 힌트를 주었습니다. "프롬프트를 고정값으로 두지 말고, 함수로 만들어봐.

입력에 따라 다른 프롬프트를 생성하도록 하는 거야." 이것이 바로 동적 프롬프트 미들웨어의 핵심 아이디어입니다. 미들웨어라는 용어가 낯설게 느껴질 수 있습니다.

쉽게 말해, 입력과 에이전트 사이에 끼어들어 입력을 가공하는 중간 레이어입니다. 마치 호텔의 컨시어지처럼, 손님이 오면 먼저 신분을 확인하고 그에 맞는 서비스를 준비하는 역할입니다.

코드의 흐름을 따라가 보겠습니다. create_dynamic_prompt 함수가 미들웨어 역할을 합니다.

먼저 사용자의 등급(user_tier)을 확인합니다. 그리고 현재 시간을 체크하여 적절한 인사말을 선택합니다.

마지막으로 이 정보들을 조합하여 시스템 컨텍스트를 동적으로 생성합니다. RunnableLambda는 일반 파이썬 함수를 LangChain의 실행 체인에 연결할 수 있게 해주는 래퍼입니다.

이를 통해 파이프(|) 연산자로 미들웨어와 에이전트를 연결할 수 있습니다. 실무에서 동적 프롬프트가 빛을 발하는 상황은 많습니다.

글로벌 서비스라면 사용자의 언어에 따라 프롬프트 언어를 바꿀 수 있습니다. B2B 서비스라면 기업 고객마다 다른 브랜딩을 적용할 수 있습니다.

이벤트 기간에는 프로모션 관련 안내를 프롬프트에 추가할 수도 있습니다. 더 고급 활용법도 있습니다.

데이터베이스에서 사용자의 과거 구매 이력을 조회하여 프롬프트에 포함시킬 수 있습니다. "이 고객은 전자제품을 자주 구매하니, 관련 상품 추천에 집중하세요"와 같은 맞춤형 지시를 동적으로 추가하는 것입니다.

주의할 점은 프롬프트가 너무 길어지지 않도록 하는 것입니다. 동적으로 추가하는 컨텍스트가 많아지면 프롬프트가 비대해집니다.

토큰 비용도 늘어나고, 오히려 에이전트가 혼란스러워할 수 있습니다. 핵심적인 컨텍스트만 선별적으로 추가하세요.

김개발 씨는 동적 프롬프트 미들웨어를 도입한 후 에이전트 관리가 훨씬 수월해졌습니다. 하나의 에이전트로 다양한 상황을 처리할 수 있게 되었기 때문입니다.

새로운 요구사항이 생겨도 미들웨어에 조건만 추가하면 되었습니다.

실전 팁

💡 - 자주 바뀌는 설정은 하드코딩하지 말고 환경 변수나 설정 파일에서 읽어오세요

  • 미들웨어의 로직이 복잡해지면 별도 클래스로 분리하여 테스트하기 쉽게 만드세요
  • 동적으로 생성된 프롬프트를 로깅하여 문제 발생 시 디버깅에 활용하세요

6. 에이전트 호출 및 결과 처리

김개발 씨의 에이전트가 드디어 완성되었습니다. 이제 실제 서비스에 연동해야 합니다.

그런데 막상 코드를 작성하려니 고민이 생겼습니다. 에러가 나면 어떻게 처리하지?

응답이 너무 오래 걸리면? 결과를 어떻게 파싱해서 사용자에게 보여주지?

에이전트 호출 및 결과 처리는 완성된 에이전트를 실제 서비스에 통합하는 마지막 단계입니다. 마치 훌륭한 요리사를 고용한 후 실제 레스토랑에서 손님을 받는 것과 같습니다.

호출, 에러 처리, 타임아웃, 결과 파싱 등을 제대로 구현해야 안정적인 서비스가 가능합니다.

다음 코드를 살펴봅시다.

from langchain.agents import AgentExecutor
import asyncio

# AgentExecutor 생성 - 에이전트 실행의 핵심
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # 디버깅을 위한 상세 로그
    max_iterations=5,  # 무한 루프 방지
    handle_parsing_errors=True  # 파싱 에러 자동 처리
)

# 동기 호출
async def invoke_agent(query: str, session_id: str):
    try:
        result = await agent_executor.ainvoke({
            "input": query,
            "chat_history": get_chat_history(session_id)
        })
        # 결과에서 최종 응답 추출
        return {"success": True, "response": result["output"]}
    except Exception as e:
        return {"success": False, "error": str(e)}

# 스트리밍 호출 - 실시간 응답 표시
async def stream_agent(query: str):
    async for chunk in agent_executor.astream({"input": query}):
        if "output" in chunk:
            yield chunk["output"]

에이전트 개발의 마지막 관문입니다. 아무리 뛰어난 에이전트라도 실제 서비스에서 안정적으로 동작하지 않으면 무용지물입니다.

김개발 씨는 박시니어 씨와 함께 프로덕션 배포를 준비하기 시작했습니다. AgentExecutor는 에이전트의 실행을 총괄하는 지휘자입니다.

에이전트 자체는 "무엇을 해야 하는지 결정"하는 두뇌이고, AgentExecutor는 "실제로 실행하고 결과를 관리"하는 몸체입니다. 도구 호출, 결과 수집, 다음 단계 진행, 종료 판단 등 모든 실행 로직이 여기에 담겨 있습니다.

코드에서 중요한 설정들을 살펴보겠습니다. max_iterations=5는 안전장치입니다.

에이전트가 무한 루프에 빠지는 것을 방지합니다. 5번의 추론-행동 사이클 안에 답을 찾지 못하면 강제로 종료합니다.

이 값은 작업의 복잡도에 따라 조절하세요. handle_parsing_errors=True는 LLM이 가끔 예상과 다른 형식으로 응답할 때 자동으로 복구를 시도합니다.

"당신의 응답을 파싱할 수 없습니다. 올바른 형식으로 다시 응답해주세요"라고 LLM에게 재요청하는 것입니다.

verbose=True는 개발 중에는 켜두고, 프로덕션에서는 끄세요. 모든 추론 과정과 도구 호출이 로그로 출력되어 디버깅에 유용하지만, 프로덕션에서는 불필요한 로그가 쌓입니다.

동기 호출과 비동기 호출의 차이도 중요합니다. **invoke()**는 동기 호출로, 결과가 나올 때까지 기다립니다.

간단한 스크립트나 배치 작업에 적합합니다. **ainvoke()**는 비동기 호출로, 여러 요청을 동시에 처리할 수 있어 웹 서비스에 적합합니다.

실제 서비스에서는 스트리밍이 사용자 경험을 크게 개선합니다. 에이전트가 생각하고 있는 과정을 실시간으로 보여주면 사용자는 기다리는 동안 지루함을 덜 느낍니다.

ChatGPT가 한 글자씩 타이핑하는 것처럼 보이는 것도 같은 원리입니다. astream() 메서드가 이를 가능하게 합니다.

에러 처리 전략도 세워야 합니다. LLM API 호출 실패, 도구 실행 오류, 타임아웃 등 다양한 예외 상황이 발생할 수 있습니다.

try-except로 감싸고, 각 상황에 맞는 사용자 친화적인 메시지를 반환하세요. "죄송합니다.

일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요." 세션 관리도 빼놓을 수 없습니다.

chat_history를 통해 이전 대화 내용을 전달하면 에이전트가 맥락을 유지합니다. 세션 ID별로 대화 기록을 저장하고 조회하는 로직이 필요합니다.

Redis나 데이터베이스를 활용하여 영속성을 확보하세요. 김개발 씨의 에이전트가 드디어 프로덕션에 배포되었습니다.

처음 며칠은 긴장의 연속이었지만, 꼼꼼히 준비한 에러 처리와 모니터링 덕분에 큰 문제 없이 운영되었습니다. 고객들의 반응도 좋았습니다.

"이제 진짜 AI 개발자가 된 것 같아요." 김개발 씨가 뿌듯하게 말했습니다.

실전 팁

💡 - 프로덕션에서는 반드시 타임아웃을 설정하세요. LLM API가 응답하지 않으면 전체 서비스가 멈출 수 있습니다

  • 에이전트 응답을 캐싱하여 동일한 질문에 빠르게 응답할 수 있도록 하세요
  • 모니터링 대시보드를 구축하여 에이전트의 성능과 에러율을 실시간으로 확인하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#LangChain#Agent#ReAct#LangGraph#LLM#SystemPrompt#AI,LLM,Python,LangChain

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.