智能居家养老健康守护系统 — 第八周项目报告

一、本周工作目标与整体进展

本周的核心目标是将 AI 能力从"对话问答"延伸到"感知真实世界数据"。上周系统已具备 5 个 Agent 多轮对话、SSE 流式输出、会话持久化等基础能力,但所有 AI 推理都依赖老人手动输入信息——这在养老场景中是不现实的。本周围绕三个方向展开:

  1. OCR 医疗单据识别:让系统能"看懂"医院检查单,自动提取指标入库
  2. 健康趋势分析报告:让 AI 基于时序体征数据主动生成分析,而非等老人提问
  3. 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健康趋势分析报告%' 区分。理由:

  1. 减少表数量:系统已有 10+ 张业务表,为每种报告类型建表会增加维护成本
  2. 统一查询入口:病历列表、家属查看等功能可以直接展示报告,无需额外对接
  3. 语义合理: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 分析病历」 仅当前报告数据 单次报告解读:指标含义、异常说明
深度分析 点击「结合历史数据深度分析」 当前报告 + 所有历史病历 个人化趋势:对比既往、评估进展

为什么要分两步?

  1. Token 预算管理:加载所有历史病历可能消耗大量 Token,如果用户只想快速看当前报告的解读,不必要地加载历史是浪费
  2. 信息层次递进:首次分析给出"这份报告说了什么",深度分析给出"和以前比怎么样"——符合人的认知习惯
  3. 用户控制权:让用户决定是否需要深度分析,而非强制加载所有数据

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),引入 markedmarkdown-it 等完整库会增加 50-100KB。AI 回复的 Markdown 语法子集是有限的(不会出现嵌套列表、数学公式、流程图等复杂语法),手写正则覆盖够用,是体积与功能之间的合理折中。

对话气泡和健康报告均使用 rich-text 组件展示转换后的 HTML。

5.2 新增对话按钮

需求背景:用户与 AI 对话后想重新开始,之前只能退出页面重新进入。

技术实现:对话页顶栏新增「新建对话」按钮。点击后的关键挑战是中断正在进行的流式请求——如果用户在 AI 正在输出时点击新建,旧的 SSE 连接必须被终止,否则:

  1. 旧消息会继续渲染到新对话中
  2. 旧会话的 saveSession 可能覆盖新会话

中断机制:通过 abort + generation 标志实现:

// 点击「新建对话」
function newConversation() {
    if (currentAbortController) {
        currentAbortController.abort();  // 中断 fetch 请求
    }
    generation = false;  // 停止渲染循环
    messages = [];
    sessionId = null;
    // 回到欢迎页
}

5.3 其他前端优化

优化项 说明
健康报告删除 历史报告列表新增删除按钮,调用 POST /api/medical-record/delete
病历列表过滤 过滤掉 AI 健康报告(record_titleAI健康趋势分析报告 开头),避免病历列表被报告淹没
老人端隐藏家属辅诊 老人端 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 字段区分数据来源——这个代价在当前规模下完全可接受。


九、技术总结与反思

本周的技术收获

  1. 多模态模型的实际应用:OCR 识别让我理解了多模态模型与传统 OCR 的本质区别——不是"提取文字"而是"理解内容"。Prompt 中注入患者上下文后,模型能结合个人情况做判断,这是传统 OCR 做不到的
  2. AI 功能的"渐进式设计":健康报告的"生成 → 追问"、病历分析的"首次 → 深度",都是将复杂 AI 能力拆分为用户可控的多步骤。这种设计降低了 Token 消耗,也给了用户选择权
  3. 复用优于新建:AI 病历分析复用情感陪伴 Agent、健康报告复用 medical_record 表——每次"复用"决策都减少了代码量和维护成本
  4. 前端中断机制:流式请求的 abort 处理看似简单,实则涉及请求生命周期、状态清理、会话保存等多个环节的协调

待改进方向

  1. OCR 鲁棒性:当前测试样本有限,需要对手写体、模糊图片、非标准格式做更多测试

  2. Token 成本控制:深度病历分析加载全部历史可能消耗大量 Token,后续可引入摘要压缩或分页加载策略

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