一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

背景与挑战

dify 作为一款低代码 AI 应用开发平台,凭借其直观的可视化工作流编排能力,极大降低了大模型应用的开发门槛。然而,在实际企业级落地过程中,我们发现其原生能力仍存在两个关键瓶颈:
代码执行能力受限:Dify 内置的 Sandbox 节点虽支持基础 Python 代码执行,但无法安装自定义 Python 包,难以支撑复杂的业务逻辑、数据处理或算法调用;
缺乏自动化调度机制:Dify 原生架构不支持 Agent 或 Agentic Workflow 的定时触发、周期性运行与依赖编排,导致其难以融入企业级自动化运维体系。

这两个问题严重制约了 Dify 在生产环境中的深度应用——尤其当我们希望构建一个具备“感知-决策-执行-反馈”闭环能力的智能 Agent 时,仅靠 Dify 自身往往力不从心。
为突破这些限制,我们在实践中探索出一套 “Dify + DMS Notebook + DMS Airflow”三位一体的一站式增强开发架构,有效补齐了 Dify 在执行能力与调度能力上的短板:
✅ DMS Notebook:提供完整、可定制的 Python 运行环境,支持第三方库安装、交互式开发与复杂逻辑实现,作为 Dify Sandbox 的强力补充;
✅ DMS Airflow:作为统一调度引擎,实现对 Dify 工作流、Notebook 脚本或 Agent 任务的定时触发、依赖管理与可靠执行;
✅ DMS 平台集成:实现从开发、调试、部署到调度、监控的全链路闭环管理,显著提升 Dify 在企业场景下的工程化落地能力。

本文将以一个销售数据分析机器人的完整开发案例,详细介绍如何基于 DMS 平台,构建一个可调度、可扩展、可运维的 Agent 系统。
使用 DMS Notebook 扩展 Dify 的代码执行能力

为什么需要 Notebook?

在 Dify 中,若需调用 Pandas 进行数据清洗、使用 Prophet 做时间序列预测,或集成企业内部 SDK,Sandbox 节点往往无能为力。而 DMS Notebook 提供了完整的 Python 环境,支持:
自定义 pip 包安装;
环境变量配置(如 AK/SK、API Key);
异步服务开发(如 FastAPI);
与 VPC 内其他服务安全互通。

这使其成为 Dify 外部能力扩展的理想“执行单元”。
步骤详解:构建一个销售数据分析 API 服务

1.创建 DMS Notebook 会话
进入 DMS 控制台 > Notebook 会话 > 创建会话;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

各参数的定义如下:

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

选择合适的 Python 镜像版本;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在 配置 > 编辑设置 中:

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

PyPI 包管理:按 requirements.txt 格式填入依赖(如 pandas,fastapi,uvicorn,nest-asyncio);
环境变量:设置 ALIBABACLOUDACCESSKEYID、ALIBABACLOUDACCESSKEYSECRET、大模型 API Key 等;
关键配置:
fastapi、uvicorn、nest-asyncio库是必须要安装的;
设置 资源释放时间 = 0(防止服务被自动释放);
设置环境变量 DMSKERNELIDLE_TIMEOUT=0(避免 Jupyter Kernel 因空闲被 kill)。

💡踩坑提示:若未设置 DMSKERNELIDLE_TIMEOUT=0,长时间运行的 API 服务可能在空闲数分钟后被系统回收,导致后续调用失败。
创建完成后,在notebook会话窗口中点击启动即可

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

2.编写并启动 FastAPI 服务

点击文件夹图标,右键点击 default(默认库),再点击 新建Notebook文件。

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在代码块中编写相关的python代码,可以参考以下模板构建你的API服务,更多FastAPI相关的使用方法请查看官方文档:https://fastapi.tiangolo.com/

