Ollamaを使ってみる その2(ローカルLLMでのコードの生成と実行、qwen2.5-coder、Node-RED)
はじめに
今回はローカルLLMでコードを生成して、そのまま実行できるようにNode-REDのフローを作成してみました。
ChatGPTとプロンプトでやり取りして生成したコードを、コピー&ペーストで実行するのがいつもの作業です。これもできれば自動化したいなと思っています。
私が開発したpython-venvノードを利用して、実行結果をフィードバックし、パッケージが足りない場合はインストールするといったことができるのかを試してみました。
▼以前の記事はこちら
qwen2.5-coderを試してみる
ローカルLLMのモデルはqwen2.5-coderを利用しました。Ollamaで利用できるモデルが多すぎて、どれが最適なのかは分かりませんが、コードを扱うことを想定しているようだったので選びました。
▼今回は7bモデルをダウンロードしました。
https://ollama.com/library/qwen2.5-coder:7b
以下のコマンドでインストールして起動しました。
ollama run qwen2.5-coder:7b
Pythonでこんにちはと表示するプログラムを書いてもらいました。
▼日本語で違和感のない回答が返ってきています。応答も早く、順次出力されていました。

▼コードだけ提示するように指定すると、```がついていました。

後でNode-REDで出力を受け取るときは、処理する必要があるかもしれません。
私が普段画像処理で用いている、YOLOのプログラムを作成できるか試してみました。
▼YOLOv5とOpenCVを利用したものが提示されました。

▼実行してみたところ、ModuleNotFoundErrorが起きていました。

普段はultralyticsパッケージを利用していたので、そのパッケージを利用したコードを生成してもらいました。
▼ultralyticsを利用したコードになっています。

提示されたコードは以下です。モデルは既にインストールしていたYOLOv8nを利用して、実行することができました。
import cv2
from ultralytics import YOLO
# モデルの読み込み(ここではYOLOv5sを使用)
model = YOLO('yolov5s.pt')
# カメラの初期化
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
print("カメラから映像が取得できません。")
break
# モデルの予測
results = model(frame)
# 結果を描画
for result in results:
boxes = result.boxes.cpu().numpy()
for box in boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0])
cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), thickness=3)
# フレームを表示
cv2.imshow('Video', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
後でNode-REDで利用することを考えると、JSON形式で出力してもらった方が扱いやすいので聞いてみました。
▼インストールが必要なパッケージ名と、PythonのコードがJSON形式で出力されています。

ここまでの確認から、Node-REDでも出力結果を利用できそうです。
Node-REDで利用する
簡単な命令で動作確認
ローカルLLMとのやり取りには、これまでも利用してきたOllamaノードを利用します。
▼以下の記事でも利用しています。
Pythonの実行には、私が開発したpython-venvノードを利用します。
▼年末に開発の変遷を書きました。
https://qiita.com/background/items/d2e05e8d85427761a609
まずは簡単なフローでOllamaノードの応答を確認してみました。
▼フローはこちら。Helloと表示したいという命令を送っています。

[{"id":"3d7edf383c255911","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"model\": \"qwen2.5-coder\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"{{payload}}\"\n }\n ]\n}","output":"json","x":1820,"y":3260,"wires":[["b2bed944826694b1"]]},{"id":"b2bed944826694b1","type":"ollama-chat","z":"22eb2b8f4786695c","name":"Chat","server":"","model":"","modelType":"str","messages":"","messagesType":"msg","format":"","stream":false,"keepAlive":"","keepAliveType":"str","tools":"","options":"","x":1650,"y":3320,"wires":[["1b0face17407124f"]]},{"id":"1b0face17407124f","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.message.content","tot":"msg"},{"t":"change","p":"payload","pt":"msg","from":"```python","fromt":"re","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"```","fromt":"str","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1820,"y":3320,"wires":[["94af02c6d4aea6ef"]]},{"id":"aa64a21b2f9adfec","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Helloと表示したいです。","payloadType":"str","x":1450,"y":3260,"wires":[["cec439e00a4acf38"]]},{"id":"cec439e00a4acf38","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"あなたはソフトウェアの一部です。\n命令をもとに、Pythonで実行するコードを生成してください。\nあなたの応答をもとにコードを実行するので、コード以外の回答は不要です。\n\n命令:{{payload}}","output":"str","x":1660,"y":3260,"wires":[["3d7edf383c255911"]]},{"id":"94af02c6d4aea6ef","type":"debug","z":"22eb2b8f4786695c","name":"debug 451","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2010,"y":3320,"wires":[]}]
▼一つ目のtemplateノードで役割を指定し、二つ目のtemplateノードでOllamaノードに送信するデータ形式に加工しています。


changeノードで応答結果の不要な部分を取り除いています。
▼応答結果のままだと、やはり```がついていました。

▼changeノードで取り除いています。

▼その結果、改行は残っていますがコードだけ取り出せました。

この出力結果を、venvノードに渡します。venvノードはmsg.codeの値をPythonのコードとして実行することができます。
▼以下のフローを作成しました。

[{"id":"3d7edf383c255911","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"model\": \"qwen2.5-coder\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"{{payload}}\"\n }\n ]\n}","output":"json","x":1820,"y":3260,"wires":[["b2bed944826694b1"]]},{"id":"b2bed944826694b1","type":"ollama-chat","z":"22eb2b8f4786695c","name":"Chat","server":"","model":"","modelType":"str","messages":"","messagesType":"msg","format":"","stream":false,"keepAlive":"","keepAliveType":"str","tools":"","options":"","x":1650,"y":3320,"wires":[["1b0face17407124f"]]},{"id":"1b0face17407124f","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.message.content","tot":"msg"},{"t":"change","p":"payload","pt":"msg","from":"```python","fromt":"re","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"```","fromt":"str","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1820,"y":3320,"wires":[["94af02c6d4aea6ef","1557307e33fbd272"]]},{"id":"aa64a21b2f9adfec","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Helloと表示したいです。","payloadType":"str","x":1450,"y":3260,"wires":[["e5abb95362f98b24"]]},{"id":"94af02c6d4aea6ef","type":"debug","z":"22eb2b8f4786695c","name":"debug 451","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2010,"y":3320,"wires":[]},{"id":"fa4619be43ee2e11","type":"venv","z":"22eb2b8f4786695c","venvconfig":"c99155da59825db2","name":"","code":"","continuous":true,"x":1830,"y":3380,"wires":[["61cb970e3c67a69b"]]},{"id":"1557307e33fbd272","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"code","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1670,"y":3380,"wires":[["fa4619be43ee2e11"]]},{"id":"61cb970e3c67a69b","type":"debug","z":"22eb2b8f4786695c","name":"debug 453","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1990,"y":3380,"wires":[]},{"id":"e5abb95362f98b24","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"あなたはソフトウェアの一部です。\n命令をもとに、Pythonで実行するコードを生成してください。\nあなたの応答をもとにコードを実行するので、コード以外の回答は不要です。\n\n命令:{{payload}}","output":"str","x":1660,"y":3260,"wires":[["3d7edf383c255911"]]},{"id":"c99155da59825db2","type":"venv-config","venvname":"pyenv","version":"default"}]
▼以下の結果が返ってきました。

