我正在用 Swift 制作一个简单的 ChatGPT 应用程序,它从流中的 API 请求数据。 API 返回正确的数据,但当我收到它时,单词和字符丢失了。我尝试调试,但我不确定为什么 url 请求没有返回完整数据。
我将在下面举两个例子。第一个使用基本的 HTTP 请求来获取响应。这就是我遇到的错误。我想保留这种方法,因为它使我能够取消任务。我不确定使用第二种方法时如何取消任务。第二种方法使用 Almofire 库来请求数据。这种方法有效并且所有数据都完整返回。实际上,我想使用 Almofire 作为我的主要方法(首先),因为它更强大,但我不确定如何具有中途取消流的能力。我希望能够了解有关数据未完整返回的方式的一些见解。
第一种方法(错误)
func sendMessageStream(Question_To_Be_Asked: String) async throws -> AsyncThrowingStream<String, Error> {
var urlRequest = self.urlRequest
urlRequest.httpBody = try jsonBody(text: Question_To_Be_Asked)
let (result, response) = try await urlSession.bytes(for: urlRequest)
try Task.checkCancellation()
guard let httpResponse = response as? HTTPURLResponse else {
throw "Invalid response"
}
guard 200...299 ~= httpResponse.statusCode else {
var errorText = ""
for try await line in result.lines {
try Task.checkCancellation()
errorText += line
}
if let data = errorText.data(using: .utf8), let errorResponse = try? jsonDecoder.decode(ErrorRootResponse.self, from: data).error {
errorText = "\n\(errorResponse.message)"
}
throw "Bad Response: \(httpResponse.statusCode), \(errorText)"
}
var responseText = ""
return AsyncThrowingStream { [weak self] in
guard let self else { return nil }
for try await line in result.lines {
//print(line) <- incomplete data
try Task.checkCancellation()
if line.hasPrefix("data: "), let data = line.dropFirst(6).data(using: .utf8), let response = try? self.jsonDecoder.decode(StreamCompletionResponse.self, from: data), let text = response.choices.first?.delta.content {
responseText += text
return text
}
}
return nil
}
}
第二种方法(工作)
func sendStreamMessage(messages: [Message]) -> DataStreamRequest{
let openAIMessages = messages.map({OpenAIChatMessage(role: $0.role, content: $0.content)})
let body = OpenAIChatBody(model: "gpt-4", messages: openAIMessages, stream: true)
let headers: HTTPHeaders = [
"Authorization": "Bearer \(Constants.openAIApiKey)"
]
return AF.streamRequest(endpointUrl, method: .post, parameters: body, encoder: .json, headers: headers)
}
func sendMessage(question: String) {
let messages = [Message(id: UUID().uuidString, role: .user, content: question, createAt: Date())]
currentInput = ""
sendStreamMessage(messages: messages).responseStreamString { [weak self] stream in
guard let self = self else { return }
switch stream.event {
case .stream(let response):
switch response {
case .success(let string):
let streamResponse = self.parseStreamData(string)
streamResponse.forEach { newMessageResponse in
guard let messageContent = newMessageResponse.choices.first?.delta.content else {
return
}
//here messageContent is final complete string from stream
}
case .failure(_):
print("Something failes")
}
print(response)
case .complete(_):
print("COMPLETE")
}
}
}
func parseStreamData(_ data: String) ->[ChatStreamCompletionResponse] {
let responseStrings = data.split(separator: "data:").map({$0.trimmingCharacters(in: .whitespacesAndNewlines)}).filter({!$0.isEmpty})
let jsonDecoder = JSONDecoder()
return responseStrings.compactMap { jsonString in
guard let jsonData = jsonString.data(using: .utf8), let streamResponse = try? jsonDecoder.decode(ChatStreamCompletionResponse.self, from: jsonData) else {
return nil
}
return streamResponse
}
}
struct ChatStreamCompletionResponse: Decodable {
let id: String
let choices: [ChatStreamChoice]
}
struct ChatStreamChoice: Decodable {
let delta: ChatStreamContent
}
struct ChatStreamContent: Decodable {
let content: String
}
struct Message: Decodable, Hashable {
let id: String
let role: SenderRole
let content: String
let createAt: Date
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct OpenAIChatBody: Encodable {
let model: String
let messages: [OpenAIChatMessage]
let stream: Bool
}
struct OpenAIChatMessage: Codable {
let role: SenderRole
let content: String
}
enum SenderRole: String, Codable {
case system
case user
case assistant
}
第二种方法使用Alamofire库来请求数据。这种方法有效并且所有数据都完整返回。实际上,我想使用 Almofire 作为我的主要方法(首先),因为它更强大,但我不确定如何具有中途取消流的能力
我同意 Alamofire 使用指南中关于“取消和恢复下载”的内容 “ 更多的是取消下载,而不是流。
但是,基本的
cancel()
方法适用于 Alamofire 中的所有 request 类型,包括 流请求。sendStreamMessage
函数以返回 DataStreamRequest
对象。然后可以将该对象存储在属性中以供以后访问DataStreamRequest
。
class ChatService {
private var currentStreamRequest: DataStreamRequest?
func sendStreamMessage(messages: [Message]) -> DataStreamRequest {
// Existing code to set up and start the stream request
let streamRequest = AF.streamRequest(endpointUrl, method: .post, parameters: body, encoder: .json, headers: headers)
currentStreamRequest = streamRequest
return streamRequest
}
func cancelStream() {
currentStreamRequest?.cancel()
currentStreamRequest = nil
}
// other methods
}
sendStreamMessage
现在将 DataStreamRequest
存储在 currentStreamRequest
中。然后 cancelStream
方法会取消此请求。let chatService = ChatService()
// Start streaming
let streamRequest = chatService.sendStreamMessage(messages: messages)
// Cancel the stream when needed
chatService.cancelStream()
请注意,它不涉及恢复数据,因为它与文件下载更相关,可以保存部分数据并稍后恢复。对于来自 API 的数据流,恢复通常是不可行的,除非 API 本身支持这种机制。