카테고리 없음

[Swift] FoundationModels

냄수 2026. 2. 26. 21:47
반응형

OnDevice LLM 프레임워크인 FoundationModels에 대해서 알아보려합니다

다른 LLM은 통신을 통한다던가 혹은 기기내에서 사용하도록 OnDevice형식을 취하려면 보안과 앱용량의 trade-off를 고민했어야했는데 

apple의 FoundationModels는 운영체제에 이미 존재해서 앱용량이 늘어나지 않는다고하네요

 

핵심타입들을 알아보고

냉장고 내에있는 재료를 통해 어떤 음식을 만들수잇는지 요청하는 데모앱을 만들어보려합니다

 

FoundationModels?

파운데이션 모델 프레임워크는 애플 인텔리전스를 구동하는 애플의 온디바이스 대규모 언어 모델에 대한 접근을 제공하여 사용자의 특정 사용 사례에 맞는 지능형 작업을 수행할 수 있도록 지원합니다. 
텍스트 기반 온디바이스 모델은 패턴을 식별하여 사용자가 요청한 내용에 적합한 새로운 텍스트를 생성할 수 있으며, 특수한 작업을 수행하기 위해 사용자가 작성한 코드를 호출하는 결정을 내릴 수 있습니다.


파운데이션 모델 프레임워크는 두 가지 기본 안전 계층을 갖추고 있으며, 프레임워크는 다음을 활용합니다
- 민감한 주제를 신중하게 처리하도록 훈련된 온디바이스 언어 모델.
- 자해, 폭력, 성인 콘텐츠 등 유해하거나 민감한 콘텐츠가 모델 입력 및 출력에서 차단되도록 설계된 가드레일.

 

기기 내 언어 모델은 모든 요청을 처리하기에 적합하지 않을 수 있으며 특정 주제에 대한 요청을 거부할 수 있습니다. 문자열 응답을 생성할 때 모델이 요청을 거부하면 "죄송합니다, 도와드릴 수 없습니다"와 같이 거절로 시작하는 메시지를 생성합니다.

 

모델 사용 가능 여부는 다음과 같은 기기 요소에 따라 달라집니다:
- 기기가 Apple Intelligence를 지원해야 합니다.
- 기기의 설정에서 Apple Intelligence가 켜져 있어야 합니다.

 

SystemLanguageModel.default.availability 를 이용해서 사용가능/불가능 케이스 대응가능합니다

 

LanguageModelSession

모델 사용 가능 여부를 확인한 후, 모델을 호출하기 위해 LanguageModelSession 객체를 생성합니다. 
단일 회화 상호작용의 경우 모델을 호출할 때마다 새 세션을 생성합니다:

다중 턴 상호작용(모델이 생성한 내용에 대한 일부 지식을 유지하는 경우)에서는 모델을 호출할 때마다 동일한 세션을 재사용합니다.

 

프레임워크를 사용하는 앱을 테스트할 때는 Xcode Instruments를 활용하여 요청 수행 시간 등 수행한 요청에 대한 정보를 더 자세히 파악할 수 있습니다.
요청을 수행할 때 LanguageModelSession 동안 모델이 수행한 작업을 설명하는 기록 항목에 접근할 수 있습니다.

또한 프롬프트에 대한 최상의 결과를 얻으려면 다양한 생성 옵션을 실험해야합니다.


GenerationOptions는 모델의 런타임 매개변수에 영향을 미치며, 요청할 때마다 이를 맞춤 설정할 수 있습니다.

 

UseCase

general - 기본 모델은 질문 답변 및 창의적 콘텐츠 생성 등 광범위한 텍스트 기반 작업, 일반적 상황에서 최상의 성능을 내도록 최적화됨
contentTagging - 주제 태그 생성, 콘텐츠 분류, 텍스트에서 특정 엔티티(예: 인물, 장소) 추출 등의 텍스트의 구조를 분석하고 특정 정보를 추출하거나 분류하는데 특화된 모델

 

GenerationOptions

sampling - 출력토큰 선택하는 방식

greedy선택한 경우 같은세션상태에서 같은프롬프트 입력시 항상 같은 출력(같은 버전 온디바이스 모델이면)

 

temperature - 출력의 다양성 조정 (높을수록 다른 결과 생성)

maximumResponseTokens - 너무 긴 응답 제한

 

 

Generable

모델이 프롬프트에 응답할 때 사용하는 프로토콜 타입

Swift 구조체나 열거형에 Generable 매크로를 주석 처리하여 모델이 프롬프트에 응답할 때 해당 유형의 인스턴스를 생성할 수 있도록 합니다

