온라인강의

랭체인 Agent, Tools, 구조화된 출력 정리

AI강선생 2026. 2. 9. 00:31

이번에는 지난번 '랭체인 1.0 설치 및 랭체인 기본 지식'에 이서 랭체인을 활용한 Agent, Tools, 구조화된 출력를 정리하고자 합니다. 

 

관련해서 수강한 강의는  인프런에서 오영재 강사님의 'LangChain version 1.0 을 활용한 생성형 AI 서비스 구축' 2번째 정리입니다. 


 

15. Agent 개요

  LLM을 핵심 엔진으로 사용하여 주어진 목표를 달성하기 위해 독립적으로작업(추론, 판단, 실행, 피드백)을 수행하는 인공지능 시스템

• 주로 LLM(대규모 언어 모델)의 능력을 기반으로 동작하며, 사용자의명령을이해하고, 판단하며, 실행

• LLM은 두뇌, Agent는 이 두뇌를 활용해 행동을 실행하는 작업자 역할

 

Agent핵심 기술 스택

• LLM (예: GPT, Claude): 언어 이해 및 생성

• LangChain / LangGraph: 에이전트 워크플로우 및 상태 관리

• Vector Database (예: Chroma, Pinecone): 정보 저장 및 검색

• Memory: 상태 유지 및 맥락 관리

• API 통합 (예: Tavily, SerpAPI): 외부 도구와 연결

 

Agent Tools Overview

• LLM이 정의된 함수/도구를 호출해 결과를 얻도록 유도

• @tool 데코레이터로 Python 함수를 도구 등록

• Agent 생성 + create_agent의 tools 파라미터로 모델에 연결

• 함수 실행 결과는 ToolMessage로 agent가 모델에 재 전달

• 모델이 도구 호출 결과를 반영해 최종 답변 작성

• 동적 모델 선택 • 도구 오류 처리

• 동적 prompt 제공

• 구조화된 출력 (Structured Output) 요청

 

16. Agent, Tools, 구조화된 출력 Overview - Part1

1) @tool을 활용한 기본 tool 설정

랭체인에서 가장 간단하게 도구를 만드는 방법은 @tool 데코레이터를 사용하는 것입니다. @tool은 **파이썬의 데코레이터(decorator)**이며, LangChain 프레임워크에서 해당 함수를 LLM(대형언어모델)이 사용할 수 있는 “도구(tool)”로 등록하는 역할을 합니다.

Type hints 는 필수입니다. 이들은 도구의 입력 스키마(input schema) 를 정의하기 때문입니다.
독스트링(docstring) 은 모델이 도구의 목적을 이해할 수 있도록 간결하면서도 유용한 정보를 포함해야 합니다.

 

1️⃣ @tool의 정식 명칭

구분설명
명칭 데코레이터(Decorator)
파이썬 개념 함수 정의 위에 붙어서 기존 함수의 동작을 확장하거나 수정하는 문법
형태 @something
실제 의미 search_db = tool(search_db) 와 동일

즉,

 
@tool
def search_db(...):

는 내부적으로 아래와 같은 의미입니다.

 

def search_db(...):

    ...

search_db = tool(search_db)

 

2️⃣ 데코레이터(Decorator)란 무엇인가

**데코레이터는 기존 함수를 수정하지 않고 기능을 덧붙이는 래퍼(wrapper)**입니다.

쉽게 말하면:

기존 함수데코레이터 적용 후
일반 파이썬 함수 추가 기능이 붙은 함수
단순 실행 실행 전후 처리 가능
메타정보 없음 설명, 타입정보 등 추가 가능

예시 개념:

 
@log
def func():
    pass

→ 실행할 때 자동으로 로그가 남도록 기능이 추가됨.

 

3️⃣ LangChain에서 @tool이 하는 일 (핵심)

LangChain에서 @tool은 단순한 데코레이터가 아니라 LLM Agent가 호출 가능한 외부 기능으로 변환하는 역할을 합니다.

✅ 주요 기능

기능설명
Tool 등록 해당 함수를 Agent가 사용할 수 있는 도구로 등록
설명 자동 추출 docstring(함수 설명)을 LLM이 읽을 수 있게 변환
입력 스키마 생성 함수 인자 타입을 기반으로 입력 형식 정의
LLM 호출 가능 모델이 판단해서 함수 실행 가능
함수 → API화 LLM 입장에서 하나의 기능 API처럼 동작
from langchain.tools import tool

