1. 整体设计先想清楚查询与推荐到底是什么关系1.1 这个项目不是做一个药品字典Spring Boot Java 做中医药方非处方药物的查询与推荐标题听起来像是一个普通的信息管理系统很多第一次接触的人会下意识地说不就是给药品表加个搜索框再用 SQL 查一下吗真正上手之后才会发现这个项目同时压着三条业务线非处方药信息要整理得有据可查中医药方背后的症状、功效、禁忌关系要能结构化表达还要根据用户输入的症状给出合理的推荐结果并且得让用户看得懂推荐理由。我最近在帮一位朋友梳理类似的健康类后端项目前前后后踩了不少坑走了不少弯路。写这篇文章不是给你贴一段能跑的代码而是把从需求拆解、领域建模到推荐逻辑落地、上线排查的完整思路讲清楚。适合正在做药品查询、健康科普类网站或者想给 Spring Boot 项目增加“基于症状推荐”能力的开发者看产品经理也可以重点读前两章理解为什么这个系统不能简单做成筛选器。这个系统说到底是一个辅助决策入口。用户不知道自己该买哪种非处方药所以输入“咳嗽”“嗓子疼”“拉肚子”这样的自然语言系统要做的不是把这几个字扔给数据库做模糊匹配而是理解症状背后的中医语义再结合药品的适用范围、禁忌人群、OTC 分类把最值得参考的几种药推荐出来。查询是刚性需求推荐是增值体验两者不能分开做。1.2 角色和主流程先定好后面才不会乱我会先梳理角色。普通用户是核心使用者他们不掌握专业术语输入内容可能是“头疼”“胃胀”“孩子积食”这类口语。药师或运营人员负责维护药品信息、症状词库和反馈审核他们不关心算法但关心数据改完是否立刻生效。系统管理员负责用户权限、日志和基础字典配置。主流程值得画一张纸用户输入原始文本系统先做文本归一化把口语映射成标准症状术语然后根据症状和药品适应症的关联关系召回候选药品再通过禁忌过滤和多因子评分排序最终返回推荐列表。每一步都可能失败比如输入无法识别就要引导用户补充更多信息比如禁忌命中就要直接剔除而不是降权。我见过不少项目一上来先写推荐算法结果词库是空的、OTC 标识也没有人维护最后算法再“高级”也只能在垃圾数据上跳舞。所以整体设计的第一原则是先把数据结构和基础检索做扎实再谈推荐。1.3 非处方药、中医药方、推荐结果三者不能混为一谈这个标题里藏着三个关键概念它们经常被混在一起。非处方药指的是不需要凭执业医师处方就能自行购买和使用的药品业务上可分为甲类 OTC 和乙类 OTC安全等级和销售管理要求有差异。中医药方按我的理解在这里更多是指中成药背后的组方逻辑和功效主治比如“清热解毒”“祛风散寒”而不是传统意义上的汤剂处方。推荐结果是系统基于公开药品信息给出的参考列表它不能也不应该承担诊断职责。如果这三层边界不清晰代码里就会出现一些混淆。比如把“自认为功效相似”的中成药直接替换推荐不考虑不良反应和禁忌或者把处方药混进非处方药查询结果给用户带来误用风险。合规红线不是上线前补一段免责声明就够而应该在数据模型上就区分开OTC 字段、批准文号、是否双跨、禁忌人群都是必须字段不能靠人工记住。我现在带的这种项目宁可功能少一点也要保证每条数据能追溯到来源。推荐可以做但推荐的边界必须可控用户输入“发热三天不退”这类危险信号时不应该硬推药品而是明确提示及时就医或咨询专业人员这是产品安全的一部分。2. 领域建模与知识库把中医药方语义变成后端能算的数据2.1 核心实体从六张表开始的领域模型数据库设计是这类系统最重要的地基。我参考实际业务场景整理了一套最少可用的表结构不建议一开始搞太复杂的图谱先把核心关系跑通后面再慢慢加。第一张是药品表用于存储非处方中药的基础信息。关键字段包括药品名称、批准文号、OTC 类型、生产厂家、剂型、包装规格、服用方法、贮藏条件、有效期和说明书全文。注意“批准文号”一定要保留一个药品可能有多个文号也可能同名但一个属于 OTC、一个属于处方药这两类必须严格区分。第二张是方剂表用于记录中成药对应的组方关系也就是“这个药由哪些中药成分组成”。然后是药材表存放每味中药的名称、性味归经、功效分类等基础信息。方剂表和药材表通过中间表关联这个中间表还要保存用量和角色也就是“君药、臣药”这类组方位置方便后续做功效解释。症状表要单独建。这张表用来存标准症状术语比如“咽喉肿痛”“风寒感冒”“食积停滞”每条症状记录标准名称、分类、常见口语表达。与之关联的是药品适应症关系表记录某药品适用于哪些标准症状关系可以带权重比如“主治”和“兼治”在推荐时的贡献不同。最后是禁忌关系表记录某药品不适用于哪些人群或症状比如孕妇禁用、儿童慎用、对某种成分过敏者禁用。这张表在推荐过滤阶段非常重要宁可数据难维护也不能让推荐结果踩中禁忌。有个容易被忽略的点字段设计时要把“数据来源”和“更新时间”存下来。推荐系统上线后总有人问“这条推荐依据是什么”如果没有数据来源排查起来只能靠猜非常被动。2.2 症状同义词把用户的人话翻译成标准术语我最想强调的永远是症状归一化。用户不会按数据库里的术语提问他们会说“拉肚子”“闹肚子”“一天跑好几次厕所”而标准症状术语可能只是“腹泻”。如果系统只做字面匹配召回率会非常难看。第一种办法是维护同义词表。这是一张二维表一个标准症状对应的多个口语词。比如“咽喉肿痛”可以关联上“喉咙痛”“嗓子疼”“嗓子痛”“咽痛”“嗓子干痛”。维护的来源是客服记录、用户搜索日志以及药师人工整理。我在项目里会让运营每月批量导入一批真实用户 query由药师审核后加入同义词表这个闭环比模型还要重要。第二种办法是用分词工具做补充。Spring Boot 项目里可以引入中文分词组件对原始输入做分词以后再拿每个词去同义词表里查询。如果几个词同时映射到同一个标准症状那这条用户输入的意思基本就明确了。但要注意分词不能解决所有问题。中药症状词汇里有很多四字短语比如“脾胃虚弱”“气血不足”这些词如果不提前维护到词典里分词器很可能把它拆得七零八碎。我建议把标准症状全部单独维护成用户词典让分词器优先识别。这个环节没有太多技术含量却是推荐准确率的第一道闸门。我的经验是宁可先手工维护 500 条高频词也不要急着上向量化。因为症状词是高度受控的没有丰富语义模型收益有限而规则词表的效果几乎是立竿见影的。2.3 药品知识从哪里来上线后怎么更新做这类系统不能凭空造数据。药品说明书是最直接、最可靠的信息来源OTC 药品的说明书是经过审批的里面的功能主治、禁忌、注意事项都有明确表述。但说明书通常是 PDF 或图片需要经过结构化和人工校对才能入库。上线初期不能一次性把整个药品库全塞进去建议先做一个迷你药品库覆盖常见的高频症状比如感冒、咳嗽、消化不良、皮肤瘙痒、跌打损伤。等推荐流程验证通过后再分批扩品类。每次新药入库要有一套校验规则必须填写 OTC 类型必须关联至少一条标准症状必须至少填写禁忌信息。如果某药连适应症都写不清楚宁可不入库因为用户误服之后的责任不是上线一个人能承担的。数据更新要做成定时任务。监管层面的 OTC 目录和市场状态会变化某些药品可能被调整为处方药这类变动必须同步到系统里。同时保留旧数据版本方便追溯历史查询和推荐结果。上线后至少每季度做一次数据核对不要想着“录完就万事大吉”。3. 推荐逻辑先用规则保证准确再用评分提升体验3.1 召回阶段先把候选集缩小到合理范围药品查询和药品推荐是两个动作。查询是用户已经知道“我要看什么”推荐是用户只有模糊需求需要我们帮忙缩小范围。推荐的第一步不是排序而是召回也就是从全部药品里挑出与用户症状相关的候选集。常规做法是这样的用户输入的原始文本经过症状归一化之后会得到一个标准症状列表。比如输入“咳嗽有痰嗓子疼”可能映射出“咳嗽”“咳痰”“咽喉肿痛”。然后拿着这些标准症状去药品适应症关系表里查询对应的药品 ID形成候选集合。这里有个关键决策多个症状之间用“与”还是用“或”。如果严格要求“包含全部症状”候选集很小但容易漏掉只写了一个对症说明的药品如果满足任一症状就有资格进入候选召回范围变大但后续排序必须跟上。我的折中方案是“至少命中一个症状”进入候选但把“命中症状数”作为后续评分的强力特征这样既能保证召回又不会让完全不相关的药混进来。召回阶段还要做一次快速过滤。去重是基础操作同一药品因为不同规格可能有多条记录要合并成同一展示项还要过滤掉已被标记为下架、过期或暂停销售的药品最关键的是直接剔除命中禁忌的药品而不是把它排在后面。禁忌过滤是刚性的一旦命中无论其他分数多高都要拿掉。3.2 排序阶段多因子评分而不是单一关键词权重候选集出来之后需要使用评分模型把最合适的药排在前面。最笨但有效的方法是线性加权评分每一项都能解释也方便调参。我常用的评分公式是总分 0.35 症状匹配分 0.25 功效覆盖分 0.15 适应症强度分 0.15 用户反馈分 0.1 数据新鲜分。其中症状匹配分根据“用户命中了几个标准症状”以及“这些症状在药品说明书中的主诉地位”计算功效覆盖分看的是药品功效与用户症状集合的重合度适应症强度分用来说明“主治”比“兼治”更有分量用户反馈分来自在线点击和上轮用户反馈数据新鲜分则是对更新频率高、来源清晰的药品小幅加成。举个例子。用户症状是“咳嗽 咽喉肿痛”。候选药品 A 的适应症写的是“适用于咳嗽痰多”B 的适应症写的是“用于咽痛、咳嗽”C 的适应症写的是“用于风热感冒引起的发热、头痛、咽喉肿痛”。按照公式C 命中了两个症状且与主诉相关性更强评分会明显高于 AA 虽然命中咳嗽但漏掉了咽喉肿痛。这个结果和药师的判断基本一致。权重的初始值可以靠人工经验定但后续一定要用线上反馈数据校准。千万不要一开始就用复杂模型排序线性加权模型跑出问题来运营和数据研发都能看懂。深度模型固然上限高但在这种低频决策、高合规要求的场景里可解释性远比模型复杂度重要。3.3 推荐理由和安全兜底返回结果不能是裸列表推荐系统如果只返回一个药品列表用户会怀疑“为什么是这几样”。所以接口要同时返回解释字段。我用的是三段式理由模板命中症状、对应功效、使用提示。比如“因为你提到了‘咳嗽’和‘咽喉肿痛’该药品适用范围包含‘咳嗽、咽喉肿痛’请按说明书使用症状加重请咨询专业人员”。这段理由不是自然语言生成而是由结构化数据拼接而成。每条理由都可以追溯后端日志里可以存下用户 query、命中的症状 ID、候选药品 ID、评分明细以及命中/过滤原因。这样运营要处理用户纠错时能很快定位到是哪一条匹配出了问题而不需要在代码里打断点。安全兜底也必须落在代码逻辑里。比如用户输入内容中出现“昏迷”“剧烈胸痛”“呼吸困难”等急症关键词系统应该直接返回“请尽快就医或拨打急救电话”不做任何药品推荐。再比如用户是孕妇或儿童这个信息如果能在输入里识别到也要优先进入人群禁忌过滤并把原因告知用户。我见过一些系统把“不推荐诊断”只放在页面底部的免责声明里这是不够的。产品交互上推荐结果的标题应该写“可参考的非处方药信息”而不是“推荐你吃”措辞要克制这既是合规需要也是让用户正确理解推荐结果边界的一部分。4. Spring Boot 后端架构与核心实现细节4.1 工程结构、依赖与配置我会用 Spring Boot 3.x Java 17 作为基础框架。项目不大不需要微服务一个单体应用加 Redis 缓存就够了。依赖上主要用到 spring-boot-starter-web、MyBatis-Plus、MySQL 驱动、Redis 客户端、参数校验组件以及测试组件。工程结构我习惯按领域分包而不是按 controller/service/mapper 这种技术分层铺开com.example.otcrecommend ├── controller // 对外API ├── service // 业务逻辑、推荐服务 ├── repository // 数据访问 ├── model // 实体和DTO ├── recommend // 召回、过滤、评分、解释 ├── symptom // 症状归一化与同义词匹配 └── config // Redis、MyBatis、线程池配置这样做的最大好处是推荐相关代码不会散落在多个 service 里后续调整规则时改动范围收敛。配置上MySQL 连接池建议用 HikariCPRedis 配置项单独抽到配置类。数据库表结构里药品适应症关系表一定给 symptom_id 和 drug_id 建联合索引这是推荐查询最常用的路径。4.2 核心 API 设计对外接口要稳定。我一般至少设计三个接口药品查询接口、推荐接口、药品详情接口。推荐接口的请求参数不要设计得太花哨一个 query 参数加一个 limit 参数就够。用户输入越自然越好不要让用户自己去选“症状标签”。推荐接口的简化返回结构如下{ query: 咳嗽有痰, normalizedSymptoms: [咳嗽, 咳痰], items: [ { drugId: 1001, drugName: 示例止咳颗粒, otcType: 乙类, reason: 因为你提到了“咳嗽”和“咳痰”该药品适用范围包含“咳嗽、咳痰”。, precaution: 请按说明书使用服用三天症状未缓解请咨询专业人员。, score: 0.86 } ] }药品查询接口则偏向搜索场景支持按药品名称、功效关键词、批准文号搜索结果按相关性和销量排序。搜索接口和推荐接口在业务上是两个入口但在后端可以共用一部分检索和缓存逻辑避免重复代码。接口层还需要加统一的错误处理。推荐结果为空时不要直接返回 200 加空数组而应该返回一个提示码让前端展示“请补充更多症状描述”。我在实际项目里遇到过前端把空推荐列表当成“无药可用”并展示空白页面用户体验极差。返回结构里必须包含给用户看的提示信息。4.3 推荐服务核心流程实现推荐服务的代码不复杂但流程要清晰。我用一个服务类串起四个阶段文本归一化、候选召回、禁忌过滤、评分排序。核心代码大致如下Service public class RecommendService { private final SymptomNormalizer symptomNormalizer; private final DrugCandidateRepository candidateRepository; private final DrugScoreCalculator scoreCalculator; public RecommendResult recommend(String rawQuery, int limit) { ListString symptoms symptomNormalizer.normalize(rawQuery); if (symptoms.isEmpty()) { return RecommendResult.notRecognized(未识别到明确症状请补充描述); } ListDrugCandidate candidates candidateRepository.findCandidatesBySymptoms(symptoms); ListDrugCandidate afterSafetyFilter candidateRepository.filterByContraindication(candidates, symptoms); ListScoredDrug scored afterSafetyFilter.stream() .map(drug - scoreCalculator.score(symptoms, drug)) .sorted(Comparator.comparing(ScoredDrug::getScore).reversed()) .limit(limit) .collect(Collectors.toList()); return RecommendResult.build(symptoms, scored); } }每个方法都对应一个独立的小类或组件。SymptomNormalizer 负责查同义词表DrugCandidateRepository 负责查询候选集DrugScoreCalculator 计算每一项得分并保留明细。这样写的好处是单元测试可以分别覆盖某个用户 query 映射到了哪些标准症状某个禁忌关系是否真的被过滤掉某个候选药的分数是否符合预期。我特别提醒一点不要在推荐服务里直接写一大段 SQL 拼接逻辑。候选召回阶段宁可拆成几个查询再用代码做聚合也不要让 SQL 承担所有判断。药品数据量本身不大性能瓶颈不在 SQL 而在缓存命中率后面再讲缓存策略。4.4 缓存与性能优化策略推荐接口比查询接口负载高因为每次推荐都要做一轮症状映射和召回过滤。如果每次都实时算数据库压力会很大响应延迟也会波动。我的方案是用 Redis 做两层缓存。第一层缓存的是症状归一化结果。用户 query 经过归一化后把“原始文本 hash - 标准症状列表”存进 Redis有效期可以设长一点比如 24 小时。因为同一句口语描述在短时间内不会有语义变化这样能省掉不少重复分词和词典查询。第二层缓存的是推荐结果列表。这里要小心推荐结果因为药品上下架或反馈变化需要更新不能缓存太久。我会设置 10 到 30 分钟的有效期同时用“主动失效 被动失效”双策略。运营修改药品数据后调用缓存删除接口把涉及该药品的缓存 key 删掉如果用户反馈“推荐不合适”也删除对应 query 的缓存。缓存 key 不要用原始 query 做字符串拼接最好对原始 query 做标准化后再取 hash。否则同一句“咳嗽有痰”可能因为全角半角、空格、大小写差异生成不同 key缓存命中率会低得离谱。我发现很多团队根本没注意这层导致 Redis 里塞了几万个其实语义相同的 key。数据库层面给查询接口需要高频使用的字段建索引药品名称字段可以建普通索引标准症状名字段建唯一索引药品症状关系表的联合索引在前面已经说过。如果药品库超过十万条可以再考虑引入全文检索引擎但项目早期不要给自己加戏。5. 实战里的典型问题与排查实录5.1 中文症状搜索MySQL 默认模糊查询太慢第一版我把症状搜索写成了 LIKE %cough%也就是对每个用户输入词都做全表扫。本地测试数据少看不出问题等到药品和症状关系表里有了上万条记录一次推荐要跑好几个 LIKE响应直接掉到两秒以上。后来我给症状表加了 MySQL 的全文索引。中文全文索引有个坑默认分词器对中文支持不好必须显式指定 ngram parser。创建语句类似这样ALTER TABLE symptom_keyword ADD FULLTEXT INDEX ft_symptom_name (symptom_name) WITH PARSER ngram;同时把 ngram_token_size 设置成 2基本能覆盖大多数中文症状词的检索场景。但要注意ngram 索引会让索引体积增大不少更新数据库时也会有额外开销所以一般只在高并发查询入口用后台维护页面不需要走这条索引。如果项目已经接入了 Elasticsearch当然可以把症状检索放到 ES 里。但对一个中小型 Spring Boot 项目MySQL 全文索引足够应付万级数据。最忌讳的是为了“以后可能要支持模糊搜索”提前引入一套搜索引擎把运维复杂度拉高好几倍。5.2 同名药品和双跨药品一个名字可能是两种身份药品数据里最容易踩到的坑是“同名不同品”。同一个药品名称可能同时存在 OTC 药和处方药两种规格甚至同一名称下还有不同生产厂家、不同批准文号的产品。如果查询接口没有强制带上 OTC 类型条件用户搜出来的结果里混着处方药这已经不只是推荐不准的问题而是安全风险。我在一次联调时发现把某个清热类中成药上传到药品库时不小心混入了处方药规格结果用户在推荐页看到了它点进详情才发现“本品为处方药请凭医生处方购买”。排查后才发现是运营导出表格时按名称去重把两个不同药品合并成了一条记录。解决方案是核心的查询和推荐 SQL 永远显式带上 otc_type 条件药品表的唯一索引不再只建在名称上要建在“批准文号 OTC 标识”上后台导入数据时必须校验双跨药品并设置“是否允许非处方展示”开关。双跨药品要单独标记面向普通用户的推荐结果中只允许展示非处方身份对应的批准文号和说明书内容。5.3 推荐结果“不准”先看召回再看排序用户反馈“推荐的药和我症状没关系”时按照我的习惯不会直接改权重。第一步先把这次推荐的日志调出来看用户输入被归一化成了哪些标准症状。如果症状映射错了那就是词库问题和评分模型无关。举一个实际例子。用户输入“后背酸痛”系统归一化后只得到“酸痛”而“后背”这个位置信息被丢了召回结果全是风湿关节用药用户当然觉得不准。问题根源不在召回和排序而在症状词库里没有“腰背酸痛”这个标准症状。把标准症状补上再把“后背”“腰背”加进同义词表推荐质量立刻提升。如果症状映射没有问题候选集也有几十个药但排序不对那就打开评分明细看看是哪一项分数把某个不太相关的药拉上来了。我习惯在返回结果里附带一个 debug 字段只有后台登录用户可见里面存着每个候选药的症状匹配分、功效覆盖分和过滤原因。有了这个字段调整权重就不再是玄学。还有一个经常被忽略的原因排序时没有加入“用户真实场景”信息。同一句“咳嗽”一个是孩子咳嗽一个是老人咳嗽适合的药范围差异很大。如果系统能从用户输入中识别出“孩子”“小孩”“宝宝”就要把这些词作为特殊人群标签加入过滤条件。这比单纯优化打分更能避免推荐风险。5.4 MyBatis 查询慢和 N1 问题推荐结果需要展示药品名称、适应症、OTC 类型、禁忌信息、反馈分数。如果每次查药品列表时循环里再逐条查症状、禁忌那就踩了 N1 问题。笨办法加缓存解决不了根本问题因为每个候选药都要查一遍数据库连接占用会瞬间打满。我的做法是先把候选药 ID 一次性取出来然后用批量查询把它们关联的症状、禁忌、反馈数据全部查出来再在内存里组装。MyBatis 里用 嵌套映射可以做到或者直接查出平铺结构在 Service 层手动分组。后一种方式更好排查因为你能直接看到当前组装到哪一步。排查工具也很重要。Spring Boot 项目建议把慢 SQL 日志打开打印超过 200 毫秒的查询Redis 客户端也要记录耗时。发现某个推荐接口平均延迟超过 500 毫秒先看是不是缓存没命中再看是不是某条 SQL 没走索引最后才怀疑代码逻辑。大多数情况下调参的速度远不如加索引和加缓存来得快。6. 上线后怎么评估与持续迭代6.1 离线指标与在线埋点推荐系统上线后不能用“感觉还行”来评价。我在离线阶段会准备一批人工标注好的测试数据比如 100 条用户症状描述每条由药师标注“应该推荐哪几类药”。然后用离线测试集跑一遍推荐接口计算两个基本指标推荐结果里包含正确药品的比例以及正确候选排在前五位的比例。前者对应召回和准确率后者对应排序质量。在线指标需要埋点。每次推荐请求发出后记录 query、返回药品 ID 列表、排序位置、用户是否点击、点击后是否查看详情。有了这些数据可以计算点击率、位置偏置、相同症状条件下不同药品的点击集中度。不要把埋点做得太重埋几个最有用的字段就够了数据质量比数据量重要。我发现很多团队忽略了一个基础动作把“无结果”和“无点击”分开统计。用户输入一句很模糊的话系统返回提示信息这不算推荐失败但如果用户明确表达了一个常见症状系统却因为词库缺失返回空结果这就是必须修复的缺陷。运营日报里要重点关注后者。6.2 用户反馈闭环把“这个药不合适”收回来推荐结果的反馈入口不能只做一个“点赞”或“点踩”。我建议产品设计成三种反馈“不适用”“不够准”“有用”。用户点“不适用”说明这款药可能与他实际情况冲突比如人群禁忌或症状不符用户点“不够准”说明排序不够好但候选方向可能没错点“有用”则是对当前推荐的肯定。反馈数据要进入两个地方。一个是评分模型的用户反馈分每次用户反馈后对应药品在未来同类症状下的得分会相应调整另一个是运营审核队列后台定期把“不适用”数量较高的药品标记出来由药师检查是否有数据错误或需要加禁忌条件。这样形成闭环后即使用户基数不大推荐准确率也会越用越稳。反馈调整要小心“反馈污染”。比如某用户因为看到自己绝对不能吃的一种药而点“不适用”这其实是禁忌过滤没做好不是排序问题。所以反馈数据落库时要同时保存关联的用户症状和当时的推荐理由方便运维追溯。6.3 合规展示与信息更新机制合规不是一句口号是要在产品页面和接口返回里都体现出来的操作。推荐结果区域要固定展示“本结果基于公开药品信息仅用于查询参考不能代替诊断和治疗”的说明药品详情页要展示说明书的核心内容包括禁忌、注意事项、贮藏方法不要只放一张图片让用户自己看。系统里还要设置内容更新提醒。药品说明书不是永不过期药监层面和厂家都可能有修订。我采用的办法是给每条药品数据记录一个“信息复核日期”超过 180 天没有审核的药品从推荐结果和搜索排序中降低权重并在后台提示运营人员复核。这条机制刚开始会被嫌麻烦但确实能在问题扩大之前拦住风险。接口层面也要有版本控制。推荐接口如果未来从规则升级成模型不能直接改旧接口建议新增 /v2/recommend旧接口保留一段时间。健康类系统尤其要维护一个稳定的 API 契约因为前端、小程序和其他合作方可能已经基于旧接口做了二次开发。6.4 一点个人实操体会这些模块跑通之后我再回头看这个项目最重要的经验不是技术选型多高级而是第一版把“安全边界”和“数据质量”做扎实。推荐算法可以后补规则评分简单但有效可解释性也好一旦症状词库和禁忌数据维护得当用户会明显感觉到推荐结果比“随机搜几个药”靠谱得多。让我特别印象深刻的是一次小流量试运行因为没有提前做禁忌校验一个用户输入了“孕妇咳嗽”系统差点把某款不宜孕妇服用的药推荐出来。幸好日志审查阶段发现了这条高危险信号紧急把人群禁忌过滤前置才避免了上线后的严重事故。这件事让我之后做任何健康相关系统都会先安排人专门审禁忌规则。如果你正在做类似项目我建议不要急着写代码先把药品说明书读五十条把高频症状列表和同义词表手工整理出来再打开编辑器。这个准备工作看起来慢实际上是在给整个项目打地基。后续如果要扩展成更智能的推荐也一定是在这套结构化数据和规则评分跑稳之后才考虑的事。