🥷

LLM プログラミングのコツ (2025年07月版)

に公開

LLM プログラミングのコツ (2025年07月版)

AI Agent にコード実装を任せるというのが、すっかり現代のプログラミングスタイルになりました。とはいえ、良質なコードを書いてくれることばかりではありません。 AI Agent が超知識豊富なのは間違いありませが、トレーニングされてない暴れ馬 のような側面もあり、うまく手綱を握らないと、どっちへ走ってゆくか分からなかったりします。うまく LLM を使いこなすには、調教師や騎手のような役割をエンジニアが担うことになるわけです。


はじめに

この記事は、主に Python(ruff, mypy, pytest など)の環境で LLM を使ったコーディング実践の中で気付いたノウハウですが、プログラミング言語を問わず役立つもののはずです。

また、筆者は Cursor, Warp, Devin を使ってますが、 LLM の機械的特性から考えて、おそらくどの AI ツールを使っても、このノウハウは変わらないでしょう。


1. Agent が自ら判断できる環境を作る

もし、テストコードがあれば「テストをパスする実装をお願いします。」で、大抵の場合、テストをパスするまで自動で実装してくれるのは、多くの人が経験してると思います。これは、テストをパスするという基準が明確なので、 Agent は自分で判断して、作業を終えるか継続するかを判断できるからです。

しかし、書かれたコードが良質とは限りません (チームのコーディングルールを守らないなど) 。巨大な関数、巨大なファイルを作ることは普通に起こりますし、激しいネストも入れ込みます。プロンプトで頑張っても、どうするかは LLM やコンテキストによって、かなり揺れます。

そこで導入するのが静的解析ツールです。もともと使っている人も多いと思いますが、そのルールを厳しく設定するのです。例えば、関数は 20 行以内で、複雑度は 5 以内で、関数の引数は 3 つ以内のようなルールを設定すると、Agent はそれを満たすまで、大抵の場合は自動的に直してくれます (ignore コメントで終わったり、ギブアップすることもあります)。ルールが甘かった時と比べて、かなり人がメンテできる範囲内のコードになります。プロンプトで頑張るよりも大幅に精度が高いです。

ruff, mypy はルールが多くて理解して設定するのはなかなか大変です。そそも静的解析が興味の対象であるというエンジニアが少ないと思います (筆者も苦手です)。「標準的な ruff, mypy の設定を日本語コメント付きで作ってください。」のようにすれば、 LLM がルールを作ってくれるので、日本語のコメントを見ながらオンオフして、調整することができます。

なお、新規プロジェクトではなく、膨大なコードが既にある場合のルール設定のやり方については、この後の "既存のコードのせいで厳しい制約を設定できない場合のやり方" で説明します。

他に筆者はカバレッジも使ってます。閾値を 80% に設定しておけば、それを満たすまでテストコードを充足してくれます。ただ、初期の頃は、モックを使ったテストコードを量産されて、人が把握するのが難しくなり、カバレッジだけ高くなっても意味のあるテストなのかどうか悩ましい感じになりました。また、カバレッジに関しては、わりとギブアップしがちな傾向もありました。プロダクトコードのテスタビリティが低いと、テストを通すための複雑な準備コードを作り切れない様子でした。行数や複雑度の制限をかけてからは、ギブアップすることなく、高いカバレッジを維持できています。

  • テストを充足させる
  • 静的解析ツールのルールを細かく設定する
  • カバレッジの閾値を設定する

2. LLM が予想しやすい情報を提供する

"昨日はとても暑かったし、たぶん" という文に続く次の単語は次のどれでしょう?

  1. カレーライス
  2. 今日も
  3. 宇宙エレベーター

特に正解があるわけではありませんし、どれでもその先の文を作れますが、 "2. 今日も" が自然な感じで、多くの人が選ぶ確率が高そうです。

では、"そろそろ夕食の買い物に行く時間だわ。昨日はとても暑かったし、たぶん" だとどうでしょ? 選択肢は先ほどと同じです。

これも特に正解があるわけではありませし、どれでもその先の文を作れます。でも、なんとなく "1. カレーライス" が合いそうな感じがしませんか?

LLM と自然な会話をしていると、思考力があるかのように錯覚してしまうことも少なくありませんが、その正体は次のトークン (≒単語) を予想し続けるオートコンプリートマシーンです。次のトークンの予想精度が上がるような情報提供をすることで、質を上げることができます。

逆もあります。

"そろそろ夕食の買い物に行く時間だわ。そういえば、政府が言ってた宇宙開発事業ってどうなったかしら? まあ、昨日はとても暑かったし、たぶん"

こうなると、ここまでは突飛な感じだった "3. 宇宙エレベーター" もなくはない…って感じではないでしょうか。

