
안녕하세요. 오늘은 Foundation Models에 대해서 알아보도록 하겠습니다.
Foundation Models는 Apple에서 제공하는 Apple Intelligence를 앱에서 직접 이용할 수 있도록 만들어진 프레임워크입니다. 디바이스의 AI를 사용하기 때문에 오프라인 상태에서도 동작하고, 앱 용량도 늘어나지 않습니다. 모든 처리가 기기 내에서 이루어지기 때문에 사용자 데이터도 안전하게 보호됩니다.
시작하기 전에 필요한 것들이 있는데요, Xcode 26이 필요하고 맥OS도 최신 버전인 Tahoe로 업데이트 해야 합니다...ㅎ
오늘 내용은
Code along with the Foundation Models framework | Meet with Apple
해당 영상을 기반으로 작성하였습니다.
한번 해보시죠!
우선 어떻게 작동하는지 테스트를 해보려고 합니다. 많이 사용하는 방법은 Playground를 이용하는 건데, 이번에 플레이그라운드 매크로가 새로 생겼습니다.
#Playground 매크로를 사용하면 옆에 Canvas가 생기면서 바로 결과를 확인할 수 있습니다. 마치 SwiftUI Preview처럼 실시간으로 결과를 볼 수 있어서 굉장히 편리합니다.

이 기능은 다음에 더 자세히 다뤄봐야겠지만, 일단 개발 편의성이 확실히 올라간 건 맞네요~!
간단한 사용법은 다음과 같습니다. 먼저 LanguageModelSession()을 생성합니다. URLSession()처럼 하나의 세션을 통해 언어 모델과 계속 대화할 수 있게 설계되어 있어서 Session이라는 이름을 사용하는 것 같습니다.
그리고 respond(to:) 메서드를 이용해서 AI에게 질문을 던집니다. to 파라미터에 프롬프트를 입력하면 일정 시간 후에 결과가 나옵니다.
#Playground {
let session = LanguageModelSession()
let response = try await session.respond(to: "한국 여행 일정 3일로 계획해줘")
}
실행하면 결과는 prompt, content, 그리고 duration으로 리턴됩니다.

