コード開発にLLMを活用する(ツール編)

# はじめに

生成AIブームの始まりから1年あまりが過ぎ、RAGを中心にしたLLMの応用技術やサービスが普及しつつあります。以下は最近の一例です。

https://xtech.nikkei.com/atcl/nxt/news/24/01086/

ですが、LLMのユースケースはRAGで行うようなFAQ対応だけではありません。

コード生成やデバッグにもLLMは使えます。

「もう普通にやってるよ！」という方もおられそうな一方で、二の足を踏んでいる方もおられるので、布教のためにこの記事を書きました。

# LLMによるコード生成・デバッグの例

今年2月頃からしばらく、自宅の押入れ・エアコンがあるお茶の間で、羽音のような「ブーン」という謎の音が毎日数回ずつ鳴りだすようになりました。音のリンクは以下です。

https://drive.google.com/file/d/1Fji-HqEXg8rx7XkOs_6DcPqy5JMqfpFJ/view?usp=sharing

音の特徴を掴むべく、録音した音のデータをフーリエ解析するプログラムをChatGPT(GPT-4)を使って作成しました。そのときのChatGPTとのやり取りのログは以下です。

- 基本機能の作成とそのデバッグ
  
https://chatgpt.com/share/e08f5cf9-8f95-4313-adde-aa07ddd1c2c2

- 機能追加とそのデバッグ

https://chatgpt.com/share/0d0e21c6-f9f6-41e3-b028-d5bbfcdf6cd4

最終的にChatGPTが作ったプログラムは以下のとおりです。(一部、作られたコードを微修正しています)

