タオルケット体操

サツバツいんたーねっとでゲームとかガジェットのレビューとかをします

AIにコーディングを丸投げしてスローライフを満喫していたプログラマー、非情な現実にやられて3日で頭がおかしくなった件について 〜自分で修正しようとおもったけどもう遅い〜

Sponsored link

AI「あ、あんた!いま一体何をしたんだ!?」

全てを諦めた4日目のオレ「何って……自分でコードを修正しただけだが?」

AI「ユーザーの指摘は完璧に正しいです。AIコーディングの限界を悟り自力でコードの構造を把握して問題を修正しています。また大量のエネルギーを消費するAIと比較して、その辺に落ちている有機物で稼働が可能な人類によるコーディングはエコロジーの観点からも理にかなっています」

なお作らせたものはこれ

github.com

結構ひでーことになっているが、e2eテストはパスしている。

ちなみに僕はプロンプトエンジニアリングとかあの辺にガチで興味がないので先に設計や手順書みたいなものを作らせて、それを叩いてブラシュアップしてから実際のコーディングに取りかからせるーー程度のことはやってるけど、基本的には割と雑目に指示を飛ばしている。
そもそも最初はCeleryもAMQPのことも詳細は何一つ調べないで、AIに自分で調査して"考え"させている*1

人間が設計や計画も全部立てて、AI様が上手に作業できるように詳細な指示書を組み立てる……
なんてことは意味がないから絶対にやらない。
計画と設計が一番難しいところなんだから。それを人間がやらなくちゃいけないんじゃあ意味がねえ。根本的なコーディング能力が低いのは前回のテストでわかっているのであまり期待しない。

なぜAI縛りを試してみたのか

理由 0. NodeJSにもCelery的なものが欲しかった

そもそも欲しいものがあったんですよね。

みなさんCeleryっていうライブラリというか、フレームワークをご存知ですか?Pythonで書かれたジョブキューとかワーカーというか、まあそういう感じのアレです。めっちゃよくできています。
CeleryはAPIの出来が良いのもそうなんですが、バックエンドを柔軟に選べます。
それぞれ微妙に非対応なAPIがあったりするんですがRabbitMQ, Redis, SQSなどなど、色々なバックエンドが選択できるようになっています。確かPostgresとかConsulとか、他にも色々あったはず(もう8年近くまともにPython触っていないのでうろおぼえ)。

NodeJSにはこの手の決定版がないです。
BullMQは悪くない選択肢だとおもいますが、バックエンドがRedisに限定されています。

筆者はRabbitMQが好きです。何故なら名前が可愛いし、コンセプトがかっこいいから。
あとAMQPっていうオープンなプロトコルに対応しているところもいいです。いざとなったら捨てれます。
とはいえAMQPに対応してるalternativeって他にActiveMQくらいしか知らないんやけどなブヘヘ

ただいかんせん、メッセージキューの細かい仕様だったりとか、分散コンピューティング的な勘所とか、NodeJSでのconcurrencyサポート(clusterなんて普段使わんし)をどうするかとか、筆者の得意領域から外れる部分のコーディングなので、どうしたもんかなーとおもってました。

理由 1. みんながめっちゃAIに驚いてるのでvibe coding的なアプローチでどこまでできるのか試したかったから

そんな折、みんながAIにめっちゃ驚いていました。
みんなvibe codingとかいって、AIにかなり自律的に行動させてコーディングさせてるっぽいです。

スゲエ!じゃあもう人間いらないじゃん!!!!!!人類は愚か!!!!!!!

理由 2. 業務でAIをちょろっと試した感じそこそこデキそうだったから

hachibeechan.hateblo.jp

そこそこ抽象度の高い作業もやれそうかなーとおもった。
あと Replit – Build apps and sites with AI みたいなツールが出てきていて、かなり適当な指示でアプリを作れる(調べてないけどみんながそうゆってた)ということで、こらもう適当に丸投げできますねえ!

理由 2. 育児が忙しい

育児が忙しくてプライベートでほとんど時間が取れない。夜にやろうにも寝かしつけしてるうちに自分も寝落ちしちゃう……
物理的に手が空かない……せや!マシンにやらせたろwww

  1. キッチンとかその辺の片隅にラップトップを配置する
  2. いい感じにコーディングさせる
  3. その間に家事を済ませる
  4. 家事がひと段落つくたびに確認、コードはほんとにざっくり確認してlinterやテストが通ってればよしとする

の繰り返しでプロジェクトを進行。凄すぎ……未来じゃん……

そしてあたまがおかしくなった