와 신기하네요! 이렇게 간단하게 AI를 사용할 수 있다니. 당장 앱에 적용하고 싶지만, 아직 해결해야 할 문제들이 좀 보입니다. 이건 차차 해결해나가도록 하죠.
AI 응답이 매번 달라지면 곤란하겠죠? 우리는 일관성 있고 정확한 정보를 원합니다.
이때 사용하는 게 instructions입니다. AI에게 주는 가이드라인이라고 생각하면 됩니다!
#Playground {
let instructions = """
사용자를 위한 여행 일정을 만드는 것이 당신의 임무입니다.
각 날짜마다 활동, 숙소, 레스토랑을 포함해야 합니다.
항상 제목, 간단한 설명, 그리고 일별 계획을 포함하세요.
"""
let session = LanguageModelSession(instructions: instructions)
let response = try await session.respond(to: "한국 여행 일정 3일로 계획해줘")
}
Instructions를 추가하니 결과가 훨씬 구조화되고 체계적으로 바뀌었습니다:
### 1일차: 서울
- 오전: 경복궁 방문 - 한국의 전통 궁궐을 둘러보세요.
- 점심: 북촌 한옥마을 근처의 한식당에서 전통 한식을 즐겨보세요.
- 오후: 인사동에서 전통 공예품과 기념품을 구매하세요.
- 저녁: 명동 거리에서 다양한 먹거리와 쇼핑을 즐기세요.
- 숙소: 신라호텔, 롯데호텔 등 서울 시내 중심에 위치
### 2일차: 서울
- 오전: 남산타워에서 서울 전경을 감상하세요.
- 점심: 남산 근처의 카페에서 가벼운 점심을 즐기세요.
- 오후: 국립중앙박물관 방문 - 한국의 역사와 문화를 깊이 탐구하세요.
- 저녁: 홍대 거리에서 다양한 문화 공연과 거리 음식을 즐겨보세요.
- 숙소: 호텔 메리어트 동대문, 그랜드 인터컨티넨탈 서울 파르나스
### 3일차: 서울
- 오전: 롯데월드 어드벤처 또는 코엑스 아쿠아리움 방문 - 가족 단위 여행객에게 적합합니다.
- 점심: 실내식당이나 카페에서 간단히 식사하세요.
- 오후: 남산서울타워 또는 남산공원 산책 - 서울의 풍경을 즐기며 여유로운 시간을 보내세요.
- 저녁: 마포나 홍대 근처에서 저녁 식사를 마무리하세요.
- 숙소: 호텔 메리어트 동대문, 그랜드 인터컨티넨탈 서울 파르나스
요구사항대로 나온 것을 확인하실 수 있습니다! 각 날짜별로 활동, 숙소가 명확하게 구분되어 나왔네요.
그런데 instructions와 prompt의 차이가 뭘까요?
정리하자면 Instructions는 세션 전체에서 유지되며, 세션의 transcript(대화 기록)에서 항상 첫 번째 항목으로 기록됩니다. 그래서 Instructions는 정적으로 유지하고 사용자 입력을 넣지 않는 게 좋습니다.
그런데 문제가 있습니다. Apple Intelligence를 지원하지 않는 기기이거나 사용자가 활성화하지 않았을 수도 있잖아요?
이럴 때는 어떻게 해야 할까요?
#Playground {
let model = SystemLanguageModel.default
// The availability property provides detailed information on the model's state.
switch model.availability {
case .available:
print("Foundation Models를 사용할 수 있으며 준비되었습니다!")
case .unavailable(.deviceNotEligible):
print("이 기기에서는 모델을 사용할 수 없습니다.")
case .unavailable(.appleIntelligenceNotEnabled):
print("설정에서 Apple Intelligence가 활성화되지 않았습니다.")
case .unavailable(.modelNotReady):
print("모델이 아직 준비되지 않았습니다. 나중에 다시 시도해주세요.")
case .unavailable(let other):
print("알 수 없는 이유로 모델을 사용할 수 없습니다.")
}
}
이렇게 시스템 모델의 availability 상태를 확인할 수 있습니다. 각 케이스별로 적절한 안내 메시지를 사용자에게 보여줄 수 있겠죠.
자, 이제 Playground에서 충분히 테스트해봤으니 실제 앱에 적용해보겠습니다!
ItineraryGenerator라는 여행일정 생성자 객체를 만들어서 진행하겠습니다. Playground에서 했던 것 처럼
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
private(set) var itineraryContent: String?
init(landmark: Landmark) {
self.landmark = landmark
let instructions = """
사용자를 위한 여행 일정을 만드는 것이 당신의 임무입니다.
각 날짜마다 활동, 숙소, 레스토랑을 포함해야 합니다.
항상 제목, 간단한 설명, 그리고 일별 계획을 포함하세요.
"""
self.session = LanguageModelSession(instructions: instructions)
}
}
Instructions를 이용해서 세션을 초기화합니다.
여기서 session을 var로 선언한 이유는 나중에 대화가 이어지면서 세션을 업데이트하거나 새로운 세션으로 교체할 수 있게 하기 위함입니다.
이제 실제로 여행 일정을 생성하는 함수를 만들어봅시다:
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = "\(landmark.name)에서 \(dayCount)일 간의 여행계획을 만드세요."
let response = try await session.respond(to: prompt)
self.itineraryContent = response.content
} catch {
self.error = error
}
}
이걸 뷰에 적용시켜 보겠습니다.
struct LandmarkTripView: View {
let landmark: Landmark
@State private var itineraryGenerator: ItineraryGenerator?
@State private var requestedItinerary: Bool = false
var body: some View {
ScrollView {
if !requestedItinerary {
VStack(alignment: .leading, spacing: 16) {
Text(landmark.name)
.padding(.top, 150)
.font(.largeTitle)
.fontWeight(.bold)
Text(landmark.shortDescription)
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
} else if let content = itineraryGenerator?.itineraryContent {
Text(LocalizedStringKey(content))
.padding()
}
}
.scrollDisabled(!requestedItinerary)
.safeAreaInset(edge: .bottom) {
ItineraryButton {
requestedItinerary = true
await itineraryGenerator?.generateItinerary()
}
}
.task {
let generator = ItineraryGenerator(landmark: landmark)
self.itineraryGenerator = generator
}
.headerStyle(landmark: landmark)
}
}
.task에 generator를 생성하고 버튼을 누렀을 경우에 생성합니다.
그럼 그 결과가 text로 나오게 됩니다.

멋있네요! 실제로 동작하는 AI 기능이 생겼습니다.
그런데 여기서 문제가 있습니다. 지금은 단순 텍스트로만 나오는데, 이걸 지도에 표시하거나 UI를 가독성 좋게 만들고 싶어요. 어떻게 하면 활용도를 높일 수 있을까요?
이제 단순 문자열이 아닌 구조화된 데이터(Swift 구조체)로 응답을 받아봅시다!
이런 장점이 생깁니다.
먼저 알아야 할 매크로가 두 가지 있습니다.
AI한테 힌트를 주는 매크로입니다. "이 속성은 이렇게 만들어줘!"
@Guide(description: "...")@Guide(description: "An exciting name for the trip.")
let title: String
@Guide(.anyOf(...))@Guide(description: "An exciting name for the trip.")
@Guide(.anyOf(ModelData.landmarkNames))
let title: String
ModelData.landmarkNames = ["에펠탑", "콜로세움", "타지마할"...]예시:
@Guide(.count(...))@Guide(description: "A list of day-by-day plans.")
@Guide(.count(3))
let days: [Day]
예시:
이건 추후에 더 자세히 다뤄보겠습니다.
@Generable
struct Itinerary: Equatable {
@Guide(description: "An exciting name for the trip.")
let title: String
@Guide(.anyOf(ModelData.landmarkNames))
let destinationName: String
let description: String
@Guide(description: "An explanation of how the itinerary meets the user's special requests.")
let rationale: String
@Guide(description: "A list of day-by-day plans.")
@Guide(.count(3))
let days: [DayPlan]
}
그래서 위와같은 데이터 구조체를 만들었습니다.
이제 적용하는 방법입니다.
아까 respond(to:) 메서드에서 generating: 파라미터에 데이터 타입을 넣습니다.
@Observable
@MainActor
final class ItineraryGenerator {
var error: Error?
let landmark: Landmark
private var session: LanguageModelSession
private(set) var itinerary: Itinerary?
init(landmark: Landmark) {
self.landmark = landmark
let instructions = """
사용자를 위한 여행 일정을 만드는 것이 당신의 임무입니다.
각 날짜마다 활동, 숙소, 레스토랑을 포함해야 합니다.
"""
self.session = LanguageModelSession(instructions: instructions)
}
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = "\(landmark.name)에서 \(dayCount)일 간의 여행계획을 만드세요."
let response = try await session.respond(
to: prompt,
generating: Itinerary.self
)
self.itinerary = response.content
} catch {
self.error = error
}
}
}
이제 content가 Itinerary 타입으로 바뀌어서 나오게 됩니다.
이제 instructions도 줄일 수있습니다. 왜냐하면 어떤 데이터 타입으로 나올지 결정되었기 때문입니다.
변경했더니 이렇게 나왔습니다.