```python
import librosa
import matplotlib.pyplot as plt
import numpy as np
import soundfile as sf


def save_plot_as_png(
    freq, power_spectrum, freq_min, freq_max, file_name="power_spectrum.png"
):
    """
    パワースペクトルをPNG形式で保存します。
    """
    plot_power_spectrum(
        freq, power_spectrum, freq_min, freq_max
    )  # 既存のプロット関数を使用
    plt.savefig(file_name)  # グラフをPNGファイルとして保存
    plt.close()  # 現在のフィギュアをクリア


def synthesize_audio_from_frequencies(y, sr, power_spectrum, threshold):
    """
    閾値以上のパワースペクトルの値の周波数のみを使用して音声を合成し、保存します。
    """
    # 閾値以上のパワースペクトルのインデックスを取得
    significant_freq_indices = np.where(power_spectrum >= threshold)[0]

    # 元のFFT結果をゼロで初期化
    Y_filtered = np.zeros_like(y, dtype=np.complex128)

    # 閾値以上の周波数成分のみを保持
    for i in significant_freq_indices:
        Y_filtered[i] = np.fft.fft(y)[i]
        if i != 0:  # DC成分を除外
            Y_filtered[-i] = np.fft.fft(y)[-i]  # 対称性を保つ

    # 逆FFTを使用して時間領域の信号に変換
    y_filtered = np.fft.ifft(Y_filtered).real

    return y_filtered, sr


def load_audio_section(file_path, start_sec=None, end_sec=None, sr=None):
    """
    音声ファイルを読み込み、指定された区間の音声データを返します。
    """
    y, sr = librosa.load(file_path, sr=sr, mono=True)
    if start_sec is not None:
        start_sample = int(start_sec * sr)
    else:
        start_sample = 0

    if end_sec is not None:
        end_sample = int(end_sec * sr)
    else:
        end_sample = len(y)

    return y[start_sample:end_sample], sr


def compute_power_spectrum(y, sr):
    """
    音声データのパワースペクトルを計算します。
    """
    Y = np.fft.fft(y)
    freq = np.fft.fftfreq(len(y), 1 / sr)
    power_spectrum = np.abs(Y) ** 2
    return freq[: len(freq) // 2], power_spectrum[: len(freq) // 2]


def plot_power_spectrum(freq, power_spectrum, freq_min=None, freq_max=None):
    """
    パワースペクトルをプロットします。freq_minとfreq_maxで周波数範囲を絞り込みます。
    """
    plt.figure(figsize=(12, 6))
    if freq_min is None and freq_max is None:
        plt.plot(freq, power_spectrum)
    else:
        if freq_min is None:
            freq_min = min(freq)
        if freq_max is None:
            freq_max = max(freq)
        mask = (freq >= freq_min) & (freq <= freq_max)
        plt.plot(freq[mask], power_spectrum[mask])
    plt.xlabel("Frequency (Hz)")
    plt.ylabel("Power")
    plt.title("Power Spectrum")
    plt.xlim(
        freq_min if freq_min is not None else min(freq),
        freq_max if freq_max is not None else max(freq),
    )


def save_frequencies_above_threshold(freq, power_spectrum, threshold, output_file):
    """
    パワースペクトルが一定の閾値以上の周波数をファイルに保存します。
    """
    with open(output_file, "w") as file_obj:
        file_obj.write("Frequency (Hz), Power\n")
        for f, p in zip(freq, power_spectrum):
            if p >= threshold:
                file_obj.write(f"{f}, {p}\n")


def plot_power_spectrum_from_mp3(
    file_path,
    start_sec=None,
    end_sec=None,
    freq_min=None,
    freq_max=None,
    threshold=0,
    debug_output_file="debug.txt",
    graph_png_file="graph.png",
    sr=None,
):
    """
    MP3ファイルから指定された区間のパワースペクトルを計算し、プロットします。
    閾値を超える周波数とパワースペクトルの値をファイルに出力するオプションを追加します。
    """
    y, sr = load_audio_section(file_path, start_sec, end_sec, sr)
    freq, power_spectrum = compute_power_spectrum(y, sr)
    plot_power_spectrum(freq, power_spectrum, freq_min, freq_max)

    # パワースペクトルが閾値以上のデータをファイルに出力
    if threshold is not None and debug_output_file is not None:
        save_frequencies_above_threshold(
            freq, power_spectrum, threshold, debug_output_file
        )

    # グラフをPNGとして保存
    save_plot_as_png(freq, power_spectrum, freq_min, freq_max, graph_png_file)

    # 音声合成と保存
    y_filtered, sr_filtered = synthesize_audio_from_frequencies(
        y, sr, power_spectrum, threshold
    )
    # soundfileライブラリを使用して保存
    sf.write("filtered_audio.wav", y_filtered, sr_filtered)


if __name__ == "__main__":
    # 実行例
    # plot_power_spectrum_from_mp3('path_to_your_mp3_file.mp3', start_sec=5, end_sec=15, freq_min=0, freq_max=5000, threshold=1e7, output_file='output_frequencies.txt')
    plot_power_spectrum_from_mp3(
        "共振音データ_5.mp3",
        freq_min=0,
        freq_max=1000,
        threshold=0.5e5,
    )
```

これを実行すると以下のグラフが得られました。パワースペクトルが等間隔に並んでいるのがわかります。

![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/485e1e02-e0a1-0a30-45d8-bcafaf7b44c3.png)

このグラフや音のファイル名などでお気づきの人もいると思いますが、パワースペクトルが強く出ている周波数は、100数十Hzほどの周波数の音とその倍音(ハーモニクス)に対応していると思われます。なにかが共振してこの周波数の音が鳴っていそうです。

何に共振してこの音が鳴っているかを探るべく、音が鳴る時間を毎日プロットしました。その結果、我が家の洗濯機を使ったときに音が強く鳴りがちだとわかりました。

自室の水道メンテやリフォームを担当された業者様に「共振では」という話を告げて相談したところ、排水管を下水につなげているエアコンから音が鳴っていることを特定できました。更に調べると排水管の逆止弁が完全に閉じきれずにできた隙間があり、洗濯機の脱水で大量に水が流れると、行き場をなくした下水の空気が逆止弁の隙間から漏れて音を鳴らしていることがわかりました。

