Blog.

Prompt API: Fundamentos

Prompt API: Fundamentos
13 min read

En el post anterior, configuramos el entorno y entendimos la arquitectura de Built-in AI. Ahora vamos a explorar la Prompt API en profundidad.

La Prompt API es la interfaz más flexible para interactuar con Gemini Nano. Si has usado la API de OpenAI o Claude, esto te resultará familiar, pero con una diferencia crucial: todo ocurre en el navegador, sin enviar datos a servidores externos.

¿Cuándo Usar la Prompt API?

Antes de empezar, es importante entender cuándo esta API es la herramienta correcta.

Las Built-in AI APIs se dividen en dos categorías principales:

  • Prompt API: Una interfaz de propósito general donde envías prompts de texto libre y recibes respuestas. Es la más flexible pero requiere más gestión manual (sesiones, tokens, contexto).
  • Task APIs: APIs especializadas para tareas específicas como Summarizer, Writer, Rewriter y Translation. Son más simples de usar pero menos flexibles.

Prompt API vs Task APIs

CaracterísticaPrompt APISummarizer/Writer/Rewriter
FlexibilidadTotal (prompt libre)Restringida a tarea específica
Gestión de TokensManual (quota local)Automática
Uso de MemoriaAlto (mantiene historial)Bajo (stateless)
Streaming
ControlTemperatura, topK, JSON schemaLimitado (tono, longitud)
ContextoMulti-turno (conversaciones)Single-shot
Caso de UsoChat, extracción compleja, RAGResumen rápido, reescritura simple

Usa Prompt API cuando:

  • Necesitas conversaciones multi-turno con contexto
  • Requieres control fino sobre parámetros del modelo
  • El contexto previo es crítico para la respuesta
  • Estás construyendo un chatbot o asistente

Usa Task APIs cuando:

  • La tarea es stateless (un resumen aislado, una traducción)
  • No necesitas mantener contexto entre llamadas
  • La tarea encaja perfectamente en una Task API existente

Hello World: Creando una Sesión

Lo primero es verificar disponibilidad e instanciar una sesión. Piensa en la sesión como el contexto de una conversación.

// 1. Verificar disponibilidad
const availability = await LanguageModel.availability();

if (availability === 'unavailable') {
  console.error('Gemini Nano no soportado en este dispositivo');
  return;
}

if (availability === 'downloadable') {
  console.log('El modelo se descargará en la primera petición');
}

// 2. Crear la sesión
const session = await LanguageModel.create();

// 3. Enviar un prompt simple
const result = await session.prompt('Explica qué es la gravedad en una frase.');
console.log(result);
// "La gravedad es la fuerza que atrae a los objetos con masa entre sí."

⚠️ User Activation: Si el modelo no está descargado (availability === 'downloadable'), la llamada a create() requiere user activation (click, keydown, etc.). No intentes descargar automáticamente al cargar la página.

La Importancia del Streaming

Para una experiencia de usuario real, el streaming no es opcional, es crítico. El usuario debe ver cómo aparece el texto progresivamente.

const stream = session.promptStreaming(
  'Escribe un poema corto sobre el código.'
);

const outputElement = document.querySelector('#output');

// El stream es un ReadableStream donde cada chunk contiene solo el texto nuevo
for await (const chunk of stream) {
  // Cada chunk contiene solo el contenido nuevo (delta), no acumulativo
  outputElement.textContent += chunk;
}

System Prompts y Contexto Inicial

Puedes establecer el "carácter" del modelo al crear la sesión usando initialPrompts.

const session = await LanguageModel.create({
  initialPrompts: [
    {
      role: 'system',
      content:
        'Eres un asistente técnico especializado en JavaScript. Respondes en máximo 2 frases.',
    },
  ],
});

const result = await session.prompt('¿Qué es hoisting?');
console.log(result);
// "Hoisting es el mecanismo por el cual declaraciones se mueven al tope de su scope durante compilación."

Nota: Los system prompts se preservan incluso si el contexto se desborda por límite de tokens.

Gestión de Tokens y Cuotas

