九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、最近はローカルLLM(オフラインで実行できる生成AI)が面白くていろいろやっているんですが、この間つかったdifyで何かできないか考えていたところ、1つ思いつきました。
※difyとは、ノーコード/ローコードで生成AIアプリやワークフローを構築できるオープンソースのプラットフォームです。
それが・・・・・
dify駆動開発
です!
ぶっちゃけ、この言葉は私の造語なんで一般的ではないですが、つまりは、
difyにできることはやらせて、時短でウェブアプリ開発しよう!
というものです。
たとえば、difyは以下のようにワークフローを使って処理の流れをノーコードで実装できるわけです。

じゃあ、このできる部分はdifyにやらせて、それ以外はLaravelとかのウェブアプリで実装するという作戦ですね。
そこで!
今回はdify駆動開発で「見積書PDFからデータ抽出する」機能を作ってみます。
なお、抽出するデータは以下4つ。
- 会社名
- 発生日
- 合計金額
- タイプ
ということで、今回は以下のような人に向けて記事を書いています。
- 「difyにできることはやらせて、時短で開発したい」
- 「めんどうな処理はdifyに任せたい」
- 「開発工数を減らしながら、機能を増やしたい」
- 「AIを使って、開発を効率化したい」
- 「ノーコードツールで、開発の一部をスキップしたい」
- 「なるべく開発しないで、プロダクト作りたい」
- 「コードを書く量を最小化したい」
- 「社外秘のデータをローカル完結で処理したい」
- 「difyで何ができるか、実例で知りたい」
- 「開発効率とコスト削減を両立させたい」

「マリカー、優勝直前で
電源が落ちたんですが…2回も😩」
目次
前提として
difyがインストールされているのが大前提です。
もしまだの方は、以下ページを参考にしてください。
【dify】検索上位10記事をつかって、SEOを考慮した記事タイトルをつくる方法
※ちなみに途中からdifyを最新版にしたので、どこかおかしなところがでるかもです…すみません。お問い合わせから質問してください🙇
Ollamaをdocker-compose内で有効にする
今回はローカルLLMが必要ですので、Ollamaが使えるようにしておきます。
Ollamaをインストールする
Ollamaのインストールは、docker-compose.yamlから行います。
ただ、今回は上書きされる可能性があるので、その元になるdocker-compose-template.yamlの方を変更します。
docker/docker-compose-template.yaml
services:
# ここにいろんなサービス
ollama:
image: ollama/ollama:latest
container_name: ollama
ports:
- "11434:11434"
volumes:
- ollama-data:/root/.ollama
environment:
- OLLAMA_HOST=0.0.0.0:11434
restart: unless-stopped
volumes:
# ここにいろんなvolume
ollama-data:
driver: local
追加したら、docker-compose.yamlを再生成します。
./generate_docker_compose
これでdocker-compose.yamlにOllamaが入っていたら、完了です!
では、一度docker環境を再起動しましょう。
docker compose up -d
モデル「gemma3n:e4b」をインストールする
では、VRAM 4GBで使える軽量&賢いGoogle製モデル「gemma3n:e4b」をインストールします。
docker exec -it ollama ollama pull gemma3n:e4b
これで、Ollamaにgemma3n:e4bがインストールされました。
DifyでOllamaが有効になるようにする
インストールした直後だとdifyはOllamaを使うことができません。
まずはOllamaを有効にしておきましょう。
画面右上から設定に移動。

モデルプロバイダーを選択

Ollamaを探してインストールします。

これでOK!
Difyで見積書の読み取をするワークフローをつくる
ワークフローをつくる
では、以下ワークフローをつくります。
- 開始:ここでPDFを選択&アップロード
- テキスト抽出ツール:PDFからテキストを抽出
- LLM:Ollama(生成AI)で情報を整理
- 終了:出力(つまり、このテキストがLaravelに渡る)