そこで逆止弁をぴっちり閉じるための措置をしたところ、音は鳴らなくなりました。

# LLMを使ったコード開発に有用なツール

前章のプログラムがあっさり作れて問題解決もなされたことにすっかり私は味を占めてしまいました。そこからというもの、コード開発に使えるLLMベースのツールの選定や、そのノウハウ蓄積に勤しんできました。

本記事では私が厳選したツールの紹介・使い方だけを説明します。それを使ったノウハウは稿を改めて説明します。

## AnythingLLM

AnythingLLMはWeb検索などの付加機能付きのRAGを簡単に作れるアプリのひとつです。

https://github.com/Mintplex-Labs/anything-llm

AnythingLLMではRAGに使う埋め込みモデルやLLMを好きに選ぶことができます。Ollamaが使える環境であれば、Ollamaにあらかじめ好きなオープンLLMを登録しておいて、それをAnythingLLMから呼び出して使うこともできます。

![Ollamaのぶ代.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/f38bdf88-d74b-f857-e9a2-ee5f11038e82.png)

## 環境構築手順

以下の記事に従うのが簡単です。Ollamaと同時起動する手順にも言及があります。

https://qiita.com/orca13/items/d84a2c398aff2334c69c

デフォルトではAnythingLLMへの通信はhttpを使いますが、httpsを使う設定にすることもできます。本記事では具体的な実現手順については触れませんが、httpsを使うようにするとChromeブラウザから質問文を音声入力できるようになります。自宅サーバにAnythingLLMを構築して、タブレットやスマホから指示を出すにはこれが便利です。

## 使い方

デフォルト設定なら、<http:localhost:3001>にブラウザからアクセスして使います。

### GitHubのリポジトリを読み込んでRAGをする

ベクトルストア構築用の便利機能もいくつかあります。たとえばGitHubのリポジトリ内のテキストファイルを纏めてベクトルストアに投入できるものがあります。社内外のGitHubリポジトリの内容を理解するのに便利です。

![github読み込み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/b701e1f5-afeb-c6a4-318e-d2afaa52f7e8.png)

実際に私が個人で作ってGitHubに置いているプログラムがあります。これはGit系のリポジトリにプルリクエストが上がったときに、そのコードレビューを自動実行するものです。

https://github.com/kzkymn/code_review_request_inspector

これをAnythingLLMに読み込ませます。

![gitHub読み込み_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/189f0666-644f-fdd9-cfff-6404dc670a0d.png)

質問してみました。これは作者の私の認識と同じです。
画面内に青字で書かれているのは、根拠文献へのリンクです。

![githubでrag.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/8958f933-7670-1d5c-4413-ae19994ae706.png)

ダメ出しもしてくれます。

![github_ダメ出し.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/fbc762ea-81e7-70a1-2034-d581c1efccd1.png)

### コード開発への適用例

AnythingLLMを使ったコード開発の例として、次元削減アルゴリズムのFIt-SNEを作者ご自身が実装したコードをGitHubから読み込んで、それを利用するユーザプロンプトを作成した事例を紹介します。FIt-SNEのリポジトリは以下です[^2]。

https://github.com/KlugerLab/FIt-SNE

[^2]: 実際はFit-SNEリポジトリを使わず、OpenTSNEライブラリに実装されたFIt-SNEを使うケースが普通だと思います。

この中のソースを先のGitHubのソース読み取り機能でベクトルストアに格納します。

![github読み込み_fitsne.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/bf64e0da-e549-6f14-2951-af0600090cbc.png)

格納したあとに、Fit-SNEの簡単なサンプルコードの例を示すよう指示します。

![fit_sne_原典のコードの動かし方.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/8681b63e-3cbb-0b2c-090d-67604b642b5a.png)

