De uma ferramenta para várias
Na semana passada o agente ganhou a primeira ferramenta: @tool com Pydantic validando os argumentos, descrição clara decidindo o resto, e o ciclo ReAct girando na mão (bind_tools) e no automático (create_agent). E prometemos: o ainvoke assíncrono ia aparecer — hoje ele aparece.
Hoje o repertório expande em três direções: o agente passa a escolher entre várias ferramentas, encadear duas ou mais na mesma pergunta, e consumir ferramentas que outra pessoa escreveu — via MCP.
Chamadas encadeadas
Na semana passada, o agente usava uma ferramenta por vez. Mas e quando a pergunta precisa de duas ou mais?
Exemplo: "Quanto é 234 × 987 + 100?"
Thought: Preciso multiplicar 234 × 987, depois somar 100 Action: calculate(234, 987, "multiply") → 230958 Action: calculate(230958, 100, "add") → 231058 Answer: "O resultado é 231.058."
O create_agent faz isso automaticamente. Ele repete o ciclo Thought→Action→Observation até ter a resposta final.
from langchain.agents import create_agent agente = create_agent(llm, [calculate, get_time, get_word_length]) resultado = agente.invoke({"messages": "Quanto é 234 vezes 987 mais 100?"}) print(resultado["messages"][-1].content) # "O resultado de 234 × 987 + 100 é 231.058."
O agente decide sozinho: preciso de duas chamadas a calculate? Então faço duas.
O problema das descrições vagas
Se duas ferramentas parecem fazer a mesma coisa, o modelo confunde.
Exemplo de conflito:
@tool def search_wiki(query: str) -> str: """Search for information.""" ... @tool def search_web(query: str) -> str: """Search for information.""" ...
Duas ferramentas com a mesma descrição genérica. O LLM não sabe qual usar.
Solução: descrições específicas
@tool def search_wiki(query: str) -> str: """Search Wikipedia for factual, historical, and encyclopedic information. Use for established facts about people, places, events, and concepts. """ ... @tool def search_web(query: str) -> str: """Search the web for current information like news, prices, and recent events. Use for real-time data that changes frequently. """ ...
Agora o modelo distingue: fato histórico → search_wiki, notícia atual → search_web.
Regra prática: cada ferramenta precisa de uma descrição exclusiva que explique quando usar.
Debug de ferramentas
O LLM nem sempre acerta. Eis os problemas mais comuns e como resolver.
Problema 1: LLM chama a ferramenta errada
Pergunta: "Que horas são?"
LLM chama: calculate em vez de get_time
Causa: descrição de calculate muito vaga ("Faz cálculos") ou descrição de get_time não menciona "que horas são".
Solução: melhorar a descrição da ferramenta correta.
# ❌ Vaga @tool def get_time() -> str: """Returns current date and time.""" # ✅ Específica @tool def get_time() -> str: """Returns the current date and time. Use when the user asks 'what time is it', 'what date is today', or any question about the current moment. """
Problema 2: argumentos errados
LLM manda: calculate(a=234, b=987, operation"234 vezes 987")=
Esperado: operation deve ser "multiply", não "234 vezes 987"
Causa: descrição do parâmetro operation não lista os valores possíveis claramente.
Solução: documentar valores permitidos no docstring.
@tool def calculate(a: float, b: float, operation: str) -> float: """Performs a mathematical operation between two numbers. Use when you need to perform numeric calculations. Args: a: First number b: Second number operation: One of 'add', 'subtract', 'multiply', 'divide' """
Problema 3: ValidationError
Se o LLM manda um argumento com tipo errado (ex: passar texto onde espera número), Pydantic gera um ValidationError com contexto claro.
# ValidationError: Input should be a valid number, # unable to parse string as a number (field: a)
Isso é bom — o agente recebe o erro e pode corrigir. Mas se acontece muito, a descrição do parâmetro pode estar confusa.
Ferramentas externas via MCP
Até agora, você escreveu as ferramentas. Mas e se outra pessoa escreveu a ferramenta e você só quer usar?
O MCP (Model Context Protocol) é um protocolo padrão para ferramentas. Pense nele como o HTTP das ferramentas de IA — assim como HTTP padronizou a web, MCP padroniza como ferramentas se conectam a agentes.
Conceito
Model Context Protocol (criado pela Anthropic, open-source):
- Um servidor MCP expõe ferramentas
- Um cliente MCP consome essas ferramentas
- Protocolo padrão — qualquer LLM pode se conectar a qualquer servidor MCP
Na semana passada (S07), você escreveu as ferramentas com @tool. Hoje (S08), você consome ferramentas que alguém escreveu, via MCP.
Servidor MCP com FastMCP
Vamos criar um servidor MCP simples — depois vamos consumi-lo como cliente.
# math_server.py — Servidor MCP com duas ferramentas from mcp.server.fastmcp import FastMCP mcp = FastMCP("Math") @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b @mcp.tool() def multiply(a: int, b: int) -> int: """Multiply two numbers together.""" return a * b if __name__ == "__main__": mcp.run(transport="stdio")
Repare: é quase idêntico ao @tool do LangChain, mas com @mcp.tool() e FastMCP em vez de langchain_core.tools.
O servidor usa transport="stdio" — roda no mesmo processo, sem servidor externo.
Cliente: consumindo MCP no LangChain
Instale: pip install langchain-mcp-adapters
import subprocess from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain.agents import create_agent server_params = StdioServerParameters( command="python", args=["math_server.py"], ) async with stdio_client( server_params, errlog=subprocess.DEVNULL) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await load_mcp_tools(session) # tools agora são @tool do LangChain — usa igual! agente = create_agent(llm, tools) resultado = await agente.ainvoke( {"messages": "Quanto é 3 + 5 multiplicado por 2?"} ) print(resultado["messages"][-1].content)
O errlog=subprocess.DEVNULL silencia o stderr do subprocesso. No Jupyter/Colab é necessário — o kernel substitui o stderr padrão e o MCP quebra com UnsupportedOperation: fileno. Localmente funciona com ou sem.
O que aconteceu:
stdio_clientiniciou o servidormath_server.pyem backgroundClientSessionfez o handshake MCPload_mcp_toolsconverteu as ferramentas MCP em@tooldo LangChaincreate_agentusou as ferramentas normalmente — igual à semana passada
Servidor MCP remoto (HTTP)
O exemplo acima usa transport="stdio" — o cliente spawn o servidor localmente. Mas o MCP também suporta transporte HTTP, permitindo conectar a servidores remotos.
Para rodar o mesmo servidor como HTTP:
# math_server_http.py — Mesmo servidor, mas acessível via HTTP from mcp.server.fastmcp import FastMCP mcp = FastMCP("Math", port=8000) @mcp.tool() def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b @mcp.tool() def multiply(a: int, b: int) -> int: """Multiply two numbers together.""" return a * b # A porta fica no FastMCP (não no run) — sobe em http://localhost:8000/mcp mcp.run(transport="streamable-http")
O código do servidor é quase idêntico: a porta fica explícita no FastMCP e o transporte muda na hora de subir. O servidor fica disponível em http://localhost:8000/mcp e pode ser acessado de qualquer máquina com conectividade.
Cliente remoto (HTTP)
No cliente, em vez de stdio_client, usamos streamablehttp_client com uma URL:
from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain.agents import create_agent # Aponta pra URL — pode ser localhost ou um servidor remoto async with streamablehttp_client( "http://localhost:8000/mcp" ) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await load_mcp_tools(session) agente = create_agent(llm, tools) # API idêntica — só muda como conecta
A API do agente é idêntica. A única diferença é como o cliente se conecta ao servidor: stdio spawn um processo local, streamable-http conecta a uma URL. Servidores MCP públicos (Slack, GitHub, Notion) funcionam assim — rodam remotamente e você só precisa da URL.
Cliente com auth (headers)
Servidores que exigem token pedem o header Authorization no cliente. Caso real: o SearXNG rodando no Raspberry Pi da rede local — o token vai no headers e o resto do cliente não muda:
import os from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client( "http://192.168.3.8:8899/mcp", # SearXNG no Raspberry Pi headers={"Authorization": f"Bearer {os.environ['SEARXNG_MCP_TOKEN']}"}, ) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # dali em diante: igual ao cliente sem auth
ClientSession, load_mcp_tools e o agente continuam exatamente iguais — só a conexão carrega o token, lido de $SEARXNG_MCP_TOKEN (export SEARXNG_MCP_TOKEN…= antes de rodar).
S07 vs S08 — resumo
| Semana | Ferramentas | Origem | Código |
|---|---|---|---|
| S07 | @tool — você escreve |
Seu código Python @tool + bind_tools + create_agent |
|
| S08 | MCP — alguém escreveu | Servidor externo load_mcp_tools + create_agent |
O agente não sabe a diferença. Pra ele, ferramenta é ferramenta.
Múltiplos servidores MCP
Você pode conectar vários servidores MCP ao mesmo agente, com AsyncExitStack:
import subprocess from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain.agents import create_agent SERVIDORES = [ StdioServerParameters(command="python", args=["math_server.py"]), StdioServerParameters(command="python", args=["string_server.py"]), ] async with AsyncExitStack() as stack: tools = [] for params in SERVIDORES: read, write = await stack.enter_async_context( stdio_client(params, errlog=subprocess.DEVNULL)) session = await stack.enter_async_context(ClientSession(read, write)) await session.initialize() tools.extend(await load_mcp_tools(session)) agente = create_agent(llm, tools) resultado = await agente.ainvoke( {"messages": "Quanto é 10 + 5? E inverta a palavra 'Guilda'."} ) print(resultado["messages"][-1].content)
(string_server.py é um segundo servidor — ferramentas de string, escritas com FastMCP do mesmo jeito.)
Cada entrada em SERVIDORES é um servidor a mais — sem aninhamento novo. E o AsyncExitStack encerra todos os subprocessos ao sair.
Existem outras formas (ex.: MultiServerMCPClient, da mesma lib), mas ela não aceita errlog — e sem isso o cliente quebra no Jupyter/Colab. O AsyncExitStack cobre os dois cenários.
Isso é poderoso: você monta um agente combinando ferramentas de fontes diferentes sem escrever código de integração.
Referências Importantes
Paper: Gorilla
Título: "Gorilla: Large Language Model Connected to Massive APIs" Autores: Shishir Patil, Tianjun Zhang, Xin Wang, Joseph E. Gonzalez Ano: 2023 Link: arXiv:2305.15334
Paper: ReAct
Título: "ReAct: Synergistic Reasoning and Acting in Language Models" Autores: Shunyu Yao, et al. Ano: 2022 Link: arXiv:2210.03629
MCP — Model Context Protocol
Site: modelcontextprotocol.io
Biblioteca: langchain-mcp-adapters v0.2.2 (MIT, Mar 2026)
GitHub: langchain-ai/langchain-mcp-adapters
Exercícios Práticos
Exercício 1: Descrições em Conflito
Crie duas ferramentas com descrições parecidas e teste o que acontece. Depois, corrija as descrições e veja a diferença.
📝 Gabarito
from langchain_core.tools import tool # ❌ Descrições vagas — LLM confunde @tool def find_word_meaning(word: str) -> str: """Search for information about a word.""" # (implementação simplificada) return f"Definition of {word}: ..." @tool def find_word_rhymes(word: str) -> str: """Search for information about a word.""" # (implementação simplificada) return f"Rhymes of {word}: ..." # Teste: "What does 'serendipity' mean?" # Possível erro: LLM escolhe find_word_rhymes em vez de find_word_meaning # ✅ Descrições específicas @tool def find_word_meaning(word: str) -> str: """Look up the definition and meaning of a word in a dictionary. Use when the user asks 'what does X mean' or 'definition of X'. """ return f"Definition of {word}: ..." @tool def find_word_rhymes(word: str) -> str: """Find words that rhyme with a given word. Use when the user asks for rhymes, similar-sounding words, or poetry help. """ return f"Rhymes of {word}: ..."
Exercício 2: Debug de Descrição
O agente abaixo chama a ferramenta errada. Corrija a descrição.
@tool def calculate(a: float, b: float, operation: str) -> float: """Do math.""" ops = {'add': a+b, 'subtract': a-b, 'multiply': a*b, 'divide': a/b} return ops[operation] @tool def get_time() -> str: """Gives info.""" from datetime import datetime return datetime.now().strftime("%d/%m/%Y %H:%M:%S") # Teste: "Que horas são?" # Resultado: LLM chama calculate em vez de get_time # Por quê? "Gives info" e "Do math" são vagas demais
📝 Gabarito
@tool def calculate(a: float, b: float, operation: str) -> float: """Performs a mathematical operation between two numbers. Use when you need to perform numeric calculations like addition, subtraction, multiplication, or division. Args: a: First number b: Second number operation: One of 'add', 'subtract', 'multiply', 'divide' """ ops = {'add': a+b, 'subtract': a-b, 'multiply': a*b, 'divide': a/b} return ops[operation] @tool def get_time() -> str: """Returns the current date and time. Use when the user asks about the current time, today's date, or 'what time is it'. """ from datetime import datetime return datetime.now().strftime("%d/%m/%Y %H:%M:%S")
Exercício 3: Servidor MCP
Crie um servidor MCP com uma ferramenta reverse_string que inverte uma string, depois escreva um cliente que a consome.
📝 Gabarito
Servidor (reverse_server.py):
from mcp.server.fastmcp import FastMCP mcp = FastMCP("Reverse") @mcp.tool() def reverse_string(text: str) -> str: """Reverses a string. Use when the user wants to flip or reverse text.""" return text[::-1] if __name__ == "__main__": mcp.run(transport="stdio")
Cliente:
import subprocess from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain.agents import create_agent from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gemma4:e2b-it-qat", base_url="http://localhost:11434/v1", api_key="ollama", temperature=0, ) server_params = StdioServerParameters( command="python", args=["reverse_server.py"], ) async with stdio_client( server_params, errlog=subprocess.DEVNULL) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await load_mcp_tools(session) agente = create_agent(llm, tools) resultado = await agente.ainvoke( {"messages": "Reverse the word 'Guilda'"} ) print(resultado["messages"][-1].content) # "The reverse of 'Guilda' is 'adliuG'."
Exercício 4: Agente com Ferramentas Mistas
Combine duas ferramentas @tool (que você escreveu) com ferramentas MCP de um servidor externo em um único agente.
📝 Gabarito
import subprocess from contextlib import AsyncExitStack from langchain_core.tools import tool from langchain.agents import create_agent from langchain_openai import ChatOpenAI from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from langchain_mcp_adapters.tools import load_mcp_tools llm = ChatOpenAI( model="gemma4:e2b-it-qat", base_url="http://localhost:11434/v1", api_key="ollama", temperature=0, ) # Suas ferramentas @tool @tool def calculate(a: float, b: float, operation: str) -> float: """Performs a mathematical operation between two numbers. Args: a: First number b: Second number operation: One of 'add', 'subtract', 'multiply', 'divide' """ ops = {'add': a+b, 'subtract': a-b, 'multiply': a*b, 'divide': a/b} return ops[operation] # Ferramentas MCP (servidor do início da apostila) SERVIDORES = [ StdioServerParameters(command="python", args=["math_server.py"]), ] async with AsyncExitStack() as stack: mcp_tools = [] for params in SERVIDORES: read, write = await stack.enter_async_context( stdio_client(params, errlog=subprocess.DEVNULL)) session = await stack.enter_async_context(ClientSession(read, write)) await session.initialize() mcp_tools.extend(await load_mcp_tools(session)) # Combina tudo all_tools = [calculate] + mcp_tools agente = create_agent(llm, all_tools) resultado = await agente.ainvoke( {"messages": "Quanto é 10 + 5 multiplicado por 3?"} ) print(resultado["messages"][-1].content)
Próxima Aula
Semana 09 (13/11): Embeddings, Similaridade e RAG — hoje o agente ganhou ferramentas de todo tipo, inclusive escritas por outra pessoa, via MCP. Na próxima ele aprende a buscar informação relevante em documentos: embeddings, similaridade e o RAG de verdade.