基于 SwiftUI 开发 iOS Ollama 客户端实践
一、背景与动机
因行业特殊性,许多敏感数据需要使用大模型进行处理和分析,无法使用公有云上的各种模型。因此,在公司内部部署了 Ollama 来跑本地模型解决问题。
此时面临一个问题:手机端需要访问 Ollama API,但市面上缺乏趁手的 App。为了解决这一痛点,决定使用 Swift + SwiftUI 开发一个原生客户端。
二、功能设计
2.1 Ollama 服务配置
支持以下配置项:
- 服务地址(URL)
- 使用的模型名称
- 系统 Prompt(System Prompt)
- 模型参数(Temperature, TopP 等,预留扩展)
2.2 Ollama API 流式调用
为了提升用户体验,采用流式调用方式。每次调用只返回一部分结果,客户端不断读取并实时渲染,直到全部返回。
2.3 会话管理
- 支持创建新会话
- 支持重开历史会话
- 支持选择会话列表中的历史记录继续对话
三、数据模型建模
为了方便调用和解析 JSON,首先定义核心数据模型。
3.1 聊天请求对象
struct OllamaChatRequest: Codable {
var model: String = ""
var messages: [OllamaMessage] = []
var stream: Bool = true
}
3.2 聊天响应对象
struct OllamaChatResponse: Codable {
var message: OllamaMessage
}
3.3 消息流对象
struct OllamaChatStream: Codable {
var done: Bool
var message: OllamaMessage
}
3.4 消息对象
struct OllamaMessage: Codable, Identifiable {
public static let ROLE_SYSTEM = "system"
public static let ROLE_ASSISTANT = "assistant"
public static let ROLE_USER = "user"
let id = UUID()
var role: String
var content: String
}
3.5 模型列表对象
struct OllamaModelResponse: Codable {
var models: [OllamaModel]
}
struct OllamaModel: Identifiable, Codable {
let id = UUID()
var name: String
}
3.6 服务配置对象
struct OllamaConfig: Codable {
public static let CONFIG_PATH = "config.json"
var url: String
var prompt: String
var model: String
var models: [OllamaModel]
}
四、网络层实现
iOS 端推荐使用 URLSession 配合 async/await 进行网络请求。针对流式输出,需要处理 SSE (Server-Sent Events) 格式的数据。
4.1 网络服务类
class OllamaNetworkService {
private let session: URLSession
init() {
self.session = URLSession(configuration: .default)
}
func chat(request: OllamaChatRequest, baseURL: String) async throws -> AsyncThrowingStream<String, Error> {
var components = URLComponents(string: "\(baseURL)/api/chat")!
guard let url = components.url else { throw URLError(.badURL) }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(request)
let (bytes, response) = try await session.bytes(for: request)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return bytes.lines.asyncThrowingMap { line in
// 解析 SSE 格式数据
if line.isEmpty || line == "\n" { return nil }
// 实际项目中需解析 JSON 提取 content 字段
return line
}.filter { $0 != nil }.compactMap { $0 }
}
}
4.2 错误处理
在网络请求中应包含完善的错误捕获机制,包括网络超时、JSON 解析失败、API 返回错误码等情况。
五、视图模型与 UI 实现
5.1 ViewModel
使用 ObservableObject 管理状态,监听流式数据更新。
class ChatViewModel: ObservableObject {
@Published var messages: [OllamaMessage] = []
@Published var isLoading = false
func sendMessage(_ text: String) async {
isLoading = true
defer { isLoading = false }
let userMsg = OllamaMessage(role: .ROLE_USER, content: text)
messages.append(userMsg)
do {
let assistantMsg = OllamaMessage(role: .ROLE_ASSISTANT, content: "")
messages.append(assistantMsg)
let service = OllamaNetworkService()
let request = OllamaChatRequest(model: "llama2", messages: messages)
for try await chunk in try await service.chat(request: request, baseURL: "http://localhost:11434") {
assistantMsg.content += chunk
Task { @MainActor in
messages[messages.count - 1] = assistantMsg
}
}
} catch {
print(error.localizedDescription)
}
}
}
5.2 界面布局
主界面采用 NavigationView 或 TabView 结构,左侧为会话列表,右侧为聊天内容区域。输入框固定在底部。
六、会话持久化
建议使用 CoreData 或 UserDefaults 存储会话历史。对于长文本,可考虑本地数据库存储;对于配置信息,可使用 Codable 序列化到文件。
七、安全与部署
- HTTPS: 生产环境建议配置 HTTPS,防止中间人攻击。
- 鉴权: 如果 Ollama 服务端开启了认证,需在 Header 中添加 Token。
- 构建: 由于涉及备案问题,可选择本地构建安装至 iPhone 进行测试,暂不上架 App Store。
八、总结
本方案展示了如何使用 Swift 和 SwiftUI 快速构建连接本地 Ollama 服务的移动端应用。通过流式接口优化体验,结合合理的架构设计,实现了敏感数据的本地化处理需求。后续可进一步扩展多模态支持及更复杂的提示词工程功能。