ちなみに今回はインポート用のyamlを用意しましたので、利用してください。
請求書や見積書のデータを読みとる.yaml
app:
description: ''
icon: 🤖
icon_background: '#FFEAD5'
mode: workflow
name: 請求書や見積書のデータを読みとる
use_icon_as_answer_icon: false
kind: app
version: 0.1.5
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
allowed_file_upload_methods:
- local_file
- remote_url
enabled: false
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 10
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 3
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
sourceType: llm
targetType: end
id: 1766951498079-source-1766951545392-target
source: '1766951498079'
sourceHandle: source
target: '1766951545392'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: start
targetType: document-extractor
id: 1766951493868-source-1767094515119-target
source: '1766951493868'
sourceHandle: source
target: '1767094515119'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
sourceType: document-extractor
targetType: llm
id: 1767094515119-source-1766951498079-target
source: '1767094515119'
sourceHandle: source
target: '1766951498079'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 開始
type: start
variables:
- allowed_file_extensions: []
allowed_file_types:
- document
allowed_file_upload_methods:
- local_file
label: pdfファイル
max_length: 48
options: []
required: true
type: file
variable: pdf
height: 90
id: '1766951493868'
position:
x: 15.953837400933764
y: 273.24610547328945
positionAbsolute:
x: 15.953837400933764
y: 273.24610547328945
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gemma3n:e4b
provider: ollama
prompt_template:
- id: e1337f7e-8fce-477b-8fcd-0abc4feb41ea
role: system
text: "PDFから抽出したテキストを以下フォーマットで出力してください。\n{\n \"company_name\": \"(会社名)\"\
,\n \"date\": \"(発生日:YYYY/MM/DD)\",\n \"total_amount\": \"(合計金額)\"\
,\n \"type\": \"帳票タイプ(estimate / invoice)\",\n}\n\n【PDFから抽出したテキスト】\n\
{{#1767094515119.text#}}"
selected: false
title: LLM
type: llm
variables: []
vision:
enabled: false
height: 98
id: '1766951498079'
position:
x: 580.8985755742976
y: 273.24610547328945
positionAbsolute:
x: 580.8985755742976
y: 273.24610547328945
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
outputs:
- value_selector:
- '1766951498079'
- text
variable: text
selected: true
title: 終了
type: end
height: 90
id: '1766951545392'
position:
x: 858.6368919941658
y: 273.24610547328945
positionAbsolute:
x: 858.6368919941658
y: 273.24610547328945
selected: true
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
is_array_file: false
selected: false
title: テキスト抽出ツール
type: document-extractor
variable_selector:
- '1766951493868'
height: 92
id: '1767094515119'
position:
x: 301.32432946995584
y: 273.24610547328945
positionAbsolute:
x: 301.32432946995584
y: 273.24610547328945
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: 141.30600128551987
y: 149.45067361343058
zoom: 0.8705505632961227
ちなみに、インポートはページ左上のボタンの中にあります。

APIキーを取得する
まずページ左側メニューから「APIアクセス」へ移動。

ページ右上部分にある「APIキー」をクリックします。

ポップアップが表示されるので「+ 新しいシークレットキーを作成」をクリック。

すると、APIキーが作成されるので、Laravelに登録しておきましょう。

