山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(八)
智能居家养老健康守护系统 — 第八周项目报告
一、本周工作目标与整体进展
本周的核心目标是将 AI 能力从"对话问答"延伸到"感知真实世界数据"。上周系统已具备 5 个 Agent 多轮对话、SSE 流式输出、会话持久化等基础能力,但所有 AI 推理都依赖老人手动输入信息——这在养老场景中是不现实的。本周围绕三个方向展开:
- OCR 医疗单据识别:让系统能"看懂"医院检查单,自动提取指标入库
- 健康趋势分析报告:让 AI 基于时序体征数据主动生成分析,而非等老人提问
- AI 病历分析:将 OCR 识别结果与 AI 对话打通,实现"识别 → 分析 → 追问"闭环
同时对前端交互体验进行了系统性优化,修复了多个稳定性问题。
二、OCR 医疗单据识别(新增)
2.1 技术背景与设计动机
养老场景中,老人的健康数据来源不只是每日手动录入——更多的数据沉淀在医院检查单、体检报告等纸质/图片载体中。传统做法是让老人或家属手动抄录指标,既繁琐又容易出错。
多模态大模型(如 qwen3.6-plus)的出现改变了这一局面:直接将图片"喂"给模型,就能获得结构化的文本理解。我们选择用 AI 而非传统 OCR(如 Tesseract)的原因是:医疗单据格式高度非标准化——不同医院的排版、术语、指标命名差异极大,传统 OCR 只能提取文字,无法理解"这个值属于哪个指标"的语义关系,而多模态模型天然具备这种理解能力。
2.2 技术实现
调用链路:
用户上传图片(OSS URL / Base64)
→ OcrServiceImpl.ocrRecognize()
→ 构造多模态消息(text prompt + image_url)
→ 调用 qwen3.6-plus(/v1/chat/completions)
→ 解析 JSON 结构化响应
→ 写入 medical_record 表(识别结果)
→ 写入 elderly_health_data 表(体征数据)
→ 返回 OcrResult(含结构化指标列表)
关键设计决策——Prompt 工程:
向多模态模型发送的 Prompt 需要明确约束输出格式,否则模型返回的 JSON 结构不稳定。核心 Prompt 策略:
你是一个医疗单据识别助手。请识别图片中的所有生化指标,
以 JSON 数组格式返回,每个元素包含:
- name: 指标名称(标准化中文名)
- value: 数值
- unit: 单位
- referenceRange: 参考范围
- abnormal: 是否异常(布尔值)
同时注入患者上下文(老人基本信息 + 疾病史),让模型在判断"是否异常"时能结合患者年龄、性别、既往病史,而非简单对照通用参考范围。例如:一位有高血压病史的老人,收缩压 135mmHg 虽在通用参考范围内,但结合其病史应标记为需要关注。
两种图片输入方式:
| 输入方式 | 适用场景 | 实现要点 |
|---|---|---|
| URL | 图片已上传至 OSS | 直接传 image_url 给模型 |
| Base64 | 前端本地拍照/选图 | 编码为 data:image/jpeg;base64,... 传入 |
体征数据自动入库:
OCR 识别出的指标不只是"看一眼就丢"——血压、血糖、心率、体温、体重、身高这 6 项核心体征会自动写入 elderly_health_data 表,与每日手动录入的数据共用同一张表,后续健康趋势分析报告可以直接使用这些数据。
2.3 稳定性问题与修复
问题 1:唯一约束冲突
elderly_health_data 表有 (elderly_id, check_date) 唯一约束。如果老人当天已手动录入过数据,OCR 再写入会触发唯一键冲突异常。
理解:这个问题的本质是"同一维度的数据有多个来源"——手动录入和 OCR 识别都可能写入同一天的数据。解决方案不能简单地"忽略冲突",因为 OCR 识别的数据可能比手动录入更准确(医院检查单通常比老人自测更精确)。
修复方案:采用"先删后插"策略——删除当日已有数据再插入新数据,并用 try-catch 包裹确保不阻塞 OCR 主流程(识别结果仍然正常返回,只是入库失败时静默处理)。
try {
// 删除当日旧数据
LambdaQueryWrapper<ElderlyHealthData> deleteWrapper = new LambdaQueryWrapper<>();
deleteWrapper.eq(ElderlyHealthData::getElderlyId, elderlyId)
.eq(ElderlyHealthData::getCheckDate, LocalDate.now());
elderlyHealthDataService.remove(deleteWrapper);
// 插入新数据
elderlyHealthDataService.save(healthData);
} catch (Exception e) {
log.warn("OCR体征入库失败(非阻塞): {}", e.getMessage());
}
问题 2:缺少患者上下文导致异常判断不准确
初版 Prompt 只传了图片,模型使用通用参考范围判断异常。对于老年用户,很多指标的"正常范围"与年轻人不同(如血压、血糖的控制目标更宽松)。
修复:在 system prompt 中注入老人基本信息(年龄、性别)和疾病史(糖尿病、高血压等),让模型结合个人情况判断。
2.4 测试结果
| 测试图片 | 识别指标数 | 准确率 | 异常标记 |
|---|---|---|---|
| 和谐人民医院门诊病历(32岁男性) | 6 项 | 100% | 体温 38.6℃ ✅ |
| 武汉市中心医院门诊病历(35岁男性) | 11 项 | 100% | 体温、脉搏、WBC、CRP ✅ |
技术理解:100% 准确率是在这两张测试样本上的结果,不代表所有场景都能达到。多模态模型对印刷体、排版清晰的病历识别效果好,但对手写体、拍照模糊、非标准格式的单据可能退化。后续需要更多样化的测试样本,以及对识别结果的置信度评估。
三、健康趋势分析报告系统(新增)
3.1 技术背景与设计动机
系统已有健康数据录入和图表展示能力(第七周完成的 /api/health-analytics 接口),但图表只是"数据的可视化"——老人和家属看到折线图,未必能理解"血压连续 3 天上升意味着什么"。
健康趋势分析报告的核心价值是将数据转化为可理解的信息:AI 不是简单复述数字,而是结合老人的疾病史、用药情况、历史趋势,给出有上下文的解读。
3.2 技术实现
数据聚合 → AI 推理 → 结构化输出的完整链路:
HealthReportServiceImpl.generateReport(elderlyId)
→ ElderlyContextService.getFullContext() // 加载健康档案
→ healthDataService.getRecentDays(elderlyId, 7) // 近7天体征
→ 构造 System Prompt(角色 + 数据 + 输出格式要求)
→ DeepSeek API 推理
→ 解析回复,存储为 medical_record
→ 返回 sessionId + reply
输出结构设计:
通过 Prompt 约束 AI 的输出结构为 5 个固定板块:
| 板块 | 作用 | 设计考量 |
|---|---|---|
| 【数据总览】 | 罗列近 7 天核心指标 | 让用户快速确认"AI 看到了哪些数据" |
| 【趋势分析】 | 各指标变化趋势描述 | 这是报告的核心价值——识别变化方向 |
| 【异常预警】 | 需要关注的异常信号 | 与"建议就医确认"区分,避免过度焦虑 |
| 【综合评估】 | 整体健康状况评价 | 通俗易懂的总结 |
| 【健康建议】 | 家属/老人可执行的行动 | 行动导向,而非纯知识输出 |
报告复用 medical_record 表存储:
这是一个重要的设计决策——不新建 health_report 表,而是将报告存入已有的病历记录表,通过 record_title LIKE 'AI健康趋势分析报告%' 区分。理由:
- 减少表数量:系统已有 10+ 张业务表,为每种报告类型建表会增加维护成本
- 统一查询入口:病历列表、家属查看等功能可以直接展示报告,无需额外对接
- 语义合理:AI 生成的健康分析本质上也是一种"医疗记录",与手工病历共用表结构语义上说得通
多轮追问能力:
报告生成后返回 sessionId,后续追问复用会话机制。这意味着 AI 在追问时"记得"之前生成的报告内容,用户可以问"刚才提到的血压趋势能详细说说吗"。追问支持普通和 SSE 流式两种模式。
3.3 与健康数据图表接口的关系
| 能力 | /api/health-analytics/chart |
/api/health-report/generate |
|---|---|---|
| 输出形式 | JSON(前端绑定 ECharts) | 自然语言文本 |
| 分析深度 | 统计聚合(均值/极值/趋势) | AI 语义理解(结合病史/用药) |
| 适用对象 | 前端大屏、小程序图表 | 老人/家属直接阅读 |
| 是否需要 AI | 否(纯后端计算) | 是(LLM 推理) |
两者互补:图表提供精确的数值展示,报告提供可理解的语义解读。
四、AI 病历分析功能(新增)
4.1 技术设计——两阶段分析
这是本周技术含量最高的功能之一。核心问题是:AI 分析病历时,应该只看当前报告,还是加载全部历史?
答案是"分两步走":
| 阶段 | 触发方式 | AI 上下文 | 分析角度 |
|---|---|---|---|
| 首次分析 | OCR 识别完成后点击「AI 分析病历」 | 仅当前报告数据 | 单次报告解读:指标含义、异常说明 |
| 深度分析 | 点击「结合历史数据深度分析」 | 当前报告 + 所有历史病历 | 个人化趋势:对比既往、评估进展 |
为什么要分两步?
- Token 预算管理:加载所有历史病历可能消耗大量 Token,如果用户只想快速看当前报告的解读,不必要地加载历史是浪费
- 信息层次递进:首次分析给出"这份报告说了什么",深度分析给出"和以前比怎么样"——符合人的认知习惯
- 用户控制权:让用户决定是否需要深度分析,而非强制加载所有数据
4.2 后端实现
接口复用:不新建 AI 病历分析专用接口,而是复用情感陪伴 Agent 的 /api/companion/chat 接口,通过新增参数控制行为:
public class CompanionChatRequest {
private Long elderlyId;
private String message;
private String sessionId;
private String mode; // "companion" / "diagnosis"
private Long medicalRecordId; // 新增:指定病历ID
private Boolean includeHistory; // 新增:是否加载历史病历
}
EmotionalCompanionServiceImpl.getSystemPrompt() 根据这两个参数动态调整 System Prompt:
if (request.getMedicalRecordId() != null) {
// 加载指定病历内容
MedicalRecord record = medicalRecordService.getById(request.getMedicalRecordId());
sb.append("\n以下是患者本次就诊的病历记录:\n");
sb.append(record.getRecordContent());
if (Boolean.TRUE.equals(request.getIncludeHistory())) {
// 加载所有历史病历
List<MedicalRecord> history = medicalRecordService.listByElderlyId(elderlyId);
sb.append("\n以下是患者的历史就诊记录:\n");
for (MedicalRecord r : history) {
sb.append("- ").append(r.getVisitDate()).append(": ")
.append(r.getRecordTitle()).append("\n")
.append(r.getRecordContent()).append("\n");
}
}
}
技术理解——为什么复用而非新建:
AI 病历分析本质上是"给 AI 一个特殊上下文让它回答问题",与情感陪伴的对话机制完全一致。区别仅在于 System Prompt 中额外注入了病历数据。如果为这个功能单独建一个 Agent,需要重复实现会话管理、流式输出、权限校验等所有基础设施,收益极低。复用现有 Agent + 参数化 Prompt 是更合理的做法。
4.3 前端交互链路
OCR 识别完成(ocr-result 页面)
→ 点击「AI 分析病历」按钮
→ 跳转 ai-chat 页面,携带参数:
medicalRecordId=123
autoMessage="请分析这份病历"
→ ai-chat 页面 onShow 检测到参数
→ 自动调用 sendMessage(),发送分析请求
→ AI 返回首次分析结果
→ 对话页底部显示「结合历史数据深度分析」按钮
→ 用户点击 → 发送新消息,携带 includeHistory=true
→ AI 加载全部历史病历,给出深度分析
这个交互设计的关键是无缝衔接:用户在 OCR 结果页看到识别数据后,一键跳转到 AI 对话页面,不需要手动复制粘贴任何信息。AI 已经"知道"用户要看哪份病历。
五、前端交互优化
5.1 Markdown 渲染
技术背景:AI 生成的回复天然包含 Markdown 格式(标题、加粗、列表、表格),但微信小程序的 rich-text 组件不原生支持 Markdown。需要在前端做一层转换。
实现方案:新增 formatReport() 函数,通过正则逐层解析 Markdown 语法并转换为 HTML:
// 支持的 Markdown 语法及对应 HTML
# 标题 → <h1>标题</h1>
**加粗** → <strong>加粗</strong>
* 列表 → <li>列表</li>
| 表格 | → <table>...</table>
--- → <hr/>
`代码` → <code>代码</code>
技术理解——为什么不引入完整的 Markdown 解析库:
小程序包体积有严格限制(主包 2MB),引入 marked 或 markdown-it 等完整库会增加 50-100KB。AI 回复的 Markdown 语法子集是有限的(不会出现嵌套列表、数学公式、流程图等复杂语法),手写正则覆盖够用,是体积与功能之间的合理折中。
对话气泡和健康报告均使用 rich-text 组件展示转换后的 HTML。
5.2 新增对话按钮
需求背景:用户与 AI 对话后想重新开始,之前只能退出页面重新进入。
技术实现:对话页顶栏新增「新建对话」按钮。点击后的关键挑战是中断正在进行的流式请求——如果用户在 AI 正在输出时点击新建,旧的 SSE 连接必须被终止,否则:
- 旧消息会继续渲染到新对话中
- 旧会话的
saveSession可能覆盖新会话
中断机制:通过 abort + generation 标志实现:
// 点击「新建对话」
function newConversation() {
if (currentAbortController) {
currentAbortController.abort(); // 中断 fetch 请求
}
generation = false; // 停止渲染循环
messages = [];
sessionId = null;
// 回到欢迎页
}
5.3 其他前端优化
| 优化项 | 说明 |
|---|---|
| 健康报告删除 | 历史报告列表新增删除按钮,调用 POST /api/medical-record/delete |
| 病历列表过滤 | 过滤掉 AI 健康报告(record_title 以 AI健康趋势分析报告 开头),避免病历列表被报告淹没 |
| 老人端隐藏家属辅诊 | 老人端 Agent 列表过滤掉「家属辅诊」,仅家属端可见——这是权限模型在 UI 层的体现 |
| 历史会话加载修复 | 点击历史会话时补全 html 字段,确保消息正确渲染 Markdown 格式 |
六、后端稳定性修复
| 问题 | 根因分析 | 修复方案 |
|---|---|---|
| OCR 体征入库唯一约束冲突 | (elderly_id, check_date) 唯一约束,同一天多次写入冲突 |
先删当日旧数据再插入,try-catch 包裹不阻塞主流程 |
| OCR 缺少患者上下文 | 未注入老人信息,模型用通用参考范围判断异常 | system prompt 注入老人基本信息 + 疾病史 |
| 流式请求中断后消息丢失 | abort 后未保存已接收的部分回复 | 中断前将已累积内容存入会话 |
七、测试结果
| 测试项 | 结果 | 说明 |
|---|---|---|
| 健康报告生成 | ✅ | 基于 7 天数据生成报告,5 个板块结构完整 |
| 报告追问 | ✅ | AI 记得报告上下文,追问回复连贯 |
| OCR 识别(2 张病历) | ✅ | 6+11 项指标全部正确 |
| OCR 体征自动入库 | ✅ | 血压、心率等写入 elderly_health_data 表 |
| AI 病历分析(首次) | ✅ | 单份报告解读准确 |
| AI 病历分析(深度) | ✅ | 加载历史后给出个人化分析 |
| Markdown 渲染 | ✅ | 标题、表格、列表正确渲染 |
| 新增对话 | ✅ | 中断旧流式请求,清空消息 |
| 历史会话加载 | ✅ | 消息正确显示 |
八、数据库说明
无新建表,所有数据复用现有表结构:
| 数据 | 存储位置 | 区分方式 |
|---|---|---|
| 健康报告 | medical_record |
record_title LIKE 'AI健康趋势分析报告%' |
| OCR 识别结果 | medical_record |
record_title LIKE 'OCR医疗单据识别%' |
| OCR 体征数据 | elderly_health_data |
check_source = 'OCR' |
| 对话会话 | chat_session |
agent_type 区分 Agent |
设计理解:复用已有表而非新建,是本项目的统一设计原则。好处是减少表数量、统一查询入口、新功能与已有功能天然兼容。代价是需要通过 record_title 前缀或 check_source 字段区分数据来源——这个代价在当前规模下完全可接受。
九、技术总结与反思
本周的技术收获
- 多模态模型的实际应用:OCR 识别让我理解了多模态模型与传统 OCR 的本质区别——不是"提取文字"而是"理解内容"。Prompt 中注入患者上下文后,模型能结合个人情况做判断,这是传统 OCR 做不到的
- AI 功能的"渐进式设计":健康报告的"生成 → 追问"、病历分析的"首次 → 深度",都是将复杂 AI 能力拆分为用户可控的多步骤。这种设计降低了 Token 消耗,也给了用户选择权
- 复用优于新建:AI 病历分析复用情感陪伴 Agent、健康报告复用 medical_record 表——每次"复用"决策都减少了代码量和维护成本
- 前端中断机制:流式请求的 abort 处理看似简单,实则涉及请求生命周期、状态清理、会话保存等多个环节的协调
待改进方向
-
OCR 鲁棒性:当前测试样本有限,需要对手写体、模糊图片、非标准格式做更多测试
-
Token 成本控制:深度病历分析加载全部历史可能消耗大量 Token,后续可引入摘要压缩或分页加载策略
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)