药智通:一个把 AI 智能体放在核心位置的医药项目
在线体验:
- 项目介绍网站:medicine.zhangyichuang.com/
- 管理端:medicine-admin.zhangyichuang.com/
- 客户端:medicine-client.zhangyichuang.com/
体验账号:
账号:admin
密码:admin123
系统截图
管理端
客户端
项目背景
这个项目最初其实是我的毕业设计。
做这个项目的起点,是我自己一次比较普通的买药经历。之前有一次我生病了,是上呼吸道感染,喉咙特别难受,当时就急着去买药。但问题是我自己也不知道应该买什么药,只能在线问诊。也是那次经历之后,我突然想到:能不能用 AI 做一个可以一步步追问症状、辅助判断情况,再推荐合适药品的系统?
后来这个想法就慢慢变成了现在的药智通。
作为一名应届生,我很清楚自己和真实工作场景里的工程实践还有差距。所以在做这个项目时,我不想只做一个简单的 AI Demo,也不想只做一个普通的商城系统,而是希望把 AI 能力放进一个相对完整的业务场景里,看看它在真实流程里到底应该怎么设计、怎么调用工具、怎么控制上下文、怎么和业务系统协作。
药智通整体是一个面向医药零售和智能问诊场景的全栈项目,包含管理端、客户端、业务后端和 Python AI Agent 服务。
项目里有商品、订单、售后、用户、钱包、运营分析、知识库、图片识别和智能助手。但我真正想表达的重点不是电商,而是 AI 智能体设计。电商更多是一个承载场景,让 AI 能力不是停留在“聊天窗口”里,而是能和商品、订单、知识库、图谱、卡片交互这些业务能力结合起来。
技术栈
这个项目不是单独的 AI Demo,而是一个完整的前后端分离项目,并额外拆出了 Python AI Agent 服务。
| 模块 | 技术 |
|---|---|
| 管理端前端 | React、TypeScript、Vite、Ant Design、Ant Design Pro Components |
| 客户端前端 | React、TypeScript、Vite、NutUI、Zustand |
| 业务后端 | Spring Boot、Java、Maven、Dubbo、MyBatis-Plus、Spring Security、RabbitMQ |
| AI Agent | Python、FastAPI、LangChain、LangGraph、Pydantic |
| 数据存储 | MySQL、MongoDB、Redis、Elasticsearch、Milvus、Neo4j |
| 文件服务 | MinIO |
| AI 能力 | RAG、Skill、工具调用、图片理解、知识图谱、流式输出、智能体追踪 |
从职责上看,前端主要负责管理端和客户端的交互体验;Java 后端负责商品、订单、售后、用户、权限、支付、日志等确定性业务;Python AI Agent 负责智能问诊、多轮对话、工具调用、RAG 检索、图片理解和药品推荐。
我这样拆分,是因为传统业务和 AI 运行时关注点不一样。订单支付、权限校验、商品库存这些逻辑需要稳定、明确、可审计;而 AI Agent 更关注上下文管理、工具选择、模型调用、流式响应和运行轨迹。把它们拆成不同服务之后,边界会更清晰,后续扩展也更方便。
整体可以理解成:
React 管理端 / 客户端
|
v
Spring Boot 业务后端
|
v
Python FastAPI AI Agent
|
v
LangGraph 工作流 / Skill / 工具调用 / RAG / 知识图谱
|
v
MySQL / Redis / MongoDB / Elasticsearch / Milvus / Neo4j / MinIO
为什么单独做 Python AI Agent
我没有把大模型调用直接塞进 Java 业务后端,而是单独拆了一个 medicine-ai-agent 服务。
原因很简单:业务后端更适合处理确定性逻辑,比如订单、支付、售后、权限、商品库存;而 AI Agent 需要处理多轮对话、上下文记忆、工具调用、RAG 检索、图片理解、流式输出和智能体追踪,这些能力放在 Python 生态里会更自然。
所以整体链路大概是这样:
用户提问
-> FastAPI 接口
-> Assistant Service
-> 会话历史和记忆加载
-> LangGraph 工作流
-> 模型判断意图
-> 按需加载 Skill / 工具 / 知识库
-> SSE 流式返回
-> 前端展示文本、卡片或业务动作
这不是一个简单的“调用模型回答问题”,而是一个围绕智能体运行时做的工程化设计。
核心设计一:Skill 渐进式加载
我认为项目里比较重要的第一个设计,是 Skill 的渐进式加载。
很多 AI 项目一开始会把所有提示词、业务规则、工具说明全部塞进系统提示词里。短期看能跑,长期看会有几个问题:
- 上下文越来越长
- 成本越来越高
- 模型注意力被无关内容稀释
- 不同业务能力边界不清晰
- 后续维护 prompt 会非常痛苦
所以我这里没有一次性把所有 SKILL.md 全部注入上下文,而是先扫描 Skill 的元数据,只把 name、description 这类摘要信息放进系统提示词。
模型先知道“有哪些能力可以用”,但不会一开始就看到所有细节。只有当任务真的命中某个 Skill 时,才通过 load_skill 去读取完整技能说明;如果 Skill 下面还有 references 等资源文件,也可以再通过资源加载工具继续读取。
它的设计思路是:
先给能力目录
-> 判断当前任务是否需要某个能力
-> 需要时加载 Skill 全文
-> 还不够时继续加载 Skill 资源
这样做本质上是在控制上下文膨胀。AI 不是不知道能力,而是不需要一开始就把所有能力细节都看完。
核心代码大致是这个思路:
def before_agent(self, state: dict[str, Any], runtime: Any) -> dict[str, Any] | None:
"""运行前只预加载 Skill 元数据。"""
skills_metadata, skill_file_index = discover_skills_metadata(
self.scope,
skill_scope=self.skill_scope,
)
self._skill_file_index = skill_file_index
if "skills_metadata" in state:
return None
return {"skills_metadata": skills_metadata}
然后再把元数据渲染成系统提示词:
def build_skills_prompt(
skills_metadata: list[SkillMetadata],
*,
system_prompt_template: str | None = None,
) -> str:
"""把 Skill 元数据渲染成系统提示词片段。"""
template = SystemMessagePromptTemplate.from_template(
system_prompt_template or load_skills_system_prompt_template()
)
formatted = template.format(skills_list=_format_skills_list(skills_metadata))
return formatted.text
真正需要详情时,再读取完整 Skill:
@tool(description="按技能名称加载完整 SKILL.md 内容。")
def load_skill(skill_name: str) -> str:
"""真正需要细节时,再按需读取 Skill 全文。"""
selected_file = skill_file_index.get(normalized_skill_name)
full_content = selected_file.read_text(encoding="utf-8")
return f"Loaded skill: {normalized_skill_name}\n\n{full_content}"
这个设计让模型的上下文更轻,也让能力组织更清晰。后续新增 Skill 时,不需要把所有东西重新塞进一个巨大 prompt,只需要维护独立的 Skill 文件。
核心设计二:工具按需加载
第二个我认为比较重要的设计,是工具不是一次性全部挂给模型。
如果一个 Agent 一开始就挂上几十个工具,模型会面临很大的选择压力。它不仅要理解用户问题,还要在大量工具里判断应该调用哪个。工具越多,越容易出现误选、乱调、重复调用的问题。
所以我的设计是:先给模型一个“可加载工具目录”,真正需要某类业务能力时,再通过 load_tools 按精确工具 key 加载。
比如管理端工具会按领域分组:
order 订单相关工具
product 商品相关工具
after_sale 售后相关工具
user 用户相关工具
analytics 运营分析相关工具
模型不是一上来就看到所有具体工具,而是先知道有哪些领域可以加载。等它确认当前任务需要订单详情或商品查询时,再加载对应工具。
这里 load_tools 的参数也被设计得很克制,只保留 tool_keys,不让模型额外输出无意义字段:
class LoadToolsRequest(BaseModel):
"""动态工具加载请求,只允许传 tool_keys。"""
model_config = ConfigDict(extra="forbid")
tool_keys: list[str] = Field(
min_length=1,
description="需要加载的业务工具 key 数组,只允许 snake_case 工具名",
)
加载完成后,把已加载工具写入运行状态:
def load_tools(
tool_keys: list[str],
runtime: ToolRuntime[None, Any],
) -> Command:
"""按需加载当前任务需要的业务工具。"""
validated_request = LoadToolsRequest.model_validate({"tool_keys": tool_keys})
normalized_tool_keys = validated_request.tool_keys
current_loaded_tool_keys = normalize_loaded_tool_keys(runtime.state) or []
merged_tool_keys = merge_unique_loaded_tool_keys(
existing_tool_keys=current_loaded_tool_keys,
requested_tool_keys=normalized_tool_keys,
)
return Command(
update={
"messages": [tool_message],
"loaded_tool_keys": merged_tool_keys,
}
)
这个设计和 Skill 渐进式加载是一致的:先让模型知道“有什么”,但不让它一次性背负“全部细节”。Skill 控制知识上下文,动态工具控制动作空间。
RAG 检索流程
项目里也做了 RAG 检索链路,用来增强知识问答。
RAG 分成两个阶段:知识入库阶段和在线查询阶段。
知识入库阶段主要做这些事情:
文件下载
-> 文件类型识别
-> 文档解析
-> 文本清洗
-> 文本切片
-> embedding 向量化
-> 写入 Milvus
在线查询阶段则是:
用户问题
-> 问题规范化
-> 可选问题改写
-> embedding
-> Milvus 多知识库向量召回
-> 可选 rerank 重排
-> 命中片段格式化
-> 注入 Agent 上下文
这里有一个细节:向量召回和重排用的问题可以不一样。
如果走问题改写链路,系统会用改写后的问题做向量召回,因为改写后的表达更适合检索;但重排阶段仍然使用用户原始问题,这样排序时还能对齐用户真实语义。
也就是说:
改写问题:用于召回
原始问题:用于重排
这个设计能让召回更宽一点,排序更准一点。
最终检索到的知识片段不会原样无限塞给模型,而是会经过格式化和长度预算控制。这样既能给模型提供参考资料,又不会让 RAG 结果把上下文撑爆。
诊断到推荐药品的流程
诊断推荐药品这部分,我没有把它设计成“用户说症状,模型马上推药”。
医药场景里这样做并不合适。用户很多时候描述不完整,比如只说“喉咙痛”“咳嗽”“头疼”,但这些症状背后可能对应很多不同方向。如果直接让模型推荐药品,很容易变成凭感觉生成结果。
所以我这里把它设计成一个逐步收敛的流程:先补充症状,再查图谱,再收敛疾病方向,最后才进入药品推荐。
整体流程大概是:
用户描述症状
-> 医疗节点进入问诊流程
-> 提取用户症状关键词
-> 查询 Neo4j 医疗知识图谱中的症状节点
-> 根据标准症状召回候选疾病
-> 查询候选疾病详情
-> 判断信息是否足够
-> 信息不足时继续生成问诊卡片
-> 病情方向基本收敛
-> 发送推荐药品确认卡
-> 用户同意后,再结合商品系统推荐药品
这里图数据库不是只做一个简单搜索,而是在问诊流程里承担了几个关键步骤。
第一步是症状标准化。用户输入的症状不一定和图谱里的症状名称完全一致,所以会先根据用户描述去匹配图谱中的 Symptom 节点。例如用户说“嗓子疼”,系统需要尽量匹配到图谱里更标准的症状表达。
第二步是根据症状召回候选疾病。图谱里疾病和症状之间有结构化关系:
(Disease)-[:has_symptom]->(Symptom)
当用户已经提供了一些症状后,系统会根据这些症状去查可能相关的疾病,并按命中症状数量排序。这样可以先得到一个候选疾病列表,而不是直接让模型凭空判断。
第三步是查询疾病详情。拿到候选疾病后,系统会继续查疾病的结构化信息,包括:
疾病描述
病因
预防方式
易感人群
治疗方式
治疗周期
治愈概率
发病概率
传播方式
医保状态
所属分类
常见症状
建议检查
常用药品
推荐药品
宜吃食物
忌口食物
推荐食谱
所属科室
并发症
这些信息背后对应图谱中的多类关系,例如:
(Disease)-[:has_symptom]->(Symptom)
(Disease)-[:need_check]->(Check)
(Disease)-[:common_drug]->(Drug)
(Disease)-[:recommand_drug]->(Drug)
(Disease)-[:do_eat]->(Food)
(Disease)-[:no_eat]->(Food)
(Disease)-[:recommand_eat]->(Food)
(Disease)-[:belongs_to]->(Department)
(Disease)-[:acompany_with]->(Disease)
第四步是继续追问。因为很多疾病可能共享一部分症状,所以系统不会只命中一个候选疾病就结束,而是会根据候选疾病之间的差异症状,生成进一步追问的问题。
比如几个候选疾病都可能有“咳嗽”,但是否“发热”、是否“流鼻涕”、是否“咽痛”、是否“咳痰”可能会影响判断方向,这时候系统就会通过问诊卡片让用户补充信息。问诊卡不是普通文本追问,而是结构化卡片,用户可以直接点击选项补充病情。
第五步才是药品推荐。当病情方向基本收敛后,系统不会立刻推药,而是先发送一个推荐药品确认卡,询问用户是否同意继续推荐药品。只有用户确认后,系统才会结合图谱里的药品线索、商城里的商品标签、商品搜索和商品详情,返回可以在前端展示的药品卡片。
所以这里的推荐不是简单地让模型说“你可以买某某药”,而是:
症状信息
-> 图谱召回疾病
-> 疾病详情辅助判断
-> 差异症状追问
-> 用户确认
-> 商品系统搜索
-> 药品卡片返回
在这个流程里,模型负责理解用户表达、组织追问和判断下一步;Neo4j 医疗知识图谱负责提供结构化医学关系;商品系统负责提供真实存在、可以展示和购买的药品数据。
需要说明的是,这部分设计不是为了替代医生,也不是为了让 AI 直接下医学结论,而是希望在 AI 追问症状、分析可能方向、推荐药品时,有一个结构化知识来源作为参考。
图谱数据来源与致谢
项目里 Neo4j 医疗知识图谱使用的数据来自开源项目 QASystemOnMedicalKG。
项目 README 中也说明了这一点:本项目 Neo4j 医疗知识图谱使用的数据来自该开源项目,图谱数据的具体导入方式可以参考原仓库中的说明文档。
这里特别感谢原作者和相关贡献者的开源工作。没有这些开源数据,项目里的图数据库问诊链路很难在毕业设计阶段完整跑起来。
在我的项目里,这些图谱数据主要用于辅助 AI 完成:
- 症状候选匹配
- 候选疾病召回
- 疾病详情查询
- 差异症状追问
- 常用药品和推荐药品参考
- 饮食建议和忌口提示
图谱不是最终答案本身,而是给智能体提供一个结构化参考。模型仍然需要结合上下文、用户补充信息、就诊人资料和商品系统数据,才能继续完成后续问诊和推荐流程。
管理端智能助手
除了客户端问诊,项目里还做了管理端智能助手。
管理端助手主要面向后台运营场景,比如查询订单、查看商品、分析售后、查看用户消费情况、生成运营分析等。这里的难点不是让模型闲聊,而是让模型能够理解后台管理问题,并调用真实业务工具返回结果。
例如管理人员可以问:
最近订单情况怎么样?
哪些商品卖得比较好?
某个用户的消费情况是什么?
售后原因分布怎么样?
这类问题如果只靠模型回答,意义不大。真正有价值的是模型能判断应该调用哪个业务工具,从后端拿真实数据,再把结果组织成可读的分析内容。
所以管理端助手和客户端助手的侧重点不同:
- 客户端更偏问诊、导购、商品推荐
- 管理端更偏数据查询、运营分析、业务辅助
但底层思路是一样的:模型负责理解意图和组织回答,真实数据必须来自业务系统。
图片理解能力
项目里也加了图片理解能力。
在医药场景里,用户不一定能准确输入药品名称,很多时候可能是上传药盒、说明书、药品图片。这时候 AI 需要先理解图片内容,再把图片里的信息转成后续流程可以使用的结构化数据。
图片理解能力可以用于:
- 药品图片识别
- 商品信息辅助录入
- 用户上传药盒后的药品理解
- 问诊过程中的图片辅助判断
这块能力不是为了炫技,而是因为医药场景天然会遇到“用户说不清楚,但能拍出来”的情况。
流式输出和智能体追踪
智能助手体验里,流式输出也很重要。
如果用户提交问题后一直等到所有工具调用、模型思考、知识检索都完成才返回,体验会很差。所以项目里通过 SSE 做了流式输出,前端可以逐步展示模型回答、工具状态和最终卡片。
同时,项目里也做了智能体运行追踪。一次智能体调用里,可能经历了路由判断、模型调用、工具调用、RAG 检索、卡片发送等多个步骤。如果没有追踪,很难定位问题到底出在哪里。
所以智能体追踪的意义不只是“记录日志”,而是为了后续排查这些问题:
- 模型为什么选了这个工具?
- 工具有没有调用成功?
- RAG 有没有检索到内容?
- 卡片有没有发送给前端?
- 最终回答是在哪一步生成的?
这对 AI 应用非常重要,因为 AI 问题经常不是一个简单异常,而是一条链路里的某个判断出现了偏差。
为什么电商只是辅助
虽然这个项目表面上看是医药电商,但我自己更愿意把它看成一个 AI 智能体项目。
电商只是为了给 AI 一个真实落点。如果没有商品、订单、售后、用户这些业务对象,AI 就只能停留在聊天层面。加入电商后,智能体才能真正调用工具、返回卡片、查询真实数据、进入具体业务流程。
所以如果你要参考这个项目,我更建议重点看 AI 智能体部分,包括:
- Skill 渐进式加载
- 动态工具按需加载
- RAG 检索链路
- LangGraph 工作流
- 问诊和荐药流程
- 前端卡片协议
- 智能体运行追踪
- Neo4j 医疗知识图谱调用流程
这些才是我做这个项目时真正想重点打磨的部分。
最后
这个项目是我的毕业设计,也是我作为应届生对 AI 工程化的一次尝试。
我知道自己和真实工作里有经验的工程师相比还有不少差距,所以这个项目不一定在每个业务细节上都足够成熟。电商部分更多是为了支撑 AI 场景,它不是项目最核心的价值。
随着 AI 写代码的发展,我觉得以后 AI 写代码会像 IDE 一样,成为开发者必须掌握的基础能力。但 AI 能帮我们写代码,不代表系统设计就不重要了。真正决定一个项目能不能跑稳、能不能扩展、能不能落地的,依然是人的设计思考。
如果贵司觉得这个项目和我的方向符合岗位要求,欢迎联系我。我也正在寻找合适的开发岗位机会。同时,如果你对项目实现、部署或 AI 智能体设计有问题,也可以通过邮箱和我交流。
邮箱:zhangchuang2726@gmail.com
希望这个项目能给你一些灵感,如果项目对你有用点一个 star 吧~