docs: 完成 MVP 架构设计文档

- 数据库设计:3张核心表 (diagnosis_record/case_library/api_document)
- Agent架构:4 Agent协作 (Supervisor/Planner/Executor/Verifier)
- 意图识别:L0正则+L1小模型Agent分层
- RAG两层加载:L1预加载通用知识 + L2按需加载具体文档
- Skill体系:/diagnose-by-orderid 标准化诊断流程
- Harness控制:5 Gates + 中断机制
- 会话管理:Redis临时存储 + 扩展方案
- 闭环机制:用户反馈 → BadCase → 优化
- 实施规划:3阶段13天
This commit is contained in:
zhuyongxin
2026-06-22 18:47:00 +08:00
parent 80eada415d
commit 429413fe64
11 changed files with 5408 additions and 0 deletions
+529
View File
@@ -0,0 +1,529 @@
# Agent 架构设计(MVP 版)
## 一、MVP 全景
```
用户输入
↓
┌──────────────────────────────────────────┐
│ 意图识别(Intent Recognition) 🆕 │
│ "用户想干什么?" │
│ │
│ 诊断意图 → 路由到诊断 Skill │
│ 文档意图 → 路由到文档问答 │
│ 案例意图 → 路由到案例查询 │
│ 闲聊 → 快速响应(不启动 Agent) │
│ 模糊/无关 → 提示用户,直接中断 │
└──────────────┬───────────────────────────┘
│ 诊断意图
↓
┌──────────────────────────────────────────┐
│ Supervisor Agent(调度者) │
│ "谁来干?什么时候停?" │
└──────────────┬───────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Planner Agent(规划者) │
│ 分析问题 → 制定策略 → 生成报告 │
└──────────────┬───────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Executor Agent(执行者) │
│ 调用工具收集证据 │
└──────────────┬───────────────────────────┘
↓
┌──────────────────────────────────────────┐
│ Verifier Agent(验证者) │
│ 事实核查 → 判定通过/修正/驳回 │
└──────────────┬───────────────────────────┘
↓
诊断报告输出
↓
用户反馈(有用/无用)
↓
案例沉淀 + BadCase 优化
```
---
## 二、意图识别(入口层)🆕
### 2.1 设计理念
```
定位:独立模块,不嵌入任何单一 Agent
分层策略(不是二选一,而是组合):
L0: 正则规则 —— 0 成本,毫秒级 ✅ MVP
├─ 处理 80%+ 的结构化查询
├─ 正则匹配订单号/traceId/错误码格式
└─ 关键词匹配("报错"/"异常"/"失败")
L1: 小模型 Agent —— 低成本,百毫秒级 ✅ MVP
├─ L0 未命中时触发
├─ 处理灵活的模糊表达("系统有点慢"、"怎么查不到了")
├─ 不启动全链路 Agent,只做意图分类
└─ 判断为诊断意图 → 路由到诊断 Skill
L2: 兜底策略 —— 极少使用
├─ L0+L1 都无法判断 → 意图不明 → 中断
└─ Phase 2 增强
```
### 2.2 意图分类与路由
```
┌────────────────────────────────────────────────────┐
│ 意图 │ 说明 │ 路由 │
├────────────────────────────────────────────────────┤
│ 诊断意图 │ 包含结构化标识或错误描述 │ → 诊断Skill│
│ 文档问答 │ "XX接口的参数有哪些" │ → 直接RAG │
│ 案例查询 │ "之前有类似的问题吗" │ → 案例检索 │
│ 闲聊 │ "你好"/"谢谢" │ → 快速响应 │
│ 意图不明 │ 无法识别 │ → 中断+提示 │
└────────────────────────────────────────────────────┘
关键原则:
- 只有诊断意图才启动 Agent 全链路
- 非诊断意图走轻量路径或直接中断
```
### 2.3 L0:正则规则(MVP,处理 80%)
```
为什么先做 L0?
→ 0 成本(不调 LLM),毫秒级响应
→ 结构化查询占比最大(订单号、traceId、错误码、关键词)
→ L0 命中直接路由,不需要走后续逻辑
规则配置(可扩展):
┌────────────────────────────────────────────────┐
│ 规则 │ 意图 │ 方式 │
├────────────────────────────────────────────────┤
│ 匹配 \d{12,} │ 诊断 │ 正则 │
│ 匹配 trace[-_]?\w{8,} │ 诊断 │ 正则 │
│ 包含"报错‖失败‖异常‖挂了‖超时" │ 诊断 │ 关键词│
│ 包含"文档‖接口‖参数‖字段‖API" │ 文档 │ 关键词│
│ 包含"案例‖之前‖类似‖历史" │ 案例 │ 关键词│
│ 长度 <= 5 字符 │ 闲聊 │ 规则 │
└────────────────────────────────────────────────┘
命中 → 直接路由,不调 L1
未命中 → 进入 L1
```
### 2.4 L1:小模型 Agent(MVP,处理剩余 20%)
```
为什么用小模型 Agent 而非嵌入到 Supervisor?
→ 意图识别是独立职责,不应耦合到任何业务 Agent
→ 轻量 Agent:单一职责,只分类不执行
→ 成本低(~50 token),延迟低(~200ms)
何时触发:L0 规则未命中
System Prompt:
"你是意图分类器,判断用户想做什么。
只返回一个词:[诊断 / 文档查询 / 案例查询 / 闲聊 / 意图不明]
诊断:用户描述了故障、报错、异常
文档查询:用户询问接口文档、字段含义
案例查询:用户询问历史案例、类似问题
闲聊:简单的问候、感谢
意图不明:无法判断用户意图"
输入:用户原始输入
输出:意图类型 + 置信度
```
### 2.5 L2:兜底策略
```
L0+L1 都无法判断 → L2 兜底
中断规则:
├─ 意图不明 → 提示用户 + 中断
│ "无法判断您的意图,请提供订单号或错误码"
├─ 闲聊 → 快速响应 + 中断
│ "我是故障诊断助手,请描述您遇到的问题"
└─ 不启动 Agent,直接返回
路由规则:
├─ 诊断意图 → 启动 Supervisor + 4 Agent 全链路
├─ 文档意图 → 不启动 Agent,直接 RAG 检索
└─ 案例意图 → 不启动 Agent,直接查询 case_library
```
### 2.5 架构位置
```
用户输入
↓
┌──────────────────────────────────────────┐
│ 意图识别模块(独立) │
│ │
│ L1: 小模型 Agent ─→ 诊断意图? │
│ │ 文档意图? │
│ │ 案例意图? │
│ │ 闲聊? │
│ ↓ │
│ L2: 兜底 ─────────→ 意图不明 → 中断 │
│ 闲聊 → 快速响应 │
└───────────────┬──────────────────────────┘
│ 诊断意图
↓
Supervisor → Planner → Executor → Verifier
```
### 2.6 MVP vs Phase 2
```
MVP L0(正则)+ L1(小模型Agent) 覆盖 95%+ 场景
Phase 2 L2(兜底增强) 细化中断提示,支持多轮澄清
```
---
## 三、4 个 Agent 设计
### 2.1 Supervisor Agent(调度者)
**职责**:总指挥,协调工作流
```
调度规则:
├─ 接收任务 → 发给 Planner 分析
├─ Planner 完成 → 发给 Executor 执行
├─ Executor 完成 → 发给 Verifier 校验
└─ Verifier PASS → 输出报告 / REJECT → 返回 Planner 重新规划
不做:
- 不直接调用工具
- 不直接生成报告
```
### 2.2 Planner Agent(规划者 + 分诊)
**职责**:分析问题、制定策略、生成报告草稿
```
分析规则:
├─ 有 errorCode + 接口 URL → EXTERNAL_API(外部接口故障)
├─ 有堆栈信息 → INTERNAL_ERROR(系统内部错误)
├─ 有数据库错误码(如 1213)→ DATABASE(数据库问题)
└─ 其他 → 通用排查
规划流程:
1. 确定 fault_category
2. 制定排查步骤(每步:工具名 + 参数 + 预期)
3. 生成决策:EXECUTE(继续执行)| FINISH(生成报告)
Replanner 职责:
├─ Executor 每次返回结果后 → 评估证据是否充分
├─ 需要补充?→ 调整步骤,继续执行
├─ 证据齐全?→ FINISH,生成报告草稿
└─ 连续 3 次失败?→ 降级
禁止:
- 编造数据
- 引用未经工具返回的内容
```
### 2.3 Executor Agent(执行者)
**职责**:调用工具收集证据
```
工具清单:
├─ queryOrder:查询订单/业务数据(MySQL 只读)
├─ searchDoc:检索接口文档(混合检索 Milvus + MySQL)
├─ recommendCase:推荐相似案例(精确匹配 + 语义检索)
└─ getCurrentTime:获取当前时间
执行规则:
├─ 每次只执行 Planner 指定的一个步骤
├─ 返回结构化的执行结果
├─ 失败时返回错误详情(便于 Planner 调整)
└─ 禁止编造结果
扩展预留:
// 代码中 Executor 是接口,后续可扩展为 SubAgent
public interface Executor {
ExecutionResult execute(Step step);
}
```
### 2.4 Verifier Agent(验证者)
**职责**:验证诊断报告,防止编造
```
验证流程:
1️⃣ 事实核查(最重要)
├─ 报告中的错误码 → 在 tool_calls 中存在?
├─ 根因结论 → 有日志/文档证据支撑?
├─ 修复方案 → 引用了文档或案例?
└─ 发现编造数据 → 直接 REJECT
2️⃣ 完整性检查
├─ 根因分析章节不能为空
├─ 证据链章节不能为空
└─ 修复方案章节不能为空
判决结果:
├─ PASS:报告成立,直接输出
├─ REVISE:小问题可修正,返回 Planner 微调
└─ REJECT:编造数据或严重错误,返回 Planner 重新分析
```
---
## 四、RAG 两层加载策略
### 4.1 设计理念
```
问题:
❌ 全前置:启动时把所有文档塞给 Agent → 信息过载,推理变慢
❌ 纯被动:等到需要才查 → Planner 没有全局视野,可能跑偏
❌ 固定步骤:每次都调 → 内部错误查接口文档浪费
正确做法:两层互补
L1 预加载(Planner 启动时)
→ 通用领域知识:系统架构、通用错误码、业务流程
→ 给 Planner 全局视野,避免方向性错误
L2 按需加载(Executor 执行中)
→ 具体接口文档:字段定义、错误码含义、调用规范
→ 给 Executor 精准证据,定位具体问题
```
### 4.2 两层对比
| | L1 预加载 | L2 按需加载 |
|------|---------|-----------|
| 触发时机 | 意图识别后,Planner 启动前 | Executor 拿到具体信息后 |
| 内容 | 通用知识(架构、流程、高频错误码) | 具体接口文档(字段、错误码含义) |
| 目的 | 让 Planner 有全局视野 | 让 Executor 有精准证据 |
| 成本 | 固定,每次诊断 1 次 | 按需,最多 2-3 次 |
| 谁负责 | Supervisor 注入 | Executor 自主调用 |
### 4.3 实现方式
```
L1 预加载:
Supervisor 在启动 Planner 前:
searchDoc(keyword="系统架构 通用错误码 业务流程")
→ 注入到 Planner 的 System Prompt 中
→ Planner 拥有"领域背景知识"
L2 按需加载:
Executor 执行 Step 2 时:
拿到 errorCode=40003, faultSource="广东"
→ 自主调用 searchDoc(errorCode="40003", faultSource="广东")
→ 获取该接口的具体字段定义和错误码说明
→ 作为证据写入诊断报告
```
---
## 五、Skill 设计(1 个)
### /diagnose-by-orderid(按订单号诊断)
```
输入:orderId
工作流(6 步):
Step 1: 查询订单信息
工具:queryOrder
失败:ABORT(订单不存在则终止)
Step 2: 检索接口文档
工具:searchDoc
参数:errorCode + faultSource
失败:SKIP(标注"文档缺失")
Step 3: 查询日志
工具:queryLogs(Mock)
参数:traceId
失败:SKIP(标注"日志缺失")
Step 4: 检索相似案例
工具:recommendCase
参数:errorCode + faultCategory
失败:SKIP(标注"无相似案例")
Step 5: 生成诊断报告
汇总所有证据,按模板生成报告
Step 6: Verifier 验证
事实核查 → 判决
门禁规则:
├─ Step 1 失败 → 终止,返回"订单不存在"
├─ Step 2-4 失败 → 跳过,标注缺失信息
├─ 任意步骤超时 30s → 终止
└─ Verifier REJECT → 返回 Planner 重新规划
```
---
## 六、Harness 控制层(精简版)
### 4.1 5 个 Quality Gates
```
输入门禁(2 个):
├─ Gate 1: 输入参数非空校验
└─ Gate 2: 5 分钟内同一订单 → 返回缓存
执行门禁(1 个):
└─ Gate 3: 工具调用超时(10 秒)
输出门禁(2 个):
├─ Gate 4: 报告章节完整性(3 章节不全 → 不通过)
└─ Gate 5: 置信度阈值(< 60 → 标记"低置信度")
```
### 4.2 中断规则
```
自动中断:
├─ 工具连续失败 3 次 → 终止,降级输出
└─ 全局超时 30 秒 → 终止
条件降级:
├─ 文档检索为空 → 跳过继续
├─ 案例推荐为空 → 跳过继续
└─ 日志查询失败 → 跳过继续
降级输出:
"无法自动诊断,请人工介入"
+ 已收集的证据(订单信息 + 部分日志 + 已知错误码)
```
---
## 七、技术实现
### 5.1 基于 Spring AI Alibaba
```java
// Supervisor - 框架提供
SupervisorAgent supervisor = SupervisorAgent.builder()
.name("diagnosis_supervisor")
.model(chatModel)
.subAgents(List.of(planner, executor, verifier))
.build();
// Planner
ReactAgent planner = ReactAgent.builder()
.name("planner_agent")
.model(chatModel)
.systemPrompt(plannerPrompt)
.outputKey("planner_plan")
.build();
// Executor(代码中预留 SubAgent 扩展接口)
ReactAgent executor = ReactAgent.builder()
.name("executor_agent")
.model(chatModel)
.systemPrompt(executorPrompt)
.methodTools(diagnosisTools)
.tools(new ToolCallback[]{queryOrder, searchDoc, recommendCase, getCurrentTime})
.build();
// Verifier
ReactAgent verifier = ReactAgent.builder()
.name("verifier_agent")
.model(chatModel)
.systemPrompt(verifierPrompt)
.outputKey("verifier_result")
.build();
```
### 5.2 工具注册
```java
@Component
public class DiagnosisTools {
@Tool(description = "查询订单/业务数据(只读)")
public OrderInfo queryOrder(@ToolParam(description = "订单号") String orderId) {
// MySQL 只读 + SQL 注入防护
}
@Tool(description = "检索接口文档")
public List<DocChunk> searchDoc(
@ToolParam(description = "错误码") String errorCode,
@ToolParam(description = "省份/服务名") String faultSource
) {
// 混合检索:精确匹配 + 向量检索
}
@Tool(description = "推荐相似历史案例")
public List<CaseResult> recommendCase(
@ToolParam(description = "错误码") String errorCode,
@ToolParam(description = "故障类别") String faultCategory
) {
// 精确匹配 MySQL + 语义检索 Milvus
}
}
```
---
## 八、闭环机制
```
诊断报告输出
↓
用户反馈(useful / not_useful)
↓
├─ useful → 自动生成 case_library
└─ not_useful → 记录 BadCase
↓
每周 BadCase 分析
↓
Prompt / Skill 优化
↓
准确率验证(测试集重跑)
```
---
## 九、MVP vs 扩展方向
| 维度 | MVP | 扩展方向 |
|------|-----|---------|
| Agent | 4 个 Agent | SubAgent 模式(专科医生) |
| Skill | 1 个 | 渐进式披露(3 层知识) |
| 工具 | @Tool 注解 | MCP 独立 Server |
| 回退 | 2 级(失败→降级) | 4 级路由 |
| Gates | 5 个 | 15 个全流程门禁 |
| 隔离 | 单 JVM | K8s Pod 进程隔离 |
| 进化 | 案例自动生成 | 模式识别 + Prompt 自优化 |
---
## 十、面试话术(精简版)
> "我用 Spring AI Alibaba 实现了一个故障诊断 Agent 系统。
>
> 入口层是**意图识别**:先判断用户想干什么——诊断故障、查文档、查案例还是闲聊。
> 非诊断意图直接走轻量路径,只有诊断意图才启动 Agent 全链路,节省资源。
>
> 4 Agent 协作:Supervisor 调度、Planner 制定策略、
> Executor 调用工具收集证据、Verifier 验证报告防止编造。
>
> 诊断流程封装成了 Skill,标准化 6 个步骤和异常处理。
> Harness 层 5 个门禁保证质量——最关键的是输出门禁,
> Verifier 会对比报告数据和工具返回数据,发现编造就驳回。
>
> 闭环机制:用户反馈 → BadCase 分析 → Prompt 优化。
> 案例自动沉淀,系统越用越智能。"
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
# 实施规划
## Phase 1:核心功能(第1周)
### 实现内容
```
✅ diagnosis_record 表
✅ case_library 表
✅ api_document 表
✅ Redis 会话管理
✅ 单次诊断流程
```
### 不实现
```
❌ conversation_history 表(先不加)
❌ 会话同步(先不做)
❌ 追问功能(先不支持)
```
### 验收标准
```
- 用户输入订单号 → 返回诊断报告
- 诊断记录持久化到 MySQL
- 可以查询历史诊断
- 可以统计诊断成功率
- 文档可以导入、查询、删除
- 案例可以推荐
```
---
## Phase 2:追问功能(第2周)
### 实现内容
```
✅ 支持多轮对话(基于 Redis 上下文)
✅ conversation_history 表(可选)
✅ 会话上下文管理
```
### 验收标准
```
- 用户可以追问细节
- Agent 能基于上下文回答
- 追问不创建新的诊断记录
```
---
## Phase 3:优化分析(第3周)
### 实现内容
```
✅ 会话同步(Redis → MySQL)
✅ BadCase 分析
✅ 追问频率统计
✅ 案例质量评分
```
### 验收标准
```
- 重要会话自动同步到 MySQL
- 可以分析用户追问模式
- 可以优化 Prompt 和功能
```
---
## 技术债务清单
### 待优化项(Phase 4+)
```
1. api_document 增强
- 软删除(archived_at)
- 启用开关(enabled)
- 批次管理(batch_id)
- 状态细化(PARSING/SPLITTING/INDEXING...)
2. case_library 增强
- 复杂评分(useful_count + score)
- 标签分类(tags)
- 版本管理
- 案例合并
3. 性能优化
- Redis 缓存有效文档列表
- 分页查询优化
- 索引优化
4. 监控告警
- 诊断成功率监控
- 诊断耗时监控
- 文档索引状态监控
```
---
## 数据迁移计划
### 如果已有旧数据
```
1. diagnosis_record 迁移
- 旧字段 → 新字段映射
- order_id → business_id
- province → fault_source
- api_url → fault_target
2. 执行迁移脚本
UPDATE diagnosis_record SET
business_id = order_id,
fault_category = 'EXTERNAL_API',
fault_source = province,
fault_target = api_url
WHERE fault_category IS NULL;
3. 验证数据一致性
```
---
## 部署检查清单
### Phase 1 部署前
```
□ MySQL 数据库已创建
□ 三张核心表已创建(diagnosis_record/case_library/api_document)
□ Redis 已配置并可连接
□ Milvus Collection 已创建
□ 向量化服务(DashScope)配置正确
□ 文件上传目录已创建并有写权限
□ 应用配置文件检查完成
```
### 配置文件示例
```yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/diagnosis_system
username: root
password: xxx
redis:
host: localhost
port: 6379
database: 0
milvus:
host: localhost
port: 19530
collection-name: api_doc_collection
dashscope:
api-key: sk-xxx
file:
upload:
path: /data/uploads
```
---
## 回滚方案
### 数据库回滚
```sql
-- 保留旧表备份
CREATE TABLE diagnosis_record_backup_20240622 AS SELECT * FROM diagnosis_record;
-- 回滚时恢复
DROP TABLE diagnosis_record;
RENAME TABLE diagnosis_record_backup_20240622 TO diagnosis_record;
```
### Milvus 回滚
```
- Milvus 数据无法回滚
- 建议:重要操作前先备份 Collection
- 或者:保留原始文件,可重新索引
```
---
## 监控指标
### 核心指标
```
1. 诊断成功率
- 目标:> 85%
- 告警:< 80%
2. 诊断耗时
- 目标:P95 < 10s
- 告警:P95 > 15s
3. 文档索引成功率
- 目标:> 95%
- 告警:< 90%
4. 案例推荐准确率
- 目标:> 70%
- 评估:用户反馈
```
+210
View File
@@ -0,0 +1,210 @@
# 会话管理设计
## 会话存储策略
### Redis(主)
**数据结构**:
```
key: session:{session_id}
value: {
"sessionId": "sess-abc",
"userId": "user-123",
"currentDiagnosisId": "diag-001",
"messages": [
{"role": "user", "content": "诊断订单 A"},
{"role": "assistant", "content": "完整报告..."}
],
"context": {
"province": "广东",
"apiName": "社保查询",
"errorCode": "40003"
},
"createdAt": "2024-06-15T14:30:00Z",
"lastActiveAt": "2024-06-15T14:35:00Z"
}
ttl: 1800秒(30分钟)
```
**优势**:
- ✅ 快速读写
- ✅ 自动过期
- ✅ 支持追问(保存上下文)
---
### MySQL(辅助,可选)
**同步策略**:
1. 重要会话同步
- 有用户反馈的会话
- 诊断失败的会话(BadCase)
- 多轮对话 > 3 轮的会话
2. 同步时机
- 会话结束时(30分钟过期)
- 用户反馈时(实时)
- 定时任务(每小时,可选)
3. 同步目标
- conversation_history 表
- 用于长期分析和审计
---
## 数据流设计
### 场景1:单次诊断(主流 80%)
```
1. 用户发起诊断
POST /api/diagnosis/start
{
"orderId": "202406150001"
}
2. 创建会话(Redis)
key: session:sess-abc
ttl: 1800秒
3. 创建诊断记录(MySQL)
INSERT INTO diagnosis_record
- diagnosis_id: diag-001
- session_id: sess-abc
- status: RUNNING
4. Agent 执行诊断
- 调用工具(queryOrder, queryLogs, searchDoc...)
- 生成报告
5. 更新诊断记录(MySQL)
UPDATE diagnosis_record
- status: SUCCESS
- root_cause: "idCard字段缺失"
- report_markdown: "完整报告..."
6. 返回报告
→ 大部分用户到此结束
```
---
### 场景2:追问(少数 20%)
```
1. 用户追问
POST /api/chat
{
"sessionId": "sess-abc",
"message": "为什么会缺失字段?"
}
2. 从 Redis 获取上下文
GET session:sess-abc
- 有之前的诊断结果
- 有对话历史
3. Agent 基于上下文回答
- 不创建新的 diagnosis_record
- 只是普通对话
4. 更新 Redis 会话
- 追加对话历史
- 刷新 TTL(重新计时30分钟)
5. 可选:保存到 conversation_history(MySQL)
- 如果需要长期分析
- 异步存储
```
---
### 场景3:同一会话多次诊断
```
1. 用户第一次诊断
"诊断订单 A"
→ diagnosis_record(diag-001, session_id=sess-abc)
2. 用户第二次诊断
"再诊断订单 B"
→ diagnosis_record(diag-002, session_id=sess-abc)
3. 会话关联
- 同一个 session_id
- 两条 diagnosis_record
- Redis 中保存完整对话历史
```
---
## 会话生命周期
```
创建
↓
活跃(每次交互刷新TTL)
↓
30分钟无活动
↓
自动过期
↓
可选:同步到 MySQL(重要会话)
```
---
## 实现示例
### Java 代码
```java
@Service
public class SessionService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String SESSION_PREFIX = "session:";
private static final Duration SESSION_TTL = Duration.ofMinutes(30);
// 创建会话
public String createSession(String userId) {
String sessionId = UUID.randomUUID().toString();
SessionData session = SessionData.builder()
.sessionId(sessionId)
.userId(userId)
.messages(new ArrayList<>())
.context(new HashMap<>())
.createdAt(LocalDateTime.now())
.lastActiveAt(LocalDateTime.now())
.build();
String key = SESSION_PREFIX + sessionId;
redisTemplate.opsForValue().set(key, toJson(session), SESSION_TTL);
return sessionId;
}
// 获取会话
public SessionData getSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
String json = redisTemplate.opsForValue().get(key);
return json != null ? fromJson(json) : null;
}
// 更新会话(刷新TTL)
public void updateSession(SessionData session) {
session.setLastActiveAt(LocalDateTime.now());
String key = SESSION_PREFIX + session.getSessionId();
redisTemplate.opsForValue().set(key, toJson(session), SESSION_TTL);
}
// 删除会话
public void deleteSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.delete(key);
}
}
```