Prompt API: Fundamentos

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ística | Prompt API | Summarizer/Writer/Rewriter |
|---|---|---|
| Flexibilidad | Total (prompt libre) | Restringida a tarea específica |
| Gestión de Tokens | Manual (quota local) | Automática |
| Uso de Memoria | Alto (mantiene historial) | Bajo (stateless) |
| Streaming | ✅ | ✅ |
| Control | Temperatura, topK, JSON schema | Limitado (tono, longitud) |
| Contexto | Multi-turno (conversaciones) | Single-shot |
| Caso de Uso | Chat, extracción compleja, RAG | Resumen 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 acreate()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:
- Monitorea el uso: Verifica
inputUsageregularmente - Resetea estratégicamente: Usa
clone()para mantener configuración pero limpiar historial - 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
| Tarea | Temperature | TopK | Razón |
|---|---|---|---|
| Clasificación | 0.2-0.3 | 1-3 | Consistencia crítica |
| Extracción de datos | 0.2-0.5 | 1-3 | Respuestas predecibles |
| Resúmenes | 0.5-0.8 | 3-8 | Balance entre precisión y variedad |
| Generación creativa | 1.0-2.0 | 64-128 | Máxima diversidad |
| Brainstorming | 1.5-2.0 | 128 | Ideas 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:
- Cargar conversaciones previas desde almacenamiento
- Inyectar contexto de RAG (Retrieval-Augmented Generation)
- Simular conversaciones para tests o debugging
- 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
| Aspecto | initialPrompts | append() |
|---|---|---|
| Cuándo | Solo al crear sesión (create()) | En cualquier momento después |
| Inferencia | No ejecuta inferencia | No ejecuta inferencia |
| System prompts | Puede incluir role: 'system' | Solo 'user' y 'assistant' |
| Uso típico | Configuración inicial permanente | Contexto 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.