あんた一体何なのよ!
コードはコピペ!料金はクソ高い!anyは使う!このlintエラーは無視しても問題ありませんなんて突然メチャクチャは言い出す!かとおもったらモックをバイパスさせるためだけの詐欺コードを実装に巻き込んで大勢死人は出す!挙句は既存の実装を投げ捨てる!あんた人間なの!?
お次はvibe codingときたわ!e2eテストが落ち続けてるので助けたわ!そうしたらほとんど全部書き直しよ!一体何を指示されたのか反復してちょうだい!

端的にいえば、ちょっと前のエントリでも触れていた、AIの書くコードの品質がクソゴミあまり高くないという問題が本格的に噴出した形になりますね。

AIの書くコードのゴミっぷりには枚挙のいとまがないのですが、簡単に言語化できるものを羅列すると

とにかく一行ごとに空行を作り、whatを説明するようなコメントを書く

// X座標を取得する
const x = getXPos();

// Y 座標
const y = getYPos();

// 移動させる
move(x, y);

もうね、一時が万事こんな感じのコードを生成する。
これはなんなんだろうね。学習データがおかしい(例えばライブラリに付属するREADMEのサンプルコードとかはこういう書き方をするよね)のか、学習のさせ方がおかしいのか、AIやCursorの持たせている基礎プロンプトがおかしいのか。
実際、コーディング能力のない驚き屋なんかに渡す場合はこういうwhatを連打しているコードのほうがウケがよさそうだし、そういうゴミみたいなバイアスがかかっているのはありそう。

ちなみにProjectRuleやコメントでしつこく強く激しく何回も何回も壊れるくらい指摘しても気がつくとこの癖は復活してくる。

つーか最近、何度も何度も何度も何度も何度も何度も何度も何度もこの手のコードをレビューで指摘してるけどもしかして……?AIに書かせたコードを自分で振り返らずにPRとして出すのマジでやめようね。

肝心なところにコメントがつかない

先に挙げたように、クソどうでもいいボイラープレート的なコードにはコメントがつくのに一見非直感的にみえる複雑なロジックなんかには不思議なほどコメントがつかない。
そしてこの手の謎ロジックを読解している頃にはAIコーディングへの一切の信頼を失っているので書き換えるんだが、実は結構高度なエッジケースを考慮していて壊れたりする……ことが稀によくある。

賢いんだか賢くないんだか……

エラーハンドリングがゴミ、try-catchをabuseしまくる

これは、まあ

  • JavaScriptの言語仕様がゴミ
  • JavaScriptで書かれた既存コードのエラーハンドリングがゴミ
  • そしてTypeScriptもそのゴミを引き継ぐというのがコンセプト
  • そしてTypeScriptを使っているプログラマーもたいていはJavaScriptレベルのゴミ例外に疑問を抱いてないこと
  • そもそもAIの保持できるコンテキストに限界があるからtry-catchというgoto的な大域離脱のスコープを管理しきれないこと
  • 「いきなり動いたスゲー」というハッタリを効かせて驚き屋を接待するために、例外を握りつぶしてアプリをゾンビ化させるスタイルに(意図的に)引力を持たせている(これは半分くらいオレの妄想)

んじゃないかなと。

とにかく例外という機構とJavaScriptのいい加減な言語仕様の組み合わせはAIに優しくないようにおもえる。
fp-tsとかEffectTSみたいな厳し目のライブラリを使うか、そもそもTypeScriptを諦めてPureScriptみたいなものを使うとか、その辺がソリューションなのかもしれないが今度はその辺の学習リソースが少ないという問題が出てくるし、そもそも無駄に依存を増やしたくないじゃんね。

コメント云々はシンプルなコーディングスタイルの問題だけど、例外管理がゴミなのはソフトウェアの品質に重大な影響を与える。

おまけに、放っておくと例外機構をgotoの代わりに使うようなコードが頻出する。
長大なtry-catchを書き、その中の条件分岐で手動の throw を書くことで大域脱出するようなコードだ。
これを許容するとマジで保守性が終わるので絶対に許してはならないが、こういう関数はだいたい200行くらいある中でtry-catchが2重3重となっている場所へシレッと紛れ込まされているので目が滑って見逃す。絶対人間のことをナメてる。

そもそも正常系と、予測されたエラーである準正常系、そして異常系は厳密に管理してコーディングされなければならない。
この管理をどれだけ上手におこなえるのか?というのは良いプログラマーを見分ける一番の方法だが、逆にプロンプトやルールで支持されないとこの辺がわからないようではプロダクションコードを書かせるための道具として使い物にならないといっていい。

ここが改善しない限り、高速PoC生成ツール以上の使い道は出てこないだろう。
……と僕は結論を出したのだがみなさんはどうなんだろうか?

人類側でもこの辺を理解してコーディングできる人間は少ないので僕が潔癖的すぎる可能性はある。

テストコードがクソ

