lanchain - agent

HanJu Han·2025년 11월 16일

1. 에이전트란 무엇인가?

LangChain 문서의 정의를 쉽게 풀면 이렇게 말할 수 있습니다. ([LangChain Docs][1])

에이전트는
언어 모델(LLM) 에게
여러 도구를 쥐여주고,
그걸 언제 어떻게 쓸지 스스로 판단하게 만든 시스템이다.

보통 LLM에게 그냥 “오늘 서울 날씨 알려줘”라고 하면, 모델은 자기가 알고 있는 과거 지식만 가지고 답을 만듭니다.
하지만 에이전트는 이렇게 행동할 수 있습니다.

  1. 사용자의 질문을 읽고
  2. “아, 이건 날씨 API 도구를 써야겠구나” 하고 결정한 뒤
  3. 날씨 도구를 호출해서
  4. 돌아온 결과를 보고
  5. 최종 답을 한 번 더 정리해서 사용자에게 전달

이런 패턴을 LangChain에서는 ReAct (Reason + Act) 루프라고 부릅니다. ([LangChain Docs][1])


2. 한 장 그림으로 보는 LangChain 에이전트

에이전트의 구조를 간단히 그림으로 표현하면 대략 이런 흐름입니다.

조금 더 풀어보면

  • User: “무선 헤드폰 중 지금 인기 많은 모델 찾아서 재고도 확인해줘.”

  • Agent

    • LLM에게 “이 질문에 답하려면 어떤 도구를 쓸까?”라고 물어봄
    • LLM이 “검색 도구 → 재고 확인 도구” 순서를 스스로 계획
  • Tools

    • search_products : 인기 상품을 검색
    • check_inventory : 특정 상품 재고 확인

문서에도 비슷한 예시가 나오는데,
“무선 헤드폰 인기 모델을 찾고 재고를 확인하는” 도구 호출 흐름을 보여줍니다. ([LangChain Docs][1])


3. 에이전트를 구성하는 핵심 요소

LangChain Agents 문서에서 말하는 핵심 요소는 다음 네 가지입니다. ([LangChain Docs][1])

  1. Model: 생각하는 뇌 역할 (LLM)
  2. Tools: 손·발 역할 (검색, DB 조회, API 호출 등)
  3. System Prompt: 이 에이전트의 성격과 역할 설명서
  4. State / Memory + Middleware: 대화 기록과 부가 상태, 그리고 그걸 가공하는 중간 레이어

각각을 아주 기초부터 볼게요.


4. Model: 에이전트의 뇌

4-1. 정적인 모델 (Static model)

가장 기본적인 방식입니다.
에이전트 생성 시 한 번만 모델을 정해두고, 이후에도 계속 그 모델만 사용하는 방식입니다. ([LangChain Docs][1])

문서에서는 이런 식으로 예시를 들고 있습니다.

from langchain.agents import create_agent

agent = create_agent(
    "gpt-5",   # 또는 "openai:gpt-5" 식의 모델 식별자
    tools=tools
)

또는 조금 더 세밀하게 설정하고 싶다면 실제 모델 인스턴스를 직접 만들어서 넘깁니다. ([LangChain Docs][1])

from langchain.agents import create_agent
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model="gpt-5",
    temperature=0.1,
    max_tokens=1000,
    timeout=30,
)

agent = create_agent(model, tools=tools)

여기서 중요한 포인트

  • 정적 모델은 “어떤 질문이 들어오든 같은 모델”을 사용
  • 시작하기 제일 쉽고, 대부분의 간단한 에이전트는 이 방식으로 충분

4-2. 동적인 모델 (Dynamic model)

조금 더 고급: 상황에 따라 모델을 바꾸고 싶을 때 사용합니다.
예를 들어

  • 짧은 질문 → 싼·빠른 모델
  • 긴 복잡한 대화 → 비싸지만 성능 좋은 모델

이걸 LangChain에서는 미들웨어(middleware) 를 통해 구현합니다. 문서에서는 @wrap_model_call 데코레이터를 사용해, 대화 길이에 따라 gpt-4o-minigpt-4o를 바꾸는 예시를 보여줍니다. ([LangChain Docs][1])

구조는 대략 이런 느낌입니다.

from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse

basic_model = ChatOpenAI(model="gpt-4o-mini")
advanced_model = ChatOpenAI(model="gpt-4o")

@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
    message_count = len(request.state["messages"])

    if message_count > 10:
        request.model = advanced_model  # 긴 대화 → 강한 모델
    else:
        request.model = basic_model     # 짧은 대화 → 저렴한 모델

    return handler(request)

