LoginSignup
6
5

Local LLM AIを簡単に構築し開発への活用を試す

Posted at

前書き

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環境でも似た感じで構築できます。

image.png

図の①②③については、次のセクションで紹介します。

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 returns n.
  • It initializes three variables: a, b, and result. a and b
    represent the first two terms of the Fibonacci sequence, while result
    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 adding a and
    b.
  • It then updates the values of a and b 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を返します。
  • abresultという3つの変数を使用します。abはフィボナッチ数列の最 初の2項を表し、resultは現在の項を表しabresultという3つの変数を使用します。abはフィボナッチ数列の最初の2項を表し、resultは現在の項を表します 。
  • ループは2からnまで実行し、次の項を計算します。これはabの合計です。
  • abの値を次の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では safetensorsGGUF に変換する方法のガイドを提供しているので、まずは変換を行います。

補足:`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導入方法紹介

手順説明はこちらになります。

変換手順説明

まず、safetensorsGGUF に変換するために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で他のオプションも指定できるようで、詳細は以下のドキュメントで参照できます。

Ollama Model File

ここでは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を選択して質問をすることができます。
基本的な使い方はすごくシンプルです。以下のように質問を投げると回答してくれます。
image.png

また、画像やファイルを添付して質問を行うことも可能です。
(もちろん画像を認識できるModelじゃないと正しい答えをもらえない可能性があります)
image.png

基本的な利用方法はシンプルですが、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のアイコンが追加されます。
image.png

このアイコンをクリックするとContinueのパンネルが表示されます。
下の+を押してモデル追加に進めることができます。
image.png

Ollamaを選択します。
image.png

Autodetectを選択するとollamaセットアップで入れていたモデルが自動で選択可能になります。また、Continueのパン寝るからChat機能ができるようになります。
image.png

Modelを選択して、質問をして回答をもらうことができればOKです。
image.png

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のアイコンをクリックし、利用するモデルが選択されていると、チャット機能を利用することができます。以前の会話から引き続き質問することも可能です。

例 :フィボナッチ数列を質問したあと、引き続き改善を問う

フィボナッチ数列を教えてと質問をし、回答としてコードと一緒に注意点を教えてくれました。
注意点でスタックオーバーフローに対して改善してほしいという質問に対して、改善したコードの例を教えてくれてます。

image.png

また、Continueパンネルの右上のView Historyアイコンを押すとチャットした履歴を見れます。

②コードを選択しChatへ質問※Ctrl(CMD) + L

IDEでコードを選択後、Ctrl(CMD) + Lを押すと、コードがチャットに貼り付けられます。
その後、質問をすることができます。
複数のファイル、コードブロックから貼り付けて一つの質問にできるのが良いポイントです。

例:2つのコード選択範囲をインプットとしその関係を問う

index.tsxとApp.tsx各々でCtrl(CMD) + L でコードをインプットして、質問を書いています。
両方を認識したうえでAIが回答してくれます。

image.png

また、コードを選択しChatへ質問する場合は、基本New Sessionになりますが、下の ← Last Session を押すと、以前の会話で引き続き質問することができます。

例:コード選択情報をNew SessionからLast Sessionに移動

コードを選択しCtrl(CMD) + L を押します。その後、赤枠の← Last Session を押します。

image.png

前の会話履歴の続きに選択したコードの情報がわたり、引き続き質問ができます。

image.png

③IDE上でコードを自動生成・編集※Ctrl(CMD) + I

IDE上でCtrl(CMD) + Iを押して指示をするとAIがIDEに直接コードを書き込んでくれます。
AIが書き込んだ差分をみて、入れたい差分だけを選択して適用することができます。
書き込みたいところにCursorをおいて指示をするとコードが書き込まれます。

例:Cursorの位置にコード生成を指示

image.png

image.png

コードを選択したあと指示をすると、指示によって選択したコードを参照し生成するか、または修正するなどができます。

質問の例

image.png

image.png

④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の部分に✓表示がつきます。

画面確認

image.png

✓を押すと一時的にOFFにすることもできます。

画面確認

image.png

ONの状態だと、コードを書く途中でAIから次に各コードをおすすめしてくれます。

画面確認

image.png

Tabを押すと全体内容を適用します。
Ctrl(CMD) & →をすると1単語づつ適用します。
まだBeta機能らしく、他の提案を出す機能はまだなさそうです。

@[ContextProvider指定]

どこかの公式ドキュメントや、ファイル、ターミナル内容などをインプットとして質問することができます。

例えば、
@Terminalを指定すると、ターミナルで出力されたエラー内容をインプットとし、どういうエラーか聞くことができます。

例:ターミナルのエラーを参照・インプットとし質問

@Terminal を指定し質問することで、VS Codeのターミナル出力内容をダイレクトで参照し、AIが回答をしてくれています。

image.png

@Docsは、事前に登録されている公開ドキュメントをベースに質問をすることができます。

例:React公式ドキュメントをコンテキストとし質問

Docsを選択します。

image.png

事前に登録されたドキュメントから質問のネタにしたいドキュメントを選択し質問します。

image.png

公式ドキュメントの内容を下に回答してくれます。

image.png

機能の詳細は、ContinueのContext Providersドキュメントを参照できます。
https://docs.continue.dev/customization/context-providers

/[コマンド指定]

チャットから特定機能を実行できるショートカットを提供します。

例えば、
/edit/commentはコード修正の質問の回答をIDEに直接渡しDiff結果を表示します。Diff確認後適用することができます。

例:`/edit`を利用し修正を指示、修正内容をIDEにストリーミング

以下のように修正内容をIDEにストリーミングしました。

image.png

機能の詳細は、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 モジュールを有効化してください。

  1. 現在のディレクトリで go mod init myapp コマンドを実行します。
  2. 必要なライブラリを 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/:idAPI追加

これからは、簡単な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()関数の生成を指示しました。

AIへ質問:これでGetDatabase()関数を作成して

※一部コードをサンプルとし、新しい関数を生成
■AIへ指示する画面
image.png

■AIからの結果
image.png

簡単ではありますが、意図通りのコードを作成してくれました。
あとは、ユーザー生成APIのHandler部分の実装を指示します。
今回は色々要件があるので、まず指示内容をIDE上のCommentで書いたあと、Continueのコード編集(Ctrl + I)でコード生成を指示します。
指示内容はこちらです。

// func CreateUserHandlerを作って
// nameをフォームデータでもらう
// db.GetDatabase()を呼び出して。sqlite3使って
// usersテーブルにinsertして
// idとnameをjsonでレスポンスして
質問:コード作成して

※コメントから、ユーザー情報追加・更新の行うコード生成
■AIへ指示する画面
image.png

■AIからの結果
image.png

GetDatabase()呼び出し部分は、AIではreturn typeが不明瞭だったせいか、エラーのあるコードですが、それ以外は意図通りに動くコードを生成してくれたうえで、色々バリデーションも作成してくれました。
GetDatabase()呼び出し部分はAIに再度指示して修正してもらいます。

AIへ質問:db, errで修正して ※db := db.GetDatabase() 部分を選択し指示

■AIへ指示する画面
image.png

■AIからの結果
image.png

正常に実行できるようになり、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)することで、ファイル内の全体コードを参照した上で、必要な部分を適切に修正してくれることを願って指示します。

AIへ質問:todo部分を修正して

※GetUserHanlderをsqlite DBから取得するように修正

■AIへ指示する画面
image.png

■AIからの結果
image.png

色々差分が出てますが、最終的には意図通りに動作する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からの結果
image.png

■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
}
AIへ質問:gormに差し替えて

※users.goのコードを選択しContinueのコード編集(Ctrl + I)で指示

■AIからの結果
image.png

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

■AIからの結果
image.png

これで動作する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へ指示する画面
image.png

■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へ指示する画面
image.png

■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
Swagger Viewer結果

image.png

/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のtitleとversionを書いて

■AIからの結果
image.png

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

image.png

やはり特定のライブラリに対しての指示は、バージョンが古い情報をベースにした回答や一部情報が欠けられた回答が発生することもあると感じます。

しかし、多くの部分の作成を補ってくれるところは、引き続き便利なところと感じます。

④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.

■AIへ指示する画面
image.png

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

image.png

少し間違う場合がありますが、ちょっとした修正で使えるくらいには作ってくれる感じです。

少し学習データが足りない場合でも、比較的に簡単な課題は、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 を使用してメール配信タスクをエンキューし、そのタスクを処理するサーバーを作成します。

コードの機能:

  1. タスクエンキュー:
    • メール配信タスクの構造体(EmailDeliveryPayload)を作成し、その内容をエンキューします。
    • メール配信タスクには、受取人のメールアドレス、件名、本文が含まれます。
  2. タスクハンドラ:
    • メール配信タスクを処理するハンドラ(handleEmailDeliveryTask)を作成し、タスクのペイロードをアンマーシャルし、メールを送信します。
  3. サーバー実行:
    • 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へ指示する画面

image.png

■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>&copy; 2023 Your Company</p>
      </Footer>
    </Container>
  );
};

export default App;

なんかStyle込みのReactを書いてくれました。
Imageファイルは適当に表示可能なURLに変えてブラウザで確認します。

ブラウザ確認

image.png

やはりここは満足にいく水準にはいかない感覚ですね。

しかしながら、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>&copy; 2023 Your Company</p>
      </footer>
    </div>
  );
};

export default App;

大きな齟齬なくTailwindCSSに差し替えてくれました。

フロントコードにおいても、Inline CSSやbootstrapなどをTailwindCSSなどの別ライブラリに差し替える際に色々補助が可能と思います。

③Component分離リファクタリング

ここでは、Header, Main, Footerが一つのコンポーネントになっているところを、どういう風に別ファイルに分けられるかを聞いて、良いヒントになり回答をもらうことができます。

AIへ質問:コンポーネントを分けてcomponents/に別のファイルで置きたい。

■AIへ指示する画面
image.png

■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>&copy; 2023 Your Company</p>
    </footer>
  );
};

export default Footer;

また、コンポーネント内容ボタンを、別のコンポーネントで抽出して、色んなカラーに対応できるようにリファクタリングすることもAIに指示して、意図通りに動作するコードの回答をもらいました。

AIへ質問:buttonは別コンポーネントに分けて別のファイルにしたい。textはpropsにして。colorはpropsにして、red, green, blueの3つに制限して。

■AIへ指示する画面
image.png

■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書いて。サンプルはこれよ。

■AIへ指示する画面
image.png

■貼ったサンプルコード

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へ指示する画面 (LastSessionで指示)

■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へ指示する画面 (LastSessionで指示)

■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へ指示する画面 (LastSessionで指示)

■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)

■Button.stories.tsx - Secondary

■Header.stories.tsx

■Main.stories.tsx

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と機械学習分野の基礎知識)
6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5