| name | foundation-models-on-device |
| description | iOS 26+에서 Apple FoundationModels 프레임워크를 사용해 온디바이스 LLM을 다루는 패턴입니다. 텍스트 생성, `@Generable` 기반 가이드 생성, tool calling, snapshot streaming을 다룹니다. |
FoundationModels: 온디바이스 LLM(iOS 26)
FoundationModels 프레임워크를 사용해 Apple의 온디바이스 언어 모델을 앱에 통합하는 패턴입니다. 텍스트 생성, @Generable 기반 구조화 출력, 커스텀 tool calling, snapshot streaming을 다룹니다. 모두 디바이스 내에서 실행되므로 개인정보 보호와 오프라인 지원에 적합합니다.
사용 시점
- Apple Intelligence를 활용한 온디바이스 AI 기능을 만들 때
- 클라우드 의존 없이 텍스트를 생성하거나 요약할 때
- 자연어 입력에서 구조화된 데이터를 추출할 때
- 도메인별 AI 액션을 위한 custom tool calling을 구현할 때
- 실시간 UI 업데이트를 위해 구조화된 응답을 스트리밍할 때
- 데이터가 기기를 벗어나지 않는 privacy-preserving AI가 필요할 때
핵심 패턴 — 사용 가능 여부 확인
세션을 만들기 전에 항상 모델 사용 가능 여부를 확인합니다.
struct GenerativeView: View {
private var model = SystemLanguageModel.default
var body: some View {
switch model.availability {
case .available:
ContentView()
case .unavailable(.deviceNotEligible):
Text("Device not eligible for Apple Intelligence")
case .unavailable(.appleIntelligenceNotEnabled):
Text("Please enable Apple Intelligence in Settings")
case .unavailable(.modelNotReady):
Text("Model is downloading or not ready")
case .unavailable(let other):
Text("Model unavailable: \\(other)")
}
}
}
핵심 패턴 — 기본 세션
let session = LanguageModelSession()
let response = try await session.respond(to: "What's a good month to visit Paris?")
print(response.content)
let session = LanguageModelSession(instructions: """
You are a cooking assistant.
Provide recipe suggestions based on ingredients.
Keep suggestions brief and practical.
""")
let first = try await session.respond(to: "I have chicken and rice")
let followUp = try await session.respond(to: "What about a vegetarian option?")
instructions 작성 시 핵심:
- 모델의 역할 정의. 예:
"You are a mentor"
- 수행할 일 명시. 예:
"Help extract calendar events"
- 스타일 선호 지정. 예:
"Respond as briefly as possible"
- 안전 장치 추가. 예: 위험한 요청에는
"I can't help with that"라고 답하기
핵심 패턴 — @Generable 기반 가이드 생성
원시 문자열 대신 구조화된 Swift 타입을 생성합니다.
1. Generable 타입 정의
@Generable(description: "Basic profile information about a cat")
struct CatProfile {
var name: String
@Guide(description: "The age of the cat", .range(0...20))
var age: Int
@Guide(description: "A one sentence profile about the cat's personality")
var profile: String
}
2. 구조화된 출력 요청
let response = try await session.respond(
to: "Generate a cute rescue cat",
generating: CatProfile.self
)
print("Name: \\(response.content.name)")
print("Age: \\(response.content.age)")
print("Profile: \\(response.content.profile)")
지원되는 @Guide 제약
.range(0...20) — 숫자 범위
.count(3) — 배열 원소 수
description: — 생성 의미론 가이드
핵심 패턴 — Tool Calling
모델이 도메인 전용 작업을 위해 커스텀 코드를 호출하게 합니다.
1. 도구 정의
struct RecipeSearchTool: Tool {
let name = "recipe_search"
let description = "Search for recipes matching a given term and return a list of results."
@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. 도구 포함 세션 생성
let session = LanguageModelSession(tools: [RecipeSearchTool()])
let response = try await session.respond(to: "Find me some pasta recipes")
3. 도구 오류 처리
do {
let answer = try await session.respond(to: "Find a recipe for tomato soup.")
} catch let error as LanguageModelSession.ToolCallError {
print(error.tool.name)
if case .databaseIsEmpty = error.underlyingError as? RecipeSearchToolError {
}
}
핵심 패턴 — Snapshot Streaming
PartiallyGenerated 타입으로 구조화된 응답을 스트리밍해 실시간 UI를 만듭니다.
@Generable
struct TripIdeas {
@Guide(description: "Ideas for upcoming trips")
var ideas: [String]
}
let stream = session.streamResponse(
to: "What are some exciting trip ideas?",
generating: TripIdeas.self
)
for try await partial in stream {
print(partial)
}
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
}
}
}
주요 설계 결정
| Decision | Rationale |
|---|
| 온디바이스 실행 | 데이터가 기기를 떠나지 않아 개인정보 보호와 오프라인 지원에 유리 |
| 4,096 토큰 제한 | 온디바이스 모델 제약. 큰 입력은 여러 세션으로 나눠야 함 |
| Snapshot streaming | 구조화 출력 친화적. 각 snapshot이 완전한 partial 상태 |
@Generable 매크로 | 구조화 생성에 대한 컴파일타임 안정성 제공, PartiallyGenerated 타입 자동 생성 |
| 세션당 한 요청 | isResponding이 동시 요청을 막음. 여러 요청은 세션을 여러 개 생성 |
response.content 사용 | 올바른 API. 결과 접근은 항상 .content |
모범 사례
- 항상
model.availability 확인 — 세션 생성 전 모든 unavailable 케이스 처리
instructions 적극 활용 — 프롬프트보다 우선 적용됩니다
- 새 요청 전
isResponding 확인 — 세션은 한 번에 하나의 요청만 처리
- 결과는
response.content로 접근 — .output이 아님
- 큰 입력은 조각내기 — instructions + prompt + output 합쳐서 4,096 토큰 제한
- 구조화 출력에는
@Generable 사용 — 원시 문자열 파싱보다 안전
GenerationOptions(temperature:)로 창의성 조절
- Instruments로 모니터링 — Xcode Instruments로 성능 프로파일링
피해야 할 안티패턴
model.availability 확인 없이 세션 생성
- 4,096 토큰 문맥 창을 초과하는 입력 전송
- 한 세션에서 동시 요청 시도
- 응답 데이터 접근에
.content 대신 .output 사용
@Generable로 충분한데도 원시 문자열을 직접 파싱
- 복잡한 다단계 로직을 프롬프트 하나에 몰아넣기
- 모델이 항상 사용 가능하다고 가정하기
사용 대상
- 개인정보 보호가 중요한 앱의 온디바이스 텍스트 생성
- 사용자 입력에서 구조화된 데이터 추출(폼, 자연어 명령)
- 오프라인으로도 동작해야 하는 AI 보조 기능
- 생성 결과를 점진적으로 보여주는 스트리밍 UI
- 검색, 계산, 조회 같은 도메인 전용 AI 액션의 tool calling