El mòdul 4 va acabar amb una frontera dibuixada amb precisió: DocuBot sap — respon amb fonts sobre tot el que diu la documentació de Nubelia — però no pot fer. La pregunta que vam deixar a l'aire, "quantes tasques té ara mateix el meu projecte Atlas?", no té resposta en cap wiki: la dada viu a l'API de Nubelia, canvia cada minut, i cap pipeline RAG del món no la recuperarà d'una col·lecció de chunks. En aquesta lliçó creuem aquesta frontera amb el mecanisme anomenat function calling (o tool use): descrivim al model quines funcions tenim, el model decideix quan i amb quins arguments convé cridar-les, i el nostre codi les executa i li retorna el resultat. És probablement la idea amb pitjor nom de tot l'ecosistema — el model no crida cap funció — i alhora una de les més transformadores: és la peça que separa un chatbot d'un assistent que opera sobre el teu sistema. Aquí construirem la primera eina de DocuBot, consultar_projecte, i tancarem per fi la pregunta d'Atlas.
Contingut
- La idea clau: el model decideix, el teu codi executa
- Definir una eina: JSON Schema i descripcions que són prompt engineering
- El flux complet amb l'SDK:
tool_use→ executar →tool_result→ resposta final - La primera eina de DocuBot:
consultar_projectecontra l'API de Nubelia - Quan l'eina falla: errors com a
tool_resultambis_error tool_choice: elecció automàtica, forçada i prohibida- Function calling com a evolució de "demanar JSON" (02-03)
La idea clau: el model decideix, el teu codi executa
Desmuntem primer el malentès que dona nom a la tècnica. En function calling el LLM no executa absolutament res: no té accés a la teva xarxa, ni a la teva base de dades, ni a l'API de Nubelia. L'única cosa que fa el model és el que sempre ha fet — generar tokens —, només que ara pot generar un tipus especial de sortida estructurada que significa "vull que cridis aquesta funció amb aquests arguments". La teva aplicació llegeix aquesta petició, decideix si l'atén, executa la funció amb codi Python normal i corrent, i li retorna el resultat al model en un segon torn perquè redacti la resposta final.
sequenceDiagram
participant U as Usuari
participant A as La teva aplicació (Python)
participant L as LLM (API)
participant N as API de Nubelia
U->>A: "Quantes tasques té el meu projecte Atlas?"
A->>L: pregunta + definicions d'eines
L-->>A: tool_use: consultar_projecte({"nom": "Atlas"})
Note over L: El model NO executa res.<br/>Només ho demana, amb arguments estructurats.
A->>N: obtenir_projecte("Atlas") — codi teu
N-->>A: {"tasques_obertes": 27, ...}
A->>L: tool_result amb aquest JSON
L-->>A: "El teu projecte Atlas té 27 tasques obertes..."
A-->>U: resposta final en llenguatge naturalAquesta divisió del treball té conseqüències importants, totes bones:
- El control és teu. Entre el
tool_usei l'execució hi ha una línia de codi Python que escrius tu. Pots validar els arguments, aplicar permisos, registrar la crida o negar-te a executar-la. El model proposa; el teu codi disposa. - La seguretat es raona com sempre. La funció que executes és codi teu amb les credencials de la teva aplicació — li apliquen els mateixos principis de tota la vida, començant pel de mínim privilegi: una eina de consulta ha de fer servir credencials de només lectura.
consultar_projecteno pot esborrar res perquè el codi que la implementa no sap esborrar res. (Les accions d'escriptura i les seves salvaguardes arriben a 05-02, i els atacs que exploten eines, a 06-01.) - El model aporta el que cap
ifno pot: entendre que "com va Atlas?", "digue'm l'estat del meu projecte Atlas" i "how many open tasks in Atlas?" són la mateixa intenció, i traduir-la a una crida amb arguments correctes.
Definir una eina: JSON Schema i descripcions que són prompt engineering
Perquè el model pugui demanar una funció, primer cal descriure-la-hi. Cada eina es defineix amb tres peces:
| Camp | Què és | Qui el "llegeix" |
|---|---|---|
name |
Identificador de la funció (lletres, números, guions baixos) | El teu codi, per saber què executar |
description |
Explicació en llenguatge natural de què fa, quan usar-la i quan no | El model — és la peça més important |
input_schema |
JSON Schema dels arguments: tipus, camps obligatoris, enums | El model (per generar arguments vàlids) i l'API (que els valida) |
La description mereix que ens hi aturem: és prompt engineering. Tot el que vam aprendre al mòdul 2 aplica aquí — el model decidirà si fa servir l'eina, i amb quins arguments, basant-se gairebé exclusivament en aquest text. Compara:
# Descripció pobra: el model haurà d'endevinar
{"description": "Consulta un projecte"}
# Descripció treballada: quan usar-la, què retorna, què NO fa
{"description": (
"Consulta l'estat EN TEMPS REAL d'un projecte de l'usuari a Nubelia: "
"tasques obertes i completades, membres de l'equip i data límit. "
"Fes-la servir quan l'usuari pregunti per dades actuals d'un projecte concret "
"('quantes tasques té Atlas?', 'com va el meu projecte Boreal?'). "
"NO la facis servir per a preguntes sobre com funciona Nubelia (plans, permisos, "
"integracions): això es respon amb la documentació."
)}La segona versió fa tres coses que la primera no fa: delimita el quan sí amb exemples (few-shot en miniatura, 02-02), delimita el quan no (evita que el model cridi l'API per a preguntes de documentació), i anuncia què retorna (el model redactarà millor la resposta final sabent què pot esperar).
El flux complet amb l'SDK: tool_use → executar → tool_result → resposta final
Vegem el mecanisme amb l'SDK anthropic que fem servir des de 03-01 — el flux és conceptualment idèntic a OpenAI, Google o Mistral (canvien els noms de camp: tools/tool_calls/function, però el ball és el mateix). Els passos:
- Primera crida: envies la pregunta i la llista d'eines (paràmetre
tools). - Si el model decideix fer-ne servir una, la resposta arriba amb
stop_reason == "tool_use"i un bloc de contingut de tipustool_useambname,input(els arguments, ja com a dict) i unidde correlació. - El teu codi executa la funció corresponent.
- Segona crida: reenvies la conversa completa — inclosa la resposta de l'assistent amb el seu bloc
tool_use— i hi afegeixes un missatge d'usuari amb un bloctool_resultque referencia aquellid. - El model, ja amb la dada a la mà, redacta la resposta final (
stop_reason == "end_turn").
Els punts 2 i 4 amaguen els dos detalls que més errors causen: el bloc tool_use de l'assistent ha de tornar a l'historial (si no, el model no sap què va demanar), i el tool_result va dins d'un missatge amb rol user — des del punt de vista de la conversa, el resultat de l'eina és una cosa que "expliques" al model, i ha de ser el primer contingut d'aquell missatge.
La primera eina de DocuBot: consultar_projecte contra l'API de Nubelia
Construïm-ho de debò. Com que en aquest curs no tenim una API de Nubelia desplegada, la simulem amb un mòdul mock — la gràcia didàctica és que al model tant li fa: ell només veu la definició de l'eina i els resultats; que al darrere hi hagi un dict o un microservei amb base de dades és invisible des del seu costat de la frontera.
# api_nubelia.py — simulació de l'API interna de Nubelia
# En producció, aquestes funcions farien crides HTTP autenticades
# (amb credencials de NOMÉS LECTURA: mínim privilegi).
class ProjecteNoTrobat(Exception):
pass
_PROJECTES = {
"atlas": {"nom": "Atlas", "estat": "actiu", "tasques_obertes": 27,
"tasques_completades": 143, "membres": 6, "data_limit": "2026-09-30"},
"boreal": {"nom": "Boreal", "estat": "actiu", "tasques_obertes": 4,
"tasques_completades": 89, "membres": 3, "data_limit": "2026-07-15"},
"cierzo": {"nom": "Cierzo", "estat": "pausat", "tasques_obertes": 12,
"tasques_completades": 51, "membres": 4, "data_limit": None},
}
def obtenir_projecte(nom: str) -> dict:
"""Retorna l'estat actual d'un projecte. Llança ProjecteNoTrobat si no existeix."""
clau = nom.strip().lower()
if clau not in _PROJECTES:
disponibles = ", ".join(p["nom"] for p in _PROJECTES.values())
raise ProjecteNoTrobat(
f"No existeix cap projecte anomenat '{nom}'. Projectes disponibles: {disponibles}."
)
return _PROJECTES[clau]Ara la definició de l'eina i el seu despatxador, en un mòdul nou que anirà creixent durant el mòdul:
# eines.py — definicions de les eines de DocuBot i la seva execució
import api_nubelia
EINES = [
{
"name": "consultar_projecte",
"description": (
"Consulta l'estat EN TEMPS REAL d'un projecte de l'usuari a Nubelia: "
"tasques obertes i completades, membres i data límit. Fes-la servir quan "
"l'usuari pregunti per dades actuals d'un projecte concret. NO la facis "
"servir per a preguntes sobre el funcionament de Nubelia (documentació)."
),
"input_schema": {
"type": "object",
"properties": {
"nom": {
"type": "string",
"description": "Nom del projecte tal com l'esmenta l'usuari, p. ex. 'Atlas'.",
},
},
"required": ["nom"],
},
},
]
def executar_eina(nom: str, arguments: dict) -> dict:
"""Pont entre el que el model demana i el codi que ho fa."""
if nom == "consultar_projecte":
return api_nubelia.obtenir_projecte(arguments["nom"])
raise ValueError(f"Eina desconeguda: {nom}")I el flux complet. Fixa't que reutilitzem cridar_amb_reintents (03-02) i MODEL (03-01) sense tocar-los — les eines són un paràmetre més de la crida. El que sí que necessitem és un system nou: el SYSTEM_DOCUBOT de 02-01 ordena basar-se exclusivament en el bloc <documentacio>, i aquesta regla, perfecta per a RAG, deixaria l'eina sense feina. Afegim a prompts.py una variant (i pugem PROMPT_VERSION, com mana 02-04):
# prompts.py (afegit a 05-01) — versió del sistema conscient de les eines SYSTEM_DOCUBOT_EINES = """Ets DocuBot, l'assistent intern de Nubelia. Només respons sobre Nubelia. Per a dades en temps real dels projectes de l'usuari, fes servir les eines disponibles. No inventis mai dades de projectes: si una eina falla o no troba alguna cosa, explica el problema amb claredat. Respon en l'idioma de l'usuari, amb to professional i concís."""
# docubot_eines.py — el flux tool_use -> executar -> tool_result
import json
from client import MODEL, extreure_text
from reintents import cridar_amb_reintents
from prompts import SYSTEM_DOCUBOT_EINES
from eines import EINES, executar_eina
def respondre_amb_eina(pregunta: str) -> str:
messages = [{"role": "user", "content": pregunta}]
# 1) Primera crida: pregunta + eines disponibles
resposta = cridar_amb_reintents(
model=MODEL,
max_tokens=1024,
system=SYSTEM_DOCUBOT_EINES,
tools=EINES,
messages=messages,
)
# El model ha respost directament, sense necessitar cap eina?
if resposta.stop_reason != "tool_use":
return extreure_text(resposta)
# 2) El model ha demanat una eina: localitzar el bloc tool_use
bloc = next(b for b in resposta.content if b.type == "tool_use")
print(f"[tool_use] {bloc.name}({bloc.input})") # traça mínima, de moment
# 3) EXECUTAR — això és Python nostre, no el model
try:
resultat = executar_eina(bloc.name, bloc.input)
contingut = json.dumps(resultat, ensure_ascii=False)
es_error = False
except Exception as e:
contingut = str(e) # l'error, explicat en text
es_error = True
# 4) Segona crida: historial complet + tool_result correlacionat per id
messages.append({"role": "assistant", "content": resposta.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": bloc.id,
"content": contingut,
"is_error": es_error,
}],
})
final = cridar_amb_reintents(
model=MODEL, max_tokens=1024,
system=SYSTEM_DOCUBOT_EINES,
tools=EINES, messages=messages,
)
return extreure_text(final)
print(respondre_amb_eina("Quantes tasques té ara mateix el meu projecte Atlas?"))Una execució típica:
[tool_use] consultar_projecte({'nom': 'Atlas'})
El teu projecte Atlas té actualment 27 tasques obertes (i 143 de completades).
El projecte està actiu, amb 6 membres i data límit el 30 de setembre de 2026.La pregunta que a 01-04 ens servia d'exemple d'allò impossible acaba de tenir resposta — i amb la dada real del sistema, no amb una al·lucinació plausible. Tres observacions abans de continuar:
bloc.inputarriba com a dict ja parsejat per l'SDK. Tot i així, tracta'l com a entrada no fiable: el model pot passar un nom de projecte que no existeix o un valor inesperat. La nostra defensa és aobtenir_projecte(normalitza i valida), no a confiar en el model.- El resultat viatja com a text (per això el
json.dumps): untool_resultés contingut per al model, i un JSON compacte i amb claus descriptives és un format que entén de meravella. No cal prosa. - Aquest flux resol un pas d'eina i acaba. Si el model necessités encadenar diverses crides (consultar la documentació i després l'API), caldria un bucle — exactament el tema de 05-02, no l'avancem.
Quan l'eina falla: errors com a tool_result amb is_error
Ja has vist el try/except al codi anterior; ara, la filosofia. Quan una eina falla, la pitjor opció és deixar que l'excepció rebenti el flux, i la segona pitjor és retornar a l'usuari un traceback. L'opció bona és la que hem implementat: retornar l'error al model com a tool_result amb is_error: True, amb un missatge d'error informatiu — i deixar que el model faci allò que sap fer millor: explicar-lo en llenguatge natural.
Pregunta: "com va el meu projecte Safir?". L'eina llança ProjecteNoTrobat amb el nostre missatge ("No existeix cap projecte anomenat 'Safir'. Projectes disponibles: Atlas, Boreal, Cierzo."), i el model respon una cosa com:
No trobo cap projecte anomenat "Safir" al teu compte de Nubelia. Els teus projectes actuals són Atlas, Boreal i Cierzo — et referies a algun d'aquests?
Fixa't en la cadena de valor: el missatge de l'excepció també és prompt engineering. Com que incloem la llista de projectes disponibles a l'error, el model pot oferir alternatives en lloc d'un "no existeix" eixut. Escriu els errors de les teves eines pensant que el seu lector és un LLM que els tornarà a explicar: què ha fallat, per què, i què s'hi pot fer. I la mateixa regla de sempre: al missatge d'error, res de secrets ni detalls interns (rutes, SQL, stack traces) — anirien directes al context del model i, potencialment, a la resposta de l'usuari.
tool_choice: elecció automàtica, forçada i prohibida
Per defecte el model decideix lliurement si fa servir una eina o respon directament. El paràmetre tool_choice et dona el comandament quan el necessites:
| Valor | Comportament | Quan usar-lo |
|---|---|---|
{"type": "auto"} |
El model decideix (per defecte amb tools) |
Assistents com DocuBot: unes preguntes necessiten eina i d'altres no |
{"type": "any"} |
Obligat a fer servir alguna eina | Routers purs: la resposta directa no és una opció vàlida |
{"type": "tool", "name": "..."} |
Obligat a fer servir aquesta eina | Extracció estructurada: vols els arguments, no una conversa |
{"type": "none"} |
Prohibit fer servir eines | Forçar una resposta de tancament amb les eines encara declarades |
El tercer cas és més útil del que sembla i connecta directament amb la secció següent: si forces una eina concreta, el model genera sempre un bloc tool_use amb arguments que compleixen el teu input_schema. És a dir: acabes d'aconseguir sortida estructurada garantida.
Function calling com a evolució de "demanar JSON" (02-03)
A 02-03 vam muntar el contracte JSON de DocuBot demanant el format al prompt i defensant-nos amb netejar_json i validar_contracte — el model de vegades embolicava el JSON amb prosa o amb tanques de codi, i calia netejar-lo. Function calling és la versió industrialitzada d'aquella idea:
| Aspecte | Demanar JSON al prompt (02-03) | Function calling |
|---|---|---|
| Com s'especifica el format | Prosa + exemple al prompt | input_schema formal (JSON Schema) |
| Fiabilitat sintàctica | Alta però no garantida (d'aquí netejar_json) |
Molt alta: el model està entrenat específicament per emetre arguments conformes a l'esquema |
| "Xerrameca" al voltant del JSON | Freqüent sense instruccions fèrries | No existeix: el bloc tool_use és estructura pura |
| Tipus i enums | Es demanen si us plau | Es declaren a l'esquema |
| Validació semàntica (valors amb sentit) | Teva (validar_contracte) |
Continua sent teva |
L'última fila és la moralitat: function calling elimina la lampisteria sintàctica, però la validació defensiva de valors no es jubila — que {"nom": "Safir"} sigui JSON perfecte no fa que Safir existeixi. L'instint de 02-03 ("no confiïs, valida") continua sent la llei; només ha canviat on aplicar-lo.
Errors Comuns i Consells
- Oblidar reenviar el torn de l'assistent. L'error més freqüent del flux: enviar el
tool_resultsense haver afegit abans{"role": "assistant", "content": resposta.content}a l'historial. L'API rebutjarà la petició — eltool_resultreferencia untool_use_idque, sense aquell missatge, no existeix a la conversa. - Descripcions d'eina esquifides. "Consulta un projecte" produeix crides de més (per a preguntes de documentació) i de menys (per a formulacions indirectes). Tracta la
descriptionamb el mateix rigor amb què vas tractarSYSTEM_DOCUBOT: quan sí, quan no, exemples. - Creure que l'esquema valida per tu. L'
input_schemagaranteix forma, no veritat. Valida els valors a la teva funció (existeix el projecte?, el rang és raonable?) com si els hagués teclejat un desconegut — perquè, en certa manera, així és. - No preveure el camí sense eina. Amb
tool_choice: auto, el model pot respondre directament (stop_reason == "end_turn"). Si el teu codi assumeix que sempre hi haurà un bloctool_use, aquellnext(...)llançaràStopIterationa la primera pregunta trivial. Comprova semprestop_reasonprimer. - Diversos
tool_useen un mateix torn. El model pot demanar més d'una eina alhora (crides paral·leles). El nostre flux d'avui en processa una perquè la seva única eina no dona per a més; el bucle de 05-02 iterarà sobre tots els blocs. Si et passa abans, respon amb untool_resultper cadatool_use_id. - Errors d'eina que no ajuden ningú.
KeyError: 'safir'com atool_resultobliga el model a improvisar. Escriu errors amb context i alternatives — i sense filtrar informació interna.
Exercicis
- Segona eina:
llistar_projectes. Afegeix aeines.pyuna eina sense arguments que retorni la llista de projectes de l'usuari (nom i estat), i ampliaexecutar_eina. Prova amb "quins projectes tinc ara mateix?" i amb "com va Atlas?" per comprovar que el model tria l'eina correcta en cada cas. - L'error com a conversa. Amb el codi de la lliçó, fes la pregunta "quantes tasques té el meu projecte Safir?" i observa la resposta. Després, empobreix a propòsit el missatge de
ProjecteNoTrobat(deixa'l en"error") i repeteix. Compara totes dues respostes finals i escriu en una frase la conclusió. - Extracció estructurada amb eina forçada. Defineix una eina
extreure_consultal'input_schemade la qual tinguicategoria(enum amb les 6 categories deprompt_classificacio()de 02-02) iprojecte(string opcional). Crida-la ambtool_choice: {"type": "tool", "name": "extreure_consulta"}sobre la pregunta "no em deixa facturar les hores del projecte Boreal" i examinabloc.input. Quina peça de 02-03 acabes de substituir?
Solucions
- Definició i despatx:
# A EINES, afegir-hi:
{
"name": "llistar_projectes",
"description": (
"Llista tots els projectes de l'usuari a Nubelia amb el seu nom i estat. "
"Fes-la servir quan l'usuari pregunti quins projectes té o demani una vista general."
),
"input_schema": {"type": "object", "properties": {}}, # sense arguments
}
# A api_nubelia.py:
def llistar_projectes() -> list[dict]:
return [{"nom": p["nom"], "estat": p["estat"]} for p in _PROJECTES.values()]
# A executar_eina:
if nom == "llistar_projectes":
return api_nubelia.llistar_projectes()Amb "quins projectes tinc?" el model ha de triar llistar_projectes; amb "com va Atlas?", consultar_projecte. Si es confon, la palanca és la description (no pas el codi!).
-
Amb l'error ric, el model respon amb alternatives concretes ("els teus projectes són Atlas, Boreal i Cierzo, et referies a algun d'aquests?"). Amb
"error"a seques, el model només pot dir que alguna cosa ha fallat, o pitjor, especular sobre el motiu. Conclusió en una frase: la qualitat de la resposta final davant d'una fallada està limitada per la qualitat del missatge d'error que retorna la teva eina — els errors de les eines són part del prompt. -
Esquema i crida:
EINA_EXTREURE = {
"name": "extreure_consulta",
"description": "Extreu la categoria i el projecte esmentat d'una consulta d'usuari.",
"input_schema": {
"type": "object",
"properties": {
"categoria": {"type": "string",
"enum": ["facturacio", "permisos", "integracions",
"api", "incidencies", "altres"]},
"projecte": {"type": "string",
"description": "Nom del projecte esmentat, si n'hi ha."},
},
"required": ["categoria"],
},
}
resposta = cridar_amb_reintents(
model=MODEL, max_tokens=256,
tools=[EINA_EXTREURE],
tool_choice={"type": "tool", "name": "extreure_consulta"},
messages=[{"role": "user", "content": "no em deixa facturar les hores del projecte Boreal"}],
)
bloc = next(b for b in resposta.content if b.type == "tool_use")
print(bloc.input) # {'categoria': 'facturacio', 'projecte': 'Boreal'}Acabes de substituir el tàndem "demanar JSON al prompt + netejar_json": amb l'eina forçada no hi ha prosa a netejar ni tanques de codi a treure. validar_contracte (la validació semàntica) continuaria tenint feina — l'enum garanteix que categoria és una de les sis, però ningú no garanteix que sigui la correcta.
Conclusió
DocuBot acaba de fer una cosa que cap RAG no podia fer: respondre amb una dada que no estava escrita enlloc quan va arrencar la conversa. El mecanisme, un cop desmuntat el nom, és honest i simple — el model decideix quina funció cridar i amb quins arguments, el teu codi executa i li retorna el resultat, el model redacta —, i les seves peces són velles conegudes amb vestit nou: la description és prompt engineering (mòdul 2), l'input_schema és el contracte JSON de 02-03 amb garanties sintàctiques de fàbrica, els errors com a tool_result amb is_error són la degradació elegant de 03-02 explicada al model, i el tool_choice forçat ens regala de passada l'extracció estructurada més robusta que coneixem. Però la nostra respondre_amb_eina() té una limitació estructural que potser ja t'ha grinyolat: executa un pas d'eina i acaba. Si l'usuari pregunta "el meu projecte Atlas supera el límit de tasques del meu pla?", calen dues consultes — la documentació per al límit del pla, l'API per a les tasques d'Atlas — i una comparació entre totes dues. Això ja no és un flux de dues crides: és un bucle en què el model raona, actua, observa el resultat i decideix si necessita continuar. Aquest bucle té nom — agent — i és exactament el que construïm a la lliçó següent, on a més el RAG del mòdul 4 patirà una transformació elegant: de pipeline a eina.
Curs d'IA Generativa i LLMs per a Desenvolupadors
Mòdul 1: Fonaments de la IA generativa
- Què és la IA generativa i per què importa als desenvolupadors
- Com funciona un LLM: tokens, embeddings i atenció
- Panorama de models i proveïdors
- Limitacions i riscos: al·lucinacions, context i costos
Mòdul 2: Prompt engineering
- Anatomia del prompt: rols, instruccions i context
- Tècniques de prompting: few-shot, cadena de raonament i plantilles
- Sortides estructurades: JSON i control de format
- Iteració i avaluació de prompts
Mòdul 3: Integració de LLMs en aplicacions
- Primera integració amb l'API d'un LLM
- Streaming, errors i reintents
- Converses i gestió del context
- Costos, latència i caching
Mòdul 4: RAG - Generació augmentada per recuperació
- Embeddings i cerca semàntica
- Bases de dades vectorials
- Ingesta i chunking de documents
- Construcció d'un pipeline RAG complet
- Avaluació de sistemes RAG
Mòdul 5: Function calling i agents
- Function calling: connectar el LLM amb el teu codi
- De LLM a agent: el bucle de raonament i acció
- Frameworks d'orquestració