@tool
def search_db(query: str, limit: int = 10) -> str:
    """검색어(query)에 해당하는 고객 데이터베이스 레코드를 조회합니다.

    Args:
        query: 검색할 키워드 또는 문장
        limit: 반환할 최대 결과 개수
    """
    return f"'{query}'에 대한 검색 결과 {limit}개를 찾았습니다."

search_db

 

5️⃣ 왜 docstring이 중요한가

여기 부분이 실제로 매우 중요합니다.

 
"""검색어(query)에 해당하는 고객 데이터베이스 레코드를 조회합니다."""

이 설명은:

  • 사람이 보는 주석이 아니라
  • LLM이 tool 선택을 할 때 사용하는 설명

입니다.

Agent는 다음과 같이 이해합니다.

"검색 관련 질문이면 search_db를 사용해야 한다"

 

위와 같은 형태의 랭체인 코드는 결국 각 LLM사의 Function calling 함수로 변환해서 수행됩니다. 

아래는 openai의 Function calling 으로 변환시킨 코드입니다.

tools = [
    {
        "type": "function",
        "function": {
            "name": "search_db",
            "description": "검색어(query)에 해당하는 고객 데이터베이스 레코드를 조회합니다.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "검색할 키워드 또는 문장"
                    },
                    "limit": {
                        "type": "integer",
                        "description": "반환할 최대 결과 개수",
                        "default": 10
                    }
                },
                "required": ["query"]
            }
        }
    }
]

 

2) Pydantic모델을 통한 고급 스키마 정의를 통한 툴 설정

 

from pydantic import BaseModel, Field
from langchain_core.tools import tool
import requests

# 입력 데이터 구조 정의 (Pydantic 사용)
class WeatherInput(BaseModel):
    """날씨 질의에 사용할 입력 스키마"""
    latitude: float = Field(description="질의할 지역의 위도를 입력합니다.")
    longitude: float = Field(description="질의할 지역의 경도를 입력합니다.")

# 현재의 온도 가져오기
@tool(args_schema=WeatherInput)
def get_weather(latitude, longitude):
    """
    제공된 좌표의 현재 기온을 섭씨(Celsius) 단위로 가져옵니다.
    """
    print('get_weather 도구 호출됨')
    response = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m")
    data = response.json()
    return data['current']['temperature_2m']

# 서울의 위도, 경도
get_weather.invoke({'latitude': 37.56667, 'longitude': 126.97806})

 

get_weather는 러너블이기 때문에 .invoke를 해야 출력이 됨

 

3)ReAct Agent

LangChain에서 말하는 ReAct Agent는 단순한 이름이 아니라, LLM이 “생각(Reasoning)”과 “행동(Action)”을 반복하면서 문제를 해결하도록 만든 에이전트 구조를 의미합니다. 과거에는 react agent(reasoning & act )라고 했지만, 랭체인 1.0부터는 그냥 agent라고 불립니다.

from langchain.agents import create_agent

# ReAct 에이전트 생성
agent = create_agent(
    model=model,
    tools=[tavily, search_db, calc, get_weather]  # Agent가 사용할 도구 목록
)

agent

# 날씨 조회 예제
result = agent.invoke(
    {"messages": [
        {'role': 'system', "content": "당신은 도움이 되는 어시스턴트입니다. 주어진 도구를 이용해 답변하세요."},
        {"role": "user", "content": "지금 서울 기온이 몇도인가요?"}
    ]}
)

result['messages'][-1].pretty_print()

 

messages 라는 키를 갖는 딕셔너리를 만들어서 invoke해야 하는데, 왜냐하면 모든 노드(함수)들의 상태(state)를 갖는데 이 state의 키가 messages

 

 

17. Agent, Tools, 구조화된 출력 Overview - Part2

4) 동적 모델(Dynamic Model)

from langchain_openai import ChatOpenAI
from langchain_google_genai import ChatGoogleGenerativeAI

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

# basic_model = ChatOpenAI(model="gpt-5-nano")
# advanced_model = ChatOpenAI(model="gpt-5-mini")

basic_model = ChatGoogleGenerativeAI(model="gemini-2.5-pro")
advanced_model = ChatGoogleGenerativeAI(model="gemini-2.5-flash")

