HANDOVER.md 15 KB

AI 药典助手 — 项目交接文档

最后更新: 2026-07-29


一、项目概述

基于《中华人民共和国药典》(2025年版)的 AI 智能问答系统,支持文本/图片/视频多模态查询,面向药师、医生、患者提供药品信息检索和用药指导。

在线地址: https://pharmacopoeia.kailin.com.cn


二、技术栈

技术 说明
前端 单页 HTML + JS static/index.html,无框架
后端 Java 21 + Spring Boot 3 端口 9000
数据库 PostgreSQL 16 + pgvector 向量检索
缓存 Redis 7 QA 缓存 + Embedding 缓存
LLM 阿里云百炼 DashScope Qwen 模型
部署 Docker Compose Nginx 反向代理
数据管道 Python docx_ingest.py, embed_only.py

三、目录结构

/opt/pharmacopoeia-ai/
├── static/                  # 前端静态页面
│   └── index.html           # 主页面(需复制到部署目录)
├── backend-java/            # Java 后端
│   ├── src/main/java/com/pharmacopoeia/
│   │   ├── controller/      # ChatController, DrugController, AuthController
│   │   ├── service/         # LLMService, RetrieverService, PromptService, QACacheService
│   │   ├── repository/      # JPA Repository
│   │   ├── entity/          # Drug, Message, Conversation, User, Brand, BrandRecommendRule
│   │   ├── security/        # JwtAuthFilter, RateLimitFilter, JwtUtil
│   │   └── config/          # AuthProperties, DashScopeRateLimitProperties
│   └── pom.xml
├── data-pipeline/           # 数据导入管道
│   ├── docx_ingest.py       # DOCX 解析入库
│   ├── embed_only.py        # 向量化
│   └── import_all.py        # 全量导入
├── deploy/                  # 部署配置
│   ├── docker-compose.yml   # PostgreSQL + Redis
│   ├── nginx-pharmacopoeia.conf
│   └── supervisord-java.conf
├── tools/
│   └── java-deploy.sh       # 启停脚本
├── .env                     # 环境变量(生产环境)
└── docs/                    # 文档
    ├── HANDOVER.md          # 项目交接文档
    └── DEPLOY.md            # 上线部署 SOP(每次部署必须参考)

四、部署架构

