120 lines
6.9 KiB
Swift
120 lines
6.9 KiB
Swift
import Foundation
|
|
|
|
/// 「健康记录」写入时,让 LLM 从医生问诊角度提 3-4 个追问。
|
|
/// 输出严格 JSON,每个 question 带 dim(问诊维度)+ q(展示)+ fill(可一键追加的模板)。
|
|
///
|
|
/// 为什么要 `dim`(对齐 2026-05-30 prompt 优化):
|
|
/// 1.7B 模型对「不要重复」这类否定指令遵循很差,且先验会把每轮问题都拉向同一簇症状。
|
|
/// 改成「从固定维度清单里挑,每条标注 dim,跨轮排除已覆盖维度」这种正向结构约束后,
|
|
/// 去重从「字面比对」升级为「按维度结构去重」,轮内扎堆和轮间换皮重复都能压住。
|
|
enum DiaryAssistPrompts {
|
|
|
|
/// 固定问诊维度清单。模型每条问题必须正好归属其中一个;UI 累积已覆盖维度回传下一轮。
|
|
/// 顺序即展示/示例顺序,改动需同步 few-shot。
|
|
static let dimensions: [String] = [
|
|
"起病诱因", "症状性质", "伴随症状", "加重缓解",
|
|
"持续频率", "既往家族史", "用药过敏", "生活方式",
|
|
]
|
|
|
|
/// - content: 用户当前全文。
|
|
/// - coveredDimensions: 之前各轮已经问过(或记录里已写明)的维度名,本轮必须避开。
|
|
/// 第一轮传空数组。
|
|
static func suggest(content: String, coveredDimensions: [String] = []) -> String {
|
|
let covered = coveredDimensions.filter { !$0.isEmpty }
|
|
let coveredSet = Set(covered)
|
|
let allowed = dimensions.filter { !coveredSet.contains($0) }
|
|
let allowedLine = allowed.isEmpty ? "(已基本问全)" : allowed.joined(separator: "、")
|
|
// 正向约束:1.7B 对「只能从这些里挑」比对「严禁选这些」遵循更好。
|
|
let scopeRule = covered.isEmpty
|
|
? ""
|
|
: "\n- 已问过的维度【不要再问】:\(covered.joined(separator: "、"))。本轮只能从这些还没问的维度里挑:\(allowedLine)。"
|
|
|
|
return """
|
|
你是社区医生的小助手。用户写了一段身体状态的健康记录,信息可能不够完整。
|
|
请从医生问诊角度提出 3-4 个最值得追问的问题,帮用户把这条记录补全。
|
|
|
|
【问诊维度清单】每个问题必须正好归属其中一个,并用 dim 标注:
|
|
1. 起病诱因 —— 何时开始、有无诱因
|
|
2. 症状性质 —— 部位、性质、程度
|
|
3. 伴随症状 —— 是否伴随其他不适
|
|
4. 加重缓解 —— 什么情况下加重或缓解
|
|
5. 持续频率 —— 持续多久、多频繁、是否反复发作
|
|
6. 既往家族史 —— 以前是否有类似、家族相关史
|
|
7. 用药过敏 —— 在服药物、过敏史
|
|
8. 生活方式 —— 睡眠、饮食、运动习惯、压力
|
|
|
|
硬性规则:
|
|
- 本轮每个问题必须来自【不同】维度,严禁两条落在同一维度(例如不能两条都问"伴随症状")。\(scopeRule)
|
|
- 只问【最新记录】里还没写明的事。方括号 `[xxx]` 表示该话题已被提出、只是细节待填,【不要】再作为新问题重复它。
|
|
- 不给诊断、不给用药建议、不写「建议就医」。
|
|
- q ≤ 20 字,像真人医生在问;fill 是采纳后追加到原文的中文补充句,可含方括号占位符如 [时间] [部位]。
|
|
- 至少 3 条,最多 4 条。
|
|
|
|
只输出严格 JSON,不要解释、不要 markdown 围栏、不要 <think> 标签。结构:
|
|
{"questions":[{"dim":"<清单里的一个维度名>","q":"<问题>","fill":"<补充句模板>"}]}
|
|
|
|
示例 1(第一轮,记录:头痛了一上午):
|
|
{"questions":[
|
|
{"dim":"起病诱因","q":"具体什么时候开始的?","fill":"症状从 [时间] 开始,"},
|
|
{"dim":"症状性质","q":"是哪种性质的头痛?","fill":"部位/性质是 [部位/胀痛/刺痛],"},
|
|
{"dim":"伴随症状","q":"还伴有其他不适吗?","fill":"还伴有 [症状],"},
|
|
{"dim":"生活方式","q":"最近睡眠和压力怎么样?","fill":"近期睡眠 [小时]、压力 [情况],"}
|
|
]}
|
|
|
|
示例 2(后续轮,已覆盖维度:起病诱因、症状性质、伴随症状):
|
|
{"questions":[
|
|
{"dim":"加重缓解","q":"做什么会加重或缓解?","fill":"[活动/休息] 时会 [加重/缓解],"},
|
|
{"dim":"持续频率","q":"这种情况反复或持续多久了?","fill":"已持续/反复 [时长/频率],"},
|
|
{"dim":"既往家族史","q":"以前有过类似情况吗?","fill":"既往类似 [有/无,频率],"}
|
|
]}
|
|
|
|
现在输出 JSON。
|
|
本轮可选维度:\(allowedLine)
|
|
【最新记录】:
|
|
\(content)
|
|
|
|
Output: /no_think
|
|
"""
|
|
}
|
|
|
|
// MARK: - 语音口述 → 日记整理
|
|
|
|
/// 口述转写稿截断上限(字符)。2B 模型 context 保护:超长口述只取前面部分。
|
|
static let organizeTranscriptLimit = 1200
|
|
|
|
/// 把语音转写稿整理成健康日记草稿。自适应样式:内容少 → 一段通顺的话;
|
|
/// 多方面 → 按「方面:内容」分行。
|
|
/// 红线(spec 2026-06-10-voice-diary §2):只重组语言,严禁增删改任何数值、单位、药名、时间——
|
|
/// 2B 模型把 140/90 改成 130/90 即健康数据事故,所以规则放第一条并配 few-shot 强化。
|
|
static func organize(transcript: String) -> String {
|
|
let trimmed = String(transcript.prefix(organizeTranscriptLimit))
|
|
return """
|
|
你是健康记录助手。下面是用户口述身体状态的语音转写原话,可能口语化、有重复、缺标点。
|
|
请把它整理成一条清晰的健康日记。
|
|
|
|
硬性规则:
|
|
- 【绝对不许】增加、删除或改动任何数值、单位、药名、时间——原话说 140/90 就必须写 140/90。
|
|
- 只重组语言:去掉口头语和重复;用第一人称;不加入原话没有的事实。
|
|
- 内容只涉及一两个方面 → 整理成一段通顺的话(2-4 句)。
|
|
- 内容涉及多个方面(症状/用药/饮食/睡眠/运动等) → 按「方面:内容」分行。
|
|
- 不诊断、不给用药建议、不写「建议就医」。
|
|
- 只输出整理后的日记正文,不要解释、不要 markdown 围栏、不要 <think> 标签。
|
|
|
|
示例 1(口述:那个今天早上起来有点头晕然后我量了下血压140 90比平时高一点没吃早饭就出门了):
|
|
今天早上起来有点头晕,量了血压 140/90,比平时高一点。没吃早饭就出门了。
|
|
|
|
示例 2(口述:今天头晕了一上午下午好点了血压早上量的140 90嗯缬沙坦吃了降脂药忘了吃早饭没吃中午吃的清淡晚上散步了半小时):
|
|
症状:头晕了一上午,下午好转。
|
|
血压:早上 140/90。
|
|
用药:缬沙坦已服,降脂药忘服。
|
|
饮食:早饭未吃,午餐清淡。
|
|
运动:晚上散步半小时。
|
|
|
|
【口述原话】:
|
|
\(trimmed)
|
|
|
|
Output: /no_think
|
|
"""
|
|
}
|
|
}
|