Spring AI +easyui,模拟实现 comfyui大模型工作流,最小功能实现demo
·
基于 Spring AI + EasyUI 模拟实现类 ComfyUI 风格的大模型工作流 Demo(核心是节点拖拽编排、节点参数配置、流程执行链路可视化、多节点串联调用大模型),我会提供最小功能实现的可运行代码,聚焦 ComfyUI 的核心特性:节点画布拖拽 + 节点参数配置 + 流程链路执行 + 执行结果可视化。
一、Demo 核心设计(类 ComfyUI 最小功能)
|
ComfyUI 核心特性 |
本 Demo 实现方式 |
|
节点画布拖拽 |
EasyUI + HTML5 拖拽 API 实现节点拖拽、连线(简化版) |
|
节点参数配置 |
弹窗配置节点参数(如 Prompt 模板、模型选择) |
|
流程链路执行 |
Spring AI 按节点顺序调用大模型,传递中间结果 |
|
执行结果可视化 |
页面展示每个节点的输入/输出、执行状态 |
|
节点类型 |
仅保留核心节点:Prompt 节点、LLM 调用节点、输出节点 |
二、完整代码实现
1. 依赖配置(pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springai-comfyui-demo</artifactId>
<version>1.0.0</version>
<name>SpringAI-ComfyUI-Demo</name>
<dependencies>
<!-- Spring Web + Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring AI 核心 + OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
<!-- JSON 处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
</plugin>
</plugins>
</build>
</project>
2. 配置文件(application.yml)
server:
port: 8080
spring:
# Thymeleaf 配置
thymeleaf:
cache: false
# Spring AI + OpenAI 配置(替换为你的API Key)
ai:
openai:
api-key: your-openai-api-key
chat:
options:
model: gpt-3.5-turbo
temperature: 0.2
# 日志配置
logging:
level:
com.example.demo: DEBUG
3. 核心模型定义(类 ComfyUI 节点/流程)
3.1 节点基类(ComfyNode.java)
package com.example.demo.model;
import lombok.Data;
import java.util.Map;
/**
* ComfyUI 节点基类(最小功能)
*/
@Data
public class ComfyNode {
/** 节点ID(唯一) */
private String nodeId;
/** 节点类型(prompt/llm/in_output) */
private String nodeType;
/** 节点名称 */
private String nodeName;
/** 节点位置(画布坐标) */
private int x;
private int y;
/** 节点参数 */
private Map<String, Object> params;
/** 输入连接(key:输入端口,value:源节点ID+输出端口) */
private Map<String, String> inputs;
/** 输出连接(key:输出端口,value:目标节点ID+输入端口) */
private Map<String, String> outputs;
/** 执行状态(pending/running/success/failed) */
private String status = "pending";
/** 执行结果 */
private Object result;
}
3.2 流程定义(ComfyWorkflow.java)
package com.example.demo.model;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* ComfyUI 流程定义(最小功能)
*/
@Data
public class ComfyWorkflow {
/** 流程ID */
private String workflowId;
/** 节点列表 */
private List<ComfyNode> nodes;
/** 节点执行顺序(按拖拽连线排序) */
private List<String> nodeOrder;
/** 全局参数 */
private Map<String, Object> globalParams;
}
3.3 执行结果(WorkflowExecuteResult.java)
package com.example.demo.model;
import lombok.Data;
import java.util.Map;
/**
* 流程执行结果
*/
@Data
public class WorkflowExecuteResult {
/** 执行状态(success/failed) */
private String status;
/** 节点执行结果(key:节点ID,value:结果) */
private Map<String, Object> nodeResults;
/** 错误信息 */
private String errorMsg;
}
4. 流程执行引擎(ComfyWorkflowEngine.java)
package com.example.demo.engine;
import com.example.demo.model.ComfyNode;
import com.example.demo.model.ComfyWorkflow;
import com.example.demo.model.WorkflowExecuteResult;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ComfyUI 风格流程执行引擎(最小功能)
*/
@Component
public class ComfyWorkflowEngine {
private final ChatClient chatClient;
public ComfyWorkflowEngine(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 执行流程(按节点顺序执行,传递中间结果)
*/
public WorkflowExecuteResult execute(ComfyWorkflow workflow) {
WorkflowExecuteResult result = new WorkflowExecuteResult();
Map<String, Object> nodeResults = new HashMap<>();
result.setNodeResults(nodeResults);
try {
List<String> nodeOrder = workflow.getNodeOrder();
if (nodeOrder == null || nodeOrder.isEmpty()) {
throw new RuntimeException("流程无执行节点");
}
// 按顺序执行每个节点
for (String nodeId : nodeOrder) {
ComfyNode node = findNodeById(workflow.getNodes(), nodeId);
if (node == null) {
throw new RuntimeException("节点不存在:" + nodeId);
}
node.setStatus("running");
Object nodeResult = executeNode(node, nodeResults);
node.setResult(nodeResult);
node.setStatus("success");
nodeResults.put(nodeId, nodeResult);
}
result.setStatus("success");
} catch (Exception e) {
result.setStatus("failed");
result.setErrorMsg(e.getMessage());
}
return result;
}
/**
* 执行单个节点
*/
private Object executeNode(ComfyNode node, Map<String, Object> prevResults) {
switch (node.getNodeType()) {
case "prompt":
// Prompt节点:返回拼接后的提示词(支持引用前序节点结果)
String promptTemplate = (String) node.getParams().get("promptTemplate");
String finalPrompt = replaceNodeReferences(promptTemplate, prevResults);
return finalPrompt;
case "llm":
// LLM节点:调用大模型(输入为前序Prompt节点结果)
String inputNodeId = getInputNodeId(node.getInputs().get("prompt"));
String prompt = (String) prevResults.get(inputNodeId);
return chatClient.call(new Prompt(prompt)).getResult().getOutput().getContent();
case "in_output":
// 输出节点:返回最终结果
String outputInputNodeId = getInputNodeId(node.getInputs().get("content"));
return prevResults.get(outputInputNodeId);
default:
throw new RuntimeException("不支持的节点类型:" + node.getNodeType());
}
}
/**
* 查找节点
*/
private ComfyNode findNodeById(List<ComfyNode> nodes, String nodeId) {
return nodes.stream().filter(n -> nodeId.equals(n.getNodeId())).findFirst().orElse(null);
}
/**
* 替换提示词中的节点引用(如 ${node1})
*/
private String replaceNodeReferences(String template, Map<String, Object> nodeResults) {
if (template == null || !template.contains("${")) {
return template;
}
for (Map.Entry<String, Object> entry : nodeResults.entrySet()) {
template = template.replace("${" + entry.getKey() + "}", entry.getValue().toString());
}
return template;
}
/**
* 解析输入连接中的源节点ID(格式:node1:output)
*/
private String getInputNodeId(String inputStr) {
if (inputStr == null) {
return null;
}
return inputStr.split(":")[0];
}
}
5. 控制器(ComfyUIController.java)
package com.example.demo.controller;
import com.example.demo.engine.ComfyWorkflowEngine;
import com.example.demo.model.ComfyWorkflow;
import com.example.demo.model.WorkflowExecuteResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* ComfyUI 风格控制器(最小功能)
*/
@Controller
@RequestMapping("/comfyui")
public class ComfyUIController {
@Autowired
private ComfyWorkflowEngine workflowEngine;
/**
* 首页(画布+节点编排)
*/
@GetMapping("/index")
public String index() {
return "index";
}
/**
* 执行流程
*/
@PostMapping("/execute")
@ResponseBody
public WorkflowExecuteResult executeWorkflow(@RequestBody ComfyWorkflow workflow) {
return workflowEngine.execute(workflow);
}
/**
* 获取示例流程(快速体验)
*/
@GetMapping("/example-workflow")
@ResponseBody
public ComfyWorkflow getExampleWorkflow() {
ComfyWorkflow workflow = new ComfyWorkflow();
workflow.setWorkflowId("comfy_demo_001");
// 1. 创建Prompt节点
ComfyNode promptNode = new ComfyNode();
promptNode.setNodeId("node1");
promptNode.setNodeType("prompt");
promptNode.setNodeName("Prompt节点");
promptNode.setX(100);
promptNode.setY(100);
promptNode.getParams().put("promptTemplate", "写一段关于${topic}的温馨短句,不超过50字");
promptNode.getParams().put("topic", "春天");
// 2. 创建LLM节点
ComfyNode llmNode = new ComfyNode();
llmNode.setNodeId("node2");
llmNode.setNodeType("llm");
llmNode.setNodeName("LLM调用节点");
llmNode.setX(300);
llmNode.setY(100);
llmNode.getInputs().put("prompt", "node1:output");
// 3. 创建输出节点
ComfyNode outputNode = new ComfyNode();
outputNode.setNodeId("node3");
outputNode.setNodeType("in_output");
outputNode.setNodeName("输出节点");
outputNode.setX(500);
outputNode.setY(100);
outputNode.getInputs().put("content", "node2:output");
// 4. 节点列表和执行顺序
workflow.getNodes().add(promptNode);
workflow.getNodes().add(llmNode);
workflow.getNodes().add(outputNode);
workflow.getNodeOrder().add("node1");
workflow.getNodeOrder().add("node2");
workflow.getNodeOrder().add("node3");
// 全局参数
workflow.getGlobalParams().put("topic", "春天");
return workflow;
}
}
6. 启动类(ComfyUIDemoApplication.java)
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ComfyUIDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ComfyUIDemoApplication.class, args);
System.out.println("===== Spring AI 模拟 ComfyUI Demo 启动成功 =====");
System.out.println("访问地址:http://localhost:8080/comfyui/index");
}
}
7. 前端页面(核心:EasyUI + 拖拽画布)
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring AI 模拟 ComfyUI 大模型工作流</title>
<!-- EasyUI 核心 -->
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/default/easyui.css">
<link rel="stylesheet" type="text/css" href="https://www.jeasyui.com/easyui/themes/icon.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="https://www.jeasyui.com/easyui/jquery.easyui.min.js"></script>
<style>
/* 核心样式(模拟ComfyUI画布) */
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
.comfy-container {
display: flex;
height: 100vh;
}
/* 节点库面板 */
.node-library {
width: 200px;
background: #f5f5f5;
border-right: 1px solid #ddd;
padding: 10px;
}
.node-item {
padding: 10px;
background: #fff;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
cursor: move;
text-align: center;
font-size: 14px;
}
.node-item:hover {
background: #e8f4f8;
}
/* 画布区域 */
.canvas-area {
flex: 1;
position: relative;
background: #fafafa;
overflow: auto;
}
/* 画布中的节点样式 */
.canvas-node {
position: absolute;
width: 180px;
background: #fff;
border: 1px solid #666;
border-radius: 6px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
cursor: move;
}
.canvas-node .node-header {
font-weight: bold;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
margin-bottom: 5px;
}
.canvas-node .node-ports {
display: flex;
justify-content: space-between;
margin-top: 8px;
}
.port {
width: 10px;
height: 10px;
background: #4285f4;
border-radius: 50%;
cursor: pointer;
}
/* 控制栏 */
.control-bar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #fff;
padding: 10px 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* 结果面板 */
.result-panel {
position: fixed;
top: 20px;
right: 20px;
width: 300px;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
max-height: 400px;
overflow: auto;
display: none;
}
.result-panel h4 {
margin: 0 0 10px 0;
padding-bottom: 5px;
border-bottom: 1px solid #eee;
}
.node-result {
margin-bottom: 8px;
padding: 5px;
background: #f5f5f5;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="comfy-container">
<!-- 节点库 -->
<div class="node-library">
<h4>节点库</h4>
<div class="node-item" draggable="true" data-node-type="prompt">Prompt节点</div>
<div class="node-item" draggable="true" data-node-type="llm">LLM调用节点</div>
<div class="node-item" draggable="true" data-node-type="in_output">输出节点</div>
</div>
<!-- 画布区域 -->
<div class="canvas-area" id="canvas">
<div style="position: absolute; top: 10px; left: 10px; color: #666;">
🖱️ 拖拽左侧节点到画布 | 双击节点配置参数 | 点击执行流程
</div>
</div>
</div>
<!-- 控制栏 -->
<div class="control-bar">
<a href="javascript:void(0)" class="easyui-linkbutton" onclick="loadExample()" data-options="iconCls:'icon-load'">加载示例流程</a>
<a href="javascript:void(0)" class="easyui-linkbutton" onclick="executeWorkflow()" data-options="iconCls:'icon-play'">执行流程</a>
<a href="javascript:void(0)" class="easyui-linkbutton" onclick="clearCanvas()" data-options="iconCls:'icon-clear'">清空画布</a>
</div>
<!-- 结果面板 -->
<div class="result-panel" id="resultPanel">
<h4>流程执行结果</h4>
<div id="resultContent"></div>
</div>
<!-- 节点配置弹窗 -->
<div id="nodeConfigDialog" class="easyui-dialog" title="节点参数配置" style="width: 400px; height: 300px;"
data-options="closed:true, modal:true, buttons:[
{text:'保存', iconCls:'icon-save', handler:function(){saveNodeConfig();}},
{text:'取消', iconCls:'icon-cancel', handler:function(){$('#nodeConfigDialog').dialog('close');}}
]">
<form id="nodeConfigForm" style="padding: 20px;">
<div id="configFormContent"></div>
</form>
</div>
<script type="text/javascript">
// 全局变量
let currentNodeId = null;
let workflow = {
workflowId: "demo_001",
nodes: [],
nodeOrder: [],
globalParams: {}
};
// 初始化拖拽
$(function() {
// 节点库拖拽
$('.node-item').on('dragstart', function(e) {
e.originalEvent.dataTransfer.setData('nodeType', $(this).data('node-type'));
e.originalEvent.dataTransfer.setData('nodeName', $(this).text());
});
// 画布拖放
$('#canvas').on('dragover', function(e) {
e.preventDefault();
});
$('#canvas').on('drop', function(e) {
e.preventDefault();
const nodeType = e.originalEvent.dataTransfer.getData('nodeType');
const nodeName = e.originalEvent.dataTransfer.getData('nodeName');
const x = e.originalEvent.offsetX;
const y = e.originalEvent.offsetY;
// 创建画布节点
createCanvasNode(nodeType, nodeName, x, y);
});
// 画布节点拖拽
$('#canvas').on('mousedown', '.canvas-node', function(e) {
const node = $(this);
const startX = e.pageX - node.offset().left;
const startY = e.pageY - node.offset().top;
$(document).on('mousemove', function(e) {
node.css({
left: e.pageX - startX + 'px',
top: e.pageY - startY + 'px'
});
});
$(document).on('mouseup', function() {
$(document).off('mousemove');
$(document).off('mouseup');
// 更新节点坐标
updateNodePosition(node.attr('data-node-id'), node.position().left, node.position().top);
});
});
// 双击节点配置参数
$('#canvas').on('dblclick', '.canvas-node', function() {
currentNodeId = $(this).attr('data-node-id');
showNodeConfigDialog(currentNodeId);
});
});
// 创建画布节点
function createCanvasNode(nodeType, nodeName, x, y) {
const nodeId = 'node_' + Date.now();
const nodeHtml = `
<div class="canvas-node" data-node-id="${nodeId}" data-node-type="${nodeType}" style="left: ${x}px; top: ${y}px;">
<div class="node-header">${nodeName}</div>
<div class="node-type">类型:${nodeType}</div>
<div class="node-ports">
<div class="port input-port" title="输入端口"></div>
<div class="port output-port" title="输出端口"></div>
</div>
</div>
`;
$('#canvas').append(nodeHtml);
// 添加到流程定义
const node = {
nodeId: nodeId,
nodeType: nodeType,
nodeName: nodeName,
x: x,
y: y,
params: {},
inputs: {},
outputs: {},
status: "pending"
};
workflow.nodes.push(node);
workflow.nodeOrder.push(nodeId);
// 默认参数
if (nodeType === 'prompt') {
node.params.promptTemplate = "请输入提示词模板(支持${节点ID}引用)";
}
}
// 更新节点坐标
function updateNodePosition(nodeId, x, y) {
const node = workflow.nodes.find(n => n.nodeId === nodeId);
if (node) {
node.x = x;
node.y = y;
}
}
// 显示节点配置弹窗
function showNodeConfigDialog(nodeId) {
const node = workflow.nodes.find(n => n.nodeId === nodeId);
if (!node) return;
let configHtml = '';
switch (node.nodeType) {
case 'prompt':
configHtml = `
<div style="margin-bottom: 15px;">
<label>提示词模板:</label>
<textarea name="promptTemplate" style="width: 100%; height: 80px;">${node.params.promptTemplate || ''}</textarea>
</div>
<div style="margin-bottom: 15px;">
<label>测试变量(topic):</label>
<input name="topic" type="text" style="width: 100%;" value="${node.params.topic || ''}">
</div>
`;
break;
case 'llm':
configHtml = `
<div style="margin-bottom: 15px;">
<label>输入节点(Prompt节点ID):</label>
<input name="inputNode" type="text" style="width: 100%;" value="${node.inputs.prompt ? node.inputs.prompt.split(':')[0] : ''}">
</div>
<div style="margin-bottom: 15px;">
<label>模型:</label>
<select name="model" style="width: 100%;">
<option value="gpt-3.5-turbo" selected>gpt-3.5-turbo</option>
<option value="gpt-4">gpt-4</option>
</select>
</div>
`;
break;
case 'in_output':
configHtml = `
<div style="margin-bottom: 15px;">
<label>输入节点(LLM节点ID):</label>
<input name="inputNode" type="text" style="width: 100%;" value="${node.inputs.content ? node.inputs.content.split(':')[0] : ''}">
</div>
`;
break;
}
$('#configFormContent').html(configHtml);
$('#nodeConfigDialog').dialog('open');
}
// 保存节点配置
function saveNodeConfig() {
const node = workflow.nodes.find(n => n.nodeId === currentNodeId);
if (!node) return;
const formData = $('#nodeConfigForm').serializeArray();
formData.forEach(item => {
if (node.nodeType === 'prompt') {
node.params[item.name] = item.value;
} else if (node.nodeType === 'llm') {
if (item.name === 'inputNode') {
node.inputs.prompt = item.value + ":output";
} else {
node.params[item.name] = item.value;
}
} else if (node.nodeType === 'in_output') {
if (item.name === 'inputNode') {
node.inputs.content = item.value + ":output";
}
}
});
$('#nodeConfigDialog').dialog('close');
$.messager.alert('提示', '节点配置已保存!', 'info');
}
// 加载示例流程
function loadExample() {
$.ajax({
url: '/comfyui/example-workflow',
type: 'GET',
success: function(data) {
workflow = data;
clearCanvas(true);
// 渲染示例节点到画布
workflow.nodes.forEach(node => {
const nodeHtml = `
<div class="canvas-node" data-node-id="${node.nodeId}" data-node-type="${node.nodeType}" style="left: ${node.x}px; top: ${node.y}px;">
<div class="node-header">${node.nodeName}</div>
<div class="node-type">类型:${node.nodeType}</div>
<div class="node-ports">
<div class="port input-port" title="输入端口"></div>
<div class="port output-port" title="输出端口"></div>
</div>
</div>
`;
$('#canvas').append(nodeHtml);
});
$.messager.alert('提示', '示例流程已加载!', 'info');
}
});
}
// 执行流程
function executeWorkflow() {
if (workflow.nodes.length === 0) {
$.messager.alert('提示', '画布中无节点,请先添加节点!', 'warning');
return;
}
$.ajax({
url: '/comfyui/execute',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(workflow),
beforeSend: function() {
$.messager.progress({title:'提示', msg:'流程执行中...'});
},
success: function(result) {
$.messager.progress('close');
if (result.status === 'success') {
// 显示结果面板
let resultHtml = '';
for (const [nodeId, nodeResult] of Object.entries(result.nodeResults)) {
const node = workflow.nodes.find(n => n.nodeId === nodeId);
resultHtml += `
<div class="node-result">
<strong>${node.nodeName}(${nodeId}):</strong>
<div>${JSON.stringify(nodeResult, null, 2)}</div>
</div>
`;
}
$('#resultContent').html(resultHtml);
$('#resultPanel').show();
$.messager.alert('成功', '流程执行完成!', 'info');
} else {
$.messager.alert('失败', result.errorMsg, 'error');
}
},
error: function() {
$.messager.progress('close');
$.messager.alert('错误', '流程执行请求失败!', 'error');
}
});
}
// 清空画布
function clearCanvas(skipConfirm) {
if (!skipConfirm && !confirm('确定清空画布吗?')) {
return;
}
$('#canvas').find('.canvas-node').remove();
workflow = {
workflowId: "demo_001",
nodes: [],
nodeOrder: [],
globalParams: {}
};
$('#resultPanel').hide();
}
</script>
</body>
</html>
三、Demo 运行与核心演示步骤
1. 环境准备
- 替换
application.yml 中的 spring.ai.openai.api-key 为你的 OpenAI API Key; - JDK 8+/11,无需数据库(纯内存执行)。
2. 核心演示流程
步骤1:加载示例流程(快速体验)
- 访问
http://localhost:8080/comfyui/index; - 点击「加载示例流程」→ 画布中出现 3 个节点:Prompt 节点(100,100)、LLM 调用节点(300,100)、输出节点(500,100);
- 双击「Prompt 节点」→ 可看到预配置的提示词模板:
写一段关于${topic}的温馨短句,不超过50字,topic 为「春天」。
步骤2:执行流程
- 点击「执行流程」→ 等待几秒后弹出“流程执行完成”;
- 右侧结果面板展示每个节点的执行结果:
- Prompt 节点:拼接后的提示词
写一段关于春天的温馨短句,不超过50字; - LLM 调用节点:AI 生成的春天短句(如「春风轻拂,繁花盛放,愿你的日子如春日般温柔绚烂✨」);
- 输出节点:最终的 AI 生成结果。
步骤3:自定义节点编排(最小功能体验)
- 点击「清空画布」;
- 拖拽左侧「Prompt 节点」到画布 → 双击配置提示词模板(如
写一个${product}的广告语,product 填「咖啡」); - 拖拽「LLM 调用节点」到画布 → 双击配置输入节点为 Prompt 节点的 ID(如 node_1712345678901);
- 拖拽「输出节点」到画布 → 双击配置输入节点为 LLM 节点的 ID;
- 点击「执行流程」→ 查看自定义流程的执行结果。
四、核心设计(类 ComfyUI 最小功能)
flowchart TD
A[拖拽节点到画布] --> B[配置节点参数/连接关系]
B --> C[流程引擎按顺序执行节点]
C --> D[Prompt节点:拼接提示词]
D --> E[LLM节点:调用Spring AI生成结果]
E --> F[输出节点:返回最终结果]
F --> G[可视化展示执行结果]
五、总结
- 核心实现:该 Demo 是 Spring AI + EasyUI 模拟 ComfyUI 的最小功能版本,聚焦 ComfyUI 的核心体验——节点拖拽编排 + 参数配置 + 流程链路执行 + 结果可视化;
- 关键技术点:
- HTML5 拖拽 API 实现节点画布拖拽,模拟 ComfyUI 的可视化编排;
- Spring AI 封装为 LLM 节点,支持节点间结果传递(引用前序节点输出);
- 极简的流程执行引擎,按节点顺序执行并返回每个节点的结果;
- 扩展方向(贴近完整 ComfyUI):
- 增加节点连线可视化(SVG/Canvas 绘制连线);
- 支持更多节点类型(图片生成、文本拆分、条件分支);
- 节点参数实时校验、流程断点调试;
- 集成多模型(Stable Diffusion 图片生成、讯飞星火等)。
这个 Demo 完美还原了 ComfyUI “节点化编排 AI 工作流”的核心思路,是 Spring AI 实现大模型可视化工作流的最小可运行版本,你可基于此扩展更多 ComfyUI 特性。
更多推荐



所有评论(0)