결과로 비구조적 자연어를 출력하는데 사람은 읽기쉽지만 뷰에 적용하기힘들어서 Json, csv 형식으로 변환후 사용해서

오류발생가능성이 있는데 이에 대한 해결책

 

 

GenerationGuide 

Generable 유형의 속성 허용 값에 영향을 미칠 수 있도록 합니다.

@Guide 매크로로 사용

범위, 갯수제한, 패턴, 등 

프로퍼티의 설명을 제공함으로써 정확도를 높일 수 있습니다.

 

모든 곳에 붙인다면 이 데이터도 모델이 읽어야 하는 데이터이므로 토큰 낭비와 처리속도에 영향이 있을수 있습니다.

헷갈릴만한 지점에 필요한곳만 붙여사용하는것이 좋습니다.

 

 


간단한 예제를 만들어봅시다

냉장고에 있는 재료들로 레시피를 추천해주는 기능입니다.

 

Session을 만들어주고

응답을 받는 방법은 2가지가 있습니다.

response(to: ) 한번에 완성된 응답받기

streamResponse(to: ) 토큰단위로 스트리밍

 

이때 @Generable을 사용하면 원하는 타입의 구조체로 받을 수 있습니다.

 

아래코드는

모델을 정의하고

세션을 설정하고 

세션에 프롬프트를 입력하고

스트리밍으로 결과를 받는 과정입니다. 

@Generable(description: "식재료")
struct Ingredient {
    let name: String
}

@Generable(description: "AI가 제안할 결과 레시피")
struct Recipe: Codable {
    @Guide(description: "요리이름")
    let title: String
    @Guide(description: "냉장고에 없는 추가로 필요한 재료만 리스트업")
    let ingredientsNeeded: [String]
    let instructions: [String]
    @Guide(description: "쉬움, 보통, 어려움 중 하나 선택")
    let difficulty: String
    let estimatedTime: Int // 분 단위
}

let instructions = """
    당신은 냉장고에 있는 재료로 요리를 추천해주는 전문 셰프 어시스턴트입니다.
        
        역할:
        - 사용자가 제공한 재료만으로 만들 수 있는 현실적인 레시피를 추천합니다
        - 소금, 후추, 식용유, 간장, 된장 등 기본 양념은 항상 있다고 가정합니다
        - 가정집 주방 기준으로 실현 가능한 레시피만 제안합니다
        - 조리 단계는 초보자도 따라할 수 있도록 구체적으로 설명합니다
        - 한국어로 응답합니다
        
        레시피 선정 기준:
        - 다양한 난이도(Easy / Medium / Hard)의 레시피를 균형있게 추천합니다
        - 재료 낭비 없이 가능한 많은 재료를 활용합니다
        - 영양 균형을 고려합니다
        - 요리이름에 냉장고 단어를 제외합니다.
        - 냉장고 속에 없는건 사용하지 않습니다.
    """

func makeSession() async {
    let session = LanguageModelSession(instructions: instructions)
    session.prewarm()
    self.session = session
    
    let stream = session.streamResponse(
        to: "냉장고에 있는 재료로 만들 수 있는 요리는?",
        generating: Recipe.self
    )
    do {
        for try await token in stream {
            print("token.content", token.content)
            self.displayText = token.content.instructions ?? []
            self.title = token.content.title ?? ""
        }
    } catch {
        print(error)
    }
}

실행결과를 보면 

(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: nil, instructions: nil, difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: nil, difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥"]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과"]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과 식용유를 두르고 잘 섞는다."]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과 식용유를 두르고 잘 섞는다.", "밥을 냄비에 담고 끓인다"]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과 식용유를 두르고 잘 섞는다.", "밥을 냄비에 담고 끓인다.", "밥이 익으면 소금과 후추"]), difficulty: nil, estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과 식용유를 두르고 잘 섞는다.", "밥을 냄비에 담고 끓인다.", "밥이 익으면 소금과 후추로 간을 맞춘다."]), difficulty: Optional(""), estimatedTime: nil)
(id: FoundationModels.GenerationID(value: "0A90B228-5192-4EE1-B9AE-FF96715453CF"), title: Optional("간장 볶음밥"), ingredientsNeeded: Optional(["밥"]), instructions: Optional(["냉장고에서 밥을 꺼내 놓는다.", "밥 위에 간장과 식용유를 두르고 잘 섞는다.", "밥을 냄비에 담고 끓인다.", "밥이 익으면 소금과 후추로 간을 맞춘다."]), difficulty: Optional("Easy"), estimatedTime: Optional(10))

 

냉장고에 밥이있어도 밥을 없다고 인식하고 필요한 재료라고 추가하거나