@wrap_model_call
def dynamic_model_selection(request: ModelRequest, handler) -> ModelResponse:
    """대화의 복잡도에 따라 사용할 모델을 동적으로 선택"""

    # 현재 대화에서 주고받은 메시지 개수를 계산
    message_count = len(request.state["messages"])

    # 메시지가 10개를 초과하면 복잡한 대화로 간주 → 고급 모델 사용
    if message_count > 10:
        # 긴 대화일 경우 고급(Advanced) 모델 사용
        model = advanced_model
    else:
        # 짧은 대화일 경우 기본(Basic) 모델 사용
        model = basic_model

    # 선택된 모델을 request에 설정
    request.model = model

    # handler를 호출하여 요청 처리 계속 진행
    return handler(request)

agent_dynamic = create_agent(
    model=basic_model,  # Default model
    tools=[search_db, calc, get_weather],
    middleware=[dynamic_model_selection]
)

agent_dynamic

 

이 코드는 LangChain Agent에서 실행 시점(runtime)에 사용할 LLM 모델을 자동으로 바꾸는 구조, 즉 **동적 모델 선택(Dynamic Model Selection)**을 구현한 예시입니다. 핵심은 대화 상태(state)와 컨텍스트(context)를 보고 비용과 성능을 동시에 최적화하는 것입니다.

아래는 구조 → 동작 흐름 → 실제 의미 순서로 정리합니다.


✅ 1️⃣ 동적 모델(Dynamic Model)이란 무엇인가

동적 모델은 다음 의미입니다.

구분설명
정적 모델 (Static model) 항상 동일한 모델 사용
동적 모델 (Dynamic model) 상황에 따라 모델을 변경

즉,

  • 간단한 질문 → 빠르고 저렴한 모델
  • 복잡한 대화 → 성능이 좋은 모델

을 자동으로 선택합니다.


✅ 2️⃣ 이 코드의 전체 구조 (한 줄 요약)

 
사용자 요청 → Agent → Middleware → 모델 선택 → 모델 실행

Agent가 바로 모델을 호출하는 것이 아니라, 중간에 Middleware가 끼어들어 모델을 바꿉니다.


✅ 3️⃣ 주요 구성 요소 설명

✅ (1) 기본 모델 / 고급 모델 정의

 
basic_model = ChatGoogleGenerativeAI(model="gemini-2.5-pro")
advanced_model = ChatGoogleGenerativeAI(model="gemini-2.5-flash")
모델역할
basic_model 기본 처리용 (비용 절약)
advanced_model 복잡한 요청 처리용

실무에서는 보통:

  • nano / flash → 빠르고 저렴
  • mini / pro → 느리지만 정확

구조로 나눕니다.


✅ (2) @wrap_model_call 데코레이터

 
@wrap_model_call

이것은 모델 호출 직전에 실행되는 미들웨어를 만드는 데코레이터입니다.

쉽게 말하면:

역할설명
위치 LLM 호출 직전
기능 요청 수정 가능
변경 가능 항목 모델, 파라미터, 메시지 등

즉,

👉 "모델을 실행하기 전에 마지막으로 개입할 수 있는 지점"

입니다.


✅ (3) ModelRequest 객체

 
request: ModelRequest

여기에는 현재 Agent 상태가 들어 있습니다.

대표적으로:

항목의미
request.state["messages"] 지금까지의 대화 전체
request.model 현재 사용할 모델
request.tools 사용 가능한 tool

✅ (4) 실제 모델 선택 로직

 
message_count = len(request.state["messages"])

현재까지 대화 메시지 개수를 확인합니다.

 
if message_count > 10: model = advanced_model else: model = basic_model

의미:

조건선택 모델
메시지 ≤ 10 기본 모델
메시지 > 10 고급 모델

즉,

  • 대화가 길어질수록 문맥 이해가 어려워짐
  • 더 성능 좋은 모델로 자동 전환

✅ (5) handler(request)

 
return handler(request)

이 부분이 중요합니다.

개념설명
handler 다음 처리 단계
역할 실제 모델 실행

middleware는 가로채서 수정만 하고 다시 흐름을 넘겨주는 역할입니다.


✅ 4️⃣ 전체 실행 흐름 (실제 동작 순서)

 
1️⃣ 사용자 질문 입력 ↓ 2️⃣ Agent 실행 시작 ↓ 3️⃣ dynamic_model_selection 실행 ↓ 4️⃣ 메시지 개수 확인 ↓ 5️⃣ 모델 선택 (basic or advanced) ↓ 6️⃣ 선택된 모델로 LLM 호출 ↓ 7️⃣ 결과 반환

✅ 5️⃣ 왜 이런 구조를 쓰는가 (실무적 이유)

LLM 운영에서 가장 큰 문제는 다음입니다.

문제설명
비용 증가 항상 고급 모델 사용 시 비용 폭증
성능 부족 항상 저가 모델 사용 시 품질 저하

