Skip to content

Latest commit

ย 

History

History
335 lines (274 loc) ยท 9.06 KB

File metadata and controls

335 lines (274 loc) ยท 9.06 KB

Foundation Models AI Reference

์˜จ๋””๋ฐ”์ด์Šค AI/LLM ๊ตฌํ˜„ ๊ฐ€์ด๋“œ. ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  Foundation Models๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐœ์š”

Foundation Models๋Š” iOS 26+์—์„œ ์˜จ๋””๋ฐ”์ด์Šค AI ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. Apple Intelligence๋ฅผ ํ™œ์šฉํ•ด ํ”„๋ผ์ด๋ฒ„์‹œ๋ฅผ ๋ณดํ˜ธํ•˜๋ฉด์„œ ํ…์ŠคํŠธ ์ƒ์„ฑ, ์š”์•ฝ, ๋„๊ตฌ ์‚ฌ์šฉ ๋“ฑ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์ˆ˜ Import

import FoundationModels

ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ

1. LanguageModelSession (์„ธ์…˜ ์ƒ์„ฑ)

// ๊ธฐ๋ณธ ์„ธ์…˜
let session = LanguageModelSession()

// ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ํฌํ•จ
let session = LanguageModelSession(
    instructions: "๋‹น์‹ ์€ ์นœ์ ˆํ•œ ์š”๋ฆฌ ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ์–ด๋กœ ๋‹ต๋ณ€ํ•˜์„ธ์š”."
)

2. ํ…์ŠคํŠธ ์ƒ์„ฑ

// ๋‹จ์ˆœ ์ƒ์„ฑ
let response = try await session.respond(to: "ํŒŒ์Šคํƒ€ ๋ ˆ์‹œํ”ผ ์•Œ๋ ค์ค˜")
print(response.content)

// ์ŠคํŠธ๋ฆฌ๋ฐ
for try await partial in session.streamResponse(to: "ํŒŒ์Šคํƒ€ ๋ ˆ์‹œํ”ผ ์•Œ๋ ค์ค˜") {
    print(partial.content, terminator: "")
}

3. Tool (๋„๊ตฌ) ์ •์˜

@Generable
struct WeatherTool: Tool {
    static let name = "weather"
    static let description = "๋„์‹œ์˜ ํ˜„์žฌ ๋‚ ์”จ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค"
    
    struct Arguments: Codable, Sendable {
        @Guide(description: "๋„์‹œ ์ด๋ฆ„ (์˜ˆ: ์„œ์šธ, ๋ถ€์‚ฐ)")
        let city: String
    }
    
    func call(arguments: Arguments) async throws -> String {
        // ์‹ค์ œ ๋‚ ์”จ API ํ˜ธ์ถœ ๋˜๋Š” ์‹œ๋ฎฌ๋ ˆ์ด์…˜
        return "\(arguments.city)์˜ ํ˜„์žฌ ๋‚ ์”จ: ๋ง‘์Œ, 23ยฐC"
    }
}

์ „์ฒด ์ž‘๋™ ์˜ˆ์ œ: AI ์ฑ—๋ด‡

import SwiftUI
import FoundationModels

// MARK: - Message Model
struct ChatMessage: Identifiable {
    let id = UUID()
    let role: Role
    let content: String
    let timestamp: Date
    
    enum Role {
        case user, assistant
    }
}

// MARK: - ViewModel
@Observable
class ChatViewModel {
    var messages: [ChatMessage] = []
    var inputText = ""
    var isLoading = false
    
    private var session: LanguageModelSession?
    
    init() {
        setupSession()
    }
    
    private func setupSession() {
        session = LanguageModelSession(
            instructions: """
            ๋‹น์‹ ์€ ์นœ์ ˆํ•˜๊ณ  ๋„์›€์ด ๋˜๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์ž…๋‹ˆ๋‹ค.
            ๊ฐ„๊ฒฐํ•˜๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
            ํ•œ๊ตญ์–ด๋กœ ๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค.
            """
        )
    }
    
    func sendMessage() async {
        let text = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !text.isEmpty else { return }
        
        // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
        let userMessage = ChatMessage(role: .user, content: text, timestamp: Date())
        messages.append(userMessage)
        inputText = ""
        isLoading = true
        
        do {
            // AI ์‘๋‹ต ์ƒ์„ฑ
            let response = try await session?.respond(to: text)
            let assistantMessage = ChatMessage(
                role: .assistant,
                content: response?.content ?? "์‘๋‹ต์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
                timestamp: Date()
            )
            messages.append(assistantMessage)
        } catch {
            let errorMessage = ChatMessage(
                role: .assistant,
                content: "์˜ค๋ฅ˜: \(error.localizedDescription)",
                timestamp: Date()
            )
            messages.append(errorMessage)
        }
        
        isLoading = false
    }
    
    func clearHistory() {
        messages.removeAll()
        setupSession()  // ์„ธ์…˜ ์ดˆ๊ธฐํ™”
    }
}

// MARK: - View
struct ChatView: View {
    @State private var viewModel = ChatViewModel()
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก
                ScrollViewReader { proxy in
                    ScrollView {
                        LazyVStack(spacing: 12) {
                            ForEach(viewModel.messages) { message in
                                MessageBubble(message: message)
                            }
                            
                            if viewModel.isLoading {
                                ProgressView()
                                    .padding()
                            }
                        }
                        .padding()
                    }
                    .onChange(of: viewModel.messages.count) {
                        if let lastMessage = viewModel.messages.last {
                            proxy.scrollTo(lastMessage.id, anchor: .bottom)
                        }
                    }
                }
                
                Divider()
                
