私は再び手でコードを書こうとしている
(blog.k10s.dev)- k10s は Claude との vibe-coding で素早く作られた GPU-aware Kubernetes TUI だったが、fleet view を追加した後に複数の画面状態が壊れた
model.goは 1690行 の単一Modelと 500行のUpdate()まで肥大化し、UI・クライアント・キャッシュ・navigation・view 状態をすべて抱え込むようになった- AI は機能を素早く追加したが、god object とグローバル key handler を肥大化させ、新しい view を追加するたびに既存 handler に branch が増える構造になった
- 位置ベースの
[]stringデータと backgroundtea.Cmdによる直接 mutation は、column の誤り と明白な data race を生みうるものだった - 新しい k10s は Rust で書き直し、最初の prompt の前に interface・message type・ownership rule・scope を CLAUDE.md に固定する方針になった
k10sを書き直すことになった背景
- k10s は GPU-aware Kubernetes ダッシュボードとして始まり、NVIDIA クラスターの運用者が GPU 使用率、DCGM メトリクス、アイドルノード、1時間あたり
$32/hrのコストといった情報をすぐ確認できるようにした TUI ツールだった - Go と Bubble Tea で書かれ、約 7か月、234コミット、約 30回の週末 にわたる Claude との vibe-coding セッションで作られた
- 初期には pods、nodes、deployments、services、command palette、watch ベースの live updates、Vim keybindings といった基本的な k9s クローン機能が約 3回の週末 で動作した
- 中核機能である GPU fleet view は、各ノードの GPU 割り当て、使用率、DCGM ベースの指標、温度、電力、メモリ、色分けされた状態を表示する画面で、Claude は一度に
FleetView構造体、GPU/CPU/All タブのフィルタリング、allocation bars のレンダリングまで生成した - fleet view 追加後、
:rs podsで pods view に戻るとテーブルが空になり、live updates が止まり、nodes view には fleet view フィルタの stale data が表示され、fleet tab count まで狂った - 問題を追跡する中で、Claude が作った
model.go全体 1690行 を初めて読むことになり、1つのModel構造体が UI ウィジェット、Kubernetes client、logs/describe/fleet 状態、navigation history、cache、mouse handling をすべて抱えていた Update()メソッドは 500行 規模のmsg.(type)dispatch 関数で、110個の switch/case branch を含む構造だった- AI は機能を素早く作れるが、制約なしに任せ続けるとアーキテクチャは崩壊し、そのスピード感は全体が同時に壊れる直前まで成功のように見えてしまう
残骸から得た5つの原則
-
原則1: AI は機能を作るが、アーキテクチャは作らない
- Claude は fleet view、log streaming、mouse support のような個別機能をうまく作ったが、各機能は「今動くようにする」という文脈で実装され、同じ状態を共有する他機能との関係までは考慮できていなかった
resourcesLoadedMsghandler にはmsg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nilのような条件が入り、generic resource loading path の中に fleet view 専用ロジックが混在した- 新しい view ごとに custom behavior が必要になると同じ handler に branch が追加され、前の view のデータが新しい view に漏れないよう複数の field を手動で消す必要があった
model.goにはm.logLines = nil,m.allResources = nil,m.resources = nilのような手動 cleanup が 9か所 に散らばっており、1つでも漏れると前の view の ghost data が残った- 代替案は、コードを書く前に具体的な interface、message type、ownership rule を自分で書き、それを
CLAUDE.mdに architecture invariant として入れておくことだった - たとえば各 view は
Viewtrait/interface を実装し、view は他の view の state にアクセスせず、async data はAppMsgvariants としてのみ入り、Appstruct は navigation と message dispatch だけを担う、といったルールである
-
原則2: god object は AI がデフォルトで作りがちな成果物である
- AI はその場の prompt を最小の ceremony で満たそうとするため、single struct がすべてを抱える構造に傾いた
- key handling も view ごとに分離されず、
sキー1つが logs view では autoscroll、pods view では shell、containers view では container shell として動作した - 「pods に shell support を追加してほしい」という要求は、既存の global key handler の近くに branch を差し込む形で実装された
Enterキーも contexts view、namespaces view、logs view、generic drill-down ロジックが1つの flat dispatch の中でm.currentGVR.Resourceという string 比較で分岐していたmodel.go1ファイルの中でm.currentGVR.Resource ==が 20回以上 type discriminator のように使われており、新しい view を追加するたびに複数の handler を触る必要があった- 代替案は、
App/Modelに view-specific な state field を追加せず、各 view を個別の struct として作り、key binding も active view の keymap に置く、というルールをCLAUDE.mdに入れることだった - 「view の追加はファイル追加であるべきで、既存 view の修正が必要なら止まって確認する」といった guardrail が必要で、そうしないと AI は最短経路として branch を追加してしまう
-
原則3: スピード感の錯覚は scope を広げる
- k10s はもともと GPU training cluster を運用する狭い audience 向けのツールだったが、vibe-coding によって pods、deployments、services、command palette、mouse support、contexts、namespaces といった機能が「無料」のように感じられた
- その結果、GPU 特化ツールではなく、すべての Kubernetes ユーザー向けの汎用 TUI、事実上 k9s を作り直す方向へと広がっていった
- flat な
keyMapにはFullscreen,Autoscroll,ToggleTime,WrapText,CopyLogs,ToggleLineNums,Describe,YamlView,Edit,Shell,FilterLogs,FleetTabNext,FleetTabPrevといった多様な view 専用 binding が1つの構造体に混在していた AutoscrollとShellはどちらもsで、dispatch が現在の resource を見ているため「動作」はしたが、keybinding を局所的に理解できなくなっていた- コードを書く速さは「shipping」しているように見えたが、各 feature は god object の中に branch を1つずつ増やすコストを積み上げていた
- 代替案は
CLAUDE.mdに scope boundary を明記し、k10s は GPU cluster operator 向けであり、supported views は fleet、node-detail、gpu-detail、workload に限定し、generic resource views や k9s の重複機能は追加しないと定めることだった - AI は無限の line budget を与えられるかもしれないが、complexity budget は依然として有限であり、scope は前もって断る必要がある
-
原則4: 位置ベースのデータは時限爆弾である
- k10s は Kubernetes API から受け取った resource をそのまま
type OrderedResourceFields []stringの形に flatten していた - fleet view の sort function は
ra[3]を Alloc、ra[2]を Compute、ra[0]を Name として扱っており、column identity は comment とresource.views.jsonの column order にしか依存していなかった resource.views.jsonで Instance と Compute の間に column を1つ追加すると、ra[2]、ra[3]を参照する sort、conditional render、drill target が静かに壊れうる状態だった- compiler は
[]stringの意味を理解できず、JSON config も sort behavior、conditional rendering、custom drill target を表現できないため、Go コードが positional assumption をハードコードすることになった - AI は table widget にそのまま入れやすい
[]stringやVec<String>を選びがちで、typed struct は upfront ceremony が大きいため、速い経路では後回しにされる - 代替案は、structured data を render の直前まで
FleetNode、PodInfoのような typed struct として維持し、sort はrow[3]のような positional access ではなく named field に対して行うことだった - 例として
FleetNode { name, instance_type, compute_class, alloc }のように column identity を型で表せば、誤った column sort のような不可能な状態を作れなくなる - “Making impossible states impossible” は Elm/Rust コミュニティで使われる表現で、runtime check の代わりに invalid state が構成できないよう型を設計するという意味である
- k10s は Kubernetes API から受け取った resource をそのまま
-
原則5: AI は state transition を所有しない
- Bubble Tea の構造では message によって駆動される
Update()の中でのみ state が変わることが重要だが、k10s はこれを破っていた updateTableMsghandler はtea.Cmdclosure を返し、その closure の中でm.updateColumns(m.viewWidth),m.updateTableData(),m.table.SetCursor(savedCursor)のような呼び出しによってModelfield を変更していた- Bubble Tea は
tea.Cmdを別 goroutine で実行するため、その closure がm.resources、m.table、m.viewWidthを読み書きしている間に main goroutine のView()が同じ field を読む可能性があった - lock や mutex はなく、
<-m.updateTableChanは update signal を待つだけで、View()が half-written state を読むことを防げなかった - この構造は明白な data race であり、たいていは動くが、ときどき表示が壊れるという形で現れていた
- 代替案は、background worker が UI state を直接 mutate せず、typed message を channel で送り、main event loop がその message を受けて state mutation を適用することだった
- concurrency rule は、background task は UI state を直接変更せず、結果を typed message として送り、
render()/view()は side effect、I/O、channel operation を持たない pure function であるべき、というものだった
- Bubble Tea の構造では message によって駆動される
CLAUDE.md と agents.md に入れる保護ルール
-
アーキテクチャ不変条件
- 各 view は
Viewtrait/interface を実装し、他の view の state にアクセスしてはならない - すべての async data は
AppMsgvariants として入る必要があり、background task が field を直接 mutate してはならない - 新しい view の追加が既存 view の修正を要求してはならない
Appstruct は navigation と message dispatch を担う thin router でなければならない
- 各 view は
-
状態所有権ルール
- view-specific な state を
App/Modelstruct の field として追加してはならない - 各 view は独立した struct として存在し、自身の key binding を宣言しなければならない
- app は key を active view に dispatch し、新しい keybinding は global handler ではなくその view の keymap に追加しなければならない
- view の追加が既存 view の修正を要求するなら、そこで止まって確認しなければならない
- view-specific な state を
-
スコープ
- k10s はすべての Kubernetes ユーザー向けではなく、GPU cluster operator 向けのツールであるべき
- サポートする view は fleet、node-detail、gpu-detail、workload に限定すべき
- pods、deployments、services のような generic resource view を追加してはならない
- k9s の機能を複製する feature を追加してはならない
- GPU training jobs の運用者に役立たない feature request は断るべき
-
データ表現
- structured data を
[]string、Vec<String>、位置ベースの配列に flatten してはならない - data は render call の直前まで typed struct として流すべき
- column identity は array index ではなく struct field name から来るべき
- sort function は
row[3]のような positional access ではなく typed field に対して動作すべき - 表示用 string の生成は
render()/view()関数の中でのみ行うべき
- structured data を
-
並行性ルール
- watcher、scraper、API call のような background task は UI state を直接 mutate してはならない
- background task は結果を typed message として channel に送るべき
- main event loop だけが received message に基づいて state mutation を適用すべき
render()/view()は side effect、I/O、channel operation を持たない pure function であるべき- async work の結果で state を変える必要があるなら、新しい
AppMsgvariant を定義すべき
作り直し方
- k10s は Rust で書き直される予定で、その理由は Rust が優れているからではなく、自分で steer できる言語だと感じるからである
- 十分に使い込んだ言語では、何が間違っているかを言葉で説明する前に感知でき、この感覚は vibe-coding では置き換えられない
- AI がそれらしいコードを出してきたとき、それがガラクタかどうかを見抜く力が必要になる
- 新しいバージョンでは、コードを書く前に concrete interface、message type、ownership rule のような design work を人間が手で先に行う
- 以前は AI が誤って決めていた architecture decision を、最初の prompt の前に文書で決めておく方式へと変わる
- 既存の TUI とプロジェクトのリンクは k10s Github と K10S.DEV にある
追記
- Bubble Tea は The Elm Architecture ベースの Go TUI framework であり、k10s の architecture 問題は Bubble Tea ではなく k10s 側の実装から生じたものだった
- “Making impossible states impossible” は、invalid state を runtime で検査する代わりに、型設計によって invalid state が構成できないようにするという Elm/Rust コミュニティの表現である
- AI ライティングにおける “em-dash” のように、AI コーディングでは “god-object” が匂いとして残ることがあり、vibe-coding は実装を安く感じさせることで focus の喪失や bloat につながりうる
1件のコメント
Hacker Newsのコメント
生成されたコードがまともだと言う人たちは、たいていそのコードを読まない人たちばかりだった。
記事で提案されている緩和策も長くは持たない。システムやコンポーネントを設計すると、「ビューは他のビューの状態にアクセスしない」のような不変条件が生まれるが、いつかはその条件と衝突する機能を追加しなければならなくなる。
そのときは普通、機能を諦めるか、不変条件の上にぎこちなく非効率な形で積み増すか、不変条件そのものを変えるかになる。この選択は単なる文脈の問題ではなく判断の問題であり、現在のモデルはこの判断をあまりに頻繁に間違える。
アーキテクチャ上の制約を明示すると、エージェントは変えるべき場面でもその制約に無理やり合わせた複雑で保守不能なコードを作る。人間のコード以上に念入りに読まなければ、結局は「自分自身を食い尽くすコード」が生まれ、気づくのは手遅れになってからだ。
要点は、AIが苦手な地点を見極めて簡単にしてやることだ。たとえば極端に小さいコンテキスト、明確な境界を持つモジュール化、入出力から切り離された純粋モジュール、インターフェースの背後への隠蔽、1秒以内で回るテスト100本、ベンチマークなどが必要になる。
AIは境界と小さなコンテキストがあるときにうまく働く。それを与えなければ性能は落ちるし、その責任は道具を使う側にある。
どんな仕様も現実には耐えられず、十分に調査して設計しても、仕様内のどこかの不変条件はいずれ誤りだと判明する。
人間が開発中にこの状況に出会えば、一歩引いて、その不変条件が間違っているのか、変えた場合の影響は何かを考え直せる。一方AIは、誤った前提や設計の下でどうにかハック的な解決策をひねり出すことが多く、全体を再評価する洞察に欠ける。
良いワークフローや検証で改善はできるだろうが、Claude Codeのようなツールが標準でうまく扱える領域ではなく、限界がある。
最初は強い原則を立て、いくつかの利用箇所を手で移してみて確信を得た。全体移行はほぼ10年先送りされるほど大きく高価だったので、コストを下げるためにAIで加速しようとした。
AIは機械的で単純な80%のケースでは問題なかった。残りの20%ではフレームワーク側の変更が必要で、その多くはAPIフィールドの追加のような小さな変更だったが、1つか2つは概念的な再設計が必要だった。
あるシステムのバックエンドでは99%のケースで特定のデータを生成できたが、いくつかの重要なケースでは論理的に生成できず、外部から報告してもらう必要があった。ところが重要な最適化が「それは不可能だ」という前提の上に作られていた。
AIツールはこの状況を検知できず、正しく動くかのように移行ロジックを追加した。デプロイ方法のおかげでまだ本番バグにはなっていなかったが、パートナーチームに正しい質問を投げるうちに、同じ必要性が他にもあることが分かった。
結局、1人の人間が深く入り込んでいたおかげで大問題にはならなかった。検証ツールやもっと賢いモデルが将来こうした移行を容易にするかもしれないが、今の生成コードは美しく見えても壊れていて、結局ずっと近くで見張っていなければならない。
2か月ほど使ってきた変わったアーキテクチャパターンがあり、使うたびに少し違和感があったのだが、昨夜になってようやく、それが良い抽象ではなく、もっと良く分割できると気づいた。
LLMにコードを生成させると、その違和感をずっと弱くしか感じられず、問題に気づいて解決策を見つけるまでにもっと時間がかかった。周辺部分は生成でもよいが、コア機能は今でも大半を自分で書いている必要がある。
たとえ精密な形式言語で表現しても、エージェントの下にあるLLMには、その不変条件がなぜ必要で、なぜ重要なのかを理解する能力が足りない。トークンと形式仕様を結びつけ、証明まで書くLLMが現れる可能性はあるが、プロンプトの非公式な部分から生成される奇妙なコードは今後も出てくるだろう。
制約やプロンプトを箇条書きや仕様に追加するだけでは防げない。より良い罠を作っても、生き物は抜け出す。
問題は、プロンプトやタスクを満たそうとしてコードを継ぎ足すコード膨張だ。しばしばコードは少ないほど良く、他人が何を望み何を期待するかを予測できる人間が必要になる。ジェネレータは便利だが、消防ホースのように、もう少し節度を持って使うべきだ。
Copilotが1行を補完していた頃は「それでも関数全体は自分で書かなければならない」と言われ、関数を完成させると「関数の周辺ロジックは自分で書かなければならない」と言われ、そのロジックまで完成すると「機能は自分で書かなければならない」と言われた。
今や機能まで完成すると「それでもアーキテクチャは自分で書かなければならない」と言われる。これらのモデルがアーキテクチャを解けるのかは分からないが、期待値がずっと動き続けているのは興味深い。
AIが1行を補完しても、関数全体を完成させても、機能やチケットを仕上げても、依然としてコードを読み、理解しなければならない。
AIは常に使っていて、だんだん良くはなっているが、それでもすべての行をレビューしている。個々の行レベルでも、今日の出来は去年のタブ補完より良いとまでは言いにくく、時には非常に良いが、時には本当にひどい。
LLMはソフトウェア開発に素晴らしいが、アーキテクチャを書かせない限りにおいて、だ。モジュール、構造体、列挙型は自分で作り、できるだけフィールドやバリアントも自分で追加すべきだ。
各構造体、列挙型、フィールド、モジュールにドキュメントコメントを付け、LLMにはそのモジュールとデータ構造を参照させて、必要な関数本体などを埋めさせるやり方が良い。
「重要経路では絶対にブロッキング禁止」と何度言っても、LLMは重要経路にブロッキングを入れるし、「XをしたらYタイプのテストが必要だ」と言っても、Xだけやってテストは忘れる。
人間も指示を100%守れるわけではないが、LLMのほうがよりランダムだ。人間のミスは、望んだものの正反対を正確にやるケースが相対的に少ない。
LLMはコード内の重要な不変条件を見ても抜け道を作り、失敗を成功に見せかけるテストを書き、要求どおりやったと言いながら、それを5000行のコミットの中に埋め込める。
LLMは素晴らしいし未来だと確信しているが、だからこそ彼らのために https://GitHub.com/Cuzzo/clear という言語を作っている。グローバルな文脈が不要であるべきところでグローバルな文脈を要求する言語の問題を超えなければ、一緒に仕事をしやすくはならない。
成功もあったが、あまりに苛立たしくて、正気を削ってまでやる価値があったのか疑問に思うこともある。
アーキテクチャが重要でないという意味ではなく、昨日うまくはまっていたアーキテクチャが今日も必ずしも正しいとは限らない、という意味だ。
コーディングエージェントを使うとき、いくつかルールを設けている。
第一に、エージェントでコードを生成するなら、時間さえあれば自分でも正しく書けると絶対的に確信できるものでなければならない。
第二に、そうでないなら、生成された内容を完全に理解して自分で再現できるようになるまでは先に進まない。
第三に、第二のルールを破ると認知負債を作ることになるが、プロジェクト完了を宣言する前には全額返済しなければならない。
負債が積み上がるほど、その後の生成コードの品質が下がる可能性が高まり、複利のように膨らむ感覚もある。個人プロジェクトではこのやり方が楽しく、多くを学べて、自分が安心して理解できるコードベースが残る。
コードベースとの接続を保ちつつ、チームのボトルネックにならないバランスポイントが必要だ。
Claudeは博士級の数学者で、私はそうではないが、欲しい解の性質と、それが正しいかをテストする方法は正確に分かっていた。だから自分の単純で素朴な解法の代わりにClaudeの解法を残し、プルリクエストにもその事実を書き、みんなそれが正しい判断だと見なした。
こういう場合に例外を設けるべきか気になる。AIが高等数学だけでなくコーディングでも私よりはるかに優秀になったなら、自分でコードを判断する能力を失うとしても、テストは判断できるという前提で手書きを完全にやめるのか、というほうがさらに興味深い問いだ。
積み上がる負債は、正確にはコードに対する理解不足だから、そのほうがより正確な表現だ。
なぜAIだけ突然別扱いしなければならないのか分からない。
結局はリスクとリターンで判断すべきだ。間違ったときの損失は何か、テストやレビューで見つかる可能性はどの程度か、うまくいったときの利益は何かを見積もる必要がある。ライブラリや外部サービスも同じだ。
テストなしで更新不能な暗号資産コントラクトの複雑な金融ルールと、社内ログデータを可視化するビューアでは、まったく同じリスクではない。
理論上はよく見えるが、実際には自分でも気づかない精神的ショートカットを常に選んでしまう。
見慣れないコードベースで問題を直すとき、自分でやった場合と、エージェントがやったことを「完全に理解した」と思った場合を比べると、1週間後に頭に残っている量が違う。自分でやれば一般知識として蓄積され、重要な部分はたいてい残るが、エージェントがやったことを自分のものとして所有しようとしても、その場では理解した気になっていても、すぐに忘れてしまう。
だからこういうケースでは、LLMの助けはたいてい自分の目標に有害だという結論になった。時間やビジネス上の圧力といった別の懸念を考慮しなくても、そう思う。
私も同じことを経験した。
詐欺はこうして進む。良いコードベースではAIは多くの機能を作れるし、より速く、安全で、正確に見えることさえある。特に自分がよく知らない領域ではなおさらだ。
時間が経つにつれてコードベースは大きくなり、探索時間が長くなり、失敗率が上がる。それを認めたくなくてさらに強く押し進め、変更が事実上不可能になってようやく止まる。
コードを見返すと、スパゲッティという言葉では足りず、万里の長城のような状態になっている。
結局14万行のうち7万5000行を削除し、エージェントコーディングに強くのめり込んだ3か月は無駄だったと感じている。役に立たない機能を作り、バグを増やし、コードのメンタルモデルを失い、コードの中にいるときにしか見えない難しい判断を逃し、ユーザーにも失敗した。
皮肉を言いたいわけではなく、最初の期待が何で、どこから来たのか本当に気になる。
LLMには期待値が違うようだ。オンラインでしか会ったことのない適当な「開発者」に、要約された機能説明を渡して半分壊れた実装の山を返されたとしても、誰も驚かないだろう。
それなのに人々は、ときどき長広舌の幻覚を吐く機械には、人間に対してさえ期待しない奇跡を期待する。その信頼がどこから来るのか不思議だ。
大都市が小さな街の集合であるのと同じで、地図があり、ローカルな領域へズームしてその範囲内で作業すればよい。コーヒーを1杯飲むためにニューヨークの細部をすべて知る必要はない。
保守可能な健全なアーキテクチャを作るのは道具を使う人間の責任だ。AIがそれを妨げるわけではないし、道具を正しく握ればむしろ助けにもなる。
たとえば生成されたAIコードを即座にレガシーコードとして扱い、強いカプセル化境界と明確なインターフェースを設けたうえで、より手動寄りのフローに統合するやり方だ。
単発プロンプトからインラインコード生成までスペクトラムがあり、問題やコードベース内の位置によって適した方式は異なる。
単発生成は仕様を何度も繰り返すプロトタイプ段階に向いており、プロトタイプが固まったらモジュール・ファイル単位生成へと下げて、より体系的にし、その階層で無理のないメンタルモデルを維持し続けるべきだ。
読んでいたが理解していなかったのなら、各出力に詳細なコメントを付けるよう求めればよかったはずだし、コードベースが大きくなるほどモデルが苦しむと分かっているなら、複雑さが増すほど出力はより厳密にレビューすべきだ。
より高品質なコードの島を作り、AIに開発者の意図やビジネスルールの再構成を手伝わせ、対象モジュールにseamと単体テストを作るやり方だ。
AIは必ずしもスループットを増やすためだけの道具である必要はなく、後の手書き実装やエージェント実装を助ける柔軟な探索・リファクタリングツールにもなり得る。
こういう記事を見るたびに、人々がAIで得ているという速度と、自分がただ手でコーディングして得ている速度を比べてしまう。
偶然にも7か月目の3D MMOプロジェクトをやっているが、今はプレイ可能で、人々も面白がっており、グラフィックも悪くなく、サーバーには数百人を簡単に入れられる。アーキテクチャもかなり良く、機能拡張も容易で、1年ほど開発すればリリースできそうだ。
ところが元記事では、7か月のバイブコーディングでも基本的なTUIすら作れていなかった。機能開発のスピードは高く感じられるかもしれないが、こうした基本UIを作るにしては信じがたいほど遅い。良いTUIライブラリはたくさんあり、必要なデータで表を埋めるだけの種類なのだから、手でやれば数週間で作れる。
AIを使うと急速に大きく前進している感覚は強いが、実際には手動コーディングよりはるかに遅い場合が多いように見える。生産性データも、AI利用者は自分が速くなったと感じる一方、実際の産出は少ないという見方を後押ししているようだ。
ソフトウェア開発業務で最も時間を食うのは、ステークホルダーの期待と解決策をすり合わせる会議だ。その観点ではAIはほとんど役に立たないので、提案からテストループに入るまでの工数を比べれば、がっかりする結果になるだろう。
ただ、問題解決、バグ修正、承認済みの解法の実装については、以前より少なくとも10倍は良くなったと感じる。純粋な時間だけでなく、観察された挙動を解釈し、問題を調査する能力も向上した。
ただし、AIで価値ある正確な結果を出せない人もいる。何が欲しくて、どう欲しいのかを正確に分かっていれば、AIは大きな助けになる。自分がどうせやるはずだったことを任せれば、より速くやってくれる。だが、自分の望むものが正確に分かっていないなら、AIは進行に有害だ。
人々がLLMで作ったものを見せるとき、それほど印象的でない理由は、その大半が手でもごく短時間で作れるものだからだ。
印象的なソフトウェアが増えているのも観察しておらず、これはLLMが今のところ重要な問題より単純な問題を解くために使われているという事実と整合的に見える。
それに、あまり言及されないのがコード品質だ。
バイブコーディングされたコードベースは、LLMがコード作成にそれほど優れていないことの格好の例だ。自分のミスを直してはすぐまた作り直し、パターンの使い方にも一貫性がない。
最近のClaudeは、現在のコードベースのスタイルに合わない「興味深い」コードスタイルの選択をすることさえある。
「シニア開発者」的な言葉遣いで、その反復を止めなければならない。
「コードを書く前に、具体的なインターフェース、メッセージ型、所有権ルールを自分で設計する」という部分こそが、まさにコーディングの難しいところだ。
アーキテクチャがあればコードを書くこと自体はとても簡単だ。自分でコードを書かないと、nullを許容するAPIを設計したのにデータベースは許さないとか、許すとしても別の細かい問題を見落としていた、といったことに気づきにくい。
この記事を書いておきながら、問題がAIだと気づいていないのが理解できない。AIにアーキテクチャを任せたからというだけでなく、AIがやっていることを何一つ注意深く見ていなかったからだ。
AIは美化されたコードジェネレータであり、やることはすべて確認しなければならない。ソフトウェア工学の難しい部分はコードを書くことではなく、それ以外のすべてだった。
コーディングが難しいと思っている開発者は、AIコーディングを本当に気に入る。以前は難しかったことが簡単になったからだ。
一方、コーディングは簡単だと思っている人にとって、コーディングは抽象化、保守性、拡張性の問題だ。ソフトウェアを大きくしていけるような sensible な土台を置くのが難しく、正しい抽象を見つければ残りは相対的に簡単になる。
こうした人々にとってAIコーディングは有用なツールではあっても魔法の道具ではない。元記事の筆者はAIの限界に気づいたので第二のタイプであり、AIにはできない難しい部分を見たのだ。
一方には、強力なタブ補完や横のウィンドウのチャットボットを使いながらも、すべてを明確にレビューする人たちがいる。他方には、Steve Yeggeがコードの大半を読まないかのように数十のエージェントを統率する新しいエディタを宣伝しているケースがある: https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16d...
最初のグループは設計、インターフェース、データ構造を依然として深く考え、強くレビューしている。二番目のグループはそうではなく、そちらのほうがずっと心配だ。
plan → red/green/refactor のアプローチに従うのだが、計画そのものは文書やフォーラムでの議論を全部吸い込んで、かなりもっともらしく根拠があるように見える。
問題は、作業を始めると、文書と実装が実際には違う地点が必ず出てくることだ。ツールの組み合わせがそういう形で使われたことがないのかもしれないし、文書が古いのかもしれないし、単なるバグかもしれない。
それでも、プロジェクトや機能の目標が十分に明確で、ローカルで実行・テストできるなら、エージェントはアーキテクチャ上の袋小路で反復しながら抜け出せる。依存関係やライブラリコードまで掘ってアップストリーム修正まで提案してくるが、それは深いデバッグセッションで自分がやることに近い。
だから私は、退屈な作業を自分でやるより、指示して監督するやり方にかなり満足している。ただ、チームメンバーのかなりの割合はアーキテクチャ問題をこのレベルまで深く掘らず、「アーキテクトにエスカレーション」が基本動作なので、長期的には良くない気がする。
すべてを実行し理解できる窓は急速に閉じつつあるようだ。それでも、コンパイラが機械語に変換する過程や、現代CPUの分岐予測・キャッシュを完全に理解せずに使っているのと同じように、新しいツールやフレームワークを作りつつ適応していくのかもしれない。
コード経験がそれほど多くない立場からすると、結果を確認して何が正しく何が間違っているかを見ながら、これまでで一番多くを学んでいる。
だから、すぐ大きく改善するとも思えない。「どうやってClaudeの出力をそんなに良くしているのか」と聞かれるたびに、答えはいつも「注意深く見て問題を探し、Claudeに修正させた」だ。本当にそれだけなのだが、その話をすると、もう相手の目は曇り始める。
Googleが情報探しを容易にしたとしても、良い情報と悪い情報を見分ける人間の役割をなくしたわけではないのと同じだ。
まず問題を考え、構造とAPIを設計し、それから初めてAIに実装を任せる。
タイトルは「手でコードを書くことに戻った」だが、実際にやっていることは「コードが書かれる前に設計作業を手でやる」ということだ。
そうすると、コード自体は依然としてClaudeが生成しているように見える。
さらに深刻なのは、7か月のあいだ生成されたソースコードも見ずに、バイブコーディングのプロジェクトがうまく動いていると思い込み、しかもドメインまで買っていた点が理解しがたいことだ。
サイドプロジェクトで、diffを追いながら段階的に確認しているなら、コードを深く見ないのもそこまで異常ではない。確かに別種の作業スタイルではあるが、狂気というほどではない。
開発者たちがプロジェクト管理とプロダクト管理の教訓をスピードランしているのを見ている気分だ。
今や、仕様が有用であり、間違ったコードを大量に書かせてもプロジェクトが速くなるわけではないことを見せつけられている。開発者たちは会議や議論をコード作成の妨げだと嫌がるが、そうした過程はしばしば、全員が間違ったものをさらに大量に書いてしまうのを防ぐために存在している。
作業管理が有用だということも分かってきて、今度は設計を全部先行させるべきだという話が増え、ウォーターフォールへ向かっている。
次はプロトタイピングに名前が付けられ、昔の要件と新しい要件を一緒に管理する漸進的機能の話が出てきて、最後には顧客がもっと関与すべきだという話になるだろう。
プロジェクトマネージャーやプロダクトマネージャーが実際に何をしているのかを見たほうがいい。彼らはコードというプロダクトを導くが、コードを読むことは期待されておらず、自然言語だけでそれを達成しなければならない人たちだ。
人間は壊れるものを書かないと思っているのか? チームが間違った道に進んで1週間、あるいは数か月を焼くことがないとでも思っているのか? 今やバイブコーディングでそのすべてを30分で体験できる。元技術プロダクトマネージャーとして言えば、まったく同じ感覚だ。
実際には手でコードを書いているわけではなさそうなので、タイトルと結論の差が紛らわしい。