コントリビューション・フェス参加のためGraphAIを学んでたらDifyとの比較になった
@snakajima さんが、AI使ったアイデアソンとGraphAIを使ったプロダクト制作のフェスを開催してた。優秀賞のMac mini に釣られ、初めて他の方のリポジトリにIssueを投稿するくらいのやる気はある。
Life is beautiful コントリビューション・フェス
GraphAIは知らなかったけど、「非同期型のAI Agent」「Difyで作ったワークフローをGraphAIに移植」というキーワードに興味を持ったので見てみる
どこから見るかと悩んだけどZennを調べれば先人の知恵がまとまっててありがたい
AI Agentを簡単に効率よく開発/実行するGraphAIの紹介
元のGithubリポジトリに書いてあった通りだけど、確かに非同期に AI + Agent でデータのやり取りをして、最終的な成果物を出すためのライブラリらしい。
朝の支度の事例が分かりやすかった。
起きたらすぐするタスク
(1) コーヒーのお湯をわかす (4分)
(2) 子供を起こす
3分おきの5回呼ぶと起きる。ただし、3分以内の呼んだり、5分以上間隔が開くと、最初からやり直し
(3) 朝食をつくる (15分)
(4) いつも遅れる新聞をとりにいく
5分以上間隔をあけて4回見に行くと、新聞はきている。ただし5分未満で見に行くと 最初からやりなおし
→ 並行したりフロー化されてたり、依存したり中断したり。Difyのワークフロー書式とは設計思想が全然違う。こっちのほうが自分が考えていたAgentワークフローに近いかも。
私はDifyer
自分は非エンジニア属性なのでコード書いたりは良く分かってない人間だけど、Difyが出たときは「これはすごい!」と思って自分でレンタルサーバー借りて個人で Dify & Firecrawl をセットアップして使い倒してたくらいには、ノードベースのAIツールに未来と魅力を感じた。
でも今はあまり使ってない。
Difyをあまり使わなくなった理由
- 「ノード繋ぐだけ!」→ノードを繋いだだけでは出来ないことのほうが多い
- エラー訂正のワークフローを自前で組む必要がある
- (基本的には)同期型のワークフローを組むことになる
- 結果をリフレクションできない (Iterationで擬似的に組めるが厳密には違う)
「Difyめっちゃいいじゃん!」って思って色々やっていた頃の思い出をキレイなままにしたかったので自分のお気持ちを表明したことが無かったけど、書き出してみるとけっこう文句があったわ。せっかくなのでまとめてみる。
「ノード繋ぐだけ」では何もできない
シンプルなワークフローであれば「LLMの出力」をそのまま無加工で出力すれば終了。でも、ちょっとでも複雑なことをしたいと思ったらプログラミング基礎知識が無いと何もできない。
例えば、RAGの検索結果とWeb検索で入手したデータを元にしてLLMに回答を作らせる場合。
- Web検索で取得したいくつかのページURLをIterationに渡して
- Iterationで Firecrawl → ページデータを取得して
- LLMに渡して要約して
- 結果をRAG用のVDBに書き込んで
- 並行して取得していたRAGからデータを受け取って
- 全部変数ノードにまとめてからコードノート(またはテンプレートノードで)String Textにして
- 調査結果をLLMにコンテキストとして渡して、最終回答を生成させる
→ これを実現するために何回 JSON Parseさせるねん!と、チャットボットフローを作ってる当時はブチ切れてた記憶がある。
何らかの処理に失敗してもPython側のエラーコードしか表示されないので、自分のParse処理が間違ってるのかLLMの出力のせいなのかDifyの仕様の問題なのか切り分けができず、けっきょくPythonコードのほうが早かった…みたいなことが何回もあった。
これ解決策は?
(1) 自分が賢くなる
OR
(2) 「ノードを繋ぐだけで、処理元のOutput → 次のInputの受け取り書式をAgentが自動検知して訂正してくれたらいいのにな〜」
って当時は思っていた記憶がある。
Agentの訂正結果がキャッシュされて2回目以降は自動的に修正されるか、または処理元・先の書式をあらかじめ自動訂正しといてくれるか。→ これが実現されたらノードをポチポチ繋ぐだけで全てのアプリが完成するので、真のノードベースAIエージェントと言えるかもしれない。
そして自分は (1) を選んだ。
ノードとノードの間に「ユニットテストノード」が常に挟まれて、成功すると消失するようなイメージ。
エラー訂正のワークフローを自前で組む必要がある
初期のLLMはJSON書式を固定化させるにも工夫が必要で、それでも1〜2%くらいはすり抜けてエラーになる出力が混じるなんてザラだった。(それでも1年前の話だったけど…)
初期のDifyは IF ELSE も貧弱で、LLM側のエラーに巻き込まれると訂正できずに処理が強制終了したり、ひどいときはシレッとエラーを含めたままチャットが継続して次の会話でエラーを巻き込んでさらに回答が酷くなるなどがザラだった。
※こういう事があるから Langchain から袂を分かつ事になったのかもしれない…
v0.10 を超えた頃にはLLM性能も飛躍的に向上して判定ノードも増えたので、「ここで失敗したらこっちに飛ばして……」という感じで分岐できて、エラーに悩むことは無くなった。
いやしかしですよ、 そもそもLLMのエラー時に「訂正して再開する」が選択肢に無い今の設計にもだいぶ不満ある。
LLMが正しい出力をすることが前提な設計思想はちょっと受け入れづらい。
そういう事もあって、Dify自体は「失敗しても痛くも痒くもないこと」にしか使ってない。社内向けのRAG付きチャットボットとか、自分用のリサーチBotとか、最終結果がおかしくても良いことはOK。
そうなると、だんだん「Google NotebookLM とかでよくね?」っていう感じになってくる。
どうなってたら良かった?
「期待している結果」ではなかったときに元のノードに戻って処理をやり直せるリフレクションノード が存在してたら、だいぶ違っていたかも。
リフレクションノードの話は後でまとめて書く
同期型のワークフローしかないよね〜 →ちょっと緩和された
初期のDifyは同期型しかなかったので、(Iterationも作業が完了するまで何もできなかった) バージョンアップで並列処理 ができるようになってだいぶ緩和した。
でも けっきょく全ての処理が完了するまで次の処理が開始できないので同期型だよね? という印象は拭えない。
自分がやりたかったこと
-
チャットボットで会話を投げるとLLMが処理しつつ並列IterationでWeb検索を実行して、あとで検索結果を教えてくれる (非同期型コミュニケーション) → 検索には時間がかかるので結果が出るまでチャットを継続したかった
-
全ての処理が終わって結果が出たあとに「記録して」と言ったら外部ツールを呼び出して記録中にも続きの会話がすぐにできる (非同期記録) → VDBに結果を記録したり外部ツールの呼び出しAPIの結果待ちに時間がかかる
いずれも「LLMからのフィードバックの速さを優先して、後処理で結果を出したほうが人間の体験がいいよね」という自分自身の経験則で作ってみたかったワークフローだけどDifyはこれが出来ない。これもDifyを toC 向けの顧客サポート向けチャットに組み込みづらかった理由のひとつ。
どうなってたら満足だった?
Difyには外部ワークフローを呼び出す機能があるので、それの結果を待たずに次のチャットが継続できる機能があればよかったかも。
外部ワークフローが処理 → VDBに記録 → チャット中にVDBに結果が増えてることをLLMが検知 → 会話に反映
こういうのもエージェント感ある。
結果をリフレクションできない
「期待している結果」ではなかったときに元のノードに戻って処理をやり直せるリフレクションノード が存在してたら、だいぶ違っていたかも。
Difyの限界を感じて自分でツールを作るようになったのはこれが大半の理由かもしれない。
期待値とは「当初の指示」であり、これを達成できるまでどこまで元の処理を繰り返し実行できるようにするかがエージェント的な挙動なのでは?
どういう風になっていたら満足できそう?
細かい設計とかは想像出来ないけど、リフレクションノードという物を仮に作るとしたら…
- 各ノードにマイルストーンを設置して、その出力を記録しておく
- リフレクションノードはそこに至るまでに生成された各マイルストーンの出力(の概要)を全て受け取れる
- リフレクションによって「ここまでの結果にエラーや問題が存在しないか?」を検証して、NGの可能性がある場合は検証結果をマイルストーンに送信してやり直す / マイルストーンのノードを修正してから再実行する
- 最終結果のリフレクションノードは全ての結果を受け取り、当初の指示を達成できたかどうかを判断する
リフレクションノードの判断基準
└エラー訂正
└人間からの当初の指示
└人間からのノードに直接書き込まれた指示
└ここまでの処理にかかった経過時間
└ここまでに使われた計算資源(Token数なども含める)
└マイルストーンに戻る際の制限 (指示の書き換えを認める・処理の書き換えを認める etc...)
→ この処理自体がものすごく難しいけど、非同期型で自立型のエージェントを作ろうって思ったら「処理を任せる=任せるための終了判断を用意する」という手順があればいいので、これについて深堀りすると良さそう。
処理にかかった時間
/ 計算資源
この2つを判断基準に入れたのは、これが機械と人間の共通言語だと思ったので。時間経過で出来ないことをいつまでもウダウダ続けず、体力というリソース不足で処理を停止するのは大事。
OpenAI o1 でも推論トークンという形式でどれだけリフレクションするかを決めてる
ノードベースで話し合うDifyのことを忘れて GraphAI を見る
ここまで書いたDifyの不満は、概ねGraphAIでは問題にすらなっていないという印象。
Difyと同じYamlで記述されていたので、もっとシンプルなノードベース・ワークフローのようなものかと思っていたけど、全く別物のように見える。
今日はここまで。
というかこれは非エンジニアの自分だと理解してる間に3ヶ月経過してしまいそうで、何か作るスタートラインに立つのは厳しそう。
2025年明けましておめでとうございます。
これを書いたのが19日前だったようなので半月以上経過してるけれど、ドキュメント見たりしてるけど他の仕事を集中していたのであまり進んでいない」という結果。
年末年始はずっとClineを触ってプロダクト開発に勤しんでいた。
Clineについては先人達の叡智にお任せ。
とにかくClineが楽しい!!
Cursorとも全く違う体験で、 Human in the loop
これぞ自分が目指していたAgent像に近い。
要件定義書とTodoを渡してポチってするだけで枠組み全部やってくれるので、あとはポチポチと肉付けと修正をするだけで良くなったので、開発体験がすごく良くなった。
Clineのことを書いてるけど、GraphAI / Difyと関係ある?
Clineの大まかな挙動はこんな感じ。
- タスクループを起動してAgentのコアとして動作する (マイルストーンの設置)
- 複数のツールの集合体 (非同期型ノード)
- 継続と終了の判断能力を持つ (リフレクションノードの概念)
Clineの設計と挙動を知ることが、AI-Agentの基礎学習として効率的だなーと感じている。これは想像だけど、GraphAIでエージェント作るときもマイルストーンを決定するエージェントコアとリフレクションノードが、各タスクを消化する小ノードの集合体を操作するような設計になるのでは。
一方、現状のAI-Agentでは「Token数」という計算資源が足枷になるので、これを解決しないと次に進めない。(200M使えるGemini-2.0-flashでもロングコンテキストになると後半はダレてしまって動かなくなる。初期のデータを破棄するとマイルストーンが消えてゴールが逸れるので、チャットUIのように初期値を消すのはNG)
この場合、駅伝方式で外部記憶をもたせるとマイルストーンまで実行できそう。
Clineで大きなマイルストーンを解決する場合
Human: マイルストーンを設定
AI: タスクとして細分化させる
Human: 細分化したタスクをDocsに記述させる
AI: タスクAを実行→成果をDocsに記述
Human: マイルストーンとDocsで進捗ごと渡す
AI: タスクAの続きからタスクBまで進行→ 成果を更新
...
今はこんな感じだけど、そのうち「Human」のところも置き換えができると良いな〜って思っている。小タスクごとにエージェントが切り替わればToken数という計算資源の問題もある程度は解決できそう。
Replit や v0 でも似たような挙動を感じられるけど、「タスクの細分化と終了判断」においてClineのほうが圧倒的に優秀だと思う。
Replitは指示を出すとタスクを細分化せずに大雑把にゴリッと作ってしまうので、詳細な機能やタスクをいちいち別Agent起動して指示しなきゃいけない。Todo.mdとか仕様設計のファイルを渡してもだいたい無視する。1件ずつ渡す必要があるならCursorでええやん?
v0はエージェントじゃなくてフロントエンド特化型のコード出力ツールなので、エージェントとも言いづらい。フロントエンドに特化するならReplitよりも使いやすい。
年始にClineを使ってたことと、駅伝を見てたおかげで、 「外部記憶」(短期記憶と長期記憶とナレッジ蓄積) の重要性に気がつけたのが良かった。
- LongRAGノード → VDBから長文記憶を時系列で引っ張ってくる機能
- Notionなどの外部ノート連携ノード → ドキュメントまるごと引っ張ってきて更新できる機能
- 記憶を整理するノード → 外部VDBに書き込むためのデータを整理するノード
こんな感じかなぁ。Google Deep Researchみたいに「単語から関連RAGをいくつか引っ張る → RAGデータから関連単語をさらに因数分解してLongRAGで複数のデータを引っ張る」(人間の思い出すスキル)を作って短文に処理してからエージェントコアにデータを渡すための専用ノードを作るのも良さそうな気がする。
RAGは1回実行しただけでは大事な情報を取得出来ないので、下処理する。
- 入力データから類語・関連ワード・英語や中文などの多言語のデータを生成して全部VDB検索する
- VDBに記録するときも類語や関連ワードを入れておく
- VDBから取得したデータから、さらに関連ワードや単語でRAGする
→ 取得したデータを短文整形する
これを同期的にやると時間というリソースをめっちゃ使うことになるので、非同期的に実装できると面白いかもしれない。それが人間でいうところの第6感、または「ひらめき」みたいなものになるかもしれない。
Dify でコメと似たような実装がされた
子チャンクヒット→親チャンクごとロードする。LongRAGみたいな?これなら雑に切り出しても精度が高い回答が作れるので記憶装置としても優秀
ここまで書いたし、いくつかの記事に分割して投稿しとこかな、スクラップだとクローズすると見えなくなる?