2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Apple IntelligenceとSwiftDataだけで「ドキュメントと会話できる」ローカルLLMチャットアプリを開発してみた

2
Posted at

Apple Intelligence × SwiftData × RAG (Retrieval-Augmented Generation)

ローカルPCのドキュメント(PDF/Text)と直接会話でき、プライバシー完全保護のネイティブmacOSアプリ開発の記録です。

※この記事は、一部AIを活用して内容を整理しています。

はじめに

AppleがmacOS 26.0で公開したFoundationModelsフレームワークは、Apple Silicon上でローカルに動作する大規模言語モデル(LLM)への直接アクセスを提供する画期的なAPIです。

このブログでは、RAG(Retrieval-Augmented Generation)機能を搭載したFoundationLLMChatというネイティブmacOSアプリの開発全記録をお届けします。

このアプリの核心機能

「あなたのドキュメントと直接会話する」

  • PDFやテキストファイルをアプリにインポート
  • ドキュメントの内容に関して質問すると、LLMが文脈を理解して回答
  • すべてローカル処理(インターネット不要、プライバシー完全保護)
  • Apple Silicon上で高速動作

技術的なアーキテクチャ、RAGシステムの実装、実装上の課題と解決策について詳しく解説します。


プロジェクト概要

FoundationLLMChatは、あなたのドキュメントと「会話」できる次世代チャットアプリケーションです。

主要機能

機能 説明
RAGドキュメントチャット PDF/Textファイルをインポートして、その内容について質問・回答
Apple Intelligence macOS 26.0+のローカルLLM(インターネット不要)
SwiftData永続化 会話履歴とドキュメントを自動保存
キーワードベースRAG 独自実装の検索・文脈構築システム
PDF対応 PDFKitによるテキスト抽出
Markdownレンダリング コードブロック・リスト・テーブル対応

技術スタック

コンポーネント 技術
UIフレームワーク SwiftUI
データ永続化 SwiftData
LLM推論 FoundationModels (Apple Intelligence)
PDF処理 PDFKit
並行処理 async/await + MainActor

アーキテクチャ設計

MVVMパターンの採用

本アプリでは**MVVM(Model-View-ViewModel)**アーキテクチャを採用しています。これにより、UIロジックとビジネスロジックを明確に分離し、テスト容易性と保守性を高めています。

View (SwiftUI) → ViewModel → Service → Apple Frameworks

アーキテクチャ図

image.png

アーキテクチャ図では、以下の5つのレイヤー構成を視覚化しています。

  • UI Layer (SwiftUI): ChatView, MessageBubble, MessageInputView, MarkdownView
  • ViewModel Layer: ChatViewModel (MainActor) - 状態管理
  • Service Layer: FoundationModelService, RAGService
  • Data Layer (SwiftData): Conversation, ChatMessage, Document, DocumentChunk
  • Apple Frameworks: FoundationModels, SwiftData, PDFKit

レイヤー構成

1. UI Layer (SwiftUI)

  • ChatView: メインチャット画面
  • MessageBubble: 吹き出し形式のメッセージ表示
  • MessageInputView: メッセージ入力欄
  • MarkdownView: Markdownレンダリング

2. ViewModel Layer

  • ChatViewModelMainActor): 状態管理とビジネスロジックの調整
    • 会話管理
    • メッセージ送受信
    • RAG制御
    • エラーハンドリング

3. Service Layer

  • FoundationModelService: Apple Intelligence APIラッパー
  • RAGService: ドキュメント検索・文脈構築

4. Data Layer (SwiftData)

  • Conversation: 会話エンティティ
  • ChatMessage: メッセージエンティティ
  • Document: ドキュメントエンティティ
  • DocumentChunk: チャンク化されたドキュメント断片

5. Apple Frameworks

  • FoundationModels: LLM推論
  • SwiftData: データ永続化
  • PDFKit: PDFテキスト抽出

RAG(Retrieval-Augmented Generation)システム

RAGとは