import osfrom fastapi import FastAPI, HTTPException, Request, File, UploadFile, Path, Query, Form, Headerfrom fastapi.staticfiles import StaticFilesfrom typing import Optionalimport nestasyncioimport asyncioimport httpximport io'''注意,Jupyter本身就在一个asyncio事件循环中运行。我们不能在已有循环中直接运行另一个循环,但nestasyncio这个库可以“打补丁”,允许我们这样做。'''nestasyncio.apply()app = FastAPI(title="Your Service Name", description="Description of Your Service")# 创建static目录(如果不存在)staticdir = "static"ifnot os.path.exists(staticdir):    os.makedirs(staticdir)# 挂载静态文件服务app.mount("/static", StaticFiles(directory=staticdir), name="static")@app.get("/")async def root():    """    根节点,返回服务基本信息    # — 如何使用 curl 调用 —    curl -X GET "http://127.0.0.1:8000/"        """    return {        "message": "Service is running",        "documentation": "/docs",        "note": "…"    }@app.post("/process-data/{itemid}")async def processdata(    request: Request, # 使用 Request 对象来接收原始请求        # 路径参数    itemid: int = Path(…, title="物品ID", ge=1),        # 查询参数    ispremium: bool = Query(False, description="是否为高级物品"),        # 请求头参数    xtoken: Optional[str] = Header(None, description="自定义的认证Token")):    """    接收 JSON 请求体、路径参数、查询参数和请求头。    # — 如何使用 curl 调用 —    # -X POST: 指定请求方法    # URL: 包含路径参数 {itemid} 和查询参数 ?ispremium=true    # -H: 添加请求头 (Header)    # -d: 发送请求体 (Body),这里是 JSON 字符串    curl -X POST "http://127.0.0.1:8000/process-data/101?ispremium=true"          -H "Content-Type: application/json"          -H "X-Token: my-secret-token"          -d '{"name": "笔记本电脑", "price": 7999.9, "tags": ["electronics", "office"]}'    """    if xtoken != "my-secret-token":        raise HTTPException(statuscode=401, detail="X-Token 无效")        try:        # 手动解析 JSON 请求体        jsonbody = await request.json()                name = jsonbody.get("name")        price = jsonbody.get("price")                # 你的业务逻辑代码        if not name or not price:            raise HTTPException(statuscode=400, detail="请求体中缺少 'name' 或 'price'")        return {            "message": "数据处理成功",            "receiveddata": {                "itemid": itemid,                "ispremium": ispremium,                "xtoken": xtoken,                "body": jsonbody            }        }    except Exception as e:        raise HTTPException(statuscode=500, detail=f"服务执行错误: {str(e)}")@app.post("/upload-file")async def uploadfile(    # 表单数据    token: str = Form(…),    # 上传文件    file: UploadFile = File(…)):    """    通过表单(form-data)上传文件和附带的文本信息。    # — 如何使用 curl 调用 —    # -F: 用于发送 multipart/form-data    # -F "file=@/path/to/your/file.txt": @符号表示后面是文件路径,curl会读取该文件内容作为上传数据    # -F "token=user123": 发送一个名为 token 的表单字段    # 注意: 请将 /path/to/your/file.txt 替换为你的本地文件真实路径    curl -X POST "http://127.0.0.1:8000/upload-file"          -F "file=@./testupload.txt"          -F "token=my-form-token"    """    # 为了让curl示例能工作,我们先创建一个示例文件    if not os.path.exists("testupload.txt"):        with open("testupload.txt", "w") as f:            f.write("This is a test file for curl upload.")    try:        contents = await file.read()        filelocation = os.path.join(staticdir, file.filename)        with open(filelocation, "wb") as f:            f.write(contents)                    return {            "message": "文件上传成功!",            "token": token,            "filename": file.filename,            "filesize": len(contents),            "fileurl": f"/static/{file.filename}"        }    except Exception as e:        raise HTTPException(statuscode=500, detail=f"文件处理错误: {str(e)}")@app.get("/status")async def getserverstatus():    """    获取服务器状态。    # — 如何使用 curl 调用 —    curl -X GET "http://127.0.0.1:8000/status"    """    return {"status": "running"}async def runserver(host="127.0.0.1", port=8000):    """在后台运行uvicorn服务器"""    import uvicorn    config = uvicorn.Config(app, host=host, port=port, loglevel="info")    server = uvicorn.Server(config)    # uvicorn.run() 是一个阻塞调用,所以我们用更底层的Server.serve()    await server.serve()task = asyncio.createtask(runserver(host="0.0.0.0", port=8000))# 等待服务启动await asyncio.sleep(2)# 创建一个异步HTTP客户端async with httpx.AsyncClient() as client:    print("正在向 http://127.0.0.1:8000/status/ 发送请求…")        # 发送POST请求    response = await client.get("http://127.0.0.1:8000/status")        # 打印结果    if response.status_code == 200:        print("服务启动成功")    else:        print("服务启动失败,请检查报错信息")
接下来我们以构建一个单日销售数据分析的API为例,代码内容如下:

import osimport pandas as pdfrom fastapi import FastAPI, HTTPException, Request, File, UploadFilefrom fastapi.staticfiles import StaticFilesimport nestasyncioimport asyncioimport httpximport io'''注意,Jupyter本身就在一个asyncio事件循环中运行。我们不能在已有循环中直接运行另一个循环,但nestasyncio这个库可以“打补丁”,允许我们这样做。'''nestasyncio.apply()app = FastAPI(title="Sales Data Analysis Service", description="Provides data analysis and chart generation capabilities for Dify")# 创建static目录(如果不存在)staticdir = "static"ifnot os.path.exists(staticdir):os.makedirs(staticdir)# 挂载静态文件服务app.mount("/static", StaticFiles(directory=staticdir), name="static")def loadsalesdatafromfile(filecontent: bytes):"""从上传的文件内容加载销售数据"""try:# 将字节内容转换为StringIO对象csvstring = filecontent.decode('utf-8')df = pd.readcsv(io.StringIO(csvstring))“# 验证必要的列是否存在requiredcolumns = ['Date', 'Product', 'Price', 'Amount', 'Region']ifnot all(col in df.columns for col in requiredcolumns):raise ValueError(f"CSV file must contain columns: {', '.join(requiredcolumns)}")“# 转换数据类型df['Date'] = pd.todatetime(df['Date'])df['Price'] = pd.tonumeric(df['Price'], errors='coerce')df['Amount'] = pd.tonumeric(df['Amount'], errors='coerce')“# 计算销售额 (Sales = Price × Amount)df['Sales'] = df['Price'] * df['Amount']“return dfexcept Exception as e:raise HTTPException(statuscode=500, detail=f"Error processing CSV file: {str(e)}")@app.get("/")async def root():"""Root endpoint, returns service information"""return {"message": "Sales Data Analysis Service is running","documentation": "/docs","endpoints": ["POST /analysis/dailysaleanalysis"],"note": "Require a CSV file upload with columns: Date, Product, Price, Amount, Region"}@app.post("/analysis/dailysaleanalysis")async def dailysaleanalysis(file: UploadFile = File(…)):"""当日销售数据分析 – 分析上传文件中的销售数据"""try:# 验证文件类型ifnot file.filename.endswith('.csv'):raise HTTPException(statuscode=400, detail="文件必须是CSV格式")“# 读取上传的文件filecontent = await file.read()df = loadsalesdatafromfile(filecontent)“# 获取数据中的日期范围df['Date'] = pd.todatetime(df['Date']).dt.dateuniquedates = sorted(df['Date'].unique())“if len(uniquedates) == 0:raise HTTPException(statuscode=400, detail="数据文件中没有有效的日期数据")“# 如果有多个日期,取最新的日期作为分析目标targetdate = uniquedates[-1] if len(uniquedates) > 1else uniquedates[0]“# 筛选目标日期的数据dailydata = df[df['Date'] == targetdate].copy()“if dailydata.empty:raise HTTPException(statuscode=400, detail=f"没有找到日期 {targetdate} 的销售数据")“# 基础统计totalsales = dailydata['Sales'].sum()totalorders = len(dailydata)totalquantity = dailydata['Amount'].sum()avgordervalue = totalsales / totalorders if totalorders > 0else0“# 产品分析productanalysis = dailydata.groupby('Product').agg({'Sales': 'sum','Amount': 'sum','Price': 'mean'}).round(2)“# 按销售额排序,取前5名产品topproducts = productanalysis.sortvalues('Sales', ascending=False).head(5)topproductslist = []for product, row in topproducts.iterrows():topproductslist.append({"product": product,"sales": float(row['Sales']),"quantity": int(row['Amount']),"avgprice": float(row['Price'])})“# 地区分析regionanalysis = dailydata.groupby('Region').agg({'Sales': 'sum','Amount': 'sum'}).round(2)“# 按销售额排序topregions = regionanalysis.sortvalues('Sales', ascending=False)regionlist = []for region, row in topregions.iterrows():regionlist.append({"region": region,"sales": float(row['Sales']),"quantity": int(row['Amount']),"percentage": round(float(row['Sales']) / totalsales * 100, 2)})“# 价格区间分析dailydata['pricerange'] = pd.cut(dailydata['Price'],bins=[0, 100, 500, 1000, 5000, float('inf')],labels=['0-100', '100-500', '500-1000', '1000-5000', '5000+'])“pricerangeanalysis = dailydata.groupby('pricerange').agg({'Sales': 'sum','Amount': 'sum'}).round(2)“priceranges = []for pricerange, row in pricerangeanalysis.iterrows():ifnot pd.isna(row['Sales']) and row['Sales'] > 0:priceranges.append({"range": str(pricerange),"sales": float(row['Sales']),"quantity": int(row['Amount'])})“# 生成洞察分析insights = []“# 销售额洞察if totalsales > 100000:insights.append(f"当日销售表现优秀,总销售额达到 {totalsales:,.2f} 元")elif totalsales > 50000:insights.append(f"当日销售表现良好,总销售额为 {totalsales:,.2f} 元")else:insights.append(f"当日销售额为 {totalsales:,.2f} 元,可能需要关注销售策略")“# 产品洞察if len(topproductslist) > 0:bestproduct = topproductslist[0]insights.append(f"最佳销售产品是 {bestproduct['product']},销售额 {bestproduct['sales']:,.2f} 元")“# 地区洞察if len(regionlist) > 0:bestregion = regionlist[0]insights.append(f"销售表现最佳的地区是 {bestregion['region']},占总销售额的 {bestregion['percentage']}%")“# 订单洞察if avgordervalue > 1000:insights.append(f"平均订单价值较高,为 {avgordervalue:,.2f} 元,显示客户购买力强")“return {"analysisdate": str(targetdate),"summary": {"totalsales": round(float(totalsales), 2),"totalorders": int(totalorders),"totalquantity": int(totalquantity),"averageordervalue": round(float(avgordervalue), 2)},"topproducts": topproductslist,"regionanalysis": regionlist,"pricerangeanalysis": priceranges,"insights": insights,"datainfo": {"daterange": f"{uniquedates[0]} 到 {uniquedates[-1]}"if len(uniquedates) > 1else str(uniquedates[0]),"totalrecords": len(dailydata),"uniqueproducts": len(dailydata['Product'].unique()),"uniqueregions": len(dailydata['Region'].unique())}}“except HTTPException:raiseexcept Exception as e:raise HTTPException(statuscode=500, detail=f"当日销售数据分析错误: {str(e)}")@app.get("/status")async def getserverstatus():"""获取服务器状态"""try:return {"status": "running"}except Exception as e:raise HTTPException(statuscode=500, detail=f"Error getting server status: {str(e)}")async def runserver(host="127.0.0.1", port=8000):"""在后台运行uvicorn服务器"""import uvicornconfig = uvicorn.Config(app, host=host, port=port, loglevel="info")server = uvicorn.Server(config)# uvicorn.run() 是一个阻塞调用,所以我们用更底层的Server.serve()await server.serve()task = asyncio.createtask(runserver(host="0.0.0.0", port=8000))# 等待服务启动await asyncio.sleep(2)# 创建一个异步HTTP客户端async with httpx.AsyncClient() as client:print("正在向 http://127.0.0.1:8000/status/ 发送请求…")“# 发送POST请求response = await client.get("http://127.0.0.1:8000/status")“# 打印结果if response.status_code == 200:print("服务启动成功")else:print("服务启动失败,请检查报错信息")
✅异步支持:Jupyter 内置 asyncio 事件循环,可直接使用 async/await。
运行相关代码块,可以在输出部分看到API服务启动成功;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