A diferencia de APIs cloud que cobran por token, aquí el límite es local: cada sesión tiene un inputQuota (contexto máximo permitido). Esto se debe a los limites de los contextos que tienen los modelos locales (asi como los que tienen modelos en cloud) y hay que tener en cuenta que estos contextos son normalmente mas chicos que los que tienen los grandes modelos en cloud.

console.log(`Tokens usados: ${session.inputUsage}/${session.inputQuota}`);

// Estimar tokens ANTES de enviar
const longPrompt = 'Explica en detalle la historia de JavaScript...';
const estimatedTokens = session.measureInputUsage(longPrompt);

if (session.inputUsage + estimatedTokens > session.inputQuota) {
  console.warn('Prompt demasiado largo. Crear nueva sesión o usar clone()');
}

¿Qué sucede cuando se llena el contexto?

Cuando inputUsage alcanza inputQuota, los mensajes más antiguos se olvidan automáticamente (excepto los system prompts). Si tu aplicación depende de contexto inicial:

  1. Monitorea el uso: Verifica inputUsage regularmente
  2. Resetea estratégicamente: Usa clone() para mantener configuración pero limpiar historial
  3. Prioriza información: Coloca datos críticos en system prompts
// Resetear contexto manteniendo configuración
const freshSession = await session.clone();
session.destroy(); // Liberar la sesión original

Controlando la Aleatoriedad: Temperature y TopK

Gemini Nano expone dos parámetros cruciales para ajustar el comportamiento del modelo:

// Obtener rangos permitidos
const params = await LanguageModel.params();
console.log(params);
// {
//   defaultTopK: 3,
//   maxTopK: 128,
//   defaultTemperature: 0.8,
//   maxTemperature: 2.0
// }

Temperature

Controla la "creatividad" o aleatoriedad de las respuestas:

  • Baja (0.2-0.5): Respuestas determinísticas y conservadoras
  • Alta (1.0-2.0): Respuestas creativas e impredecibles
// Para clasificación o extracción de datos: temperatura baja
const sessionDeterministic = await LanguageModel.create({
  temperature: 0.2,
  topK: 1,
});

const sentiment = await sessionDeterministic.prompt(
  'Clasifica el sentimiento: "Este producto es horrible"'
);
// "Negativo" (consistente, sin variación)
// Para generación creativa: temperatura alta
const sessionCreative = await LanguageModel.create({
  temperature: params.maxTemperature,
  topK: params.maxTopK,
});

const story = await sessionCreative.prompt(
  'Escribe un título creativo para una historia sobre IA'
);
// Resultados variados: "El Despertar de las Neuronas", "Sinápsis Artificial", etc.

Regla Práctica

TareaTemperatureTopKRazón
Clasificación0.2-0.31-3Consistencia crítica
Extracción de datos0.2-0.51-3Respuestas predecibles
Resúmenes0.5-0.83-8Balance entre precisión y variedad
Generación creativa1.0-2.064-128Máxima diversidad
Brainstorming1.5-2.0128Ideas inesperadas

Ciclo de Vida de Sesiones

Cada sesión consume VRAM significativa. No destruir sesiones correctamente puede causar degradación de rendimiento.

Patrón Anti-Pattern

// ❌ MAL: Sesión queda en memoria indefinidamente
async function handleUserQuery(text) {
  const session = await LanguageModel.create();
  return await session.prompt(text);
  // Session nunca se destruye → memory leak
}

Patrón Correcto

// ✅ BIEN: Cleanup explícito
async function handleUserQuery(text) {
  const session = await LanguageModel.create();
  try {
    return await session.prompt(text);
  } finally {
    session.destroy();
  }
}

Patrón para Conversaciones

Para conversaciones lo ideal es reusar la sesión. Por ejemplo, podriamos hacer lo siguiente:

class ChatManager {
  constructor() {
    this.session = null;
  }

  async initialize() {
    this.session = await LanguageModel.create({
      initialPrompts: [
        {
          role: 'system',
          content: 'Eres un asistente técnico conciso.',
        },
      ],
    });
  }

  async sendMessage(text) {
    if (!this.session) await this.initialize();
    return await this.session.promptStreaming(text);
  }

