Claudeでパーソナルソフトウェアを作る
原文は Nelson Elhage により に公開されました。 このブログを購読する
今月初め、Emacsのパッケージの一部をClaudeを使ってRustに移植し、実行時間を1000倍以上短縮した(具体的な一例では、90秒から約15msへ)。
これは、私が仕事でも自分のコンピューティング環境の整備でも、比較的日常的にやっているヤク剃りの一種だ。ただ今回は、Claudeが私の監督のもとでプロジェクトのほぼ全体を実行してくれ、私はほとんどコードを書かずに済んだ。手作業でやるのに比べて、プロジェクトは大幅に高速化された。
このプロジェクトに取りかかる時点でも、Claudeには「ちょっと優れたドキュメント検索エンジン兼、ちょっと優れたStack Overflow」程度の小さな手助けしか期待していなかった。Anthropicで働き、時々Simon Willisonを読んでいるにもかかわらず、私の期待はどうやら大きく時代遅れだったようだ!
この経験によって、ソフトウェアエンジニアリングや自分自身の仕事におけるLLMの役割についての考えが大きく変わった。考えはまだ整理の途上だが、この文章はその経験を記録し、自分の行動や信念、期待をどうアップデートすべきかを考えながら思考を声に出してみようとする試みだ。
問題
ここ1年ほど、私はObsidian.mdのヘビーユーザーになり、Emacs org-mode filesとWorkflowyの組み合わせから置き換えた。Obsidianが気に入っている点は多いが、この記事にとって重要なのは、(a)データをローカルディスク上の素のMarkdownファイルとして直接保存すること、そして(b)それらを操作するためのまずまずのEmacs modeが存在することだ。ラップトップでも基本的にはアプリ自体を使っているが、複雑なテキスト操作のためにEmacsに切り替えたり、コーディング中にエディタ内でTODOリストを管理できたりするのは、私にとってとても価値がある。
しかし、vaultが大きくなるにつれて問題が発生した。obsidian.elが使い物にならないほど遅くなったのだ。ノートを開くだけでEmacsがハングするようになり、当初は数秒だったのが、やがて1分以上かかるようになった。原因は判明したところによるとobsidian-update関数だった。obsidian.elは内部のインベントリを更新するためにvault全体を定期的に再走査し、すべてのノートからタグやメタデータをスキャンするのだが、このスキャン(完全にelispで書かれている)が非常に遅いのだ。
私の計画
私は一応elispを書けるが、プロファイリングや最適化の経験はなく、GitHub issueにもすでに他の人たちによる試みがいくつか投稿されていた。そこで別のアプローチを取ることにした。
Rustで短いプログラムを書いてvaultをスキャンしてインベントリを作成し、最小限のメタデータをJSONとして出力させ、obsidian.el側でその出力を読み込むように改変する計画だった。Rustの方が書き慣れているし、一般的に非常に高性能で、優れたプロファイリングツールも揃っていることがわかっていたからだ。
こうした解決策を上流に取り込むのは難しいかもしれないが、自分用としては比較的小規模なプロジェクトで済むだろうとかなり楽観視していたし、この方法で自分の問題は確実に解決できると確信していた。
Claudeを使ってみる
思いつきで、この問題をClaudeがどれだけ解決してくれるか試してみることにした。あまりうまくいくとは思っていなかったが、とにかくやってみようと――もしかしたらClaudeが驚かせてくれるかもしれない。比較的明確な目標を持った、比較的よく定義されたリクエストから始めることにしたが、プロジェクトの大部分を一気にやってもらうよう頼んでみた:
[Uploaded file:
obsidian.el]
obsidian-update関数がひどく遅いです。関連するロジックをRustプログラムに移植して、関連する詳細をJSONで出力することで高速化したいと考えています。そのようなRustプログラムの最初のバージョンを書いてもらえますか? vaultのディレクトリはコマンドライン引数として受け取るようにし、その他の値はすべてデフォルト値に設定されていると想定して構いません。
要するに、これはそのまま動いた。初期バージョンはこちら。
単一のプロンプトで、chain-of-thoughtやその他の明示的な推論なしに、Claudeは約1,000行のEmacs Lispを読み込み、obsidian-updateに関わる約200行を特定し、そのコードとファイルの残りの部分との間のデータ境界を見極め、そのデータのためのJSONフォーマットを設計し、関連するロジックを約150行のRustに移植した。
生成されたコードは一発でコンパイルできて動作し、Claudeは必要な依存関係を含む動作するCargo.tomlまで出力してくれた。
さらに進んでみた――Emacs側も書けるだろうか?欲張って、Emacs adviceを使ってobsidian.elにパッチを当てるよう頼むことにした。上流のプロジェクトをフォークしたりパッチを当てたりすることなく、自分の設定に含められるものを作るのが狙いだった。そのプロンプトがこちら:
[Uploaded file:
obsidian.el]
[Uploaded file:obsidian-scan/src/main.rs]では、
obsidian.elがobsidian-scanを使うようにパッチを当てるelispコードを書いてください。元のファイルを直接編集するのではなく、elispの“advice”機能を使ってください。そうすれば、obsidian.elをロードした後にあなたが作ったスニペットを自分の設定に含められるようになります。
これもまた、ほぼそのまま動いた。バグは(私が気づいた限りでは)一つだけだった。Emacs側でタグの先頭に#を付け足していたのだが、Rust側ですでに#が含まれていたため、##writingのようなタグができてしまっていたのだ。Claudeに修正を頼んだ:
obsidian--tags-listに##writingのように##が二重になったエントリが入ってしまっています。Rust側とelisp側、どちらを修正すべきだと思いますか? どちらかを選んで更新してください。
Claudeはelispを選んで、些細なバグ修正を加えてファイルを書き直してくれた。これはこのプロジェクトにおける残りのやり取りすべてに共通する特徴だった――私が改善や修正を求めると、Claudeはせいぜい小さなつまずきがある程度で、それを実行してくれた。
このスタイルでさらに数回やり取りを重ねた。特に、Claudeに(ファイル -> タグ)のマッピングを出力させ、それをEmacsのobsidian-tag-findで使うように頼んだ。現在ではRustプログラムとelispパッチの両方を自分のシステム設定に追加し、これまでのところとても満足している!
プロジェクト全体にかかったのは、たった半日程度だった。
振り返り
Claudeは私が思っていた以上にコーディングが得意だった
最初のプロンプトをClaudeに投げたとき、正直あまり期待していなかった。ただ、Claude 3.5 Sonnetはコーディングが大幅に改善されたと聞いていたし、一般的になるべく定期的にLLMを試すようにしている――失敗を予想するときであっても。そうしないと、LLMの能力について正確な感覚を保つ方法が他に見つからないからだ。
まず最初の書き換えに感心した。明確な仕様があればClaudeは100行程度のコードなら問題なく書けるだろうと思っていたが、千行のelispの中から暗黙的な仕様を抽出し、その仕様に沿って実行してみせたことには驚かされた。さらに、システムを反復的に改善していく過程にも感心した。Claudeはコンテキストウィンドウから適度な量のコードを読み取り、それらを更新したり、かなり洗練された方法で応答したりするのが引き続き非常にうまかった。試したことのほぼすべてが「そのまま動いた」のだ。
これらのモデルが指数関数的なペースで改善していることや、6か月や12か月前に形成された印象はほぼ確実に大きく時代遅れになっていることは、頭ではよく理解している。しかし、その抽象的な理解だけでは、新たな現状について正確な印象を形成するには不十分だ!私が知る限り、そのための唯一の方法は、実際にモデルを使い続け、対話し続けること(そしてそうしている他者のレポートを読むこと)だ。失敗を予想しつつもこのプロンプトを試したのは、まさにそのためであり、この振り返りを書いているのも同じ理由からだ。
「感心すべき」なのだろうか?
このノートを書いている間だけでも、Claudeに対する自分自身の反応や考えに魅了されている。
上に書いたように、私の最初の反応は「驚きと感心」であり、Claudeは期待を上回った。元のobsidian.elは約10,000トークンの長さがある。Anthropicがそれだけのテキストをそもそも処理できる最初の公開モデルをリリースしたのは、2023年になってからのことだ。しかも私のタスクでは、Claudeは入力から単一の事実や要約された要点を抽出するだけでなく、ファイル中に散らばった数多くの細かな詳細を正確に理解し解釈する必要があった。モデルがそんなことをできるようになったのはごく最近のことだ!しかもそれを単一のプロンプトで、chain-of-thoughtや明示的な推論なしにやってのけたのだ。だから、ええ、感心したのだ。
同時に、この記事を書き進める間にも、自分の心の中でおなじみのゴールポストの移動が起きているのに気づいた!驚きや感嘆から、ほとんどシニカルともいえる矮小化へと自分の気持ちが移ろいでいくのを感じた。少なくとも二つの観点から:
- まあ、でも本来これくらいは予想すべきだったんじゃないか?多くのエンジニアがClaude 3.5 Sonnetのコーディング能力に驚嘆しているのを見てきた。Anthropicの面接問題を解くのを個人的に目にしたこともある。今回の課題はもう少しコード量が多かったが、はるかに大きなコードを読み込んで質問に答えるのを見たこともある。私が驚いたのは、私の期待値の問題であって、モデルが特別にすごいというわけではないのかもしれない!
- このタスクは本当にそれほどすごいことなのか?このプロジェクトは「たった」約1000行のコードしか扱っていない。私が普段取り組んでいるプロジェクトの多くは数十万行、時には数百万行に及ぶ!elispをRustに書き換えるのは聞こえはすごいかもしれないが、所詮は翻訳問題に「過ぎない」。根本的にはかなり明確に仕様が定まっているし、言語モデルは一般的に「翻訳っぽく見える」タスクがかなり得意なのだ。
この二つの見方――畏怖と矮小化――は、どちらも異なる意味で妥当だと思う!次のことはすべて事実だ:
- LLMのコーディング能力は1年前と比べてはるかに、はるかに向上している
- そしてさらに言えば、現在の能力は2〜3年前であれば間違いなく不可能なSFのように聞こえただろう
- しかし同時に、現在のモデルは良くも悪くも「同じトレンドの単なる継続」に過ぎない。印象的ではあるが、1年前にMLのインサイダーやリーダーたちが予測していた到達点とそれほど大きく変わらないようにも見える
- また、現在のモデルは印象的ではあるものの、依然として人間のエキスパートの性能には遠く及ばない。適切な文脈では有用だが、(まだ!)「私の仕事を代わりにやってくれる」わけではない
これは、今のMLやLLMの本質そのものなのだろうと思う。指数関数的な性能向上によって、私たちは常に、畏怖や驚嘆やSFのような体験と、「なんだ、また同じことの繰り返しか、大したことないじゃないか」という感覚との中間に永遠に置かれているのだ。
慎重ながらもワクワクしている
このプロジェクトを通じて、Claudeや他のLLMの支援を得てソフトウェアを作ることに対して、これまでにない個人的なワクワク感や熱意を感じたように思う。
具体的には、「自分のかゆいところに手が届く」ためにソフトウェアを書くこと――自分が抱えた問題を解決し、欲しかったツールを作ること――の喜びと興奮を、ここ何年もほとんど感じていなかった形で味わった。若い頃、特に学部生の頃は、自分で書いた、あるいは友人や僕らのような人々が書いたソフトウェアの断片に囲まれていた。それらは自分たち自身の問題を解決し、自分たちに新たな能力を与えるためのものだった。私たちは、自分たちのコンピューティング環境を自ら形作り、自分たちの道具を作り、望む体験を自ら作り出せるという、途方もない力と楽観と可能性、そして生きた確信を感じていた。この精神はさまざまなスケールに存在し、数人の友人の間で使われる小さなスクリプトにも、当時最大かつ最も成功したオープンソースプロジェクトの多くにも通じていた。
それからの年月で、世界は変わり、私自身も変わった。私たちのデータは、無関心から悪意に至るまでの独占企業によって所有・運営される、さまざまなサイロ化されたクラウドサービスの中にますます隔離されるようになった。あらゆるものがより複雑になり、抽象化と複雑さの何重もの塔の背後に隠され、不可解なOAuthの向こう側にある。私自身も、そうした組織で働くことについて読み、キャリアを過ごす中で、すべてのソフトウェアをあたかも大規模にデプロイされ何年も生き続けるかのように、反射的に書くことを学んだ。私はその働き方自体は楽しんですらいるし、堅牢に構築し、確固たる基盤を見つけるという挑戦も好きだ。しかし、Google APIへの認証方法を調べたり、JavaScriptフレームワークの新バージョンへ移行したりするたびに感じる絶望感と相まって、結局のところ、自分や友人のために小さなソフトウェアを作ることをほとんどやめてしまっていた。
今、久しぶりに、あの頃の興奮のきらめきを再び感じている!Claudeは、難解な認証システムを扱うことや、Reactの現行バージョンを把握することなどにおいて、私が到底及ばないほど優れている。まるで――今、あるいは少なくとも近いうちに――自分があったらいいなと思うツールやスクリプトを正確に記述さえすれば、あとはClaudeが面倒な作業をやってくれる世界に生きているかのように感じる。時が経ち、APIが非推奨になり、どこかのフレームワークのv27に移行しなければならなくなっても、Claudeならその作業もこなしてくれるだろう。いや、数年ごとにすべてのコードを捨て去り、同じ問題提起と最新のドキュメントのコピーを持ってClaudeとの新しいセッションでゼロからやり直す、そんな世界に生きているのかもしれない。希望と興奮のきらめきを感じる――この20年でソフトウェアエンジニアリングに忍び込んだ無数の厄介事の多くをClaudeが処理してくれ、そのおかげで高レベルの設計やコンセプトの段階に留まり、強迫的な完璧主義が細部に囚われるのを防げるような形で、自分用に、あるいはコミュニティのために、小さなソフトウェアを作ることを再び学べるだろうか?
ツールと戦っていたのであって、ツールと共に働いていたわけではなかった
Claude.app / claude.aiは、この種の問題に対してあまりうまく設計されているとは感じなかった。UIからディスク上のファイルへコピー&ペーストしたり、エラーやその他の出力をClaudeに貼り付けて返したりするのに多くの時間を費やした。
会話に適切なコンテキストを持たせる方法を見つけるのに苦労した。最終的に3つのファイル――元のobsidian.el、Rustに移植したmain.rs、そしてelispのアダプタコードであるobsidian-scan.el――を扱うことになった。1つの長い会話を使い続けるべきなのか、それともそれらすべてをProjectにまとめて定期的に新しい会話を作るべきなのか(いつ?)が明確ではなかった。1つの長い会話で作業するのはシンプルだが、コンテキストウィンドウが長くなるにつれて動作が遅くなり、履歴の中に見えるさまざまなバージョンのファイルにClaudeがやがて混乱することもわかった。Claudeが作成したartifactsをプロジェクトに戻すために「add to project」ボタンを使ってみたが、主に2つの課題があった:
- 生成されたartifactsについてClaudeと急速にイテレーションしていたため、Project内に古いバージョンが残ってしまう
- Claudeではartifactsの名前変更やタイトル変更ができなかったため、「Obsidian.el Performance Enhancement with Rust Scanner」という名前のファイルができてしまい、質問したり特定のファイルへの変更を頼んだりする際に参照しづらかった。
この分野は現在非常に活発に開発が進んでいる領域だ。適切なMCP serverや、数多くある新しいAIファーストなIDEのいずれかを使えば、もっとシンプルになった可能性もある。
とはいえ全体として、この経験はLLMを取り巻くツーリングやインターフェース設計が実際の能力に大きく遅れを取っており、将来のモデル改善とは別に、活発な実験と開発が必要な領域であるという私の信念を強めた。
定義されたインターフェースの間で作業する
Claudeと作業する際、私は本能的に、問題を比較的よく定義されテスト可能なインターフェースを持つ単位に分解することを選んでいた。例えば、Rustとelispの両方のコードへの変更を1つのクエリで頼むのではなく、Rust側に機能追加を頼み、出力をJSONで確認してスポットチェックしてから、対応するelisp側の変更を頼むようにした。
これがどれだけ必要だったかはわからないが、Claudeのためというより自分のためにそうしていたのだと思う。インターフェースの境界やtestabilityに基づいて分割することで、自分が何が起きているかを理解し検証できていると感じられ、混乱が生じたときにどう介入・デバッグすればよいかがわかるようになったのだ。これは、少なくとも部分的には、よりジュニアなエンジニアと働くときのやり方と似ている――システム設計や分解の多くを自分で担い、よく定義されたコンポーネントを委譲するようなものだ。
これは正確性やテストの面でも確実に役立つだろう。ClaudeのRustコードが元のelispとどれだけ一致しているかは、わざわざ検証しなかった。気づくバグがあるまでは、実際には気にしないからだ。しかし、「このファイルシステムツリーが与えられたとき、これらのタグやその他のメタデータを含むこれらのノートを見つけるべきだ」というのは非常にテストしやすい問題であり、もしもっと気になる場合やバグが見つかった場合でも、自分で内部を掘り下げるのではなく、テストケースとしてエンコードしてClaudeにコードの洗練を頼むことで解決できる。
少なくとも当面の間、これは強力なパターンであり続けるだろうと予想している。人間がインターフェースの境界やシステムの分解を定義し、Claudeがその「行間」で作業する、という形だ。おそらく時が経つにつれ、Claudeはより大きなサブシステムをほぼ自律的に扱えるようになり、私たちもより曖昧な仕様を持つものに対処するための自信や手法を身につけていくだろう。
コードはかつてないほど安くなった
しばらくの間、私はコード行数そのものは比較的安いものだという見方をますます強めてきた。ソフトウェア開発において価値ある資源は、むしろコードに対する理解――それが何をし、なぜそうするのか――や、何を書くべきかという知識のように思える1。
LLMは、少なくとも短期的には、このダイナミクスを大幅に加速させそうだ。今やわずか数セントで数千行のコードを生成できる。しかし、誰もそれらを理解することはなく、LLMは今のところ、それらのコードを生成することに比べれば、デバッグやリファクタリング、設計、保守は不得意だ。したがって、コードはかつてないほど安くなったが、洞察や優れたアーキテクチャ設計、そして理解は、少なくとも今のところ、これまで以上に価値あるものになるだろうと私は考えている。
この変化は、どのようなアーキテクチャパターンが意味をなすかにも影響するだろう。おそらく削除しやすいコードを書くことがこれまで以上に重要になる。そうすればモジュール全体を捨てて、Claude N+1やGPT M+1に最初からやり直してもらえばいいのだから。異なる言語エコシステムや性能特性の利点を享受でき、4つの難解な言語に精通したエンジニアを採用することをそれほど心配しなくて済むのであれば、「ポリグロット」なシステム――複数の異なるプログラミング言語をまたがるシステム――を構築するハードルははるかに低くなるかもしれない。これからが面白くなりそうだ。
NaurのProgramming as Theory-Buildingについて、いつか何か書きたいと思っている。私のお気に入りの論文の一つであり、この洞察を捉えたものとして私が知る限り最初のものだ。 ↩︎
記事をランダムに読む
コメント
ログインしてコメントする