feat(Ask): 检索过程可视化 — RAG 命中记录以 chips 展示,生成前先看见
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,8 +35,56 @@ struct HealthExportService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 检索结果摘要 —— 把「本地 RAG 找到了什么」拿给 UI 演出来(§12 卖点 3)。
|
||||
struct RetrievalSummary: Sendable, Equatable {
|
||||
var chips: [String]
|
||||
var indicatorCount: Int
|
||||
var reportCount: Int
|
||||
var symptomCount: Int
|
||||
var diaryCount: Int
|
||||
|
||||
var totalCount: Int { indicatorCount + reportCount + symptomCount + diaryCount }
|
||||
|
||||
/// 同名指标合并计数(保持检索的新→旧顺序),超出 cap 折叠成 "+N"。纯函数,单测覆盖。
|
||||
static func groupedChips(_ names: [String], cap: Int = 8) -> [String] {
|
||||
var order: [String] = []
|
||||
var counts: [String: Int] = [:]
|
||||
for n in names {
|
||||
if counts[n] == nil { order.append(n) }
|
||||
counts[n, default: 0] += 1
|
||||
}
|
||||
var chips = order.map { name -> String in
|
||||
let c = counts[name] ?? 1
|
||||
return c > 1 ? "\(name) ×\(c)" : name
|
||||
}
|
||||
if chips.count > cap {
|
||||
let overflow = chips.count - cap
|
||||
chips = Array(chips.prefix(cap)) + ["+\(overflow)"]
|
||||
}
|
||||
return chips
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func from(snapshot: Snapshot) -> RetrievalSummary {
|
||||
var chips = groupedChips(snapshot.indicators.map(\.name), cap: 8)
|
||||
chips += snapshot.reports.prefix(3).map(\.title)
|
||||
chips += snapshot.symptoms.prefix(3).map(\.name)
|
||||
if !snapshot.diaries.isEmpty {
|
||||
chips.append(String(appLoc: "日记 ×\(snapshot.diaries.count)"))
|
||||
}
|
||||
return RetrievalSummary(
|
||||
chips: chips,
|
||||
indicatorCount: snapshot.indicators.count,
|
||||
reportCount: snapshot.reports.count,
|
||||
symptomCount: snapshot.symptoms.count,
|
||||
diaryCount: snapshot.diaries.count
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
case phaseChanged(Phase)
|
||||
case retrieved(RetrievalSummary)
|
||||
case token(TokenChunk)
|
||||
case completed(persistentID: PersistentIdentifier)
|
||||
// .failed 走 stream throw,不在 Event 里
|
||||
@@ -80,6 +128,7 @@ struct HealthExportService {
|
||||
// —— Phase 2: 检索 ——
|
||||
continuation.yield(.phaseChanged(.retrieving))
|
||||
let snapshot = Self.retrieve(intent: intent, ctx: modelContext)
|
||||
continuation.yield(.retrieved(RetrievalSummary.from(snapshot: snapshot)))
|
||||
try Task.checkCancellation()
|
||||
|
||||
// —— Phase 3: 生成 ——
|
||||
@@ -178,9 +227,10 @@ struct HealthExportService {
|
||||
}
|
||||
|
||||
/// 多轮导出页的单轮问答。只回答,不入库。
|
||||
/// 事件流:先 .retrieved(检索可视化),后 .token 流式正文;不发 .phaseChanged / .completed。
|
||||
func answer(question: String,
|
||||
conversation: [HealthExportDialogueTurn],
|
||||
in modelContext: ModelContext) -> AsyncThrowingStream<TokenChunk, Error> {
|
||||
in modelContext: ModelContext) -> AsyncThrowingStream<Event, Error> {
|
||||
AsyncThrowingStream { continuation in
|
||||
let task = Task { @MainActor in
|
||||
do {
|
||||
@@ -191,6 +241,7 @@ struct HealthExportService {
|
||||
}
|
||||
|
||||
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
|
||||
continuation.yield(.retrieved(RetrievalSummary.from(snapshot: snapshot)))
|
||||
let dataJSON = Self.serializeData(snapshot: snapshot)
|
||||
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
|
||||
let prompt = HealthExportPrompts.dialogueAnswer(
|
||||
@@ -209,7 +260,7 @@ struct HealthExportService {
|
||||
if clean.count > displayed.count, clean.hasPrefix(displayed) {
|
||||
let delta = String(clean.dropFirst(displayed.count))
|
||||
displayed = clean
|
||||
continuation.yield(TokenChunk(text: delta, decodeRate: chunk.decodeRate))
|
||||
continuation.yield(.token(TokenChunk(text: delta, decodeRate: chunk.decodeRate)))
|
||||
} else if clean != displayed {
|
||||
displayed = clean
|
||||
}
|
||||
@@ -245,6 +296,7 @@ struct HealthExportService {
|
||||
|
||||
continuation.yield(.phaseChanged(.retrieving))
|
||||
let snapshot = Self.retrieveDialogueSnapshot(ctx: modelContext)
|
||||
continuation.yield(.retrieved(RetrievalSummary.from(snapshot: snapshot)))
|
||||
let dataJSON = Self.serializeData(snapshot: snapshot)
|
||||
let transcript = HealthExportDialogueTurn.transcript(from: conversation)
|
||||
try Task.checkCancellation()
|
||||
|
||||
Reference in New Issue
Block a user