Integrar LLMs: patrones que aguantan en producción
Patrones prácticos para funciones con LLM fiables y rentables: ingeniería de prompts, streaming, salidas estructuradas, respaldos y control de costes.

Los LLMs ya son infraestructura
En 2024, las funciones de IA marcaban la diferencia. En 2026, son lo mínimo esperado. La pregunta ya no es si integrar LLMs, sino cómo hacerlo de forma fiable, mantenible y económica.
Esta guía trata sobre patrones de producción — no sobre demos.
Patrón 1: Streaming primero
Un LLM puede tardar entre 5 y 30 segundos en generar una respuesta completa. Sin streaming, el usuario se queda mirando una pantalla en blanco. Con streaming, ve el contenido aparecer de inmediato.
// app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages,
});
// Return a ReadableStream that the client can consume incrementally
const readable = new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
if (
chunk.type === "content_block_delta" &&
chunk.delta.type === "text_delta"
) {
controller.enqueue(new TextEncoder().encode(chunk.delta.text));
}
}
controller.close();
},
});
return new Response(readable, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Transfer-Encoding": "chunked",
},
});
}// Client-side — render as the stream arrives
async function streamResponse(prompt: string) {
const response = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ messages: [{ role: "user", content: prompt }] }),
});
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let result = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += decoder.decode(value, { stream: true });
setOutput(result); // Update UI incrementally
}
}Patrón 2: Salidas estructuradas para un parseo fiable
El texto libre generado por un LLM no es fiable como dato estructurado. Usa tools o function calling para obtener respuestas tipadas.
import Anthropic from "@anthropic-ai/sdk";
import { z } from "zod";
const client = new Anthropic();
const ProductExtractionSchema = z.object({
name: z.string(),
price: z.number(),
currency: z.string(),
availability: z.enum(["in_stock", "out_of_stock", "limited"]),
features: z.array(z.string()),
});
type ProductExtraction = z.infer<typeof ProductExtractionSchema>;
async function extractProductInfo(rawText: string): Promise<ProductExtraction> {
const response = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
tools: [
{
name: "extract_product",
description: "Extract structured product information from text",
input_schema: {
type: "object",
properties: {
name: { type: "string" },
price: { type: "number" },
currency: { type: "string" },
availability: {
type: "string",
enum: ["in_stock", "out_of_stock", "limited"],
},
features: { type: "array", items: { type: "string" } },
},
required: ["name", "price", "currency", "availability", "features"],
},
},
],
tool_choice: { type: "tool", name: "extract_product" },
messages: [{ role: "user", content: rawText }],
});
const toolUse = response.content.find((c) => c.type === "tool_use");
if (!toolUse || toolUse.type !== "tool_use") {
throw new Error("LLM did not use the extraction tool");
}
return ProductExtractionSchema.parse(toolUse.input);
}Esto te da salidas type-safe validadas en tiempo de ejecución — se acabó el parseo frágil de respuestas de LLM con expresiones regulares.
Patrón 3: Gestión de prompts
Los prompts son código. Trátalos como tal.
// prompts/summarize.ts — versioned, testable prompts
export const SUMMARIZE_PROMPT = {
version: "v3",
system: `You are a technical content summarizer.
Your summaries are:
- Concise: 3-5 bullet points maximum
- Technical: preserve exact technical terms
- Actionable: focus on what the reader should know or do
Never include filler phrases like "In this article..." or "To summarize..."`,
user: (content: string, maxBullets: number = 5) =>
`Summarize the following technical content in ${maxBullets} bullet points:\n\n${content}`,
};
// Use the prompt
const response = await client.messages.create({
model: "claude-sonnet-4-6",
system: SUMMARIZE_PROMPT.system,
messages: [{ role: "user", content: SUMMARIZE_PROMPT.user(articleText) }],
max_tokens: 512,
});Mantén los prompts en archivos dedicados, versiónalos y escribe pruebas contra resultados de referencia para los prompts críticos.
Patrón 4: Gestión de costos
Los costos de los LLM escalan con el uso. Las llamadas sin caché ni límites de velocidad pueden salir sorprendentemente caras a gran escala.
// 1. Cache responses for identical inputs
import { createHash } from "crypto";
async function cachedLLMCall(prompt: string, options: LLMOptions) {
const cacheKey = createHash("sha256")
.update(`${options.model}:${options.system}:${prompt}`)
.digest("hex");
const cached = await redis.get(`llm:${cacheKey}`);
if (cached) return JSON.parse(cached);
const response = await callLLM(prompt, options);
// Cache for 24 hours — same input → same output
await redis.setEx(`llm:${cacheKey}`, 86400, JSON.stringify(response));
return response;
}
// 2. Use the right model for the task
const MODEL_TIERS = {
classification: "claude-haiku-4-5-20251001", // Fast, cheap, good for routing
summarization: "claude-sonnet-4-6", // Balanced capability and cost
complex_reasoning: "claude-opus-4-6", // Use sparingly for hard problems
};
// 3. Track costs per feature
async function trackLLMCost(
feature: string,
inputTokens: number,
outputTokens: number,
model: string,
) {
const cost = calculateCost(model, inputTokens, outputTokens);
await metrics.increment("llm.cost", cost, { feature, model });
await metrics.increment("llm.tokens.input", inputTokens, { feature });
await metrics.increment("llm.tokens.output", outputTokens, { feature });
}Patrón 5: Degradación elegante
Las API de LLM tienen latencia, límites de tasa y caídas ocasionales. Tu función debe degradarse con elegancia en lugar de fallar por completo.
async function getAISuggestions(content: string): Promise<Suggestion[]> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5s timeout
try {
const suggestions = await callLLM(content, { signal: controller.signal });
clearTimeout(timeout);
return suggestions;
} catch (error) {
clearTimeout(timeout);
if (error.name === "AbortError") {
console.warn("LLM timeout — using rule-based fallback");
} else {
console.error("LLM error:", error);
}
// Fall back to deterministic, rule-based suggestions
return getRuleBasedSuggestions(content);
}
}Ten siempre un fallback. Las funciones potenciadas por LLM deben mejorar la experiencia, no convertirse en una dependencia que la rompa.
Checklist de producción
Antes de lanzar cualquier función con LLM:
- Streaming para cualquier respuesta que tarde más de 1 segundo
- Salidas estructuradas para cualquier dato que se vaya a parsear
- Validación y sanitización de entradas (la inyección de prompts es real)
- Caché de respuestas para entradas repetidas
- Seguimiento de costos por función
- Comportamiento de timeout y fallback
- Limitación de tasa por usuario/IP
- Registro de entradas y salidas (con cuidado — considera la privacidad)
- Conjunto de evaluación: 20+ casos de prueba con salidas esperadas
Los LLM son herramientas poderosas. La ingeniería que los rodea es lo que determina si tu función de IA es una ventaja competitiva o un lastre.