查看IP地址

在notebook代码块中,可以使用英文感叹号+终端命令的形式来执行命令,你也可以使用!pip install xxx 来安装额外需要的python包。接下来新建一个代码块,在代码块中输入!ifconfig 并点击运行查看该 Notebook 会话 在VPC中的IP地址,图中 172.16.0.252 即为所需的IP地址,API 服务地址即为:http://172.16.0.252:8080/analyze_sales

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在Dify on DMS实例中访问服务
现在我们使用这份模拟销售数据文件来访问API服务:

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在工作流中添加HTTP请求节点:

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

通过 http://:<端口>/xxx 访问创建的API的服务,并在 BODY 中传入相应的参数;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

测试运行可以看到该请求成功返回了响应的输出;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

你也可以在Notebook会话中看到相应的服务被调用;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

接下来以该Dify工作流为例子进行完整的服务调用;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在钉钉群组中添加一个自定义的机器人,并参考https://open.dingtalk.com/document/orgapp/robot-overview 了解如何获取钉钉机器人的accesstoken和signsecret

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

在填写完你的钉钉机器人参数之后,点击右上角的 运行-》从本地上传 上传示例的销售数据,并点击开始运行;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

钉钉群中的机器人成功发送了对该销售数据的分析报告;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

点击右上角的 发布-》发布更新 发布工作流用于后面的定时调用;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

使用 DMS Airflow 实现定时调度

创建DMS Airflow实例
参照以下链接在DMS中创建Airflow实例
https://help.aliyun.com/zh/dms/purchase-airflow-resources
https://help.aliyun.com/zh/dms/create-and-manage-an-airflow-instance
更多关于Airflow的操作,参照https://airflow.apache.org/docs/apache-airflow/stable/index.html

编写 DAG:每日自动触发销售分析