agent = create_agent(
    model=basic_model,
    tools=tools,
    middleware=[dynamic_model_selection]
)

정리하면

  • Static model: 한 모델로 쭉 가는 단순 구조
  • Dynamic model: 상태(state)에 따라 모델을 갈아끼우는 고급 패턴

5. Tools: LLM에게 손·발 달아주기

5-1. 도구가 왜 필요한가?

LLM은 원래 “텍스트를 생성하는 모델”일 뿐,
직접 데이터베이스를 조회하거나 API를 호출하지 못합니다.

에이전트에서 Tools는 다음 역할을 합니다. ([LangChain Docs][1])

  • 여러 도구를 순차적으로 호출
  • 때로는 병렬 호출
  • 실패 시 재시도·에러 처리
  • 툴 호출 사이에 상태(state)를 유지

5-2. 도구 정의하기

가장 기본적인 형태는 “파이썬 함수에 @tool 데코레이터를 붙이는 것”입니다. ([LangChain Docs][1])

from langchain.tools import tool
from langchain.agents import create_agent

@tool
def search_product(query: str) -> str:
    """상품 검색"""
    return f"검색 결과: {query} 관련 상위 5개 상품을 찾았습니다."

@tool
def get_weather(location: str) -> str:
    """날씨 조회"""
    return f"{location}의 현재 날씨: 맑음, 23도"

tools = [search_product, get_weather]

agent = create_agent(model, tools=tools)

이렇게 등록해 두면, LLM은 대화 중에

  • “지금은 검색 도구를 쓰자”
  • “이제 날씨 도구를 쓰자”

와 같은 판단을 하고 search_product, get_weather를 호출할 수 있습니다. ([LangChain Docs][1])


5-3. 도구 에러 핸들링

실제 서비스에서는

  • API 타임아웃
  • 예외 발생
  • 잘못된 인자 전달

같은 에러가 자주 납니다.
아래는@wrap_tool_call 미들웨어로 에러를 잡아서 LLM이 이해할 수 있는 형태의 메시지로 바꿔 주는 예시를 보여줍니다. ([LangChain Docs][1])

from langchain.tools import tool
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call, ToolRequest, ToolResponse
from langchain_core.messages import ToolMessage
from langchain_openai import ChatOpenAI

# 1) 원래의 도구 정의
@tool
def divide(a: float, b: float) -> float:
    """a를 b로 나누어 결과를 반환합니다."""
    return a / b

tools = [divide]

# 2) wrap_tool_call 미들웨어 정의
@wrap_tool_call
def safe_tool_executor(request: ToolRequest, handler) -> ToolResponse:
    """
    - request: 어떤 도구를 어떤 인자로 호출하려는지 정보가 들어 있음
    - handler: 실제로 도구를 실행하는 함수
    """
    try:
        # 실제 도구 실행
        response = handler(request)
        return response

    except ZeroDivisionError as e:
        # 0으로 나누기 같은 에러를 잡아서 LLM이 이해할 수 있는 메시지로 변환
        error_msg = ToolMessage(
            content="0으로 나누기는 불가능합니다. 분모(b)에 0이 아닌 값을 넣어주세요.",
            tool_call_id=request.id,
        )
        return ToolResponse(output=[error_msg])

    except Exception as e:
        # 그 외 에러에 대한 일반적인 처리
        error_msg = ToolMessage(
            content=f"도구 실행 중 에러가 발생했습니다: {str(e)}",
            tool_call_id=request.id,
        )
        return ToolResponse(output=[error_msg])


# 3) 에이전트 생성
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)

agent = create_agent(
    model=model,
    tools=tools,
    middleware=[safe_tool_executor],  # 여기서 wrap_tool_call 미들웨어를 등록
)

# 4) 에이전트 호출 예시
result = agent.invoke({
    "messages": [
        {"role": "user", "content": "10을 0으로 나눠줘"}
    ]
})

print(result["messages"][-1].content)

핵심 아이디어는

  • 도구 내부에서 에러 발생 → 그냥 크래시 시키지 말고
  • ToolMessage 형태로 “이런 에러가 났다”고 모델에게 알려주기
  • 모델이 그걸 보고 “입력 다시 물어보기”, “다른 도구 사용 시도하기” 같은 대응을 하도록 만드는 것

5-4. ReAct 루프: 생각하고, 도구 쓰고, 또 생각하기

문서에서는 에이전트가 도구를 어떻게 사용하는지 ReAct 루프 예시로 설명합니다. ([LangChain Docs][1])

