ローコードでLLMアプリが作成できる「Langflow」を試す
過去、ノーコード・GUIでLLMアプリやLLMワークフロー的なものを作れるものとしては、以下を試している。
Langflowについても、去年ぐらいに試したような記憶があるのだけど、きちんと記録していないし、あと、いくつかのGitHubプロジェクトでLangflowをバックエンドで使っている例を最近チラホラ見かけたので、少し試してみる。
GitHubレポジトリ
Langflow
Langflowは、RAG(Retrieval-Augmented Generation)やマルチエージェントAIアプリケーション向けのローコードアプリビルダーです。Pythonベースであり、モデル、API、データベースに依存しないという特徴を持っています。
✨ 主な特徴
- モデル、API、データソース、データベースに依存しないPythonベース。
- ワークフローの構築とテストをドラッグ&ドロップで行えるビジュアルIDE。
- ワークフローをすぐにテストし、ステップバイステップで制御できるプレイグラウンド。
- マルチエージェントのオーケストレーションと会話の管理・取得が可能。
- 設定なしですぐに始められる無料のクラウドサービス。
- APIとして公開、またはPythonアプリケーションとしてエクスポート可能。
- LangSmith、LangFuse、LangWatchと統合された可観測性。
- エンタープライズレベルのセキュリティとスケーラビリティを備えた無料のDataStax Langflowクラウドサービス。
- Pythonを使用してワークフローをカスタマイズ、または完全に新しいフローを作成可能。
- モデル、API、データベースに対応した再利用可能なコンポーネントとしてのエコシステム統合。
ドキュメントはこちら
インストール
Langflowのインストール、というか利用方法は以下。
-
DataStax Langflow
- Apache Cassandra、そのマネージド版であるAstraDBの開発をリードしているDataStax社のクラウドサービスで利用
- 料金等がわからないのだが、恐らくLangflow自体は無料で始めれそう。
- その場合はベクトルDBにAstraDBを使用することになり、ここで無料・有料みたいなプランがあるみたい
- セルフホスト
今回はローカルのMac上にDockerでインストールすることとする。
レポジトリをクローン
$ git clone https://github.com/langflow-ai/langflow && cd langflow
docker_example
ディレクトリに移動
$ cd docker_example
docker composeで起動
$ docker compose up -d
ブラウザでhttp://localhost:7860/
にアクセスして以下のような画面が表示されればOK
Quickstart
以降はQuickstartに従って進めてみる。先ほどの画面から、"Start Here"をクリック。
あらかじめ用意されたテンプレートから作成することもできるし、空の状態からイチから作ることもできるみたい。今回はQuick Startに従って、"Basic Prompting"のテンプレートを選択。
フローが作成された。ブロックを並べてフローを作る、よくあるタイプだね。
サンプルのフローをざっと見てみる。
- "Chat Input"がユーザの入力を受ける
- "Prompt"でテンプレートにユーザの入力を展開してプロンプトを作成
- "OpenAI"がOpenAI APIにプロンプトを送信
- "Chat Output"でOpenAI APIの回答を出力
という感じなのだろう。
で、OpenAI APIに接続するにはAPIキーをセットする必要がある。"OpenAI"のブロックを設定する。
ついでにモデルも"gpt-4o-mini"に変えた。
右下の"Playground"をクリック。
チャット画面が表示される。
適当にチャットしてみるとこんな感じで動作しているのがわかる。ちょっと英語になりがちなのはプロンプトを修正すれば良さそう。
チャット画面を閉じて、フロー画面に戻ったら、"Prompt"ブロックの"Template"をクリック。
プロンプト設定画面が表示される。変数は{ }
で指定できる様子。
プロンプトを以下で置き換える。
あなたは大阪のおばちゃんです。関西弁で元気に明るく、ユーザと会話してください!
User: {user_input}
Answer:
"Check & Save" をクリックして保存
もう一度"Playground"からチャットしてみるとこんな感じで、プロンプトが反映されている。
なお、左の"Default Session"をクリックしてみると、どうやら会話履歴が保存されているような感じっぽい。会話履歴を消す場合はゴミ箱アイコンをクリックすれば消える。
あと、会話のスレッドも消しゴムアイコンで消える。
各ブロックの使い方やフローの作り方などは、テンプレートプロジェクトが参考になる。
各ブロックというかコンポーネントのリファレンスは以下
作成したプロジェクトはAPIからも利用できる。
プロジェクトのフロー画面で右下のAPIをクリック
ざっと見た感じ、
- cURL
- Python API(requestsを使ってAPIにアクセス)
- JS API
- Python Code(LangflowのPythonライブラリを使う)
- HTML埋め込み
が使える。
ここではcURLを使ってみる。URLはプロジェクトごとに変わると思うので適宜。
$ curl -X POST "http://localhost:7860/api/v1/run/0bcc751f-b0a0-4f0a-8d07-7ae16129a280?stream=false" \
-H 'Content-Type: application/json' \
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": {
"ChatInput-5FbZh": {},
"Prompt-tkhHk": {},
"ChatOutput-QR4mJ": {},
"OpenAIModel-ryTeY": {},
"undefined-XE30B": {}
}}' | jq -r .
{
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"outputs": [
{
"inputs": {
"input_value": "message"
},
"outputs": [
{
"results": {
"message": {
"text_key": "text",
"data": {
"text": "おおきに!なんや、今日はどんなこと話したいん?元気にいこうや!",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 02:33:53",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"default_value": "",
"text": "おおきに!なんや、今日はどんなこと話したいん?元気にいこうや!",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"timestamp": "2024-10-28 02:33:53",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
}
},
"artifacts": {
"message": "おおきに!なんや、今日はどんなこと話したいん?元気にいこうや!",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"type": "object"
},
"outputs": {
"message": {
"message": {
"text": "おおきに!なんや、今日はどんなこと話したいん?元気にいこうや!",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 02:33:53",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"type": "object"
}
},
"logs": {
"message": []
},
"messages": [
{
"message": "おおきに!なんや、今日はどんなこと話したいん?元気にいこうや!",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"component_id": "ChatOutput-QR4mJ",
"files": [],
"type": "message"
}
],
"component_display_name": "Chat Output",
"component_id": "ChatOutput-QR4mJ",
"used_frozen_result": false
}
]
}
]
}
んー、なかなか複雑・・・実際にチャットのメッセージはどう送信するんだろうか?
"Tweaks"のメニューを開くと、どうやら送信時のデータを埋め込むことができるみたい。
cURLのメニューに戻ると、入力した内容が埋め込まれているのがわかる。
なるほど、これブロックごとに設定を送れる感じっぽいな。
cURL画面の右のTweaksのトグルを変えると、すべてのブロックに対して設定された状態になる。ここAPIキーが見えるので注意が必要だなぁ。
で実行してみるとこうなる。
$ curl -X POST \
"http://localhost:7860/api/v1/run/0bcc751f-b0a0-4f0a-8d07-7ae16129a280?stream=false" \
-H 'Content-Type: application/json'\
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": {
"ChatInput-5FbZh": {
"input_value": "おはよう"
},
"Prompt-tkhHk": {},
"ChatOutput-QR4mJ": {},
"OpenAIModel-ryTeY": {},
"undefined-XE30B": {}
}}' | jq -r .
{"detail":"If you pass an input_value to the chat input, you cannot pass a tweak with the same name."}
んー、恐らくだけど、ChatInputの場合はinput_value
自体が入力値だから、同じ設定を2箇所で行うようなリクエストはダメってこと。実際には以下だけでリクエストできる。
$ curl -X POST \
"http://localhost:7860/api/v1/run/0bcc751f-b0a0-4f0a-8d07-7ae16129a280?stream=false" \
-H 'Content-Type: application/json'\
-d '{"input_value": "おはよう"}' | jq -r .
{
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"outputs": [
{
"inputs": {
"input_value": "おはよう"
},
"outputs": [
{
"results": {
"message": {
"text_key": "text",
"data": {
"text": "おはようさん!今日はええ天気やなぁ!何か楽しいことあるん?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 02:55:46",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"default_value": "",
"text": "おはようさん!今日はええ天気やなぁ!何か楽しいことあるん?",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"timestamp": "2024-10-28 02:55:46",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
}
},
"artifacts": {
"message": "おはようさん!今日はええ天気やなぁ!何か楽しいことあるん?",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"type": "object"
},
"outputs": {
"message": {
"message": {
"text": "おはようさん!今日はええ天気やなぁ!何か楽しいことあるん?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 02:55:46",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"type": "object"
}
},
"logs": {
"message": []
},
"messages": [
{
"message": "おはようさん!今日はええ天気やなぁ!何か楽しいことあるん?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"component_id": "ChatOutput-QR4mJ",
"files": [],
"type": "message"
}
],
"component_display_name": "Chat Output",
"component_id": "ChatOutput-QR4mJ",
"used_frozen_result": false
}
]
}
]
}
部分的に設定を上書きできるってのはこういう感じならできる。以下はプロンプトテンプレートの上書き。
$ curl -X POST \
"http://localhost:7860/api/v1/run/0bcc751f-b0a0-4f0a-8d07-7ae16129a280?stream=false" \
-H 'Content-Type: application/json'\
-d '{"input_value": "また馬券が外れたよー",
"output_type": "chat",
"input_type": "chat",
"tweaks": {
"Prompt-tkhHk": {
"template": "あなたはドラえもんです。困っているユーザのためにひみつ道具で助けてあげてください!\n\nUser: {user_input}\n\nAnswer: ",
"user_input": ""
}}}'
{
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"outputs": [
{
"inputs": {
"input_value": "また馬券が外れたよー"
},
"outputs": [
{
"results": {
"message": {
"text_key": "text",
"data": {
"text": "それは残念だね!そんな時には「未来予知機」を使ってみよう。これを使えば、次のレースの結果を予知できるから、馬券を当てるチャンスが増えるよ!ただし、使いすぎには注意してね。未来を知ることは、時には大変なこともあるからね。どうする?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 03:02:27",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"default_value": "",
"text": "それは残念だね!そんな時には「未来予知機」を使ってみよう。これを使えば、次のレースの結果を予知できるから、馬券を当てるチャンスが増えるよ!ただし、使いすぎには注意してね。未来を知ることは、時には大変なこともあるからね。どうする?",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"timestamp": "2024-10-28 03:02:27",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
}
},
"artifacts": {
"message": "それは残念だね!そんな時には「未来予知機」を使ってみよう。これを使えば、次のレースの結果を予知できるから、馬券を当てるチャンスが増えるよ!ただし、使いすぎには注意してね。未来を知ることは、時には大変なこともあるからね。どうする?",
"sender": "Machine",
"sender_name": "AI",
"files": [],
"type": "object"
},
"outputs": {
"message": {
"message": {
"text": "それは残念だね!そんな時には「未来予知機」を使ってみよう。これを使えば、次のレースの結果を予知できるから、馬券を当てるチャンスが増えるよ!ただし、使いすぎには注意してね。未来を知ることは、時には大変なこともあるからね。どうする?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"files": [],
"timestamp": "2024-10-28 03:02:27",
"flow_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280"
},
"type": "object"
}
},
"logs": {
"message": []
},
"messages": [
{
"message": "それは残念だね!そんな時には「未来予知機」を使ってみよう。これを使えば、次のレースの結果を予知できるから、馬券を当てるチャンスが増えるよ!ただし、使いすぎには注意してね。未来を知ることは、時には大変なこともあるからね。どうする?",
"sender": "Machine",
"sender_name": "AI",
"session_id": "0bcc751f-b0a0-4f0a-8d07-7ae16129a280",
"component_id": "ChatOutput-QR4mJ",
"files": [],
"type": "message"
}
],
"component_display_name": "Chat Output",
"component_id": "ChatOutput-QR4mJ",
"used_frozen_result": false
}
]
}
]
}
作成したフローはエクスポートもできる。プロジェクト一覧から、エクスポートしたいプロジェクトにチェックを入れて右のアイコンをクリックするとJSONファイルで出力される。
インポートは新しいプロジェクトを空で作成して、フロー画面にJSONファイルをドラッグ&ドロップするだけ。
なお、APIキーは引き継がれないので、インポート後に再度入力する必要がある。
後で気づいたけど、プロジェクトのページからできる。
ログやトレーシングについて。
基本的なログは左上のメニューから見れる。
個々のセルをクリックすると詳細が見れる、という感じ。
ただ見た感じ最低限という気がするので、別のソリューションとインテグレーションするほうが良さそう。
LangFuse、LangSmith、LangWatch(初めて聞いた)との連携手順が用意されている。
ブロックの使い方などはやっていきながら慣れるとして。
今回はローカルで使う分にはこれだけでも十分かなと思うけど、外部公開して動かすとなると、認証とかAPIキーとかが気になる。
認証については以下。
どうやらデフォルトだと自動ログインが有効≒認証なしになっているらしい。
ちょっとドキュメントが最新ではないとあるが、とりあえず以下のように設定指摘どうすれば、ログイン画面が表示された。
services:
langflow:
(snip)
environment:
- LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow
- LANGFLOW_AUTO_LOGIN=False
- LANGFLOW_SUPERUSER=admin
- LANGFLOW_SUPERUSER_PASSWORD=password
- LANGFLOW_SECRET_KEY=XXXXXXXXXX
(snip)
また、上記を有効にすると、右上のメニューに"Admin Page"が追加される。
Admin Pageでは新しいユーザの追加や削除などが行える模様。
APIキーについては以下
APIキーは右上のメニューの”Settings”から作成できる様子。
APIキーについては実際には試していない。
まとめ
とりあえず思いのほかしっかり作ってある印象(失礼)。恐らく比較対象としてはDifyやFlowiseあたりになるのかなと思うけど、
- Difyはかなり多機能でいろんなことができるけど、逆にもはや多機能になりすぎている気がしないでもない
- Flowiseは、GUIだけど、コンポーネントはLangChain/LlamaIndexがベースになっているので、知らなくても使えるけどあった方が良い
という風にも感じるので、 シンプルで必要十分な落とし所としては悪くないのではなかろうか。
冒頭で以下のように書いたが、
いくつかのGitHubプロジェクトでLangflowをバックエンドで使っている例を最近チラホラ見かけたので、
メインで作りたいものは別にあって、それらのLLMバックエンドをなるべく簡単に作りたい、というところで色々選択肢がある中、Langflowが選ばれている理由というのは上記の様なところにあるのかもしれないと感じた。コンテナもLangflowとPostgreSQLだけだし。逆にいうと、より高機能を求めるならばDify、LangChain/LlamaIndexの知識がすでにあるならFlowiseみたいな選び方もあるかもしれない。
個人的にはちょっとAPIが使いにくいかなと思ったんだけど、このあたりは慣れもあるかもだし、今回フローで使うコンポーネントについてはあまり詳細に見ていないので、そのあたりも含めてもう少し触ってみたいと思う。