로그를 찍어보니
(lldb) po itinerary
▿ Itinerary
- title : "Sahara Desert Adventure"
- destinationName : "Sahara Desert"
- description : "Experience the vast beauty and mystery of the Sahara Desert."
- rationale : "This itinerary offers a balanced mix of adventure, relaxation, and cultural immersion in the Sahara Desert, ensuring an unforgettable experience."
▿ days : 3 elements
▿ 0 : DayPlan
- title : "Day 1: Arrival and Exploration"
- subtitle : "Welcome to the Sahara!"
- destination : "Sahara Desert"
▿ activities : 3 elements
▿ 0 : Activity
- type : FoundationModelsCodeAlong.Kind.sightseeing
- title : "Visit Erg Chebbi Sand Dunes"
- description : "Explore the stunning Erg Chebbi sand dunes, one of the most iconic sites in the Sahara."
▿ 1 : Activity
- type : FoundationModelsCodeAlong.Kind.foodAndDining
- title : "Dine at a Local Berber Restaurant"
- description : "Enjoy traditional Berber cuisine under the stars at a local restaurant."
▿ 2 : Activity
- type : FoundationModelsCodeAlong.Kind.hotelAndLodging
- title : "Stay at a Desert Camp"
- description : "Overnight at a luxurious desert camp with camel rides and stargazing opportunities."
▿ 1 : DayPlan
- title : "Day 2: Cultural Immersion and Adventure"
- subtitle : "Experience Desert Life"
- destination : "Sahara Desert"
▿ activities : 3 elements
▿ 0 : Activity
- type : FoundationModelsCodeAlong.Kind.sightseeing
- title : "Explore Todra Gorge"
- description : "Hike through the majestic Todra Gorge, known for its dramatic cliffs."
▿ 1 : Activity
- type : FoundationModelsCodeAlong.Kind.foodAndDining
- title : "Traditional Moroccan Lunch"
- description : "Savor a hearty Moroccan lunch at a local eatery."
▿ 2 : Activity
- type : FoundationModelsCodeAlong.Kind.sightseeing
- title : "Visit a Berber Village"
- description : "Learn about the nomadic lifestyle by visiting a nearby Berber village."
▿ 2 : DayPlan
- title : "Day 3: Relaxation and Departure"
- subtitle : "Unwind and Reflect"
- destination : "Sahara Desert"
▿ activities : 3 elements
▿ 0 : Activity
- type : FoundationModelsCodeAlong.Kind.foodAndDining
- title : "Farewell Dinner"
- description : "Enjoy a farewell dinner featuring regional specialties and live music."
▿ 1 : Activity
- type : FoundationModelsCodeAlong.Kind.hotelAndLodging
- title : "Check Out and Depart"
- description : "Check out of your desert camp and begin your journey home, taking memories of the Sahara with you."
▿ 2 : Activity
- type : FoundationModelsCodeAlong.Kind.sightseeing
- title : "Sunset at the Oasis"
- description : "Watch the breathtaking sunset over the desert oasis before departing."
이렇게 나왔습니다. 신기하네요
다음 단계는 프롬프팅 기법입니다!
질문하는 방식을 개선해서 더 정확하고 일관된 응답을 받는 방법입니다.
핵심 기법:
이전에 generateItinerary에서 dayCount나 landmark.name을 동적으로 받지 못하는 문제가 있었습니다.
PromptBuilder를 이용하면:
1. 동적인 값을 프롬프트에 넣을 수 있음
2. 예시 데이터를 제공해서 정확도 증가
3. Swift 구문처럼 자연스럽게 작성
약간 느낌이 SwiftUI의 VStack { } 같은 느낌입니다.
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
public struct Prompt : Sendable {
/// Creates an instance with the content you specify.
public init(_ content: some PromptRepresentable)
}
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension Prompt : PromptRepresentable {
/// An instance that represents a prompt.
public var promptRepresentation: Prompt { get }
}
@available(iOS 26.0, macOS 26.0, *)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
extension Prompt {
public init(@PromptBuilder _ content: () throws -> Prompt) rethrows
}
우선 정의는 이렇게 나와있는데 이것도 다음에 잘 다뤄봐야겠습니다.
우선 example은 다음과 같습니다.
extension Itinerary {
static let exampleTripToJapan = Itinerary(
title: "일본 벚꽃 여행",
description: "도쿄와 교토의 아름다운 봄...",
days: [
Day(activity: "...", hotel: "...", restaurant: "..."),
// ...
]
)
}
이걸 제공하는 것을
One-shot Prompting (예시 제공) 이라고한다고 합니다.
| 설명 | 예시 개수 | |
|---|---|---|
| Zero-shot | 예시 없이 요청만 | 0개 |
| One-shot | 예시 1개 제공 | 1개 |
| Few-shot | 예시 여러 개 제공 | 2~5개 |
이 앱에서는 One-shot 사용 (일본 여행 예시 1개)
그래서 리팩토링을 진행합니다.
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"\(landmark.name)에 대한 \(dayCount)일 여행 일정을 만들어줘."
"재미있는 제목과 설명을 포함해줘."
"여기 원하는 형식의 예시가 있어. 내용은 복사하지 말고 형식만 참고해:"
Itinerary.exampleTripToJapan
}
let response = try await session.respond(
to: prompt,
generating: Itinerary.self
)
self.itinerary = response.content
} catch {
self.error = error
}
}
다음은 스트리밍 응답에 대해서 알아보겠습니다!
지금까지의 방식에는 개선해야 할 점이 있습니다:
ChatGPT처럼 응답이 생성되면서 실시간으로 UI가 채워지는 방식으로 변경할 수 있습니다!
여기서 알아야 할 핵심 개념은 PartiallyGenerated입니다.
PartiallyGenerated가 뭐야?비유로 설명:
기존 Itinerary:
// 모든 값이 있어야 함 (완성품)
Itinerary(
title: "파리 3일", // ✅ 있음
description: "낭만적인...", // ✅ 있음
days: [Day(...), ...] // ✅ 있음
)
Itinerary.PartiallyGenerated:
// 일부만 있어도 됨 (제작 중)
Itinerary.PartiallyGenerated(
title: "파리 3일", // ✅ 있음
description: nil, // ⏳ 아직 안 만들어짐
days: nil // ⏳ 아직 안 만들어짐
)
장점:
title만 만들어도 바로 화면에 보여줄 수 있음description 만들어지면 추가로 보여줌그래서 @Observable을 사용하면 상태 변경에 따른 UI 업데이트를 SwiftUI에서 효과적으로 나타낼 수 있습니다!
@Observable
@MainActor
final class ItineraryGenerator {
...
private(set) var itinerary: Itinerary.PartiallyGenerated?
...
}
우선 PartiallyGenerated? 타입으로 변경합니다.
그리고 AsyncStream을 이용해서 값의 변경을 일으킵니다.
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
// 기존 (한 번에 받기)
let response = try await session.respond(to: prompt,
generating: Itinerary.self)
self.itinerary = response.content
// 변경 후 (스트리밍으로 받기)
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self)
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
}