예를 들어:

  1. 사용자: “지금 인기 많은 무선 헤드폰 찾아주고, 재고 있는지도 알려줘.”

  2. 모델의 내부 단계

    • Reasoning: “인기는 시점에 따라 다르니 검색 도구를 써야겠다.”
    • Acting: search_products("무선 헤드폰") 호출
  3. 도구 응답: “WH-1000XM5, …”

  4. 모델의 내부 단계

    • Reasoning: “1위 모델 재고를 확인해야겠다.”
    • Acting: check_inventory("WH-1000XM5") 호출
  5. 도구 응답: “재고 10개”

  6. 최종 답변 생성: “현재 가장 인기 있는 모델은 WH-1000XM5이고, 재고는 10개입니다.”

이 과정을 시각적으로 그리면:

이게 바로 Agents 문서에서 말하는 “도구를 루프 안에서 반복적으로 사용하는 에이전트”입니다. ([LangChain Docs][1])


6. System Prompt: 에이전트의 성격 설정

에이전트에게 “어떤 방식으로 답해야 하는지”를 알려주는 설명서입니다. ([LangChain Docs][1])

6-1. 기본 system prompt

가장 단순하게는 system_prompt 문자열 하나를 넘깁니다.

agent = create_agent(
    model,
    tools,
    system_prompt="당신은 간결하고 정확하게 답하는 유능한 비서입니다."
)
  • 이 문장을 기반으로 에이전트가 “나는 어떻게 행동해야 하지?”를 이해하게 됩니다.
  • 안 넘기면, 대화 메시지 내용만 보고 스스로 역할을 추론합니다. ([LangChain Docs][1])

6-2. 동적 system prompt (dynamic_prompt)

조금 더 고급: “상황에 따라 system prompt를 바꾸고 싶다”면 @dynamic_prompt 데코레이터를 사용합니다. ([LangChain Docs][1])

문서에서는 예를 들어

  • user_role"expert" → 기술적으로 깊게 설명
  • "beginner" → 최대한 쉽게 설명

처럼 runtime context에 따라 프롬프트를 다르게 만드는 예시를 보여줍니다.

아이디어만 간단히 표현하면:

from typing import TypedDict
from langchain.agents import create_agent
from langchain.agents.middleware import dynamic_prompt, ModelRequest

class Context(TypedDict):
    user_role: str

@dynamic_prompt
def user_role_prompt(request: ModelRequest) -> str:
    role = request.runtime.context.get("user_role", "user")
    base = "You are a helpful assistant."
    if role == "expert":
        return base + " Provide detailed technical responses."
    elif role == "beginner":
        return base + " Explain concepts simply and avoid jargon."
    return base

agent = create_agent(
    model="gpt-4o",
    tools=[...],
    middleware=[user_role_prompt],
    context_schema=Context
)

이렇게 하면

  • 같은 에이전트여도, context={"user_role": "expert"} / "beginner"에 따라 행동 방식이 달라짐 ([LangChain Docs][1])

7. 에이전트 호출: invoke와 stream

7-1. 기본 호출: invoke

에이전트는 상태(State) 안에 messages라는 대화 기록을 가지고 있습니다. ([LangChain Docs][1])

가장 기본적인 호출은:

result = agent.invoke(
    {"messages": [{"role": "user", "content": "서울 날씨 알려줘"}]}
)
  • 에이전트는 이 메시지를 보고 필요하면 도구를 호출하고
  • 최종적으로 답변이 들어 있는 result를 반환합니다. ([LangChain Docs][1])

7-2. 진행 상황 스트리밍: stream

에이전트가 여러 단계(도구 호출 → reasoning → 또 도구 호출)를 거치는 동안,
중간 상황을 실시간으로 보고 싶다면 stream을 사용합니다. ([LangChain Docs][1])

문서 예시는 이런 식입니다. ([LangChain Docs][1])

for chunk in agent.stream(
    {
        "messages": [{"role": "user", "content": "AI 관련 뉴스를 찾고 요약해줘"}]
    },
    stream_mode="values",
):
    latest = chunk["messages"][-1]
    if latest.content:
        print("Agent:", latest.content)
    elif latest.tool_calls:
        print("Calling tools:", [tc["name"] for tc in latest.tool_calls])

이렇게 하면

  • 도구를 호출할 때마다 “지금 어떤 도구를 부르는지”
  • 중간 reasoning 메시지가 나오면 그 텍스트를
  • 실시간으로 터미널에 출력할 수 있습니다.

8. Structured Output: 결과를 깔끔한 구조로 받기