結果として、[リポジトリ自体にサンプルコードがある](https://github.com/KlugerLab/FIt-SNE/blob/master/examples/test.ipynb)ことを伝えてくれたうえで、簡易的なサンプルコードも自作してくれました。

```python
import sys
sys.path.append('../FIt-SNE/')
from fast_tsne import fast_tsne
import numpy as np

# ランダムデータを生成
X = np.random.randn(1000, 50)

# FIt-SNEを実行
Z = fast_tsne(X)

print(Z)
```

リポジトリにあるサンプルコードは作成されたコードと同様、`numpy`の2次元の`ndarray`として与えた入力を`fast_tsne()`関数で処理する内容になっており、作成されたコードもそれを適切に踏襲した正しいコードになっていることがわかります。

https://github.com/KlugerLab/FIt-SNE/blob/master/examples/test.ipynb

### 【おまけ】エージェント機能によるWeb検索

AnythingLLMにはデフォルトでエージェント機能が用意されており、たとえばWeb検索を動的実行して回答するRAGができます。

あらかじめAnythingLLMのエージェント機能の設定でGoogle Search APIのアクセス情報を書いておくと、それを使って検索してくれるようになります。他の検索エンジンの一部もサポートがあります。

![a_llm_Web検索.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/94943999-f2a6-92a0-34c5-375d4ed9e9ae.png)

エージェントを実行するには、入力窓の@ボタンを押し、その後出てくる黒い枠(下図上部)をクリックします。

![エージェント起動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/210c6ec2-c195-6028-fdfd-2dc2b65f3cbf.png)

そうすると入力窓に"@agent"という文字列が表示されます。

![エージェント設定済み.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/f124e40d-fa21-dcd9-ebbc-e1a398a20190.png)

これに続けて質問文を入力すると、Web検索して得た情報をパースして回答を出してくれます。ただし時折パースに時間とLLMのトークンをかけてしまい、お金がどんどん溶けていくことがあるのに注意が必要です。

![a_llm_Web検索_結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/bd61a8f9-74c2-d247-00af-9b99753db469.png)

# OpenDevin

OpenDevinは、LLMによるソフトウェア自動生成やその実行環境構築に特化したツールです。単にコードを指示通りに自動生成するだけでなく、コード実行用のサンドボックス環境をDockerで構築し、その上にPython環境を構築してくれます。

https://zenn.dev/acntechjp/articles/d1fc09306cf08f


ただし、こちらがコード仕様を説明してからOpenDevinが仕様通りのコードをワンパス動くまでの間には、概ね数十万トークンの入出力が消費されます[^1]。GPT-4oを使っていてもワンパス2ドル位かかります。

[^1]: 何にそこまでトークンを費やしているかは未調査です。おそらくはDockerやPythonの環境構築時のログや、不具合発生時に関連するコードやstacktraceを頻繁にLLMに投げて回答を得ているのではと思います。

## 環境構築手順

日本語文献ですと、下記の記事の手順が一番わかりやすいです。

https://zenn.dev/acntechjp/articles/d1fc09306cf08f

## 使い方

デフォルトでは<http:localhost:3000>にアクセスして使います。Difyが同じポート3000を使うので注意が必要です。Difyと併用したい方はOpenDevinのDockerを実行するときにポートの指定を変えてお使いください。

OpenDevinのページにアクセスしてしばらくすると、下図のメインページ左上のチャット欄にLLMからのメッセージが表示されます。ここからユーザ指示を受け付けられるようになります。

![OpenDevin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/e29efc54-85e6-4499-b7cd-3106c1df372d.png)

試しにチャット欄から「1+1=2を実行するC++のコードとその実行環境を作って」と指示してみると、チャット欄にどんどんLLMからのメッセージが表示され、最後に画面右下のターミナルでC++のコードが自動実行されます。

![OpenDevin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/f09874f8-7096-ed6d-d837-953a025d106d.png)

OpenDevinが作ったコードはブラウザ上で編集して実行できますが、えてして編集の反映に失敗することがあります。

![OpenDevin_edit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/5c58a5ba-927f-6ebd-c11c-7b894733ab73.png)

OpenDevinが作成したファイル自体は、そのDockerコンテナを構築したときにワークスペースとして指定したディレクトリにあります。

例えばLinux環境の`~/OpenDevin/workspace`を指定していた場合、その直下にファイルがあります。直接viを使って編集してみます。`1 + 1`を`1 + 2`に変えてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/9c9ede5d-aa6e-e334-8539-879b45db7d96.png)