print("Hello")が問題なく実行されていることが分かります。
パッケージのインストールを含めたフローの作成
先程のフローで、命令文だけ変えてみました。
▼JSON形式で、インストールが必要なパッケージ名とPythonのコードを回答するように指定しています。

▼文字列として回答が返ってきました。

これをJSON形式にパースし、pipのインストールが必要な場合はインストールし、Pythonのコードを実行するようにします。
▼以下のフローを作成しました。

Helloと表示するコードは問題なく実行されていました。
▼以下の結果が返ってきました。問題なく実行されています。

さてここからが難しいのですが、「ultralyticsを利用した、yolov8n.ptによるYOLOのカメラに対するリアルタイム物体検出を行いたいです。」という命令を与えてみました。様々なエラーが起きたので、順番に対処していきました。最終的なフローは最後に示します。
▼pipはultralyticsになっていて、コードも生成されていますが、エラーが起きています。

"""が含まれています。複数行のコードをJSON形式としてパースするには改行文字が必要なようだったので、命令で指定するようにしました。
▼Node-REDのJSONノードでパースすると指定しました。

修正後、もう一度実行してみました。
▼パッケージのインストールが行われました。

▼一行目にjsonという文字が書かれることと、書かれないことがありました。

▼changeノードで行頭のjsonという文字は取り除くようにしました。

生成されたコードでは、OpenCVを利用してウィンドウを表示するようになっていませんでした。そこで、「カメラの映像はOpenCVで確認したいです。」という命令を追加しました。
▼コードの実行と検出まではできたものの、エラーが起きるということもありました。

この後もプロパティの指定などで様々な問題が起きたのですが、実行できるようになりました。
▼検出状態がリアルタイムに表示され、結果が出力されました!

エラーがあるたびに実行する必要があったので、エラーノードにつなげました。これで、実行可能なコードになるまで勝手に実行してくれるようになりました。
▼最終的なフローは以下のようになりました。

[{"id":"4670a91b11fe217a","type":"catch","z":"22eb2b8f4786695c","name":"","scope":["51bf2cfb9a7e738f","7531b18cdd18c0b8","dd6f7612338bcd1f"],"uncaught":false,"x":1570,"y":3180,"wires":[["97ea72a11a11f075"]]},{"id":"97ea72a11a11f075","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"エラーが起きました:{{payload}}\n直前の命令:{{flow.command}}","output":"str","x":1720,"y":3180,"wires":[["3d7edf383c255911"]]},{"id":"3d7edf383c255911","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"model\": \"qwen2.5-coder\",\n \"messages\": [\n {\n \"role\": \"user\",\n \"content\": \"{{payload}}\"\n }\n ]\n}","output":"json","x":1820,"y":3260,"wires":[["b2bed944826694b1"]]},{"id":"b2bed944826694b1","type":"ollama-chat","z":"22eb2b8f4786695c","name":"Chat","server":"","model":"","modelType":"str","messages":"","messagesType":"msg","format":"","stream":false,"keepAlive":"","keepAliveType":"str","tools":"","options":"","x":1650,"y":3320,"wires":[["1b0face17407124f"]]},{"id":"1b0face17407124f","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.message.content","tot":"msg"},{"t":"change","p":"payload","pt":"msg","from":"```python","fromt":"str","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"```","fromt":"str","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"^json","fromt":"re","to":"","tot":"str"},{"t":"change","p":"payload","pt":"msg","from":"\"\"\"","fromt":"str","to":"\"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1820,"y":3320,"wires":[["94af02c6d4aea6ef","7531b18cdd18c0b8"]]},{"id":"7531b18cdd18c0b8","type":"json","z":"22eb2b8f4786695c","name":"LLM:JSON形式に変換","property":"payload","action":"obj","pretty":false,"x":1450,"y":3380,"wires":[["b7a3e4a8bc54bccf","73914d706c4e8af3"]]},{"id":"aa64a21b2f9adfec","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"Helloと表示したいです。","payloadType":"str","x":1450,"y":3260,"wires":[["cec439e00a4acf38"]]},{"id":"cec439e00a4acf38","type":"template","z":"22eb2b8f4786695c","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"あなたはソフトウェアの一部です。\n命令をもとに、Pythonで実行するコードを生成してください。\n\n以下のようなJSON形式で回答してください。\n{\n \"pip\": <スペース区切りでインストールが必要なパッケージ名>,\n \"python\": <Pythonのコード>\n}\n\nPythonのコードは、複数行の場合は必ず改行文字を入れてください。\nあなたの回答をNode-REDのJSONノードでパースします。\nあなたの回答をもとにコードを実行するので、それ以外の回答は不要です。\n命令:{{payload}}","output":"str","x":1660,"y":3260,"wires":[["3d7edf383c255911","b4563966bae22d59"]]},{"id":"b7a3e4a8bc54bccf","type":"debug","z":"22eb2b8f4786695c","name":"debug 450","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1670,"y":3380,"wires":[]},{"id":"94af02c6d4aea6ef","type":"debug","z":"22eb2b8f4786695c","name":"debug 451","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2010,"y":3320,"wires":[]},{"id":"dd6f7612338bcd1f","type":"venv","z":"22eb2b8f4786695c","venvconfig":"c99155da59825db2","name":"LLM:コードの実行","code":"","continuous":true,"x":1980,"y":3480,"wires":[["9cbd84a26d1c55f1"]]},{"id":"9cbd84a26d1c55f1","type":"debug","z":"22eb2b8f4786695c","name":"debug 452","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":2190,"y":3480,"wires":[]},{"id":"e91fc18c6dd51401","type":"switch","z":"22eb2b8f4786695c","name":"pipの有無","property":"payload","propertyType":"msg","rules":[{"t":"nempty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":1600,"y":3440,"wires":[["f133f395ac894a59"],["b15f647449aca0ed"]]},{"id":"f133f395ac894a59","type":"pip","z":"22eb2b8f4786695c","venvconfig":"4657b6fbdbaf6f7e","name":"LLM:パッケージのインストール","arg":"","action":"install","tail":false,"x":1840,"y":3420,"wires":[["b15f647449aca0ed"]]},{"id":"61090aee4620d386","type":"inject","z":"22eb2b8f4786695c","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"ultralyticsを利用した、yolov8n.ptによるYOLOのカメラに対するリアルタイム物体検出を行いたいです。カメラの映像はOpenCVで確認したいです。","payloadType":"str","x":1510,"y":3220,"wires":[["cec439e00a4acf38"]]},{"id":"b4563966bae22d59","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"command","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1850,"y":3220,"wires":[[]]},{"id":"73914d706c4e8af3","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"code","pt":"flow","to":"payload.python","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"payload.pip","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1420,"y":3440,"wires":[["e91fc18c6dd51401"]]},{"id":"b15f647449aca0ed","type":"change","z":"22eb2b8f4786695c","name":"","rules":[{"t":"set","p":"code","pt":"msg","to":"code","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1770,"y":3480,"wires":[["dd6f7612338bcd1f"]]},{"id":"c99155da59825db2","type":"venv-config","venvname":"pyenv","version":"default"},{"id":"4657b6fbdbaf6f7e","type":"venv-config","venvname":"pyenv","version":"default"}]
▼実際に実行したときの映像はこちら。
3回ぐらい撮影していたのですが、一度もエラーが起きずに実行できました。逆にエラーが起きたときのフィードバックを撮影したいところです。
最後に
ローカルLLMによるコードの生成と、実行を直結させることができました。他のノードやこれまで試してきたフローと組み合わせて使おうと思います。
ダッシュボードで命令を入力したり、うまくいったコードを保存して自動化するといったことができそうです。
間違えてファイルを削除してしまうといった事態も起こりかねないので、ある程度環境を隔離しておいた方がいいような気はしています。
▼音声での命令、翻訳、音声合成など、入力と出力のバリエーションが増えつつあります。この辺りも併用できそうです。