때로는 “자연어 문장”이 아니라, 딱 정해진 필드로 결과를 받고 싶을 때가 있습니다.

예:
“이 문장에서 이름, 이메일, 전화번호를 추출해줘.”

이럴 때는 LangChain이 제공하는 structured output 기능을 사용합니다. ([LangChain Docs][1])

8-1. ToolStrategy

ToolStrategy는 “가상의 도구 호출”을 이용해서 구조화된 결과를 만드는 방법입니다.
어떤 tool-calling 지원 모델이든 사용할 수 있습니다. ([LangChain Docs][1])

from pydantic import BaseModel
from langchain.agents import create_agent
from langchain.agents.structured_output import ToolStrategy

class ContactInfo(BaseModel):
    name: str
    email: str
    phone: str

agent = create_agent(
    model="gpt-4o-mini",
    tools=[search_tool],
    response_format=ToolStrategy(ContactInfo)
)

result = agent.invoke({
    "messages": [{
        "role": "user",
        "content": "John Doe, john@example.com, (555) 123-4567에서 연락처를 뽑아줘"
    }]
})
structured = result["structured_response"]  # ContactInfo 인스턴스

8-2. ProviderStrategy

ProviderStrategy는 OpenAI 같은 제공자 자체의 structured output 기능을 활용합니다.
더 안정적인 대신, 해당 기능을 지원하는 모델에서만 사용할 수 있습니다. ([LangChain Docs][1])

from langchain.agents.structured_output import ProviderStrategy

agent = create_agent(
    model="gpt-4o",
    response_format=ProviderStrategy(ContactInfo)
)

참고: langchain 1.0부터는 단순히 response_format=ContactInfo처럼 직접 스키마만 넘기는 방식은 지원되지 않고, 반드시 ToolStrategy 또는 ProviderStrategy를 써야 합니다. ([LangChain Docs][1])


9. Memory와 State: 에이전트의 단기 기억

Agents 문서에서 말하는 “Memory”는 사실 에이전트의 State 개념과 연결됩니다. ([LangChain Docs][1])

기본적으로

  • 모든 에이전트는 AgentState 안에 messages를 포함
  • 이게 곧 “대화 기록”이자 단기 기억

여기에 더해서

  • 유저 선호도(예: 상세/간단, 한국어/영어)
  • 세션 내 설정 값

등을 함께 저장하고 싶다면 커스텀 state schema를 정의할 수 있습니다. ([LangChain Docs][1])

두 가지 방식이 있습니다.

  1. Middleware로 state 정의 (권장)

    • 특정 미들웨어/툴에서 사용하는 상태를 함께 묶어서 관리
  2. state_schema 인자로 AgentState를 상속한 TypedDict 전달

예시 구조는 문서에 다음과 같이 나옵니다. ([LangChain Docs][1])

  • CustomState(AgentState)user_preferences: dict 추가
  • invoke 시에 {"user_preferences": {...}} 같이 넘겨서 사용

10. Middleware: 에이전트를 자유자재로 커스터마이징

마지막으로, 문서 끝에서 미들웨어(Middleware) 를 중요한 확장 지점으로 소개합니다. ([LangChain Docs][1])

미들웨어로 할 수 있는 일들:

  • 모델 호출 전에 상태를 가공 (예: 오래된 메시지 trimming, 컨텍스트 주입)
  • 모델 응답을 검사·수정 (예: 필터링, 규칙 위반 감지)
  • 도구 실행 에러를 일괄 처리 (@wrap_tool_call)
  • 동적 모델 선택 (@wrap_model_call)
  • 동적 시스템 프롬프트 (@dynamic_prompt)

즉, 에이전트의 생명주기 전체에서 “가로채서 가공할 수 있는 훅(hook)”을 제공하는 레이어라고 보면 됩니다.


11. 정리: LangChain Agents를 이해하는 최소 개념

정리해보면, LangChain Agents 문서의 핵심은 다음 네 줄로 요약할 수 있습니다.

  1. 에이전트 = LLM + Tools + ReAct 루프
  2. create_agent(model, tools, ...) 로 생산 준비된 에이전트를 만들 수 있다. ([LangChain Docs][1])
  3. 모델, 도구, 시스템 프롬프트, 상태(State)를 미들웨어로 동적으로 제어할 수 있다.
  4. invoke로 한 번에 결과를 받거나, stream으로 중간 상태를 스트리밍 받을 수 있다. ([LangChain Docs][1])
profile
시리즈를 기반으로 작성하였습니다.

0개의 댓글