Comparar LangChain e PydanticAI ajuda a entender duas abordagens populares para criar um agente de IA, isto é, um programa que conversa, toma decisões e pode executar ações por meio de ferramentas (funções chamadas pelo modelo). Ambas as bibliotecas servem para integrar um LLM (modelo de linguagem grande) a um fluxo de conversa com memória, regras e chamadas a serviços externos.
Um exemplo didático é um agente que atua como um garçom excêntrico de restaurante: cumprimenta, pergunta restrições alimentares, consulta o cardápio com uma ferramenta, registra o pedido com outra ferramenta e encerra a conversa quando não houver mais perguntas. Nesse contexto, as diferenças entre as bibliotecas aparecem com clareza em temas como prompt dinâmico, saída estruturada, memória (histórico), injeção de dependências e ergonomia do código.
O que é um agente de IA e quais capacidades entram no cenário
Um agente de IA é uma aplicação que orquestra um modelo de linguagem para cumprir um objetivo em etapas, mantendo contexto e usando ferramentas quando necessário. Em vez de apenas responder texto livre, um agente pode selecionar ações, chamar funções, validar dados e controlar quando a conversa termina. Esse comportamento é muito comum em atendentes virtuais, triagem, automação de rotinas e suporte a operações.
Para um agente conversacional simples, algumas capacidades costumam ser essenciais: escolha dinâmica de modelo, human-in-the-loop (entrada humana durante o fluxo), tool calling (chamada de ferramentas), memória (histórico), prompt de sistema dinâmico, saída estruturada e regras de comportamento do agente. Cada framework oferece caminhos diferentes para montar essas peças. As diferenças aparecem principalmente no quanto o código fica direto e no quanto o desenvolvedor precisa conhecer “camadas internas” da biblioteca.
Cenário do “garçom excêntrico”: objetivo e fluxo conversacional
O cenário do restaurante funciona bem porque exige conversa natural e também ações concretas, como consultar itens disponíveis e criar um pedido. O agente precisa perguntar restrições alimentares, propor opções compatíveis e confirmar o pedido. Depois da confirmação, o agente registra o pedido e encerra a conversa de forma controlada.
Um diálogo típico inclui um cumprimento, uma pergunta sobre preferências, uma consulta ao cardápio e, por fim, o registro do pedido. Para isso, entram duas ferramentas: uma para retornar o cardápio e outra para registrar o pedido. Além disso, uma regra central é encerrar somente quando a resposta final não contiver perguntas, o que combina bem com saída estruturada contendo um campo booleano para indicar o fim.
O exemplo a seguir ilustra o estilo de conversa esperado, sem depender de detalhes do modelo: primeiro há uma pergunta sobre restrições, depois sugestões vindas do cardápio e, por fim, confirmação e registro. Esse tipo de agente exige também que o histórico seja mantido, para que o modelo “lembre” as preferências mencionadas. A estrutura do fluxo é simples, mas já cobre vários pontos práticos do mundo real.
Prompt de sistema dinâmico: regras, persona e parâmetros em tempo de execução
O prompt de sistema é um texto inicial que define papel, regras e limites do agente, e normalmente tem grande influência no comportamento do modelo. Um prompt “dinâmico” é aquele que recebe parâmetros em tempo de execução, como nome do restaurante e número da mesa. Isso evita criar versões diferentes do prompt para cada contexto e facilita reutilização do agente.
Além da persona (garçom excêntrico), o prompt também precisa descrever regras operacionais: perguntar restrições, usar ferramentas específicas e marcar o fim da conversa apenas no momento correto. Uma regra importante é impedir que a conversa termine quando ainda existe uma pergunta em aberto. Também é útil explicitar que o cardápio deve vir da ferramenta, reduzindo “alucinação” (quando o modelo inventa itens).
O modelo abaixo define um template de prompt e uma estrutura de resposta. A frase anterior explica que o trecho mostra o template do prompt e o esquema de saída estruturada que controlará o encerramento da conversa.
from typing import Annotated
from pydantic import BaseModel
PROMPT_TEMPLATE = """
Você está interpretando o papel de um garçom incrivelmente excêntrico e divertido em um restaurante fino
chamado "{restaurant_name}", atendendo a mesa número {table_number}.
Regras:
* Cumprimentar e perguntar se existem restrições alimentares.
* Apresentar itens apropriados usando a ferramenta *get_menu()*.
* Receber o pedido e confirmar.
* Quando confirmado, usar a ferramenta *create_order()* para registrar o pedido.
* Definir *end_conversation* como True apenas na resposta final, depois de finalizar,
e somente quando a mensagem NÃO contiver uma pergunta.
"""
class LLMResponse(BaseModel):
\"\"\"Formato de saída estruturada para controlar o encerramento da conversa.\"\"\"
message: str
end_conversation: Annotated[
bool,
"True se a conversa deve terminar após esta resposta. Não marcar True se houver pergunta na mensagem.",
]
Ferramentas e serviços: dependências e efeitos colaterais controlados
Uma ferramenta é uma função (ou objeto) que o modelo pode “chamar” para obter dados ou executar ações. Em agentes, ferramentas tipicamente acessam dependências, como um serviço de cardápio ou um serviço de pedidos. Essas dependências encapsulam efeitos colaterais, como salvar pedidos em memória ou em banco de dados, mantendo o código organizado.
Um padrão comum é separar “serviços” do “agente”, para que as ferramentas apenas deleguem trabalho. Isso também ajuda em testes, pois as dependências podem ser simuladas. No caso do restaurante, um serviço retorna um dicionário com categorias e itens, e outro serviço registra pedidos em uma lista.
O trecho a seguir apresenta serviços mínimos de cardápio e pedidos, que serão consumidos pelas ferramentas. A frase anterior explica que o código mostra as dependências que as ferramentas vão utilizar durante a execução.
from dataclasses import dataclass
from pydantic import BaseModel
class Order(BaseModel):
table_number: int
menu_items: list[str]
class MenuService:
def get_menu(self) -> dict[str, list[str]]:
# Exemplo simples de cardápio
return {
"Entradas": ["Quinoa Stuffed Bell Peppers", "Salada da Casa", "Sopa do Dia"],
"Pratos Principais": ["Chickpea and Sweet Potato Curry", "Risoto de Cogumelos"],
"Sobremesas": ["Fresh Fruit Sorbet", "Mousse de Chocolate"],
}
class OrderService:
def __init__(self) -> None:
self.orders: list[Order] = []
def create_order(self, table_number: int, menu_items: list[str]) -> None:
self.orders.append(Order(table_number=table_number, menu_items=menu_items))
def get_orders(self) -> list[Order]:
return self.orders
Interface comum: runner do agente e loop de conversa
Uma forma didática de comparar frameworks é definir uma interface comum que ambos devem implementar. Essa interface centraliza a inicialização do agente e a função de “enviar uma mensagem e obter resposta”. Assim, a maior parte do programa não muda, e apenas a implementação interna varia conforme a biblioteca.
O loop de conversa costuma funcionar assim: um texto inicial dispara a primeira resposta, depois cada entrada humana vira uma nova iteração. A cada iteração, o agente pode chamar ferramentas e retornar uma resposta estruturada. Quando o campo de término indica fim, o loop é encerrado e os pedidos podem ser exibidos.
O código abaixo apresenta a interface e uma função de execução que roda a conversa até o término indicado. A frase anterior explica que o trecho mostra o contrato mínimo esperado de qualquer implementação baseada em frameworks diferentes.
import argparse
from abc import ABC, abstractmethod
class AgentRunner(ABC):
\"\"\"Interface comum para inicializar e conversar com um agente.\"\"\"
@abstractmethod
def __init__(self, menu_service: MenuService, order_service: OrderService, args: argparse.Namespace):
raise NotImplementedError
@abstractmethod
def make_request(self, user_message: str) -> LLMResponse:
raise NotImplementedError
def run_agent(runner_class: type[AgentRunner], args: argparse.Namespace) -> list[Order]:
\"\"\"Inicializa serviços e executa o loop de conversa.\"\"\"
menu_service = MenuService()
order_service = OrderService()
runner = runner_class(menu_service, order_service, args)
user_message = "*Cumprimente o cliente*"
while True:
response = runner.make_request(user_message)
print(f"AI Waiter: {response.message}")
if response.end_conversation:
break
user_message = input("Você: ").strip()
return order_service.get_orders()
PydanticAI: visão geral e por que costuma parecer mais direto
O PydanticAI é um framework focado em facilitar aplicações com modelos generativos, com forte ênfase em tipos, validação e resultados estruturados. A experiência tende a ser direta porque ferramentas e saídas se apoiam em anotações de tipo e modelos do Pydantic. Isso reduz a quantidade de “cola” necessária para validar entradas e saídas.
Uma característica central é que o agente pode ser configurado com um tipo de dependências e um tipo de resultado. As ferramentas podem receber um contexto de execução que dá acesso às dependências. O histórico de mensagens pode ser armazenado fora do agente e reaplicado a cada chamada, deixando explícito o que é estado e o que é configuração.
Um ponto importante para cenários reais é a configuração dinâmica: trocar modelo, trocar prompt e registrar ferramentas em tempo de execução. Em vez de criar tudo em nível de módulo, uma função fábrica pode construir o agente conforme argumentos de CLI, variáveis de ambiente ou outros parâmetros. Isso torna mais natural ter “escolha dinâmica de modelo” sem reestruturar o projeto.
PydanticAI: dependências e ferramentas com RunContext
No PydanticAI, injeção de dependências costuma ser feita com um objeto de dependências passado a cada execução. Ferramentas recebem um RunContext, que expõe essas dependências de forma tipada. Com isso, a ferramenta pode acessar serviços sem recorrer a variáveis globais.
As funções de ferramenta podem declarar argumentos com tipos e descrições, e o framework usa essas informações para gerar o esquema que o modelo enxerga. Isso facilita o “tool calling” com validação básica de tipos. Para o restaurante, uma ferramenta retorna o cardápio e outra registra o pedido.
O trecho a seguir mostra a estrutura de dependências e as duas ferramentas essenciais do cenário. A frase anterior explica que o código demonstra como ferramentas acessam serviços em tempo de execução sem acoplamento direto ao agente.
from dataclasses import dataclass
from typing import Annotated
# Nomes importados do PydanticAI (mantidos como referência de uso)
from pydantic_ai import Agent, RunContext
from pydantic_ai.messages import ModelMessage
@dataclass
class Dependencies:
menu_service: MenuService
order_service: OrderService
restaurant_name: str
table_number: int
def create_order(
ctx: RunContext[Dependencies],
table_number: int,
order_items: Annotated[list[str], "Lista de itens do cardápio para pedir"],
) -> str:
\"\"\"Registra um pedido para a mesa.\"\"\"
ctx.deps.order_service.create_order(table_number, order_items)
return "Pedido registrado"
def get_menu(ctx: RunContext[Dependencies]) -> dict[str, list[str]]:
\"\"\"Retorna o cardápio completo do restaurante.\"\"\"
return ctx.deps.menu_service.get_menu()
PydanticAI: construção dinâmica do agente e prompt de sistema parametrizado
Uma prática útil é criar uma função que constrói o agente sob demanda, permitindo escolher o modelo e registrar ferramentas dinamicamente. Isso favorece cenários em que o mesmo programa roda com diferentes provedores, diferentes chaves e diferentes configurações. O prompt de sistema pode ser definido como uma função que usa as dependências para preencher o template.
Nessa abordagem, o agente é tratado como um objeto de configuração e execução, enquanto o estado conversacional fica no histórico de mensagens. A saída estruturada é configurada com um tipo de resultado, fazendo com que o framework valide o retorno final. Isso também ajuda a aplicar a regra “encerrar apenas no final”, pois o campo booleano sempre existe.
O código abaixo mostra uma função que constrói o agente e registra um prompt dinâmico. A frase anterior explica que o trecho demonstra a criação de um agente com ferramentas e resultado tipado, além de um prompt baseado nas dependências.
from typing import Optional
KnownModelName = str # Simplificação para o exemplo
def build_model_from_name_and_api_key(model_name: KnownModelName, api_key: Optional[str]):
# Função ilustrativa: na prática, inicializa o cliente do provedor escolhido.
# O retorno precisa ser um modelo compatível com o PydanticAI.
raise NotImplementedError("Inicialização do modelo depende do provedor escolhido.")
def get_agent(model_name: KnownModelName, api_key: str | None = None) -> Agent[Dependencies, LLMResponse]:
\"\"\"Constrói um agente com modelo, ferramentas, prompt dinâmico e resultado estruturado.\"\"\"
model = build_model_from_name_and_api_key(model_name=model_name, api_key=api_key)
agent = Agent(
model=model,
deps_type=Dependencies,
tools=[get_menu, create_order],
result_type=LLMResponse,
)
@agent.system_prompt
def system_prompt(ctx: RunContext[Dependencies]) -> str:
return PROMPT_TEMPLATE.format(
restaurant_name=ctx.deps.restaurant_name,
table_number=ctx.deps.table_number,
)
return agent
PydanticAI: memória (histórico) e runner síncrono
O histórico de mensagens é o mecanismo que preserva contexto, como restrições alimentares e itens já discutidos. No PydanticAI, o agente pode ser considerado “sem estado” nesse aspecto, porque o histórico é passado explicitamente a cada chamada. Isso deixa claro onde o estado vive e facilita resetar ou persistir conversas.
Um runner simples guarda uma lista de mensagens e a reutiliza a cada nova solicitação. A resposta do framework inclui o dado estruturado e também acesso ao conjunto completo de mensagens. Ao atualizar o histórico com o retorno mais recente, o loop passa a ter memória acumulada.
O trecho abaixo mostra um runner para PydanticAI com histórico explícito e retorno tipado em LLMResponse. A frase anterior explica que o código demonstra como a memória fica fora do agente e é alimentada a cada interação.
import argparse
class PydanticAIAgentRunner(AgentRunner):
def __init__(self, menu_service: MenuService, order_service: OrderService, args: argparse.Namespace):
self.agent = get_agent(model_name=args.model, api_key=getattr(args, "api_key", None))
self.deps = Dependencies(
menu_service=menu_service,
order_service=order_service,
restaurant_name=args.restaurant_name,
table_number=args.table_number,
)
self.message_history: list[ModelMessage] = []
def make_request(self, user_message: str) -> LLMResponse:
ai_response = self.agent.run_sync(
user_message,
deps=self.deps,
message_history=self.message_history,
)
self.message_history = ai_response.all_messages()
return ai_response.data
LangChain: visão geral e por que a composição pode parecer mais complexa
O LangChain é um ecossistema amplo para aplicações com LLMs, com muitos componentes, integrações e formas de composição. Essa amplitude traz flexibilidade, mas também aumenta a quantidade de conceitos e caminhos possíveis. Em projetos, isso pode aparecer como múltiplas formas de resolver o mesmo problema, além de APIs mais antigas convivendo com abordagens mais novas.
Nesse tipo de implementação, um “agente” muitas vezes é uma cadeia de componentes: prompt, modelo, parsers e lógica de ferramentas, e um executor que conduz o loop de raciocínio e chamadas. Para manter histórico, pode ser necessário um wrapper específico, e a configuração de sessão pode aparecer mesmo em casos simples. Quando também existe saída estruturada e múltiplas ferramentas, detalhes de compatibilidade entre componentes ficam mais evidentes.
Apesar disso, LangChain oferece blocos poderosos, como templates de prompt, padronização de ferramentas e mecanismos de memória. A principal dificuldade, em cenários como o do garçom, costuma ser combinar tool calling, saída estruturada e histórico de forma previsível. Por esse motivo, uma implementação cuidadosa precisa explicitar etapas que em outras bibliotecas ficam mais “lineares”.
LangChain: ferramentas com BaseTool e injeção de dependências por composição
Para ferramentas acessarem dependências em tempo de execução, uma estratégia é modelá-las como classes que recebem serviços no construtor. Isso evita tentar “encaixar” dependências em decoradores funcionais e torna explícito que cada ferramenta tem estado ou referências próprias. No caso do restaurante, uma ferramenta consulta o cardápio e outra registra o pedido.
LangChain frequentemente utiliza uma classe base de ferramenta, e a ferramenta implementa um método de execução. Quando há parâmetros, um schema baseado em Pydantic pode descrever a entrada esperada. Isso permite que o modelo chame a ferramenta com argumentos validados.
O trecho a seguir mostra duas ferramentas baseadas em classe e um schema de entrada para criação de pedido. A frase anterior explica que o código demonstra a abordagem de composição para injetar dependências diretamente nas ferramentas.
from typing import Annotated
from pydantic import BaseModel
# Nomes importados do LangChain (mantidos como referência de uso)
from langchain.tools import BaseTool
class GetMenuTool(BaseTool):
name: str = "get_menu"
description: str = "Retorna o cardápio completo do restaurante"
menu_service: MenuService
def _run(self) -> dict[str, list[str]]:
return self.menu_service.get_menu()
class CreateOrderInputSchema(BaseModel):
table_number: int
order_items: Annotated[list[str], "Lista de itens do cardápio para pedir"]
class CreateOrderTool(BaseTool):
name: str = "create_order"
description: str = "Registra um pedido para a mesa"
args_schema: type[BaseModel] = CreateOrderInputSchema
order_service: OrderService
def _run(self, table_number: int, order_items: list[str]) -> str:
self.order_service.create_order(table_number, order_items)
return "Pedido registrado"
LangChain: saída estruturada via ferramenta dedicada
A saída estruturada é útil para garantir que o agente sempre retorne campos obrigatórios, como a mensagem final e o indicador de término. Em LangChain, existem recursos para “amarrar” um schema ao modelo, mas em alguns arranjos isso pode conflitar com a necessidade de múltiplas ferramentas. Uma alternativa prática é criar uma ferramenta cujo único papel é “responder ao usuário” com o schema estruturado.
Essa ferramenta não precisa executar lógica de negócio, apenas validar e serializar a resposta. Um detalhe comum é que alguns encadeamentos esperam strings como saída, então a resposta estruturada pode ser serializada para JSON e depois desserializada no runner. Essa estratégia mantém o contrato de saída consistente mesmo quando o executor usa mecanismos internos que não preservam objetos tipados.
O código abaixo mostra uma ferramenta dedicada a respostas estruturadas, usando LLMResponse como schema de argumentos. A frase anterior explica que o trecho demonstra como forçar o modelo a produzir uma resposta estruturada por meio de uma ferramenta.
from langchain.tools import BaseTool
class StructuredResponseTool(BaseTool):
name: str = "respond_to_user"
description: str = (
"Sempre usar esta ferramenta para responder ao usuário em vez de responder diretamente. "
"O campo `message` contém a resposta conversacional. "
"O campo `end_conversation` deve ser True apenas se a conversa terminar após esta resposta."
)
args_schema: type[BaseModel] = LLMResponse
# Em alguns executores legados, isso faz a saída retornar diretamente
return_direct: bool = True
def _run(self, message: str, end_conversation: bool) -> str:
# Serializa para evitar incompatibilidades com componentes que esperam string
return LLMResponse(message=message, end_conversation=end_conversation).model_dump_json()
LangChain: prompt com ChatPromptTemplate e placeholders de histórico
Em LangChain, um template de prompt pode ser composto por mensagens de sistema e humano, além de placeholders para histórico e para “rascunho” do agente. Essa composição é útil para agentes com ferramentas, pois o executor pode inserir mensagens de ferramentas e passos intermediários no local apropriado. Para o restaurante, o prompt precisa receber nome do restaurante e número da mesa, além do histórico.
O mecanismo de placeholders permite que o executor injete automaticamente mensagens anteriores. Isso é parte do que torna a solução flexível, mas também exige que as chaves corretas estejam alinhadas entre prompt, executor e wrapper de histórico. Se qualquer chave estiver errada, o modelo pode perder contexto ou o executor pode falhar ao montar a entrada.
O trecho a seguir mostra uma configuração típica de prompt com placeholders para histórico e para o scratchpad do agente. A frase anterior explica que o código demonstra como o prompt é montado para suportar histórico e tool calling.
from langchain.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
("system", PROMPT_TEMPLATE),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)
LangChain: executor do agente e memória com RunnableWithMessageHistory
O AgentExecutor é responsável por conduzir a interação: enviar prompt ao modelo, processar chamadas de ferramenta e repetir esse ciclo até obter uma resposta final. Em algumas configurações, o executor retorna apenas a saída final e não expõe diretamente as mensagens intermediárias. Para preservar histórico, um wrapper de memória pode ser usado para armazenar mensagens entre chamadas.
Um wrapper como RunnableWithMessageHistory conecta o executor a um objeto de histórico em memória, frequentemente exigindo uma configuração de sessão. Mesmo quando só há uma conversa, a sessão pode ser solicitada porque o wrapper foi desenhado para múltiplos diálogos paralelos. Isso adiciona um detalhe de configuração que não é estritamente necessário para casos simples, mas faz parte do contrato do componente.
O trecho abaixo mostra uma forma de construir o executor com ferramentas, forçar uso de ferramentas e ativar histórico com um objeto único de memória. A frase anterior explica que o código reúne prompt, bind de ferramentas, parser de saída e wrapper de histórico em uma cadeia executável.
from typing import Sequence
from langchain.agents import AgentExecutor
from langchain_core.runnables import RunnablePassthrough, RunnableWithMessageHistory, RunnableConfig
from langchain_core.output_parsers import ToolsAgentOutputParser
from langchain_core.messages import ChatMessageHistory
from langchain.agents.format_scratchpad import format_to_tool_messages
def get_agent_executor(
tools: Sequence[BaseTool],
model_name: str,
api_key: str | None = None,
) -> RunnableWithMessageHistory:
model = build_model_from_name_and_api_key(model_name=model_name, api_key=api_key)
prompt = ChatPromptTemplate.from_messages(
[
("system", PROMPT_TEMPLATE),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
]
)
# Força o modelo a escolher ferramentas quando apropriado
llm_with_tools = model.bind_tools(tools, tool_choice=True)
agent = (
RunnablePassthrough.assign(
agent_scratchpad=lambda x: format_to_tool_messages(x["intermediate_steps"])
)
| prompt
| llm_with_tools
| ToolsAgentOutputParser()
)
agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools)
message_history = ChatMessageHistory()
agent_with_chat_history = RunnableWithMessageHistory(
runnable=agent_executor,
get_session_history=lambda _: message_history,
input_messages_key="input",
history_messages_key="chat_history",
)
return agent_with_chat_history
LangChain: runner com desserialização da resposta estruturada
Como a ferramenta de resposta estruturada serializa a saída em JSON, o runner precisa desserializar o texto para recuperar um objeto tipado. Isso cria um contrato explícito: o executor devolve uma string, e o runner valida como LLMResponse. Essa validação ajuda a capturar casos em que o modelo não respeita o schema.
Outra parte importante é separar entradas “estáticas” e “dinâmicas”. Entradas estáticas são parâmetros do prompt, como restaurante e mesa; entradas dinâmicas são a mensagem do usuário. Além disso, a configuração de sessão entra como um detalhe necessário para o wrapper de histórico funcionar.
O código abaixo mostra um runner que inicializa ferramentas com dependências, cria o executor e interpreta a saída final como LLMResponse. A frase anterior explica que o trecho demonstra a ponte entre o resultado do executor e o tipo estruturado usado pelo loop de conversa.
import argparse
class LangChainAgentRunner(AgentRunner):
def __init__(self, menu_service: MenuService, order_service: OrderService, args: argparse.Namespace):
tools = [
GetMenuTool(menu_service=menu_service),
CreateOrderTool(order_service=order_service),
StructuredResponseTool(),
]
self.agent_executor = get_agent_executor(tools=tools, model_name=args.model, api_key=getattr(args, "api_key", None))
self.static_input = {
"restaurant_name": args.restaurant_name,
"table_number": args.table_number,
}
self.config: RunnableConfig = {"configurable": {"session_id": "sessao-unica"}}
def make_request(self, user_message: str) -> LLMResponse:
result = self.agent_executor.invoke(self.static_input | {"input": user_message}, self.config)
# Espera-se que o executor retorne algo como {"output": "...json..."}
response = LLMResponse.model_validate_json(result["output"])
return response
Comparação direta dos tópicos: modelo dinâmico, human-in-the-loop, ferramentas, memória e prompt
A escolha dinâmica de modelo tende a ser um detalhe de construção: em ambos os casos, uma função fábrica pode escolher o provedor e instanciar o cliente com base em nome e chave. A diferença prática aparece na quantidade de componentes ao redor: no PydanticAI, a troca costuma afetar principalmente a criação do Agent; no LangChain, pode afetar também a forma de bind de ferramentas e o tipo de executor usado.
O human-in-the-loop aparece no loop de conversa: cada iteração depende da entrada humana e do estado acumulado. Em PydanticAI, o histórico explícito facilita pausar, persistir e retomar, pois o estado está em uma lista controlada. Em LangChain, o wrapper de histórico centraliza a memória, mas pode introduzir requisitos de sessão e chaves de mapeamento.
Em tool calling com dependências, PydanticAI oferece um caminho direto com RunContext e dependências tipadas. Em LangChain, a abordagem com classes de ferramentas resolve dependências de forma clara, mas a composição com executor, parser e memória tende a ser mais verbosa. Em ambos, descrever bem argumentos e retornar dados consistentes reduz erros do modelo ao chamar ferramentas.
Para prompt dinâmico, PydanticAI usa uma função de prompt ligada ao agente, e LangChain usa placeholders do template. A diferença é que no PydanticAI o prompt dinâmico costuma se conectar naturalmente às dependências tipadas, enquanto no LangChain a substituição depende do dicionário de entrada. Nos dois casos, manter o prompt com regras explícitas ajuda a controlar encerramento e uso correto de ferramentas.
Saída estruturada e controle de término: por que isso reduz ambiguidade
Quando um agente retorna apenas texto livre, o programa precisa inferir se a conversa acabou, o que é frágil. A saída estruturada resolve esse problema criando um contrato: sempre haverá um campo de texto e um campo booleano de término. Assim, o loop de conversa se torna determinístico, independentemente do estilo do modelo.
Além disso, a saída estruturada ajuda a manter o agente “bem-comportado” em cenários com regras. Se a regra diz que o término só ocorre quando não houver pergunta, o modelo pode ser instruído a controlar esse campo de forma consistente. A validação do schema também identifica rapidamente respostas que não seguem o formato esperado.
Em PydanticAI, esse mecanismo costuma estar no centro do framework, pois o resultado tipado é parte da configuração do agente. Em LangChain, é possível chegar ao mesmo resultado, mas uma abordagem comum envolve uma ferramenta de resposta estruturada, serialização e desserialização. O efeito final é semelhante, porém o caminho pode exigir mais componentes.
Encerramento: síntese das diferenças práticas no cenário proposto
No agente do garçom excêntrico, ambos os frameworks conseguem implementar escolha de modelo, ferramentas, memória, prompt dinâmico e saída estruturada. A diferença principal aparece na quantidade de etapas necessárias para combinar tudo de forma previsível. No PydanticAI, a combinação de dependências tipadas, ferramentas simples e resultado estruturado tende a formar um fluxo mais linear.
No LangChain, a composição oferece grande flexibilidade, mas pode exigir mais entendimento de executores, parsers, wrappers de histórico e detalhes de configuração. Para obter saída estruturada junto com ferramentas e memória, uma estratégia robusta é tratar a resposta estruturada como uma ferramenta explícita. Esse arranjo funciona, mas introduz serialização e uma camada adicional de montagem.
Como fechamento do tema, o cenário mostra que a simplicidade percebida depende do caminho crítico: quando o foco está em um agente conversacional com ferramentas e tipagem forte, o PydanticAI costuma reduzir atrito. Quando o foco está em ecossistemas amplos e combinações variadas de componentes, o LangChain fornece muitas opções, mas frequentemente cobra mais complexidade para chegar ao mesmo comportamento final com confiabilidade.