# 品牌药品推荐 - 后端接口分析与方案(已完成实施)
状态:已实施 (2026-07-29) 详见 HANDOVER.md 第十六章节。
用户提问 → RAG检索 → LLM生成回答(SSE流式) → 从sources中提取药名 →
关键词匹配(异步) → 返回匹配的品牌推荐 → 前端展示在回答末尾
buildSources 返回的 sources 列表当前 chatStream 尾部 tailFlux 中的 sources 结构(来自 buildSources line 720+):
{
"drug_id": "xxx",
"name": "布洛芬缓释胶囊", // ← 匹配目标
"section": "用法与用量",
"category": "化学药品",
"source": "2025版药典二部",
"excerpt": "..."
}
| 规则 | 说明 |
|---|---|
| 匹配方式 | sources[i].name 包含关键词(忽略大小写) |
| 示例 | 标题"布洛芬缓释胶囊"包含关键词"布洛芬" → 命中 |
| 同一标题多命中 | 去重展示所有匹配的品牌 |
| 无命中 | 不展示推荐区域,不影响正常回答 |
| 时机 | AI回答生成后,meta事件发送前(不增加用户可感知延迟) |
现有流: ...[token1] [token2] ... [tokenN] → [meta: {sources, intent, cid}] → 完成
改造后: ...[token1] [token2] ... [tokenN] → [brand_recommend: {...}] → [meta: {...}] → 完成
新增 SSE 事件类型 brand_recommend,携带匹配结果。
brand_recommend_rulesCREATE TABLE brand_recommend_rules (
id SERIAL PRIMARY KEY,
keyword VARCHAR(100) NOT NULL UNIQUE, -- 匹配关键词(药名)
brand_name VARCHAR(200) NOT NULL, -- 推荐品牌药品名称(必填)
description VARCHAR(500), -- 推荐品牌药品说明(可选)
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 启用/停用开关
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
BrandRecommendRule.java — entityBrandRecommendRuleRepository.java — 查询启用的规则BrandRecommendService@Service
public class BrandRecommendService {
// 核心方法:根据 sources 列表返回匹配的品牌推荐
public List<BrandMatchResult> match(List<Map<String, Object>> sources) {
// 1. 从 sources 提取所有 drug name
// 2. 查询所有 is_active=true 的规则
// 3. 对每个 name 做包含匹配(忽略大小写)
// 4. 去重,返回匹配结果
}
}
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/admin/brand-recommend-rules |
列表(分页+搜索) |
POST |
/api/v1/admin/brand-recommend-rules |
新增 |
PUT |
/api/v1/admin/brand-recommend-rules/{id} |
修改 |
DELETE |
/api/v1/admin/brand-recommend-rules/{id} |
删除 |
POST |
/api/v1/admin/brand-recommend-rules/import |
Excel 导入 |
GET |
/api/v1/admin/brand-recommend-rules/export |
Excel 导出 |
在 chatStream 的 tailFlux 中,LLM 流结束后、meta 事件前,插入品牌推荐:
Flux<ServerSentEvent<String>> tailFlux = Flux.defer(() -> {
List<Map<String, Object>> sources = buildSources(docs);
// 新增:品牌推荐匹配
List<BrandMatchResult> brandRecs = brandRecommendService.match(sources);
// 先发 brand_recommend 事件,再发 meta
return Flux.concat(
buildBrandRecommendEvent(brandRecs),
buildMetaEvent(sources, intent, cid)
);
});
需求原文说匹配对象为「参考原文标题」,即 sources[].name。但你说"回答了感冒灵颗粒"—这是匹配 AI 回答正文还是 sources?
sources[].name(参考原文标题,已结构化)前端期望的 brand_recommend 事件数据格式是什么?建议:
{
"event": "brand_recommend",
"data": {
"recommendations": [
{"brand_name": "芬必得布洛芬缓释胶囊", "matched_keyword": "布洛芬", "description": "缓解疼痛"},
{"brand_name": "999感冒灵颗粒", "matched_keyword": "感冒灵", "description": null}
]
}
}
需求说"用户翻看历史对话时,历史回答中的品牌推荐正常展示,不因后台规则变更而丢失"。这意味着品牌推荐结果需要持久化存储到 messages 表中,而不是每次渲染时重新匹配。
是否在 messages 表加一个 brand_recommendations JSONB 字段,保存当时匹配的品牌推荐结果?
确认是否现在创建 brand_recommend_rules 表和相关 Entity/Repository/Service,还是先只做方案分析?