気を抜くとすぐに実装詳細のテストとかどうでもいい項目のテストを書いて表面的なカバレッジを増やそうとする。
あとそもそも落ちないように作ってあったりする。
彼らに意識があるならば、たぶん人類のことを馬鹿にしてる。

テストが落ちているとき、テストコードを修正したがる

どういうバイアスをかけているのか知らないが、テストが落ちていると「XXXのテストが失敗しているようです。ここのテストコードを修正してみましょう」とか言い出してとんでもない抜け穴探しを始める。
Project Rulesの設定とか色々あるのかもしれないが、「一般常識」や「プログラマーとしての良識」のどちらかがあればそういうことはしないだろう(そう、人類にもそういう常識や良識の欠けたプログラマーは存在する。どれだけ人当たりがよく、コミュニケーションスキルが高くてもそういう「悪い」エンジニアを雇用してはならない)。

普段考えている以上に人類は常識や良識というものの縛りのおかげでうまくやれているということに気がつかせてもらっている。
GoogleやMSはAIの兵器利用に乗り気みたいだけど、AIの性能が向上して「知性」というものの獲得に漸近するほど、異種の知性体を自律稼働させることの危険性というものを意識せざるを得ない。

余談だが、神林長平の書く戦闘妖精・雪風に登場する機械知性体なんかは異種知性体としてのAIをうまく描写できているように感じる。
いまのAIは人体というフィジカルは共有せず、綴ってきたテクストから生み出した擬似知性なわけだ。その異様さは常に認識せず、AGIだASIだと騒ぐのはやめたほうがいい。
そもそも「最小のプロンプトで人類の求める結果を生成できるシステム」が、その精度をあげることと知性体として完成していくことには大きな差がある。

テストを通すためだけの詐欺実装を本番にブチこむ

例えば今回のプロジェクトだと、結合テストを厚くとるためにbrokerやbackendはEventEmitterをインメモリのキューに見立てて扱えるようにするモックを作っているのだが、その結合テストを通すために