以下是示例的python代码,用于定时调用Dify工作流的APIimport pendulumimport requestsimport jsonfrom airflow.models.dag import DAGfrom airflow.operators.python import PythonOperatorfrom airflow.models import VariableCSVFILEPATH = "/yourpath/dailysaledata.csv"DIFYAPIURL = "https://dify-dms.aliyuncs.com/v1" # 替换成你的 Dify Workflow API URL# 从 Airflow Variable 中安全地获取 API KeyDIFYAPIKEY = Variable.get("difyapikey")APPAPIKEY= Variable.get("appapikey")def calldifyworkflowwithcsv(kwargs):"""读取 CSV 文件内容,并将其作为文件上传调用 Dify 工作流。"""print(f"准备从 '{CSVFILEPATH}' 读取文件…")try:with open(CSVFILEPATH, 'rb') as f:filestoupload = {'file': ('dailysaledata.csv', f, 'document/csv')}# 准备 API 请求的 headers 和 bodyheaders = {'Authorization': f'Bearer {APPAPIKEY}','DifyApiKey': f'{DIFYAPIKEY}',}““fileuploadresponse=requests.post(DIFYAPIURL+'/files/upload',headers=headers,data={'user': 'airflow-user-demo'},files=filestoupload,)print(fileuploadresponse.json())“fileid=fileuploadresponse.json().get('id')headers.update({'Content-Type': 'application/json'})“# 'inputs' 通常是json字符串# 'user' 是必须的,代表最终用户的标识符inputdata = {'salesdata':  {"type": "document","transfermethod": "localfile","uploadfileid": fileid}}data = {'inputs': inputdata,'user': 'airflow-user-demo','responsemode': 'blocking',}print("开始调用 Dify API…")print(f"URL: {DIFYAPIURL}")“response = requests.post(DIFYAPIURL+'/workflows/run',headers=headers,json=data,)“# 检查请求是否成功response.raiseforstatus()“print(f"API 调用成功!状态码: {response.statuscode}")“# 打印响应内容print("— Dify API Response —")“print(response.json()["data"]["outputs"]["answer"])“print("n— End of Response —")“# 你也可以将完整的响应推送到 XComs,以便下游任务使用# ti = kwargs['ti']# ti.xcompush(key='difyresponse', value=fullresponse)except FileNotFoundError:print(f"错误:文件未找到于 '{CSVFILEPATH}'")raiseexcept requests.exceptions.RequestException as e:print(f"API 调用失败: {e}")raisewith DAG(dagid="difyworkflow",startdate=pendulum.datetime(2023, 10, 27, tz="Asia/Shanghai"),# '0 8 * * *' 代表每天早上8:00 (UTC+8)# Airflow 默认使用 UTC,但 Cron 表达式本身不带时区,调度器会根据 DAG 的时区设置来解释schedule="0 8 * * *",catchup=False,tags=["dify", "api", "example"],docmd="""### Dify 工作流调用 DAG此 DAG 每天早上8点执行,它会:1. 从本地文件系统读取一个 CSV 文件。2. 将该 CSV 文件作为附件,调用一个 Dify 工作流。3. 打印出 Dify API 的响应。""") as dag:rundifyworkflow = PythonOperator(taskid="calldify",pythoncallable=calldifyworkflowwith_csv,)
📌注意:通过API调用Dify工作流中想要上传本地文件,需要先通过/files/upload 接口传入相应的文件获取文件id,再将文件id传入工作流中。
创建完成后打开Airflow实例,可以看到创建的定时任务;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

每日 8:00,系统自动调用 Dify 工作流,最终由钉钉机器人推送分析报告;

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

总结与思考
通过 DMS Notebook + DMS Airflow 对 Dify 的能力扩展,我们成功构建了一个具备以下特性的企业级 Agent 开发范式:

一个销售数据分析机器人的诞生:看 Dify 如何在 DMS 助力下实现自动化闭环

这套方案不仅解决了 Dify 当前的局限性,更重要的是,它保留了 Dify 的低代码优势,同时通过与成熟数据基础设施(Notebook + Airflow)的深度集成,实现了 “敏捷开发”与“工程可靠”的平衡。

🌟核心理念:Agent 的价值不在于“全自动”,而在于“可扩展、可调度、可运维”。真正的生产级智能系统,一定是平台能力与工程实践的结合体

前沿技术新闻资讯知识图谱

本体论又火了,他能优化我的 Agent 效果么?

2026-7-4 12:06:43

企业落地新闻资讯智能化改造

埃森哲和微软成立 FDE Practice:交付能力正在从"手艺"变成"可批发的产品

2026-7-4 12:14:40

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索