FoundationModels: LLM en el Dispositivo (iOS 26)
Patrones para integrar el modelo de lenguaje local de Apple en aplicaciones utilizando el framework FoundationModels. Cubre generación de texto, salida estructurada con @Generable, llamadas a herramientas personalizadas y transmisión de instantáneas, todo ejecutándose en el dispositivo para garantizar privacidad y soporte sin conexión.
Cuándo Activarlo
- Construir funciones potenciadas por IA usando Apple Intelligence en el dispositivo
- Generar o resumir texto sin dependencia de la nube
- Extraer datos estructurados a partir de entrada en lenguaje natural
- Implementar llamadas a herramientas personalizadas para acciones de IA específicas del dominio
- Transmitir respuestas estructuradas para actualizaciones de UI en tiempo real
- Necesitar IA que preserve la privacidad (ningún dato sale del dispositivo)
Patrón Principal — Verificación de Disponibilidad
Siempre verifica la disponibilidad del modelo antes de crear una sesión:
struct GenerativeView: View {
private var model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
ContentView()
case .unavailable(.deviceNotEligible):
Text("Dispositivo no elegible para Apple Intelligence")
case .unavailable(.appleIntelligenceNotEnabled):
Text("Activa Apple Intelligence en Ajustes")
case .unavailable(.modelNotReady):
Text("El modelo se está descargando o no está listo")
case .unavailable(let other):
Text("Modelo no disponible: \(other)")
}
}
}
Patrón Principal — Sesión Básica
// Una sola interacción: crea una nueva sesión cada vez
let session = LanguageModelSession()
let response = try await session.respond(to: "¿Cuál es un buen mes para visitar París?")
print(response.content)
// Múltiples interacciones: reutiliza la sesión para mantener contexto conversacional
let session = LanguageModelSession(instructions: """
Eres un asistente de cocina.
Proporciona sugerencias de recetas basadas en los ingredientes.
Mantén las sugerencias breves y prácticas.
""")
let first = try await session.respond(to: "Tengo pollo y arroz")
let followUp = try await session.respond(to: "¿Qué tal una opción vegetariana?")
Puntos clave para las instrucciones:
- Define el rol del modelo ("Eres un mentor")
- Especifica qué hacer ("Ayuda a extraer eventos del calendario")
- Establece preferencias de estilo ("Responde lo más breve posible")
- Añade medidas de seguridad ("Responde con 'No puedo ayudar con eso' para solicitudes peligrosas")
Patrón Principal — Generación Guiada con @Generable
Genera tipos Swift estructurados en lugar de cadenas de texto sin procesar:
1. Define un Tipo Generable
@Generable(description: "Información básica de perfil sobre un gato")
struct CatProfile {
var name: String
@Guide(description: "La edad del gato", .range(0...20))
var age: Int
@Guide(description: "Un perfil de una oración sobre la personalidad del gato")
var profile: String
}
2. Solicita Salida Estructurada
let response = try await session.respond(
to: "Genera un gato de rescate adorable",
generating: CatProfile.self
)
// Accede directamente a los campos estructurados
print("Nombre: \(response.content.name)")
print("Edad: \(response.content.age)")
print("Perfil: \(response.content.profile)")
Restricciones de @Guide Compatibles
.range(0...20)— rango numérico.count(3)— cantidad de elementos en un arraydescription:— guía semántica para la generación
Patrón Principal — Llamada a Herramientas
Permite que el modelo invoque código personalizado para tareas específicas del dominio:
1. Define una Herramienta
struct RecipeSearchTool: Tool {
let name = "recipe_search"
let description = "Busca recetas que coincidan con un término dado y devuelve una lista de resultados."
@Generable
struct Arguments {
var searchTerm: String
var numberOfResults: Int
}
func call(arguments: Arguments) async throws -> ToolOutput {
let recipes = await searchRecipes(
term: arguments.searchTerm,
limit: arguments.numberOfResults
)
return .string(recipes.map { "- \($0.name): \($0.description)" }.joined(separator: "\n"))
}
}
2. Crea una Sesión con Herramientas
let session = LanguageModelSession(tools: [RecipeSearchTool()])
let response = try await session.respond(to: "Búscame algunas recetas de pasta")
3. Maneja Errores de Herramientas
do {
let answer = try await session.respond(to: "Encuentra una receta de sopa de tomate.")
} catch let error as LanguageModelSession.ToolCallError {
print(error.tool.name)
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
// Maneja el error específico de la herramienta
}
}
Patrón Principal — Transmisión de Instantáneas
Transmite respuestas estructuradas para UI en tiempo real con tipos PartiallyGenerated:
@Generable
struct TripIdeas {
@Guide(description: "Ideas para próximos viajes")
var ideas: [String]
}
let stream = session.streamResponse(
to: "¿Cuáles son algunas ideas de viaje emocionantes?",
generating: TripIdeas.self
)
for try await partial in stream {
// partial: TripIdeas.PartiallyGenerated (todas las propiedades Optional)
print(partial)
}
Integración con SwiftUI
@State private var partialResult: TripIdeas.PartiallyGenerated?
@State private var errorMessage: String?
var body: some View {
List {
ForEach(partialResult?.ideas ?? [], id: \.self) { idea in
Text(idea)
}
}
.overlay {
if let errorMessage { Text(errorMessage).foregroundStyle(.red) }
}
.task {
do {
let stream = session.streamResponse(to: prompt, generating: TripIdeas.self)
for try await partial in stream {
partialResult = partial
}
} catch {
errorMessage = error.localizedDescription
}
}
}
Decisiones de Diseño Clave
| Decisión | Justificación |
|---|---|
| Ejecución en el dispositivo | Privacidad — ningún dato sale del dispositivo; funciona sin conexión |
| Límite de 4,096 tokens | Restricción del modelo en el dispositivo; divide datos grandes en varias sesiones |
| Transmisión de instantáneas (no deltas) | Amigable con salida estructurada; cada instantánea es un estado parcial completo |
Macro @Generable | Seguridad en tiempo de compilación para generación estructurada; genera automáticamente el tipo PartiallyGenerated |
| Una solicitud por sesión | isResponding evita solicitudes concurrentes; crea varias sesiones si es necesario |
response.content (no .output) | API correcta — siempre accede a los resultados mediante la propiedad .content |
Mejores Prácticas
- Siempre verifica
model.availabilityantes de crear una sesión — maneja todos los casos de no disponibilidad - Usa
instructionspara guiar el comportamiento del modelo — tienen prioridad sobre las indicaciones - Verifica
isRespondingantes de enviar una nueva solicitud — las sesiones manejan una solicitud a la vez - Accede a
response.contentpara los resultados — no a.output - Divide entradas grandes en fragmentos — el límite de 4,096 tokens se aplica a instrucciones + indicación + salida combinados
- Usa
@Generablepara salida estructurada — garantías más fuertes que analizar cadenas sin procesar - Usa
GenerationOptions(temperature:)para ajustar la creatividad (valores más altos = más creativo) - Monitorea con Instruments — usa Instruments de Xcode para perfilar el rendimiento de las solicitudes
Anti-Patrones a Evitar
- Crear sesiones sin verificar primero
model.availability - Enviar entradas que excedan la ventana de contexto de 4,096 tokens
- Intentar solicitudes concurrentes en una sola sesión
- Usar
.outputen lugar de.contentpara acceder a los datos de respuesta - Analizar respuestas de cadena sin procesar cuando la salida estructurada con
@Generablefuncionaría - Construir lógica compleja de múltiples pasos en una sola indicación — divídela en varias indicaciones enfocadas
- Asumir que el modelo siempre está disponible — la elegibilidad del dispositivo y la configuración varían
Cuándo Usarlo
- Generación de texto en el dispositivo para aplicaciones sensibles a la privacidad
- Extracción de datos estructurados a partir de entrada del usuario (formularios, comandos en lenguaje natural)
- Funciones asistidas por IA que deben funcionar sin conexión
- UI que transmite y muestra contenido generado progresivamente
- Acciones de IA específicas del dominio mediante llamadas a herramientas (búsqueda, cómputo, consulta)