明白,你要的是真能落地、可扩展、适合项目长期使用的,不是 demo 级别的。
你指出的问题也是对的:
我前面那个版本太简陋
AIMessage 没定义,不能直接用
所以这次我直接给你一套完整的工业级 MessageBuilder,目标是:
直接兼容你现在这个 MacPaw/OpenAI
支持 .system / .user / .assistant
支持链式调用
支持 @resultBuilder DSL
支持过滤空文本
支持从你自己的业务模型转换
支持 append 历史消息
支持校验
结构清晰,后面方便继续扩展 tool / image / developer message
一、先给你最终使用效果
方式 1:链式写法
let messages = MessageBuilder()
.system("You are a helpful assistant.")
.user("你好")
.assistant("你好呀,请问我可以帮你做什么?")
.user("帮我介绍一下 SwiftUI")
.build()方式 2:DSL 写法
let messages = MessageBuilder.build {
SystemMessage("You are a helpful assistant.")
UserMessage("你好")
AssistantMessage("你好呀,请问我可以帮你做什么?")
UserMessage("帮我介绍一下 SwiftUI")
}方式 3:从你自己的业务模型转
let messages = MessageBuilder()
.append(historyMessages)
.build()这里的 historyMessages 可以是你自己的 ChatMessageModel 数组。
二、完整代码
你可以直接复制。
import Foundation
import OpenAI
// MARK: - Domain Role
enum AppMessageRole: String, Codable, Sendable {
case system
case user
case assistant
}
// MARK: - Builder Error
enum MessageBuilderError: LocalizedError {
case emptyMessages
case invalidSystemMessagePosition
case emptyContentAfterSanitization
var errorDescription: String? {
switch self {
case .emptyMessages:
return "消息数组为空,无法构建 ChatQuery。"
case .invalidSystemMessagePosition:
return "system 消息应尽量放在消息数组开头。"
case .emptyContentAfterSanitization:
return "消息内容在清洗后为空。"
}
}
}
// MARK: - Internal Message Model
struct MessageItem: Hashable, Sendable {
let role: AppMessageRole
let content: String
init(role: AppMessageRole, content: String) {
self.role = role
self.content = content
}
}
// MARK: - Convert To OpenAI
extension MessageItem {
func toOpenAIMessage() -> ChatQuery.ChatCompletionMessageParam {
switch role {
case .user:
return .user(.init(content: .string(content)))
case .system:
return .system(.init(content: .textContent(content)))
case .assistant:
return .assistant(.init(content: .textContent(content)))
}
}
}
// MARK: - Business Model Convertible
protocol MessageBuildable {
var builderRole: AppMessageRole { get }
var builderContent: String { get }
}
extension MessageItem: MessageBuildable {
var builderRole: AppMessageRole { role }
var builderContent: String { content }
}
// MARK: - DSL Node
protocol MessageNode {
var item: MessageItem? { get }
}
struct SystemMessage: MessageNode, Sendable {
let text: String
init(_ text: String) { self.text = text }
var item: MessageItem? { .init(role: .system, content: text) }
}
struct UserMessage: MessageNode, Sendable {
let text: String
init(_ text: String) { self.text = text }
var item: MessageItem? { .init(role: .user, content: text) }
}
struct AssistantMessage: MessageNode, Sendable {
let text: String
init(_ text: String) { self.text = text }
var item: MessageItem? { .init(role: .assistant, content: text) }
}
// MARK: - Result Builder
@resultBuilder
enum MessageNodeBuilder {
static func buildBlock(_ components: [MessageItem]...) -> [MessageItem] {
components.flatMap { $0 }
}
static func buildExpression(_ expression: MessageNode) -> [MessageItem] {
if let item = expression.item {
return [item]
}
return []
}
static func buildExpression(_ expression: MessageItem) -> [MessageItem] {
[expression]
}
static func buildExpression(_ expression: [MessageItem]) -> [MessageItem] {
expression
}
static func buildOptional(_ component: [MessageItem]?) -> [MessageItem] {
component ?? []
}
static func buildEither(first component: [MessageItem]) -> [MessageItem] {
component
}
static func buildEither(second component: [MessageItem]) -> [MessageItem] {
component
}
static func buildArray(_ components: [[MessageItem]]) -> [MessageItem] {
components.flatMap { $0 }
}
static func buildLimitedAvailability(_ component: [MessageItem]) -> [MessageItem] {
component
}
}
// MARK: - Industrial MessageBuilder
struct MessageBuilder: Sendable {
struct Configuration: Sendable {
var trimsWhitespaceAndNewlines: Bool = true
var removesEmptyMessages: Bool = true
var validatesSystemAtFront: Bool = false
}
private(set) var items: [MessageItem]
private let configuration: Configuration
init(
items: [MessageItem] = [],
configuration: Configuration = .init()
) {
self.items = items
self.configuration = configuration
}
// MARK: Factory
static func build(
configuration: Configuration = .init(),
@MessageNodeBuilder _ content: () -> [MessageItem]
) -> [ChatQuery.ChatCompletionMessageParam] {
MessageBuilder(
items: content(),
configuration: configuration
).build()
}
static func make(
configuration: Configuration = .init(),
@MessageNodeBuilder _ content: () -> [MessageItem]
) -> MessageBuilder {
MessageBuilder(
items: content(),
configuration: configuration
)
}
// MARK: Chainable API
func system(_ text: String) -> MessageBuilder {
appending(.init(role: .system, content: text))
}
func user(_ text: String) -> MessageBuilder {
appending(.init(role: .user, content: text))
}
func assistant(_ text: String) -> MessageBuilder {
appending(.init(role: .assistant, content: text))
}
func append(role: AppMessageRole, content: String) -> MessageBuilder {
appending(.init(role: role, content: content))
}
func append(_ item: MessageItem) -> MessageBuilder {
appending(item)
}
func append<S: Sequence>(_ items: S) -> MessageBuilder where S.Element == MessageItem {
var copy = self
copy.items.append(contentsOf: items)
return copy
}
func append<S: Sequence>(_ models: S) -> MessageBuilder where S.Element: MessageBuildable {
let mapped = models.map {
MessageItem(role: $0.builderRole, content: $0.builderContent)
}
return append(mapped)
}
func appendIf(_ condition: Bool, role: AppMessageRole, content: String) -> MessageBuilder {
guard condition else { return self }
return append(role: role, content: content)
}
func prependSystem(_ text: String) -> MessageBuilder {
var copy = self
copy.items.insert(.init(role: .system, content: text), at: 0)
return copy
}
func removingAll() -> MessageBuilder {
MessageBuilder(configuration: configuration)
}
// MARK: Build
func build() -> [ChatQuery.ChatCompletionMessageParam] {
sanitizedItems().map { $0.toOpenAIMessage() }
}
func buildValidated() throws -> [ChatQuery.ChatCompletionMessageParam] {
let sanitized = sanitizedItems()
guard !sanitized.isEmpty else {
throw MessageBuilderError.emptyMessages
}
if configuration.validatesSystemAtFront {
let firstSystemIndex = sanitized.firstIndex(where: { $0.role == .system })
if let firstSystemIndex, firstSystemIndex != 0 {
throw MessageBuilderError.invalidSystemMessagePosition
}
}
return sanitized.map { $0.toOpenAIMessage() }
}
func rawItems() -> [MessageItem] {
sanitizedItems()
}
// MARK: Private
private func appending(_ item: MessageItem) -> MessageBuilder {
var copy = self
copy.items.append(item)
return copy
}
private func sanitizedItems() -> [MessageItem] {
items.compactMap { item in
let text: String
if configuration.trimsWhitespaceAndNewlines {
text = item.content.trimmingCharacters(in: .whitespacesAndNewlines)
} else {
text = item.content
}
if configuration.removesEmptyMessages && text.isEmpty {
return nil
}
return MessageItem(role: item.role, content: text)
}
}
}三、如果你已经有自己的
ChatMessageModel
你只需要让它遵守 MessageBuildable 就行。
例如:
struct ChatMessageModel {
let role: ChatRole
let content: String
}
enum ChatRole {
case system
case user
case assistant
}
extension ChatMessageModel: MessageBuildable {
var builderRole: AppMessageRole {
switch role {
case .system: return .system
case .user: return .user
case .assistant: return .assistant
}
}
var builderContent: String {
content
}
}这样你就可以直接:
let openAIMessages = MessageBuilder()
.append(historyMessages)
.build()四、推荐用法
1)最适合 AIService 的写法
func generateText() async throws {
let messages = MessageBuilder()
.system("You are a helpful assistant.")
.user("What is the capital of France?")
.build()
let query = ChatQuery(
messages: messages,
model: .gpt5
)
let result = try await openAI.chats(query: query)
print(result.choices.first?.message.content ?? "")
}2)带历史聊天记录
let messages = MessageBuilder()
.prependSystem("你是一个专业的 Swift 助手,回答简洁准确。")
.append(historyMessages)
.user("请解释一下 @Observable 和 ObservableObject 的区别")
.build()3)DSL 写法
let messages = MessageBuilder.build {
SystemMessage("你是一个专业的 Swift 助手")
UserMessage("你好")
AssistantMessage("你好呀,很高兴见到你。")
UserMessage("帮我介绍一下 SwiftUI")
}4)带条件拼接
let messages = MessageBuilder()
.system("你是一个专业中文助手")
.appendIf(!memoryPrompt.isEmpty, role: .system, content: memoryPrompt)
.append(historyMessages)
.user(inputText)
.build()五、为什么这版更工业级
你刚刚不满意前一个版本,本质上是因为它只有“能用”,没有“工程性”。
这版补上了这些点:
1. 有明确的内部消息模型
不是凭空来了个 AIMessage。
这里是:
MessageItem它是 builder 内部统一消息结构。
2. 有业务模型适配协议
protocol MessageBuildable你项目里任何消息模型都能接进来,不需要硬耦合。
3. 支持链式、DSL、批量 append
而不是只有最原始的 mutating func append。
4. 有配置项
比如:
是否 trim
是否删空消息
是否校验 system 位置
5. 有校验入口
你可以:
try builder.buildValidated()而不是永远无脑 build。
六、你现在最应该怎么接进项目
如果按你当前 AIChats 的方向,我建议:
在 AI 模块里放三个文件
MessageBuilder.swift
放我上面这一整套
ChatMessageModel+MessageBuildable.swift
给你的业务消息模型做适配
OpenAIService.swift
内部使用:
let messages = MessageBuilder()
.append(historyMessages)
.user(prompt)
.build()这样你以后所有地方都不用再直接手写:
.system(.init(content: .textContent(...)))
.user(.init(content: .string(...)))七、如果你还想再上一个层级
这套已经比普通 demo 强很多了。
但如果你说的“非常工业级”是更高一级,比如你后面还想支持:
developer message
tool message
structured output
image input
refusal handling
response api 迁移
token 截断策略
system prompt 合并策略
history window 裁剪
那我建议下一步继续升级成:
MessagePipeline
而不是单纯 MessageBuilder
也就是把它拆成:
MessageBuilder
MessageSanitizer
MessageWindowingPolicy
SystemPromptComposer
OpenAIMessageMapper
这会更像真正生产级架构。