apple/WWDC

Foundation Models 프레임워크 자세히 알아보기 (deep dive into the Foundation Models framework) -wwdc25

lgvv 2025. 7. 11. 02:01

Foundation Models 프레임워크 자세히 알아보기 (deep dive into the Foundation Models framework) -wwdc25

 

 

 

파운데이션 모델 프레임워크가 온디바이스 LLM에 대해서 직접적인 접근성과 편리한 Swift API를 제공하며 애플 생태계에서 지원하는 모든 기기에서 동작함.

 

 

세션에서 수행해야 할 일을 모델에 알릴 수 있으며, 프롬프트는 사용자의 입력을 활용하면 됨.

 

파운데이션 모델이 텍스트를 생성하는 방법과 주의해야 할 점을 자세히 알아볼 예정.

세션에서 respond(to: )를 호출하면 먼저 세션의 명령어와 프롬프트를 가져옴(프롬프트는 여기서 사용자의 입력)

그러면 텍스트가 토큰으로 변환됨.

토큰은 작은 서브스트링으로 때로는 단어이지만 보통 몇 개의 문자만으로 구성

LLM은 토큰 시퀀스를 입력으로 받고 새로운 토큰 시퀀스를 출력으로 생성.

 

Foundation Model이 정확히 어떤 토큰을 사용하는지는 신경쓰지 않아도 되는데 API가 알아서 추상화해서 처리하기 때문.

하지만 토큰을 무작위로 사용해서는 안됨.

명령어와 프롬프트에서 토큰이 증가할수록 지연시간도 늘어남

 

응답 토큰을 생성하기 전에 모델은 모든 입력 토큰을 처리해야 하기 때문임.

또한 토큰 생성에는 연산이 필요하여 출력이 길수록 생성하는데 더 많은 시간이 걸림.

 

 

LangugageModelSession은 Stateful에 속함.

모든 respond(to: ) 호출은 transcirpt에 기록됨.

transcirpt는 특정 세션의 모든 프롬프트와 응답을 포함함.

이는 디버깅할 때 유용하며 해당 내용을 UI에 표시할 수도 있음.

 

 

하지만 이 세션의 최대 크기에는 한계가 있음.

많은 요청을 생성하거나 큰 프롬프트를 입력하거나 큰 출력을 받는 경우에 컨텍스트의 한도에 도달할 수 있음.

사용 가능한 컨텍스트 크기를 세션이 초과하면 오류가 발생함.

 

이런 오류에 대해서 catch를 통해 처리할 준비가 되어 있어야 함.

 

 

특히 컨텍스트 크기를 초과할 경우에는 exccededContextWindowSize 에러를 통해 처리할 수 있음.

이렇게 할 경우 이전 기록 없이 새로운 세션이 시작됨.

 

하지만 이렇게 처리할 경우 이전 대화 내용을 잃게 되므로 좋지 않음.

 

 

 

현재의 trasnscript가 다음 세션에서도 이어지는게 경험적으로 좋음.

세션의 transcript에서 entry를 가져온 다음, 새로운 entry 배열로 압축할 수 있음.

 

영상 예시에서 게임 대화의 경우에는 세션의 transcirpt의 첫 entry인 instruction을 가져올 수 있음

최종 entry인 성공적인 최종 응답도 가져옴.

 

이 정보를 새로운 세션으로 전달하면 캐릭터는 다시 대화를 이어갈 수 있음.

 

 

그런데 세션 transcript읜 첫 entry는 초기 명령어를 포함한다는 것을 기억해야 함.

영상에서 새로운 세션에 transcript를 전달할 때 이 명령어를 포함해야 함.

transciprt에 관련 부분만 가져와 간단하면서도 효과적으로 문제를 해결할 수 있음.

 

 

하지만 간단하지 않은 경우도 존재함

entry가 더 많은 transcript가 있다고 가정.

