Apple Intelligence 써보기 - SwiftUI Foundation Models 프레임워크

농담고미고미·2025년 6월 28일
0

프론트엔드

목록 보기
11/12

Foundation Models 프레임워크가 공개되면서 온디바이스로 간단한 대화형 AI 기능 구현이 가능해졌다. 모델이 30억 파라미터의 규모의 LLM이라고 함. 아 빨리 써보고 싶어!!!!

애플 개발자 문서

@Generable
struct SearchSuggestions {
	@Guide(description: "A list of suggested search terms", .count(4))
    var searchTerms: [String]
}

@Guide은 주석 같은 역할을 한다. count, maximumCount와 같은 속성을 설정할 수 있다.

// Responding with a Generable Type

let promt = """
	Generate a list of suggested search terms for an app about visiting famous landmarks.
	"""
    
let response = try await session.respond(
	to: prompt,
    generating: SearchSuggestions.self
)

print(response.content)

여기서 response에서 좀 더 안전하게 할려면

catch { LanguageModelSession.GenerationError.guardrailViolation { // Handle the safety error
}

를 한다. guardrails는 인풋 프롬프트랑 모델의 아웃풋을 체크한다.

그리고 앱에서 인간에게 직접적인 인풋을 받는 프롬프트는 위험성을 증가한다. 따라서 가능한 인풋 프롬프트를 사용자에게 직접 받는 걸 피하고 인풋이 뭔지 제대로 알 수 있게 boundary를 두는게 안전하다.

enum TopicOptions {
    case family
    case nature
    case work 
}
let topicChoice = TopicOptions.nature
let prompt = """
    Generate a wholesome and empathetic journal prompt that helps \
    this person reflect on \(topicChoice)
    """

혹은 프롬프트 인풋을 사용자에게 자유롭게 이용하게 하고 싶으면, 아웃풋에 바운더리를 거는 것도 좋은 방법이다.

@Generable
enum Breakfast {
    case waffles
    case pancakes
    case bagels
    case eggs 
}
let session = LanguageModelSession()
let userInput = "I want something sweet."
let prompt = "Pick the ideal breakfast for request: \(userInput)"
let response = try await session.respond(to: prompt, generating: Breakfast.self)

위 코드에서 사용자가 자연어로 입력한 요청을 기반으로 AI는 breakfast enum 중 가장 적절한 항목을 직접 선택하여 반환한다. generating: Breakfast.self는 응답을 해당 enum 중 하나로 제한하는 역할을 한다.

텍스트는 토큰이라는 짧은 문자 단위로 생성된다. 스트리밍 출력 시 토큰은 델타라는 형태로 전달되지만 파운데이션 모델 프레임워크는 다른 방식을 사용한다.

파운데이션 모델 프레임워크는 구조적 출력을 이용한다. 원시 델타 대신 스냅샷을 스트리밍한다.

Snapshot: Message.PartiallyGenerated(greeting: nil)
Snapshot: Message.PartiallyGenerated(greeting: "")
Snapshot: Message.PartiallyGenerated(greeting: "Welcome")
Snapshot: Message.PartiallyGenerated(greeting: "Welcome to")
Snapshot: Message.PartiallyGenerated(greeting: "Welcome to Cup")

PartiallyGenerated은 모든 속성이 옵셔널이다.

struct Itinerary: Generable {
	var name: String
    var days: [Day]
    
    nonisolated struct PartiallyGenerated: ConvertibleFromGeneratedContent {
    	var name: String?
        var days: [Day.PartiallyGenerated]?
    }
}

nonisolated는 actor 외부에서도 자유롭게 접근 가능한 메서드/속성/타입이 되게 한다.

PartiallyGenerated은 streamResponse 메서드 호출 시 사용된다.

// Streaming partial generations

let stream = session.streamResponse(
	to: "Craft a 3-day itineray to Mt. Fuji.",
    generating: Itinerary.self
)

스트림 응답은 비동기 시퀀스를 반환한다.

for try await partial in stream {
	print(partial)
}

그럼

Itinerary.PartiallyGenerated(name: "Mt. Fuji", days: [Day.PartiallyGenerated(...)])

와 같이 출력된다. 보면 저 문장에서 여행지 이름과 날짜를 추론해서 뽑아낸 뒤 Itinerary 모델로 바꿔준다.

그럼 뷰에서는 어떻게 사용할까?

struct ItineraryView: View {
	let session: LanguageModelSession
    let dayCount: Int
    let landmarkName: String
    
    @State private var itinerary: Itinerary.PartiallyGenerated?
    
    var body: some View {
    	// ...
        Button("Start") {
        	Task {
            	do {
                	let prompt = """
                    Generate a \(dayCount) day itinerary \ to \(landmarkName).
                    """
                    
                    let stream = session.streamResponse(
                    	to: prompt,
                        generating: Itinerary.self
                    )
                    
                    for try await partial in stream {
                    	self.itinerary = partial
                    }
                } catch {
                	print(error)
                }
            }
        }
    }
}

AI 모델에게 프롬프트를 보낸다 (streamingResponse) -> 모델이 응답을 부분적으로 스트리밍한다. -> 그 스트림을 for try await로 반복하면서 각 파트를 하나씩 받아서 받아올 때마다 self.itinerary에 최신 상태로 업데이트한다.

로딩되는 동안 애니메이션을 이용한 전환을 하면 사용자 경험이 향상된다. 그리고 뷰에서 배열로 쓸 때 뷰의 식별자 관리에 신경 써야한다.

그리고 파라미터 순서대로 생성된다는 것도 기억하자. 퍼포먼스에 영향을 줄 수 있다.

@Generable
struct Itinerary {
	@Guide(description: "Plans for each day")
    var days: [DayPlan]
    
    @Guide(description: "A brief summary")
    var summary: String
}

이런 경우에 요약 파라미터가 DayPlan보다 뒤에 오는게 로딩 시간이 짧다.

Tool calling도 간단히 보자.

import FoundationModels

struct GetWeatherTool: Tool {
	let name = "getWeather"
    let description = "Retrieve the letest weather information for a city"
    
    @Generable
    struct Arguments {
    	@Guide(description: "The city to fetch the weather for")
        var city: String
    }
    
    func call(arguments: Arguments) async throws -> ToolOutput {
    	let places = try await CLGeocoder().geocodeAddressString(arguments.city)
        let weather = try await WeatherService.shared.weather(for: places.first!.location!)
        let temperature = weather.currentWeather.temperature.value
        let content = GeneratedContent(properties: ["temperature": temperture])
        let output = ToolOutput(content)
        return output
    }
}

사용자가 입력한 도시이름을 좌표로 변환한ㄷ.
해당 위치의 날씨 정보를 가져온다.
현재 온도 값만 추출한다.
결과를 GeneratedContent로 감싸고 ToolOutput로 반환한다.

Tool 사용 예시는 아래와 같다.

let session = LanguageModelSession(
	tools: [GetWeatherTool()],
    instructions: "Help the user with weather forecasts."
)

let response = try await session.respond(
	to: "What is the temperature in Cupertino?"
)

print(response.content)
// It's 71'F in Cupertino!

멀티턴 인터랙션도 가능하다. 프롬프트 흐름을 기억한다는 것.

// Multi-turn interactions

let session = LanguageModelSession()

let firstHaiku = try await session.respond(to: "Write a haiku about fishing")
print(firstHaiku.content)

let secondHaiku = try await session.respond(to: "Do another one about golf")
print(secondHaiku.content)

print(session.transcript)

위 코드에서 2번째 하이쿠를 적어달라고 하지 않고, 골프에 대해 하나 더 적어보라고 했다. 그러나 알아서 흐름을 인식해서 2번째 골프에 대한 하이쿠를 작성해준다. 그리고 transcript을 출력하면 각 프롬프트와 응답을 전부 출력한다.

@State private var session = LanguageModelSession()

@State private var haiku: String?

var body: some View {
	if let haiku {
    	Text(haiku)
    }
    Button("Go!") {
    	Task {
        	haiku = try await session.respond(
            	to: "Write a haiku about something you havenn't yet"
            ).content
        }
    }
    .disabled(session.isResponding)
}

여러 어댑터가 있지만 Content tagging adapter가 유용하다. 주제 감지를 기본적으로 지원한다.

// Content tagging use case

@Generable
struct Result {
	let topics: [String]
}

let session = LanguageModelSession(model: SystemLanguageModel(useCase: .contentTagging))
let response = try await session.respond(to: ..., generating: Result.self)

위의 코드와 같이 주제를 뽑아내서 topics에 저장하고, Result 모델을 응답으로 반환한다. 응용하면 감정이나 행동을 input text에서 뽑아낼 수도 있다.

// Content tagging use case

@Generable
struct Top3ActionEmotionResult {
	@Guide(.maximumCount(3))
    let actions: [String]
    @Guide(.maximumCount(3))
    let emotions: [String]
}

let session = LanguageModelSession(
	model: SystemLanguageModel(useCase: .contentTagging),
    instructions: "Tag the 3 most important actions and emotions in the given input text."
)
let response = try await session.respond(to: ..., generating: Top3ActionEmotionResult.self)

이 Foundation Modeld은 apple intelligence를 지원하는 기기에서만 작동한다!! 사용 가능한지 뷰에서 컨트롤해줘야 함...

// Availability checking

struct AvailabilityExample: View {
	private let model = SystemLanguageModel.default
    
    var body: some View {
    	switch model.availability {
        case .available:
        	Text("Model is available")
        case .unavailable:
        	Text("Model is unavailable")
            Text("Reason: \(reason)")
        }
    }
}
profile
농담곰을 좋아해요 말랑곰탱이

0개의 댓글