Ir al contenido

Stack · Anthropic Claude

La AI pasa a formar parte del producto

Prompt caching que reduce los costes en un orden de magnitud, tool use que convierte a Claude en un operador, servidores MCP que conectan el modelo a tu stack y la traza de auditoría que te deja pasar la compliance.

Por qué este stack

El prompt caching es la palanca que la mayoría de los equipos se pierde

Un system prompt estable y un documento de referencia largo entran en cache después de la primera petición. Las siguientes pagan alrededor del diez por ciento del coste de tokens de input. Diseñamos la estructura del prompt para que el caching aplique en cada llamada relevante, y eso mueve una integración Claude de "funcionalidad cara" a "siempre activa".

El tool use convierte a Claude en un miembro del equipo

Un tool es una función tipada que el modelo puede llamar. Definimos los tools como schemas TypeScript, el modelo elige uno, nuestro handler lo ejecuta y el resultado vuelve a la conversación. El patrón funciona para leer la base de datos, hacer una búsqueda, llamar a una operación de Stripe o invocar otro agente.

Los servidores MCP conectan el modelo a tu stack

Model Context Protocol es cómo un agente le habla a tu código aplicativo sin scaffolding REST. Hemos enviado servidores MCP para Design System Ops, Dev Ops y Social Ops; el mismo patrón vale para tus herramientas internas. El modelo usa tu código, tu código no parsea el modelo.

Streaming, pero hecho bien

Streaming basado en SSE con manejo correcto de backpressure, renderizado parcial de mensajes, error boundaries que no colapsan la UI cuando el modelo falla a mitad de stream, y parsing estructurado de eventos para que una llamada a tool dentro de un stream no sea un hack de parsing.

Cost accounting y governance desde el primer día

Cada petición a Claude queda loggeada con modelo, tokens de input, tokens de output, cache hit ratio y coste. Rate limits por tenant, presupuestos por usuario, y un dashboard financiero que no exige un paso de export a hoja de cálculo.

Qué construimos con esta tecnología

Integración tipada Anthropic SDK

Client TypeScript con retries, backoff consciente del rate-limit, manejo estructurado de errores, selección de modelo por tarea (Opus / Sonnet / Haiku).

Arquitectura de prompt caching

Breakpoints de cache colocados sobre las partes estables (system prompt, docs de referencia, ejemplos largos), cache hit ratio medible en el dashboard.

Framework de tool use

Tools definidos como schemas TypeScript con validación Zod. La cadena de handlers es transaccional respecto a la base de datos, así una escritura desencadenada por el modelo hace rollback ante una validación fallida.

Servidor MCP para tu stack

Un servidor Model Context Protocol que expone las capacidades de tu aplicación a Claude (leer DB, escribir, llamar APIs, disparar workflows). Versionado, gobernado, con audit log.

UI de streaming

Transporte Server-Sent Events, renderizado parcial de mensajes, renderizado inline de tool calls, error boundaries recuperables.

Orquestación de agentes

Agentes multi-step que planifican, ejecutan tools, observan resultados y deciden qué hacer luego. Bucles con número máximo de pasos y condiciones de salida explícitas.

Persistencia de conversaciones

Store de conversaciones en base de datos con sumarización para threads largos, gestión de la context window consciente del presupuesto de tokens.

Token accounting + reportes de coste

Logging por petición de modelo, tokens, coste, cache hit ratio. Agregados por tenant, presupuestos por usuario, reporting mensual conectado al dashboard de administración.

Mitigación de prompt injection

Aislamiento del system prompt, fencing de input no confiable, validación del output contra schema esperado, manejo del refusal ante instrucciones adversariales.

Pipeline de validación de output

Outputs estructurados validados contra schemas Zod, re-prompt en parse fallido, dead-letter logging para los casos irrecuperables.

Política de selección de modelo

Haiku para tareas de alto volumen y bajo coste, Sonnet para el nivel estándar, Opus para reasoning difícil. La política vive en el código, bajo source control.

Estrategia de fallback

Degradación graceful cuando Anthropic no está disponible, retry con backoff, fallback cross-provider opcional donde el caso de uso lo permite.

Una llamada tipada a Claude con prompt caching, tool use y streaming

Una función hace todo el flujo. El system prompt y un documento de referencia entran en cache en la primera petición. El modelo puede llamar a un tool `lookupCustomer` cuyo resultado vuelve a la conversación. La respuesta va en stream al client por SSE.

La mayoría de las "integraciones Claude" conectan un input de chatbot a la API y se quedan ahí. La AI en producción tiene otra cara. La función de abajo es la que enviamos: una llamada tipada a messages.create, system prompt y documento de referencia en cache, un tool lookupCustomer que el modelo puede llamar, la respuesta que vuelve al client por Server-Sent Events.

1. El client tipado

El Anthropic SDK queda envuelto en un client fino que añade retries, mapeo de errores y una union tipada de modelo. La selección de modelo ocurre en el call site; el client no elige.

// src/lib/claude/client.ts
import Anthropic from '@anthropic-ai/sdk'

export const claude = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  maxRetries: 3,
})

export type ClaudeModel =
  | 'claude-opus-4-7'
  | 'claude-sonnet-4-6'
  | 'claude-haiku-4-5'

export const MODEL_BY_TASK: Record<'reasoning' | 'standard' | 'cheap', ClaudeModel> = {
  reasoning: 'claude-opus-4-7',
  standard: 'claude-sonnet-4-6',
  cheap: 'claude-haiku-4-5',
}