LLM は、常に最も確率の高いトークンを出力するのではなく、わざとある程度のランダム性を持たせているそうです。それが会話らしさを生み出しているわけですが、同じプロンプトでも同じ結果にならない理由でもあります。

さて、LLM に "カレーライス" を出力させたいという目的があったとします。ここまでを振り返ってみると、"夕食の買い物" はそこに近付きましたが、"宇宙開発事業" はどちらかというと混乱させる情報だと言えるのではないでしょうか。

ちょっと抽象的な話ではありましたが、まとめておきます。

  • LLM が予想しやすい情報を提供する
  • LLM が予想しにくくなる情報は提供しない

3. LLM が作った作業計画も LLM のコンテキストになる

「xxx を作ってください。」
「xxx を作ってください。ただし、作る前に、作業計画を作ってから進めてください。」

この二つでは、後者の方が良い結果になる傾向が高いです。LLM が作った作業計画そのものもコンテキストに含まれるので、作業単位が小さくなるからです。常に良い計画を立ててくれるわけではなく、後半にまとめて問題解決する計画も多いので、「xxx を作ってください。」と大差ないケースもありますが、それでも計画に従って動いてくれるので、何をやってるのかを把握しやすいですし、途中で止めることもできます。

最近のバージョンアップで Cursor に TODO 機能が付き、自動で TODO が出るようになりました。この Tips が不要になるかと思ったのですが、意外とそうでもないようです。プロンプトで明示すると、 LLM が作った作業計画の一つをさらに細分化した TODO を作って進むようです。

ツールの TODO がどのように生成されてるのかは分かりませんが、プロンプトなら情報を付与して自分好みの作業計画を作ることができます。「grep で TODO コメントを検索し、ファイルを一つずつ直す作業計画を作ってください。」「lint で出ているエラーを把握して、重要なものから直す作業計画を作ってください。」など。

  • 作業計画を LLM に作ってもらう
  • どのように作業を進めたいかを表明して、作業計画を作ってもらう

4. まとめ作業の禁止、 DoD を含むプロンプト

「作業計画を一つずつ進めてください。一つの作業の最後は、必ず ut と lint を実施してください。 ut と lint が通ったら、次の作業に進んでください。」

こんなプロンプトを入れています。なぜ、こんなプロンプトを入れてるかと言うと、 LLM が学習してる素材はネットに数多ある結果素材だからです。その結果に至るまでに、どんな考えを元に、どんな手順を踏んで、どんな失敗があったかといったあたりは、 LLM が学習する素材として圧倒的に少ないと思われます。ということは、積極的に人が介在してコントロールしなければ、効率が上がらないレイヤーだということになります。

実際、効果は高い印象でした。一つのタスクごとにコード品質のチェックをするようにしておくと、その過程を LLM が学習するようで、四つめぐらいの作業から lint エラーが出ないコードを最初から書くようになります (類似性があれば)。ただし、この効果は、作業計画の質が前提になります。先に全てのコードを直してから進むような作業計画だと、フィードバックが後半に集まるので、 LLM にとっては直す作業のパターンを学習することはあっても、そもそも直さなくていい良質なコードを書く学習はしません (書き終わってるし…)。

以前はこれを .cursor/rules に書いていたのですが、適用されないことも多くて試行錯誤した結果、どうやらプロンプトだと 100% 実施されるようです。できるだけ簡単なプロンプトで進めたいという思いで、ルールの充足を頑張ってたのですが、動きを見ているとプロンプトに書かれたことの方がかなり優先順位が高く扱われるようです。

  • 一つずつ進めてもらう
  • 一つずつフェードバック効果があるような作業計画が大切

5. テンプレートや例示を含める

「テスト関数名は日本語にして、必ず型を指定するようにしてください。末尾は "のテスト" のような冗長な説明は不要です。何のテストか分かりやすいように Gherkin 方式のコメントを付けて、コードを分てください。」のように言葉で説明して頑張ってました。そこそこうまく行くのですが、突然英語になったり、我流になることもちょくちょく発生していました。そこで例示やテンプレートによる方法に変えたところ、こちらの方が安定して動作するようです。気のせいかコードの質もいいような印象です。LLM のプロンプトエンジニアリング によると、 few-shot というテクニックだそうです。

テスト関数名は日本語にしてください。
例:
def test_新規登録ボタンをクリックするとモーダルが開く(self) -> None:
    # Given
    # When
    # Then
テスト関数は次のテンプレートに従ってください。
---
def test_{日本語の名称}(self) -> None:
    # Given
    {準備}
    # When
    {テスト対象の実行}
    # Then
    {結果の確認}

言葉で説明するのは、長くなると認知負荷がかかり、効果的な説明文を考えるのもなかなか大変になるので、例示やテンプレートが LLM にとって効果的なら、積極的に使った方が良さそうです。

  • 例、もしくはテンプレートを入れる
  • "類似しているので test_hogehoge.py を参考にしてください。" も有効