RAGは、LLMに外部知識を提供する手法です。ユーザーの質問に関連するドキュメントの断片を検索し、プロンプトに追加することで、より正確で文脈に沿った回答を生成できます。

本アプリのRAG実装

ドキュメント処理フロー

PDF/Text → テキスト抽出 → チャンク分割 → キーワード抽出 → SwiftData保存

チャンク化アルゴリズム:

func chunkDocument(chunkSize: Int = 1000, overlap: Int = 200) -> [DocumentChunk] {
    var chunks: [DocumentChunk] = []
    let text = content
    var startIndex = text.startIndex

    while startIndex < text.endIndex {
        let endIndex = text.index(startIndex, offsetBy: chunkSize, limitedBy: text.endIndex) ?? text.endIndex
        let chunkText = String(text[startIndex..<endIndex])

        let chunk = DocumentChunk(content: chunkText, index: chunks.count, document: self)
        chunks.append(chunk)

        // オーバーラップを考慮して次の開始位置を設定
        if endIndex == text.endIndex {
            break
        }
        startIndex = text.index(startIndex, offsetBy: chunkSize - overlap, limitedBy: text.endIndex) ?? text.endIndex
    }

    return chunks
}

検索アルゴリズム

キーワードベースの検索を実装しています。

func retrieveRelevantChunks(query: String, documents: [Document], maxChunks: Int = 3) -> [DocumentChunk] {
    let queryWords = query.lowercased()
        .components(separatedBy: CharacterSet.alphanumerics.inverted)
        .filter { $0.count > 2 }

    var scoredChunks: [(chunk: DocumentChunk, score: Int)] = []

    for document in documents {
        for chunk in document.chunks ?? [] {
            var score = 0
            let chunkContent = chunk.content.lowercased()

            // 完全一致に高いスコア
            if chunkContent.contains(query.lowercased()) {
                score += 10
            }

            // 単語ごとの一致
            for word in queryWords {
                if chunkContent.contains(word) {
                    score += 1
                }
            }

            // キーワードリストとの一致
            for word in queryWords {
                if chunk.keywords?.contains(word) == true {
                    score += 2
                }
            }

            if score > 0 {
                scoredChunks.append((chunk, score))
            }
        }
    }

    // スコア順にソートして上位を返す
    let sortedChunks = scoredChunks.sorted { $0.score > $1.score }
    return Array(sortedChunks.prefix(maxChunks).map { $0.chunk })
}

スコアリング体系:

  • 完全一致: +10点
  • 単語一致(コンテンツ内): +1点
  • キーワードリスト一致: +2点

処理フロー図

image.png

RAG + メッセージ処理のフローは以下の通りです。

  1. ユーザー入力を受信
  2. RAGが有効かチェック
  3. Yesの場合:retrieveRelevantChunks() → buildContextFromChunks()
  4. buildPromptWithRAG()でプロンプト構築
  5. ユーザーメッセージを保存
  6. アシスタントプレースホルダーを作成
  7. generateResponse()でLLM推論
  8. 成功時:コンテンツを更新・保存
  9. 失敗時:エラーハンドリング

ユーザーシーケンス

image.png

以下に、ドキュメントインポートからLLM応答取得までのシーケンスを示します。

Phase 1: ドキュメントインポート (1-6)

  1. ユーザーがPDF/Textファイルを選択
  2. UI LayerがViewModelに通知
  3. ViewModelがドキュメントをチャンク化
  4. チャンクがSwiftDataに保存
  5. UIが更新されドキュメント一覧を表示

Phase 2: メッセージ送信とRAG (7-12)

  1. ユーザーがメッセージを送信
  2. ViewModelがメッセージを保存
  3. RAG Serviceが関連チャンクを検索
  4. トップ3チャンクが返却
  5. プロンプト(RAGコンテキスト含む)をLLMに送信
  6. LLM応答が返却されUIに表示

実装上の課題と解決策

課題1: FoundationModelsの可用性チェック

FoundationModelsフレームワークはmacOS 26.0+とApple Siliconが必要です。

