Com construir un agent d'IA amb LangChain.js i NestJS
3 de juliol del 2026 · SOM-OS
Hi ha dues maneres de construir un agent d’IA per a empreses amb Node.js. La primera: aferrar l’API d’OpenAI en un controlador Express, sense memòria cau, sense cues, sense eines. Funciona en una demo i explota en producció el dia que arriben 200 peticions concurrents. La segona: arquitectura real amb LangChain.js sobre NestJS, cues amb BullMQ, memòria amb Redis i eines tipades. Aquest tutorial cobreix la segona.
Si no tens clar per què la IA necessita arquitectura abans que codi, llegeix IA sense context és un lloro car i torna. Aquí donam per fet que ja saps per què.
Arquitectura de l’agent
El flux és aquest: l’usuari envia un missatge a l’endpoint de NestJS. NestJS posa la feina a la cua de BullMQ (Redis). Un worker processa la cua, crida l’agent LangChain amb les eines disponibles, desa la conversa a PostgreSQL i torna la resposta a través de WebSocket o consultes periòdiques. El model LLM no manté estat — el context viu a la teva base de dades, no a la memòria del model.
Client (Nuxt/React)
│ POST /agents/:id/messages
▼
┌──────────────────────┐
│ Controlador NestJS │ validació, auth, rate-limit
└──────────┬───────────┘
│ enqueue(job)
▼
┌──────────────────────┐
│ Cua BullMQ (Redis) │ cues, reintents, prioritat
└──────────┬───────────┘
│ process(job)
▼
┌──────────────────────┐
│ Worker de l'agent │ agent LangChain + eines
│ - historial (PG) │
│ - eines (DB, API) │
│ - model (OpenAI/etc)│
└──────────┬───────────┘
│ ws.emit / db.save
▼
El client rep la resposta
Cada peça té una raó. La cua evita que 200 usuaris simultanis facin caure el servei. El worker separa la latència de l’LLM de la petició HTTP. La persistència de l’historial a PostgreSQL permet auditar cada conversa i reprendre contextos.
Pas 1: instal·lar dependències
npm i @nestjs/common @nestjs/core @nestjs/platform-fastify \
@langchain/core @langchain/openai @langchain/community \
langchain bullmq ioredis @prisma/client
Feim servir l’adaptador de Fastify a NestJS per rendiment. Si vens d’Express, la migració és trivial i ja hem explicat el perquè a NestJS vs Express.
Pas 2: el mòdul de l’agent
El mòdul agrupa el servei de l’agent, la cua i el worker. Registr el worker com a provider perquè el cicle de vida de NestJS el gestioni:
import { Module } from '@nestjs/common';
import { AgentService } from './agent.service';
import { AgentQueue } from './agent.queue';
import { AgentWorker } from './agent.worker';
@Module({
providers: [AgentService, AgentQueue, AgentWorker],
exports: [AgentService],
})
export class AgentModule {}
Pas 3: el controlador
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { AgentService } from './agent.service';
import { JwtGuard } from '../auth/jwt.guard';
class SendMessageDto {
conversationId: string;
content: string;
}
@Controller('agents')
@UseGuards(JwtGuard)
export class AgentsController {
constructor(private readonly agent: AgentService) {}
@Post('messages')
async send(@Body() dto: SendMessageDto) {
const jobId = await this.agent.enqueue(dto);
return { jobId, status: 'queued' };
}
}
El controlador no crida l’LLM. Només posa la feina a la cua. La resposta arriba de manera asíncrona. Aquesta és la diferència entre un agent de demo i un de producció: l’usuari no espera 30 segons amb la petició oberta.
Pas 4: la cua amb BullMQ
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Queue } from 'bullmq';
import IORedis from 'ioredis';
@Injectable()
export class AgentQueue implements OnModuleInit {
private queue: Queue;
onModuleInit() {
const connection = new IORedis(process.env.REDIS_URL, {
maxRetriesPerRequest: null,
});
this.queue = new Queue('agent-jobs', { connection });
}
async add(data: SendMessageDto) {
return this.queue.add('run-agent', data, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
}
}
Tres reintents amb espera exponencial. Si l’LLM falla pel límit de peticions, BullMQ ho reintenta sense perdre el missatge. Sense cua, un temps d’espera esgotat del proveïdor et costa una conversa sencera.
Pas 5: l’agent amb LangChain
Ara el worker. Aquí viu la lògica: recupera l’historial de la conversa, configura les eines, executa l’agent i desa el resultat.
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { Worker, Job } from 'bullmq';
import { ChatOpenAI } from '@langchain/openai';
import { AgentExecutor, createToolCallingAgent } from 'langchain/agents';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { searchCatalogTool } from './tools/search-catalog.tool';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class AgentWorker implements OnModuleInit, OnModuleDestroy {
private worker: Worker;
constructor(private readonly prisma: PrismaService) {}
onModuleInit() {
this.worker = new Worker(
'agent-jobs',
async (job: Job) => this.handle(job),
{ connection: { url: process.env.REDIS_URL } },
);
}
onModuleDestroy() { return this.worker.close(); }
private async handle(job: Job) {
const { conversationId, content } = job.data;
// 1. Historial des de PostgreSQL
const history = await this.prisma.message.findMany({
where: { conversationId },
orderBy: { createdAt: 'asc' },
take: 20,
});
const model = new ChatOpenAI({
model: 'gpt-4o-mini',
temperature: 0.2,
});
// 2. Eines amb accés al teu sistema
const tools = [searchCatalogTool(this.prisma)];
// 3. Instruccions de marca
const prompt = ChatPromptTemplate.fromMessages([
['system',
'Ets un assistent de vendes de SOM-OS. ' +
'Respon en català de Mallorca, en segona persona. ' +
'Fes servir les eines per cercar al catàleg abans de respondre. ' +
'Si no ho saps, digues que no ho saps. No inventis.'],
new MessagesPlaceholder('history'),
['human', '{input}'],
new MessagesPlaceholder('agent_scratchpad'),
]);
const agent = await createToolCallingAgent({
llm: model,
tools,
prompt,
});
const executor = new AgentExecutor({ agent, tools });
// 4. Executar
const result = await executor.invoke({
input: content,
history: history.map((m) => [m.role, m.content]),
});
// 5. Persistir
await this.prisma.message.createMany({
data: [
{ conversationId, role: 'human', content },
{ conversationId, role: 'ai', content: result.output },
],
});
return { conversationId, output: result.output };
}
}
Cinc passos: historial, model, eines, instruccions, persistència. L’agent té context dels darrers 20 missatges, eines per accedir al catàleg real, instruccions de marca i desa cada intercanvi per auditar-lo.
Pas 6: definir una eina
Una eina és una funció tipada que l’agent decideix quan fer servir. Aquí en tens una que cerca productes al catàleg:
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
export const searchCatalogTool = (prisma) =>
tool(
async ({ query }) => {
const products = await prisma.product.findMany({
where: { name: { contains: query, mode: 'insensitive' } },
take: 5,
});
return JSON.stringify(products);
},
{
name: 'search_catalog',
description:
'Cerca productes al catàleg pel nom. ' +
'Torna fins a 5 resultats amb id, nom, preu i estoc.',
schema: z.object({
query: z.string().describe('terme de cerca'),
}),
},
);
L’esquema amb Zod diu al model quins arguments accepta l’eina. LangChain el converteix automàticament a crides a funcions. L’agent decideix si la crida segons el context de l’usuari.
Pas 7: desplegament
El desplegament mínim en producció: NestJS en un contenidor, Redis en un altre, PostgreSQL gestionat (Supabase, Neon o RDS). Variables d’entorn: REDIS_URL, DATABASE_URL, OPENAI_API_KEY. Un Dockerfile base:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/main.js"]
Desplega a Fly.io, Railway o Coolify sobre el teu VPS. Allò que importa: Redis i el worker viuen al mateix contenidor o separats, però la cua és el punt de sincronització. No posis el worker en serverless — les feines llargues no sobreviuen als inicis en fred.
Allò que no et diu el tutorial ràpid
Tres coses que aprenguérem en producció i que cap “construeix un agent en 10 minuts” esmenta:
- Límits de peticions de l’LLM. OpenAI et talla a 500 RPM al nivell 1. Si el teu agent fa servir 2 eines per missatge, una sola conversa consumeix 3 peticions. Sense cua i sense espera entre reintents, caus el dia del llançament.
- Finestra de context. 20 missatges ocupen ~4.000 tokens. Amb 50, superes el límit del model barat. Necessites un resum automàtic o un truncament intel·ligent de l’historial — no carregar-ho tot cada vegada.
- Cost. Un agent amb gpt-4o-mini i 2 eines per missatge costa ~0,002€ per interacció. Sembla poc fins que tens 1.000 usuaris amb 10 converses diàries. Són 20€/dia només en tokens. Mesura abans d’escalar.
Un agent d’IA en producció no és un endpoint. És un sistema: cua, persistència, eines, límit de peticions i cost controlat. Sense això, tens un lloro car que cau el dia del llançament.
Per què NestJS i no Express aquí
Podries fer això amb Express. Però un agent amb eines, cues, websockets i persistència necessita mòduls, injecció de dependències i estructura. Express et deixa improvisar — NestJS t’obliga a organitzar. La comparativa completa és a NestJS vs Express, però el resum és: si la teva IA creixerà més enllà d’una demo, NestJS estalvia refactorització.
Si vols veure com queda un agent amb context real en producció, mira el cas de Sando Capital: un agent que qualifica clients potencials financers amb el catàleg carregat i regles d’encaix. I si el teu negoci encara no té les dades connectades que un agent necessita, comença pel Mapa del Caos abans que per LangChain.