Swift开发者必备:OpenAIKit客户端集成与API调用实战指南
1. 项目概述:为什么我们需要一个 Swift 版的 OpenAI 客户端?
如果你是一名 iOS 或 macOS 开发者,最近肯定没少和 OpenAI 的 API 打交道。无论是想给 App 加个智能对话功能,还是集成一个图片生成模块,OpenAI 提供的 API 都是目前最直接、最强大的选择。但官方只提供了 Python 和 Node.js 的 SDK,在 Swift 项目里直接调用,意味着你得自己处理网络请求、JSON 编解码、错误处理等一系列繁琐的底层工作。这就像你想做一顿大餐,却得先从种菜开始。
OpenAIKit的出现,就是为了解决这个痛点。它是一个纯 Swift 编写的开源库,封装了与 OpenAI API 交互的所有细节。简单来说,它让你能用几行 Swift 代码,就完成之前可能需要上百行才能搞定的 API 调用。从创建聊天对话、生成文本补全,到调用 DALL·E 生成图片,再到处理语音转文本,它都提供了类型安全、符合 Swift 习惯的接口。对于 Swift 开发者而言,这不仅仅是节省了时间,更重要的是,它让集成 AI 功能变得和调用本地框架一样自然和可靠。
2. 核心设计思路与架构解析
2.1 设计哲学:简洁、安全与类型安全
OpenAIKit的设计遵循了几个核心原则,这也是它区别于简单网络封装器的关键。
首先,简洁性。它的 API 设计高度模仿了 OpenAI 官方 Python SDK 的命名和结构,降低了学习成本。例如,创建一个文本补全,你只需要调用openAIClient.completions.create(...),直观明了。
其次,安全性。项目 README 开篇就强调了 API Key 的安全问题。它强烈建议通过环境变量注入密钥,而非硬编码在源码中。这不仅仅是“最佳实践”,而是必须遵守的准则。因为 OpenAI 的 API Key 权限极高,一旦泄露,攻击者可以查看账单、滥用额度甚至操控你的组织设置。OpenAIKit本身不存储密钥,它只是从你提供的Configuration对象中读取,这迫使开发者必须思考密钥的管理策略。
第三,类型安全与现代化。库全面拥抱 Swift 的现代并发特性(async/await),所有网络请求都是异步的。同时,它利用 Swift 的强类型系统,将 API 的请求参数和响应模型都定义成了结构体(struct)。这意味着你在编码时就能获得编译器的帮助:传错了参数类型、漏了必填字段,编译器都会提前报错,而不是等到运行时才发现 API 返回了一个模糊的错误。
2.2 底层网络抽象:支持 SwiftNIO 与 URLSession 双引擎
这是OpenAIKit一个非常务实的设计。它没有把自己绑定在某个特定的网络库上,而是抽象出了一套协议,同时支持两种主流的 Swift 网络客户端。
SwiftNIO 模式:这是服务端 Swift 项目(如 Vapor)的标配。
OpenAIKit使用AsyncHTTPClient这个基于 SwiftNIO 的库。它的优势在于高性能和高并发,特别适合在服务端环境中处理大量请求。文档里特意提到了一个关键点:通常建议为整个应用生命周期创建一个共享的HTTPClient,并在应用关闭时优雅地关闭它。这是因为创建和销毁 HTTP 客户端是有开销的,复用客户端可以提升性能。代码示例中的defer块和syncShutdown()就是确保资源正确释放的“标准操作”。URLSession 模式:这是苹果平台(iOS, macOS 等)的原生网络库。对于绝大多数客户端 App 来说,直接使用
URLSession是最简单、最无需引入额外依赖的选择。OpenAIKit通过一个不同的初始化方法,让你可以传入自定义的URLSession实例。这给了客户端开发者极大的灵活性,你可以配置缓存策略、超时时间、Cookie 存储等,与 App 现有的网络层无缝集成。
这种双引擎设计,体现了库作者对 Swift 全栈开发生态的深刻理解,让同一个工具能在服务器和客户端场景下都游刃有余。
2.3 模块化覆盖:从 GPT 到 DALL·E,再到语音
根据 README 中的清单,OpenAIKit几乎覆盖了 OpenAI API 的所有核心端点(Endpoint)。我们可以将其分为几大类:
核心语言模型:
- Completions(补全):经典的文本生成接口,对应 GPT-3 系列模型(如
text-davinci-003)。你给一段提示(Prompt),它帮你补全后面的内容。 - Chat(聊天):这是目前最流行的接口,对应 GPT-3.5-Turbo 和 GPT-4 模型。它使用基于消息(
system,user,assistant)的对话格式,能处理多轮上下文,是构建聊天机器人的基础。 - Edits(编辑):根据指令修改文本,例如“将这段文字翻译成法语并使其更正式”。
- Completions(补全):经典的文本生成接口,对应 GPT-3 系列模型(如
多模态能力:
- Images(图像):集成 DALL·E 模型,根据文字描述生成图像,或对已有图像进行编辑、生成变体。
- Speech to text(语音转文本):集成 Whisper 模型,将音频文件转换为文字。这对于构建语音助手、会议记录等应用至关重要。
辅助与基础设施:
- Embeddings(嵌入):将文本转换为高维向量。这是构建语义搜索、文本分类、推荐系统等高级 AI 应用的基石。
- Moderations(审核):检查文本内容是否违反 OpenAI 的使用政策,用于构建安全的内容过滤器。
- Models(模型):查询可用模型列表,获取模型详情。
- Files(文件):管理上传到 OpenAI 的文件,主要用于微调(Fine-tuning)任务。
注意:清单中显示
Fine-tunes(微调)和Function calling(函数调用)尚未实现。如果你需要这些前沿或高级功能,可能需要等待库更新,或考虑自行扩展。不过,对于绝大多数应用场景(聊天、生成、嵌入、审核),OpenAIKit已经提供了完备的支持。
3. 从零开始:在项目中集成与基础使用
3.1 环境准备与依赖管理
假设你正在开发一个 iOS App,我们来看看如何一步步集成OpenAIKit。
第一步:创建项目与添加依赖如果你使用 Xcode 的 Swift Package Manager(SPM)来管理依赖(这是现在的主流方式),操作非常简单:
- 在 Xcode 中打开你的项目,点击项目导航器中的项目文件。
- 选择你的 App Target,切换到“Package Dependencies”选项卡。
- 点击“+”按钮,在搜索框中粘贴仓库地址:
https://github.com/dylanshine/openai-kit.git。 - Xcode 会自动获取包信息。在“Dependency Rule”中,通常选择“Up to Next Major Version”并填写
1.0.0,这表示允许自动更新到2.0.0以下的所有版本。 - 点击“Add Package”,Xcode 会解析依赖。完成后,在弹窗中勾选
OpenAIKit库,将其添加到你的 Target 中。
如果你用的是服务端项目,在Package.swift文件中添加依赖,正如 README 所示:
dependencies: [ .package(url: "https://github.com/dylanshine/openai-kit.git", from: "1.0.0") ], targets: [ .target(name: "YourTargetName", dependencies: [ .product(name: "OpenAIKit", package: "openai-kit"), ]), ]第二步:安全地管理 API Key绝对不要将 API Key 写在代码里!我们采用环境变量注入的方式。 对于 iOS 开发,一个常见且安全的方法是使用 Xcode 的配置(Configuration)和模式(Scheme):
- 在项目导航器中,选中你的 Target,进入“Build Settings”选项卡。
- 点击“+”按钮,选择“Add User-Defined Setting”。
- 命名一个设置,例如
OPENAI_API_KEY。 - 为不同的配置(Debug/Release)设置不同的值。在 Debug 模式下,你可以填入测试用的 Key;在 Release 模式下,这个值应该留空,并通过 CI/CD 流程在构建时注入。
- 在代码中,通过
Bundle.main的infoDictionary来读取(虽然不完全等同于环境变量,但原理类似)。更推荐的方式是创建一个配置文件(如Config.plist),将其加入.gitignore,然后在代码中读取。
对于服务端项目(如 Vapor),使用.env文件是标准做法:
- 在项目根目录创建
.env文件。 - 写入你的密钥:
OPENAI_API_KEY=sk-your-actual-key-here。 - 至关重要:确保
.env文件在.gitignore中,防止意外提交。 - 在代码中,通过
ProcessInfo.processInfo.environment字典来读取,正如 README 示例所示。
3.2 客户端初始化实战
根据你的项目类型,选择不同的初始化方式。
场景一:iOS/macOS App(使用 URLSession)这是客户端应用最典型的场景。你通常会在一个单例或依赖注入容器中创建并持有OpenAIKit.Client实例。
import OpenAIKit class AIService { // 单例模式,全局共享一个客户端 static let shared = AIService() private let client: OpenAIKit.Client private init() { // 1. 安全地获取 API Key(这里以从 Info.plist 读取为例) guard let apiKey = Bundle.main.infoDictionary?["OPENAI_API_KEY"] as? String, !apiKey.isEmpty else { fatalError("请在 Info.plist 中配置 OPENAI_API_KEY") } // 2. 创建配置。组织 ID 可选,如果你属于某个 OpenAI 组织才需要。 let configuration = Configuration(apiKey: apiKey) // 3. 使用默认的 URLSession 初始化客户端 // 你也可以自定义 URLSessionConfiguration,例如设置超时时间 let session = URLSession(configuration: .default) self.client = OpenAIKit.Client(session: session, configuration: configuration) } // 后续的 API 调用方法... }场景二:Vapor 服务端应用(使用 SwiftNIO)在 Vapor 项目中,我们通常利用其依赖注入系统,在应用启动时配置客户端,并在路由处理器中使用。
// 通常在 configure.swift 中 import Vapor import OpenAIKit public func configure(_ app: Application) throws { // 1. 从环境变量读取配置 guard let apiKey = Environment.get("OPENAI_API_KEY") else { app.logger.critical("未设置 OPENAI_API_KEY 环境变量") throw Abort(.internalServerError) } let organization = Environment.get("OPENAI_ORGANIZATION") // 可选 // 2. 创建共享的 HTTPClient(遵循 Vapor 的生命周期管理) let httpClient = HTTPClient(eventLoopGroupProvider: .shared(app.eventLoopGroup)) // 在应用关闭时清理 app.http.client.shared.defer { try httpClient.syncShutdown() } // 3. 创建 OpenAI 配置和客户端 let configuration = Configuration(apiKey: apiKey, organization: organization) let openAIClient = OpenAIKit.Client(httpClient: httpClient, configuration: configuration) // 4. 将客户端注册为服务,方便在路由中获取 app.openAI = openAIClient } // 扩展 Application 以便存储客户端 extension Application { private struct OpenAIKey: StorageKey { typealias Value = OpenAIKit.Client } var openAI: OpenAIKit.Client? { get { self.storage[OpenAIKey.self] } set { self.storage[OpenAIKey.self] = newValue } } } // 在路由处理器中使用 app.get("ask") { req -> EventLoopFuture<String> in guard let openAI = req.application.openAI else { throw Abort(.internalServerError, reason: "OpenAI 服务未配置") } // 注意:在 Vapor 的 EventLoopFuture 环境中,需要使用 `flatMapThrowing` 等方式适配 async/await // 更现代的做法是使用 Vapor 4 的 `req.eventLoop.performWithTask` return req.eventLoop.performWithTask { let completion = try await openAI.completions.create(model: .gpt3(.davinci), prompts: ["Hello, world"]) return completion.choices.first?.text ?? "No response" } }实操心得:在服务端使用 SwiftNIO 模式时,务必注意
HTTPClient的生命周期管理。一个常见的错误是在每个请求中都创建新的HTTPClient,这会导致端口和内存资源迅速耗尽。最佳实践是在应用启动时创建一个共享实例,并确保在应用关闭时(或测试结束时)调用syncShutdown()进行清理。defer语句是保证这一点的好帮手。
4. 核心 API 使用详解与最佳实践
4.1 文本生成:Completions 与 Chat 的抉择
这是最常用的功能。OpenAIKit提供了completions和chat两个端点,它们有什么区别?又该如何选择?
Completions(补全):
- 对应模型:主要是 GPT-3 系列(如
text-davinci-003,text-curie-001)。GPT-4 通常不通过此端点调用。 - 交互模式:单次提示(Prompt)。你给它一段文本,它接着往下写。
- 使用场景:文案续写、代码补全、大纲生成等“单向延伸”的任务。它的接口相对简单。
let completion = try await openAIClient.completions.create( model: .gpt3(.davinci), // 指定模型 prompts: ["Once upon a time in a land far away,"], // 提示词 maxTokens: 50, // 生成的最大令牌数(约等于单词数) temperature: 0.7 // 创造性,0.0最确定,1.0最随机 ) print(completion.choices.first?.text ?? "")Chat(聊天):
- 对应模型:
gpt-3.5-turbo,gpt-4,gpt-4-turbo等。 - 交互模式:基于消息列表的对话。消息有角色之分:
system(设定助手行为)、user(用户输入)、assistant(助手历史回复)。 - 使用场景:所有需要多轮对话、上下文记忆的聊天机器人、智能客服、复杂任务分解等。这是目前的主流和推荐方式,因为性价比更高(
gpt-3.5-turbo效果强且便宜)。
let messages: [Chat.Message] = [ .system(content: "你是一位乐于助人的诗歌助手。"), .user(content: "为我写一首关于 Swift 编程语言的俳句。") ] let chatCompletion = try await openAIClient.chats.create( model: .gpt3_5Turbo, // 或 .gpt4 messages: messages, maxTokens: 100, temperature: 0.8 ) if let responseMessage = chatCompletion.choices.first?.message { print(responseMessage.content) }如何选择?
- 无脑选 Chat:对于绝大多数新的对话式应用,直接使用 Chat 端点配合
gpt-3.5-turbo或gpt-4。它设计更现代,能处理上下文,且成本通常更低。 - 考虑 Completions:如果你的任务是非常简单的“提示-补全”,且对老模型(如
text-davinci-003)有特定需求或已有大量基于此模型调优的提示词,可以继续使用。
4.2 图像生成:玩转 DALL·E
OpenAIKit的images端点让生成图像变得异常简单。核心方法是.create,它对应 DALL·E 的“根据描述生成图像”。
do { let imageResult = try await openAIClient.images.create( prompt: "A serene landscape painting of a misty mountain lake at sunrise, digital art", // 描述词 numberOfImages: 2, // 生成数量,最多10张(注意成本) size: .size1024 // 图片尺寸:.size256, .size512, .size1024 // responseFormat: .url // 返回图片URL(默认,一小时后过期) // responseFormat: .b64_json // 返回Base64编码的图片数据 ) for data in imageResult.data { if let url = data.url { print("生成的图片URL: \(url)") // 在iOS中,你可以用 Kingfisher/SDWebImage 等库加载这个URL // 例如:imageView.kf.setImage(with: url) } else if let b64Json = data.b64Json { // 处理Base64字符串,解码成Data,然后创建UIImage if let imageData = Data(base64Encoded: b64Json), let uiImage = UIImage(data: imageData) { // 更新UI DispatchQueue.main.async { self.imageView.image = uiImage } } } } } catch { print("生成图片失败: \(error)") }重要注意事项:
- 成本与速率限制:DALL·E 生成图片是按张数和分辨率收费的,
1024x1024最贵。频繁调用容易触发速率限制。务必在客户端实现适当的加载状态和错误重试机制。- 内容政策:OpenAI 有严格的图像生成内容政策。避免生成涉及名人、暴力、色情或政治敏感内容的提示词,否则请求会被拒绝。
- Base64 vs URL:
responseFormat默认为.url,返回一个临时链接(一小时有效)。这对于快速预览很方便。如果你需要永久保存图片,应该在收到 URL 后立即将其下载到自己的服务器或存储服务中。选择.b64_json会直接返回图片数据,适合需要立即在客户端显示且不想额外发起网络请求的场景,但请注意 JSON 数据量会很大。
4.3 嵌入与语义搜索:构建智能的“理解”能力
Embeddings(嵌入)是解锁高级 AI 应用的关键。它将一段文本(一个词、一句话、一篇文章)转换成一个高维向量(一组数字)。语义相似的文本,其向量在空间中的距离也更近。
一个简单的使用示例:
let response = try await openAIClient.embeddings.create( model: .textEmbeddingAda002, // 推荐使用的嵌入模型,性价比高 input: "The food was delicious and the waiter was friendly." // 可以是一个字符串,也可以是字符串数组 ) if let embedding = response.data.first?.embedding { // `embedding` 是一个 [Double] 数组,长度取决于模型(如 ada-002 是 1536 维) print("得到嵌入向量,维度:\(embedding.count)") // 你可以将这个向量存储到数据库(如 PostgreSQL 的 vector 类型,或 Redis) }实际应用场景:假设你有一个新闻 App,想要实现“搜索相关文章”的功能,而不仅仅是关键词匹配。
- 预处理(离线):将你数据库里的所有新闻文章,通过
embeddings接口转换为向量,并存储起来。 - 查询时(在线):当用户输入搜索词“一场激动人心的体育赛事”时,同样将这个查询词转换为向量。
- 计算相似度:在数据库中,计算查询向量与所有文章向量的余弦相似度(Cosine Similarity)或点积。
- 返回结果:将相似度最高的几篇文章返回给用户。即使用户的搜索词里没有“篮球”、“NBA”等具体词汇,只要文章语义上与“激动人心的体育赛事”相关,就能被检索出来。
OpenAIKit为你完成了最关键的第1步和第2步——将文本转化为机器可理解的数学形式。剩下的向量存储和相似度计算,你需要借助其他数据库(如pgvector扩展的 PostgreSQL、Redis 的 RediSearch、或专业的向量数据库如 Pinecone、Weaviate)来完成。
5. 错误处理、调试与性能优化
5.1 结构化错误处理
OpenAIKit抛出的错误类型是OpenAIKit.APIErrorResponse。它包含了 API 返回的详细信息,对于调试至关重要。
do { let result = try await openAIClient.chats.create(model: .gpt4, messages: messages) // 处理成功结果 } catch let error as OpenAIKit.APIErrorResponse { // 专门处理 API 错误 print("API 错误类型: \(error.type)") print("错误信息: \(error.message)") print("错误代码: \(error.code ?? \"N/A\")") // 根据错误类型提示用户或进行重试 switch error.type { case "invalid_request_error": // 可能是参数错误,提示用户检查输入 showAlert("请求参数有误") case "rate_limit_error": // 速率限制,提示用户稍后再试,并实施指数退避重试 showAlert("请求过于频繁,请稍后再试") scheduleRetry(after: 2.0) // 自定义的重试逻辑 case "insufficient_quota": // 额度不足,需要充值 showAlert("API 额度已用尽") default: showAlert("请求失败: \(error.message)") } } catch { // 处理其他类型的错误,如网络错误、解码错误等 print("其他错误: \(error.localizedDescription)") }常见错误类型及应对策略:
错误类型 (error.type) | 可能原因 | 建议处理方式 |
|---|---|---|
invalid_request_error | 请求参数缺失、格式错误、模型不存在等。 | 检查请求体 JSON 格式、参数值是否在允许范围内(如temperature应在 0-2 之间)。 |
authentication_error | API Key 无效、过期或没有权限。 | 确认 API Key 是否正确,是否有访问目标模型的权限(如 GPT-4 可能需要单独申请)。 |
rate_limit_error | 短时间内请求过多,超过 RPM(每分钟请求数)或 TPM(每分钟令牌数)限制。 | 实现指数退避重试机制。这是必须的!首次重试等待 1-2 秒,下次加倍,并设置最大重试次数。 |
insufficient_quota | 账户余额或免费额度已用完。 | 提示用户需要充值或升级套餐。 |
server_error | OpenAI 服务器内部错误。 | 等待一段时间后重试。 |
5.2 调试与日志记录
在生产环境中,详细的日志对于排查问题必不可少。由于OpenAIKit底层使用的是URLSession或AsyncHTTPClient,你可以通过这些库的配置来开启日志。
对于 URLSession(客户端):你可以自定义URLSessionConfiguration,但更常见的做法是在网络层拦截请求和响应。你可以使用URLProtocol子类,或者更方便地,使用像Alamofire这样的网络库(它内置了强大的日志功能)来创建URLSession,然后再传给OpenAIKit.Client。
对于 AsyncHTTPClient(服务端):AsyncHTTPClient支持日志记录。你可以在创建HTTPClient时传入一个Logger实例。
import Logging var logger = Logger(label: "com.yourcompany.openai-client") logger.logLevel = .debug // 设置为 .debug 或 .trace 以获取详细日志 let httpClient = HTTPClient( eventLoopGroupProvider: .shared(eventLoopGroup), configuration: HTTPClient.Configuration(logger: logger) // 传入 logger )这样,所有发出的 HTTP 请求和收到的响应头(出于安全,默认不会记录响应体)都会被记录下来,方便你查看实际的请求 URL、Header 和状态码。
5.3 性能优化与成本控制要点
复用客户端实例:如前所述,无论是
URLSession还是HTTPClient,都应该在应用生命周期内复用。频繁创建和销毁会带来不必要的开销。合理设置超时:OpenAI 的 API 响应时间受模型、输入长度和服务器负载影响。为网络请求设置合理的超时时间(如 60 秒),避免长时间阻塞 UI 或线程。
// 对于 URLSession let config = URLSessionConfiguration.default config.timeoutIntervalForRequest = 60.0 config.timeoutIntervalForResource = 120.0 let session = URLSession(configuration: config) // 对于 AsyncHTTPClient var clientConfig = HTTPClient.Configuration() clientConfig.timeout.connect = .seconds(30) clientConfig.timeout.read = .seconds(60) let httpClient = HTTPClient(eventLoopGroupProvider: .shared(group), configuration: clientConfig)控制令牌使用(Token Usage):这是成本控制的核心。API 按输入和输出的总令牌数收费。
- 监控用量:每次 API 调用的响应中都包含
usage字段,记录了本次消耗的令牌数。你应该记录这些数据,用于监控和预算控制。
let chatResponse = try await openAIClient.chats.create(...) let totalTokens = chatResponse.usage.totalTokens print("本次调用消耗了 \(totalTokens) 个令牌。")- 设置
max_tokens:务必为生成类请求(Completions, Chat)设置合理的maxTokens上限,防止生成过长内容导致意外高费用。 - 精简输入:在构建
system或user消息时,尽量言简意赅。不必要的上下文会增加令牌消耗。
- 监控用量:每次 API 调用的响应中都包含
实现请求队列与限流:如果你的应用可能同时发起大量请求(例如,用户批量处理文档),务必在客户端实现一个简单的请求队列或限流机制,避免瞬间触发 OpenAI 的速率限制(Rate Limit),导致所有后续请求失败。可以使用
DispatchSemaphore或第三方库(如RateLimiter)来控制并发请求数。
6. 进阶话题与项目扩展
6.1 处理流式响应(Streaming)
OpenAI 的 Chat Completions API 支持流式响应(stream: true),这意味着你可以像接收视频流一样,逐字逐句地收到模型的回复,而不是等待整个回复生成完毕。这对于打造类似 ChatGPT 的实时打字机效果体验至关重要。
遗憾的是,根据OpenAIKit的 README 和当前源码(截至我知识截止日期),它似乎尚未原生支持流式响应。这是一个重要的功能缺口。
临时解决方案与扩展思路:如果你急需此功能,有两条路:
- 降级使用:如果不要求实时性,可以继续使用非流式接口,一次性获取全部回复。
- 自行扩展或寻找替代:你可以考虑直接使用
URLSession或AsyncHTTPClient的流式处理能力,手动构建请求和解析 Server-Sent Events (SSE) 格式的响应。这需要你深入研究 OpenAI 的流式 API 文档。或者,你可以关注OpenAIKit项目的 Issues 和 Pull Requests,看是否有社区贡献了流式支持,也可以考虑其他更成熟的 Swift OpenAI 客户端库(如OpenAIby MacPaw),它们可能已经实现了该功能。
6.2 与 SwiftUI 的集成模式
在 SwiftUI 应用中,通常结合ObservableObject或新的@Observable宏来管理 AI 服务状态。
import SwiftUI import OpenAIKit @MainActor class ChatViewModel: ObservableObject { @Published var messages: [ChatMessage] = [] // 你的UI模型 @Published var isThinking = false @Published var errorMessage: String? private let openAIService: AIService // 前面封装的服务类 init(service: AIService = .shared) { self.openAIService = service } func sendMessage(_ text: String) async { let userMessage = ChatMessage(id: UUID(), role: .user, content: text) messages.append(userMessage) isThinking = true errorMessage = nil do { // 1. 准备消息历史(注意控制上下文长度) let openAIMessages = messages.suffix(10).map { msg in // 只保留最近10条作为上下文 Chat.Message(role: .init(rawValue: msg.role.rawValue)!, content: msg.content) } // 2. 调用 API let response = try await openAIService.client.chats.create( model: .gpt3_5Turbo, messages: openAIMessages, maxTokens: 500 ) // 3. 处理回复 if let assistantContent = response.choices.first?.message.content { let assistantMessage = ChatMessage(id: UUID(), role: .assistant, content: assistantContent) messages.append(assistantMessage) } } catch let error as OpenAIKit.APIErrorResponse { errorMessage = "AI 服务错误: \(error.message)" } catch { errorMessage = "网络或未知错误: \(error.localizedDescription)" } isThinking = false } } // 在 SwiftUI View 中使用 struct ChatView: View { @StateObject private var viewModel = ChatViewModel() @State private var inputText = "" var body: some View { VStack { List(viewModel.messages) { message in MessageBubble(message: message) } .overlay { if viewModel.isThinking { ProgressView() } } HStack { TextField("输入消息...", text: $inputText) Button("发送") { Task { await viewModel.sendMessage(inputText) inputText = "" } } .disabled(viewModel.isThinking || inputText.isEmpty) } .padding() if let error = viewModel.errorMessage { Text(error).foregroundColor(.red) } } } }6.3 异步任务的管理与取消
在 iOS 开发中,用户可能会快速切换界面,或者发送消息后立刻取消。我们需要妥善管理这些异步的 AI 请求,避免资源浪费和潜在的错误。
使用Task和取消标识:
class ChatViewModel: ObservableObject { // ... 其他属性 ... private var currentTask: Task<Void, Never>? // 持有当前任务引用 func sendMessage(_ text: String) { // 如果已有任务在运行,先取消它 currentTask?.cancel() currentTask = Task { // 在任务开始前检查是否已被取消 guard !Task.isCancelled else { return } // ... 准备消息,更新 UI ... do { // 调用可能耗时的 API let response = try await openAIService.client.chats.create(...) // 关键:在更新 UI 前,再次检查任务是否被取消。 // 因为用户可能在请求过程中离开了页面。 guard !Task.isCancelled else { return } // 更新 UI(必须在主线程) await MainActor.run { self.messages.append(assistantMessage) } } catch { // 如果错误是因为任务被取消,我们选择静默处理,不更新错误状态 if (error as? URLError)?.code == .cancelled { print("请求被用户取消") return } // 处理其他错误... } finally { // 请求结束,清理任务引用 if !Task.isCancelled { await MainActor.run { self.isThinking = false self.currentTask = nil } } } } } func cancelRequest() { currentTask?.cancel() currentTask = nil isThinking = false } }通过持有Task的引用并在适当的时候调用cancel(),我们可以实现用户友好的中断操作。同时,在await调用前后检查Task.isCancelled,可以确保被取消的任务不会再去更新已经无效的 UI 状态。
OpenAIKit作为一个基础工具库,为你扫平了与 OpenAI API 通信的障碍。但要构建一个健壮、高效、用户体验良好的 AI 应用,你还需要在它的基础上,仔细处理错误、管理状态、控制成本、并优化性能。希望这篇详细的指南,能帮助你在 Swift 生态中,更自信地驾驭 AI 能力。
