Files
kangkang/康康/Services/DiaryAssistService.swift
link2026 b79ae54b7b ```
feat(iOS): 更新MNN后端模型配置优化性能

将MNN主模型从Qwen3.5-4B(~2.64GiB)降级为Qwen3.5-2B(~1.1GiB),因为4B版本
实测运行过慢,影响用户体验。iPhone17+/SME2设备使用2B模型,保留MLX
兜底方案用于模拟器和备用场景,确保AI推理性能和存储效率的平衡。
```
2026-06-09 22:20:07 +08:00

102 lines
4.1 KiB
Swift

import Foundation
/// AI : LLM 3-4
///
/// HealthExportService ,(< 400 token),
/// await
///
/// :DiaryQuickSheet
@MainActor
struct DiaryAssistService {
static let shared = DiaryAssistService()
private init() {}
/// fill ,
/// `dim` ( `DiaryAssistPrompts.dimensions`),
/// `adopted` UI ;`round` UI append ,
struct Question: Identifiable, Hashable {
let id: UUID
let q: String
let fill: String
let dim: String
var adopted: Bool
var round: Int
init(id: UUID = UUID(),
q: String,
fill: String,
dim: String = "",
adopted: Bool = false,
round: Int = 0) {
self.id = id
self.q = q
self.fill = fill
self.dim = dim
self.adopted = adopted
self.round = round
}
}
enum AssistError: Error, LocalizedError {
case modelNotReady
case empty
case parseFailed(String)
var errorDescription: String? {
switch self {
case .modelNotReady: return String(appLoc: "AI 模型尚未准备好")
case .empty: return String(appLoc: "AI 没有给出建议,请稍后重试")
case .parseFailed(let m): return String(appLoc: "结果解析失败:\(m)")
}
}
}
/// 3-4
/// - coveredDimensions: ,( question.dim),
/// prompt
/// : AIRuntime actor , Capture / Export GPU
func suggest(content: String,
coveredDimensions: [String] = []) async throws -> (questions: [Question], decodeRate: Double) {
do {
try await AIRuntime.shared.prepare()
} catch {
throw AssistError.modelNotReady
}
let prompt = DiaryAssistPrompts.suggest(content: content, coveredDimensions: coveredDimensions)
var collected = ""
var lastRate: Double = 0
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 400)
for try await chunk in stream {
collected += chunk.text
if chunk.decodeRate > 0 { lastRate = chunk.decodeRate }
}
// 1. <think>...</think>( HealthExportService )
let stripped = HealthExportService.stripThinkBlocks(collected)
// 2. JSON( CaptureService.extractJSONObject)+
let jsonStr = CaptureService.repairJSON(CaptureService.extractJSONObject(from: stripped))
guard let data = jsonStr.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]),
let dict = obj as? [String: Any] else {
throw AssistError.parseFailed("非 JSON 输出")
}
guard let rawQuestions = dict["questions"] as? [[String: Any]] else {
throw AssistError.parseFailed("缺少 questions 字段")
}
let questions = rawQuestions.compactMap { d -> Question? in
guard let q = (d["q"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines), !q.isEmpty else {
return nil
}
let fill = (d["fill"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let dim = (d["dim"] as? String)?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return Question(q: q, fill: fill, dim: dim)
}
guard !questions.isEmpty else { throw AssistError.empty }
return (Array(questions.prefix(4)), lastRate)
}
}