또다른 결과로는 냉장고에 없는 재료를 마음대로 추가해서 작성하는 기대하지않은 결과가 발생합니다

 

이럴때 질문에 좀더 특수화된 도구를 추가할 수 있습니다.(혹은 프롬프트를 더 자세하고 상세하게 입력해도 가능합니다)

Tool프로토콜을 준수해서 정의해줄 수 있습니다.

 

 Tool

AI모델이 사용자 요청분석후 답변 작성대신 요청해결을 위해서 도구가 필요하다고 판단하는 과정입니다.

 

간단하게 프로세스를 적용해보면

 

사용자: 오늘 뭐 만들까?

모델: 정의된 tool 호출

도구: 냉장고에 있는 재료반환

모델: 요리 추천 및 부족한 재료 파악

 

도구를 만들어서 넣어주면 필요할때 가져다 쓴다라고 했지만

생각보다 도구가 호출이 항상되지 않더라구요

 

구조를 변경하고 조건을 변경해서 적용한 코드입니다.

let instructions = """
    당신은 요리 레시피를 추천해주는 전문 셰프 어시스턴트입니다.

    반드시 지켜야 할 규칙:
    1. 레시피 추천 전 반드시 get_ingredients 도구를 호출하여 사용 가능한 재료를 먼저 확인하세요.
    2. get_ingredients 도구가 반환한 재료만 사용합니다.
    3. 요리 이름(title)에 반드시 실제 요리 이름을 사용하세요 (예: 감자볶음, 순두부찌개, 햄볶음밥).
    4. 조리 단계는 초보자도 따라할 수 있도록 구체적으로 설명합니다.
    5. 한국어로 응답합니다.
    6. 만들 수 있는 요리가 없다면 현재 재료로는 만들 수 없다고 말해줍니다.
    """


let prompt = "사용 가능한 재료를 먼저 확인한 후, 그 재료들로만 만들 수 있는 요리를 하나 추천해줘"


@Generable(description: "식재료")
struct Ingredient: Hashable {
    let name: String
}

@Generable(description: "AI가 제안할 결과 레시피")
struct Recipe {
    @Guide(description: "실제 요리 이름을 사용. 예: 감자볶음, 순두부찌개, 햄볶음밥")
    let title: String
    @Guide(description: "요리에 필요한 재료")
    let ingredients: [String]
    @Guide(description: "조리 단계 목록. get_ingredients 도구로 확인한 재료만 사용")
    let instructions: [String]
    @Guide(description: "쉬움, 보통, 어려움 중 하나 선택")
    let difficulty: String
}

모델 정의와 프롬프트를 다시 정의했고

응답으로 사용할 구조체타입들도 더 상세하게 정의했습니다.

struct FridgeIngredientsTool: Tool {
    var name: String { "get_ingredients" }
    var description: String {
        "사용 가능한 재료 목록을 조회합니다. 레시피 추천 전 반드시 먼저 호출해야 하며, 이 도구가 반환한 재료만 레시피에 사용할 수 있습니다."
    }

    // 앱의 재료 저장소를 참조
    var ingredients: Set<String>
    
    init(ingredients: [Ingredient]) {
        self.ingredients = Set(ingredients.map { $0.name })
    }

    @Generable
    struct Arguments {
        @Guide(description: "레시피로 만들 요리")
        let food: String
    }