동적 모델은 이를 해결합니다.

상황선택
단순 질문 저비용 모델
긴 분석 대화 고성능 모델

 성능 대비 비용 최적화(cost-performance optimization) 구조입니다.


✅ 핵심 요약

항목의미
Dynamic Model 실행 시점에 모델을 변경
@wrap_model_call 모델 호출 전 개입하는 미들웨어
ModelRequest 현재 Agent 상태 정보
핵심 목적 비용 절감 + 성능 유지
동작 방식 상태 기반 모델 라우팅

이 코드는 LangChain Agent에서 실행 시점(runtime)에 사용할 LLM 모델을 자동으로 바꾸는 구조, 즉 **동적 모델 선택(Dynamic Model Selection)**을 구현한 예시입니다. 핵심은 대화 상태(state)와 컨텍스트(context)를 보고 비용과 성능을 동시에 최적화하는 것입니다.

아래는 구조 → 동작 흐름 → 실제 의미 순서로 정리합니다.


5) 동적 시스템 프롬프트

실행 시점의 컨텍스트(runtime context) 나 에이전트 상태(agent state) 에 따라 시스템 프롬프트를 동적으로 변경해야 하는 고급 사용 사례에서는 미들웨어(middleware) 를 사용할 수 있습니다.
@dynamic_prompt 데코레이터를 사용하면, 모델 요청(model request) 에 따라 시스템 프롬프트를 동적으로 생성하는 미들웨어를 만들 수 있습니다.

ModelRequest 안에는 모델 호출에 필요한 모든 정보가 들어 있습니다. (예: 모델 이름, 입력 메시지들, 현재까지의 내부 상태, 도구 사용 여부, 런타임(runtime) 객체 등)

request.runtime은 LangChain v1의 Agents SDK / Middleware 시스템에서 “현재 실행 중인 에이전트 호출의 런타임 상태(runtime state)” 를 담고 있는 객체입니다.

즉, agent.invoke()가 실행되는 순간의 컨텍스트(context), 도구 호출 정보, 메시지 히스토리 등을 담고 있으며, 미들웨어(dynamic_prompt 등)가 이를 읽어서 동적으로 프롬프트나 행동을 바꾸는 데 사용합니다.

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:
    """사용자 역할(user role)에 따라 시스템 프롬프트를 생성"""

    # 실행 컨텍스트(runtime context)에서 사용자 역할 정보를 가져옴
    # 기본값은 "user"
    user_role = request.runtime.context.get("user_role", "user")

    # 기본 프롬프트 정의
    base_prompt = "당신은 도움이 되는 어시스턴트입니다."

    # 사용자 역할에 따라 프롬프트를 다르게 설정
    if user_role == "expert":
        # 전문가(expert)인 경우: 기술적으로 자세한 답변을 제공
        return f"{base_prompt} 기술적으로 자세하고 전문적인 답변을 제공하세요."
    elif user_role == "beginner":
        # 초보자(beginner)인 경우: 쉬운 설명과 비전문 용어 사용
        return f"{base_prompt} 개념을 쉽게 설명하고 전문 용어 사용을 피하세요."

    return base_prompt

agent_dynamic_prompt = create_agent(
    model=model,
    tools=[search_db, calc, get_weather],
    middleware=[user_role_prompt],
    context_schema=Context
)

# 실행 컨텍스트(context)에 따라 시스템 프롬프트가 동적으로 설정됨
result = agent_dynamic_prompt.invoke(
    {
        "messages": [
            {"role": "user", "content": "기계 학습(machine learning)을 한 문장으로 설명해줘."}
        ]
    },
    context={"user_role": "expert"}  # 사용자 역할을 '전문가'로 지정
)

result['messages'][-1].pretty_print()

 

6) 구조화된 출력 (Structured output)

특정 상황에서는 에이전트가 정해진 형식의 출력 결과를 반환하도록 하고 싶을 때가 있습니다.
이때 LangChain은 response_format 매개변수를 통해 구조화된 출력을 생성하는 여러 가지 방법을 제공합니다.

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_structured = create_agent(
    model=model,
    tools=[search_db, calc, get_weather],
    response_format=ToolStrategy(ContactInfo)
)

result = agent_structured.invoke({
    "messages": [{"role": "user", "content": "다음에서 연락처 정보를 추출하세요: John Doe, john@example.com, (555) 123-4567"}]
})

result["structured_response"]

final_result=dict(result["structured_response"])

final_result['name']