
LangChain Agent에서 Middleware는 “루프를 가로채서” 상태/모델/도구를 바꾸는 강력한 레버였습니다.
이번 편에서는 그 개념을 실제로 활용해 Dynamic Prompt / Dynamic Model / Dynamic Tools를 구현하는 방법과, Sub Agent를 라우팅하여 활용하는 방법을 다룹니다.
LLM의 응답 품질을 안정적으로 높이려면 “시스템 프롬프트를 고정하지 않는 것”이 중요합니다.
사용자 속성(언어, 역할, 권한 등)에 따라 시스템 프롬프트를 동적으로 생성할 수 있습니다.
from dataclasses import dataclass
from langchain.agents.middleware import dynamic_prompt, ModelRequest
@dataclass
class LanguageContext:
user_language: str = "English"
@dynamic_prompt
def user_language_prompt(request: ModelRequest) -> str:
"""Generate system prompt based on user role."""
user_language = request.runtime.context.user_language
base_prompt = "You are a helpful assistant."
if user_language != "English":
return f"{base_prompt} only respond in {user_language}."
elif user_language == "English":
return base_prompt
dynamic_prompt는 매 루프마다 시스템 프롬프트를 재생성합니다.request.runtime.context를 통해 사용자 특성 기반 프롬프트를 만들 수 있습니다.언어, 레벨, 권한, 업무 도메인 등에 따라 prompt를 바꾸는 패턴이 가장 효과적입니다.짧은 대화는 저렴한 모델, 긴 대화는 큰 컨텍스트 모델로 갈아끼워, 비용과 성능을 동시에 관리할 수 있습니다.
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from langchain.chat_models import init_chat_model
from typing import Callable
large_model = init_chat_model("claude-sonnet-4-5")
standard_model = init_chat_model("gpt-5-nano")
@wrap_model_call
def state_based_model(request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
"""Select model based on State conversation length."""
# request.messages is a shortcut for request.state["messages"]
message_count = len(request.messages)
if message_count > 10:
# Long conversation - use model with larger context window
model = large_model
else:
# Short conversation - use efficient model
model = standard_model
request = request.override(model=model)
return handler(request)
비용, 컨텍스트 길이, 속도를 기준으로 모델 라우팅 룰을 만드는 방식이 안정적입니다.도구도 “모두에게 항상 열어둘 필요”가 없습니다.
예: 내부 직원은 SQL 조회 가능, 외부 고객은 검색만 허용 등.
from langchain.agents.middleware import wrap_model_call, ModelRequest, ModelResponse
from typing import Callable
@wrap_model_call
def dynamic_tool_call(request: ModelRequest,
handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
"""Dynamically call tools based on the runtime context"""
user_role = request.runtime.context.user_role
if user_role == "internal":
pass # internal users get access to all tools
else:
tools = [web_search] # external users only get access to web search
request = request.override(tools=tools)
return handler(request)
def build_graph():
research_graph = build_subgraph(analysis_node, "research")
action_graph = build_subgraph(action_node, "action")
answer_graph = build_subgraph(chat_node, "answer")
g = StateGraph(GraphState)
g.add_node("route", route_node)
g.add_node("research_subgraph", research_graph)
g.add_node("action_subgraph", action_graph)
g.add_node("answer_subgraph", answer_graph)
g.set_entry_point("route")
g.add_conditional_edges(
"route",
lambda s: s["route"],
{
"analysis": "research_subgraph",
"action": "action_subgraph",
"chat": "answer_subgraph",
},
)
g.add_edge("research_subgraph", END)
g.add_edge("action_subgraph", END)
g.add_edge("answer_subgraph", END)
return g.compile()
create_agent의 sub_agent는 tool call처럼 host가 sub agent 사용 여부를 판단하는 방식이지만, 이렇게 직접 구현함으로써 sub agent 사용을 강제할 수 있습니다.지금까지 LangChain/LangGraph로 에이전트를 개발하는데 활용되는 추상화 모듈들의 종류와 활용법, 작동 원리 등에 대해 알아봤습니다.
사실 LangChain Agent 시리즈를 빨리 끝내고 LangChain에서 새로 출시된 deepagents에 대한 시리즈를 이어서 작성하려고 했는데, 지금까지의 내용만 제대로 이해했다면 포스팅 하나 정도로 충분히 소개할 수 있을 것 같습니다.
따라서 다음 포스팅에서는 번외편 - LangChain Deepagent에 대한 전반적인 내용을 한 번에 소개해보도록 하겠습니다.