解決策: 条件付きコンパイル

#if canImport(FoundationModels)
import FoundationModels
// 実際の実装
#else
// フォールバック実装
class FoundationModelService {
    @Published var availabilityStatus: ModelAvailabilityStatus = .unavailable(
        reason: "FoundationModels framework not available. Requires macOS 26.0+ and Apple Silicon."
    )
}
#endif

課題2: SwiftDataのモデル関係

ConversationChatMessageの1対多関係を正しく設定する必要がありました。

解決策: RelationshipとInverseの設定

@Model
class Conversation {
    @Relationship(deleteRule: .cascade, inverse: \ChatMessage.conversation)
    var messages: [ChatMessage]?
}

@Model
class ChatMessage {
    @Relationship
    var conversation: Conversation?
}

deleteRule: .cascadeを設定することで、会話削除時に関連メッセージも自動削除されます。

課題3: メインスレッドでのUI更新

SwiftUIの@Publishedプロパティはメインスレッドでのみ更新する必要があります。

解決策: @MainActorの適用

@MainActor
class ChatViewModel: ObservableObject {
    @Published var isGenerating: Bool = false
    @Published var errorMessage: String?
    // ...
}

課題4: 非同期ストリーミング応答の処理

FoundationModelsは現時点でストリーミング応答を直接サポートしていないため、逐次更新の実装が課題でした。

解決策: プレースホルダー方式

// まず空のプレースホルダーを作成
let assistantMessage = ChatMessage(role: .assistant, content: "", conversation: conversation)
assistantMessage.isStreaming = true
context.insert(assistantMessage)

try await context.save()

// 非同期で応答を生成
Task {
    let response = try await modelService.generateResponse(for: prompt)

    // 応答を更新
    assistantMessage.content = response
    assistantMessage.isStreaming = false
    try context.save()
}

パフォーマンス最適化

RAGの最適化

チャンクサイズのチューニングについては、1000文字のチャンクサイズと200文字のオーバーラップを採用しています。これにより文脈の連続性を保ちつつ、検索精度を向上させています。

検索結果の制限については、トップ3チャンクのみを使用することで、プロンプトサイズを抑えレイテンシを削減しています。

データベース最適化

// 会話履歴は最新10件のみをプロンプトに含める
let recentMessages = conversation.sortedMessages.suffix(10)

デモ動画

FoundationLLMChatの主要機能を紹介するデモ動画です。

demo.gif


今後の展望

Github上で公開

現在リポジトリを準備中のため、近い将来に公開する予定です。

計画中の機能

セマンティック検索では、現在のキーワードベースから埋め込みベースの検索への移行を予定しています。CoreMLを活用したローカル埋め込み生成により、より精度の高い検索を実現する予定です。

ストリーミング対応については、FoundationModelsのストリーミングAPIが公開された際に速やかに対応していきます。

マルチモーダル対応では、画像入力への対応を検討しています。

エクスポート機能として、会話履歴のMarkdown/PDF出力も実装予定です。

まとめ

FoundationLLMChatは、Appleの最新技術(FoundationModels、SwiftData、SwiftUI)を組み合わせることで、プライバシーを完全に保ちながら強力なLLM機能を提供するアプリケーションです。

RAG機能により、ユーザーのドキュメントを文脈として活用できるため、単なるチャットアプリを超えた個人の知識ベースと対話できるツールとなっています。

学んだこと

  1. SwiftDataのパワー: 宣言的なデータモデリングと自動永続化により、複雑なデータ管理をシンプルに記述できます
  2. FoundationModelsの可能性: ローカル推論によるプライバシー保護と高速応答を両立できます
  3. MVVMの重要性: 状態管理を明確に分離することで、保守性が大きく向上します
  4. RAGの実装: キーワードベース検索でも、チューニング次第で十分実用的な精度を実現できます

参考リンク


本ブログは2026年3月に作成されました。FoundationModels APIはベータ版のため、今後変更される可能性があります。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?