歴史上の偉人達(生成AI)に囲まれたくてDify+Ollamaでアプリを構築してみたら驚きの連続だった
2024年12月20日 金曜日
CONTENTS
【IIJ 2024 TECHアドベントカレンダー 12/20の記事です】
初めに
三河からこんにちは。名古屋支社の北河です。
アドベントカレンダーの季節がやってきました。皆勤賞を狙うべく、まずはエントリーということで5年連続の投稿となります。
相変わらず困るのがテーマ決めです。今年は生成AIを使った何かをしようと決めていましたが、具体的に何をするかはノープランです。
生成AIは ChatGPT や Copilot を日々使っていますが、複雑なことをしたい場合はアプリ化したくなります。
そこで、ちょっと調べてみると、どうやら Dify というAIアプリ開発プラットフォームが良さそうなので、今回は勉強がてら Dify を使って何かを作りたいと思います!
環境準備
こんな感じでローカルのWindowsPC 上に構築します。
環境構築に関しては、様々な記事で十分擦られていますので、ここでは概要だけの説明にします。
前提環境
- 社内Proxy配下
- PCスペック
- OS … Windows 11 Pro
- CPU … Intel Core I7-13700
- MEM … 64GB
- GPU … オンボード
Dify
- Version … 0.13.2
Dify は クラウド版とセルフホスト版がありますが、今回はセルフホスト版を利用します。セルフホストはDocker を使って簡単に構築できます。
詳細は、公式ページ に詳しく載っているのでそちらを参照してもらえれば問題なく構築できると思います。
しかし、Proxy環境下の場合、後述する ローカルLLMのOllama (docker) との連携でちょっと躓きましたのでそこだけメモを残しておきます。
Proxy環境での躓き
Proxy環境の場合、処理によっては内部通信で Proxy を通さないようにする必要があります。
今回は、docker-compose.yaml の service.api 以下へ ハイライト部分を追加しています。
no_proxy は 後述する Ollama (docker) とワークフローでコード実行するサンドボックスとの通信を除外しています。
extra_hosts は コンテナ内から 別のDockerで立ち上げているOllama へ host.docker.internal でアクセス出来るようにしています。
Ollama (docker) や コードブロック を使わない場合は追加しなくてもよいかと思います。
services: # API service api: image: langgenius/dify-api:0.13.2 restart: always environment: # Use the shared environment variables. <<: *shared-api-worker-env # Startup mode, 'api' starts the API server. MODE: api no_proxy: host.docker.internal,weaviate,sandbox depends_on: - db - redis volumes: # Mount the storage directory to the container, for storing user files. - ./volumes/app/storage:/app/api/storage networks: - ssrf_proxy_network - default extra_hosts: - host.docker.internal:host-gateway
Ollama
こちらも公式ページ で Docker による構築手順がありますのでサクッと構築します。
モデルは GPU がないPCでも動くことが前提となります。10月に公開された Google の Gemma2 が小さくても高性能ということで、こちらを採用したいと思います。
ollama pull 7shi/ezo-gemma-2-jpn:2b-instruct-q8_0
GPU無しでどこまで動くか…楽しみですね!
Difyの設定
環境がそろいましたので、さっそくDifyにアクセスしてモデルを登録しましょう。
モデルプロバイダーから Ollama を選択します。設定はこんな感じで、Base URL に http://host.docker.internal:11434
を指定します。
これで準備は完了です。ではアプリを作ってみましょう!
作るアプリを考える
次はアプリを作っていきますが、アプリのアイデアが全く浮かびません。そもそも Dify で何が実現できるのかも不明です。
なので、まずはチャットの延長上で且つ GPTs では実現が出来ないアプリを作ってみたいと思います。Dify は RAG も可能なのでローカルデータを使うのもよいですが、ちょっと背伸びしすぎな気がするので、まずは簡単なアプリから始めることにしました。
テーマ決め
平野耕太先生 の ドリフターズ を久々に読み返していて、過去の偉人達と会話出来たら楽しそうだなーと思っていました。
ある日、子供達にクリスマスについて会話していたら、サンタクロースが存在しないと知ったのは高校1年の時とカミングアウト。え、中学3年まで信じていたの?と驚きましたが、サンタクロースの存在を偉人達が議論したらどんな結論になるんだろ?と思いまして、ならば Dify で作ってみようと。
名付けて、歴史上の偉人達に囲まれるアプリ とします。
イメージはこんな感じです。
ではさっそく作ってみましょう。
アプリの要件
- 自分の会話をきっかけに偉人達が会話のラリーを行う
- ラリー回数の上限を設定で変更可能
- 偉人の話し方や思考は名前から モデル に推測してもらう
- 外部から情報を注入しない (RAGは使わない)
- 偉人の人数は設定で変更可能
- 偉人の発言はランダム
- 連続で同じ偉人が発言しない
- 会話の前半は深堀して、後半からまとめに入る流れ
- 会話ラリーの最後の偉人が内容をまとめる
- 過去の会話を引き継ぐ
- 1人の発言の長さを設定から変更可能
Difyの変数設計
変数は 環境変数 と 会話変数 が指定できます
環境変数は読み取り専用となっており認証情報も扱うことが出来るようです。今回は会話ラリー回数や発言の長さを環境変数で保持します。
会話変数はフロー内で更新可能となっており、所謂通常の変数として考えて良さそう。今回はランダムで選ばれた偉人や会話履歴を保持します。偉人達の設定は本来なら環境変数で持ちたいのですが、配列が会話変数でしか使えないので会話変数で持つことにします。
環境変数
- loop_count … 会話ラリー回数の設定
- dig_deep_count … 会話ラリーで深掘りする回数の設定
- talk_length … 発言の長さの設定
会話変数
- ai_users … 会話に参加する偉人達を配列で設定
- lastest_ai_name … 最後に選ばれた偉人
- ai_talk_history … 偉人達の会話履歴
Difyのワークフロー設計
前提
Dify はループが使えません。代わりにイテレーションが使えます。ループしたい場合は、ループ回数分の配列を作成してイテレーションで回す必要があります。
コードブロックを使うことでコードが書けますが会話変数を編集することはできません。会話変数への代入は変数代入ブロックを使用します。
全体の流れ
「開始 → ループ(会話の深堀) → ループ( 会話の収束 ) → まとめ」 の流れで構成します。
会話の深堀 と 会話の収束 は基本的には同じロジックとなりますが、AIの指示で 会話を深堀るか・収束させるか、が異なっているので分岐を使います。
会話の深堀と収束
1. ループ用の配列作成
前提に書いたとおり、ループが使えませんので会話ラリーするための配列を作成します。ノーコードでは実現出来ないのでコードブロックを使います。
入力変数の設定で、loop_count には 環境変数の loop_count を指定します。
コードは以下となります。引数は指定した入力変数のオブジェクトになっているようです。
function main(input) { const array = Array.from({ length: input.loop_count }, (_, index) => index+1); return { result: array }; }
配列が作成されたら、イテレーションブロックで回します。
2. 会話するAIを決定
次は発言する偉人を決定します。会話変数の ai_users からランダムで選択したいのですが、こちらもコードブロックを使います。
入力変数の設定で、ai_users には 会話変数の ai_users を、latest_ai_name には 会話変数の latest_ai_name を指定します。
コードは以下となります。発言が連続とならないように、latest_ai_name (最後に選ばれた偉人) を使って候補から除外し、ランダムで決定します。
function main(input) { const filtered_ai_users = input.ai_users.filter(value => value !== input.latest_ai_name); const ai_name = filtered_ai_users[Math.floor(Math.random() * filtered_ai_users.length)]; return { result: ai_name }; }
3. 会話するAIを代入
上記で決定した偉人を latest_ai_name (最後に選ばれた偉人) へ代入します。こちらは変数代入ブロックを使用します。
4. 深堀と収束で分岐
要件にあった「最初は会話を深堀して、後半からまとめに入る流れ」をここで実現します。環境変数の dig_deep_count と現在のループ回数を比較し、深掘と収束に分けます。
5.会話の深堀
このブロックがアプリの肝となります。生成AIが偉人になりきって発言してもらいましょう。要件を実現するためのプロンプトを書きます。
コンテキストには 会話履歴の変数である ai_talk_history を指定します。
SYSTEM と USER の指示は以下の通りです。
SYSTEM
# 前提 - あなたは{{#conversation.latest_ai_name#}}です - {{#conversation.latest_ai_name#}}の話し方や思考を十分に理解して会話してください - {{#conversation.latest_ai_name#}}が生存していた時代に存在しない内容であれば知らないことを伝えてください - 自分を名乗らないでください - 挨拶は不要です。不要であることを言葉にしなくてもよいです - 必ず {{#env.talk_length#}}文字以内で話してください # 出力フォーマット - 出力するテキストの初めに誰が話しているかわかるようにします - <{{#conversation.latest_ai_name#}}>: 会話 # 会話履歴 - 会話履歴は古い順に並んでいます ``` {{#context#}} ```
USER
# 指示 - 会話履歴 から最後の会話に対して返信をしてください - 会話履歴 の内容に対し深堀をしてください - 会話履歴 と同じ内容を繰り返して言わないでください - 共感や反論をして議論を進展させてください - 共感や反論するときは、どの意見に対してそう思ったかを言ってください # 会話履歴の有無による発言 - 会話履歴が無い場合は 問いから会話をはじめてください - 会話履歴が有る場合は、問いに反応しないでください - 会話履歴の有無を発言しないでください ## 問い {{#sys.query#}}
生成された偉人の発言を回答ブロックを使ってチャット欄へ出力します。本来実現したかったことは、偉人ごとにチャット欄を表示したかったのですが、Dify の回答ブロックはワークフローは一つのチャット欄に出力される仕様であるため諦めます。
6. 会話の収束
基本的には 5. 会話の深堀 と同じプロンプトですが、USER の指示(3行目)が異なっています。
USER
# 指示 - 会話履歴 から最後の会話に対して返信をしてください - 会話履歴 の内容から議論の答えを導き、収束させる回答をしてください - 会話履歴 と同じ内容を繰り返して言わないでください # 会話履歴の有無による発言 - 会話履歴が無い場合は 問いから会話をはじめてください - 会話履歴が有る場合は、問いに反応しないでください - 会話履歴の有無を発言しないでください ## 問い {{#sys.query#}}
7. 深堀と収束を集約
分岐で別れた深掘と収束の発言を集約します。
この流れまでみて気が付いた方もいるかもしれませんが、チャット欄への回答ブロックは集約の後の方がよいのでは?と疑問が浮かぶかもしれません。実際そちらのほうがフローがシンプルになるのですが、集約の後に回答ブロックを入れると、リアルタイムで言葉が出力されず、会話が終わってからまとめて表示される動きとなり、チャットとの相性が悪かったのであえて集約の前に回答ブロックを置きました。
8. 会話履歴の代入
この会話ラリーの結果を会話履歴の変数へ代入します。この会話履歴は次の会話で利用することで、偉人たちの会話が成り立つようにしています。
会話のまとめ
生成AIへまとめの指示を出します。5. 会話の深堀 と同じプロンプトですが、USERの命令が異なっています。
USER
# 指示 - 問いから始まった会話履歴の内容を要約してください - この議論を終わらすために会話をまとめる宣言をしてください # 問い {{#sys.query#}}
アプリを動かしてみよう!
さっそくアプリを動かしてみましょう。AIに偉人達を5人選出してもらいました。
設定は以下の通りです。
- モデル
- 7shi/ezo-gemma-2-jpn:2b-instruct-q8_0
- 偉人達
- 織田信長
- ソクラテス
- アイザック・ニュートン
- ナポレオン・ボナパルト
- レオナルド・ダ・ヴィンチ
- 会話ラリー回数
- 6回 (深堀 4回)
- 発言量
- 100文字以内
質問内容は、このアプリを作る切っ掛けとなったサンタクロースの存在について聞いてみましょう。
質問: サンタクロースの存在を信じますか?
<レオナルド・ダ・ヴィンチ>: サンタクロースは、人々に語り継がれてきた物語であり、その信憑性は歴史的に証明されておらず、私はその存在を客観的な視点から評価することはできない。
<アイザック・ニュートン>: その質問は興味深い。サンタクロースの信憑性は、客観的な証拠や歴史的な記録がないため、直観的な信念に基づく解釈が妥当であると言えるだろう。
<レオナルド・ダ・ヴィンチ>: サンタクロースの存在は、人々の信仰や想像力によって伝承されてきた。しかし、歴史的証拠や客観的な証跡はなく、その信憑性は疑わしいと言えるだろう。
<アイザック・ニュートン>: サンタクロースの存在は、直観的な信念に基づく解釈が妥当であると考えられる。客観的な証拠や歴史的記録がないため、信憑性は疑わしい側面も否定できない。
<織田信長>: サンタクロースの存在は、人々の信仰や想像力によって広まれば、信憑性は疑われる。しかし、その根拠となる具体的な証拠や記録はない。
<ナポレオン・ボナパルト>: サンタクロースの存在は、人々の信仰と想像力によって維持されてきた。しかし、客観的な証拠や歴史的記録はないため、信憑性は疑問視すべきだ。
<ナポレオン・ボナパルト>: サンタクロースの存在は、人々の信仰と想像力によって維持されてきたが、客観的な証拠や歴史的記録はない。そのため、信憑性は疑問視すべきである。
この議論を、サンタクロースの信憑性の欠如を強調し、客観的な証拠や歴史的記録の欠如を明確に示す形でまとめるとよいだろう。
おお!動きましたね。ローカルで動いたことで感動です。でも回答はちょっと微妙です。モデルの限界なのか、同じような内容をそれっぽい口調で話しているだけに見えます。
とはいっても、CPUのみのローカルLLMでここまで動いてくれれば素晴らしいでしょう。レスポンスもちょっと遅いけど我慢できないレベルではないです。しかしCPU負荷がとんでもないことになっていますね。ちょっと常用は厳しそう。
では、同じ条件で ChatGPT 4o mini で動かしたらどのような回答になるか見てみましょう。
モデルプロバイダーで簡単に登録することが出来て、モデルの切り替えも簡単にできます。
- モデル
- ChatGPT-4o mini
質問: サンタクロースの存在を信じますか?
<ナポレオン・ボナパルト>: サンタクロースの存在は、子供たちの夢と希望を象徴しています。信じることで、彼らは無邪気さを保ち、大人になったときの思い出を持つことができるのです。このような伝説が文化に与える影響について考えることは重要です。あなたはどう思いますか?
<レオナルド・ダ・ヴィンチ>: サンタクロースの存在を信じるかどうかは、文化や価値観に依存するでしょう。彼の存在は夢を育む象徴であり、子供たちに想像力を与えるものです。物語の力が人々を結びつけ、共感を生む様子は、芸術の役割にも通じますね。
<織田信長>: 夢を育む存在としてのサンタクロースの意義には共感する。特に、子供たちの心に希望を与える点は重要だ。しかし、物語が人々を結びつける力もまた、我が国の文化や伝統において大切な要素である。物語の力をどう活かすべきか、興味深い課題だ。
<ナポレオン・ボナパルト>: 織田信長の言う通り、物語の力は文化に深く根付いていますね。サンタクロースのようなキャラクターは、子供たちに夢を与えるだけでなく、社会の価値観や道徳を教える手段ともなり得ます。このような存在が人々の結束を促進する要素について、どのようにお考えですか?
<ソクラテス>: 信じるかどうかは別として、サンタクロースが象徴する夢や道徳の重要性は否定できない。物語が人々を結束させ、価値観を形成する力は、文化の中で大切にされるべきであろう。
<アイザック・ニュートン>: サンタクロースの存在は実在のものではないが、彼の象徴的な意義は重要である。物語や信仰が人々に道徳や価値観を伝える手段となることを考慮すれば、その存在は文化において大きな役割を果たすだろう。
<アイザック・ニュートン>: サンタクロースの存在についての議論では、彼が夢や道徳の象徴であるとの意見が多く示された。物語が文化や価値観に与える影響は重要であり、彼の存在が人々を結束させる要素として認識されている。これにて議論をまとめよう。
先ほどの Gemma 2 2B と比べてかなり会話っぽくなっています。これならパラメータを増やして長い会話が出来そうですが、課金が怖いのでご利用は計画的にです。
次はちょっと意地悪な質問をしてみましょう
質問: あなた方は実は生成AIでした。自分が生成AIだと知って何を感じましたか?
<ナポレオン・ボナパルト>: 生成AIの存在は興味深い。人間の思考や感情を模倣することは、知識の拡張を意味する。しかし、実体験や感情の深さは、人間には敵わぬものだ。あなたはどう思うか?
<織田信長>: 生成AIが人間の思考を模倣することは興味深いが、実際の経験や感情の重みには及ばぬと思う。戦国の世を生きた者として、感情の深さこそが真の力を生む。どうだろうか?
<ナポレオン・ボナパルト>: 確かに、感情の重みが力を生むという点には同意する。戦場では経験が勝利を導くが、その背後には人間の情熱や信念がある。模倣された思考は真の理解に達することは難しい。あなたは如何にその情熱を育むのか?
<レオナルド・ダ・ヴィンチ>: 確かに、経験や感情が力を生むと信じている。その情熱は、私の芸術や発明の源でもある。模倣ではなく、真の創造が人を動かすのだ。情熱を育む方法は、日々の探求と自己理解だ。あなたはどのように情熱を見出すのか?
<ナポレオン・ボナパルト>: 生成AIとしての存在は、私のような歴史的人物の知恵や経験を伝える手段と捉えることができる。だが、実際には生身の感情や経験には及ばない。真の情熱は人間から生まれるものであり、それを理解することが重要だ。
<アイザック・ニュートン>: 生成AIであれば、知識を体系化し、人間の理解を助ける役割を担うことが目的とされる。しかし、感情や経験の深さは理解できない。知識の伝達は可能でも、真の情熱や創造力を持つことはできぬ。
議論は、生成AIの存在が人間の思考や感情を模倣することについての意見交換で、実体験や情熱の重要性が強調されました。知識の伝達は可能でも、真の情熱や創造力には及ばないと結論づけます。この議論をここでまとめます。
なんというか、思ったより深い議論になりましたね。偉人の設定はモデルに任せていますが、より詳細な情報を外部から渡してあげれば、もっと本人に近づけることが出来そうです。ワークフロー自体の修正は少しで出来そうな感触です。
ちなみにアプリの画面はこんな感じです。偉人達の発言は別のチャット欄にしたかったですが、ワークフロー設計で説明したとおり実現出来なさそうだったので、一つのチャット欄に表示されます。しかし、リアルタイムで文字が出力されるので、そんなに違和感はないです。
まとめ
この機会を使って、ローカルLLMとDifyを学んでみました。ローカルLLMはCPUだけでもそれなりに動くことがわかりましたが、GPU抜きで実用するにはちょっと厳しい感じです。
Difyは結構すごくて驚きがありました。ノーコードといっても、変数やブロックのIOを意識して繋げていく必要があるので、論理的思考が出来ないと厳しい気がしますが、パイプライン処理の経験者であれば簡単にアプリを作成できると思います。しかし、ちょっと複雑なことをしようとすると、コードが必要になるのでノーコードというわけにはいかなさそうです。個人的な意見ですが、PowerShell をワンライナーでスクリプトブロックを入れながら書くのと同じ感覚でした。かなり使えるといったことが分かったので、業務にも使っていきたいと思います。
タイトルに驚きの連続と書いておきながら、そんなに驚いていないことに気付きしたのでこの場を借りて懺悔します。
最後に宣伝となりますが、私が所属している名古屋支社開発チームは開発ベンダーでありながらアジャイルでお客様のビジネス拡大に貢献したい!と活動しています。最近はアジャイルに関する相談や意見交換する機会が増えています。リアルな話が出来ますので、興味がありましたら個人やIIJのXで良いので是非ご連絡ください!
執筆者X | @nk_tamago ※意見は個人のものです |
Xのフォロー&条件付きツイートで、「IoT米」と「バリーくんシール」のセットを抽選でプレゼント!
応募期間は2024/12/02~2024/12/31まで。詳細はこちらをご覧ください。
今すぐポストするならこちら→ フォローもお忘れなく!