우선 새로운 세션에 항상 transcript를 먼저 전달하며 시작하는 것이 좋음.

 

하지만 중간에 위치한 여러 entry도 대화와 관련이 있을 수 있음.

이 경우에는 transcript를 요약하는것이 더 좋음.

외부 라이브러리를 사용하거나 Foundation Models 프레임워크로 transcript의 일부를 요약할 수 있음.

 

 

같은 질문을 던져도 다른 출력이 나올 수 있는데, 그 이유는 샘플링 때문에임.

 

 

출력을 생성할 때 모델은 토큰을 한번에 하나씩 만듦.

이를 위해 모델은 특정 토큰이 선택될 가능성을 뜻하는 확률 분포를 만듦.

 

Foundation Model은 기본적으로 특정 확률 범위 내에서 토큰을 고름.

확률이 높을수록 선택할 가능성은 높지만 결국 확률적으로 다른 선택을 할 수도 있음.

 

이 과정은 생성되는 모든 토큰에 대해서 진행되며, 토큰을 선택하는 과정을 샘플링이라고 함.
Foundation Models은 기본적으로 무작위 샘플링을 채택함.

 

 

하지만 경우에 따라서는 반복이 가능한 시나리오에서 결정론적인 출력이 필요한 경우도 있음

GenerationOptions API를 통해 샘플링 메서드를 제어할 수 있음. greedy로 설정하면 결정론적 출력을 얻을 수 있음.

이렇게 설정할 경우 프롬프트에 대한 동일한 응답 및 출력을 얻을 수 있음.

그러나 이러한 옵션은 온디바이스 모델의 특정 버전에 대해서만 적용되는데, OS 업데이트의 일부로 모델이 업데이트 되면, greedy 샘플링을 사용해도 프롬프트에 의해서 다른 출력이 반환될 수 있음.

 

랜덤 샘플링의 경우에 temperature도 조정할 수 있음.

예를 들어 temperature를 0.5로 설정하면 출력만 약간 달라짐.

더 높은 값으로 설정할 경우에는 같은 프롬프트에 대해서도 상당히 다른 출력을 생성할 수 있음.

 

 

또한 프롬프트에서 사용자 입력을 가져올 때 해당 입력의 언어가 지원되지 않을수도 있음.

또한 모델이 특정 언어를 확인하는 API도 존재함.

 


LLM에서 구조화 된 응답을 얻는것은 상당히 어려움.

예상하는 특정 필드를 사용해 모델을 실행한 다음에 추출하는 파싱 코드를 추가할 수 있음.

하지만 이 방법은 관리하기 매우 어렵고 취약함.

 

 

Foundation Models에는 @Generable 이라는 API가 존재함.

 

 

 

해당 매크로는 컴파일 타임에 스키마를 생성하고 모델은 예상되는 구조를 생성하기 위해 이 스키마를 활용함.

이 매크로는 세션을 대상으로 요청할 때 자동으로 호출되는 이니셜라이저를 만듦

 

 

 

이번에는 @Generable을 활용하고자 respond에 생성할 타입을 모델에 알리는 generating 인자를 전달함.

파운데이션 모델은 @Generable 타입에 대해서 세부 정보를 모델이 학습한 특정 형식으로 프롬프트에 자동으로 포함함.

 

 

즉, Generable 타입에 어떤 필드가 있는지 모델에 알려주지 않아도 됨.

Generable 매크로는 low-level constrained decoding을 사용함.

이는 모델이 특정 스키마를 따르는 텍스트를 생성하도록 하는 기술임 (스키마는 매크로에 의해서 생성되는 스키마)

 

LLM은 토큰을 생성하며 이는 나중에 텍스트로 변경되고, Generable을 사용하면 이 텍스트가 type-safe한 방식으로 자동으로 파싱됨.

토큰은 디코딩 루프라고 하는 루프에서 생성됨.

 

 

 

constrained decoding을 사용하지 않으면 모델이 유효하지 않은 필드 이름을 생성할 수 있음.