6. こまめにコミットして、いつでも巻き戻せるようにする

こんな工夫をしても、気付けば途中までうまくいってたのに、後半はハルシネーションがひどい…みたいになってしまうこともあります。

  • とにかくこまめに git commit (LLM に任せるのは常にクリーンな状態)
  • git commit --amend --no-edit で意味のある単位になるように調整

蛇足ながら、この辺りの操作を人がやるなら jj (jujutsu) だと楽です。綺麗なコミットを積み重ねてゆくなら jujutsu が便利です。ただ LLM の学習量は圧倒的に git なので、難しい git 操作も日本語で頼むと LLM がやってくれます。ツールの良し悪しより、 LLM ファーストの方が大事かもなぁ〜と、 jujutsu ユーザーとしては悩ましく経過観察してます。

  • LLM に作業を頼むなら、commit 後のクリーンなワークスペースで
  • 意味のある単位でのコミット作りは amend 活用

7. 既存のコードのせいで厳しい制約を設定できない場合のやり方

最初に、静的解析ツールの設定を厳しくて、長かったり複雑だったりするコードを生成させないようにする Tips を書きました。しかし、新規プロジェクトならともかく、既に大量のコードが存在する場合、既存コードが邪魔して制約を入れるのが難しかったりします。その場合、静的解析ツールの設定を甘くして、その後、徐々に厳しくするという戦略を取ります。

ただ、なかなか手間がかかります。「既存のコードでエラーが出なくなるように、ruff, mypy のルールを変更してください。」とやると、割と大きな単位でルールを削ったり、ディレクトリ単位で無効化したりするので、ゆるゆるなルールになります。そうなると、既存の一部のコードのせいで、そのコードと同程度のよろしくないコードを LLM が生成できてしまいます。

なので、できるだけ例外規定が特定のファイルの特定のルールだけになるような設定をする必要があります。うまくやる方法は見つけられず、ハルシネーションが起こりやすい部分なので、結構地道にやることになると思います (地道にやりました)。LLM が生成するコードに制限をつけることが目的なので、特殊処理 (config や log, db session など、機能追加時に LLM が参照しないもの) の例外ルールはそのままで構わないでしょう。ディレクトリ単位で妥当な例外ルール、ファイル単位で妥当な例外ルールに絞り込んでゆきます。簡単なリファクタリングで例外規定を消せるなら、リファクタリングした方がいいでしょう。

そして本丸。例外設定ができないルールとの戦いです。LLM が生成するコードに厳しい制約を課すためには、既存コードのリファクタリングが伴います。複雑度の高いコード、ネストの深いコード、やたら長いコードは制限したいわけです。

複雑度を 15, 14, 13, 12 …と徐々に下げて、LLM にエラーの修正を依頼して行きました (ってか、最中です)。目標値を定めて一度にやるのは、あまりお勧めしません。私が試したところ、途中で「現実的な数値ではありません。」と挫折されました。大量の設計変更のコードレビューは正直無理でした。そして、テストは通るけれども動かない状態になってしまい、原因調査が大変でした。小さな個人の実験プロジェクトでこれですので、本番稼働している仕事のシステムで実施するのは、おそらく大冒険になるでしょう。

  • まずは既存コードに合わせた制約を作る
  • 発展可能な制約になるようにオプティマイズする
  • 現実と向き合いながら、徐々に厳しい制約に発展させてゆく

まとめ

LLM を活用したプログラミングで成果を最大化するには、次のポイントが大切です。

  • LLMは「意味」を理解しているわけではなく、文脈から次の単語を予測するマシーン。だからこそ、欲しい出力に近づくような文脈や情報を意識的に与えることが重要です。
  • 検証環境(ruff, mypy, pre-commitなど)を最初に整え、ルールを明確にすることで、LLM が自律的に品質を担保しやすくなります。既存コードが多い場合は、最初は緩いルールから始め、段階的に厳しくしていくのが現実的です。
  • 作業計画や TODO を LLM 自身に作ってもらうことで、作業単位が小さくなり、途中経過や品質チェックがしやすくなります。
  • 各作業ごとにテスト & Lint を実施し、即座にフィードバックを得ることで、LLM がより良いコードを出力するように「学習」していきます。
  • 具体的な例やテンプレート(few-shot プロンプティング)を活用することで、LLM の出力の安定性と品質が向上します。
  • こまめなコミットと巻き戻しの仕組み(git/jj)を活用し、失敗のリスクを最小化しましょう。LLM の出力が迷走しても、すぐにやり直せる安心感が生産性を高めます。

LLMは、適切な文脈・検証環境・分割と即検証・段階的なルール強化・テンプレート活用・巻き戻しの安全弁が揃えば、ただの予測マシーンから「頼れる開発パートナー」へと進化します。

参考資料

Discussion