首页 / 资讯中心 / 文章详情

Dify工作流实现双模型分栏流式对比:从原理到实践

Dify工作流实现双模型分栏流式对比:从原理到实践 ★ FEATURED ARTICLE
最开始我对比两个模型的回答质量用的还是最笨的办法开两个浏览器标签页把同一个问题分别粘进去再左右来回切。遇到长回答还得自己对对齐位置效率低到让人怀疑人生。直到我把这套动作搬进 Dify用可视化工作流把两个模型接进同一条链路一次请求发出去两个模型同时在后台跑结果通过分栏页面流式打印出来——整个体验完全不一样了。双模型分栏同步流式输出这个需求听起来很唬人其实拆开就是同题、双答、并排、流式。这篇文章会把完整的实现思路、节点配置和前端接入代码都拆开讲顺便聊聊为什么在这个场景下Dify 比起 OpenWebUI 更值得优先考虑。适合正在做模型选型对比、想给团队搭一个多模型评测小工具、或者单纯想在同一个画布上直观感受不同模型输出差异的人。1. 方案选型与核心需求拆解1.1 双模型对比场景的需求到底长什么样很多人做模型对比第一反应是我开两个网页不就行了但真上手就会发现痛点很明显。同一个问题两遍提问意味着你要手动维护输入的一致性两个模型并行回复意味着你要持续横向对照滚动进度如果想记录评测结论还得自己复制粘贴、标注模型名。这套流程如果只是偶尔三五次还能忍一旦要成规模做评测纯手工就彻底不行了。所以双模型对比工具的隐藏需求其实是三块。第一块是输入统一同一个问题只敲一遍同时发给两个模型。第二块是输出并排左右两栏天然形成视觉对照谁答得长、谁答得短、谁在胡扯一眼就能看出来。第三块是流式体验最好两个模型边生成边滚动像两个选手同时答题而不是等全部生成完才一次性弹出来。这个需求本身不复杂但对工具形态提出了明确要求要能编排多模型调用要能聚合多路输出要能支持流式协议。再说得直白一点整个需求落到执行层就四个字同题、双答、并排、流式。目标是把一次评测动作从手动操作半小时压缩成输入问题、点发送、看结果。1.2 为什么在这个场景下 Dify 比 OpenWebUI 更合适先给 OpenWebUI 说句公道话它是一个很好用的自托管聊天界面界面清爽支持接入多种模型后端社区里也有大量插件。我自己也用过一段时间日常单聊非常舒服。但它的核心交互模型是一个会话对应一个模型消息列表按时间线平铺切换模型靠的是下拉选择器。这个架构没问题可一旦你想让两个模型同时回答同一道题并把结果分栏展示它默认做不到需要你去改前端状态管理、改消息渲染逻辑、改 API 调用层本质上是做二次开发。Dify 的出发点不一样。它有一个可视化工作流引擎应用可以被拆成节点图开始节点拿用户输入LLM 节点负责调模型结束节点负责聚合结果。两个 LLM 节点只要相互没有依赖关系就会并行执行天然支持同题双答这个诉求。再加上结束节点支持多变量输出前端可以按字段分别渲染左右两栏等于把最费劲的编排和聚合部分都交给了平台侧。所以秒杀这个说法严格来说是不严谨的OpenWebUI 本身没做错什么只是它和 Dify 的定位不一样。如果只用一句话总结我的判断OpenWebUI 是给人和模型聊天设计的Dify 更适合人编排模型做事的场景。双模型分栏对比恰好属于后者。对比项OpenWebUIDify 工作流多模型同时输出不支持需自行改前端原生支持节点并行调度多路流式聚合消息流按会话隔离结束节点聚合多变量输出零代码程度低改代码才能扩展高拖节点配置即可日常单聊体验很好一般偏工具化脚本化评测记录弱强可对接 API 存档1.3 两条落地路径工作流聚合与双请求并行具体落地时有两条路径可以选我先把它们摆在前面免得后面越读越乱。路径 A 是单工作流聚合。在 Dify 里创建一个工作流应用里面同时挂两个 LLM 节点前端只需要发一个请求后端等两条模型分支跑完后把结果以 SSE 流式吐给前端前端按输出字段分栏渲染。优点是节点少、逻辑集中、一次请求搞定缺点是响应时间受制于最慢的那个模型两边没法独立逐字跳动。路径 B 是双请求并行。在 Dify 里分别创建两个对话应用一个用模型 A一个用模型 B前端同时向两个应用的 API 发请求各自独立接收流式数据再放到同一页面的一左一右两个容器里。优点是流式体验最真实两个模型谁先出字谁先展示缺点是要维护两套应用上下文割裂后端没法做统一聚合和后续扩展。路径 A 在架构简洁上完胜路径 B 在流式细腻度上占优。如果你的核心诉求是快速搭建一个对比台子路径 A 已经足够如果你想追求极致的逐字双栏跳动可以看路径 B 的补充说明。本文主体按照路径 A 来讲因为它在 Dify 侧的零代码优势最明显。2. 环境准备与模型接入2.1 部署 Dify 并做基础初始化Dify 的部署本身不算复杂官方提供了 Docker Compose 编排模板基本上是在一台装有 Docker 和 Docker Compose 的机器上拉取编排文件后执行启动命令然后等待服务起来就行。我第一次部署时大概花了十几分钟主要是镜像拉取需要一点时间。docker compose up -d启动完成后访问部署机器的对应端口会进入初始化页面。这里需要创建一个管理员账号邮箱和密码按你自己的习惯填建议密码用强密码记在密码管理器里。初始化完成后进入工作台左侧能看到应用列表、工具、知识库等菜单。不同版本界面布局会略有差异但核心概念是稳定的。有一个小建议部署时把服务器时区、网络策略提前确认好尤其是后面要调用外部模型 API 时网络连通性决定你能不能正常走通两个模型节点。如果你在本地跑通后要迁移到服务器Docker 的 volume 数据是跟着 compose 工程走的记得一并备份迁移。2.2 接入两个模型供应商工作台里找到设置进入模型供应商页面。Dify 会列出一批主流接入方式包括商业 API、开源模型本地部署、以及各类兼容协议。我们要做双模型对比至少需要接两家或者接同一家但使用两个不同的模型实例。接入时的核心动作是填 API 密钥和模型名称。商业 API 一般是在对应的模型服务平台申请密钥然后把密钥配到 Dify 里选择具体的模型标识。本地部署的开源模型则要看你的模型服务是否暴露了兼容接口把服务地址和模型名填进去跑通连通性测试即可。这里建议你做一张接线表把两个模型各自的模型名称、上下文长度、最大输出 Token 上限、服务地址、密钥归属方记录清楚。别嫌麻烦后面配置 LLM 节点时Max Token 和上下文长度都要参考这张表填填错了要么报错要么输出被截断。模型代称接入方式关键参数记录模型 A商业 API / 本地服务模型名、上下文长度、Token 上限模型 B商业 API / 本地服务模型名、上下文长度、Token 上限2.3 创建应用并确认接口形态模型接入完成后回到应用列表新建一个工作流应用。为什么用工作流而不是聊天助手原因很简单工作流应用的完成形态是输入一堆变量、输出一堆变量非常适合双模型聚合聊天助手的默认形态是单会话对话虽然也可以拖节点但要做多变量分栏输出反而不够直接。如果之后你确实希望保留多轮会话上下文比如用户追问时不丢失前面说过的话可以改用聊天流应用类型。它的节点编排思路和工作流基本一致但系统会自动维护会话历史LLM 节点可以用记忆能力。单次对比评测用工作流足够想要追问式评测再切聊天流。应用创建完成后先记住两个入口一个是 WebApp 的预览地址适合在浏览器里直接点击测试另一个是API 访问页面里面能看到应用专属的 API 密钥和请求地址。后面我们要走的就是 API 接口因为分栏展示需要自定义前端页面不走 Dify 自带的 WebApp 界面。提示应用 API 密钥是数据访问凭证在浏览器前端直接暴露等于裸奔生产环境必须放到你自己的后端服务里通过服务端转发请求不要直接下发到页面。3. 双模型分栏工作流编排3.1 工作流的节点结构长什么样进入工作流画布后默认会有一个开始节点和一个结束节点。我们要在中间挂两个并行的 LLM 节点最终结构是开始节点连接 LLM 节点 A 和 LLM 节点 B这两个节点再分别连接结束节点。从画布上看两个 LLM 节点是并排放在开始节点之后的。它们之间没有连线也没有数据依赖Dify 在调度时会认为这两个分支互不阻塞于是并行执行。这里有一个常见误解要澄清并行不是两个模型同时从一个节点里被调用而是两个独立节点同时运行。你可以在结束节点里看到两个分支的名字在运行记录里看到两个节点的耗时它们是两条独立执行线。我给这套结构取了名字叫做1 拖 2 汇 1一个开始节点拖出两条模型分支最后汇聚到一个结束节点。后面你想扩展成三模型、四模型只需继续往中间加拖出去的 LLM 节点结构逻辑完全一样。3.2 开始节点与 LLM 节点的参数配置开始节点决定用户输入从哪里进来。工作流应用默认情况下用户的问题会被放在sys.query这个系统字段中你可以在开始节点里把它显式暴露成一个输入变量命名为query类型选择段落。这样后续 LLM 节点就能直接引用这个变量保证两个模型拿到的是同一个用户问题。LLM 节点 A 的配置是这样模型选择模型 A系统提示词填你是模型 A 的评测实例请客观、结构化地回答用户问题不要提及你的身份名称用户提示词填{{query}}温度设为 0.2Max Token 设为 1024。LLM 节点 B 完全对标只是模型改成模型 B系统提示词里对应改成你是模型 B 的评测实例。温度为什么要设低因为我们在做对比评测要的是可复现的、稳定输出的结果不是天马行空的创作。温度高会让同一个问题每次回答都不一样对比失去参考意义。如果你做的是创意文案对比可以把温度调到 0.7 左右这是场景差异不是铁律。节点模型温度Max Token系统提示词要点LLM 节点 A模型 A0.21024你是模型 A 的评测实例客观回答LLM 节点 B模型 B0.21024你是模型 B 的评测实例客观回答3.3 结束节点与流式输出开关两个 LLM 节点之后来到结束节点。结束节点是整条工作流的出口这里有两个关键设置。第一是流式输出模式。Dify 的结束节点允许在普通输出和流式输出之间切换普通输出会把所有变量打包成 JSON 一次性返回流式输出则会以 SSE 协议边生成边推送给客户端。我们要做的是流式打字机效果必须把结束节点的输出类型选成流式输出。第二是输出变量的定义。在结束节点里新增两个输出变量一个叫model_a_answer取值来源指向 LLM 节点 A 的输出另一个叫model_b_answer取值来源指向 LLM 节点 B 的输出。保存后在节点输出区域能看到完整输出字段。前端解析流式数据时就是按这两个字段名来分别更新左右两栏的。这里你可能会问为什么结束节点要等两个分支都跑完才输出因为结束节点必须拿到全部输入变量而它的输入来自两条分支所以天然要等最后一个模型完成。这意味着整体响应时间是 max(模型 A 耗时, 模型 B 耗时)。这个等最慢模型的特性在路径 A 里是明确的取舍想要两边独立先到先得那就去看路径 B 的双请求方案。3.4 发布应用并拿到 API 凭证到这一节Dify 侧的核心配置就算完成了。点击画布右上角的发布按钮让应用进入已发布状态。注意未发布的应用调试时只能从画布内部跑发布后才能通过外部 API 和 WebApp 访问。发布之后进入API 访问页面新建一个 API 密钥复制保存。接下来用 curl 或者 API 调试工具做一个冒烟测试确认工作流能正常返回流式数据。一个最简请求长这样curl -X POST https://your-domain/v1/workflows/run \ -H Authorization: Bearer app-xxxxxx \ -H Content-Type: application/json \ -d { inputs: {query: 请用三点理由说明为什么需要多模型对比评测}, response_mode: streaming, user: tester }不同版本的 Dify 在 API 路径和字段上可能有细微差异以你部署平台上API 访问页给出的接口文档为准。第一次调通流式接口后你会看到响应体是一串data:开头的事件流里面带着输出字段的增量信息这就是下一步前端要解析的内容。4. 前端分栏同步流式渲染4.1 流式响应的底层格式为什么前端能拿到打字机效果底层靠的是 SSE也就是 Server-Sent Events。它不是 WebSocket而是一种基于 HTTP 的长连接协议服务端可以持续向客户端推送数据客户端用一个普通的 fetch 请求就能读取流式内容。Dify 的流式接口返回的 SSE 数据每一段长这样以data:开头后面跟着一个 JSON 字符串以空行分隔不同事件。JSON 里通常包含执行元信息和输出数据其中输出数据再嵌套outputs字段。我们需要关心的是outputs.model_a_answer和outputs.model_b_answer。每次收到新的 data 事件这两个字段的内容往往是截至当前的累计文本所以前端可以简单地直接把当前值赋给对应容器。一个比较容易踩的点是 SSE 数据会被网络传输切成多个 chunk你不能假设一次 fetch read 就拿到完整一行。正确做法是维护一个缓冲区把读取到的数据按换行符切分完整的行再做解析半截的行留在缓冲区等下一个 chunk。这部分逻辑不复杂但写错会导致字段丢失和 JSON 解析报错。4.2 一个可直接用的分栏页面页面结构很简单顶部一个输入框和一个发送按钮下方左侧放模型 A 的回答右侧放模型 B 的回答。布局直接用 CSS Grid 就能实现两栏宽度相等中间留一点间距。div idinputBar input idquestionInput placeholder输入你想对比的问题 / button idsendBtn发送/button /div div idcompareGrid div classcol idcolA div classcol-title模型 A/div div classcol-body idoutputA/div /div div classcol idcolB div classcol-title模型 B/div div classcol-body idoutputB/div /div /div#compareGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; min-height: 400px; } .col { border: 1px solid #e5e5e5; border-radius: 8px; padding: 16px; background: #fafafa; } .col-title { font-weight: 600; margin-bottom: 8px; } .col-body { white-space: pre-wrap; word-break: break-word; }前端读取流式的核心逻辑封装在一个异步函数里。发送请求后从response.body拿到读取器循环读取数据块解码后切行遇到data:开头就解析 JSON再根据outputs更新两个栏位。用white-space: pre-wrap是为了保留换行否则多行回答会挤成一坨。async function runCompare() { const query document.getElementById(questionInput).value.trim(); if (!query) return; const outputA document.getElementById(outputA); const outputB document.getElementById(outputB); outputA.textContent 模型 A 思考中...; outputB.textContent 模型 B 思考中...; const resp await fetch(/api/dify/workflow, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ query }) }); const reader resp.body.getReader(); const decoder new TextDecoder(); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); const lines buffer.split(\n); buffer lines.pop() || ; for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; const payload JSON.parse(trimmed.slice(5).trim()); const outputs payload.data payload.data.outputs; if (outputs) { if (outputs.model_a_answer ! null) { outputA.textContent outputs.model_a_answer; } if (outputs.model_b_answer ! null) { outputB.textContent outputs.model_b_answer; } } } } } document.getElementById(sendBtn).addEventListener(click, runCompare);如果你希望回答里的 Markdown 标题、加粗、列表能被渲染成样式可以把textContent改成innerHTML再在写出内容前调用一个 Markdown 渲染库把纯文本转成 HTML 后塞进栏位。不引入额外库也可以纯文本阅读对评测场景完全够用。我把这段逻辑放在服务于零代码目标下做了一个边界说明Dify 侧确实全程零代码但前端总得要一小段接入代码这是所有平台 API 模式下都绕不开的。好在代码是通用的第一次写好之后以后换模型、换任务都不用改前端。4.3 追求极致流式双请求并行方案路径 A 的缺点是两边要等最慢的模型。如果你对两边独立出字有执念可以走双请求并行方案在 Dify 里分别建两个模型单应用的对话助手各自露出一个 API 接口前端同时发两个 fetch 请求。核心改动是去掉同一个请求里对两个字段的依赖改成两个独立的异步流各自写各自的容器。两个 fetch 之间没有等待关系哪个模型先出字哪一栏就先滚动。缺点也很明显两条请求对应两套应用你需要在 Dify 侧维护两个应用的应用配置和提示词如果之后想加裁判模型、加知识库还要在外部把它们串起来。方案同步感流式细腻度实现成本扩展性单工作流聚合两边同时滚动受限于最慢模型低高双请求并行各自独立滚动完全实时中低我的建议是先把单工作流聚合跑通它已经能满足绝大多数对比需求。只有当你在意两边完全独立逐字渲染时再去尝试双请求并行不要在第一步就上复杂方案。5. 实操中常见的坑与排查5.1 流式输出不生效先查这三处我实测下来流式输出不生效通常不是前端问题而是后端三个设置中的一个没到位。第一结束节点的输出模式是否真的切到了流式输出。它和普通输出在画布上看不出来但运行记录里能明显看到差异普通输出是整体包在 JSON 里返回流式输出则是一段一段data:推出来。第二请求体里的response_mode是否传了streaming。如果你在 API 请求里没传或者传了blockingDify 就不会走流式协议而是等完整结果一次性返回。第三反向代理是否缓冲了响应。如果前端和 Dify 之间隔着一层 Nginx默认的proxy_buffering会等响应积累到一定程度才统一转发表现就和流式完全相反。需要在代理层关闭缓冲location /api/dify/ { proxy_pass http://your-dify-backend/; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; }5.2 输出字段是空的或前端取不到这是第二个高频坑。Dify 工作流里自动生成的节点输出变量名常常带上一长串前缀比如llm_xxx_text看起来和你在结束节点里定义的名字不直接对应。如果前端解析时硬编码了model_a_answer而实际返回的是带前缀的名字就会一直取不到内容。解决办法有两个。一个是在结束节点里把输出变量的 key 手动改成model_a_answer这种简洁名字这样前端代码就能按预期解析。另一个办法是先打印一次完整的 API 返回结构把实际的字段名抄下来再写前端。另一个细节是 JSON 转义。SSE 里传输的字段是 JSON 字符串多行文本会以\n呈现如果你没有调用JSON.parse就直接用输出的内容会是一堆反斜杠 n。前端的解析代码里一定要严格走JSON.parse再取字段不要在未解析的字符串上做正则截取。5.3 两边模型速度差异大出现明显空窗模型 A 可能秒回模型 B 可能要等二十秒合并输出时两栏的进度感非常不对称。这个问题的根子在模型本身的生成速度和 Max Token 上限差异。处理的办法是尽量把两个 LLM 节点的 Max Token 设为相同值避免一方因为上限小提前收尾另一方还在长篇大论。如果生成速度本身差异大可以考虑给快的模型在系统提示词里加一句请尽可能详细地展开你的回答让两边篇幅更接近。前端方面发送请求后两栏可以先显示模型思考中...的占位状态避免用户看着空白误以为卡死。5.4 长回答被截断与请求超时当模型回答很长超过节点配置的 Max Token 上限输出会在到达上限处戛然而止这个被截断的长短取决于设置。排查时先看两个 LLM 节点的 Max Token 是否足够再确认模型服务商侧的上下文上限是否匹配。请求超时则是另一个常见现象。模型生成慢、回答长、代理层读超时时间又设得短就会出现连接被服务端或中间层掐断。前端 fetch 请求如果中断两栏都停在半截状态。处理方式代理层proxy_read_timeout调长Dify 侧配长一点的超时窗口前端读取循环里做简单容错断流后提示连接中断请重试。5.5 快速排查速查表现象可能原因直接解决办法流式完全没效果一次性返回结束节点非流式模式切到流式输出前端收到但页面无变化API 字段名不匹配打印实际返回结构并修正字段内容里全是\n字符串没做 JSON.parse先 parse 再赋值请求被卡到超时代理或服务端超时短调大超时、关缓冲一个模型输出被截断Max Token 不足调大上限或裁短提示词首帧等待过久聚合等待最慢分支改用双请求并行方案5.6 跨域与密钥保护最后这点非常重要直接关系到生产环境能不能用。Dify 的 API 密钥一旦暴露在公网页面里任何人都能拿着你的密钥去调用应用额度会被刷爆甚至被拿去跑别的任务。所以前端页面不应该直接拿 API 密钥请求 Dify 接口。正确姿势是自己做一个极薄的转发层前端请求你自己的域名下的某个路径你的服务端在这个路径里带上 Dify 的 API 密钥转发到 Dify 接口再把流式响应原样返回给前端。前面 4.2 节示例代码里请求的是/api/dify/workflow而不是 Dify 的真实域名就是给这个转发层留的位置。CORS 跨域问题也在转发层统一解决因为同域请求不存在跨域限制。6. 扩展从双模型对比到评测工作台6.1 多模型扩展与布局调整双模型跑通之后加第三个、第四个模型只是重复劳动。回到工作流画布把新的 LLM 节点拖出来接在开始节点之后输出引到结束节点。前端布局从两栏改成四栏只需改动一行 CSS 的grid-template-columns把1fr 1fr换成repeat(4, 1fr)。模型多了以后建议给每栏加上模型标识、响应耗时等元信息让对比结果更有参考价值。6.2 加一个裁判模型做自动点评多模型对比离不开最终结论。你可以在两个模型答案的基础上再挂第三个 LLM 节点专门扮演裁判角色。它的输入引用model_a_answer和model_b_answer提示词写成请从准确性、完整性、结构化程度三个维度对比两个回答并给出你的倾向结论。这样一次请求下来你不仅拿到两个模型的回答还拿到一份自动生成的评测意见。Dify 节点之间传递变量非常方便裁判节点的用户提示词直接用双大括号引用前面节点的输出变量即可。不需要写任何胶水代码这是工作流模式对比起代码拼接的又一大优势。6.3 接上知识库变成双模型 RAG 对比如果你的评测场景涉及私有知识比如公司内部文档或产品资料可以给工作流加一个知识检索节点。用户问题进来后先走知识检索把召回结果拼进两个 LLM 节点的提示词里让两个模型都基于同一份参考资料作答。这样做的好处是两者拿到的是完全相同的检索上下文评测公平性更有保证。知识检索节点在 Dify 里是独立节点输出变量通常是一段包含召回内容的文本。把这段文本插入到 LLM 节点的用户提示词前面比如请基于以下资料回答问题{{knowledge}}两个节点各自引用同一个知识变量检索只需要执行一次不会额外消耗双倍检索费用。6.4 沉淀成团队工具的一点个人心得整套流程跑通后我最直接的体感是再做模型对比我不会开一堆标签页了。打开工具页面输入问题点发送两栏同时滚动答案稳定排列旁边还挂着裁判点评效率和体验是手工方案没法比的。最后分享一条我踩了几次坑才总结出来的经验。评测对比时两个模型的温度、Max Token、系统提示词必须严格对齐任何一边的不同都会让结果失去可比性。建议把评测用的问题集固定下来输出结果用非流式模式另存一份 JSON 存档流式分栏只用来给人类实时查看。真正要做数据分析和结论沉淀时结构化的非流式数据比流式界面可靠得多。这套模板改一改提示词、换一换模型几乎可以复用到所有我到底该选哪个模型的决策场景里。
阅读完成 · 觉得有帮助?
咨询建站