OpenDevinの画面をリロードすると、前のセッションを続けるか、新しいセッションを始めるか聞かれます。新しいセッションは実質画面のクリアに相当する機能です。なぜなら、新しいセッションにしてもワークスペース内のファイルは消えず、過去のセッションでのチャット履歴だけが消えるからです。

![OpenDevin_session.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/ee37c9c0-9285-f025-9151-02f6b1f7b903.png)


先ほど編集したC++のコードをコンパイルして実行する指示を出してみると、以下のように編集が反映されて`1 + 2 = 3`と出力されるようになりました。

![OpenDevin_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/b9c086f3-cce0-e995-1f20-a2abed34ce68.png)

### 応用例

下記の記事は、t-SNEやそれに類したアルゴリズムの処理速度を比較したものですが、その際の検証に使ったコードやrequirements.txtはOpenDevinで作った環境とコードをもとに作りました。

https://qiita.com/kzkymn/private/d32461e9d0b0f19ec5cf

ではなぜそのときの記録がないか、ですが、**セッションをリセットして消してしまったからです…** 
過去のセッションを保存できさえすればかなり使い良いツールなのですが。

# 今回紹介したツールを使うときの注意点

エージェント機能がハマりこむと、どんどんトークンが消費されていくということが一番の注意点です。

AnythingLLMのエージェント機能を有効にして問い合わせをしたり、OpenDevinがエージェント的な振る舞いで環境構築をしていく裏では、べらぼうにLLMが呼び出されています。

本記事を書くための動作確認だけで、今月(2024年7月)の5日目にしてGPT-4oの入出力は400万トークンを超えました。当然、数千円オーダの課金をしています。

![トークンが費やされていく.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268034/3e3d7da4-a9b6-41b2-281f-6ccc8f2468f5.png)

個人で覚悟して使う分には良いですし、クオータをかければなおよいのですが、会社でこれらのツールを使う場合には相当気を配る必要があります。さもなくば、気づいたらエージェントを動かすだけで月額数十万に達してしまいかねません。

AnythingLLMでエージェント機能からのレスポンスが遅いときはチャット欄に`/exit`と入力するとどこかのタイミングでエージェントが止まります。それでも反応が悪ければ、AnythingLLMのプロセスやコンテナを止めてしまうのが一番確実です。

OpenDevinの場合は、最初の環境構築とコードのワンパスを通すところまで使うのがベストだと思います。そこを人間がやるには面倒で工数もかかるので、多少金食い虫でもOpenDevinを使ったほうが楽です。
そこから先は手でコードを改修するか、AnythingLLMでエージェント機能を使わずに改修・デバッグの指示を投げて、改修後のコード実行は人間の手で行うのが無難だと思います。

# 【おまけ】画像などのマルチモーダル対応はできないの？

AnythingLLMやOpenDevinで画像などのマルチモーダル対応はできないの？と思った方もおられると思いますが、まだできません。少なくともAnythingLLMではマルチモーダル対応の要望が24年1月にissueとして挙がっていますが、実装はいつになるだろうか…というところです。

https://github.com/Mintplex-Labs/anything-llm/issues/558

マルチモーダル対応ができる類似ツールにOpen WebUIがあります。下記のリンク先にあるdockerのコマンドを叩けば、すぐにインストール・実行できます。

https://github.com/open-webui/open-webui?tab=readme-ov-file#installing-open-webui-with-bundled-ollama-support

ただしAnythingLLMとは異なり、RAGに使うベクトルストアをスレッドごとに分けられなかったり、エージェント機能がないというデメリットもあります。Open WebUIとAnythingLLMを同じ環境にどちらもインストールして使い分けられるようにするのがベストだと思います。