데이터 타입도 ResponseStream으로 바뀌었습니다.
View에서도 변경을 합니다.
struct ItineraryView: View {
let landmark: Landmark
let itinerary: Itinerary.PartiallyGenerated
모든 값이 옵셔널이 되어서 처리할 때도 언랩핑을 해서 로직 처리를 해줘야 합니다.
if let title = itinerary.title {
Text(title)
.contentTransition(.opacity)
.font(.largeTitle)
.fontWeight(.bold)
}
영상은 찍지 못했지만, 값이 변경될 때마다 UI가 실시간으로 업데이트되는 것을 확인할 수 있었습니다!
이제 Tool Calling에 대해서 알아보겠습니다.
AI는 학습된 지식만 알고 있습니다. 실시간 데이터나 최신 정보는 가져오지 못하죠.
AI한테 Swift 함수를 사용할 수 있게 해줍니다! AI가 필요하다고 판단하면 직접 우리가 정의한 함수를 호출할 수 있습니다.
Tool 이름과 설명 정의
final class FindPointsOfInterestTool: Tool {
// MARK: - Tool 이름과 설명
let name = "findPointsOfInterest"
let description = "Finds points of interest for a landmark."
let landmark: Landmark
init(landmark: Landmark) {
self.landmark = landmark
}
}
Tool이라는 프로토콜이 있습니다.

프로토콜의 정의를 보면, "사이드 이펙트를 수행하거나 런타임에 정보를 가져오기 위해 모델이 호출할 수 있는 것"이라고 되어 있습니다.
필수 요구사항:
name: Tool의 고유 IDdescription: Tool이 무엇을 하는지 설명Arguments: Tool에 전달할 인자 타입call(arguments: Arguments) async throws: 실제 실행 메서드name의 역할findPointsOfInterest 호출description의 역할검색 가능한 카테고리 정의
// Tool 밖에 정의 (파일 끝부분에)
@Generable
enum Category: String, CaseIterable {
case hotel
case restaurant
}
역할:
장점:
.hotel, .restaurant (정해진 것만)@Generable
struct Arguments {
@Guide(description: "This is the type of business to look up for.")
let pointOfInterest: Category
}
이렇게 하면
AI가 Arguments를 정의해서 Arguments(pointOfInterest: .hotel)
실제 검색을 수행합니다.
역할:
@Generable을 붙입니다. func call(arguments: Arguments) async throws -> String {
let results = await getSuggestions(category: arguments.pointOfInterest, landmark: landmark.name)
return """
There are these \(arguments.pointOfInterest) in \(landmark.name):
\(results.joined(separator: ", "))
"""
}
@Generable
enum Category: String, CaseIterable {
case hotel
case restaurant
}
func getSuggestions(category: Category, landmark: String) -> [String] {
switch category {
case .hotel: return ["Hotel 1", "Hotel 2", "Hotel 3"]
case .restaurant: return ["Restaurant 1", "Restaurant 2", "Restaurant 3"]
}
}
실제로 호출하는 곳에서 이렇게 정의를 합니다.
getSuggestions 이 부분이 실제 서버 호출로 바뀔 수있겠죠
이제 이 Tool을 연결해보겠습니다.
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
Tool을 생성하고
instructions에 내용을 추가합니다.
init(landmark: Landmark) {
self.landmark = landmark
// 기존
let instructions = """
Your job is to create an itinerary for the user.
Each day needs an activity, hotel and restaurant.
"""
self.session = LanguageModelSession(instructions: instructions)
// 변경 후
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
let instructions = Instructions {
"Your job is to create an itinerary for the user."
"For each day, you must suggest one hotel and one restaurant."
"Always use the 'findPointsOfInterest' tool to find hotels and restaurant in \(landmark.name)"
}
// session 초기화는 다음 단계에서!
}
let pointOfInterestTool = FindPointsOfInterestTool(landmark: landmark)
명령도 추가
"Always use the 'findPointsOfInterest' tool to find hotels and restaurant in \(landmark.name)"
Instructions 없으면:
Instructions 있으면:
findPointsOfInterest 호출// 기존
self.session = LanguageModelSession(instructions: instructions)
// 변경 후
self.session = LanguageModelSession(
tools: [pointOfInterestTool],
instructions: instructions
)
데이터 타입에서 추측할 수 있듯이 여러가지 tool을 넣을 수 있습니다.
let hotelTool = FindHotelTool(landmark: landmark)
let restaurantTool = FindRestaurantTool(landmark: landmark)
let weatherTool = GetWeatherTool()
self.session = LanguageModelSession(
tools: [hotelTool, restaurantTool, weatherTool],
instructions: instructions
)
다음은 Options에 대해서 알아보겠습니다.
Greedy Sampling 옵션 추가
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Give it a fun title and description."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
// 기존
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self)
// 변경 후
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self,
options: GenerationOptions(sampling: .greedy))
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
}

