药智通:基于 LangGraph、RAG 和知识图谱的医药 AI Agent 实践

0 阅读17分钟

药智通:一个把 AI 智能体放在核心位置的医药项目

项目地址:github.com/lalala2726/…

在线体验:

体验账号:

账号:admin
密码:admin123

系统截图

管理端

agent-trace-detail.png

agent-monitor-metrics.png

agent-monitor-overview.png

operations-analytics.png

ai-assistant-demo.png

after-sales-overview.png

客户端

client-interface-07.pngclient-interface-08.pngclient-interface-10.pngclient-interface-06.png
client-interface-09.pngclient-interface-04.pngclient-interface-01.pngclient-interface-02.png
client-interface-05.pngclient-interface-03.png

项目背景

这个项目最初其实是我的毕业设计。

做这个项目的起点,是我自己一次比较普通的买药经历。之前有一次我生病了,是上呼吸道感染,喉咙特别难受,当时就急着去买药。但问题是我自己也不知道应该买什么药,只能在线问诊。也是那次经历之后,我突然想到:能不能用 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 AgentPython、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 的元数据,只把 namedescription 这类摘要信息放进系统提示词。

模型先知道“有哪些能力可以用”,但不会一开始就看到所有细节。只有当任务真的命中某个 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 吧~