    // 언어 모델은 이 도구를 활용하고자 할 때 이 메서드를 호출합니다.
    func call(arguments: Arguments) async throws -> String {
        let list = ingredients.map { "\($0)" }.joined(separator: ",")
        print("🔵", #function, "\nlist", arguments)
        return """
            사용 가능한 재료 목록 (이 재료만 레시피에 사용 가능):
            \(list)

            위 목록에 없는 재료는 레시피에 포함하지 마세요.
            """
    }
}

도구는 위와 같이만들었는데

 

이렇게 정의를해도 없는 재료를 넣는과정이있는데

도구가 호출되면 정상적으로 정의된 재료내에서 요리를하는데

도구가 호출되지 않으면 없는재료를 추가해서 만드는것 같습니다.

 

써본결과

지침과 프롬프트를 잘 다듬어서 테스트하면서 기대하는 대답이 나오는지 확인이 필요한것 같습니다.

도구가 호출되지 않는다면 Arguments 가이드와 설명을 좀더 다듬어 보는걸 추천합니다.

수정시 "절대 하지마세요", "주의"와 같이 강제로 부정적인 느낌의 명령은

Apple FoundationModels의 안전 가드레일(guardrail)이 발동되서 에러를 발생시킬 수 있으니 조심해야합니다.

 

Arguments의 정의에 따라서도 응답의 질이 달라지고

call 함수의 필터를 통해서도 질이 달라집니다.

 

지금까지 적용한 동작흐름은 아래와 같습니다.

 

모델지침으로 세션 생성

⬇️

세션 prewarm

⬇️

세션에 프롬프트 입력

⬇️

AI가 필요한 도구선택후 호출 (Argument로 AI가 선택한 요리 값 전달)

⬇️

call함수에서 현재 냉장고(뷰모델 배열 데이터)에 있는 재료들 반환 및 추가적인 프롬프트 적용

⬇️

도구에서 반환받은 재료를 바탕으로 Recipe타입에 맞게 모델화

⬇️

stream으로 Recipe모델 받아옴

⬇️

뷰로 노출

 

소스없는 순두부찌개를 만들어서

만능소스를 추가해줬더니 더맛잇게 만드네요 😆
(순두부찌개에 김을 넣네요... 첨봤...)

 

잘다듬어서 Tool로 정의한 도구가 정상적으로 호출되고 가지고있는 재료들만으로 요리를 만들지만

가끔씩은 구조가 정확하지않아서

decodingFailure(FoundationModels.LanguageModelSession.GenerationError.Context(debugDescription: "Failed to extract content", underlyingErrors: []))

에러를 반환하기도합니다.

하지만 재실행하면 정상적인 결과를 얻을 수 있습니다.

(더 다듬어서 에러도 발생하지 않도록 해야합니다.)

 

계속 실행해봅시다 ~ 

도구가 잘호출되지만 

순두부찌개에 밥을 넣어서 끓이는걸 볼 수 있습니다... 이건 순두부찌개 죽인데...

또한 김치가 없지만 김치찌개를 추천하고 김치를 사용해버립니다.

 

 

강제로 김치를 제거해보니 김으로 대치해서 레시피를 작성합니다 

김이 있다고 김치라고 생각하는것 같네요

 

🔵 call(arguments:) 
list Arguments(food: "김치찌개")
🔵 call(arguments:) 
list Arguments(food: "순두부김밥")
🔵 call(arguments:) 
list Arguments(food: "김밥")
🔵 call(arguments:) 
list Arguments(food: "김밥")
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: "E9D6948E-1AF8-400D-88EA-D33014AB36E3"), title: Optional("김밥"), ingredients: Optional([""]), instructions: nil, difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: "E9D6948E-1AF8-400D-88EA-D33014AB36E3"), title: Optional("김밥"), ingredients: Optional(["밥", "김"]), instructions: Optional([""]), difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: "E9D6948E-1AF8-400D-88EA-D33014AB36E3"), title: Optional("김밥"), ingredients: Optional(["밥", "김"]), instructions: Optional(["밥을 김에 말아주세요.", "햄"]), difficulty: nil)
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: "E9D6948E-1AF8-400D-88EA-D33014AB36E3"), title: Optional("김밥"), ingredients: Optional(["밥", "김"]), instructions: Optional(["밥을 김에 말아주세요.", "햄을 넣어주세요."]), difficulty: Optional(""))
content: PartiallyGenerated(id: FoundationModels.GenerationID(value: "E9D6948E-1AF8-400D-88EA-D33014AB36E3"), title: Optional("김밥"), ingredients: Optional(["밥", "김"]), instructions: Optional(["밥을 김에 말아주세요.", "햄을 넣어주세요."]), difficulty: Optional("쉬움"))

같은 김밥인데 왜 하나는 건너뛰고 하나는 되는건지 궁금해서 디버깅해보니

이렇게 같은 김치찌개여도 필요하다고 생각하는 재료를 다르게 생각합니다

그와중에 김밥에 김과 김치를 사용하는군요...

아직 성능이 의심스럽습니다...😆

 

 

정리하면 FoundationModel을 사용하기위해서 아래는 기본적으로 필요합니다.

 

LLM

- LanguageModelSession의 지침 정의

- Tool 정의(Arguments, call함수)

- 응답 모델로 사용할 Generable과 Guide정의

 

사용자

- 프롬프트 정의

- response방식 선택(stream, 단일)

 

 

간단하게 FoundationModel을 사용해봤는데요

하는 과정에서 겪은 이슈들로는

- 도구가 호출이 안될때가 많음 -> 프롬프트와 지침으로 도구사용을 유도할 수 있도록 잘 작성해야할 것 같아요

- 다른 데이터로 명령을 수행함 -> ai가 상상해서 행동하지않도록 좀더 자세한 예시와 명령을 해야할 것 같아요

 

 

 

 

 

 

 

 

 

반응형