.env
DIFY_API_KEY=app-*******************
そして、APIキーをコンフィグから取得できるようにします。
config/dify.php
<?php
return [
'api-key' => env('DIFY_API_KEY', ''),
];
これで、config('dify.api-key')でAPIキーを取得できるようになりました。
Laravelでdify APIにアクセスできるようにする
では、各Laravelパーツを紹介していきます。
routes/web.php
use App\Http\Controllers\DifyController;
// 省略
Route::prefix('dify')->group(function () {
Route::get('/create', [DifyController::class, 'create'])->name('dify.create');
Route::post('/', [DifyController::class, 'store'])->name('dify.store');
});
app/Http/Controllers/DifyController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Response;
use Illuminate\Support\Facades\Http;
class DifyController extends Controller
{
public function create(Request $request): Response
{
return inertia('Dify/Create', [
'flash' => $request->session()->get('flash'),
]);
}
public function store(Request $request): RedirectResponse
{
// バリデーションは省略しています
$difyUser = 'root';
$file = $request->file('file');
$path = $file->store('dify');
$fullPath = storage_path('app/private/' . $path);
$authToken = config('dify.api-key');
// difyへファイルをアップロードする
$uploadUrl = 'http://localhost/v1/files/upload';
$file_response = Http::withHeaders([
'Authorization' => 'Bearer '. $authToken,
])->attach(
'file',
file_get_contents($fullPath),
$file->getClientOriginalName(),
['Content-Type' => $file->getClientMimeType()]
)->post($uploadUrl, [
'user' => $difyUser,
]);
if (! $file_response->successful()) {
return redirect()->route('dify.create')->with('flash', 'Difyへのアップロードに失敗しました');
}
$file_json = $file_response->json();
$UploadFileId = data_get($file_json, 'id');
// difyのワークフローを実行する
$workflowUrl = 'http://localhost/v1/workflows/run';
$payload = [
'inputs' => [
'pdf' => [
'transfer_method' => 'local_file',
'upload_file_id' => $UploadFileId,
'type' => 'document',
],
],
'response_mode' => 'blocking',
'user' => $difyUser,
];
$workflow_response = Http::withHeaders([
'Authorization' => 'Bearer '. $authToken,
'Content-Type' => 'application/json',
])->post($workflowUrl, $payload);
if (! $workflow_response->successful()) {
return redirect()->route('dify.create')->with('flash', 'ワークフローの実行に失敗しました');
}
$workflow_json = $workflow_response->json('data.outputs.text');
$workflow_json = $this->stripCodeFences($workflow_json);
logger($workflow_json); // 本来はここで何か処理をする
return redirect()->route('dify.create')->with('flash', 'ワークフローの実行に成功しました');
}
private function stripCodeFences($text)
{
$text = preg_replace('/```(?:[a-zA-Z0-9_\-]+\n)?([\s\S]*?)```/', '$1', $text);
$text = str_replace('```', '', $text);
return trim($text);
}
}
resources/js/Pages/Dify/Create.vue
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const props = defineProps<{ flash?: string | null }>();
type UploadForm = {
file: File | null;
};
const form = useForm<UploadForm>({
file: null,
});
const fileInput = ref<HTMLInputElement | null>(null);
const loading = ref(false);
const onFileChange = (e?: Event) => {
const files = (e?.target as HTMLInputElement)?.files;
if (!files || files.length === 0) return;
form.file = files[0];
};
const submit = () => {
if (!form.file) return;
loading.value = true;
form.post(route('dify.store'), {
preserveState: true,
onFinish: () => {
loading.value = false;
form.reset('file');
if (fileInput.value) fileInput.value.value = '';
},
});
};
</script>
<template>
<Head title="Dify Upload" />
<div class="mx-auto mt-8 max-w-xl">
<h1 class="mb-4 text-2xl font-semibold">PDFをDifyへアップロード</h1>
<div v-if="props.flash" class="mb-4 text-green-600">
{{ props.flash }}
</div>
<div class="rounded bg-white p-6 shadow">
<input
ref="fileInput"
type="file"
accept="application/pdf"
@change="onFileChange"
class="mb-4 block w-full"
/>
<div class="flex items-center space-x-2">
<button
class="rounded bg-indigo-600 px-4 py-2 text-white"
:disabled="loading"
@click.prevent="submit"
>
<span v-if="!loading">アップロード</span>
<span v-else class="flex items-center gap-2">
<svg
class="h-4 w-4 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v8z"
></path>
</svg>
アップロード中...
</span>
</button>
</div>
</div>
</div>
</template>
はい!
これで準備は完了です。
テストしてみる
では、実際に「Laravel→dify API」へのアクセスをしてみましょう!
difyとLaravelを起動して、Laravel上で以下のような請求書PDFを選択してみましょう。

選択したところ。

では、アップロードボタンをクリックしてみます。

アップロード中の表示がでて、数秒経つと・・・・・・

はい!
成功したことが表示されました。
では、Laravel内に出力されたログを確認してみましょう。
うまくいくでしょうか・・・・・・
[2026-01-01 20:02:58] local.DEBUG: {
"company_name": "株式会社テスト",
"date": "2025年12月18日",
"total_amount": "67,897",
"type": "estimate"
}
はい!
- 会社名
- 発生日
- 合計金額
- タイプ
の4つが正確に取得できています。
成功です😊✨
※もちろん、途中のプロンプトを変更すると取得したいデータも変更できますよ!
企業様へのご提案
difyは非エンジニアでもワークフローをつくることができます。
そこで、difyを自社で開発し、必要な部分をエンジニアに任せることで、工数を大幅に減らせる場合があります。
※もちろん実装したい内容によります。
そこで、一度difyを使ってみて「これをウェブサイトから実行したい」といったご希望があるようでしたら、お力になれるかと思います。
ぜひお問い合わせからご相談ください。
お待ちしております😊
おわりに
ということで、今回は「dify駆動開発」をやってみました!
感想としては、
- パターンによっては、通常より工数を減らせる
- ローカルLLMとの親和性が高い
- dify APIはウェブサイトとの連携が簡単
って感じでした。
ただし、デメリットとしては以下のように感じました。
- difyで表示されるウェブページ自体のカスタマイズは難しい
- dify + Laravel + phpstormを使っているとパソコンがフリーズするぐらい負荷が大きい
そのため、やりたいことによってdifyを使うかどうかをきちんと判別すべきだいう考えです。
ちなみに、時短につながりそうな機能もご紹介しましょう。
それは、今回もつかった「PDFやWordからテキストを抽出する機能」です。
通常だと、Laravel内でPDFTOTEXTやOCRを実装しないといけませんが、difyには標準搭載されていますので、APIアクセスだけでOKというわけですね。
とにもかくにも、面白いんで、ぜひ皆さんもdifyで何か作ってみてください。
ではでは〜!

「Googleの軽量ローカルLLM、
かしこかった!!」





