FoundationModels: LLM en Dispositivo (iOS 26)
Patrones para integrar el modelo de lenguaje en dispositivo de Apple en aplicaciones usando el framework FoundationModels. Abarca 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 privacidad y soporte sin conexión.
Cuándo Activarlo
- Crear funciones impulsadas por IA usando Apple Intelligence en el dispositivo
- Generar o resumir texto sin dependencia de la nube
- Extraer datos estructurados de entrada de 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("El dispositivo no es elegible para Apple Intelligence")
case .unavailable(.appleIntelligenceNotEnabled):
Text("Por favor, 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 contexto de conversación
let session = LanguageModelSession(instructions: """
Eres un asistente de cocina.
Proporciona sugerencias de recetas basadas en 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 de la forma 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 formato:
1. Define un Tipo Generable
@Generable(description: "Información básica del perfil de un gato")
struct CatProfile {
var name: String
@Guide(description: "La edad del gato", .range(0...20))
var age: Int
@Guide(description: "Una frase de perfil 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 a los campos estructurados directamente
print("Nombre: \(response.content.name)")
print("Edad: \(response.content.age)")
print("Perfil: \(response.content.profile)")
Restricciones @Guide Soportadas
.range(0...20)— rango numérico.count(3)— cantidad de elementos en un arreglodescription:— guía semántica para la generación
Patrón Principal — Llamadas 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: "Encuéntrame 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 emocionantes de viaje?",
generating: TripIdeas.self
)
for try await partial in stream {
// partial: TripIdeas.PartiallyGenerated (todas las propiedades Opcionales)
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 Clave de Diseño
| Decisión | Justificación |
|---|---|
| Ejecución en dispositivo | Privacidad — ningún dato sale del dispositivo; funciona sin conexión |
| Límite de 4.096 tokens | Restricción del modelo en 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 múltiples sesiones si es necesario |
response.content (no .output) | API correcta — accede siempre a los resultados a través de 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 — noresponse.output - Divide entradas grandes en fragmentos — el límite de 4.096 tokens aplica a la combinación de instrucciones + indicación + salida
- Usa
@Generablepara salida estructurada — garantías más sólidas que analizar cadenas de texto sin formato - Usa
GenerationOptions(temperature:)para ajustar la creatividad (más alto = más creativo) - Monitorea con Instruments — usa Instruments de Xcode para perfilar el rendimiento de las solicitudes
Antipatrones a Evitar
- Crear sesiones sin verificar
model.availabilityprimero - 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 de texto sin formato cuando la salida estructurada con
@Generablefuncionaría - Construir lógica compleja de varios pasos en una sola indicación — divide 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 dispositivo para aplicaciones sensibles a la privacidad
- Extracción de datos estructurados de la entrada del usuario (formularios, comandos de lenguaje natural)
- Funciones asistidas por IA que deben funcionar sin conexión
- UI de transmisión que muestra progresivamente contenido generado
- Acciones de IA específicas del dominio mediante llamadas a herramientas (búsqueda, cálculo, consulta)