Saltar a contenido

Patrones de Integracion con LLMs


Proposito

Patrones arquitectonicos concretos para integrar LLMs en aplicaciones de produccion. No es teoria sobre LLMs: es como conectarlos a tu sistema de forma confiable, segura, y eficiente.


1. Patron: Structured Output

Problema

Los LLMs retornan texto libre. Tu aplicacion necesita datos estructurados.

Solucion

Forzar al LLM a retornar JSON con un schema definido.

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  messages: [
    {
      role: 'user',
      content: `Analiza este mensaje del paciente y extrae la informacion.
        Mensaje: "Necesito un turno de cardiologia para el proximo martes en la manana"

        Responde SOLO con JSON valido en este formato:
        {
          "specialty": string,
          "preferredDate": string (YYYY-MM-DD o "next_tuesday"),
          "preferredTimeOfDay": "morning" | "afternoon" | "evening" | "any",
          "urgency": "low" | "medium" | "high"
        }`
    }
  ]
});

// Parsear y validar
const parsed = JSON.parse(response.content[0].text);
const validated = BookingIntentSchema.parse(parsed); // zod validation

Cuando usarlo

Siempre que necesites datos del LLM para procesamiento programatico.


2. Patron: Tool Use / Function Calling

Problema

El LLM necesita acceder a datos de tu sistema (BD, APIs) para responder.

Solucion

Definir "tools" que el LLM puede invocar.

const tools = [
  {
    name: 'search_doctors',
    description: 'Busca medicos por especialidad y disponibilidad',
    input_schema: {
      type: 'object',
      properties: {
        specialty: { type: 'string', description: 'Nombre de la especialidad' },
        dateFrom: { type: 'string', description: 'Fecha inicio (YYYY-MM-DD)' },
        dateTo: { type: 'string', description: 'Fecha fin (YYYY-MM-DD)' }
      },
      required: ['specialty']
    }
  },
  {
    name: 'create_booking',
    description: 'Crea una reserva de turno',
    input_schema: {
      type: 'object',
      properties: {
        doctorId: { type: 'string' },
        date: { type: 'string' },
        startTime: { type: 'string' }
      },
      required: ['doctorId', 'date', 'startTime']
    }
  }
];

// El LLM decide que tool llamar basado en el mensaje del usuario
const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-6',
  tools,
  messages: [{ role: 'user', content: 'Quiero un turno de cardiologia' }]
});

Consideraciones de seguridad

  • Valida los parametros que el LLM pasa a las tools (no confies ciegamente).
  • Usa autorizacion: la tool create_booking debe verificar que el paciente esta autenticado.
  • Limita las tools disponibles segun el contexto (un paciente no deberia tener tools de admin).

3. Patron: RAG (Retrieval-Augmented Generation)

Problema

El LLM no conoce los datos especificos de tu sistema.

Solucion

Buscar informacion relevante y pasarla como contexto.

Paso 1: El usuario pregunta algo.
Paso 2: Buscas en tu BD/documentos la informacion relevante.
Paso 3: Pasas la pregunta + la informacion al LLM.
Paso 4: El LLM genera una respuesta basada en tus datos.

Implementacion simple (sin embeddings)

async function answerQuestion(question: string, patientId: string) {
  // Buscar contexto relevante
  const upcomingBookings = await bookingRepo.findByPatient(patientId, {
    status: 'CONFIRMED',
    dateFrom: new Date()
  });

  const context = upcomingBookings.map(b =>
    `Turno: ${b.date} ${b.startTime} con ${b.doctor.name} (${b.specialty.name}) en ${b.clinic.name}`
  ).join('\n');

  const response = await anthropic.messages.create({
    model: 'claude-sonnet-4-6',
    messages: [{
      role: 'user',
      content: `Eres un asistente del sistema de turnos medicos.
        Responde la pregunta del paciente usando SOLO la informacion proporcionada.

        Turnos del paciente:
        ${context}

        Pregunta: ${question}`
    }]
  });

  return response.content[0].text;
}

Implementacion con embeddings (para datos grandes)

1. Indexar documentos/datos en vector DB (Pinecone, pgvector).
2. Convertir la pregunta en embedding.
3. Buscar los documentos mas similares (cosine similarity).
4. Pasar los documentos relevantes como contexto al LLM.

4. Patron: Guardrails

Problema

El LLM puede generar respuestas incorrectas, inseguras, o fuera de scope.

Solucion

Capas de validacion antes y despues del LLM.

Input -> [Input Guardrail] -> LLM -> [Output Guardrail] -> Response
              |                              |
         - Sanitizar                    - Validar formato
         - Detectar injection           - Verificar facts
         - Verificar permisos           - Filtrar PII
                                        - Verificar seguridad
// Input guardrail
function sanitizeInput(userMessage: string): string {
  // Detectar intentos de prompt injection
  if (containsInjectionAttempt(userMessage)) {
    throw new AppError('INVALID_INPUT', 400);
  }
  // Limitar largo
  return userMessage.slice(0, 2000);
}

// Output guardrail
function validateOutput(llmResponse: string, context: any): string {
  // Verificar que no expone datos sensibles
  if (containsPII(llmResponse, context.allowedData)) {
    return 'No puedo compartir esa informacion. Contacta a la clinica directamente.';
  }
  // Verificar que no inventa datos
  if (!factCheck(llmResponse, context.knownFacts)) {
    return 'No tengo informacion suficiente para responder. Contacta a la clinica.';
  }
  return llmResponse;
}

5. Patron: Caching de Respuestas LLM

Problema

Las llamadas a LLMs son lentas (1-10s) y caras.

Solucion

Cache para queries repetitivas o similares.

Estrategia Cuando Ejemplo
Cache exacto Mismo input = mismo output "Que especialidades tienen?" (siempre igual)
Cache semantico Inputs similares = mismo output "horarios de cardiologia" ~ "disponibilidad cardiologo"
Pre-computar Respuestas predecibles FAQs, descripciones de especialidades

6. Patron: Streaming

Problema

La respuesta del LLM tarda varios segundos. El usuario ve una pantalla en blanco.

Solucion

Streaming token por token al frontend.

// Backend (NestJS)
@Get('chat')
async chat(@Query('message') message: string, @Res() res: Response) {
  res.setHeader('Content-Type', 'text/event-stream');

  const stream = await anthropic.messages.stream({
    model: 'claude-sonnet-4-6',
    messages: [{ role: 'user', content: message }]
  });

  for await (const event of stream) {
    if (event.type === 'content_block_delta') {
      res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
    }
  }

  res.end();
}

7. Resumen de Patrones

Patron Problema que resuelve Complejidad
Structured Output LLM retorna texto, necesito JSON Baja
Tool Use LLM necesita datos de mi sistema Media
RAG LLM no conoce mis datos Media-Alta
Guardrails LLM puede dar respuestas incorrectas/inseguras Media
Caching LLM es lento y caro Baja-Media
Streaming El usuario espera mucho Baja

Checklist de Completitud

  • 6 patrones documentados con codigo
  • Consideraciones de seguridad
  • Ejemplos del dominio de turnos medicos
  • Complejidad relativa por patron
  • Revisada por otro ingeniero

Archivos relacionados