  cleanup() {
    if (this.session) {
      this.session.destroy();
      this.session = null;
    }
  }
}

// Uso
const chat = new ChatManager();
await chat.initialize();

// Conversación
await chat.sendMessage('¿Qué es REST?');
await chat.sendMessage('¿Y GraphQL?');

// Limpiar al salir
window.addEventListener('beforeunload', () => chat.cleanup());

Clonación de Sesiones

Para resetear el contexto pero mantener la configuración inicial:

const originalSession = await LanguageModel.create({
  temperature: 0.3,
  topK: 3,
  initialPrompts: [
    { role: 'system', content: 'Eres un experto en JavaScript' },
  ],
});

// Después de muchas interacciones, el contexto está lleno
console.log(originalSession.inputUsage); // 3800/4096

// Clonar mantiene la configuración pero resetea el historial
const freshSession = await originalSession.clone();
console.log(freshSession.inputUsage); // ~100/4096 (solo system prompt)

// Liberar la sesión original
originalSession.destroy();

Agregar Contexto Dinámicamente con append()

El método append() permite inyectar mensajes al historial de la sesión sin ejecutar inferencia. Esto es útil para:

  1. Cargar conversaciones previas desde almacenamiento
  2. Inyectar contexto de RAG (Retrieval-Augmented Generation)
  3. Simular conversaciones para tests o debugging
  4. Ahorrar tokens de inferencia al construir contexto

Ejemplo Básico: Restaurar Conversación

// Usuario cierra y reabre la app
const previousConversation = loadFromLocalStorage('chat_history');
// [
//   { role: 'user', content: 'Necesito ayuda con async/await' },
//   { role: 'assistant', content: 'Con gusto te ayudo...' },
//   ...
// ]

const session = await LanguageModel.create();

// Restaurar el historial sin re-ejecutar inferencias
await session.append(previousConversation);

// Ahora el usuario puede continuar donde dejó
const result = await session.prompt('¿Cómo manejo errores en async/await?');
// El modelo tiene contexto de toda la conversación previa

Ejemplo: RAG con Inyección de Contexto

// El usuario hace una pregunta
const userQuery = '¿Cuál es nuestra política de devoluciones?';

// 1. Buscar información relevante en tu base de datos
const relevantDocs = await searchDocuments(userQuery);

// 2. Inyectar documentos como contexto sin invocar el modelo
await session.append([
  {
    role: 'user',
    content: `Contexto relevante:\n${relevantDocs.map(d => d.content).join('\n\n')}`,
  },
  {
    role: 'assistant',
    content: 'He revisado la documentación relevante.',
  },
]);

// 3. Ahora hacer la pregunta real con contexto inyectado
const answer = await session.prompt(userQuery);
// El modelo responde basándose en los documentos inyectados

Diferencia clave: append() vs initialPrompts

AspectoinitialPromptsappend()
CuándoSolo al crear sesión (create())En cualquier momento después
InferenciaNo ejecuta inferenciaNo ejecuta inferencia
System promptsPuede incluir role: 'system'Solo 'user' y 'assistant'
Uso típicoConfiguración inicial permanenteContexto dinámico/temporal

Patrón Avanzado: Context Window Management

class SmartChatSession {
  constructor() {
    this.session = null;
    this.conversationHistory = [];
  }

  async initialize() {
    this.session = await LanguageModel.create({
      initialPrompts: [
        {
          role: 'system',
          content: 'Eres un asistente técnico conciso.',
        },
      ],
    });
  }

  async chat(userMessage) {
    // Añadir mensaje del usuario
    this.conversationHistory.push({
      role: 'user',
      content: userMessage,
    });

    // Verificar si nos acercamos al límite de contexto
    if (this.session.inputUsage > this.session.inputQuota * 0.8) {
      console.warn('Contexto al 80%, optimizando...');

      // Opción 1: Clonar sesión (resetea historial, mantiene config)
      const fresh = await this.session.clone();
      this.session.destroy();
      this.session = fresh;

      // Opción 2: Re-inyectar solo últimos N mensajes importantes
      const recentHistory = this.conversationHistory.slice(-10);
      await this.session.append(recentHistory);
    }

    // Obtener respuesta
    const response = await this.session.prompt(userMessage);

    // Guardar respuesta en historial
    this.conversationHistory.push({
      role: 'assistant',
      content: response,
    });

    // Persistir para restaurar después
    saveToLocalStorage('chat_history', this.conversationHistory);

    return response;
  }
}