用户 → Nginx(:80) → /api/* → Java(:9000)
                   → /     → static/index.html

Java(:9000) → PostgreSQL(:5432)  # 数据存储 + 向量检索
            → Redis(:6379)       # QA 缓存 + Embedding 缓存
            → DashScope API      # 阿里云百炼 LLM

五、环境变量 (.env)

# --- 核心 ---
SERVER_PORT=9000
POSTGRES_PASSWORD=pharma2025
QWEN_API_KEY=sk-xxx                    # 必填,阿里云百炼
QWEN_MODEL=qwen3.7-max

# --- 鉴权 ---
AUTH_ENABLED=false                     # 上线后改为 true
JWT_SECRET=YourSuperSecretKey...       # 生产环境更换

# --- 限流 ---
RATE_LIMIT_PER_MINUTE=200
RATE_LIMIT_PER_HOUR=5000
RATE_LIMIT_PER_DAY=200

# --- 百炼 API 全局限流(客户确认后启用)---
DASHSCOPE_RATE_LIMIT_ENABLED=false
DASHSCOPE_RATE_LIMIT_PER_MINUTE=100
DASHSCOPE_RATE_LIMIT_PER_HOUR=2000
DASHSCOPE_RATE_LIMIT_PER_DAY=50000

六、部署命令

每次部署必须参考《上线部署 SOP》(docs/DEPLOY.md),包含完整的部署前 Checklist、部署后验证和回滚流程。

# 启动/停止/重启
./tools/java-deploy.sh start
./tools/java-deploy.sh stop
./tools/java-deploy.sh restart

# 一键部署(自动拉代码 + 编译 + 备份 + 重启)
./tools/java-deploy.sh deploy

# 回滚到上一个备份
./tools/java-deploy.sh rollback

# 查看状态
./tools/java-deploy.sh status

# 查看日志
./tools/java-deploy.sh logs        # 最近 100 行
./tools/java-deploy.sh logf        # 实时跟踪

备份说明: deploy 命令会在 backend-java/backups/ 目录下按时间戳保存备份 JAR,自动保留最近 5 个。


七、API 接口

认证

方法 路径 说明
POST /api/v1/auth/login/wechat 微信登录

聊天

方法 路径 说明
POST /api/v1/chat/ask 文本问答(非流式)
POST /api/v1/chat/stream 文本问答(SSE 流式)
POST /api/v1/chat/ask-image 图片问答
POST /api/v1/chat/stream-image 图片问答(流式)
POST /api/v1/chat/ask-multimodal 多模态问答(文本+图片+视频)
POST /api/v1/chat/stream-multimodal 多模态问答(流式)
GET /api/v1/chat/recent-messages?limit=50 恢复最近对话
GET /api/v1/chat/history 对话历史列表
POST /api/v1/chat/feedback 提交反馈

药品库

方法 路径 说明
GET /api/v1/drug/search?page=1&pageSize=20&keyword= 药品搜索
GET /api/v1/drug/{drugId} 药品详情
GET /api/v1/drug/category/tree 分类树

品牌推荐

方法 路径 说明
- 品牌详情字段 brand_id, brand_name, matched_keyword, function, usage_dosage, contraindication, ingredients, properties, specification, adverse_reactions, precautions, execution_standard, storage, jump_url

管理端(需 JWT)

方法 路径 说明
GET /api/v1/admin/knowledge/brands 品牌列表(分页+搜索)
POST /api/v1/admin/knowledge/brands 新增品牌
PUT /api/v1/admin/knowledge/brands/{id} 修改品牌
DELETE /api/v1/admin/knowledge/brands/{id} 删除品牌
POST /api/v1/admin/knowledge/brands/import 品牌 CSV 导入
GET /api/v1/admin/knowledge/brands/export 品牌 CSV 导出
GET /api/v1/admin/knowledge/brands/template 下载导入模板
GET /api/v1/admin/knowledge/brand-recommend-rules 规则列表(分页+搜索)
POST /api/v1/admin/knowledge/brand-recommend-rules 新增规则
PUT /api/v1/admin/knowledge/brand-recommend-rules/{id} 修改规则
PATCH /api/v1/admin/knowledge/brand-recommend-rules/{id}/toggle 启用/停用切换
DELETE /api/v1/admin/knowledge/brand-recommend-rules/{id} 删除规则
POST /api/v1/admin/knowledge/brand-recommend-rules/import 规则 CSV 导入
GET /api/v1/admin/knowledge/brand-recommend-rules/export 规则 CSV 导出
GET /api/v1/admin/knowledge/brand-recommend-rules/template 下载导入模板

八、鉴权机制

配置 说明
auth.enabled=false 开发模式 所有接口放行
auth.enabled=true 生产模式 /api/** 需要 Bearer token

Token 流程:

  1. 外部系统通过 URL 参数 ?token=xxx 传入 JWT,前端写入 localStorage
  2. 发送消息时无 token → 弹登录框(微信小程序内跳转登录页,或跳转外部 login_url
  3. API 返回 401 → 前端自动弹登录框

公共路径(无需 token): /health, /api/v1/auth/**, /static/**, /


九、限流机制

层级 配置 作用
用户侧 rate-limit.* 每用户/IP 的聊天频率限制
百炼 API dashscope.rate-limit.* 全局 LLM 调用量控制(默认关闭)

仅对 POST /chat/ 接口生效,药品库查询不限制。


十、QA 缓存

  • 同一问题(规范化文本)24 小时内只调一次 LLM
  • 缓存存 Redis,key: aiyaodian:qa:{sha256}
  • 并发控制:同一问题同时到达的请求,只有第一个调 LLM,其余等待
  • 清缓存:redis-cli KEYS "aiyaodian:qa:*" | xargs redis-cli DEL

十一、数据库

核心表

用途
drugs 药品主表(含 sections JSONB)
drug_chunks 药品分块 + pgvector 向量
conversations 对话会话
messages 对话消息(含 user_key 隔离、brand_recommendations JSONB 持久化)
brands 品牌药品详情(长文本信息 + 跳转链接)
brand_recommend_rules 品牌推荐匹配规则(关键词 → 品牌 ID 映射 + 层级)
users 用户表

用户隔离

  • 登录用户:user_key = JWT subject
  • 匿名用户:user_key = ip:xxx.xxx.xxx.xxx

数据库迁移

-- 品牌推荐功能(2026-07-29)
-- 执行: psql -h <host> -U <user> -d pharmacopoeia -f database/migrate_brand_recommend.sql
-- 创建 brands 表、brand_recommend_rules 表(含外键),messages 加 brand_recommendations jsonb

-- 最近新增字段
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS user_key VARCHAR(128);
ALTER TABLE messages ADD COLUMN IF NOT EXISTS user_key VARCHAR(128);

十二、前端(static/index.html)

功能

  • AI 对话:文本 + 图片/视频上传识别
  • 药品库:分页浏览、搜索、详情查看
  • 本地持久化:IndexedDB(128MB/500条),微信 WebView 降级为后端恢复
  • 登录:微信小程序内跳转登录,桌面端弹框提示

小屏适配

@media(max-width:400px) 针对 iPhone SE、小米 12 等设备缩小按钮和间距

来源明细去重

后端 buildSources() 两层去重:映射前原始去重 + 映射后别名去重


十三、药品库数据

  • 数据来源:2025年版《中国药典》一至四部 DOCX 文件
  • 解析脚本:data-pipeline/docx_ingest.py
  • 排除类别:通则、凡例与纲要、凡例、概要、生物制品(默认不显示)
  • 药品清单:docs/all_drugs.txt(共 6316 种)

十四、常见问题

Q: 缓存未生效? 清 Redis 缓存后重启:docker exec pharmacopoeia-redis redis-cli KEYS "aiyaodian:qa:*" | xargs redis-cli DEL

Q: 分页不正常? 前端参数名必须是 pageSize(驼峰),不是 page_size。后端 @RequestParam 严格匹配。

Q: 微信 WebView 无历史记录? IndexedDB 可能被清理,前端会自动降级调 /api/v1/chat/recent-messages 从后端恢复。

Q: 对话框按钮被遮挡? 已添加 @media(max-width:400px) 小屏适配,如仍有问题可调整媒体查询断点。


十五、后续待办

  • 上线 auth.enabled=true,配置 AUTH_ENABLED 环境变量
  • 微信小程序跳转登录页联调
  • 百炼 API 限流值客户确认后启用
  • 定期清理过期对话数据(messages 表)
  • 监控 DashScope API 调用量
  • 运营管理端对接品牌管理和匹配规则 CRUD 接口

十六、品牌推荐功能(2026-07-29 新增)

架构

用户提问 → RAG检索 → LLM生成回答(SSE流) → buildSources提取药名
→ BrandRecommendService 分层关键词匹配 → SSE 注入 brand_recommend 事件
→ 结果持久化到 messages.brand_recommendations JSONB

两张新表

brands (品牌表)
├── id, name
├── 长文本: function_indication, usage_dosage, contraindication,
│           ingredients, properties, specification, adverse_reactions,
│           precautions, execution_standard, storage
├── jump_url          — 跳转链接
├── description       — 简要描述
├── sort_order        — 排序
└── is_active         — 启用开关

brand_recommend_rules (匹配规则表)
├── id
├── keyword           — 匹配关键词
├── brand_id          — FK → brands.id
├── tier              — 优先级(1最高)
└── is_active         — 启用开关

匹配逻辑

  1. sources[].name 提取药名集合
  2. 按 tier 升序(1→2→3…)逐层匹配:source name 包含 keyword
  3. 命中即停止,不再尝试更低 tier
  4. 所有 tier 的 source 匹配都无命中 → 回退到全文匹配(fullAnswer 包含 keyword),同样按 tier 逐层
  5. 同层级内去重,返回匹配到的规则对应的品牌详情(含所有长文本 + jump_url)

SSE 事件顺序

token → token → ... → brand_recommend → meta → 完成

brand_recommend 事件数据格式:

{
  "recommendations": [
    {
      "brand_id": 1,
      "matched_keyword": "布洛芬",
      "tier": 1,
      "brand_name": "芬必得布洛芬缓释胶囊",
      "function": "用于缓解轻至中度疼痛...",
      "usage_dosage": "口服。成人一次1粒...",
      "jump_url": "",
      ...
    }
  ]
}

历史持久化

品牌推荐结果在 SSE 流结束后写入 messages.brand_recommendations(JSONB 列),用户翻看历史对话时直接从该字段读取,不受后续规则表变更影响。getConversationDetailgetRecentMessages 接口已包含该字段。

部署前注意

部署前必须执行 database/migrate_brand_recommend.sql 创建新表和新列。该脚本使用 CREATE TABLE IF NOT EXISTSADD COLUMN IF NOT EXISTS,可重复执行。


十七、已知问题记录

17.1 HttpServletRequest 代理对象线程安全问题(已修复 2026-07-24)

现象: SSE 流式回答写完后,日志出现:

IllegalStateException: No thread-bound request found

调用链:Reactor doOnComplete 回调 → getCurrentUserKey() → request.getRemoteAddr() → RequestContextHolder 抛出异常

根因: HttpServletRequest 以构造注入方式注入 Controller,Spring 注入的是代理对象(RequestObjectFactory),每次方法调用都通过 RequestContextHolder(ThreadLocal)查找当前线程绑定的 request。Reactor 回调运行在 reactor-http-nio 线程上,原始请求线程已释放,ThreadLocal 中不再有 request。

修复: Controller 所有请求方法入口处提前调用 getCurrentUserKey() 获取 plain String,在 Reactor lambda 中只使用已捕获的 final 局部变量,绝不在回调线程中访问 request 代理对象。

17.2 SSE 流式结束后 AccessDeniedException(已修复 2026-07-24)

现象: SSE 流式 (/api/v1/chat/stream) 完成时日志出现:

AccessDeniedException: Access Denied
  at AuthorizationFilter.doFilter

堆栈中出现 AsyncContextImpl$AsyncRunnable.run,前端收到网络异常。

根因: Spring MVC SSE 响应走 Servlet 3.0 异步模式。流式写入完成后,Tomcat 触发异步分发(Async Dispatch)通知框架。JwtAuthFilter extends OncePerRequestFilter 默认跳过异步分发(shouldNotFilterAsyncDispatch() 返回 true),导致 SecurityContext 为空。后续 AuthorizationFilter 检查时找不到认证信息,抛出 AccessDeniedException。走 ExceptionTranslationFilterGlobalExceptionHandler → 前端 SSE 连接中断 → 显示网络异常。

修复: JwtAuthFilter 覆写 shouldNotFilterAsyncDispatch() 返回 false,让异步分发时也重新解析 JWT 并设置 SecurityContext