前書き
zennの本(無料)でもご覧いただけます。
- 本内容は主観を含めています。
- 誤った情報、誤記などありましたら、ご連絡いただけると幸いです。
1. 背景
最近はAI技術の急速な発展により、LLM(Large Language Model)を活用したアプリケーションが注目を集めています。しかし、多くのLLMアプリケーションはクラウド環境での利用を強いられることが多く、データプライバシーやセキュリティ、費用面の都合で導入にハードルを高く感じることもあると思います。
ここは開発においても似た感じで、Github Copilotはエンジニアの生産性を大幅にあげてくれるとよく言われていますが、上記の都合などでAIの活用に滞っていることもあると思います。
しかし、最近はプライベートナLocal環境でも簡単にLLMを駆動し活用することを試せる時代になりつつあります。LLMの最適化の発展、オープンソースモデルとAIツールの活発な共有、マシーンの発展の恩恵ですね。
Github Copilotが提供している価値と方向性が同じオープンソースの道具としても、コーディングに特化して学習されたLLM、LLMクライアントとしてIDE上でコーディングをアシストするアプリケーションなどがあります。
ここでは、このようなLocal環境でLLMとAIツールを導入から、具体例にどう使えそうかの試し例を紹介していこうと思います。まだ商用AIサービスに比べて課題はあるもの、クラウドAIツールへのハードルを感じていた方にもLLMの世界を探検できる切っ掛けになると嬉しいです。
2. 概要
1) 記事構成
記事の本内容は以下のセクションで構成されています。
- Local LLM 駆動環境とModelのセットアップ
- Local LLM AIツールのセットアップ (Chat・開発アシスタントツール)
- AIツールで開発への活用に試す
- APIコードの作成
- Document生成
- コードリファクタリング
- Frontend Reactコード作成
2) 環境構成
今回AIツールを試すことにあたっての環境の全体像はこうなります。
Windows環境で試していますが、Mac環境でも似た感じで構築できます。
図の①②③については、次のセクションで紹介します。
3) 今回試しに使うLocal LLM 駆動環境・AIツール
① Ollama
- https://ollama.com/
- LLMをLocal環境で簡単に駆動できるopen-source platformです。
- モデルを簡単にインポート、実行、管理できます。
- ターミナルでの実行と、REST APIでの実行をサポートします。REST APIによりいろんな活用が簡単にできます。
- Metaが公開したLlamaをベースとします。
- 駆動するモデルとしては、以下の3つを利用します。
- codegemma:7b-instruct (メインで使うModel)
- deepseek-coder:1.3b (Tab AutoCompletion用)
- llava:latest (画像認識 > テキスト生成用)
② Open-webui
- https://docs.openwebui.com/
- https://github.com/open-webui/open-webui
- AIチャットツールです。ChatGPTのようなWebUIをLocal環境で簡単に構築できます。
- テキスト、画像、ファイルなどを簡単にインプットできるなどエンドユーザーに便利な機能を提供します。以外でも、画像生成やRAGにも活用できそうです。
- Ollamaをサポートします。
③ Continue
- https://www.continue.dev/
- AI コーティング・アシスタントツールです。
- 色んなAIサービスとの連携をサポートします。
- 連携したAIサービスを経由し、IDEからチャット・コード生成・修正・分析などのコーディングアシストをサポートします。
- Ollamaとの連携をサポートするので、Local LLM AIのコーディング・アシスタントを構築できます。
- VS Code, JetBrains IDEをサポートします。
4) AIツールで開発への活用に試すこと
① APIコードの作成
- 段階的に簡単なDB操作のAPIを作成してみます。
- Go & Ginプロジェクトの作りを試します。
- APIとsqlite3でのDB操作ロジックを作成を試します。
- sqlite3の部分をGormに差し替えを試します。
- 上記の一連の機能に対して、要件をまとめ一つの質問で一気に作成することを試します。
② ドキュメント生成
- AIで作成したAPIコードからドキュメント生成を試します。
- README, Swagger, DBML, Mermaid Diagramを作成してみます。
③ リファクタリング
- コード分離、重複コード改善を試します。
- Adapter・Factory Patternへのコード構造の改善を試します。
④フロントコードの作成
- ウェブデザイン画像から指示文を出すことを試します。
- 上記の指示からReactコードの作成を試します。
- StyleをTailwindCSSに差し替えを試します。
- 簡単なリファクタリングの例として、1つのコンポーネントを適切に複数のコンポーネントに分けることを試します。
- ドキュメント作成の例として、コンポーネントごとにStorybookコード作成を試します。
3. Local LLM 駆動環境とModelのセットアップ
Windows WSL2 & Ubuntu環境で今回は説明します。
Mac環境もセットアップでやることはほぼ同じなので、Macユーザーの方が見ても差し支えはないと思います。
1) ollamaのインストール
Ubuntuの場合は以下のOllama on Linuxを参照して導入します。
https://github.com/ollama/ollama/blob/main/docs/linux.md
※Macや他の環境の場合は、以下の公式ホームからインストール手順を参考できます。
公式ホーム:https://ollama.com/
Github:https://github.com/ollama/ollama
インストール手順説明 (Windows & WSL2 & Ubuntu)
基本以下のコマンドを実行すると問題なくインストールされます。
$ curl -fsSL https://ollama.com/install.sh | sh
正常にインストールされると以下のようなメッセージが出ます。
>>> Enabling and starting ollama service...
Created symlink /etc/systemd/system/default.target.wants/ollama.service → /etc/systemd/system/ollama.service.
>>> NVIDIA GPU installed.
Created symlink
インストールがのメッセージからわかる情報で、Linuxインストールの場合は基本auto startとなります。auto startを望まない場合は、以下のコマンドを実施します。
$ sudo systemctl stop ollama
$ sudo systemctl disable ollama
Removed /etc/systemd/system/default.target.wants/ollama.service.
NVIDIA GPU installed.
NVIDIAやAMD GPUがある場合はGPUを演算に活用できます。
NVIDIA GPUの場合、基本NVIDIA GPUをWSL2 Ubuntuで自動認識するされるので、Ollamaインストールの結果でのGPU利用可能が確認できます。
正常にセットアップされると、以下のコマンドでollamaを実行できます。
# Ollama サーバー起動 (手動スタート時)
$ ollama serve
...実行ログ
# モデルリスト出力
$ ollama list
NAME ID SIZE MODIFIED
...モデルがある場合はモデルが表示
2) Ollama公式が提供するModelを導入
Ollamaの利用においては、目的に応じて、「Model」と呼ばれるものを直接入れて利用する必要があります。Modelを入れる方法はいくつかあります。まず一番簡単な方法として、Ollama公式が提供するModelを導入します。
ollamaの公式ホームから基本提供しているModelを探して入れることができます。
ここでは、GoogleがOpenSourceとして公開したModelのGemmaをベースに、コーディングに特化し学習されたモデルである、 codegemma:7b-instruct
を入れてみます。
補足: Tagの7b, instructの意味について
7bはLarge Language Model (LLM)が保有するパラメータ数を意味します。
LLMはパラメータ数がBillion単位の自然言語処理モデルを意味しており、7bについたbがその単位を意味しています。
他のモデルは13b,70bなどより高いパラメータ数も提供しています。パラメータが高いほどもっと複雑な推論が可能とされてますが、高いPC仕様が必要となってくるとか、アンダーフィット・オーバーフィットを考慮して選ぶ必要もあるらしく、自分の環境と目的に適しているモデルを選択すると良いですね。
一般的に回答精度と速度のバランスが良い7bが無難とよく言われます。
より速度優先の環境では2bのモデルもよく採用されます。
insturctは独自で自然語をもっと特化したモデルと言われてます。ここはまだ明確な違いを認識してないですが、CodeGemmaの場合instructがもっと自然な印象を受けたので利用しています。
筆者もAI分野については初心者なので、興味のある方は機械学習とモデル開発について学習してみるのもいいと思います。
※またCodeGemma以外のモデルについては「4) コーディング特化Modelの種類と選定について」パートを参照ください。
横に見えるrun
コマンドを実行すると、モデルのpullとrunを同時に行います。
モデルをpull
のみしたい場合は、run
の代わりにpull
を実行します。
pull
したあと、run
を実行すると以下のように結果が確認できるとOKです。
pull & run 実行結果確認
$ ollama pull codegemma:7b-instruct
...
success
$ ollama list
NAME ID SIZE MODIFIED
codellama:latest 8fdf8f752f6e 3.8 GB About a minute ago
$ ollama run codegemma:7b-instruct
>>> hello. who are you?
I am a large language model, trained by Google.
>>> こんにちは。あなたは誰ですか?
私はGoogleによってトレーニングされた大きな言語モデルです。
>>>
Use Ctrl + d or /bye to exit.
動作確認として、英語と日本語でフィボナッチ数列を書いてと頼むと結果を得られます。
質問例:`make function that calculates fibonacci on TypeScript`
>>> make function that calculates fibonacci on TypeScript
function fibonacci(n: number): number {
if (n <= 1) {
return n;
}
let a = 0;
let b = 1;
let result = 0;
for (let i = 2; i <= n; i++) {
result = a + b;
a = b;
b = result;
}
return result;
}
Usage:
console.log(fibonacci(6)); // Output: 8
console.log(fibonacci(10)); // Output: 55
Explanation:
- The function takes a single argument,
n
, which represents the number
of terms in the Fibonacci sequence to calculate. - It handles the base cases where
n
is less than or equal to 1, in which
case the function returnsn
. - It initializes three variables:
a
,b
, andresult
.a
andb
represent the first two terms of the Fibonacci sequence, whileresult
will store the current term. - The function enters a loop that iterates from 2 to
n
. In each
iteration, it calculates the next term in the sequence by addinga
and
b
. - It then updates the values of
a
andb
to the next two terms in the
sequence. - After the loop completes, the function returns
result
, which
represents the nth term in the Fibonacci sequence.
Time Complexity: O(n), where n is the number of terms in the Fibonacci
sequence.
Space Complexity: O(1), as the function uses only a constant amount of
additional space.
質問例:`TypeScriptでフィボナッチ数を計算する関数をかいて`
>>> TypeScriptでフィボナッチ数を計算する関数をかいて
```typescript
function fibonacci(n: number): number {
if (n <= 1) {
return n;
}
let a = 0;
let b = 1;
let result = 0;
for (let i = 2; i <= n; i++) {
result = a + b;
a = b;
b = result;
}
return result;
}
使用方法:
console.log(fibonacci(6)); // 出力: 8
console.log(fibonacci(10)); // 出力: 55
説明:
- この関数は、フィボナッチ数列の第n項を計算します。
- 基底ケースとして、nが1以下である場合、関数はnを返します。
-
a
、b
、result
という3つの変数を使用します。a
とb
はフィボナッチ数列の最 初の2項を表し、result
は現在の項を表しa
、b
、result
という3つの変数を使用します。a
とb
はフィボナッチ数列の最初の2項を表し、result
は現在の項を表します 。 - ループは2からnまで実行し、次の項を計算します。これは
a
とb
の合計です。 -
a
とb
の値を次の2項に更新します。 - ループが完了すると、関数は
result
を返します。これはフィボナッチ数列の第n項です。
時間複利: O(n),ここでnはフィボナッチ数列の項数です。
空間複利: O(1),関数は追加の espaçoを使用しないためです。
3) カスタムModelの手動導入
ここではCodeGemmaを利用しますが、手動導入の方法を紹介するという目的として、一つModelを紹介します。
OllamaはOpenSource LLM駆動プラットフォームとして、Ollama公式以外で公開されている任意のModelやユーザーで学習がチューニングさせたModelを駆動することも可能です。
例えばCodeLlama
を、日本語で使えるように学習させたELYZA-japanese-CodeLlama
などもあります。zennに記事としても公開されています。
https://zenn.dev/elyza/articles/fcbf103e0a05b1
①Hugging FaceからModelを入手
ELYZA-japanese-CodeLlama
の公式Hugging Faceは以下となりsafetensors形式のモデルを入手できます。この記事の手順として、Modelのダウンロードは git clone
で行います。
https://huggingface.co/elyza
補足: HuggingFaceとは
機会学習と自然言語処理(NLP)を専門とする会社として、そのためのOpenSource Libraryとツールの開発に多くの貢献をしている会社です。PyTorch基盤のTransforers ライブラリを始め色んなLibraryを開発していると言われてます。
また、全世界のユーザーがNLP ModelとDataSetを共有できるHugging Face Hubというオンラインプラットフォームを運用しており、簡単にNLP Modelを共有し合うことが可能になりました。
簡単に話すと機会学習とAIにおいてGithubのようなインパクトをもたらしたプラットフォームのイメージといえます。
②safetensorsをGGUF形式に変換
Modelのファイル形式の種類としても色々ありますが、llama基盤のollamaは GGUF
形式ファイルのみサポートします。
ただ、Hugging Faceなどで共有されるModelのファイル形式はsafetensors
が一番多いと言われています。
幸い、Ollamaでは safetensors
をGGUF
に変換する方法のガイドを提供しているので、まずは変換を行います。
補足:`safetensors`,`GGUF` ,その他Modelファイル形式について
safetensors
は、Hugging Faceが提供するSafetensorsにより作成されたモデルです。
https://github.com/huggingface/safetensors
GGUFはllama.cppの @ggerganovという方により開発された、早い実行に最適化されたBinary形式のModelフォーマットです。
https://huggingface.co/docs/hub/gguf
https://github.com/ggerganov/ggml/blob/master/docs/gguf.md
また、上記以外のModel形式も色々あり、safetensorsの「Yet another format ?」のセクションが参考できます。
https://github.com/huggingface/safetensors?tab=readme-ov-file#yet-another-format-
変換手順については、以下の2つが参考になります。
Ollamaドキュメント - importing pytorch--safetensors
@kota33(kota)さんのQiita記事のelyza-japanese-codellama導入方法紹介
手順説明はこちらになります。
変換手順説明
まず、safetensors
をGGUF
に変換するためにllama.cppをインストールする必要があります。
$ git clone https://github.com/ggerganov/llama.cpp
$ cd llama.cpp
$ make
HuggingFaceで共有されているModelは、gitを利用してremote repositoryからcloneすることが可能ですが、Modelはファイルサイズが大きいのでgit-lfsをインストールする必要がありそうです。
$ sudo apt-get install git-lfs
$ git lfs install
あと、repositoryをcloneして、以下の手順で変換を行います。
$ git clone https://huggingface.co/elyza/ELYZA-japanese-CodeLlama-7b-instruct
$ python3 convert.py --outfile models/elyza-japanese-codellama-7b-instruct.gguf models/ELYZA-japanese-CodeLlama-7b-instruct
$ ./quantize models/elyza-japanese-codellama-7b-instruct.gguf models/elyza-japanese-codellama-7b-instruct.Q4_K_M.gguf Q4_K_M
「Q4_K_M」の意味補足
どういう量子化(Quantization)をしたかの表現のようです。
Hugging FaceのGGUF説明のQuantization Typesを参考できます。
https://huggingface.co/docs/hub/gguf#quantization-types
③GGUFファイルをollamaのモデルとして追加
以下のimport from ggufの手順をみて追加できます。
https://github.com/ollama/ollama?tab=readme-ov-file#import-from-gguf
手順説明 & 動作確認
まず、Modelfileを作成します。
Modelfileで他のオプションも指定できるようで、詳細は以下のドキュメントで参照できます。
ここではFROMのみ指定で次に進みます。
# vim ./Modelfile (new file)
FROM ./elyza-japanese-codellama-7b-instruct.Q4_K_M.gguf
あと、ollama create コマンドで追加します。successが確認できたらOKです。
ollama listでモデルが追加されたか確認します。
ollama run でターミナル上で質問をすると回答をもらえます。
# ollama create [任意の名前] -f [Modelfile名前]
$ ollama create jp-codellama-7b-inst -f Modelfile
transferring model data
...
success
$ ollama list
NAME ID SIZE MODIFIED
codellama:latest 8fdf8f752f6e 3.8 GB About a minute ago
jp-codellama-7b-inst:latest 7dfc73e89e99 4.1 GB About a minute ago
$ ollama run jp-codellama-7b-inst
>>> TypeScriptでフィボナッチ数を計算する関数をかいて
みました。
## フィボナッチ数とは?
フィボナッチ数とは、前の2つの数の和である数のことです。
例えば、1, 1, 2, 3, 5, 8, 13…のようになります。
## コード
```typescript
function fibonacci(n: number):number {
if (n === 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
...省略
>>>
Use Ctrl + d or /bye to exit.
4) コーディング特化Modelの種類と選定について
今回は、感覚的に日本語との相性がいいものを優先してcodegemma:7b-instruct
を使っています。
しかしながら、コーディング特化モデルはいろいろあり、性能面だけでみるとCodeGemmaより優秀だと言われているModelも多くあるようです。
例えば以下のようなモデルがあります。
また、Modelのパラメータ数によっても性能の違いはあります。
筆者も、まだ評価基準や味方については詳しくない状態ですが、以下の「EvalPlus Leaderboard」に、100個以上のコーディング特化Modelの種類と評価がまとまっているので、参考すると良いと思います。
https://evalplus.github.io/leaderboard.html
また、CodeGemmaとDeepSeekに関する分析は以下の記事が参考になります。
https://didyouknowbg8.wordpress.com/2024/04/10/codegemma-open-source-code-models-by-google/#core-concepts-establishing-a-common-ground
筆者もこの記事をほぼ書き終えたあとでこのサイトの存在をしったので、このあと他のモデルも色々試して行こうと思っています。
4. Local LLM AIツールのセットアップ (Chat・開発アシスタントツール)
1) open-webuiのセットアップ
ChatGPTのようにWeb UIからLLMの活用が可能なツールとして、open-webuiをセットアップします。
セットアップ手順などは以下のURLで参照できます。
https://github.com/open-webui/open-webui
セットアップ & 動作確認
dockerで実行する場合は、以下のコマンドを参考して簡単に利用可能になります。
# Makefile
run:
docker run -d -p 3100:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
rm:
docker rm -f open-webui
正常にDockerコンテナーが実行されたら http://localhost:3100
にアクセスします。
最初の起動はサインアップを行うことで、アドミンとしてアカウントが生成され、ログイン状態になります。
ログイン状態で、上のModel選択からModelを選択して質問をすることができます。
基本的な使い方はすごくシンプルです。以下のように質問を投げると回答してくれます。
また、画像やファイルを添付して質問を行うことも可能です。
(もちろん画像を認識できるModelじゃないと正しい答えをもらえない可能性があります)
基本的な利用方法はシンプルですが、WebUI LLM Chatシステム構築のための豊富な機能を色々提供しているので、興味のある方は、Settingsの設定項目のAdvance Parametersや、公式ホームのチュートリアルを参考すると良いと思います。
2) VS CodeにContinueをセットアップ
Github CopilotのようにIDEからLLMを活用し、コード生成や編集、分析が可能なツールとして、Continueをセットアップします。
公式ホームはこちらです。インストールや利用方法に関する情報を確認できます。
インストール&動作確認 (VS Code)
VS Code拡張はこちらで入手することができます。
https://marketplace.visualstudio.com/items?itemName=Continue.continue
拡張のインストールが完了したら、VS Codeの左のバーにContinueのアイコンが追加されます。
このアイコンをクリックするとContinueのパンネルが表示されます。
下の+を押してモデル追加に進めることができます。
Autodetectを選択するとollamaセットアップで入れていたモデルが自動で選択可能になります。また、Continueのパン寝るからChat機能ができるようになります。
3) JetBrains IDEにContinueをセットアップ
以下のPluginをインストールすると導入できます。
https://plugins.jetbrains.com/plugin/22707-continue
ただ、VS Code拡張に比べてサポートしていない機能はいくつかあります。
4) Continueの使い方の紹介
Continueの公式ホームのHow to use Continueを参考。わかりやすく解説されている。
https://docs.continue.dev/how-to-use-continue
ここでも、いくつか機能を実際に使いながら紹介します。
①Chat機能
Continueのアイコンをクリックし、利用するモデルが選択されていると、チャット機能を利用することができます。以前の会話から引き続き質問することも可能です。
例 :フィボナッチ数列を質問したあと、引き続き改善を問う
フィボナッチ数列を教えてと質問をし、回答としてコードと一緒に注意点を教えてくれました。
注意点でスタックオーバーフローに対して改善してほしいという質問に対して、改善したコードの例を教えてくれてます。
また、Continueパンネルの右上のView History
アイコンを押すとチャットした履歴を見れます。
②コードを選択しChatへ質問※Ctrl(CMD) + L
IDEでコードを選択後、Ctrl(CMD) + L
を押すと、コードがチャットに貼り付けられます。
その後、質問をすることができます。
複数のファイル、コードブロックから貼り付けて一つの質問にできるのが良いポイントです。
例:2つのコード選択範囲をインプットとしその関係を問う
index.tsxとApp.tsx各々でCtrl(CMD) + L
でコードをインプットして、質問を書いています。
両方を認識したうえでAIが回答してくれます。
また、コードを選択しChatへ質問する場合は、基本New Sessionになりますが、下の ← Last Session
を押すと、以前の会話で引き続き質問することができます。
例:コード選択情報をNew SessionからLast Sessionに移動
コードを選択しCtrl(CMD) + L
を押します。その後、赤枠の← Last Session
を押します。
前の会話履歴の続きに選択したコードの情報がわたり、引き続き質問ができます。
③IDE上でコードを自動生成・編集※Ctrl(CMD) + I
IDE上でCtrl(CMD) + I
を押して指示をするとAIがIDEに直接コードを書き込んでくれます。
AIが書き込んだ差分をみて、入れたい差分だけを選択して適用することができます。
書き込みたいところにCursorをおいて指示をするとコードが書き込まれます。
コードを選択したあと指示をすると、指示によって選択したコードを参照し生成するか、または修正するなどができます。
④Tabでコード自動完成
コード自動完成機能をEnableさせると、コードを書く途中で、次に書くコードをおすすめしてくれます。
設定で調整が可能な部分ではありますが、頻繁にLLMとの問い合わせが発生する機能なので、以下のような軽量Modelを使うことがおすすめで。
- deepseek-coder:1.3b
- codegemma:2b
Continueパンネルの右下にある設定ボタンを押すとContinueのconfig.jsonが開かれます。
jsonに以下のような設定を入れると、Tabでコード自動完成機能が活性化されます。
設定コード例
...
"tabAutocompleteModel": {
"title": "deepseek-coder:1.3b",
"provider": "ollama",
"model": "deepseek-coder:1.3b", //ollama listで表示されるモデルを指定
"apiBase": "http://localhost:11434" //ollamaのREST API ポイントを指定
},
...
正常に設定されると、VS Codeの右の下に Continueの部分に✓表示がつきます。
✓を押すと一時的にOFFにすることもできます。
ONの状態だと、コードを書く途中でAIから次に各コードをおすすめしてくれます。
Tabを押すと全体内容を適用します。
Ctrl(CMD) & →
をすると1単語づつ適用します。
まだBeta機能らしく、他の提案を出す機能はまだなさそうです。
⑤@
[ContextProvider指定]
どこかの公式ドキュメントや、ファイル、ターミナル内容などをインプットとして質問することができます。
例えば、
@Terminal
を指定すると、ターミナルで出力されたエラー内容をインプットとし、どういうエラーか聞くことができます。
@Docs
は、事前に登録されている公開ドキュメントをベースに質問をすることができます。
例:React公式ドキュメントをコンテキストとし質問
Docsを選択します。
事前に登録されたドキュメントから質問のネタにしたいドキュメントを選択し質問します。
公式ドキュメントの内容を下に回答してくれます。
機能の詳細は、ContinueのContext Providersドキュメントを参照できます。
https://docs.continue.dev/customization/context-providers
⑥/
[コマンド指定]
チャットから特定機能を実行できるショートカットを提供します。
例えば、
/edit
や/comment
はコード修正の質問の回答をIDEに直接渡しDiff結果を表示します。Diff確認後適用することができます。
機能の詳細は、ContinueのSlash Commandsドキュメントを参照できます。
https://docs.continue.dev/customization/slash-commands
5. AIツールで開発への活用に試す (APIコードの作成)
これでLocal環境だけで駆動可能なAIとコーディングをともにする準備ができました。
ここからは、筆者の主観で開発で活用したい部分を試して行きます。
コードや自然語有無、マーシンの条件などで回答品質がブレる可能性もあると思うので、あくまで参考レベルでご覧いただけると幸いです。
(特に実務でよく出会うコードでどこまで通用するかは、環境によって温度感がかなり違うこともあるかもしれません)
1) GolangのAPIコードを作成
①webui) How to Startコードを生成
まずopen-webuiからGolang & Ginのアプリケーションの作り方を問いました。
AIへ質問:GoとGinを使ってアプリケーションを作りたい
■ AIからの回答
必要なライブラリ
import (
"github.com/gin-gonic/gin"
)
アプリケーションの構造
// main.go
func main() {
// アプリケーションの起動
router := gin.Default()
// ルートの定義
router.GET("/", helloHandler)
// アプリケーションの起動
router.Run(":8080")
}
// helloHandler は、GET "/" リクエストに対するハンドラです。
func helloHandler(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, Gin!",
})
}
説明
- アプリケーションの起動には、
gin.Default()
関数を使用して Gin のデフォルトルーターを作成します。 -
router.GET("/", helloHandler)
のように、GET
リクエストで "/" ルートにhelloHandler
を割り当ます。 -
helloHandler
は、gin.Context
を引数として受け取り、JSON レスポンスを返します。
実行
このコードを実行すると、次のように動作します。
$ go run main.go
[GIN] info start server on 0.0.0.0:8080
ブラウザで http://localhost:8080/
にアクセスすると、次の JSON レスポンスが返ります。
{
"message": "Hello, Gin!"
}
その他の機能
Gin はさまざまな機能を提供します、たとえば:
- URL パラメータの解析
- フォームデータの解析
- セキュリティ機能
- API の構築
参考
親切に色々教えてくれますね。ただ、回して見たらエラーが出たので、エラーをそのまま貼って質問しました。
AIへ質問:main.go:1:1:expected 'package', found 'import'
■ AIからの回答
コードは正しいですが、最初の行で問題があります。main.go
ファイルの最初の行で、package main
を宣言する必要があります。
package main
import (
"github.com/gin-gonic/gin"
)
package main 宣言がなかったですね。正しい回答をくれました。
でも、また違うエラーに出会ったので、エラー内容のまま質問しました。
AIへ質問:main.go:4:5:no required module provides package github.com/gin-gonic/gin: go.mod file not found in current directory or any parent directory; see 'go help modules'
■ AIからの回答
コードは正しいですが、最初の行で問題があります。main.go
ファイルの最初の行で、package main
を宣言する必要があります。
package main
import (
"github.com/gin-gonic/gin"
)
また、エラーメッセージは、Go モジュール関連の問題です。Go モジュールを有効化していない可能性があります。以下の手順で Go モジュールを有効化してください。
- 現在のディレクトリで
go mod init myapp
コマンドを実行します。 - 必要なライブラリを
go get
コマンドでインストールします。
この手順を実行すると、エラーが発生する可能性はありません。
必要なライブラリがインストールされてないと、正しい回答を教えてくました。
AIの指示通りにして、正常実行になりました。
Go実行結果
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> main.helloHandler (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
$ curl http://localhost:8080/ | jq
{
"message": "Hello, Gin!"
}
プロジェクトセットアップからの質問とコード生成は、最初から正常動作する回答は出ない場合が多い印象でした。ただ、初期セットアップ段階で出るようなエラーに対しては、対象方法は明確なケースも多く、十分に活用できる印象でした。
②webui ) /users/:id
API追加
これからは、簡単なAPIの追加を試します。
まずはシンプルにAPIを追加したいと質問してみました。
追加の仕方の例や、動作確認まで教えてもらい、正常動作も確認できました。
AIへ質問:`/users/:id`を追加したい
■ AIからの回答
次のコードを helloHandler
関数に追加します。
router.GET("/users/:id", getUserHandler)
// getUserHandler は、GET "/users/:id" リクエストに対するハンドラです。
func getUserHandler(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{
"id": id,
})
}
このコードは、users/:id
ルートに getUserHandler
を割り当ます。getUserHandler
は、id
パラメータを取得し、JSON レスポンスで返します。
実行
このコードを実行すると、次のように動作します。
$ go run main.go
[GIN] info start server on 0.0.0.0:8080
ブラウザで http://localhost:8080/users/123
にアクセスすると、次の JSON レスポンスが返ります。
{
"id": "123"
}
Go実行結果
$ curl http://localhost:8080/users/999 | jq
{
"user_id": "999"
}
router指定ロジックとHandlerを別のファイルで構成したかったので、その希望を質問してみました。
AIへ質問:Handler関数を/handlersに個々のファイルで置きたい。package宣言もいれて。
■ AIからの回答
以下のコードは、Handler関数を個々のファイルで置きたい場合の例です。
handlers/users.go
package handlers
import (
"github.com/gin-gonic/gin"
)
// getUserHandler は、GET "/users/:id" リクエストに対するハンドラです。
func GetUserHandler(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{
"id": id,
})
}
handlers/products.go
package handlers
import (
"github.com/gin-gonic/gin"
)
// getProductHandler は、GET "/products/:id" リクエストに対するハンドラです。
func GetProductHandler(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{
"id": id,
})
}
main.go
import (
"github.com/gin-gonic/gin"
"myapp/handlers"
)
func main() {
router := gin.Default()
// ルートの定義
router.GET("/users/:id", handlers.GetUserHandler)
router.GET("/products/:id", handlers.GetProductHandler)
router.Run(":8080")
}
このコードでは、users.go
ファイルとproducts.go
ファイルでHandler関数を定義しています。main.go
ファイルでは、これらのHandler関数をルーティングしています。
このコードを実行すると、次のように動作します。
$ go run main.go
[GIN] info start server on 0.0.0.0:8080
ブラウザで http://localhost:8080/users/123
および http://localhost:8080/products/456
にアクセスすると、それぞれのHandler関数のJSON レスポンスが返ります。
main.goとhandler/user.goのファイルで分けるようにパスとファイル名と、各々のコードも教えてくれました。
また、正常動作を確認できました。
Go実行結果
go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> myapp/handlers.HelloHandler (3 handlers)
[GIN-debug] GET /users/:id --> myapp/handlers.GetUserHandler (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2024/05/18 - 14:41:20 | 200 | 32.391µs | ::1 | GET "/users/999"
curl http://localhost:8080/users/999 | jq
{
"id": "999"
}
簡単な要件のコード挿入においては、エラーもなくある程度適切な回答を貰える印象でした。
APIのURIからある程度機能を推論してレスポンスやインタフェースの命名をしてくれるのは面白いと感じます。
③webui) sqlite3のDB初期化コード作成
ユーザー情報をDBに生成し取得する機能を作るために、まずはDBを初期化するコードをAIに指示してみます。
以下のように、少し具体的に何をしてほしいのかを指示しました。
質問:サーバ実行時にsqlite3の初期化を行いたい。
myapp.dbファイルがなければ生成する usersテーブルがなければ作る。 usersテーブルはid, nameで構成される。
■ AIからの回答
次のコードは、サーバ実行時にsqlite3の初期化を行い、users
テーブルを作成する例です。
db/db.go
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
// InitDatabase は、データベースを初期化します。
func InitDatabase() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "myapp.db")
if err != nil {
return nil, err
}
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)`)
if err != nil {
return nil, err
}
return db, nil
}
main.go
import (
"github.com/gin-gonic/gin"
"myapp/handlers"
"myapp/db"
)
func main() {
// データベースの初期化
db, err := db.InitDatabase()
if err != nil {
panic(err)
}
defer db.Close()
router := gin.Default()
// ルートの定義
router.GET("/users/:id", handlers.GetUserHandler)
router.GET("/products/:id", handlers.GetProductHandler)
router.Run(":8080")
}
このコードでは、db/db.go
ファイルでInitDatabase()
関数でデータベースを初期化しています。main.go
ファイルでは、この関数を呼び出し、データベース接続を取得しています。最後に、データベース接続を閉じるようにしています。
このコードを実行すると、次の動作が実行されます。
-
myapp.db
ファイルがなければ自動的に生成されます。 -
users
テーブルがなければ自動的に作成されます。 - サーバが起動し、ルーティングが実行されます。
db/db.go
の作成とmain.go
でいれるコードと説明をもらい、回答どおりにすると正常動作も確認できました。
Go実行結果
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> myapp/handlers.HelloHandler (3 handlers)
[GIN-debug] GET /users/:id --> myapp/handlers.GetUserHandler (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8080
$ sqlite3 myapp.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> .schema users
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
DB初期化はマイグレーション管理ライブラリなどの仕組みを使うのが理想ですが、いくつかドキュメントを確認しながら行う必要がある機能を、質問一つで意図通りに正常動作するコードと説明を教えてもらったのはいいと思いました。
④Continue) DBにユーザーを生成するコードを生成
これからはIDEの機能を活用してコードを作って行きます。
まず、他のコードでDB Resourceを使えるように、DB resourceを返却する関数を作ります。
DB初期化コードの1行をサンプルに、Continueのコード編集(Ctrl + I
)で、GetDataBase()関数の生成を指示しました。
簡単ではありますが、意図通りのコードを作成してくれました。
あとは、ユーザー生成APIのHandler部分の実装を指示します。
今回は色々要件があるので、まず指示内容をIDE上のCommentで書いたあと、Continueのコード編集(Ctrl + I
)でコード生成を指示します。
指示内容はこちらです。
// func CreateUserHandlerを作って
// nameをフォームデータでもらう
// db.GetDatabase()を呼び出して。sqlite3使って
// usersテーブルにinsertして
// idとnameをjsonでレスポンスして
GetDatabase()呼び出し部分は、AIではreturn typeが不明瞭だったせいか、エラーのあるコードですが、それ以外は意図通りに動くコードを生成してくれたうえで、色々バリデーションも作成してくれました。
GetDatabase()呼び出し部分はAIに再度指示して修正してもらいます。
正常に実行できるようになり、APIからDBにデータが追加されることが確認できました。
実行結果
$ curl -X POST http://localhost:8080/users -d "name=OH" | jq
{
"id": 1,
"name": "OH"
}
$ sqlite3 myapp.db
sqlite> select * from users;
1|OH
IDEからコメントと指示を組み合わせて、少しは複雑な指示に対してもAIは意図に近いコードを返してくれました。Local LLMでもこのレベルの作業が可能なのが嬉しいですね。
⑤Continue) ユーザー取得APIをDBを参照するように修正
最初に借りでAIに作成してもらったユーザー取得APIもDBから取得するように修正を指示します。
今回は、修正したい部分をtodoコメントを入れて、全体コードを選択したあと、Continueのコード編集(Ctrl + I
)することで、ファイル内の全体コードを参照した上で、必要な部分を適切に修正してくれることを願って指示します。
色々差分が出てますが、最終的には意図通りに動作するGetUserHandlerのコードを生成してくれました。
AcceptしたあとのGetUserHandlerコード
// getUserHandler は、GET "/users/:id" リクエストに対するハンドラです。
func GetUserHandler(c *gin.Context) {
// SQLite3データベースの接続を取得
db, err := db.GetDatabase()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
return
}
// ユーザーのIDを取得
id := c.Param("id")
// usersテーブルからユーザーを取得
var user struct {
ID int64
Name string
}
err = db.QueryRow("SELECT id, name FROM users WHERE id = ?", id).Scan(&user.ID, &user.Name)
if err != nil {
c.JSON(404, gin.H{
"error": "ユーザーが見つかりませんでした。",
})
return
}
// ユーザーのIDと名前をJSONでレスポンス
c.JSON(200, gin.H{
"id": user.ID,
"name": user.Name,
})
}
動作確認
$ curl -X GET http://localhost:8080/users/1 | jq
{
"id": 1,
"name": "OH"
}
マーシンパフォーマンスにもよりますが、ある程度多い分量のコードもインプットとして使い、必要な部分を絞って指示を出すこともLocal LLMで使えることが嬉しいですね。
⑥Continue) Sqlite3 LibraryからGormに差し替え
今回はライブラリーレベルでの差し替えを試します。
まず前の段階で作成していたdb.goとusers.goをGormに差し替えるように指示してみました。
AIへ質問:gormに差し替えて
※db.goのコードを選択しContinueのコード編集(Ctrl + I
)で指示
■AIからの結果
■Acceptしたコード
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
}
func InitDatabase() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
if err != nil {
return nil, err
}
db.AutoMigrate(&User{})
return db, nil
}
func GetDatabase() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
users.goでdb.User is not a type
が出たので、AIに修正を指示します。
AIへ質問:handlers/users.go:23:17:db.User is not a type handlers/users.go:53:16: db.User is not a type
これで動作するusers.goのコードになりました。
users.goコード
package handlers
import (
"github.com/gin-gonic/gin"
"myapp/db"
)
// User represents a user entity.
type User struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"not null"`
}
// GetUserHandler は、GET "/users/:id" リクエストに対するハンドラです。
func GetUserHandler(c *gin.Context) {
// SQLite3データベースの接続を取得
db, err := db.GetDatabase()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
return
}
// ユーザーのIDを取得
id := c.Param("id")
// usersテーブルからユーザーを取得
var user User
err = db.First(&user, id).Error
if err != nil {
c.JSON(404, gin.H{
"error": "ユーザーが見つかりませんでした。",
})
return
}
// ユーザーのIDと名前をJSONでレスポンス
c.JSON(200, gin.H{
"id": user.ID,
"name": user.Name,
})
}
// CreateUserHandler は、POST "/users" リクエストに対するハンドラです。
func CreateUserHandler(c *gin.Context) {
name := c.PostForm("name")
// SQLite3データベースの接続を取得
db, err := db.GetDatabase()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
return
}
// usersテーブルに新しいユーザーを追加
user := User{Name: name}
err = db.Create(&user).Error
if err != nil {
c.JSON(500, gin.H{
"error": "ユーザーの追加に失敗しました。",
})
return
}
// ユーザーのIDと名前をJSONでレスポンス
c.JSON(201, gin.H{
"id": user.ID,
"name": user.Name,
})
}
db.goの修正によりmain.goもエラーになりました。
ただここはAIより解決するのが難しかったので、自分で修正しました。
main.goエラーと修正
// 以下のエラーを修正
// ./main.go:15:14: db.Close undefined (type *gorm.DB has no field or method Close)
func main() {
db, err := db.InitDatabase()
if (err != nil) {
panic(err)
}
//defer db.Close() ※ここを以下のコードに差し替え
defer func() {
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
if err := sqlDB.Close(); err != nil {
panic(err)
}
}()
//...省略
}
これで、エラーは解消されGormを使ったAPIコードが作成できました。
同じ機能に対して、違うライブラリやフレームワークに差し替える際の活用が期待される題目でした。EOLのライブラリやフレームワークにおいてLocal LLMを活用し脱却を少しでも加速することができるととっても嬉しいと感じます。
⑦最終コード
最終的に以下のようなコードになり、正常動作するアプリケーションとなりました。
全体コード
// main.go
package main
import (
"github.com/gin-gonic/gin"
"myapp/handlers"
"myapp/db"
)
func main() {
// データベースの初期化
db, err := db.InitDatabase()
if err != nil {
panic(err)
}
defer func() {
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
if err := sqlDB.Close(); err != nil {
panic(err)
}
}()
// アプリケーションの起動
router := gin.Default()
// ルートの定義
router.GET("/", handlers.HelloHandler)
router.GET("/users/:id", handlers.GetUserHandler)
router.POST("/users", handlers.CreateUserHandler)
// アプリケーションの起動
router.Run(":8080")
}
// db/db.go
package db
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string
}
func InitDatabase() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
if err != nil {
return nil, err
}
db.AutoMigrate(&User{})
return db, nil
}
func GetDatabase() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
if err != nil {
return nil, err
}
return db, nil
}
//handlers/users.go
package handlers
import (
"github.com/gin-gonic/gin"
"myapp/db"
)
// User represents a user entity.
type User struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"not null"`
}
// GetUserHandler は、GET "/users/:id" リクエストに対するハンドラです。
func GetUserHandler(c *gin.Context) {
// SQLite3データベースの接続を取得
db, err := db.GetDatabase()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
return
}
// ユーザーのIDを取得
id := c.Param("id")
// usersテーブルからユーザーを取得
var user User
err = db.First(&user, id).Error
if err != nil {
c.JSON(404, gin.H{
"error": "ユーザーが見つかりませんでした。",
})
return
}
// ユーザーのIDと名前をJSONでレスポンス
c.JSON(200, gin.H{
"id": user.ID,
"name": user.Name,
})
}
// CreateUserHandler は、POST "/users" リクエストに対するハンドラです。
func CreateUserHandler(c *gin.Context) {
name := c.PostForm("name")
// SQLite3データベースの接続を取得
db, err := db.GetDatabase()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
return
}
// usersテーブルに新しいユーザーを追加
user := User{Name: name}
err = db.Create(&user).Error
if err != nil {
c.JSON(500, gin.H{
"error": "ユーザーの追加に失敗しました。",
})
return
}
// ユーザーのIDと名前をJSONでレスポンス
c.JSON(201, gin.H{
"id": user.ID,
"name": user.Name,
})
}
// handlers/hello.go
package handlers
import (
"github.com/gin-gonic/gin"
)
func HelloHandler(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello, Gin!",
})
}
動作確認(ユーザー生成、紹介)
$ curl -X POST http://localhost:8080/users -d "name=OH" | jq
{
"id": 1,
"name": "OH"
}
$ curl -X GET http://localhost:8080/users/1 | jq
{
"id": 1,
"name": "OH"
}
簡単でありコード構造やバリデーション、エラーハンドリングにおいては改善の余地はあるかもですが、このようにユーザー追加と紹介APIをほぼAI情報だよりで作成することができました。
また、HelloコードからAPIハンドル、sqlite操作追加、Gormに差し替えまえ段階的にコード生成と修正をAIに指示し繰り返すことで
- この段階をほぼAIが生成したコードで意図通りの動作までたどり着いた
- Local LLMでできた
ことは、印象的と感じました。
2) 今までのGolangのAPI機能を一気に生成
前の段階では、回り道をしながら段階的にAPIを作成しました。
今回は前の段階で作った要件のアプリケーションコードを、まとめた要件で、一つの質問で生成することを試します。
①webui) コード生成
以下のように使う言語・FWとDB要件、API URIと遂行する機能の概要を一気に指示します。
AIへ質問:Go, Gin, Gormでアプリケーション作って。 サーバ起動時にmyapp.db、usersテーブル作って。fieldはidとnameにして。 /user/:id は、ユーザーをDBから取得してjsonで返して。ユーザーがなければ404を応答して。 /user/newは、ユーザーを生成して。 バリデーションもして。 作ったあとコードを説明して。
■AIからの結果
package main
import (
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"not null"`
}
var db *gorm.DB
func main() {
db, _ = gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
db.AutoMigrate(&User{})
r := gin.Default()
r.GET("/user/:id", getUser)
r.POST("/user/new", createUser)
r.Run(":8080")
}
func getUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"message": "User not found"})
return
}
c.JSON(200, user)
}
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"message": "Invalid request body"})
return
}
if user.Name == "" {
c.JSON(400, gin.H{"message": "Name is required"})
return
}
db.Create(&user)
c.JSON(201, user)
}
コード説明
-
User
構造体:データベースに保存するユーザーのデータ。 -
db
:データベースのクライアント。 -
main()
:サーバ起動時にデータベースとテーブルを作成。 -
/user/:id
:ユーザーを取得してJSONで返します。ユーザーが存在しない場合は404を返します。 -
/user/new
:ユーザーを生成し、バリデーションを行い、データベースに保存します。バリデーションエラーがある場合は400を返します。
機能
このアプリケーションは、以下の機能を提供します。
- ユーザーの取得
- ユーザーの生成
注意
- アプリケーションは単体テストには対応していません。
- データベースはサーバ起動時に自動的に作成されます。
- データベースのスキーマは自動的にマイグレーションされます。
一つのファイルのコードで教えてくれますが、見た感じシンプルで意図通りに書かれたコードをもらったように見えます。
②webui) テストcurlコマンド生成/動作確認
動作確認したいので、AIにテストのcurlコマンドを質問します。その後、直接叩いて動作確認をしてみます。
AIへ質問:正常ケースをテストするためのcurlコマンドも教えて。jqつけて。
■AIからの結果
curl -X GET localhost:8080/user/1 | jq
curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' localhost:8080/user/new | jq
動作確認 (正常ケース)
] ```bash $ curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' localhost:8080/user/new | jq { "ID": 1, "Name": "John" }$ curl -X GET localhost:8080/user/1 | jq
{
"ID": 1,
"Name": "John"
}
</details>
<details><summary>AIへ質問:エラーケースをテストするためのcurlコマンドも教えて。jqつけて。</summary>
■AIからの結果
```bash
curl -X GET localhost:8080/user/999 | jq
curl -X POST -H "Content-Type: application/json" -d '{}' localhost:8080/user/new | jq
動作確認(エラーケース)
$ curl -X GET localhost:8080/user/999 | jq
{
"message": "User not found"
}
$ curl -X POST -H "Content-Type: application/json" -d '{}' localhost:8080/user/new | jq
{
"message": "Name is required"
}
ユーザー情報取得のエラーケースのcurlコマンドは少し修正の余地はありますが、AIから簡単に検証コマンドの参考を生成し使えるのが便利と感じます。
こういう風に指示によっては、簡単なアプリケーションを一気に作ることができる可能性も期待できます。Chatベースだと、少し限界はありますが、Auto-GPTのようにLocal LLM基盤でAI自らが目的の達成まで推論を繰り返しに遂行するAI Agentなどを活用することも考えられると思い、どこかで試したいなと思ったりします。
6. AIツールで開発への活用に試す(Document生成)
コードを触るエンジニアよりのドキュメントがメインになりますが、コードからドキュメントを生成することを試しました。
①Continue) readme.md作成
前の段階でAIが生成したコードをベースにREADME.MD作成を指示しました。
AIへ質問:README.MDを作成して。code blockで囲んで。
■AIからの結果
# myapp
これは、Go言語で開発されたWebアプリケーションのREADMEです。
## アプリケーションの概要
このアプリケーションは、ユーザーを管理する機能を提供します。ユーザーは作成、詳細が表示、更新できます。
## テック
* Go言語
* Ginフレームワーク
* MySQLデータベース
## プログラムの構造
* `main.go`: アプリケーションのメイン処理
* `handlers/*.go`: ハンドラー
* `db/*.go`: データベース接続と操作
## アプリケーションの機能
* `/`: ホーム画面
* `/users/:id`: ユーザー詳細画面
* `/users`: ユーザー作成画面
## 導入
1. Go言語の環境を準備してください。
2. このディレクトリをダウンロードし、`go run main.go`を実行してください。
## プログラムの動作
アプリケーションの動作を説明してください。
## テスト
アプリケーションのテストを説明してください。
## デバッグ
アプリケーションのデバッグ方法を説明してください。
## Contributing
アプリケーションへの貢献を説明してください。
やはり現場によってまとめたい情報はそれぞれと思うので悩ましい結果ではありますが、アプリケーションの概要の整理観点では、いい感じでまとめてくれた印象です。
中身よりは、AIが提示してくれる観点が、ドキュメントでどういう観点で内容をまとめるかのヒントにつなげることができるかなと期待しています。
②Continue) Swagger作成 (yaml)
今回は、API定義書でよく使われるSwaggerをyaml形式で生成を指示しました。
AIへ質問:swagger.yaml作成して
■AIからの結果
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/user/{id}:
get:
summary: Get a user by ID
parameters:
- in: path
name: id
schema:
type: integer
required: true
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: Not Found
/user/new:
post:
summary: Create a new user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Bad Request
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
この YAML ファイルは、次の機能を提供します。
/user/{id} Endpointsでユーザーを取得する API を定義します。
/user/new Endpointsで新しいユーザーを作成する API を定義します。
User データ型を定義します。
この YAML ファイルを swagger.yaml と名付け、main.go ファイルと同じディレクトリに保存してください。
これらのファイルをコンパイルして、Swagger ドキュメントを生成してください。これは、API のドキュメントを自動生成し、クライアント開発者に役立ちます。ext
/user/new
のreuqestは、AIで生成したコードの内容もあり、idをrequest parameterとしてもらうようにでていて、こういう風に修正が必要なところは出る場合はあります。
その反面、一つ一つ書いているとなかなか手間がかかるSwaggerの作成をLocal LLMの補助をもらって著しく早く作成できるのは嬉しいポイントと思います。
③Continue) Swagger作成(gin-swagger)]
プレームワークによって、コードとSwagger定義を統合し作成できるライブラリもあったりします。実際のコードとの連携を取ることで、情報齟齬の防止とyaml方式より少ないコード量でSwaggerを作成できるメリットがあります。だとしても、Swaggerを書くのはコストがかかることですね。
ここに対してもAIに指示をして作成を試みます。
あいにく、導入自体は情報が古くAIからの回答はそこまで役に立たなかったので、手動導入しました。
https://github.com/swaggo/gin-swagger
そして以下の質問を通して、Ginコードにgin-swaggerコメントを挿入するように指示しました。
AIへ質問:gin-swaggerコメント書いて
■AIからの結果から修正したこと
- @Routerは追加してくれなかったので手動で追加しました。
-
@Failure部分は
{object} gin.H
のTypeがつけられますが、swag init
でエラーになるので、一旦タイプは未指定にしてます。別途ErrorTypeを作成して使うと良いですね。
■AIからの結果
// getUser godoc
// @Summary Get a user by ID
// @Description Get a user by their unique identifier.
// @Tags users
// @Param id path int true "User ID"
// @Success 200 {object} User "OK"
// @Failure 404
// @Router /user/{id} [get]
func getUser(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(404, gin.H{"message": "User not found"})
return
}
c.JSON(200, user)
}
// createUser godoc
// @Summary Create a new user
// @Description Create a new user with the provided name.
// @Tags users
// @Accept json
// @Param user body User true "User object"
// @Success 201 {object} User "Created"
// @Failure 400
// @Router /user/new [post]
func createUser(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"message": "Invalid request body"})
return
}
if user.Name == "" {
c.JSON(400, gin.H{"message": "Name is required"})
return
}
db.Create(&user)
c.JSON(201, user)
}
@Router
の漏れと@Failure
のType指定でエラーになる少し修正が必要な部分はありましたが、多くの部分を自動で作成してくれた印象です。
上記の2つを修正したら、正常に表示可能なSwaggerが生成されたことが確認できます。
Swagger生成結果
$ swag init
2024/05/18 23:33:01 Generate swagger docs....
2024/05/18 23:33:01 Generate general API Info, search dir:./
2024/05/18 23:33:01 Generating main.User
2024/05/18 23:33:01 create docs.go at docs/docs.go
2024/05/18 23:33:01 create swagger.json at docs/swagger.json
2024/05/18 23:33:01 create swagger.yaml at docs/swagger.yaml
やはり特定のライブラリに対しての指示は、バージョンが古い情報をベースにした回答や一部情報が欠けられた回答が発生することもあると感じます。
しかし、多くの部分の作成を補ってくれるところは、引き続き便利なところと感じます。
④Continue) DBML生成 (Sampleコードを教えて結果を生成)
データベースのエンティティ関係表現のツールとして、DBMLというものがあります。
直観的な構文でERDをコードベースで表現できるMarkup言語です。
cliやonlineでERDに変換・編集が可能です。
しかし、新しい構文を覚えていちいち書くのも物覚えが悪い筆者には大変コストがかかることですね。なので、これもAIに指示してみます。
例として3つのテーブルと外部キーがある定義を持つGolang&GormコードからDBMLを生成してみます。
Goコード
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint
Name string
Profile Profile
Orders []Order
}
type Profile struct {
ID uint
UserID uint
Bio string
}
type Order struct {
ID uint
UserID uint
Item string
}
func main() {
// データベース接続
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatalln("failed to connect database")
}
// テーブルの自動マイグレーション
db.AutoMigrate(&User{}, &Profile{}, &Order{})
// データの初期化
db.Create(&User{
Name: "Alice",
Profile: Profile{
Bio: "Software Engineer",
},
Orders: []Order{
{Item: "Laptop"},
{Item: "Mouse"},
},
})
// データの取得
var users []User
db.Preload("Profile").Preload("Orders").Find(&users)
// 結果の表示
for _, user := range users {
fmt.Printf("User: %s\n", user.Name)
fmt.Printf(" Profile: %s\n", user.Profile.Bio)
for _, order := range user.Orders {
fmt.Printf(" Order: %s\n", order.Item)
}
}
}
codegemmaでDBMLは学習はしているようですが、少し精度が落ちますね。
この場合は、サンプルをまず教えたあと、書いてもらうように指示すると、良い結果が期待できます。
sample.dbml内容
// Use DBML to define your database structure
// Docs: https://dbml.dbdiagram.io/docs
Table follows {
following_user_id integer
followed_user_id integer
created_at timestamp
}
Table users {
id integer [primary key]
username varchar
role varchar
created_at timestamp
}
Table posts {
id integer [primary key]
title varchar
body text [note: 'Content of the post']
user_id integer
status varchar
created_at timestamp
}
Ref: posts.user_id > users.id // many-to-one
Ref: users.id < follows.following_user_id
Ref: users.id < follows.followed_user_id
また、日本語で質問するとまた精度が良くない感じだったので、英語で質問をしています。
AIへ質問:from main.go, make DBML that can use dbdiagram.io that can use dbdiagram.io. about syntax, refer to sample.dbml.
dbdiagram.ioで正常で表示できるコードをAIから作成してもらうことができました。
結果コードとDiagram表示結果
// Use DBML to define your database structure
// Docs: https://dbdiagram.io/docs
Table users {
id integer [primary key]
name varchar
created_at timestamp
}
Table profiles {
id integer [primary key]
user_id integer
bio text
created_at timestamp
}
Table orders {
id integer [primary key]
user_id integer
item varchar
created_at timestamp
}
Ref: users.id > profiles.user_id // one-to-one
Ref: users.id > orders.user_id // one-to-many
少し間違う場合がありますが、ちょっとした修正で使えるくらいには作ってくれる感じです。
少し学習データが足りない場合でも、比較的に簡単な課題は、fine-tuningまでいかなくても、参考資料をインプットして求む回答を得ることも期待できます。
この過程をもっと高度化したRAGという方法になり、目的に合わせた回答に最適化を行う動きも最近は活発そうです。
⑤Continue) Mermaid生成 (状態遷移、シークエンス)
コード条件によっては結果に差はあるかもですが、MermaidはDBMLに比べてかなりいい感じでかいてくれる印象があります。
コード例 (注文ステータス)
package main
import (
"fmt"
)
type OrderState string
const (
NewOrder OrderState = "New Order"
Processing OrderState = "Processing"
Shipping OrderState = "Shipping"
Delivered OrderState = "Delivered"
Cancelled OrderState = "Cancelled"
)
type Order struct {
ID int
State OrderState
}
func (o *Order) NextState() {
switch o.State {
case NewOrder:
o.State = Processing
case Processing:
o.State = Shipping
case Shipping:
o.State = Delivered
}
}
func (o *Order) Cancel() {
if o.State == NewOrder {
o.State = Cancelled
} else {
fmt.Println("Order can only be cancelled from the New Order state.")
}
}
func main() {
order := Order{ID: 1, State: NewOrder}
order.Cancel()
fmt.Printf("Order State: %s\n", order.State)
order.NextState()
fmt.Printf("Order State: %s\n", order.State)
order.Cancel()
fmt.Printf("Order State: %s\n", order.State)
order.NextState()
fmt.Printf("Order State: %s\n", order.State)
order.Cancel()
fmt.Printf("Order State: %s\n", order.State)
}
AIへ質問:状態遷移をMermaidで書いて
■AIからの結果
graph LR
A[New Order] --> B[Processing]
B --> C[Shipping]
C --> D[Delivered]
A --> E[Cancelled]
コード例 (在庫・決済・メール送信の流れの例)
package main
import (
"fmt"
)
// User struct holds user information.
type User struct {
ID int
Name string
Email string
}
// Product struct holds product information.
type Product struct {
ID int
Name string
Stock int
}
// Order struct holds order information.
type Order struct {
ID int
User User
Product Product
Quantity int
}
// InventoryService struct handles stock updates.
type InventoryService struct{}
// UpdateStock updates the stock of a product.
func (is *InventoryService) UpdateStock(product *Product, quantity int) error {
if product.Stock < quantity {
return fmt.Errorf("not enough stock for product %s", product.Name)
}
product.Stock -= quantity
fmt.Printf("Stock updated for product %s: %d left\n", product.Name, product.Stock)
return nil
}
// PaymentService struct handles payment processing.
type PaymentService struct{}
// ProcessPayment processes the payment for a user.
func (ps *PaymentService) ProcessPayment(user User, amount float64) error {
fmt.Printf("Processing payment for user %s: $%.2f\n", user.Name, amount)
return nil
}
// EmailService struct handles sending emails.
type EmailService struct{}
// SendOrderConfirmation sends an order confirmation email to the user.
func (es *EmailService) SendOrderConfirmation(user User, order Order) {
fmt.Printf("Sending order confirmation to %s for order %d\n", user.Email, order.ID)
}
// OrderService struct manages the order processing workflow.
type OrderService struct {
InventoryService InventoryService
PaymentService PaymentService
EmailService EmailService
}
// PlaceOrder processes the order by updating stock, processing payment, and sending an email.
func (os *OrderService) PlaceOrder(user User, product Product, quantity int) (Order, error) {
order := Order{ID: 1, User: user, Product: product, Quantity: quantity}
// Update stock
if err := os.InventoryService.UpdateStock(&product, quantity); err != nil {
return Order{}, err
}
// Process payment
if err := os.PaymentService.ProcessPayment(user, float64(product.Stock)*quantity); err != nil {
return Order{}, err
}
// Send order confirmation email
os.EmailService.SendOrderConfirmation(user, order)
fmt.Printf("Order placed: %+v\n", order)
return order, nil
}
func main() {
// Initialize services
inventoryService := InventoryService{}
paymentService := PaymentService{}
emailService := EmailService{}
orderService := OrderService{
InventoryService: inventoryService,
PaymentService: paymentService,
EmailService: emailService,
}
// Initialize user and product
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
product := Product{ID: 1, Name: "Laptop", Stock: 10}
// Place order
order, err := orderService.PlaceOrder(user, product, 2)
if err != nil {
fmt.Println("Failed to place order:", err)
} else {
fmt.Printf("Order details: %+v\n", order)
}
}
AIへ質問:SequenceDiagramをMermaidで書いて。
■AIからの結果
sequenceDiagram
participant User
participant Product
participant InventoryService
participant PaymentService
participant EmailService
participant OrderService
User->>OrderService: PlaceOrder(user, product, quantity)
OrderService->>InventoryService: UpdateStock(product, quantity)
InventoryService->>Product: Stock updated for product Laptop: 8 left
OrderService->>PaymentService: ProcessPayment(user, amount)
PaymentService->>User: Processing payment for user Alice: $160.00
OrderService->>EmailService: SendOrderConfirmation(user, order)
EmailService->>User: Sending order confirmation to alice@example.com for order 1
OrderService->>User: Order placed: {ID: 1, User: {ID: 1, Name: Alice, Email: alice@example.com}, Product: {ID: 1, Name: Laptop, Stock: 8}, Quantity: 2}
UMLはコードの分析と認識合わせで共通言語としてすごく役に立つと思いますが、時々時間がかかってしまうところですね。
設計段階での作成もですが、コードをリバースして把握したいときにすごく活用できるかもと期待しています。
7. AIツールで開発への活用に試す(コードリファクタリング)
今回は、コードのリファクタリングを少し試して見ます。
①コード分離(関数の分離)
リファクタリングで一番頻繁につかわれて、リファクタリングのファーストステップとして、コードの分離が多いかなと思います。
ここでは例として、ファイル読み取り、変換、保存の機能が1つの関数で書かれている例で、コード分離を指示して見ました。
コード例
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func processFile(filePath string) {
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading file:", err)
return
}
var processedLines []string
for _, line := range lines {
processedLine := strings.ToUpper(line)
processedLines = append(processedLines, processedLine)
}
outputFile, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating output file:", err)
return
}
defer outputFile.Close()
writer := bufio.NewWriter(outputFile)
for _, line := range processedLines {
_, err := writer.WriteString(line + "\n")
if err != nil {
fmt.Println("Error writing to output file:", err)
return
}
}
writer.Flush()
}
func main() {
processFile("input.txt")
}
AIへ質問:関数の中を別の関数に綺麗に分離して。
■AIからの結果
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func processFile(filePath string) {
lines := readLines(filePath)
processedLines := processLines(lines)
writeLines(processedLines)
}
func readLines(filePath string) []string {
file, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening file:", err)
return nil
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading file:", err)
return nil
}
return lines
}
func processLines(lines []string) []string {
var processedLines []string
for _, line := range lines {
processedLine := strings.ToUpper(line)
processedLines = append(processedLines, processedLine)
}
return processedLines
}
func writeLines(lines []string) {
outputFile, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating output file:", err)
return
}
defer outputFile.Close()
writer := bufio.NewWriter(outputFile)
for _, line := range lines {
_, err := writer.WriteString(line + "\n")
if err != nil {
fmt.Println("Error writing to output file:", err)
return
}
}
writer.Flush()
}
func main() {
processFile("input.txt")
}
コード分離の目的としては、まずある目的の単位でコード観点を分離し、把握しやすくするところで、次のリファクタリングにつなげるためとして、うまく分けてくれました。
これは、コード条件によっては結果の精度のブレはあるかもですが、一旦試す感じで活用でも、多く役に立ちそうなポイントですね。
②コードの分離(機能を構造体へ分離)
コード分離のもう一つの一環として、特定機能をクラスや構造体に分離することも試しました。
ここでは、仮の口座の振込と残高に対する処理に対して、構造体へに分離することを試しました。
コード例
package main
import "fmt"
func main() {
// init balance
balance := 1000.0
// depsoit
deposit := 500.0
balance += deposit
fmt.Printf("Deposited: %.2f\n", deposit)
fmt.Printf("Balance: %.2f\n", balance)
// withdrawal
withdrawal := 300.0
if withdrawal <= balance {
balance -= withdrawal
fmt.Printf("Withdrawn: %.2f\n", withdrawal)
} else {
fmt.Println("Insufficient funds")
}
fmt.Printf("Balance: %.2f\n", balance)
}
AIへ質問:取引の残高変数と機能を構造体にリファクタして。
■AIからの結果
package main
import "fmt"
type Account struct {
balance float64
}
func (a *Account) Deposit(amount float64) {
a.balance += amount
fmt.Printf("Deposited: %.2f\\n", amount)
}
func (a *Account) Withdraw(amount float64) {
if amount <= a.balance {
a.balance -= amount
fmt.Printf("Withdrawn: %.2f\\n", amount)
} else {
fmt.Println("Insufficient funds")
}
}
func (a *Account) GetBalance() float64 {
return a.balance
}
func main() {
// create account
account := &Account{balance: 1000.0}
// deposit
account.Deposit(500.0)
fmt.Printf("Balance: %.2f\\n", account.GetBalance())
// withdrawal
account.Withdraw(300.0)
fmt.Printf("Balance: %.2f\\n", account.GetBalance())
}
リファクタされたポイント:
- 取引の残高を構造体
Account
のメンバ変数として保存する。 -
Deposit()
メソッドで入金処理を行い、残高を更新する。 -
Withdraw()
メソッドで出金処理を行い、残高を更新する。 -
GetBalance()
メソッドで残高を取得する。 - メイン関数は、
Account
のインスタンスを作成し、入金、出金、残高を表示する。
残高属性と残高紹介、入金・出勤の機能に対して構造体に作ってくれました。
③重複コードのリファクタリング
重複コードもまた、よく出会うパターンと思います。
今回は、HTTPClientを通してデータを取得する例ですが、2つの別のURLからデータを取得する例として、HTTPClient処理部分が重複している例の改善を指示してみました。
コード例
httpを利用してデータを取得するロジック重複している例
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
// get animal contents url
url1 := "https://random.dog/woof.json"
resp1, err1 := http.Get(url1)
if err1 != nil {
fmt.Println("Error:", err1)
return
}
defer resp1.Body.Close()
body1, err1 := ioutil.ReadAll(resp1.Body)
if err1 != nil {
fmt.Println("Error:", err1)
return
}
fmt.Println("Response from URL 1:", string(body1))
// get weather
url2 := "https://goweather.herokuapp.com/weather/Shibuya-ku"
resp2, err2 := http.Get(url2)
if err2 != nil {
fmt.Println("Error:", err2)
return
}
defer resp2.Body.Close()
body2, err2 := ioutil.ReadAll(resp2.Body)
if err2 != nil {
fmt.Println("Error:", err2)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
AIへ質問:コード重複をリファクタリングしたい。
■AIからの結果
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func getData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func main() {
// get animal picture url
url1 := "<https://random.dog/woof.json>"
body1, err := getData(url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// get weather
url2 := "<https://goweather.herokuapp.com/weather/Shibuya-ku>"
body2, err := getData(url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
リファクタリングされたポイント
-
getData()
関数を追加し、HTTP リクエストを共通化しました。 -
getData()
関数はエラー処理を含み、HTTP リクエストで発生するエラーを返します。 - メイン関数では、
getData()
関数を使用して、それぞれの URL からデータをフェッチします。
重複する部分を一つの関数として切り出して、コードが短くなりました。
重複機能を排除し凝集度を上げるリファクタリングのファーストステップとしてよく試すことができるような機能と思います。
④Adapterにリファクタリング
HTTP例のコードで、今回はHTTPClientの依存性をゆるくして、他の外部インタフェースで柔軟に差し替えたり、テストしやすくMockを使える形でAdapterパターンへのリファクタリングをAIに指示してみます。
③のコードからリファクタリングをします。Mockも作ってもらいます。
コード例
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func getData(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func main() {
// get animal contents url
url1 := "https://random.dog/woof.json"
body1, err := getData(url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// get weather
url2 := "https://goweather.herokuapp.com/weather/Shibuya-ku"
body2, err := getData(url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
AIへ質問:Http部分をテストしやすいようにAdapter Patternにリファクタリングして。
■AIからの結果
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
type HttpAdapter struct{}
func (a *HttpAdapter) Get(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
type DataFetcher interface {
Get(url string) ([]byte, error)
}
func getData(fetcher DataFetcher, url string) ([]byte, error) {
return fetcher.Get(url)
}
func main() {
// Adapter for Http client
fetcher := &HttpAdapter{}
// get animal contents url
url1 := "<https://random.dog/woof.json>"
body1, err := getData(fetcher, url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// get weather
url2 := "<https://goweather.herokuapp.com/weather/Shibuya-ku>"
body2, err := getData(fetcher, url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
このコードでは、HttpAdapterというAdapterを作成し、DataFetcherインターフェースを介してHttpクライアントを操作します。この構成により、Httpクライアントを簡単に差し替えることができるので、テストしやすいコードになります。
具体的な変更点:
- HttpAdapter構造体を作成し、Get()メソッドを実装します。これはHttpクライアントのGet()メソッドと同様の動作を行います。
- DataFetcherインターフェースを定義し、Get()メソッドの宣言を宣言します。
- getData()関数を修正し、DataFetcherインターフェースの変数を受け取るようにします。
- main()関数を修正し、HttpAdapterをDataFetcherとして使用します。
この構成により、Httpクライアントをテストしやすいコードを構築でき、Httpクライアントの振る舞いを簡単に変更できるようになります。
AIへ質問:MockHttpAdapterも作って
■AIからの結果
type MockHttpAdapter struct {
Data map[string][]byte
Err error
}
func (a *MockHttpAdapter) Get(url string) ([]byte, error) {
if data, ok := a.Data[url]; ok {
return data, nil
}
return nil, a.Err
}
func getData(fetcher DataFetcher, url string) ([]byte, error) {
return fetcher.Get(url)
}
func main() {
// Mock data
mockData := map[string][]byte{
"url1": []byte("mock response 1"),
"url2": []byte("mock response 2"),
}
// Mock adapter with mock data
fetcher := &MockHttpAdapter{Data: mockData}
// get animal contents url
url1 := "url1"
body1, err := getData(fetcher, url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// get weather
url2 := "url2"
body2, err := getData(fetcher, url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
このコードでは、MockHttpAdapterというモック構造体を作成し、Get()メソッドを実装します。このモック構造体には、特定のURLに対するレスポンスとエラーを保存できます。getData()関数を修正し、DataFetcherインターフェースの変数を受け取るようにします。
main()関数を修正し、MockHttpAdapterをDataFetcherとして使用します。この構成により、Httpクライアントの振る舞いを簡単に変更できるようになります。
このモック構造体を使用することで、Httpクライアントの振る舞いを簡単にテストできます。
AI修正反映コードと動作確認
先のMockHttpAdapterの例を適切にいれる。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
type HttpAdapter struct{}
func (a *HttpAdapter) Get(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
type MockHttpAdapter struct {
Data map[string][]byte
Err error
}
func (a *MockHttpAdapter) Get(url string) ([]byte, error) {
if data, ok := a.Data[url]; ok {
return data, nil
}
return nil, a.Err
}
type DataFetcher interface {
Get(url string) ([]byte, error)
}
func getData(fetcher DataFetcher, url string) ([]byte, error) {
return fetcher.Get(url)
}
func main() {
// Adapter for Http client
fetcher := &HttpAdapter{}
// get animal contents url
url1 := "https://random.dog/woof.json"
body1, err := getData(fetcher, url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// Mock data
mockData := map[string][]byte{
"https://goweather.herokuapp.com/weather/Shibuya-ku": []byte("mock response 2"),
}
// Mock adapter with mock data
mockFetcher := &MockHttpAdapter{Data: mockData}
// get weather
url2 := "https://goweather.herokuapp.com/weather/Shibuya-ku"
body2, err := getData(mockFetcher, url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
■実行結果
$ go run main.go
Response from URL 1: {"fileSizeBytes":3312702,"url":"https://random.dog/20e0908d-287f-4448-a805-c88415b9f93e.mp4"}
Response from URL 2: mock response 2
データ取得はDataFetcherを通して行うようにし、DataFetcherには引数としてFetcherインタフェースを具現するAdapterを強制することによって、ビジネス層と外部I/F層の依存がゆるくなったコードになりました。また正常動作するMockも作ってくれました。
デザインパターンを活用したリファクタリングは色々悩む場合も多いですが、実際直したいコードベースで色々ヒントを貰える、それがLocal LLMで試せるという意味ですごく嬉しいと感じます。
⑤Factoryにリファクタリング
今回は④のコードから、HttpAdapterとMockHttpAdapterのインスタンス(実態)をFactoryで生成するように指示します。
コード改善の意味では少し最適ではない方向性かもですが、あくまで例として、今後色々Adapterが追加された際に、インスタンス生成とクライアントコードの結合が強くならないことを想定し、やって見ました。
コード例
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
type HttpAdapter struct{}
func (a *HttpAdapter) Get(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
type MockHttpAdapter struct {
Data map[string][]byte
Err error
}
func (a *MockHttpAdapter) Get(url string) ([]byte, error) {
if data, ok := a.Data[url]; ok {
return data, nil
}
return nil, a.Err
}
type DataFetcher interface {
Get(url string) ([]byte, error)
}
func getData(fetcher DataFetcher, url string) ([]byte, error) {
return fetcher.Get(url)
}
func main() {
// Adapter for Http client
fetcher := &HttpAdapter{}
// get animal picture url
url1 := "https://random.dog/woof.json"
body1, err := getData(fetcher, url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// Mock data
mockData := map[string][]byte{
"https://goweather.herokuapp.com/weather/Shibuya-ku": []byte("mock response 2"),
}
// Mock adapter with mock data
mockFetcher := &MockHttpAdapter{Data: mockData}
// get weather
url2 := "https://goweather.herokuapp.com/weather/Shibuya-ku"
body2, err := getData(mockFetcher, url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
AIへ質問:HttpAdapterとMockHttpAdapterを一つのFactoryにして。
■AIからの結果
type DataFetcherFactory struct{}
func (f *DataFetcherFactory) CreateHttpAdapter() DataFetcher {
return &HttpAdapter{}
}
func (f *DataFetcherFactory) CreateMockHttpAdapter(data map[string][]byte) DataFetcher {
return &MockHttpAdapter{Data: data}
}
使用例:
// HttpAdapterを作成する
factory := &DataFetcherFactory{}
fetcher := factory.CreateHttpAdapter()
// MockHttpAdapterを作成する
mockData := map[string][]byte{
"<https://goweather.herokuapp.com/weather/Shibuya-ku>": []byte("mock response 2"),
}
mockFetcher := factory.CreateMockHttpAdapter(mockData)
AI修正反映コードと動作確認
■修正コード
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
type HttpAdapter struct{}
func (a *HttpAdapter) Get(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
type MockHttpAdapter struct {
Data map[string][]byte
Err error
}
func (a *MockHttpAdapter) Get(url string) ([]byte, error) {
if data, ok := a.Data[url]; ok {
return data, nil
}
return nil, a.Err
}
type DataFetcher interface {
Get(url string) ([]byte, error)
}
func getData(fetcher DataFetcher, url string) ([]byte, error) {
return fetcher.Get(url)
}
type DataFetcherFactory struct{}
func (f *DataFetcherFactory) CreateHttpAdapter() DataFetcher {
return &HttpAdapter{}
}
func (f *DataFetcherFactory) CreateMockHttpAdapter(data map[string][]byte) DataFetcher {
return &MockHttpAdapter{Data: data}
}
func main() {
factory := &DataFetcherFactory{}
// HttpAdapterを作成する
fetcher := factory.CreateHttpAdapter()
// get animal picture url
url1 := "https://random.dog/woof.json"
body1, err := getData(fetcher, url1)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 1:", string(body1))
// Mock data
// MockHttpAdapterを作成する
mockData := map[string][]byte{
"https://goweather.herokuapp.com/weather/Shibuya-ku": []byte("mock response 2"),
}
mockFetcher := factory.CreateMockHttpAdapter(mockData)
// get weather
url2 := "https://goweather.herokuapp.com/weather/Shibuya-ku"
body2, err := getData(mockFetcher, url2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Response from URL 2:", string(body2))
}
■実行結果
$ go run main.go
Response from URL 1: {"fileSizeBytes":2798698,"url":"https://random.dog/9da3d969-69f4-4203-a12c-8964a363d4ba.mp4"}
Response from URL 2: mock response 2
良いヒントになる感じで、DataFetcherFactoryを作ってくれました。
言語ごとにデザインパターンの適用の仕方もちょっとづつ差があったりして、迷う場合も時々ありますが、AIの回答で色々ヒントを貰えるのが嬉しいと感じます。
⑥コード分析
GoのAsynqを使っているコードの例で、何をしているコードなのかを分析してもらった例です。
コード例
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/hibiken/asynq"
)
const (
TypeEmailDelivery = "email:deliver"
)
type EmailDeliveryPayload struct {
Recipient string
Subject string
Body string
}
func main() {
// Create a Redis connection for asynq
redisConnOpt := asynq.RedisClientOpt{
Addr: "localhost:6379",
}
// Create a new asynq client
client := asynq.NewClient(redisConnOpt)
// Enqueue a task
payload := EmailDeliveryPayload{
Recipient: "example@example.com",
Subject: "Welcome",
Body: "Welcome to our service!",
}
task, err := asynq.NewTask(TypeEmailDelivery, payload)
if err != nil {
log.Fatalf("could not create task: %v", err)
}
_, err = client.Enqueue(task, asynq.Queue("email"))
if err != nil {
log.Fatalf("could not enqueue task: %v", err)
}
fmt.Println("Task enqueued")
// Create a new asynq server
server := asynq.NewServer(
redisConnOpt,
asynq.Config{
Concurrency: 10,
},
)
// Create a handler for the email delivery task
mux := asynq.NewServeMux()
mux.HandleFunc(TypeEmailDelivery, handleEmailDeliveryTask)
// Run the server
if err := server.Run(mux); err != nil {
log.Fatalf("could not run server: %v", err)
}
}
// handleEmailDeliveryTask is the task handler for email delivery
func handleEmailDeliveryTask(ctx context.Context, t *asynq.Task) error {
var p EmailDeliveryPayload
if err := t.UnmarshalPayload(&p); err != nil {
return fmt.Errorf("could not unmarshal payload: %v", err)
}
// Simulate email delivery
fmt.Printf("Sending email to %s: subject=%s, body=%s\n", p.Recipient, p.Subject, p.Body)
time.Sleep(2 * time.Second) // Simulate a delay in sending email
return nil
}
AIへ質問:何をするコードか分析して
■AIからの回答
このコードは、Asynq を使用してメール配信タスクをエンキューし、そのタスクを処理するサーバーを作成します。
コードの機能:
-
タスクエンキュー:
- メール配信タスクの構造体(
EmailDeliveryPayload
)を作成し、その内容をエンキューします。 - メール配信タスクには、受取人のメールアドレス、件名、本文が含まれます。
- メール配信タスクの構造体(
-
タスクハンドラ:
- メール配信タスクを処理するハンドラ(
handleEmailDeliveryTask
)を作成し、タスクのペイロードをアンマーシャルし、メールを送信します。
- メール配信タスクを処理するハンドラ(
-
サーバー実行:
- Asynq のサーバーを起動し、タスクハンドラを登録します。
- 待ち時間があるタスクがキューに待ち、タスクハンドラによって処理されます。
コードのポイント:
- Asynq のクライアントとサーバーを使用し、非同期タスク処理を可能にします。
- メール配信タスクの構造体でタスクのデータを保存します。
- テストコードでメール配信の遅延を模擬しています。
コードの利点:
- メール配信タスクの並行実行を可能にします。
- メール配信の遅延を考慮できます。
- テストコードでタスクの挙動を検証できます。
コードの欠点:
- メール配信のエラー処理は考慮されていません。
- メール配信サービスとの統合は考慮されていません。
リファクタリングではないですが、前段階として、コード分析の活用にも期待を持てると思います。
8.AIツールで開発への活用に試す(Frontend Reactコード作成)
最後にフロントのコードの作成も少し試して見ました。
ここではごく簡単な例で、主にReactのマークアップを中心に試していますが、Reactのロジック処理においても有効活用できると思います。
①画像からReactコードを生成してみる
open-webuiでは画像をアップロードしAIに質問することができます。
今回は、一つのWebpageの画像と似た感じのコードを作成してもらうことを試します。
まず、イメージ認識に特化したModelを入手する必要があります。llavaというモデルがありOllamaで提供しているのでこれを使ってみます。
https://ollama.com/library/llava
open-webuiでllava:latest
モデルを選択し質問をします。
AIへ質問:discribe it to frontend engineer.
■AIへ指示する画面
■AIからの回答
This is a screenshot of a webpage interface. The design appears to be for a modern, business-oriented website. Here's a description of the elements visible in the image:
1. **Color Scheme**: The color scheme includes shades of pink and white, giving it a clean and contemporary feel.
2. **Typography**: There are two distinct font styles used on this page. One is bold and sans-serif, which is likely used for headings or important text, while the other appears to be a more traditional serif font, possibly used for body content or secondary information.
3. **Layout**: The layout is divided into three columns, with each column containing different content elements such as text, images, and buttons. This is a common design choice for webpages that want to organize information in a structured way.
4. **Header**: At the top of the page, there's a header section that includes the company logo on the left, followed by a navigation menu with links to different parts of the website. The background behind the logo seems to have a gradient or blend from light to dark pink.
5. **Content Areas**: Each column has its own content area, with the right column being more prominent due to its larger size and centered positioning. This suggests that this column likely contains the most important information for visitors.
6. **Imagery**: There's an image of a bird on the left side of the page, which may serve as a decorative element or be related to the theme of the website.
7. **Call-to-Action (CTA)**: In the right column, there is a call-to-action button that stands out due to its size and positioning near the bottom of the content area. This button likely directs users to take some specific action like "Get Started" or "Contact Us".
8. **Footer**: At the very bottom, there's a footer section which typically includes contact information, legal disclaimers, and links to social media profiles or other useful resources.
9. **Responsive Design**: The design suggests that the website is responsive, meaning it should adapt to different screen sizes on mobile devices.
10. **Use of Space**: The use of space is strategic with a good balance between text and images. It allows for a clear hierarchy and helps users understand what they're looking at.
Overall, the page seems to be designed with user experience in mind, aiming to provide information clearly and guide visitors on where to go next within the website.
このあとcodegemma
モデルを選択し、先ほどで貰ったAIからの指示文をもって、Reactコード生成を指示します。
AIへ質問:wirte react code on typescript withcss (llavaからの回答を貼り付け)
■AIからの回答
import React from 'react';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f8f9fa;
`;
const Header = styled.header`
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px;
background-image: linear-gradient(to right, #e6b3e6, #c2c8f0);
`;
const Logo = styled.img`
width: 100px;
height: auto;
`;
const Nav = styled.nav`
display: flex;
gap: 20px;
`;
const NavLink = styled.a`
text-decoration: none;
color: #333;
font-weight: bold;
`;
const Content = styled.main`
display: flex;
flex-grow: 1;
`;
const Column = styled.div`
flex: 1;
padding: 20px;
text-align: center;
`;
const Image = styled.img`
width: 100%;
height: auto;
`;
const Button = styled.button`
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
`;
const Footer = styled.footer`
display: flex;
justify-content: center;
padding: 20px;
background-color: #e6e6e6;
`;
const App = () => {
return (
<Container>
<Header>
<Logo src="/logo.png" alt="Company Logo" />
<Nav>
<NavLink href="#">Home</NavLink>
<NavLink href="#">About</NavLink>
<NavLink href="#">Contact</NavLink>
</Nav>
</Header>
<Content>
<Column>
<Image src="/bird.jpg" alt="Bird Image" />
</Column>
<Column>
<h1>Your Business Solution</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc efficitur lorem nec lorem efficitur, id efficitur lectus efficitur.</p>
<Button>Get Started</Button>
</Column>
</Content>
<Footer>
<p>© 2023 Your Company</p>
</Footer>
</Container>
);
};
export default App;
なんかStyle込みのReactを書いてくれました。
Imageファイルは適当に表示可能なURLに変えてブラウザで確認します。
やはりここは満足にいく水準にはいかない感覚ですね。
しかしながら、LocalLLMで画像を認識させたあとテキストを抽出してコードを生成できたという事自体が今後の期待感をもたせるポイントと思います。
②TailwindCSSに差し替えリファクタリング
Sytle部分をTailwindCSSに差し替えるように指示を試しました。
AIへ質問:refactoring using tailwindcss.
■AIからの結果
import React from "react";
const App = () => {
return (
<div className="h-screen bg-gray-100">
<header className="flex justify-between items-center p-4 bg-gradient-to-r from-purple-400 to-blue-400">
<img src="logo192.png" alt="Company Logo" className="w-16" />
<nav className="flex gap-4">
<a href="#" className="font-bold">Home</a>
<a href="#" className="font-bold">About</a>
<a href="#" className="font-bold">Contact</a>
</nav>
</header>
<main className="flex flex-grow">
<div className="w-1/2 p-4 text-center">
<img src="https://cdn.download.ams.birds.cornell.edu/api/v1/asset/202984001/1200?l2yomzac60wkr95h" alt="Bird Image" className="w-full" />
</div>
<div className="w-1/2 p-4">
<h1 className="text-2xl font-bold">Your Business Solution</h1>
<p className="text-gray-600">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc efficitur lorem nec lorem efficitur, id efficitur lectus efficitur.</p>
<button className="px-4 py-2 bg-blue-500 text-white rounded">Get Started</button>
</div>
</main>
<footer className="flex justify-center p-4 bg-gray-300">
<p>© 2023 Your Company</p>
</footer>
</div>
);
};
export default App;
大きな齟齬なくTailwindCSSに差し替えてくれました。
フロントコードにおいても、Inline CSSやbootstrapなどをTailwindCSSなどの別ライブラリに差し替える際に色々補助が可能と思います。
③Component分離リファクタリング
ここでは、Header, Main, Footerが一つのコンポーネントになっているところを、どういう風に別ファイルに分けられるかを聞いて、良いヒントになり回答をもらうことができます。
AIへ質問:コンポーネントを分けてcomponents/に別のファイルで置きたい。
■AIからの回答
import React from "react";
import Header from './components/Header';
import Main from './components/Main';
import Footer from './components/Footer';
const App = () => {
return (
<div className="h-screen bg-gray-100">
<Header />
<Main />
<Footer />
</div>
);
};
export default App;
import React from "react";
const Header = () => {
return (
<header className="flex justify-between items-center p-4 bg-gradient-to-r from-purple-400 to-blue-400">
<img src="logo192.png" alt="Company Logo" className="w-16" />
<nav className="flex gap-4">
<a href="#" className="font-bold">Home</a>
<a href="#" className="font-bold">About</a>
<a href="#" className="font-bold">Contact</a>
</nav>
</header>
);
};
export default Header;
import React from "react";
const Main = () => {
return (
<main className="flex flex-grow">
<div className="w-1/2 p-4 text-center">
<img src="<https://cdn.download.ams.birds.cornell.edu/api/v1/asset/202984001/1200?l2yomzac60wkr95h>" alt="Bird Image" className="w-full" />
</div>
<div className="w-1/2 p-4">
<h1 className="text-2xl font-bold">Your Business Solution</h1>
<p className="text-gray-600">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc efficitur lorem nec lorem efficitur, id efficitur lectus efficitur.</p>
<button className="px-4 py-2 bg-blue-500 text-white rounded">Get Started</button>
</div>
</main>
);
};
export default Main;
import React from "react";
const Footer = () => {
return (
<footer className="flex justify-center p-4 bg-gray-300">
<p>© 2023 Your Company</p>
</footer>
);
};
export default Footer;
また、コンポーネント内容ボタンを、別のコンポーネントで抽出して、色んなカラーに対応できるようにリファクタリングすることもAIに指示して、意図通りに動作するコードの回答をもらいました。
AIへ質問:buttonは別コンポーネントに分けて別のファイルにしたい。textはpropsにして。colorはpropsにして、red, green, blueの3つに制限して。
■AIからの回答
interface Props {
text: string;
color: 'red' | 'green' | 'blue';
}
const Button = ({ text, color }: Props) => {
const backgroundColor = {
red: 'bg-red-500',
green: 'bg-green-500',
blue: 'bg-blue-500',
}[color];
return (
<button className={`px-4 py-2 ${backgroundColor} text-white rounded`}>{text}</button>
);
};
export default Button;
import Button from './Button';
const Main = () => {
return (
<main className="flex flex-grow">
<div className="w-1/2 p-4 text-center">
<img src="<https://cdn.download.ams.birds.cornell.edu/api/v1/asset/202984001/1200?l2yomzac60wkr95h>" alt="Bird Image" className="w-full" />
</div>
<div className="w-1/2 p-4">
<h1 className="text-2xl font-bold">Your Business Solution</h1>
<p className="text-gray-600">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc efficitur lorem nec lorem efficitur, id efficitur lectus efficitur.</p>
<Button text="Get Started" color="blue" />
</div>
</main>
);
};
複雑になりがちだったReactコードにおいてもリファクタリングで活用できると期待してます。
④Storybook作成
Storybookは、ReactアプリケーションのUIコンポーネントに対して、データベースやサーバーに繋がらなくても、独自環境でコンポーネント事に表示と動作を確認することができるツールです。
UIコンポーネントのドキュメントとして、テスト道具としてよく活用されます。
https://storybook.js.org/
こちらも古いバージョンで学習データが足りないか、かなり違う方向性の回答が出たりしてましたが、幸いサンプルコードと一緒に指示することで、ある程度意図通りのStorybookコードは生成してくれました。
元本
(Button.tsx)interface Props {
text: string;
color: 'red' | 'green' | 'blue';
}
const Button = ({ text, color }: Props) => {
const backgroundColor = {
red: 'bg-red-500',
green: 'bg-green-500',
blue: 'bg-blue-500',
}[color];
return (
<button className={`px-4 py-2 ${backgroundColor} text-white rounded`}>{text}</button>
);
};
export default Button;
AIへ質問:このコンポーネントのstories.tsx書いて。サンプルはこれよ。
■貼ったサンプルコード
import type { Meta, StoryObj } from '@storybook/react';
import { Button, ButtonProps } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
■AIからの回答
import type { Meta, StoryObj } from '@storybook/react';
import { Button, Props } from './Button';
const meta: Meta<typeof Button> = {
component: Button,
title: 'Button',
argTypes: {
text: {
control: { type: 'text' },
},
color: {
control: { type: 'radio' },
options: ['red', 'green', 'blue'],
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Primary: Story = {
args: {
text: 'Primary Button',
color: 'red',
},
};
export const Secondary: Story = {
args: {
text: 'Secondary Button',
color: 'green',
},
};
export const Tertiary: Story = {
args: {
text: 'Tertiary Button',
color: 'blue',
},
};
Buttonのimportなどは少し修正は必要でしたが、それ以外は、意図通りに動作するStorybookコードを生成してくれました。
他のファイルも生成してみます。サンプルのインプットした会話Sessionに引き続き、他のファイルを作ってもらいます。
Footer.tsxのStorybookコード生成
■AIからの結果
import type { Meta, StoryObj } from '@storybook/react';
import { Footer } from './Footer';
const meta: Meta<typeof Footer> = {
component: Footer,
title: 'Footer',
};
export default meta;
type Story = StoryObj<typeof Footer>;
export const Default: Story = {};
Main.tsxのStorybookコード生成
■AIからの回答
import type { Meta, StoryObj } from '@storybook/react';
import { Main } from './Main';
const meta: Meta<typeof Main> = {
component: Main,
title: 'Main',
};
export default meta;
type Story = StoryObj<typeof Main>;
export const Default: Story = {};
Header.tsxのStorybookコード生成
■AIからの回答
import type { Meta, StoryObj } from '@storybook/react';
import { Header } from './Header';
const meta: Meta<typeof Header> = {
component: Header,
title: 'Header',
};
export default meta;
type Story = StoryObj<typeof Header>;
export const Default: Story = {};
Storybookを起動して動作確認すると、Button、Footer、Header、Mainのコンポーネントに対して正常に表示されることが確認できます。
Buttonに置いては、red・green・blueの3つのパターンでUIを確認することもできます。
Storybook動作確認
■Button.stories.tsx - Primary ![](https://storage.googleapis.com/zenn-user-upload/d799c6e9c31d-20240524.png)Storybookコードもstoriesファイルを別途作成する必要があり、これもまた独自の書き方があるので、コンポーネントに対して別途コードを作成するのがコストとしてあります。
その反面、一度作っておくとエンジニアはもちろん、デザイナーや企画者とのUI・UXのコミュニケーションとコンポーネントの一貫性を維持したUI・UX・コード作りにも役に立つと思うので、こちらをAIが補ってくれることは、すごく期待しています。
9. 終わりに
この記事の内容が、似た考えや興味・課題感をもっている方へ何らかのポジティブ効果をお届けてできたら嬉しいと思います。
やってみた思った感想を最後に終わりにしようと思います。
Local LLM AIツールを試して良かったと思った点
- これらがLocal LLMでプライバシー問題もなく、ほぼマシーン費用のみで使える
- 思ったより簡単にLocal LLMとAIツールを構築できる時代になった
- 推論性能が数ヶ月の間でもすごく上がっている。継続的に性能向上・軽量化が進んでいる恩恵。
- プライベートサーバー環境として構築して、社内なので多数が利用できるような仕組みも作りやすそう
開発への活用を試した例において良かったと思った点
- コード生成、リファクタリング、コードリプレースなど、エンジニアのプログラミング助力者として活用できるポイントは確かにある。今後もっと多くのことができる期待感を持てる
- ドキュメントやスクリプト作成において多くの時間を短縮できる期待感がある
- 回答の精度が不足な部分においても、ツールで提供する外部データや文書参照機能、または質問の内容に見本を指定することで、比較的に手間なくほしい回答を誘導し作業させることができる。
課題と思った点
- ChatGPT4に比べるとやはり精度に惜しさはある
- ChatGPT4の進歩がすごすぎる
- プライバシー問題や費用問題がなければ、Saasサービス使うのが、やはり精度・効率面で多くメリットありの感覚
- 精度を担保しつつ軽量化が数ヶ月ですごく進歩したとしても、やはりハイスペックマシーンではないと、回答が遅い感覚があり
- Macで開発への活用を考えるとM1 pro 16GBで7bが、使えないほどではないがかなり重いと感じる
- 英語以外の多国語においては、少しでも高度な質問になると精度が落ちる課題を感じる
今後、開発への活用を試して効率を上げたいこと
- テストコードの作成
- データ分析への活用
- インフラの作成・管理・自動化
- サービスモニタリング・障害管理
今後、Local LLMに対してやって見たいと思ったこと
- 独自データと必要目的に合わせてAI回答をカスタマイズ
- RAG (embedding化された独自データを参照させ答えを誘導する)
- Fine-Tuning(独自データを追加学習させる)
- 開発工程・業務の自動化活用にLocal LLMの活用を模索
- 個人サーバーでLocal LLM AIを活用したAI・Webアプリケーションを実際にホストしサービスしてみる
- 機械学習の基礎から(tensorflowと機械学習分野の基礎知識)