2. La definición del tool

Un tool es una función tipada con un JSON schema. Zod produce el schema; el handler corre en el flujo transaccional normal de la aplicación. El modelo nunca toca la base de datos directamente.

// src/lib/claude/tools/lookup-customer.ts
import { z } from 'zod'
import { zodToJsonSchema } from 'zod-to-json-schema'
import { adminClient } from '@/lib/supabase/admin'

export const lookupCustomerInput = z.object({
  email: z.string().email(),
})

export const lookupCustomerTool = {
  name: 'lookupCustomer',
  description:
    'Find a customer by email. Returns the canonical record or null.',
  input_schema: zodToJsonSchema(lookupCustomerInput, {
    target: 'jsonSchema7',
  }) as Record<string, unknown>,
} as const

export async function runLookupCustomer(
  input: z.infer<typeof lookupCustomerInput>,
): Promise<unknown> {
  const { data, error } = await adminClient
    .from('customers')
    .select('id, name, plan, status, created_at')
    .eq('email', input.email)
    .maybeSingle()

  if (error) throw error
  return data ?? null
}

3. La llamada en streaming con prompt caching

El system prompt y el documento de referencia llevan cache_control. Después de la primera petición, las siguientes pagan la tarifa cache-read (alrededor del diez por ciento del precio estándar de input) sobre esos bloques. Los mensajes de la conversación se quedan sin cache porque cambian en cada turno.

// src/lib/claude/run.ts
import { claude, MODEL_BY_TASK } from './client'
import { lookupCustomerTool, runLookupCustomer, lookupCustomerInput } from './tools/lookup-customer'
import { systemPrompt } from './prompts/system'
import { referenceDoc } from './prompts/reference'

interface RunInput {
  conversation: { role: 'user' | 'assistant'; content: string }[]
  tenantId: string
}

export async function* runClaude(input: RunInput) {
  const stream = await claude.messages.create({
    model: MODEL_BY_TASK.standard,
    max_tokens: 4096,
    system: [
      {
        type: 'text',
        text: systemPrompt,
        cache_control: { type: 'ephemeral' },
      },
      {
        type: 'text',
        text: referenceDoc,
        cache_control: { type: 'ephemeral' },
      },
    ],
    messages: input.conversation,
    tools: [lookupCustomerTool],
    stream: true,
  })

  for await (const chunk of stream) {
    yield chunk

    // Cuando el modelo decide llamar a un tool, el stream emite un
    // content_block_stop con el payload tool_use. Ejecutamos el tool y
    // continuamos la conversación con el resultado.
    if (
      chunk.type === 'content_block_stop' &&
      'content_block' in chunk &&
      chunk.content_block?.type === 'tool_use' &&
      chunk.content_block.name === 'lookupCustomer'
    ) {
      const parsed = lookupCustomerInput.parse(chunk.content_block.input)
      const result = await runLookupCustomer(parsed)
      yield {
        type: 'tool_result' as const,
        tool_use_id: chunk.content_block.id,
        content: JSON.stringify(result),
      }
    }
  }
}

4. La route Server-Sent Events

El generator del streaming pasa por un transporte Server-Sent Events para que el navegador reciba los tokens parciales según llegan. El route handler también escribe una fila de log por llamada, así que cost accounting y audit no son opcionales.

// app/api/chat/route.ts
import { runClaude } from '@/lib/claude/run'
import { logClaudeRequest } from '@/lib/claude/logging'
import { getServerSession } from '@/lib/auth/server'

export const runtime = 'nodejs'

export async function POST(request: Request) {
  const session = await getServerSession()
  if (!session) return new Response('unauthorised', { status: 401 })

  const { conversation } = await request.json()
  const start = Date.now()

  const stream = new ReadableStream({
    async start(controller) {
      let inputTokens = 0
      let outputTokens = 0
      let cacheHits = 0

      try {
        for await (const chunk of runClaude({
          conversation,
          tenantId: session.tenantId,
        })) {
          if (chunk.type === 'message_start') {
            inputTokens = chunk.message.usage.input_tokens
            cacheHits = chunk.message.usage.cache_read_input_tokens ?? 0
          }
          if (chunk.type === 'message_delta') {
            outputTokens = chunk.usage.output_tokens
          }
          controller.enqueue(
            new TextEncoder().encode(`data: ${JSON.stringify(chunk)}\n\n`),
          )
        }
      } finally {
        controller.close()
        await logClaudeRequest({
          tenantId: session.tenantId,
          userId: session.userId,
          model: 'claude-sonnet-4-6',
          inputTokens,
          outputTokens,
          cacheHits,
          durationMs: Date.now() - start,
        })
      }
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
}

5. Qué te compra esto

El prompt caching convierte una llamada de cinco céntimos en una de medio céntimo. El tool use convierte al modelo de una superficie de chat en un operador que lee tu base de datos, llama a tus APIs, dispara tus workflows. El audit log le da al equipo de finanzas un número con el que planificar y a security un registro que firmar. Todo entra en cuatro archivos porque el SDK está bien diseñado y la aplicación deja de pelearse con él.

Esto es lo que significa "AI es el equipo" en código.

Lecturas relacionadas

Preguntas frecuentes

Cuéntanos qué estás construyendo con Claude

Una integración Claude en producción en dos a cuatro semanas.