                // ์ž…๋ ฅ ํ•„๋“œ
                HStack(spacing: 12) {
                    TextField("๋ฉ”์‹œ์ง€ ์ž…๋ ฅ...", text: $viewModel.inputText)
                        .textFieldStyle(.roundedBorder)
                    
                    Button {
                        Task {
                            await viewModel.sendMessage()
                        }
                    } label: {
                        Image(systemName: "arrow.up.circle.fill")
                            .font(.title)
                    }
                    .disabled(viewModel.inputText.isEmpty || viewModel.isLoading)
                }
                .padding()
            }
            .navigationTitle("AI ์ฑ—๋ด‡")
            .toolbar {
                Button("์ดˆ๊ธฐํ™”") {
                    viewModel.clearHistory()
                }
            }
        }
    }
}

struct MessageBubble: View {
    let message: ChatMessage
    
    var body: some View {
        HStack {
            if message.role == .user { Spacer() }
            
            Text(message.content)
                .padding(12)
                .background(message.role == .user ? Color.blue : Color.gray.opacity(0.2))
                .foregroundStyle(message.role == .user ? .white : .primary)
                .clipShape(RoundedRectangle(cornerRadius: 16))
            
            if message.role == .assistant { Spacer() }
        }
    }
}

#Preview {
    ChatView()
}

Tool ์‚ฌ์šฉ ์˜ˆ์ œ

// ์—ฌ๋Ÿฌ ๋„๊ตฌ ์ •์˜
@Generable
struct CalculatorTool: Tool {
    static let name = "calculator"
    static let description = "์ˆ˜ํ•™ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค"
    
    struct Arguments: Codable, Sendable {
        @Guide(description: "๊ณ„์‚ฐ์‹ (์˜ˆ: 2 + 2, 10 * 5)")
        let expression: String
    }
    
    func call(arguments: Arguments) async throws -> String {
        // ๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ ๋กœ์ง
        let expr = NSExpression(format: arguments.expression)
        if let result = expr.expressionValue(with: nil, context: nil) as? NSNumber {
            return "๊ฒฐ๊ณผ: \(result)"
        }
        return "๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    }
}

@Generable
struct SearchTool: Tool {
    static let name = "search"
    static let description = "์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค"
    
    struct Arguments: Codable, Sendable {
        @Guide(description: "๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ")
        let query: String
    }
    
    func call(arguments: Arguments) async throws -> String {
        // ์‹ค์ œ๋กœ๋Š” API ํ˜ธ์ถœ
        return "'\(arguments.query)'์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค..."
    }
}

// ๋„๊ตฌ์™€ ํ•จ๊ป˜ ์„ธ์…˜ ์ƒ์„ฑ
let session = LanguageModelSession(
    instructions: "๋„๊ตฌ๋ฅผ ์ ๊ทน ํ™œ์šฉํ•ด ์‚ฌ์šฉ์ž๋ฅผ ๋„์™€์ฃผ์„ธ์š”.",
    tools: [WeatherTool(), CalculatorTool(), SearchTool()]
)

// ๋„๊ตฌ ํ˜ธ์ถœ์ด ํ•„์š”ํ•œ ์งˆ๋ฌธ
let response = try await session.respond(to: "์„œ์šธ ๋‚ ์”จ ์–ด๋•Œ?")
// AI๊ฐ€ ์ž๋™์œผ๋กœ WeatherTool ํ˜ธ์ถœ

์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต

@Observable
class StreamingViewModel {
    var streamedText = ""
    var isStreaming = false
    
    func streamResponse(prompt: String) async {
        let session = LanguageModelSession()
        isStreaming = true
        streamedText = ""
        
        do {
            for try await partial in session.streamResponse(to: prompt) {
                streamedText += partial.content
            }
        } catch {
            streamedText = "์˜ค๋ฅ˜: \(error.localizedDescription)"
        }
        
        isStreaming = false
    }
}

struct StreamingView: View {
    @State var viewModel = StreamingViewModel()
    
    var body: some View {
        VStack {
            ScrollView {
                Text(viewModel.streamedText)
                    .padding()
            }
            
            Button("์ƒ์„ฑ ์‹œ์ž‘") {
                Task {
                    await viewModel.streamResponse(prompt: "์ธ๊ณต์ง€๋Šฅ์˜ ์—ญ์‚ฌ๋ฅผ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”")
                }
            }
            .disabled(viewModel.isStreaming)
        }
    }
}

์ฃผ์˜์‚ฌํ•ญ

  1. iOS 26+ ์ „์šฉ: ์ด์ „ ๋ฒ„์ „์—์„œ๋Š” ์‚ฌ์šฉ ๋ถˆ๊ฐ€
  2. Apple Silicon ํ•„์š”: ์˜จ๋””๋ฐ”์ด์Šค AI๋Š” Neural Engine ํ•„์š”
  3. ํ”„๋ผ์ด๋ฒ„์‹œ: ๋ฐ์ดํ„ฐ๊ฐ€ ๊ธฐ๊ธฐ๋ฅผ ๋– ๋‚˜์ง€ ์•Š์Œ
  4. Sendable ์ค€์ˆ˜: Tool Arguments๋Š” Sendable ํ•„์ˆ˜
  5. @Generable ๋งคํฌ๋กœ: Tool ์ •์˜ ์‹œ ํ•„์ˆ˜

๊ฐ€์šฉ์„ฑ ํ™•์ธ

if LanguageModelSession.isAvailable {
    // Foundation Models ์‚ฌ์šฉ ๊ฐ€๋Šฅ
} else {
    // ๋Œ€์ฒด ๋กœ์ง (์˜ˆ: ์„œ๋ฒ„ API)
}