LLMでコードを生成する懸念点とより良い向き合い方
コードを書くときに理解が進む
Section titled “コードを書くときに理解が進む”どんな作用が起きているのかは知らないけれども、コードを書いているときに、なぜか書いている部分だけではなく書いている範囲よりも広い部分の理解が進む体験をすることがよくある。自分の体験でいえばMackerelのフロントエンド実装はそうだし、diamond も書いているうちに理解が進んだコンポーネントのひとつに入る。
それまでフロントエンドの実装は何度もレビューで見ていたはずだし、実装する前に設計が妥当かどうかの検討はしていたはずだが、実際に書いてみると型のエラーに当たり、修正する過程で「connect は定型句として覚えてしまっていたけど、実際はこうやって動いていたのか」という理解に繋がったり、「モジュール(パッケージ)が蜜結合している」とか「動くけど理屈が通らない」またはシンプルに「使い勝手が悪い」といった状況に陥る。
具体例でいえば、diamond で拡張データポイントを実装していたとき、MetricCache と DiamondStorage の両方に
import { Centroid } from "./metric";
function encodeValue(value: number, count: number, digest: Centroid[]) => Bufferという関数がそれぞれ実装されていた(若干シグネチャは違うが実装は同じ)。扱うデータも同じものなので util.ts にまとめて使おうと思ったところ、util.ts から metric.ts を参照することになってしまった。個人の感覚では util.ts はもっと一般的な関数が集められる場所であって、metric.ts を参照するなら別の名前にすべきではないか?となり、最終的に metric.ts で encodeValue を実装することになった。
他にも、頭の中ではうまく繋がるはずだったパッケージ関数が、実際にドキュメントコメントを書いてみると挙動をうまく説明できないことに気づく場合もある。うまく言えないが、コメントが実装のトレースでしかなくて抽象化した表現にならないというのだろうか。無理して書くこともできるが、こういった場合は設計がまずく使い勝手も悪いので、抽象化できるように仕様を変える。
こちらも実例を挙げると、plug を作っていたときGoで関数が同一かどうか調べるための一般的な方法がないことを知ったのだが、それでもシンプルな使い勝手にしようと何度も関数のインターフェイスを変更して、最終的にplugはなぜ文字列をキーとして扱うのかのように落ち着いたけれど、ここで得たものは実装に限らず色々あった。LLMがある程度動くものを生成してしまっていたら、インターフェイスを途中で妥協したか、そもそも plug を使う嬉しさがなくなって実現自体を諦めてしまっていたと思う。
自分の脳がそういう特性なのかもしれないけれど、著名人も設計だけでは良いフィードバックを得られないと言っているので、希少な特性というわけでもないだろう。
LLMでコード生成すると良い場面
Section titled “LLMでコード生成すると良い場面”思考の道具としてLLMを使う事例では、どういった場面でLLMを使うといいのかを具体的な事例とあわせて書いたが、ではコード生成をいつ行うと問題が起きづらいのか。おそらくだが学びの得られない、もしくは効果の薄い場面、例えば以下のような場面ではコード生成をするといいのだろうと思う。
- モック開発など使い捨てのコードが必要なとき
- コード全体を把握していて間違えようがないとき
- 既存実装を横展開するだけの退屈な作業をするとき
こういった場面では、設計のフィードバックが得られる期待値が低いので、LLMで生成すると時間の節約になる。ただし、上記でモックを挙げたが、新サービスのモックは重要な点がサービスの触り心地なのでコードを手で書く意味はないが、機能開発(PoC)の場合は設計へのフィードバックがあるので、こちらは自分で書いたほうがいいだろう。