Changes for src/apren/llms/llm_patterns.md: 12 added lines, 2 removed lines.
Original line number
Diff line number
Diff line
@@ -608,14 +608,24 @@ def stock(sku: str) -> int:
defpolitica_devolucions()->str:
"""Text vigent de la política de devolucions."""
returnopen("docs/devolucions.md").read()
if__name__=="__main__":
mcp.run()# transport stdio per defecte
```
Durant el desenvolupament, el servidor s'aixeca amb la CLI del SDK, que l'obre en un inspector on pots cridar les eines a mà abans de connectar-hi cap agent:
Amb transport `stdio`, qui llança el servidor és el client, com a subprocés, i li parla per l'entrada i la sortida estàndard. La conseqüència pràctica sorprèn el primer dia: **dins d'un servidor stdio no hi pot haver cap `print()`**, perquè stdout és el canal del protocol. Els missatges de registre van a stderr.
Per provar les eines a mà abans de connectar-hi cap agent, l'ecosistema té un inspector web. Aquesta és l'ordre que funciona, des de la carpeta del servidor i amb l'entorn actiu:
> ⚠️ **No facis servir `mcp dev server.py`**: llança el servidor en un entorn aïllat que només conté el paquet `mcp`, sense les dependències del teu projecte, i es queda a mig connectar. El mode web, sense `--cli`, es rendeix als 15 segons, que en màquines lentes és menys del que triga el servidor a arrencar.
Fixa't que l'inspector no és imprescindible: `tools/list` és una crida del protocol com qualsevol altra, i un script de vint línies amb el client del SDK et dona la mateixa informació sense dependre de Node.
Decisions clau de disseny:
-**Granularitat de les eines**: una eina per operació de negoci, no una per endpoint HTTP. El model raona millor amb `stock(sku)` que amb `http_get(url)`.
Changes for src/apren/llms/llm_systems.md: 25 added lines, 8 removed lines.
Original line number
Diff line number
Diff line
@@ -257,6 +257,8 @@ El format de *chat completions* és el punt d'interoperabilitat entre proveïdor
### Abstraccions multi-proveïdor i SDKs
> 📝 Els dos exemples d'aquesta secció, LangChain i LiteLLM, són per **comparar formes**, no per executar-los: cap de les dues llibreries no forma part de l'entorn de treball d'aquests materials, que treballa directament amb l'SDK d'OpenAI contra un servidor propi. El que has de saber llegir és què canvia entre una abstracció i l'altra, i quin preu té cadascuna.
Per a codi multi-proveïdor, la solució habitual és el **patró estratègia** que ofereix LangChain: defineix tipus de missatge propis (`SystemMessage`, `HumanMessage`, `AIMessage`) i cada integració de proveïdor els tradueix al format natiu. La lògica de l'aplicació és idèntica entre proveïdors; el que canvia és la instanciació:
```python
@@ -366,8 +368,11 @@ class AnàlisiSentiment(BaseModel):
Quan `instructor` no és accessible o el model no admet el seu mecanisme (p.ex. un model local sense `response_format`), les alternatives per ordre de fiabilitat:
**`response_format` directe**: si el model implementa `response_format={"type": "json_schema", ...}` però no uses `instructor`, pots cridar l'API directament i validar amb Pydantic. És l'opció menys invasiva.
**`response_format` directe**: si el model implementa `response_format={"type": "json_schema", ...}` però no uses `instructor`, pots cridar l'API directament i validar amb Pydantic. És l'opció menys invasiva, i la que fa que la restricció d'esquema i la validació quedin totes dues a la vista.
```python
ESQUEMA={
"name":"sentiment",
"schema":AnàlisiSentiment.model_json_schema(),
"strict":True,# decodificació restringida: el servidor no pot generar res fora de l'esquema
Amb `"strict": True` cal tenir present que *tots* els camps han d'aparèixer a `required`: aquí "requerit" vol dir que la clau ha d'existir, no que hagi de tenir valor, i l'opcionalitat real s'expressa amb `"type": ["string", "null"]`. Si el servidor d'inferència no accepta aquesta forma, la sortida de recanvi és treure `strict` i `additionalProperties` i confiar en la validació Pydantic: la garantia passa de "el servidor no pot generar-ho malament" a "ho detectem a la frontera", que és una xarxa de seguretat que en tot cas has de tenir.
**Function calling com a esquema**: definir una eina fictícia amb l'esquema desitjat i forçar el model a cridar-la. Quan el model respecta `tool_choice`, els arguments sempre són JSON vàlid; alguns LLMs locals l'ignoren.
```python
@@ -572,7 +584,10 @@ La resposta degradada ha de ser semànticament vàlida per al cas d'ús concret,
Quan el LLM és opcional (enriquiment, classificació no bloquejant), la indisponibilitat pot ser transparent per a l'usuari. Quan és el camí crític, no ho pot ser.
wait=wait_exponential_jitter(initial=2,max=120),# jitter: evita que tots reintentin alhora
)
defcridar_model_async(messages):...
```
El *jitter* del cas asíncron no és un detall: si el servei cau i tots els workers reintenten amb el mateix backoff exponencial, tornen a picar a la porta tots junts i el tomben una altra vegada just quan es recuperava. Afegir soroll aleatori a l'espera reparteix els reintents en el temps.
**Errors que no s'han de reintentar**: 400 (prompt invàlid), 401 (credencials), 404 (model no trobat). El resultat no canviarà.
LiteLLM Router implementa fallback de proveïdor natiu: si el model principal retorna errors persistents, enruta automàticament a un model de backup sense canvis al codi de l'aplicació.
Les dues peces van juntes i cap no substitueix l'altra: `response_format` restringeix el que el servidor pot generar, i `model_validate_json` comprova a la frontera que el que ha arribat és el que esperaves. Vegeu [Sortida estructurada i validació](llm_systems.md#sortida-estructurada-i-validació) per a les alternatives quan el servidor no admet aquesta forma.
Aquest esquelet cobreix el que fa falta per a volums de milers d'ítems: concurrència limitada, idempotència i errors aïllats per ítem. Quan el volum creix fins al punt que el procés ha de sobreviure a reinicis, repartir-se entre màquines o reprendre's al mig, el següent pas és una cua de tasques de veritat (Celery o RQ amb Redis), que és el mateix disseny amb l'estat fora del procés. I si treballes contra una API comercial, comprova abans si té una **Batch API**: sol costar la meitat per token i t'estalvia tota aquesta maquinària.
---
## 7. Assistent conversacional amb eines
@@ -924,15 +953,15 @@ Node disparador
### Entrades i sortides
**Entrada:** text pla (petició) + estat inicial del graf (`TypedDict`, el JSON intern de LangGraph que cada node pot llegir i modificar). L'estat és el mecanisme de comunicació entre nodes: cada node llegeix el que necessita i escriu el que produeix.
**Entrada:** text pla (petició) i, si el flux es modela com un graf d'estats, l'estat inicial (un `TypedDict` que cada node pot llegir i modificar). L'estat és el mecanisme de comunicació entre nodes: cada node llegeix el que necessita i escriu el que produeix.
**Sortida:** text pla o markdown (resultat del node final) + historial d'execució del graf en JSON (seqüència de nodes executats, estat en cada pas) per a auditoria. Si s'activa el checkpointing, l'estat persisteix a la BD entre execucions.
### Decisions de disseny clau
**Estat compartit com a contracte**: l'estat del graf (`TypedDict`) és la interfície entre nodes. Cada node llegeix el que necessita i escriu el que produeix. Dissenyar bé l'estat evita que els nodes s'acoplin entre si.
**Estat compartit com a contracte**: quan hi ha graf, el seu estat (`TypedDict`) és la interfície entre nodes. Cada node llegeix el que necessita i escriu el que produeix. Dissenyar bé l'estat evita que els nodes s'acoplin entre si.
**Per què no un `if/else` en el backend**: un dispatcher amb `if/else`pot fer routing senzill, però no pot gestionar paral·lelisme, checkpointing d'estat, ni fluxos amb bucles condicionals (revisor que torna al redactor si la qualitat no és suficient). LangGraph expressa aquests patrons de forma declarativa.
**Per què no un `if/else` en el backend**: sovint sí, i l'exemple d'aquesta secció ho és. Un dispatcher amb `if/else`fa routing senzill perfectament; el que no fa és paral·lelisme amb síntesi, checkpointing d'estat ni bucles condicionals (un revisor que torna la feina al redactor si la qualitat no és suficient). Un graf d'estats expressa aquests tres patrons de forma declarativa, i és quan n'has de menester algun que el framework es paga.
**Agents especialitzats, no eines especialitzades**: la raó per tenir múltiples agents és que cada especialista necessita un context i instruccions radicalment diferents. Un agent de facturació ha de tenir accés a la BD de factures i conèixer la política de reemborsaments; un agent tècnic necessita documentació tècnica i eines de diagnosi. Barrejar-ho en un sol agent amb moltes eines degrada la qualitat i complica el control.
@@ -949,92 +978,58 @@ Node disparador
### Exemple
```python
fromlanggraph.graphimportStateGraph,END
fromtypingimportTypedDict,Literal
El nucli del patró no és cap llibreria: és un classificador que tria, i un especialista per branca amb el seu propi system prompt i les seves pròpies eines.
```python
# Exemple: routing de tickets de suport a agents especialitzats
classEstatTicket(TypedDict):
missatge:str
categoria:str
resposta:str
defsupervisor(estat:EstatTicket)->dict:
ESPECIALISTES={
"facturació":"Ets un especialista en facturació. Tens accés a la política de "
"reemborsaments i pots consultar l'historial de pagaments.",
"tècnic":"Ets un especialista tècnic. Diagnostica el problema i proposa una "
"solució pas a pas.",
"comercial":"Ets un agent comercial. Informa sobre plans, preus i condicions "
"contractuals.",
}
defclassificar(missatge:str)->str:
categoria=client.chat.completions.create(
model=MODEL,
messages=[
{"role":"system","content":
"Classifica el ticket de suport. Respon únicament amb una paraula: "
"'facturació', 'tècnic' o 'comercial'."},
{"role":"user","content":estat["missatge"]},
{"role":"user","content":missatge},
],
temperature=0,
).choices[0].message.content.strip()
return{"categoria":categoria}
returncategoriaifcategoriainESPECIALISTESelse"tècnic"# ruta per defecte
defagent_facturació(estat:EstatTicket)->dict:
resposta=client.chat.completions.create(
defatendre(missatge:str)->str:
categoria=classificar(missatge)
returnclient.chat.completions.create(
model=MODEL,
messages=[
{"role":"system","content":
"Ets un especialista en facturació. Tens accés a la política de reemborsaments "
"i pots consultar l'historial de pagaments. Resol el dubte del client."},
Cada especialista podria ser al seu torn un [bucle d'agent](#5-agent-amb-eines) amb el seu catàleg d'eines, i el `dict` seguiria sent el mateix. Fixa't que el supervisor no comparteix el seu system prompt amb els especialistes: entre agents només hi viatja el resultat.
app=graf.compile()
**Què hi afegeix LangGraph.** Mentre el flux sigui "classifica i despatxa", el codi de sobre ja és la solució i un framework només hi posa vocabulari. El que compres quan te'n vas a un graf d'estats és el que aquest `if` implícit no sap fer: arestes condicionals que **tornen enrere** (un revisor que retorna la feina al redactor mentre no arribi al llistó), execució en **paral·lel** de branques independents amb un node de síntesi al final, i **checkpointing** de l'estat perquè una execució llarga es pugui aturar, esperar una aprovació humana i reprendre's des d'on era.
# ús
resultat=app.invoke({
"missatge":"No he rebut la factura del mes passat.",
"categoria":"",
"resposta":"",
})
print(resultat["resposta"])
```python
# Fragment il·lustratiu: LangGraph no forma part de l'entorn de treball