16. 사전 구축 컴포넌트와 다중 분기 라우팅
15장에서 우리는 에이전트 그래프를 직접 조립했습니다. LLM을 호출하는 노드, 도구를 실행하는 노드, 그리고 계속 진행할지 종료할지를 결정하는 조건부 엣지로 구성된 그래프였습니다. 사실 이 구조는 도구 호출 에이전트의 표준 구조라고 할 만큼 널리 사용됩니다. 그래서 LangChain과 LangGraph는 이 구조를 매번 직접 작성하지 않아도 되도록 사전 구축(prebuilt) 컴포넌트로 제공하고 있습니다.
이 장의 전반부에서는 사전 구축 컴포넌트로 15장에서 만든 에이전트를 다시 만들어 보겠습니다. 도구 실행 노드와 라우팅 함수를 ToolNode와 tools_condition으로 바꾸고, 마지막에는 그래프 조립 전체를 create_agent 함수 호출 하나로 대체합니다. 동작은 15장과 동일하면서도 코드가 얼마나 줄어드는지를 확인하게 될 것입니다.
후반부에서는 사전 구축 컴포넌트와 15장에서 배운 그래프 조립 방식을 혼합하여 복잡한 에이전트를 만들어 보겠습니다. 예제로 만들 에이전트는 사용자의 요청을 먼저 분류해서, 복잡한 상담이면 고성능 모델이 답하고 단순한 질문이면 저렴한 소형 모델이 답하는 고객 문의 에이전트입니다. 요청의 유형에 따라 처리 경로가 갈라지는 다중 분기 구조입니다.
16.1) 사전 구축 컴포넌트와 create_agent
이 절에서는 15장에서 만든 그래프의 tool_node 함수와 should_continue 함수를 사전 구축 컴포넌트인 ToolNode와 tools_condition으로 바꿔 보겠습니다. 그다음에는 그래프를 직접 조립하지 않고 create_agent 함수로 한 번에 만들어 보겠습니다. 각 단계에서 확인할 것은 하나입니다. 코드는 줄어들지만 에이전트의 동작은 15장과 동일하다는 것입니다.
16.1.1) ToolNode와 tools_condition
ToolNode는 도구 실행을 대신 처리해 주는 사전 구축 노드입니다. State의 마지막 메시지(LLM이 응답한 AIMessage)에 tool_calls가 있으면, 요청된 도구를 실행하고 그 결과를 ToolMessage로 만들어 messages에 추가합니다. 15장에서 작성한 tool_node 함수와 같은 일을 하는 것입니다. 여기에 더해, LLM이 여러 도구를 한 번에 요청하면 병렬로 실행하는 기능도 갖추고 있습니다.
도구 실행 중 발생하는 예외 처리도 지원합니다. ToolNode(tools, handle_tool_errors=True)처럼 설정하면 도구가 예외를 일으켜도 그래프가 중단되지 않습니다. 예외 내용이 에러 메시지를 담은 ToolMessage로 변환되어 LLM에게 전달되므로, LLM이 실패 원인을 보고 인자를 고쳐 다시 시도할 수 있습니다.
ToolNode는 도구 목록을 전달하여 생성합니다. 15장에서 만든 두 도구를 그대로 사용하겠습니다.
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""도시의 현재 날씨를 가져옵니다."""
fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
return fake_data.get(city, f"No weather data for {city}.")
@tool
def calculate(expression: str) -> str:
"""간단한 산술 표현식을 계산합니다. 예: '3 * 21'."""
return str(eval(expression)) # 주의: eval()은 보안에 취약합니다. 프로덕션에서는 사용하지 마세요.
tools = [get_weather, calculate]
# 15장의 tool_map + tool_node 함수가 이 한 줄로 대체됩니다
tool_node = ToolNode(tools)ToolNode는 전달받은 도구 목록으로 15장의 tool_map과 같은 이름-도구 매핑을 내부에 만들어 둡니다. 실행 시점에는 LLM이 요청한 도구 이름으로 이 매핑에서 도구를 찾아 호출합니다. 즉, 15장에서 직접 작성했던 tool_map 딕셔너리, tool_calls를 순회하는 for 루프, ToolMessage를 만들어 모으는 코드가 모두 ToolNode 안에 들어 있는 것입니다.
tools_condition은 15장에서 작성한 should_continue 함수를 대체하는 사전 구축 라우팅 함수입니다. 먼저 15장의 should_continue를 다시 보겠습니다.
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
"""도구를 실행할지 그래프를 종료할지 결정합니다."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return END마지막 메시지에 tool_calls가 있으면 도구 노드의 이름인 "tool_node"를 반환하고, 없으면 END를 반환했습니다. tools_condition도 똑같이 동작합니다. 다만 반환하는 노드 이름이 다릅니다. should_continue는 우리가 그래프에 등록한 이름 "tool_node"를 반환하도록 작성했지만, tools_condition은 "tools"라는 이름을 반환하도록 만들어져 있습니다.
15.2.4에서 배웠듯이 라우팅 함수가 반환하는 값은 다음에 실행할 노드의 이름입니다. 반환된 이름의 노드가 그래프에 없으면 라우팅이 실패합니다. 따라서 tools_condition을 사용할 때는 도구 노드를 "tools"라는 이름으로 등록해야 합니다.
from langgraph.prebuilt import ToolNode, tools_condition
builder.add_node("tools", ToolNode(tools)) # 노드 이름을 "tools"로 등록
builder.add_conditional_edges("llm_call", tools_condition) # "tools" 또는 END로 라우팅이렇게 하면 tools_condition이 "tools"를 반환했을 때 방금 등록한 ToolNode로 정확히 연결됩니다.
이제 나머지 부분까지 포함하여 15장의 그래프 전체를 다시 조립해 보겠습니다. 도구, State, llm_call 노드는 15장 그대로입니다.
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""도시의 현재 날씨를 가져옵니다."""
fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
return fake_data.get(city, f"No weather data for {city}.")
@tool
def calculate(expression: str) -> str:
"""간단한 산술 표현식을 계산합니다. 예: '3 * 21'."""
return str(eval(expression)) # 주의: eval()은 보안에 취약합니다. 프로덕션에서는 사용하지 마세요.
tools = [get_weather, calculate]
class AgentState(MessagesState):
llm_calls: int
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
def llm_call(state: AgentState):
"""LLM을 호출하고 응답을 리턴합니다."""
response = model_with_tools.invoke(state["messages"])
return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1,
}
builder = StateGraph(AgentState)
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools)) # 15장의 tool_node 함수 대신 ToolNode 사용
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition) # 15장의 should_continue 대신 tools_condition 사용
builder.add_edge("tools", "llm_call")
agent = builder.compile()15장의 코드와 비교해 보세요. tool_map 딕셔너리, tool_node 함수, should_continue 함수가 통째로 사라졌습니다. 이 세 가지가 하던 일은 이제 ToolNode(tools)와 tools_condition이 대신합니다. 도구 노드의 이름은 tools_condition이 라우팅하는 이름에 맞춰 "tools"로 등록했습니다. 15장과 같은 질문으로 실행해 보겠습니다.
result = agent.invoke({
"messages": [HumanMessage(content="Get the temperature in Cairo, then multiply the number by 3.")],
"llm_calls": 0,
})
print(result["messages"][-1].content)
print(f"\n총 LLM 호출 횟수: {result['llm_calls']}")출력:
Current temperature in Cairo: 31°C. Multiplied by 3 = 93.
총 LLM 호출 횟수: 315장과 동일한 결과입니다. 날씨를 조회하고, 그 결과로 계산을 수행하고, 최종 답변을 만드는 동작이 그대로 유지되면서, 우리가 직접 작성하고 관리해야 하는 코드만 줄어들었습니다.
만약 도구 노드를 "tools"가 아닌 다른 이름으로 등록하고 싶다면 어떻게 해야 할까요? 이럴 때는 add_conditional_edges의 세 번째 인자에 tools_condition의 반환값을 어느 노드로 연결할지 지정하는 딕셔너리를 전달합니다. tools_condition의 반환값은 "tools" 또는 END 두 가지이므로, 각각을 키로 하여 연결할 노드를 지정하면 됩니다. 예를 들어 도구 노드를 "run_tools"라는 이름으로 등록한다면 다음과 같이 조립합니다.
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
"llm_call",
tools_condition,
{"tools": "run_tools", END: END} # "tools" 반환 → run_tools 노드로, END 반환 → 종료
)ToolNode와 tools_condition으로 대체된 것은 그래프의 부품입니다. 노드를 추가하고 엣지로 연결하는 조립은 여전히 우리가 직접 하고 있습니다. 그렇다면 이 조립까지 대신하게 할 수는 없을까요? 그 역할을 하는 것이 다음에 만나볼 create_agent입니다.
16.1.2) create_agent
create_agent는 도구 호출 에이전트의 그래프 조립 전체를 대신해 주는 LangChain의 팩토리 함수입니다. 모델과 도구 목록을 전달하여 호출하면, 16.1.1에서 우리가 직접 조립한 것과 같은 구조의 그래프를 만들어 컴파일까지 마친 상태로 돌려줍니다. 직접 만들어 보겠습니다.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
@tool
def get_weather(city: str) -> str:
"""도시의 현재 날씨를 가져옵니다."""
fake_data = {"Tokyo": "18°C, cloudy", "Cairo": "31°C, sunny"}
return fake_data.get(city, f"No weather data for {city}.")
@tool
def calculate(expression: str) -> str:
"""간단한 산술 표현식을 계산합니다. 예: '3 * 21'."""
return str(eval(expression)) # 주의: eval()은 보안에 취약합니다. 프로덕션에서는 사용하지 마세요.
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
system_prompt="You are a helpful assistant.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Get the temperature in Cairo, then multiply the number by 3.")],
})
print(result["messages"][-1].content)출력:
Current temperature in Cairo: 31°C. Multiplied by 3 = 93.State 정의도, 노드 함수도, add_node와 add_edge도 없습니다. create_agent 호출 하나가 그 전부를 대신했고, 결과는 16.1.1과 동일합니다.
내부에서 일어나는 일도 우리가 이미 아는 그대로입니다. create_agent는 전달받은 모델로 LLM 호출 노드를 만들고, 전달받은 도구 목록으로 ToolNode를 만들고, 이 둘을 tools_condition과 루프백 엣지로 연결합니다. 결과적으로 16.1.1에서 조립한 것과 같은 루프 구조의 그래프가 만들어집니다.
파라미터를 살펴보겠습니다. 사실 create_agent는 처음이 아닙니다. 11장에서 대화형 RAG를 만들 때 사용해 보았지만, 파라미터를 자세히 다루지는 않았습니다. 여기서 하나씩 살펴보겠습니다.
model: 에이전트가 사용할 LLM입니다."openai:gpt-5-mini"처럼"프로바이더:모델명"형식의 문자열을 전달하는 것이 가장 간편합니다. 모델 파라미터를 직접 설정하고 싶다면ChatOpenAI(model="gpt-5-mini")처럼 초기화한 모델 인스턴스를 전달하면 됩니다. 내부적으로 LLM 호출 노드가 이 모델을 사용합니다.tools: 에이전트가 사용할 도구 목록입니다. 이 도구들로 내부에ToolNode가 만들어집니다.system_prompt: 에이전트의 행동 지침입니다. LLM을 호출할 때마다 메시지 목록 맨 앞에 시스템 메시지로 추가됩니다.checkpointer: 대화 상태를 저장하여 에이전트가 이전 대화를 기억할 수 있게 합니다. 11장에서InMemorySaver()와thread_id로 멀티턴 대화를 구현할 때 사용했던 그 파라미터입니다. 동작 원리는 17장에서 자세히 다룹니다.response_format: 에이전트의 최종 답변을 구조화된 출력으로 받고 싶을 때 사용합니다. 7장에서 배운 Pydantic 모델을 전달하면, 에이전트의 답변이 해당 모델의 객체로result["structured_response"]에 담겨 반환됩니다.middleware: 에이전트 실행 루프의 특정 시점에 실행할 함수를 등록합니다. 11장에서trim_old_messages를 등록했던 파라미터입니다. 아래에서 자세히 설명합니다.
response_format이 실제로 어떻게 동작하는지 확인해 보겠습니다.
from pydantic import BaseModel
from langchain.agents import create_agent
class WeatherReport(BaseModel):
city: str
temperature: str
condition: str
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
response_format=WeatherReport,
)
result = agent.invoke({
"messages": [HumanMessage(content="What's the weather in Tokyo?")],
})
print(result["structured_response"])출력:
city='Tokyo' temperature='18°C' condition='cloudy'에이전트가 get_weather 도구를 호출하여 얻은 정보를 WeatherReport 스키마에 맞는 객체로 정리하여 반환했습니다.
middleware
에이전트 루프에는 단계가 있습니다. LLM을 호출하고, 도구를 실행하고, 다시 LLM을 호출하는 단계들이 반복됩니다. 미들웨어(middleware)는 이 단계들의 앞뒤에 우리가 만든 함수를 끼워 넣어 실행하는 기능입니다. 함수를 실행할 시점은 데코레이터로 지정합니다. @before_model은 LLM을 호출하기 직전을, @after_model은 LLM 응답을 받은 직후를 의미합니다. 이 함수를 middleware 파라미터에 등록하면 지정한 시점마다 실행됩니다.
미들웨어는 11장에서 이미 사용해 보았습니다. 오래된 메시지를 잘라내는 trim_old_messages 함수에 @before_model을 붙여 등록했는데, 이 함수가 LLM을 호출하기 직전에 항상 실행되었기 때문에 매번 메시지 목록을 정리할 수 있었습니다.
미들웨어가 언제 실행되는지 확인할 수 있도록, LLM을 호출할 때마다 메시지 개수를 출력하는 미들웨어를 만들어 보겠습니다.
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
"""LLM 호출 직전마다 메시지 개수를 출력합니다."""
print(f"[before_model] LLM 호출 직전, 현재 메시지 {len(state['messages'])}개")
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
middleware=[log_llm_call],
)
result = agent.invoke({
"messages": [HumanMessage(content="Get the temperature in Cairo, then multiply the number by 3.")],
})
print(result["messages"][-1].content)출력:
[before_model] LLM 호출 직전, 현재 메시지 1개
[before_model] LLM 호출 직전, 현재 메시지 3개
[before_model] LLM 호출 직전, 현재 메시지 5개
Current temperature in Cairo: 31°C. Multiplied by 3 = 93.log_llm_call 미들웨어 함수가 세 번 실행되었습니다. 사용자의 요청을 처리하는 동안 LLM이 세 번 호출되었고, 그때마다 직전에 미들웨어가 실행된 것입니다. 출력된 메시지 개수를 보면 각 시점의 State 상태도 알 수 있습니다. 첫 번째 호출 직전에는 사용자의 질문(HumanMessage) 1개만 있었고, 이후 루프가 돌 때마다 도구 호출을 요청하는 AIMessage와 그 실행 결과인 ToolMessage가 추가되면서 3개, 5개로 늘어난 것입니다.
한 가지 알아둘 것이 있습니다. LangChain 1.0 이전에는 이 역할을 하는 함수가 LangGraph 쪽에
create_react_agent라는 이름으로 있었고, 지금은 폐기 예정(deprecated)입니다. 인터넷의 오래된 자료에서from langgraph.prebuilt import create_react_agent를 본다면, 지금 배우고 있는create_agent의 이전 버전이라고 이해하면 됩니다.
15장에서 직접 작성했던 에이전트를 ToolNode, tools_condition, 그리고 create_agent로 간결하게 만들어 보았습니다. 다음 절에서는 이 사전 구축 컴포넌트와 그래프 조립 방식을 혼합하여 더 복잡한 에이전트를 만들어 보겠습니다.
16.2) 다중 분기 에이전트 구축하기
이 절에서 만들 다중 분기 에이전트는 요청이 들어오면 어떤 종류의 요청인지 먼저 파악하고, 종류에 따라 각각 다른 모델이나 도구로 처리하는 에이전트입니다. 전체 그래프는 15장에서 배운 방식으로 직접 조립하고, 그중 도구 호출 루프가 필요한 부분은 create_agent로 만들어 보겠습니다.
16.2.1) 요구 사항과 설계
여기서는 이 장의 도입부에서 언급했던 고객 문의 에이전트를 만들어 보겠습니다. 요구 사항은 다음과 같습니다.
- 단순 문의 ("영업시간이 어떻게 되나요?") → 저렴한 소형 모델이 바로 답변합니다.
- 복잡한 상담 ("주문한 상품이 파손되어 왔는데 교환이나 환불 중 뭐가 나은가요?") → 고성능 모델이 답변합니다.
- 주문 조회 ("주문 #12345 배송 상태 알려주세요") → 주문 조회 도구를 사용하는 에이전트가 처리합니다.
핵심은 문의 유형별로 서로 다른 모델과 도구 구성으로 처리된다는 점입니다. 이를 위해 요청을 먼저 분류하고, 유형에 따라 다르게 처리하는 그래프를 만들겠습니다. 구조는 다음과 같습니다.
요청이 들어오면 classify 노드가 어떤 유형의 문의인지 분류하여 State에 기록합니다. 그다음 조건부 엣지가 State에 기록된 유형을 읽어 알맞은 노드로 라우팅합니다. simple_handler는 단순 문의를 담당하고, complex_handler는 복잡한 상담을 처리합니다. order_agent는 주문 조회 도구로 배송 상태를 확인하여 답변합니다. order_agent는 일반적인 도구 사용 노드이므로 create_agent로 만듭니다. 이제 각 부분을 순서대로 만들어 보겠습니다.
16.2.2) 분류 노드와 라우팅 함수
먼저 State를 정의합니다. 문의 유형에 대한 분류 결과를 저장할 수 있도록 intent 필드를 추가하겠습니다. 세 가지 유형은 각각 "simple", "complex", "order" 값으로 나타내겠습니다.
from langgraph.graph import MessagesState
class State(MessagesState):
intent: str # 분류 결과: "simple", "complex", "order"다음은 분류 노드입니다. 사용자의 요청이 세 유형 중 어디에 해당하는지 LLM으로 판정하고, 결과를 intent에 기록합니다. 판정 결과는 7장에서 배운 구조화된 출력으로 받습니다. 스키마의 intent 필드를 Literal 타입으로 선언하면, LLM의 응답은 선언된 값들 중 하나로 제한됩니다.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class IntentRoute(BaseModel):
"""사용자 문의의 분류 결과."""
intent: Literal["simple", "complex", "order"] = Field(
description=(
"simple: general questions like business hours or greetings. "
"complex: consultations that need careful reasoning, like disputes or refunds. "
"order: requests to look up a specific order."
)
)
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
def classify(state: State):
"""사용자 문의의 유형을 분류합니다."""
question = state["messages"][-1].content
result = classifier_llm.invoke(
f"Classify the customer's request.\n\nRequest: {question}"
)
return {"intent": result.intent}분류에는 최소형 모델(gpt-5.4-nano)을 사용했습니다. "이 문의가 어느 유형인가"를 판단하는 단순한 작업이므로 고성능 모델이 필요 없습니다. 그리고 분류 노드는 모든 요청이 거쳐 가는 관문이기 때문에 저렴하고 빠른 모델이 좋습니다.
다음은 라우팅 함수입니다. State에 저장된 분류 결과를 그대로 반환합니다.
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
"""분류 결과에 따라 다음 노드를 결정합니다."""
return state["intent"]다음에 실행할 노드는 classify가 이미 결정하여 intent에 기록했으므로, 라우팅 함수는 그 값을 그대로 반환하고 있습니다.
16.2.3) 유형별 핸들러 노드
이제 세 가지 문의 유형을 각각 처리할 핸들러를 만들겠습니다.
단순 문의 핸들러는 소형 모델을 한 번 호출합니다. 실제라면 RAG를 적용해 사내 문서를 검색해서 답변해야겠지만, 이 장에서 다루는 주제에 집중하기 위해 단순하게 구성하였습니다.
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
def simple_handler(state: State):
"""단순 문의에 소형 모델로 답변합니다."""
response = simple_llm.invoke(state["messages"])
return {"messages": [response]}복잡한 상담 핸들러는 고성능 모델을 사용합니다. 단순 문의 핸들러와 마찬가지 이유로 단순하게 응답만 하도록 구성했습니다.
complex_llm = ChatOpenAI(model="gpt-5.4")
def complex_handler(state: State):
"""복잡한 상담에 고성능 모델로 답변합니다."""
response = complex_llm.invoke(state["messages"])
return {"messages": [response]}주문 조회 핸들러는 주문 조회 도구를 사용해야 하므로 도구 호출 루프가 필요합니다. 일반적인 도구 호출 에이전트 구조와 동일하므로 create_agent로 만들겠습니다.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""주문 번호로 배송 상태를 조회합니다."""
fake_data = {"12345": "In transit, expected tomorrow", "67890": "Delivered"}
return fake_data.get(order_id, f"Order {order_id} not found.")
order_agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[get_order_status],
)16.2.4) 그래프 조립과 실행
지금까지 만든 노드들을 연결하여 그래프로 만들겠습니다. classify를 시작점에 두고, 조건부 엣지로 세 핸들러에 연결한 뒤, 각 핸들러가 실행을 마치면 종료하도록 구성하겠습니다.
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent) # create_agent가 만든 그래프를 노드로 등록
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
agent = builder.compile()세 유형의 문의를 하나씩 실행하여 각각 어느 핸들러에서 처리되는지 확인해 보겠습니다.
from langchain_core.messages import HumanMessage
for question in [
"What are your business hours?",
"My order arrived damaged. Should I get an exchange or a refund?",
"What's the delivery status of order 12345?",
]:
result = agent.invoke({"messages": [HumanMessage(content=question)]})
print(f"Q: {question}")
print(f"[{result['intent']}] A: {result['messages'][-1].content}\n")출력:
Q: What are your business hours?
[simple] A: I don’t have fixed business hours—I’m available 24/7.
...
Q: My order arrived damaged. Should I get an exchange or a refund?
[complex] A: If your order arrived damaged, you should generally be entitled to a **replacement/exchange or a full refund**.
...
Q: What's the delivery status of order 12345?
[order] A: Order 12345 is **in transit** and is **expected tomorrow**.세 문의가 각각 다른 경로로 처리되었습니다. 단순 문의는 simple_handler가 소형 모델로, 복잡한 상담은 complex_handler가 고성능 모델로 답했고, 주문 조회는 order_agent가 get_order_status 도구를 호출하여 답했습니다.