16. Componentes Pré-construídos e Roteamento Multi-ramo
No Capítulo 15, montamos um grafo de agente manualmente — um nó de modelo, um nó de ferramenta e uma aresta condicional que decide se deve continuar em loop ou parar. Essa é essencialmente a estrutura padrão para agentes que chamam ferramentas, então o LangChain e o LangGraph a fornecem como componentes pré-construídos(prebuilt components) que você pode usar em vez de escrever a mesma estrutura do zero toda vez.
Na primeira metade deste capítulo, vamos reconstruir o agente do Capítulo 15 usando componentes pré-construídos. Vamos substituir o nó de execução de ferramentas e a função de roteamento por ToolNode e tools_condition, e finalmente substituir toda a montagem do grafo por uma única chamada a create_agent. Você verá que o comportamento permanece idêntico ao do Capítulo 15, enquanto o código encolhe consideravelmente.
Na segunda metade, vamos combinar componentes pré-construídos com a abordagem de montagem manual de grafo do Capítulo 15 para construir um agente mais complexo. O agente que vamos construir roteia cada requisição para um manipulador diferente — consultas complexas vão para um modelo de alto desempenho, enquanto perguntas simples são respondidas por um modelo menor e mais barato. Essa é uma estrutura multi-ramo onde o caminho de processamento se divide com base no tipo de requisição.
16.1) Componentes Pré-construídos e create_agent
Nesta seção, vamos substituir a função tool_node e a função should_continue do grafo do Capítulo 15 pelos componentes pré-construídos ToolNode e tools_condition. Depois disso, vamos pular completamente a montagem manual e criar o grafo inteiro com uma única chamada a create_agent. O ponto a observar em cada etapa é que o código fica mais curto enquanto o comportamento do agente permanece idêntico ao do Capítulo 15.
16.1.1) ToolNode e tools_condition
ToolNode é um nó pré-construído que lida com a execução de ferramentas para você. Quando a última mensagem no Estado (o AIMessage retornado pelo LLM) contém tool_calls, ele executa as ferramentas solicitadas e adiciona os resultados a messages como objetos ToolMessage. Ele faz o mesmo trabalho que a função tool_node que escrevemos no Capítulo 15. Além disso, quando o LLM solicita várias ferramentas de uma vez, ele as executa em paralelo.
Ele também suporta tratamento de exceções durante a execução de ferramentas. Se você definir ToolNode(tools, handle_tool_errors=True), o grafo não travará mesmo quando uma ferramenta lançar uma exceção. A exceção é convertida em um ToolMessage carregando os detalhes do erro, que é passado ao LLM para que ele possa ver a falha e tentar novamente com argumentos corrigidos.
Você cria um ToolNode passando-lhe uma lista de ferramentas. Vamos usar as mesmas duas ferramentas do Capítulo 15.
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtém o clima atual de uma cidade."""
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:
"""Avalia uma expressão aritmética simples. Exemplo: '3 * 21'."""
return str(eval(expression)) # Aviso: eval() é um risco de segurança. Não use em produção.
tools = [get_weather, calculate]
# O tool_map + a função tool_node do Capítulo 15 substituídos por esta única linha
tool_node = ToolNode(tools)O ToolNode constrói internamente um mapeamento de nome para ferramenta a partir da lista de ferramentas, exatamente como o tool_map do Capítulo 15. Em tempo de execução, ele procura cada ferramenta pelo nome que o LLM solicitou e a chama. Em outras palavras, o dicionário tool_map, o loop for sobre tool_calls e o código que constrói e coleta objetos ToolMessage — tudo isso agora vive dentro do ToolNode.
tools_condition é a função de roteamento pré-construída que substitui a função should_continue do Capítulo 15. Vamos olhar novamente para o should_continue do Capítulo 15.
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
"""Decide se deve executar ferramentas ou encerrar o grafo."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return ENDEle retornava "tool_node" (o nome registrado do nosso nó de ferramenta) quando a última mensagem tinha tool_calls, e END caso contrário. O tools_condition funciona exatamente da mesma maneira, com uma diferença no nome que retorna. Enquanto o should_continue foi escrito para retornar "tool_node" — o nome que registramos no nosso grafo — o tools_condition está fixado no código para retornar "tools".
Como aprendemos na Seção 15.2.4, o valor que uma função de roteamento retorna é o nome do próximo nó a executar. Se nenhum nó com esse nome existir no grafo, o roteamento falha. Então, ao usar tools_condition, o nó de ferramenta deve ser registrado com o nome "tools".
from langgraph.prebuilt import ToolNode, tools_condition
builder.add_node("tools", ToolNode(tools)) # Registra com o nome "tools"
builder.add_conditional_edges("llm_call", tools_condition) # Roteia para "tools" ou ENDDessa forma, quando tools_condition retorna "tools", ele se conecta exatamente ao ToolNode que acabamos de registrar.
Agora vamos remontar o grafo completo do Capítulo 15, incluindo todas as peças restantes. As ferramentas, o Estado e o nó llm_call permanecem inalterados em relação ao Capítulo 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:
"""Obtém o clima atual de uma cidade."""
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:
"""Avalia uma expressão aritmética simples. Exemplo: '3 * 21'."""
return str(eval(expression)) # Aviso: eval() é um risco de segurança. Não use em produção.
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):
"""Chama o LLM e retorna sua resposta."""
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)) # ToolNode em vez da função tool_node do Capítulo 15
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition) # tools_condition em vez do should_continue do Capítulo 15
builder.add_edge("tools", "llm_call")
agent = builder.compile()Compare isso com o código do Capítulo 15. O dicionário tool_map, a função tool_node e a função should_continue desapareceram todos. O trabalho que faziam agora é tratado por ToolNode(tools) e tools_condition. O nó de ferramenta é registrado como "tools" para corresponder ao nome para o qual tools_condition roteia. Vamos executá-lo com a mesma pergunta do Capítulo 15.
result = agent.invoke({
"messages": [HumanMessage(content="Obtenha a temperatura no Cairo e depois multiplique o número por 3.")],
"llm_calls": 0,
})
print(result["messages"][-1].content)
print(f"\nTotal de chamadas ao LLM: {result['llm_calls']}")Saída:
Temperatura atual no Cairo: 31°C. Multiplicado por 3 = 93.
Total de chamadas ao LLM: 3O resultado é idêntico ao do Capítulo 15. O agente consulta o clima, realiza o cálculo e produz a resposta final — o mesmo comportamento é preservado, enquanto o código que precisamos escrever e manter encolheu.
E se você quiser registrar o nó de ferramenta com um nome diferente de "tools"? Nesse caso, passe um dicionário de mapeamento como terceiro argumento para add_conditional_edges, especificando a qual nó cada valor de retorno de tools_condition deve se conectar. Como tools_condition retorna "tools" ou END, você usa esses valores como chaves e os mapeia para os nós de destino. Por exemplo, se você registrar o nó de ferramenta como "run_tools":
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
"llm_call",
tools_condition,
{"tools": "run_tools", END: END} # retorno "tools" → nó run_tools, retorno END → encerrar
)O ToolNode e o tools_condition substituem partes individuais do grafo — as tediosas — mas adicionar nós e conectá-los ainda é responsabilidade nossa. Poderíamos delegar também essa montagem? É exatamente isso que create_agent faz.
16.1.2) create_agent
create_agent é uma função fábrica do LangChain que lida com toda a montagem do grafo de um agente que chama ferramentas. Passe a ela um modelo e uma lista de ferramentas, e ela constrói um grafo com a mesma estrutura que montamos em 16.1.1, já compilado e pronto para executar. Vamos tentar.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
@tool
def get_weather(city: str) -> str:
"""Obtém o clima atual de uma cidade."""
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:
"""Avalia uma expressão aritmética simples. Exemplo: '3 * 21'."""
return str(eval(expression)) # Aviso: eval() é um risco de segurança. Não use em produção.
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
system_prompt="Você é um assistente prestativo.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Obtenha a temperatura no Cairo e depois multiplique o número por 3.")],
})
print(result["messages"][-1].content)Saída:
Temperatura atual no Cairo: 31°C. Multiplicado por 3 = 93.Nenhuma definição de Estado, nenhuma função de nó, nenhum add_node ou add_edge. Uma única chamada a create_agent fez tudo isso, e o resultado é idêntico ao de 16.1.1.
O que acontece internamente é exatamente o que já sabemos. O create_agent cria um nó de chamada ao LLM a partir do modelo que você passa, constrói um ToolNode a partir da lista de ferramentas e os conecta com uma aresta tools_condition e uma aresta de retorno em loop. O resultado é um grafo com estrutura de loop idêntico ao que montamos em 16.1.1.
Vamos olhar os parâmetros. O create_agent na verdade não é novo para nós — nós o usamos brevemente no Capítulo 11 ao construir RAG conversacional, mas não entramos em detalhes sobre os parâmetros. Vamos analisá-los um por um.
model: O LLM que o agente usará. A abordagem mais simples é passar uma string de provedor como"openai:gpt-5-mini". Se você precisar configurar os parâmetros do modelo diretamente, passe uma instância de modelo inicializada comoChatOpenAI(model="gpt-5-mini"). Internamente, o nó de chamada ao LLM usa este modelo.tools: A lista de ferramentas que o agente pode usar. UmToolNodeé construído a partir delas internamente.system_prompt: Instruções de comportamento para o agente. Isso é prefixado como uma mensagem de sistema à lista de mensagens em cada chamada ao LLM.checkpointer: Salva o estado da conversa para que o agente possa lembrar dos turnos anteriores. Esse é o mesmo parâmetro que usamos comInMemorySaver()ethread_idno Capítulo 11 para implementar conversas multi-turno. Vamos cobrir como ele funciona em detalhes no Capítulo 17.response_format: Use isto quando você quiser a resposta final do agente como saída estruturada. Passe um modelo Pydantic (o mesmo conceito do Capítulo 7), e o objeto validado estará disponível emresult["structured_response"].middleware: Registra funções para executar em pontos específicos do loop de execução do agente. Esse é o parâmetro que usamos para registrartrim_old_messagesno Capítulo 11. Vamos explicá-lo em detalhes abaixo.
Vamos ver como o response_format funciona na prática.
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="Como está o clima em Tokyo?")],
})
print(result["structured_response"])Saída:
city='Tokyo' temperature='18°C' condition='cloudy'O agente chamou a ferramenta get_weather e depois organizou as informações em um objeto WeatherReport correspondente ao esquema.
middleware
O loop do agente tem estágios distintos. Ele chama o LLM, executa ferramentas, chama o LLM novamente — esses estágios se repetem. O Middleware permite que você insira suas próprias funções antes ou depois desses estágios. Você especifica o momento com um decorador: @before_model significa imediatamente antes da chamada ao LLM, e @after_model significa imediatamente após o LLM responder. Registre a função no parâmetro middleware, e ela é executada no ponto designado toda vez.
Já usamos middleware no Capítulo 11. Decoramos uma função trim_old_messages com @before_model e a registramos — como ela era executada antes de cada chamada ao LLM, podia truncar a lista de mensagens toda vez.
Vamos construir um middleware que imprime a contagem de mensagens imediatamente antes de cada chamada ao LLM, para que possamos ver exatamente quando ele é executado.
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
"""Imprime a contagem de mensagens imediatamente antes de cada chamada ao LLM."""
print(f"[before_model] Prestes a chamar o LLM, mensagens atuais: {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="Obtenha a temperatura no Cairo e depois multiplique o número por 3.")],
})
print(result["messages"][-1].content)Saída:
[before_model] Prestes a chamar o LLM, mensagens atuais: 1
[before_model] Prestes a chamar o LLM, mensagens atuais: 3
[before_model] Prestes a chamar o LLM, mensagens atuais: 5
Temperatura atual no Cairo: 31°C. Multiplicado por 3 = 93.O middleware log_llm_call foi executado três vezes. O LLM foi chamado três vezes ao processar a requisição do usuário, e o middleware foi executado imediatamente antes de cada chamada. As contagens de mensagens nos dizem o Estado em cada ponto: antes da primeira chamada havia apenas a pergunta do usuário (HumanMessage) — 1 mensagem. Após cada iteração do loop, o AIMessage solicitando uma chamada de ferramenta e o ToolMessage com o resultado foram adicionados, crescendo para 3, depois 5.
Uma coisa a saber: antes do LangChain 1.0, esse papel era desempenhado por uma função chamada
create_react_agentno lado do LangGraph, e agora ela está descontinuada. Se você virfrom langgraph.prebuilt import create_react_agentem tutoriais ou posts de blog mais antigos, entenda que é uma versão anterior docreate_agentque você está aprendendo agora.
Reconstruímos o agente do Capítulo 15 de forma concisa usando ToolNode, tools_condition e create_agent. Na próxima seção, vamos combinar esses componentes pré-construídos com montagem manual de grafo para construir um agente mais complexo.
16.2) Construindo um Agente Multi-ramo
O agente multi-ramo(multi-branch agent) que vamos construir nesta seção primeiro determina que tipo de requisição está tratando e depois lida com cada tipo com um modelo diferente ou um conjunto diferente de ferramentas. Vamos montar o grafo geral manualmente usando a abordagem do Capítulo 15, e usar create_agent para as partes que precisam de um loop de chamada de ferramentas.
16.2.1) Requisitos e Design
Vamos construir o agente de suporte ao cliente mencionado na introdução deste capítulo. Aqui estão os requisitos:
- Consultas simples ("Qual é o horário de funcionamento?") → Um modelo pequeno de baixo custo responde diretamente.
- Consultas complexas ("Meu pedido chegou danificado — devo pedir uma troca ou um reembolso?") → Um modelo de alto desempenho responde.
- Consultas de pedidos ("Qual é o status de entrega do pedido #12345?") → Um agente com uma ferramenta de consulta de pedidos lida com isso.
Cada tipo de consulta precisa de uma configuração diferente de modelo e ferramenta, então precisamos de um grafo que primeiro classifique cada requisição e depois a roteie para o manipulador correto. Aqui está a estrutura:
Quando uma requisição chega, o nó classify determina qual tipo de consulta ela é e registra o resultado no Estado. Uma aresta condicional então lê o tipo registrado do Estado e roteia para o nó apropriado. O simple_handler lida com consultas simples, e o complex_handler trata de consultas complexas. O order_agent usa uma ferramenta de consulta de pedidos para verificar o status de entrega e responder. Como o order_agent é um nó padrão que chama ferramentas, vamos construí-lo com create_agent. Agora vamos construir cada peça em ordem.
16.2.2) O Nó de Classificação e a Função de Roteamento
Primeiro, vamos definir o Estado. Vamos adicionar um campo intent para armazenar o resultado da classificação. Os três tipos serão representados pelos valores "simple", "complex" e "order".
from langgraph.graph import MessagesState
class State(MessagesState):
intent: str # Resultado da classificação: "simple", "complex", "order"Em seguida, o nó de classificação. Ele usa um LLM para determinar a qual dos três tipos a requisição do usuário pertence e registra o resultado em intent. Recebemos o resultado da classificação usando saída estruturada, que aprendemos no Capítulo 7. Quando declaramos o campo intent do esquema com um tipo Literal, a resposta do LLM fica restrita a um dos valores declarados.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class IntentRoute(BaseModel):
"""Resultado da classificação para uma consulta de cliente."""
intent: Literal["simple", "complex", "order"] = Field(
description=(
"simple: perguntas gerais como horário de funcionamento ou saudações. "
"complex: consultas que precisam de raciocínio cuidadoso, como disputas ou reembolsos. "
"order: requisições para consultar um pedido específico."
)
)
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
def classify(state: State):
"""Classifica o tipo de consulta do cliente."""
question = state["messages"][-1].content
result = classifier_llm.invoke(
f"Classifique a requisição do cliente.\n\nRequisição: {question}"
)
return {"intent": result.intent}Usamos o menor modelo (gpt-5.4-nano) para a classificação. Decidir "qual é o tipo desta consulta?" é uma tarefa simples que não precisa de um modelo de alto desempenho. E como o nó de classificação é um portal por onde toda requisição passa, um modelo barato e rápido é preferível.
Em seguida, a função de roteamento. Ela simplesmente retorna o resultado da classificação armazenado no Estado.
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
"""Determina o próximo nó com base no resultado da classificação."""
return state["intent"]O nó classify já decidiu qual nó deve executar em seguida e o registrou em intent, então a função de roteamento apenas retorna esse valor como está.
16.2.3) Nós Manipuladores por Tipo
Agora vamos construir os manipuladores para cada um dos três tipos de consulta.
O manipulador de consultas simples chama um modelo pequeno uma vez. Em um agente de suporte ao cliente real, você aplicaria RAG para pesquisar documentos internos em busca de respostas, mas mantivemos o manipulador simples para permanecer focados no tópico deste capítulo.
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
def simple_handler(state: State):
"""Responde consultas simples com um modelo pequeno."""
response = simple_llm.invoke(state["messages"])
return {"messages": [response]}O manipulador de consultas complexas usa um modelo de alto desempenho. Pela mesma razão do manipulador de consultas simples, mantivemos ele simples — apenas gerando uma resposta.
complex_llm = ChatOpenAI(model="gpt-5.4")
def complex_handler(state: State):
"""Responde consultas complexas com um modelo de alto desempenho."""
response = complex_llm.invoke(state["messages"])
return {"messages": [response]}O manipulador de consultas de pedidos precisa usar uma ferramenta de consulta de pedidos, o que significa que requer um loop de chamada de ferramentas. Como sua estrutura é idêntica à de um agente padrão que chama ferramentas, vamos construí-lo com create_agent.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Consulta o status de entrega de um pedido pelo número do pedido."""
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) Montando e Executando o Grafo
Vamos conectar todos os nós que construímos em um grafo. Vamos colocar o classify no ponto de partida, conectá-lo aos três manipuladores por meio de uma aresta condicional e configurar cada manipulador para encerrar após concluir.
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) # Registra o grafo do create_agent como um nó
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()Vamos executar uma consulta de cada tipo e ver qual manipulador a processa.
from langchain_core.messages import HumanMessage
for question in [
"Qual é o horário de funcionamento?",
"Meu pedido chegou danificado. Devo pedir uma troca ou um reembolso?",
"Qual é o status de entrega do pedido 12345?",
]:
result = agent.invoke({"messages": [HumanMessage(content=question)]})
print(f"P: {question}")
print(f"[{result['intent']}] R: {result['messages'][-1].content}\n")Saída:
P: Qual é o horário de funcionamento?
[simple] R: Não tenho horários de funcionamento fixos—estou disponível 24 horas por dia, 7 dias por semana.
...
P: Meu pedido chegou danificado. Devo pedir uma troca ou um reembolso?
[complex] R: Se o seu pedido chegou danificado, você geralmente deve ter direito a uma **substituição/troca ou um reembolso total**.
...
P: Qual é o status de entrega do pedido 12345?
[order] R: O pedido 12345 está **em trânsito** e é **esperado para amanhã**.Cada consulta foi processada por um caminho diferente. O simple_handler respondeu à consulta simples com um modelo pequeno, o complex_handler respondeu à consulta complexa com um modelo de alto desempenho, e o order_agent chamou a ferramenta get_order_status para responder à consulta de pedido.