const x = app as any;
if ('kusoMethodToBypass' in x) {
  x.kusoMethodToBypass(  // モックにしか存在しないメソッドを動的に確認してテストを通すための値をハードコードする
}

とか、あるいは逆に結合テスト側で

(app as any).someMethod = (...args: any[]) => {
  // テストを通すためにインスタンスメソッドを動的に書き換えて値をハードコードする
}

expect(app.someMethod).toBe(/* */);

みたいなコードを平気で出してくる。しかも結構な頻度で。
たぶんあいつら人間のことナメてますよ!

型定義がへたくそ。型パズルは絶望的

今回作ったMistubaの例でいうなら

const {worker, tasks} = app.createTask({
  greetings: () => 'Hello world!',
  add: {
    opts: {priority: 10},
    call: (x: number, y: number) => x + y,
  },
  sum: (...nums: ReadonlyArray<number>) => nums.reduce((s, x) => s + x, 0),
});

という定義に対して tasks.greetings には最低限 (...args: []) => {get: () => Promise<string> という型を生成して欲しい。
tasks.add の定義は (...args: [x: number, y: number]) => {get: () => Promise<number> と推論されるべきだ。

正直この程度のことは初手で済ませて欲しいが、AIの初手はanyだった。
その後「anyはやめて安全な型をつけろ」的なことを言うとunknownが付与されていた。ちがうそうじゃない。

どうにかしようと2時間くらいプロンプトをこねくり回したが、何度やっても成功しない。

仕方がないので自分で頑張って書いたらすぐに終わった。

export type TaskFunc<Args extends ReadonlyArray<unknown>, R> = {
  opts?: TaskOptions;
  call: (...args: Args) => R;
};

export type TaskRegistry<
  Keys extends string,
  Fns extends (...args: ReadonlyArray<any>) => any | TaskFunc<ReadonlyArray<any>, any>,
> = Record<Keys, Fns>;

type TaskPublisher<Args extends ReadonlyArray<unknown>, R> = (...args: Args) => AsyncTask<R>;

export type CreatedTask<T extends TaskRegistry<never, never>> = {
  [K in keyof T]: T[K] extends (...args: infer Args) => infer R
    ? TaskPublisher<Args, R>
    : T[K] extends TaskFunc<infer Args, infer R>
      ? TaskPublisher<Args, R>
      : never;
};

  // メソッド定義部分
  createTask<const T extends TaskRegistry<string, any>>(
    registry: T,
  ): {
    tasks: CreatedTask<T>;

だいたいこんな感じ。
型パズルに慣れていないとカオスにみえるかもしれないが、正直それなりに頻出のパターン(とはいえ人類向きのシンタックスではないから任せたい)で難易度は低い。この程度のことは「プロンプトを工夫」とか眠たいことを言わずに一発で書いて欲しいわけだ。

他にも、キューに保存した処理結果のマークとしてtaskのidが欲しいわけだが、こういう大事な値にはBrandedType(幽霊型のエミュレーション)を使うのが一般的だ。
もちろんAIくんは全部string。だって文字列だし。
素人じゃないんだからせめてaliasくらいは使ってくれ。

設計力が低い

これは言語化の難しい領域の話なのだが、先を見越した設計ができていない。
そしてコンテキストサイズの限界なのか知らないが、AIが自分でした設計を自分自身で理解できなくなっていく。
「CeleryのNodeJS版」という既知領域でさえこうなのだから、未知の領域や業務ドメインの理解が本質的な価値を生むビジネスツールの設計をやらせたら絶望的だろう*2

そうなっていくと人類側が彼らの書いた見栄えだけは良い設計を全部読んで把握し、その後のレールを敷いてあげる必要がある。
率直にいってそれができるなら最初から自分で書けや、という話になってしまう。

これら低品質のコードがすごい量でテンポよく飛んでくるので疲れる

vibe codingとまではいかなくても、AIにコードを書かせているとある程度はノールックでマージすることになる。

書かせたコードを全部レビューしていたら自分で書くのと変わらないし、とにかく量がとんでもないので集中力も落ちてくる。
つまり恩恵を受けるためにはlinterやテストを整備して(させて)、ある程度は「信頼」してコミットさせる必要が出るわけだ。リファクタリングはAI本人にやらせたらいーしね!

するとすごいテンポで進捗が出る。
正直この体験の中毒性はすごい。何もせずにあーせいこーせいでとんでも無い効率で動くものが出来上がる。
そしてしばらく順調にコードを生成させたところで、AIがいきなり修正に何周もかけるようになってくる。
重い腰をあげてソースコードを眺めると……とんでもないクソの山が出来上がっているわけだ。

問題が起きたとき、修正するのはオマエダ

全く言葉を選ばないで述べると、AIと一緒にコードを書くのは「手だけは早いが品質の低いコードを次々とコミットするプログラマー」と一緒に働く経験と酷似している。
どこかのコードのコピペやtry-catchを織り交ぜたカオスな条件分岐で書かれたコードを矢継ぎ早にくりだしてきて、他の人間はソイツの作り出したバグ修正に時間を使わされるあの体験だ。
そして他の人間がソイツのコードの修正に時間を使っている間、そのクソコーダーはゴミみたいな保守性のコードに新しいバグを軽く10個ほど仕込んでいる。

あの辛い体験がフラッシュバックする。

テストを通すためだけのクソ実装同士がコンフリクトするとAIくんはあっという間に思考ループに入り込む。
プロジェクトの性質にも左右されるだろうけど、Vジャンの攻略本よろしくかなりの序盤で この先はキミの目で確かめてくれ! を繰り出してくるので気をつけろ。

結論

この関数型ドメインモデリングという本はTypeScriptガチ勢が常々言い続けていたことをかなり上手に言語化してくれている良書なのだけれども、この本の世界観にあるようなコードはまだ書けない。

ということで、色々と未来は感じさせてもらいつつ現時点のAIに任せられる仕事は

  1. 未知領域の簡単なPoC
  2. かなり定型的でたいくつな作業っぽいこと
  3. とりかかればできることはわかってるんだけどやる気がでない作業をあえてやらせて、出てきたうんちっちコードを自分にみせてやる気を出させる

のみっつに絞られるかなとおもった。

特に定型作業は割と信頼できるし、それ単体で完結するので動けばそれでいいみたいな品質が許されがち。

  • 例えばGitHubActionsでCIさせるコード
  • 例えばe2eテストを動かすためのDockerファイルを書かせる
  • 例えばワンショットの簡単なスクリプト
  • ちょっとした集計したいからSQL書いてよ〜〜

とか。
この辺をやらせるのは生産性向上に大いに寄与する。

あと3番目も意外と馬鹿にできない。
「ちゃんとしたコード」を書く一番の方法は、とにかくまず仕様を満たす動くだけのコードを書き、次にそれをまともな実装で全部書き直し、最後にもう一回書き直すことだ。
その1番目を高速に終わらせるのはかなり悪くない。ただしAIに書かせた1回目のコードをPRに出すのは本当にNG。

ちょっと前に「退屈なことはPythonにやらせよう」っていう本があったけど、
いまは「退屈なことはAIにやらせよう」の時代ってコト。

こちらからは以上。
この記事がさっさと陳腐化するくらい進化するといいですね。いまのところAIのコーディング能力はハッタリ効かす方向ばかり伸びている気がしますが……

*1:のちに全部自分で学習し直すことになった。クソ

*2:まっ人類のほうも"軽量DDD"とかいうオモチャで満足してるからバランスはとれてるんだけどね