토큰이 생성될 때마다 모델의 어휘에 포함된 모든 토큰에 대해 확률 분포가 생성됨.

 

constrained decoding은 유효하지 않은 토큰을 마스킹하여 모델은 아무 토큰을 선택할 수 있는게 아니라 스키마에 따라 유효한 토큰만 선택할 수 있게 도움.

즉, 이러한 과정으로 인해 수동으로 파싱할 필요가 없게 도와줌.

 

 

@Generable은 온디바이스 LLM으로부터 출력을 얻는 가장 효과적인 방법이며 struct 뿐만아니라 enum에서도 활용할 수 있음.

@Generable은 Int를 포함해 대부분의 Swift 타입을 지원함

 

 

특정 범위 내에 위치해야 한다면 @Guide를 활용해 범위를 지정할 수 있음.

 

 

@Guide를 활용해 배열에 딱 3개만 담기도록 할 수 있음.

참고로 @Generalble의 속성을 소스코드에서 선언된 순서대로 실행된다.

 

위의 예제에서는 name이 제일 먼저 생성되고, encounter가 제일 나중에 만들어짐.

즉, 특성 속성의 값이 다른 속성에 영향을 받는경우 순서가 중요할 수 있음.

 

전체 아웃풋이 전체 다 생성될 때까지 기다리지 않고 속성별로도 스트리밍 가능함.

 

 

 

특정 경우에는 @Guide에 description을 통해 설명을 추가할 수 있음.

이렇게 할 경우에 프로프트에 여러가지 설명을 추가하는 대신 직접 @Generable 타입에 바로 설명할 수 있음.

이렇게 하면 각 description이 어떻게 연관되어 있는지 모델이 이해할 수 있음.

 

 

 

Guide를 활용하면 여러 형태로 제한할 수 있음.

특히 정규 표현식을 활용한 가이드는 강력한데, Foundation Models에서는 정규 표현식 패턴으로 응답 값을 정의할 수 있음.

 

 

정규 표현식을 통해 이렇게 적용할 수도 있고, 경우에 따라서는 Regex Builder Syntax를 활용할 수 있음.

 

 

 

구조화 된 출력의 안정성을 얻을 수 있고, 파싱할 필요도 없음

 

 

@Generable은 특히 컴파일 타임에 구조를 알고 있을 때 유용함.

매크로가 스키마를 생성해주며 출력으로 타입의 인스턴스를 얻게 됨.

 


컴파일 타임에 @Generable을 알게 된다면 Generable struct를 정의하면 됨.
하지만, 런타임 시점에 구조를 알게 되는 경우도 존재하는데, Dynamic schemas가 이때 도움이 됨.

DynamicGenerationSchema를 사용해 런타임에 스키마를 만들 수 있음

 

컴파일 시점에 정의된 struct처럼 동적 스키마에는 Property 목록이 존재함.

String, 배열을 추가할 수 있고, 경우에 따라서는 다른 동적 스키마에 대한 참조를 포함할 수도 있음.

그러므로 런타임에 정의되는 맞춤형 스키마를 참조할 수도 있음.

 

 

riddle의 첫 속성은 String 타입은 question이고

riddle의 두번째 속성은 customType의 answers 속성인 Answer임.


answerBuilder 대한 부분에서 answers의 속성은 이름에 따라서 Answer 스키마를 참조함.

각 동적 스키마는 독립적이므로, riddle 스키마는 실제로 Answer의 동적 스키마를 포함하지 않음.

 

추론을 시작하기 전에 먼저 동적 스키마를 유효한 스키마로 변환해야 함, 타입 참조와 같은 불일치가 동적 스키마에 존재한다면 오류가 발생할 수 있음.
즉, @Generable에서 타입이 다른 경우 해당 값이 샘플링에 의해 마스킹되어 선택되지 않듯이 타입이 불일치하여 원하는 정보가 선택되지 않는다는 의미

 

 