플레이그라운드에서 확인해보니 검색 결과가 Tool의 더미데이터에 영향을 받았습니다.
검색 결과를 서버 요청이나 데이터에 한정지어서 결과를 냈던 것이지요
GenerationOptions(sampling: .greedy)가 뭐야?Sampling: AI가 다음 단어를 선택하는 방식
options: GenerationOptions(sampling: .greedy)
특징:
예시:
왜 Tool 쓸 때 Greedy가 좋을까요?
Tool 호출은 정확해야 하니까!

SamplingMode는 greedy와 random이 있는데
정확히는 모르겠지만 대략적으로
greedy는 가장 높은 확률의 응답값만 도출하기때문에 항상 동일한 결과를 나타내고
Random은
특징: - 확률이 threshold 이상인 토큰들 중에서 랜덤 선택 - probabilityThreshold: 0.9 → 상위 90% 확률 토큰들만 고려 - seed: 랜덤 시드 (재현성 위해 고정 가능)
이런 느낌이라 아마 더 창의적이겠지만 확률의 값에 의해 더 변동적일 것입니다.
Temperature라는 것도 있는 것 같은데 .. 이건 넘어가겠습니다.
다음은 성능 최적화입니다!
최적화를 하기 위해서는 먼저 성능을 측정해야겠죠? Instruments를 사용해서 Profile을 해봅시다.

