山东大学软件学院项目实训-基于语言大模型的智能居家养老健康守护系统-个人博客(六)
基于 DeepSeek 大模型的多 Agent 多轮对话技术实现
一、需求背景
在智慧养老场景中,我们需要为老年人提供全方位的 AI 健康服务。不同于简单的单轮问答,养老场景下的 AI 助手需要:
- 记住上下文:老人描述症状可能分多次表达,AI 必须串联前后信息
- 个性化回应:基于老人的病历、用药、体征数据给出针对性建议
- 多角色服务:同一套技术底座支撑 5 个功能定位不同的 Agent
本文以「智能居家养老健康守护系统」的后端实现为案例,详细介绍如何基于 DeepSeek 大模型构建支持多轮对话、流式输出、会话持久化的多 Agent 系统。
二、系统中的 5 个 AI Agent
| Agent | 服务对象 | 核心能力 |
|---|---|---|
| 情感陪伴(康心) | 老人 | 心理慰藉、陪诊助手 |
| 健康陪诊(康伴) | 老人 | 全科健康问答、导诊 |
| 健康干预(养怡) | 老人 | 饮食/运动/作息个性化方案 |
| 用药安全审核 | 老人 | 药物配伍禁忌筛查 |
| 家属辅诊(家护) | 家属 | 跨角色健康分析与照护建议 |
每个 Agent 拥有独立的 System Prompt 和对话上下文,但共享同一套会话管理和大模型调用基础设施。
三、多轮对话的核心挑战
3.1 上下文窗口管理
大模型的 API 是无状态的——每次请求都需要把完整的对话历史发送给模型。这带来了几个问题:
- 消息拼接顺序:必须严格按 system → user → assistant → user → assistant… 的顺序组织
- 上下文长度限制:DeepSeek 的 context window 有限,过长的对话需要做裁剪
- 个性化注入时机:老人的健康档案信息应在 system prompt 中注入,且不能让 AI 主动暴露"我已知道你的信息"
我们的消息构建策略:
private List<Map<String, String>> buildMessages(ChatSession session, ElderlyContext context) {
List<Map<String, String>> messages = new ArrayList<>();
// 1. System Prompt + 个性化健康档案
messages.add(Map.of("role", "system", "content", buildSystemPrompt(context)));
// 2. 历史对话按时间顺序追加
for (ChatMessage msg : session.getMessages()) {
messages.add(Map.of("role", msg.getRole(), "content", msg.getContent()));
}
return messages;
}
3.2 会话持久化
每个 Agent 的对话需要跨请求保持状态。用户第一次发消息时创建会话,后续通过 sessionId 关联。
方案演进:我们从最初的 JSON 文件存储迁移到了 PostgreSQL 关系型数据库。
文件存储的问题:
- 按目录查找会话,目录名硬编码导致新增 Agent 时遗漏
- 无法按用户维度筛选,不支持分页
- 并发写入时存在竞态风险
数据库方案的优势:
- 通过
session_id唯一索引精确定位,不依赖目录结构 - 支持按
elderly_id、agent_type灵活查询 ON DELETE CASCADE保证删除一致性- 天然支持事务和并发
表结构设计:
CREATE TABLE chat_session (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL UNIQUE,
agent_type VARCHAR(32) NOT NULL, -- 区分不同Agent
mode VARCHAR(32), -- 子模式(如饮食/运动)
title VARCHAR(200), -- 首条消息摘要
elderly_id BIGINT, -- 关联老人
family_id BIGINT, -- 关联家属
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
);
CREATE TABLE chat_message (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role VARCHAR(16) NOT NULL, -- user/assistant
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT now(),
CONSTRAINT fk_msg_session FOREIGN KEY (session_id)
REFERENCES chat_session(session_id) ON DELETE CASCADE
);
3.3 增量保存策略
对话是增长型数据——每次交互新增 2 条消息(user + assistant)。为避免全量覆写,采用增量插入:
public ChatSession saveSession(ChatSession session) {
// 查询已持久化的消息数量
long existingCount = chatMessageMapper.selectCount(
new LambdaQueryWrapper<ChatMessage>()
.eq(ChatMessage::getSessionId, session.getSessionId())
);
// 仅插入新增部分
List<ChatMessage> allMessages = session.getMessages();
if (allMessages.size() > existingCount) {
List<ChatMessage> newMessages = allMessages.subList((int) existingCount, allMessages.size());
for (ChatMessage msg : newMessages) {
msg.setSessionId(session.getSessionId());
chatMessageMapper.insert(msg);
}
}
return session;
}
四、SSE 流式输出
养老场景对体验要求高——老人等待完整回复可能产生焦虑。采用 Server-Sent Events(SSE)实现逐字输出:
@PostMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody @Valid HealthChatRequest request) {
return healthCompanionService.chatStream(request);
}
Service 层通过线程池异步执行,主线程立即返回 SseEmitter:
public SseEmitter chatStream(HealthChatRequest request) {
SseEmitter emitter = new SseEmitter(120_000L);
executor.execute(() -> {
try {
String fullReply = streamFromDeepSeek(session, context, emitter);
session.addMessage(new ChatMessage("assistant", fullReply));
chatSessionService.saveSession(session);
} catch (Exception e) {
emitter.send(SseEmitter.event().name("error").data("服务暂时不可用"));
emitter.completeWithError(e);
}
});
return emitter;
}
流式读取 DeepSeek 返回的 SSE 数据,逐块转发给前端:
while ((line = reader.readLine()) != null) {
if (!line.startsWith("data: ")) continue;
String data = line.substring(6).trim();
if ("[DONE]".equals(data)) {
emitter.send(SseEmitter.event().name("done").data("[DONE]"));
break;
}
JsonNode delta = objectMapper.readTree(data)
.path("choices").get(0).path("delta");
String content = delta.path("content").asText("");
if (!content.isEmpty()) {
fullReply.append(content);
emitter.send(SseEmitter.event().name("message").data(content));
}
}
五、System Prompt 设计要点
好的 System Prompt 直接决定了 Agent 的回答质量。总结几个关键设计原则:
- 明确身份和边界:给 Agent 一个名字和明确的职责范围,如"你是临床药师,不做诊断"
- 安全红线:遇到急症关键词必须建议拨打 120,不确定时坦诚表达
- 结构化输出:通过格式模板引导输出(如"【审核结果】【风险等级】【详细分析】")
- 个性化注入:将老人健康数据追加在 system prompt 末尾,并明确告知"不要主动提及已知信息"
- 温度参数区分:问答类 Agent 用 0.6,审核类用 0.3(需要更确定性的回答)
六、个性化健康上下文
每个 Agent 在对话时都会加载老人的完整健康档案,包括:
- 基本信息(姓名、年龄、性别)
- 疾病列表
- 当前用药计划(药名、剂量、用法)
- 近期健康数据(血压、血糖、心率趋势)
- 病历摘要
这些数据由 ElderlyContextService 统一聚合,各 Agent 按需注入到 system prompt 中。
七、遇到的问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 部分 Agent 的会话删除失效 | 文件存储的查找函数只搜索了 2 个目录 | 迁移到数据库,通过 session_id 唯一索引精确定位 |
| 流式对话中断后消息丢失 | 异常时未保存已接收的部分回复 | 在 catch 中保存已累积的 fullReply |
| 首次对话无 sessionId | 前端不知道该传什么 | 后端约定"不传则新建",返回时携带 sessionId |
| 对话过长导致 token 超限 | 历史消息无限追加 | 限制 max_tokens + 未来可做消息裁剪 |
八、总结与展望
通过统一的会话管理层 + 差异化的 System Prompt + 共享的大模型调用基础设施,我们用较少的代码量实现了 5 个功能各异的 AI Agent。核心收获:
- 分层抽象:会话管理、模型调用、上下文聚合各自独立,新增 Agent 只需编写 Prompt
- 增量持久化:避免全量写入,数据库方案天然支持并发和查询
- 流式体验:SSE 给老年用户带来明显的体验提升
后续可优化方向:
- 引入消息裁剪策略,保证超长对话不超 token 限制
- 添加 Redis 缓存热点会话,减少数据库读取
- 接入向量数据库实现 RAG,让 Agent 能检索医学知识库
更多推荐
所有评论(0)