유효한 스키마가 존재하면 평소처럼 세션을 실행하면 됨.

여기서 중요한 점은 출력 타입이 GeneratedContent 인스턴스로 이 인스턴스는 동적 값을 포함함.

또한 이 값은 동적 스키마의 속성 이름으로 쿼리를 수행할 수 있음.

 

가이드 생성을 이용해 출력이 스키마와 같은지 확인함.

즉, 동적 스키마를 사용해도 스키마의 값이 불일치 할 경우에는 만들지 않고 해당 값을 개발자가 수동으로 파싱할 필요가 없음

(위에 언급했듯이 샘플링 시 마스킹 되어서 값이 일치하지 않으면 추출되지 않음)

 

 

 

 

@Generable은 컴파일 타임에 정의하여 정해진 타입으로 받아볼 수 있음

런타임에 변경이 필요한 부분은 Dynamic schema를 활용해 생성된 컨텐츠를 기반으로 쿼리를 통해 값을 사용할 수 있음.

 

 

 

Tool calling은 개발자가 만든 함수를 모델이 호출하게 하는 기능임

다운로드 가능 컨텐츠(DLC)를 만들어 더 개인화 된 개인 경험을 생성하고자 함. 

Tool calling을 사용하면 모델이 자율적으로 정보를 가져올 수 있음.

 

 

Tool을 정의하는건 간닪나데, Tool 프로토콜을 사용하면 됨.

먼저 name과 description을 선언하는데, 이 내용은 API가 프롬프트에 자동으로 입력함.

모델이 언제, 얼마나 자주 Tool을 호출하는 것이 좋을지 판단하게 됨.

 

짧으면서 읽기 쉬운 영어 이름을 Tool에 사용할 것.

약어는 피하고, 너무 긴 설명도 쓰지 말 것.

구현 방식도 설명하지 말 것.

 

이 문자열은 프롬프트에 그대로 입력되기 때문에 문자열이 길수록 토큰 수가 증가해 지연 시간이 더 길어질 수 있음.

 

 

대신 이름에 동사를 사용하고 설명은 한 문장으로 간결하게 작성하기.

그리고 여러 가지로 다른 값을 주어서 어떤 값이 최적의 결과를 나타내는지 확인하는 것도 필요함

 

 

 

다음으로는 Tool에 Arguements를 정의함

@Generable을 사용해 모델이 Tool을 호출할 때 유효한 입력값을 줄 수 있음

 

 

모델이 Tool을 호출할 때 call 메서드를 부르는데 외부에서는 이걸 전달 받아서 사용할 수 있음.

Tool을 사용하기 위해서는 session의 이니셜라이저로 전달할 수 있음.

추가 정보가 필요할 때 모델이 Tool을 호출함.

 

 

Tool을 class로 바꾸면 데이터를 저장할 수 있고 call 메서드에서 상태를 바꿀 수 있음.

 

 

좌측 이미지처럼 Tool을 사용할 때는 해당 형태로 프로세스를 따름.

우측 이미지처럼 경우에 따라서는 Tool은 한번의 요청에서 여러번 호출할 수 있고 이때는 병렬로 호출될 수 있음.

여러번 호출하는 것의 경우에는 컨텍스트에 따라 결정됨

 

 

요약하자면 Tool calling은 모델이 요청중에 외부 데이터에 접근하기 위해 코드를 호출할 수 있도록 함.

이 데이터는 연락처와 같은 개인정보나 웹 소스에서 가져온 외부 데이터일 수 있음.

하나의 요청에서 Tool calling을 여러번 수행할 수 있고, 그 경우에는 병렬로 호출될 수 있으면 class를 활용해 상태를 저장할 수 있음.

모델은 컨텍스트에 따라 호출 여부를 결정함.

 

 

 

(참고)

https://developer.apple.com/kr/videos/play/wwdc2025/301/