Instruments에서 Foundation Models를 검색합니다.

측정 결과:
목표는 첫 응답 속도를 개선하고 더 빠르고 반응성 좋은 AI 기능을 만드는 것입니다.

핵심 내용:
이 메서드는 세션에 필요한 리소스를 미리 메모리에 로드하고, 선택적으로 프롬프트의 prefix를 캐싱합니다.
언제 사용하나요?
respond(to:) 호출 최소 1초 전에 실행해야 합니다View의 .task 또는 onAppear에서 호출하면 좋습니다.
1️⃣ 리소스 로드
2️⃣ Prompt Prefix 캐싱 (선택사항)
.task {
// 뷰가 나타날 때 실행
// 비동기 작업 가능
}
타이밍:
1. 화면 열림 (LandmarkDetailView → LandmarkTripView)
↓
2. .task 실행
↓
3. generator.prewarmModel() 호출 (백그라운드)
↓
4. 사용자가 화면 보며 "Generate Itinerary" 버튼 찾음 (1~3초)
↓
5. 버튼 클릭
↓
6. 이미 준비됨! 즉시 생성 시작 ⚡
화면 열기
↓
랜드마크 정보 읽기 (2~3초)
↓
버튼 찾기
↓
버튼 클릭

Pre-warming을 적용하니까:
확실히 개선됐습니다!
변경할 부분:
func generateItinerary(dayCount: Int = 3) async {
do {
let prompt = Prompt {
"Generate a \(dayCount)-day itinerary to \(landmark.name)."
"Here is an example of the desired format, but don't copy its content:"
Itinerary.exampleTripToJapan
}
// 기존
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self)
// 변경 후
let stream = session.streamResponse(to: prompt,
generating: Itinerary.self,
includeSchemaInPrompt: false)
for try await partialResponse in stream {
self.itinerary = partialResponse.content
}
} catch {
self.error = error
}
}
includeSchemaInPrompt: false가 뭐야?
@Generable 구조체의 정의@Generable
struct Itinerary {
let title: String
let description: String
let days: [Day]
}
AI한테 전달되는 Schema 정보:
{
"type": "object",
"properties": {
"title": { "type": "string" },
"description": { "type": "string" },
"days": {
"type": "array",
"items": { "$ref": "#/definitions/Day" }
}
},
"required": ["title", "description", "days"]
}
✅ Few-shot 예시가 있을 때:
let prompt = Prompt {
"Generate a 3-day itinerary"
"Here is an example:"
Itinerary.exampleTripToJapan // 이미 형태를 보여줌!
}
AI 입장:
왜 Schema를 제거하는걸까요? 더 정확하면 좋을 것 같은데
1️⃣ 토큰 절약
Schema 포함: 1000 토큰
Schema 제외: 500 토큰
→ 500 토큰 절약! (50% 감소)
2️⃣ 처리 속도 향상
토큰 적으면 처리 빠름
특히 첫 토큰까지 시간 단축
3️⃣ 비용 절감 (API 사용 시)
토큰 수로 과금되는 경우
50% 토큰 절약 = 50% 비용 절감
⚠️ 주의사항
❌ Schema 제거하면 안 되는 경우:
Few-shot 예시 없을 때:
let prompt = Prompt {
"Generate a 3-day itinerary"
// 예시 없음!
}
let stream = session.streamResponse(
to: prompt,
generating: Itinerary.self,
includeSchemaInPrompt: false // ❌ 위험!
)
→ AI가 구조를 모름!
Few-shot 예시 있을 때:
let prompt = Prompt {
"Generate a 3-day itinerary"
"Here is an example:"
Itinerary.exampleTripToJapan // ✅ 예시 있음
}
let stream = session.streamResponse(
to: prompt,
generating: Itinerary.self,
includeSchemaInPrompt: false // ✅ 안전!
)
→ AI가 예시 보고 따라함!
📊 성능 비교
| Schema 포함 | Schema 제외 | |
|---|---|---|
| 토큰 수 | ~1000 | ~500 ⚡ |
| 첫 토큰 | 1.5초 | 0.8초 ⚡ |
| 정확도 | 높음 | 높음 (예시 있으면) ✅ |

최종 결과:
오늘 Foundation Models Framework를 통해 iOS 앱에 AI를 넣는 방법을 배웠습니다.
정말 신기하고 강력한 기능이네요! 실제 앱에 적용해보면서 더 많은 것을 배워봐야겠습니다 🚀