2.3 Eines i Function Calling¶
Objectiu
Entendre com els agents connecten els LLM amb el món real mitjançant eines (tools) i el mecanisme de function calling. Aprendre a definir, registrar i depurar eines en LangChain.
🔧 Què és una Eina?¶
Una eina (tool) és qualsevol funció que l'agent pot cridar per obtenir informació o executar accions que el LLM no pot fer per si sol:
Cerca i Informació
Buscar a internet, consultar Wikipedia, llegir notícies, obtenir preus en temps real.
Execució de Codi
Executar Python, fer càlculs, generar gràfics, processar dades estructurades.
Sistema de Fitxers
Llegir PDFs, escriure fitxers, processar CSVs, gestionar directoris.
APIs Externes
Enviar correus, crear events al calendari, consultar bases de dades, cridar serveis web.
Sense eines, un agent és com un expert tancat en una habitació sense telèfon ni internet. Les eines són els "braços" de l'agent.
⚡ Function Calling: Com el LLM Decideix Usar una Eina¶
El mecanisme de function calling és la capacitat dels LLM moderns per generar crides estructurades a funcions en comptes de text lliure, quan detecten que necessiten informació externa.
El Flux Complet¶
Usuari: "Quina temperatura fa ara a Barcelona?"
┌─ LLM rep el missatge
│ Detecta que no té dades en temps real
│ Decideix cridar l'eina "cercar_temps"
│
├─ LLM genera (JSON estructurat, NO text):
│ {
│ "tool": "cercar_temps",
│ "arguments": {"ciutat": "Barcelona"}
│ }
│
├─ L'executor crida la funció real
│ → API meteorològica retorna: "22°C, ennuvolat"
│
├─ L'executor retorna el resultat al LLM
│
└─ LLM genera la resposta final:
"Actualment a Barcelona hi ha 22°C amb cel ennuvolat."
Com Sap el LLM Quines Eines Té Disponibles?¶
Al inici de cada crida, s'envia al LLM el schema (descripció estructurada) de totes les eines disponibles:
# Això és el que rep el LLM (simplificat)
tools_schema = [
{
"name": "cercar_temps",
"description": "Obté la temperatura i condicions meteorològiques actuals d'una ciutat",
"parameters": {
"type": "object",
"properties": {
"ciutat": {
"type": "string",
"description": "Nom de la ciutat, ex: 'Barcelona', 'Madrid'"
}
},
"required": ["ciutat"]
}
},
{
"name": "calcular",
"description": "Executa càlculs matemàtics o expressions Python",
"parameters": {
"type": "object",
"properties": {
"expressio": {
"type": "string",
"description": "Expressió matemàtica o codi Python a avaluar"
}
},
"required": ["expressio"]
}
}
]
La descripció és crítica
El LLM tria quina eina usar basant-se únicament en la descripció textual. Una descripció vaga o incorrecta farà que l'agent falli o triï l'eina equivocada. La descripció és el "manual d'instruccions" de l'eina per al LLM.
🛠️ Definir Eines amb LangChain¶
Mètode 1: Decorador @tool (el més senzill)¶
from langchain_core.tools import tool
@tool
def calcular(expressio: str) -> str:
"""
Avalua una expressió matemàtica i retorna el resultat.
Usa-la per a qualsevol càlcul numèric: sumes, percentatges,
conversions d'unitats, etc.
Args:
expressio: Expressió matemàtica, ex: '25 * 1.21' o '(100 / 3) * 2'
Returns:
El resultat numèric com a text.
"""
try:
# Eval segur: només operadors matemàtics
resultat = eval(expressio, {"__builtins__": {}}, {})
return f"Resultat: {resultat}"
except Exception as e:
return f"Error en el càlcul: {str(e)}"
@tool
def obtenir_hora_actual(zona_horaria: str = "Europe/Madrid") -> str:
"""
Retorna la data i hora actuals en la zona horària indicada.
Usa-la quan l'usuari pregunti per la data o hora actual.
Args:
zona_horaria: Zona horària IANA, ex: 'Europe/Madrid', 'America/New_York'
"""
from datetime import datetime
import zoneinfo
ara = datetime.now(zoneinfo.ZoneInfo(zona_horaria))
return ara.strftime("%A, %d de %B de %Y, %H:%M:%S %Z")
# Verificar que LangChain ha llegit el schema correctament
print(calcular.name) # → "calcular"
print(calcular.description) # → text del docstring
print(calcular.args) # → {'expressio': {'type': 'string', ...}}
Mètode 2: StructuredTool (per a eines amb múltiples paràmetres)¶
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
class CercaNotíciesInput(BaseModel):
consulta: str = Field(description="Termes de cerca en català o anglès")
max_resultats: int = Field(default=3, description="Nombre màxim de resultats (1-10)")
idioma: str = Field(default="ca", description="Idioma: 'ca', 'es', 'en'")
def cerca_noticies(consulta: str, max_resultats: int = 3, idioma: str = "ca") -> str:
"""Cerca notícies recents a internet i retorna els títols i resums."""
# Implementació real usaria DuckDuckGo, Tavily, Serper, etc.
# Aquí: versió simplificada per a exemple
return f"[Simulació] Resultats per '{consulta}': notícia 1, notícia 2..."
eina_noticies = StructuredTool.from_function(
func=cerca_noticies,
name="cerca_noticies",
description="Cerca informació actualitzada a internet sobre qualsevol tema",
args_schema=CercaNotíciesInput,
)
Mètode 3: Eines Predefinides de LangChain¶
LangChain inclou eines llestes per usar, sense implementació:
# Eines de cerca
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_community.tools.tavily_search import TavilySearchResults
# Eines de codi
from langchain_experimental.tools import PythonREPLTool
# Eines de fitxers
from langchain_community.tools.file_management import (
ReadFileTool, WriteFileTool, ListDirectoryTool
)
# Eines de Wikipedia
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
# Instanciar
cerca_web = DuckDuckGoSearchRun()
wikipedia = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper(lang="ca"))
python_repl = PythonREPLTool()
# Totes les eines van al llistat de l'agent
tools = [cerca_web, wikipedia, python_repl]
🏗️ Integrar Eines en un Agent¶
import os
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
# 1. LLM
llm = ChatAnthropic(
model="claude-sonnet-4-6",
api_key=os.environ["ANTHROPIC_API_KEY"]
)
# 2. Eines personalitzades
@tool
def calcular(expressio: str) -> str:
"""Avalua expressions matemàtiques. Ex: '15 * 1.21', '100 / 7'"""
try:
return str(eval(expressio, {"__builtins__": {}}, {}))
except Exception as e:
return f"Error: {e}"
@tool
def convertir_divises(quantitat: float, de_divisa: str, a_divisa: str) -> str:
"""
Converteix entre divises. Usa codis ISO 4217.
Ex: convertir_divises(100, 'EUR', 'USD')
NOTA: Retorna una estimació; per valors exactes usar API bancària.
"""
# Taxes aproximades (producció: usar API com Frankfurter)
taxes = {"EUR": 1.0, "USD": 1.08, "GBP": 0.86, "JPY": 161.0}
if de_divisa not in taxes or a_divisa not in taxes:
return f"Divisa no suportada. Disponibles: {list(taxes.keys())}"
resultat = quantitat / taxes[de_divisa] * taxes[a_divisa]
return f"{quantitat} {de_divisa} ≈ {resultat:.2f} {a_divisa}"
# 3. Eines predefinides
cerca = DuckDuckGoSearchRun()
# 4. Llistat complet
tools = [calcular, convertir_divises, cerca]
# 5. Prompt de l'agent
prompt = ChatPromptTemplate.from_messages([
("system", """Ets un assistent útil amb accés a eines.
Usa les eines quan necessitis informació actualitzada o fer càlculs.
Respon sempre en català."""),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
# 6. Crear i executar agent
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# 7. Provar
resposta = executor.invoke({
"input": "Quant és el 21% d'IVA de 350€? I a quants dòlars equivalen?",
"chat_history": []
})
print(resposta["output"])
Sortida esperada amb verbose=True¶
> Entering new AgentExecutor chain...
Invoking: `calcular` with {'expressio': '350 * 0.21'}
Resultado: 73.5
Invoking: `calcular` with {'expressio': '350 + 73.5'}
Resultado: 423.5
Invoking: `convertir_divises` with {'quantitat': 423.5, 'de_divisa': 'EUR', 'a_divisa': 'USD'}
423.5 EUR ≈ 457.38 USD
El 21% d'IVA de 350€ és **73,50€**, el que dona un total de **423,50€**.
Això equivaldria aproximadament a **457,38 dòlars** (USD).
> Finished chain.
🎨 Tipus d'Eines per Categoria¶
Eines de Cerca i Coneixement¶
| Eina | Paquet | Ús |
|---|---|---|
DuckDuckGoSearchRun |
langchain-community |
Cerca web gratuïta, sense API key |
TavilySearchResults |
langchain-community |
Cerca optimitzada per a agents (recomanada) |
WikipediaQueryRun |
langchain-community |
Consultes a Wikipedia |
ArxivQueryRun |
langchain-community |
Cerca papers científics |
YouTubeSearchTool |
langchain-community |
Cerca vídeos a YouTube |
Eines de Codi i Càlcul¶
| Eina | Paquet | Ús |
|---|---|---|
PythonREPLTool |
langchain-experimental |
Executa codi Python arbitrari |
E2BDataAnalysisTool |
langchain-community |
Sandbox segur per a codi (recomanat producció) |
PythonREPLTool en producció
PythonREPLTool executa codi Python sense sandbox. En producció, usa sempre un entorn aïllat (Docker, E2B, etc.). Un usuari maliciós podria injectar import os; os.system("rm -rf /").
Eines de Fitxers i Documents¶
from langchain_community.tools.file_management import (
ReadFileTool,
WriteFileTool,
CopyFileTool,
DeleteFileTool,
ListDirectoryTool,
MoveFileTool,
)
# Sempre limitar a un directori segur
from langchain.agents.agent_toolkits import FileManagementToolkit
toolkit = FileManagementToolkit(
root_dir="./dades_agent", # L'agent NOMÉS pot accedir aquí
selected_tools=["read_file", "write_file", "list_directory"]
)
tools_fitxers = toolkit.get_tools()
Eines d'APIs i Serveis Externs¶
# Correu electrònic
from langchain_community.tools.gmail.send_message import GmailSendMessage
# Calendari
from langchain_community.tools.google_calendar.create_event import GoogleCalendarCreateEvent
# Base de dades SQL
from langchain_community.tools.sql_database.tool import QuerySQLDataBaseTool
from langchain_community.utilities import SQLDatabase
db = SQLDatabase.from_uri("sqlite:///empresa.db")
eina_sql = QuerySQLDataBaseTool(db=db)
# APIs personalitzades amb requests
@tool
def consultar_estoc(referencia: str) -> str:
"""
Consulta l'estoc d'un producte per la seva referència.
Retorna la quantitat disponible al magatzem.
"""
import requests
try:
r = requests.get(f"https://api.empresa.com/estoc/{referencia}", timeout=5)
dades = r.json()
return f"Referència {referencia}: {dades['quantitat']} unitats disponibles"
except Exception as e:
return f"Error consultant l'estoc: {e}"
🐛 Depurar i Validar Eines¶
Provar una Eina Directament¶
# Sempre prova les eines de forma aïllada abans d'integrar-les
print(calcular.invoke("15 * 1.21")) # → "18.15"
print(calcular.invoke("hola")) # → "Error: ..."
print(convertir_divises.invoke({
"quantitat": 100,
"de_divisa": "EUR",
"a_divisa": "USD"
})) # → "100 EUR ≈ 108.00 USD"
Gestió d'Errors: Fer les Eines Robustes¶
@tool
def llegir_pdf(ruta_fitxer: str) -> str:
"""
Llegeix el contingut textual d'un fitxer PDF.
Usa-la per analitzar documents, contractes o informes en format PDF.
Args:
ruta_fitxer: Ruta al fitxer PDF, ex: 'documents/informe.pdf'
"""
import os
from pathlib import Path
# Validació: el fitxer existeix?
if not os.path.exists(ruta_fitxer):
return f"Error: el fitxer '{ruta_fitxer}' no existeix."
# Validació: és un PDF?
if not ruta_fitxer.lower().endswith(".pdf"):
return f"Error: '{ruta_fitxer}' no és un fitxer PDF."
# Validació: mida raonable (evitar PDFs de centenars de MB)
mida_mb = os.path.getsize(ruta_fitxer) / (1024 * 1024)
if mida_mb > 50:
return f"Error: el fitxer és massa gran ({mida_mb:.1f} MB). Màxim: 50 MB."
try:
import PyPDF2
with open(ruta_fitxer, "rb") as f:
reader = PyPDF2.PdfReader(f)
text = "\n".join(p.extract_text() or "" for p in reader.pages)
return text[:8000] # Limitar tokens
except Exception as e:
return f"Error llegint el PDF: {str(e)}"
Bones Pràctiques per a Eines Fiables¶
# ✅ Eina ben dissenyada
@tool
def cerca_producte(nom: str, categoria: str = "tots") -> str:
"""
Cerca productes al catàleg de l'empresa per nom o paraules clau.
Usa-la quan l'usuari pregunti per preus, disponibilitat o
característiques de productes específics.
Args:
nom: Nom o paraules clau del producte. Ex: 'portàtil', 'monitor 27"'
categoria: Filtre opcional: 'informàtica', 'so', 'imatge', 'tots'
Returns:
Llista de productes trobats amb preu i estoc.
"""
# Retorna sempre un string (mai None, mai excepció no capturada)
resultats = _consultar_bd(nom, categoria)
if not resultats:
return f"No s'han trobat productes per '{nom}' a la categoria '{categoria}'."
return "\n".join(f"- {p['nom']}: {p['preu']}€ ({p['estoc']} unitats)"
for p in resultats[:5])
🧩 Toolkits: Eines Agrupades¶
Els toolkits agrupen eines relacionades. LangChain n'inclou per a casos d'ús comuns:
# Toolkit per a anàlisi de dades amb Pandas
from langchain_experimental.agents import create_pandas_dataframe_agent
import pandas as pd
df = pd.read_csv("vendes_2024.csv")
agent_pandas = create_pandas_dataframe_agent(
llm=llm,
df=df,
verbose=True,
allow_dangerous_code=True # Necessari per a PythonREPL intern
)
# L'agent pot fer consultes en llenguatge natural!
agent_pandas.invoke("Quines són les 3 categories amb més vendes el Q4?")
agent_pandas.invoke("Mostra un gràfic de barres de vendes mensuals")
🔑 Conceptes Clau d'aquesta Secció¶
| Terme | Definició |
|---|---|
| Tool / Eina | Funció que l'agent pot cridar per obtenir informació o executar accions. |
| Function Calling | Capacitat del LLM per generar crides estructurades (JSON) a funcions externes. |
| Tool Schema | Descripció estructurada (nom, paràmetres, tipus) que el LLM usa per saber com cridar l'eina. |
| AgentExecutor | Component de LangChain que gestiona el cicle raonament→acció→observació. |
| Toolkit | Col·lecció d'eines relacionades per a un cas d'ús específic. |
| REPL | Read-Eval-Print Loop. Entorn d'execució de codi interactiu. |
✅ Activitats de Consolidació¶
Exercici 2.3.1 — Primera Eina Personalitzada
Crea una eina convertir_temperatura que:
- Accepti un valor numèric i les unitats d'origen i destí
- Suporti Celsius, Fahrenheit i Kelvin
- Retorni el resultat formatat amb 2 decimals
- Gestioni errors (unitats no vàlides, valors impossibles com -300°C)
Prova-la directament amb .invoke() i integra-la en un agent.
Exercici 2.3.2 — Agent Multi-Eina
Construeix un agent "Assistent de Viatges" amb les eines:
DuckDuckGoSearchRun(informació sobre destinacions)calcular(conversió de divises, càlcul de dies)- Una eina personalitzada
crear_itinerarique generi un fitxer.txtamb l'itinerari
L'agent ha de poder respondre: "Planifica un viatge de 5 dies a Lisboa per a 2 persones amb pressupost de 1.500€"
Exercici 2.3.3 — Anàlisi de Seguretat
Analitza el codi de PythonREPLTool de LangChain:
- Quin tipus d'atac permet si s'usa sense restriccions?
- Quines mesures de mitigació implementaries?
- Busca i documenta com E2B (e2b.dev) aïlla l'execució de codi
📚 Lectures Complementàries¶
- LangChain — Tools How-To Guide
- Anthropic — Tool Use Documentation
- OpenAI — Function Calling Guide
- E2B — Code Interpreter SDK — Sandbox segur per a execució de codi