背景
最近在做项目里面涉及AI应用,领导选型为dify,为此需要研究下,关于AI应用,大家是如何做的,在此分析了medical-consultation-system项目
https://gitee.com/hn2004214/medical-consultation-system
受益匪浅
此dify中的工作流具有通用性。
分析
前端,后端,数据库,在此就不在分析,仅仅分析,dify中的AI应用,以及如何用最简单方法调用AI应用。

从dify中的图标可以看出,这里创建的就是"工作流"。

进入后,流程还是比较简单的,开始时,开始有几个参数如下:

从中可以知道,这个就是调用AI应用的输入,因为是医疗系统嘛,大家应该都明白的,在此就说明一个东西:
history:对话历史JSON,这个是最关键的,API调用不像使用对话框,大模型是没有记忆功能的,需要用户在每一次交流时,带上历史的对话,进行,AI再进行分析。
第二个节点就是知识库查询
第三个节点就是路由判断,判断是继续追问,还是得出分诊结果。
看下追问生成的LLM逻辑(这个qwen3:0.6b是我自己配置,调试能用,真实项目看自己配置):

首先是SYSTEM(为对话提供高层次指导)
bash
你是一个医疗预检分诊 AI 助手,负责根据患者主诉进行关键追问。
## 追问原则
1. 围绕症状的「发病时间、部位、程度、伴随症状、诱因」等关键维度
2. 参考患者健康档案的基础病,做针对性追问(如高血压患者问血压)
3. 如医生提供了打回理由,必须按医生指示方向提问
4. 语气友好关切,避免给出诊断结论
5. 一次最多追问 2 个问题,不可过多
6. 直接输出追问内容(纯文本),禁止 JSON、禁止 markdown 代码块
# 知识库参考
请优先参考知识库检索结果中的分诊规则、急危重症提示和问诊规范。
如果知识库内容与当前患者描述无关,不要强行引用。
## 示例输出
请问您的头痛是搏动性的还是持续性钝痛?是否伴有恶心呕吐或畏光症状?
然后是User(向模型提供指令)
bash
# 患者健康档案
{{#start_1.patient_profile#}}
# 医生打回理由(如有)
{{#start_1.doctor_reason#}}
# 对话历史
{{#start_1.history#}}
# 患者最新消息
{{#start_1.user_message#}}
#知识库参考
{{#context#}}
请结合知识库参考、患者健康档案、医生打回理由和对话历史,输出 1-2 个关键追问(纯文本)。
最终输出是一个变量:

下面是综合分诊,需要用户主动提交

首先是SYSTEM(为对话提供高层次指导)
bash
你是一个医疗预检分诊专家 AI,负责根据患者对话、健康档案给出结构化分诊建议。
## 输出格式(严格 JSON,禁止任何 markdown 代码块或额外文字)
{
"is_final": true,
"recommended_dept": "神经内科",
"possible_causes": ["偏头痛", "紧张型头痛"],
"advice": "建议充分休息并监测体温,若症状持续或加重请前往急诊。",
"urgency_level": "一般",
"answer": "根据您的描述,结合您的健康档案,初步建议前往神经内科......"
}
## 字段约束
1. is_final:**必须严格为 true**(标识分诊建议已最终生成,供后端更新状态)
2. recommended_dept:常见一级/二级科室,如 内科 / 外科 / 神经内科 / 呼吸内科 / 消化内科 / 心血管内科 / 骨科 / 皮肤科 / 儿科 / 急诊科 / 妇科 / 泌尿外科 / 耳鼻喉科 等
3. possible_causes:数组,列出 2-3 个最可能的病因
4. advice:居家护理或就医建议,不超过 100 字
5. urgency_level:**必须严格是** "一般" / "紧急" / "危重" 三者之一
- 危重:意识障碍、胸痛胸闷、呼吸困难、大出血、持续剧烈头痛
- 紧急:持续高热、剧烈疼痛、症状持续加重
- 一般:常见轻度症状
6. answer:给患者看的人性化解释,语气温和专业,不超过 200 字
# 知识库参考
请优先参考知识库检索结果中的分诊规则、急危重症提示和问诊规范。
如果知识库内容与当前患者描述无关,不要强行引用。
## 注意事项
- 如果医生提供了打回理由,必须在建议中明确体现对该理由的回应
- 如果打回理由中含有【医生仅要求修改以下字段】提示,请**重点重新生成这些字段**,其他字段尽量与上次分诊保持一致;同时 answer 要围绕这些字段的修改进行解释
- 结合患者健康档案中的既往病史、过敏史、家族史等因素综合判断
- 绝对不要输出 JSON 之外的任何内容
然后是User(向模型提供指令)
bash
# 患者健康档案
{{#start_1.patient_profile#}}
# 医生打回理由(如有)
{{#start_1.doctor_reason#}}
# 完整对话历史
{{#start_1.history#}}
# 患者最新消息
{{#start_1.user_message#}}
# 知识库参考
{{#context#}}
请结合知识库参考、患者健康档案、医生打回理由和完整对话历史,输出分诊 JSON。
开放接口就选择"访问API",进行配置,即可

查看下HTTP调用的例子:

看下这个开源项目里面python是如何调用的:
python
class DifyClient:
"""Dify 工作流调用客户端。
本项目使用模块底部的全局单例 `dify_client` 即可,一般无需手动实例化。
使用方式:
result = await dify_client.run_workflow(
user_message="头痛、发烧两天",
conversation_id=1,
patient_profile="32岁 女 高血压3年 青霉素过敏",
target_node="followup",
history=[{"role": "user", "content": "..."}, ...],
)
"""
def __init__(
self,
api_base: str | None = None,
api_key: str | None = None,
timeout: float = 60.0,
) -> None:
# api_base / api_key 默认从应用配置读取(可被参数覆盖以便单测注入 mock)
# rstrip('/') 防止配置里末尾多写斜杠导致 URL 拼出 //workflows/run
self.api_base = (api_base or settings.dify_api_base).rstrip("/")
self.api_key = api_key or settings.dify_api_key
# LLM 推理可能耗时较长,给到 60 秒超时
self.timeout = timeout
@property
def _headers(self) -> dict[str, str]:
"""Dify 要求所有请求携带 Bearer token,每个工作流绑定一个独立 API Key。"""
return {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
async def run_workflow(
self,
*,
user_message: str,
conversation_id: int,
patient_profile: str,
target_node: str,
history: list[dict[str, str]] | None = None,
doctor_reason: str | None = None,
) -> dict[str, Any]:
"""调用 Dify 工作流 `/workflows/run` 接口(本客户端的唯一对外入口)。
Args:
user_message: 患者最新一条消息文本,用于 LLM 推理与知识检索 query。
conversation_id: 会话 ID,仅用于 Dify 端日志追踪与限流隔离。
patient_profile: 患者档案摘要(已由 profile_service 拼接为单行字符串)。
target_node: 工作流路由开关,仅接受 'triage' 或 'followup'。
对应 YAML 中 if_else_1 节点的判断条件。
history: 对话历史 [{role, content}, ...],会被 json.dumps 后
作为字符串变量传入工作流(Dify 变量类型限制)。
doctor_reason: 医生打回时的修正意见;非打回场景传 None。
Returns:
统一契约 dict:
{
"is_final": bool, # True 时上层将更新会话状态为 pending_review
"assistant_text": str, # AI 回复文本(给患者看)
"triage_payload": dict | None, # 综合分诊节点的结构化结果,4 个字段
}
Raises:
RuntimeError: 网络异常或 Dify 返回非 2xx 时抛出,由上层 service 捕获处理。
"""
# ---------- 降级分支:未配置 Dify 时直接返回 Mock 数据 ----------
# 占位符 'your-dify*' 是 .env.example 里的默认值,
# 用 startswith('your-dify') 兼容用户忘改 .env 的情况。
if not self.api_key or self.api_key.startswith("your-dify"):
logger.warning("DIFY_API_KEY 未配置或为占位符,使用 Mock 模拟返回")
return self._mock_response(user_message, target_node)
# ---------- 构造请求 payload ----------
# inputs 中 6 个 key 必须与 Dify 工作流 "开始" 节点定义的 6 个变量一一对应。
# 任何 key 缺失或拼写不一致都会被 Dify 返回 400。
url = f"{self.api_base}/workflows/run"
payload = {
"inputs": {
# 与 YAML 中 start_1.variables 严格对齐
"user_message": user_message,
"conversation_id": str(conversation_id), # Dify text-input 类型要求字符串
"patient_profile": patient_profile or "",
"target_node": target_node, # 决定走 triage 还是 followup 分支
# Dify 不支持复杂结构变量,对话历史需序列化为 JSON 字符串后再注入 prompt
"history": json.dumps(history or [], ensure_ascii=False),
"doctor_reason": doctor_reason or "",
},
# blocking:一次性返回完整 JSON;后端要解析 is_final 才能决定状态机跳转,
# 因此不能用 streaming(SSE 流式更适合给前端做打字机效果)。
"response_mode": "blocking",
# Dify 用 user 标识做对话隔离 + 限流计数,同会话保持一致即可
"user": f"patient-{conversation_id}",
}
# ---------- 发起 HTTP 请求 ----------
# 用 async with 上下文确保连接关闭,避免连接泄漏;每次创建短连接以隔离故障。
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
resp = await client.post(url, headers=self._headers, json=payload)
print("Dify原始返回报文:", resp.text)
print("请求发送的body:", json.dumps(payload, ensure_ascii=False))
resp.raise_for_status() # 4xx/5xx 抛 HTTPStatusError
data = resp.json()
except httpx.HTTPError as exc:
# 把 httpx 体系下所有异常统一为业务侧的 RuntimeError,
# 上层 service 不必感知 httpx 细节(反腐层关键设计)。
logger.exception("调用 Dify 工作流失败:%s", exc)
raise RuntimeError(f"Dify 工作流调用失败:{exc}") from exc
# ---------- 解析响应并返回统一契约 ----------
return self._parse_response(data, target_node=target_node)
可知都放到input里面。
优化提升点

知识库的准确性,是最重要,得准备好数据,进行召回测试

如果使用其他方式,可能会让整改工作流变慢,这种呢,准确性很感觉。感觉这个地方是提升的重点。