Nota de Performance: append() es mucho más eficiente que re-enviar mensajes vía prompt() porque no ejecuta inferencia. Usa append() cuando necesites construir contexto sin generar respuestas.

Cancelación de Operaciones (AbortController)

Los usuarios deben poder detener la generación de texto. Esto mejora la UX y ahorra tokens cuando la respuesta no es útil o toma un camino incorrecto. Algo interesante es que podemos abortar tanto la sesión entera así como el prompt específico.

const controller = new AbortController();
const stopButton = document.querySelector('#stop');

stopButton.onclick = () => controller.abort();

try {
  const stream = session.promptStreaming(
    'Explica la historia completa de la computación',
    { signal: controller.signal }
  );

  for await (const chunk of stream) {
    outputElement.textContent = chunk;
  }
} catch (error) {
  if (error.name === 'AbortError') {
    outputElement.textContent += '\n\n[Generación detenida]';
  }
}

Patrón recomendado: Siempre proporciona un botón de "Stop" visible durante la generación en streaming. La capacidad de detener una respuesta insatisfactoria es crítica para la percepción de control del usuario.

Seguridad: Sanitización de Salida

Los LLMs pueden ser manipulados vía prompt injection. Nunca uses innerHTML directamente con salida de LLM.

// ✅ SEGURO: textContent escapa automáticamente
outputElement.textContent = await session.prompt(userInput);

// ❌ PELIGROSO: Nunca hagas esto
outputElement.innerHTML = await session.prompt(userInput);

Markdown Sanitizado

Si necesitas renderizar markdown del modelo como HTML, usa DOMPurify para sanitizar y permitir solo tags seguros.

import { marked } from 'marked';
import DOMPurify from 'dompurify';

const markdown = await session.prompt(userInput);
const rawHtml = marked.parse(markdown);
// Sanitizar permitiendo solo tags seguros
const cleanHtml = DOMPurify.sanitize(rawHtml, {
  ALLOWED_TAGS: ['p', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li'],
  ALLOWED_ATTR: [], // No permitir atributos (class, style, onclick, etc.)
});

outputElement.innerHTML = cleanHtml;

Manteniendo el Contexto Multi-turno

Una sesión mantiene el historial de la conversación automáticamente:

const session = await LanguageModel.create();

await session.prompt('Mi nombre es Carlos y soy ingeniero de software.');
// Modelo almacena: "El usuario se llama Carlos y es ingeniero"

await session.prompt('¿Cuál es mi profesión?');
// Respuesta: "Eres ingeniero de software."

await session.prompt('¿Y mi nombre?');
// Respuesta: "Tu nombre es Carlos."

Conclusión

En este artículo exploramos los fundamentos de la Prompt API:

  • Sesiones: Crear, configurar y destruir sesiones correctamente
  • Streaming: Implementar UX responsive con texto progresivo
  • System Prompts: Establecer comportamiento consistente
  • Gestión de tokens: Monitorear quota y manejar desbordamiento
  • Temperature y TopK: Controlar aleatoriedad del modelo
  • Seguridad: Sanitizar outputs para prevenir inyecciones

Pero esto no es todo lo relacionado a Prompt API, hay cosas muy interesantes que quedaron fuera como:

  • Salida estructurada: JSON Schema para respuestas predecibles
  • Entrada multimodal: Procesar imágenes y audio
  • Tool Use: El modelo invoca funciones JavaScript autónomamente
  • Prefix guidance: Forzar formatos específicos

Con estos fundamentos, ya puedes construir chatbots y asistentes básicos completamente en el cliente, manteniendo privacidad y latencia baja.

Comments

Share your thoughts and join the discussion


Related Posts