Administrator
发布于 2026-03-09 / 2 阅读
0
0

链式和DSL写法

明白,你要的是真能落地、可扩展、适合项目长期使用的,不是 demo 级别的。

你指出的问题也是对的:

  1. 我前面那个版本太简陋

  2. 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

这会更像真正生产级架构。


评论