이번에는 지난번 '랭체인 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) 와 동일 |
즉,
는 내부적으로 아래와 같은 의미입니다.
def search_db(...):
...
search_db = tool(search_db)
2️⃣ 데코레이터(Decorator)란 무엇인가
**데코레이터는 기존 함수를 수정하지 않고 기능을 덧붙이는 래퍼(wrapper)**입니다.
쉽게 말하면:
| 일반 파이썬 함수 | 추가 기능이 붙은 함수 |
| 단순 실행 | 실행 전후 처리 가능 |
| 메타정보 없음 | 설명, 타입정보 등 추가 가능 |
예시 개념:
→ 실행할 때 자동으로 로그가 남도록 기능이 추가됨.
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이 중요한가
여기 부분이 실제로 매우 중요합니다.
이 설명은:
- 사람이 보는 주석이 아니라
- 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}¤t=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가 끼어들어 모델을 바꿉니다.
✅ 3️⃣ 주요 구성 요소 설명
✅ (1) 기본 모델 / 고급 모델 정의
| basic_model | 기본 처리용 (비용 절약) |
| advanced_model | 복잡한 요청 처리용 |
실무에서는 보통:
- nano / flash → 빠르고 저렴
- mini / pro → 느리지만 정확
구조로 나눕니다.
✅ (2) @wrap_model_call 데코레이터
이것은 모델 호출 직전에 실행되는 미들웨어를 만드는 데코레이터입니다.
쉽게 말하면:
| 위치 | LLM 호출 직전 |
| 기능 | 요청 수정 가능 |
| 변경 가능 항목 | 모델, 파라미터, 메시지 등 |
즉,
👉 "모델을 실행하기 전에 마지막으로 개입할 수 있는 지점"
입니다.
✅ (3) ModelRequest 객체
여기에는 현재 Agent 상태가 들어 있습니다.
대표적으로:
| request.state["messages"] | 지금까지의 대화 전체 |
| request.model | 현재 사용할 모델 |
| request.tools | 사용 가능한 tool |
✅ (4) 실제 모델 선택 로직
현재까지 대화 메시지 개수를 확인합니다.
의미:
| 메시지 ≤ 10 | 기본 모델 |
| 메시지 > 10 | 고급 모델 |
즉,
- 대화가 길어질수록 문맥 이해가 어려워짐
- 더 성능 좋은 모델로 자동 전환
✅ (5) handler(request)
이 부분이 중요합니다.
| handler | 다음 처리 단계 |
| 역할 | 실제 모델 실행 |
middleware는 가로채서 수정만 하고 다시 흐름을 넘겨주는 역할입니다.
✅ 4️⃣ 전체 실행 흐름 (실제 동작 순서)
✅ 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']
'온라인강의' 카테고리의 다른 글
| 에이전트 시스템 설계 및 UX디자인 (0) | 2026.02.12 |
|---|---|
| 랭체인 1.0 단기 메모리(Short-term-Memory) (0) | 2026.02.11 |
| 랭체인 1.0 설치 및 랭체인 기본 지식 (0) | 2026.02.08 |
| 초보자들을 위한 Redis 자료구조 & 활용 기초 (0) | 2026.02.07 |
| [인프런 챌린지] 4주 완성 백엔드 설계 챌린지 섹션7 타임라인 서비스(4주차) (0) | 2026.02.05 |