tl;dr

自然言語、擬コード、コードは競合するツールではありません。異なる精度レベルに応じた異なる言語です。

AIを活用したプログラミングの時代において、プログラミングを学ぶことは、手で一行一行のコードを書くことだけではありません。あるアイデアがより正確になる必要があるとき、その翻訳をどのように制御し、人間がする決定とAIシステムが埋め込む決定を区別する方法について理解することも重要です。

Richard Feynmanからの引用

プログラミングがAIの時代に単純に消える理由を理解するには、より一般的な区別を考慮する必要があります。明確さと精度の違いです。

Richard Feynmanは数学について語ったとき、このアイデアを説明しました。

話しの実際の問題は、正確な言語ではありません。問題は明確な言語です。目的は、相手にアイデアを明確に伝えることです。文の意味が疑いがある場合にのみ正確性が必要であり、その疑いが存在する場所に正確性を置く必要があります。

何かを絶対的に正確に言えることは、その物が現実世界から抽象化されて現実の物を表さなくなった場合だけです。純粋な数学はまさに現実世界から抽象化されたものです。純粋な数学には、その特殊で技術的な主題を処理するための特別な正確な言語があります。

しかし、現実世界の対象と関係する場合、この正確な言語は何の意味もありません。特殊な細やかな点が正確に区別される必要がある場合を除き、それ以外ではただの冗長で混乱を招くものです。

興味深い点は、フェイマンが数学を批判しているのではなく、より形式的な主義がより深い理解を意味するという考えを批判している点です。

彼の主張は単純です:

  • 明確性よりも精度が重要である;
  • 本当に曖昧な場所ではのみ精度を導入すべきである;
  • 純粋な数学は、数学自身で定義された抽象概念を扱うため、完全に正確である;
  • 実世界について話すとき、常に近似、仮定、不完全な定義が存在する。

フェイマンが数学について話していたが、これはプログラミングについて考える際にも役立つ。プログラミングが純粋な数学と同様であるわけではなく、自然言語が曖昧であるため、プログラミングは形式的な精度を導入するからである。

この記事の中心思想は以下の通りである:

プログラミングを学ぶことは依然として重要である。なぜなら、困難な問題は単にコードを生成するだけでなく、曖昧な人間の意図と形式的なシステムがインストラクションを実行する際に共感を伴わないものとの間の翻訳を制御することである。

人々の言語とコンピュータの言語

プログラミングは手順を伝える方法であり、機械に何をしたいかを伝える方法である。

CPU(中央処理単位)は英語、スペイン語、Python、C++のように人間が理解できるわけではない。最も基本レベルでは、非常に単純な指令を実行するだけである:数字を読み、それらを比較し、加算し、メモリ内の別の場所からデータをコピーし、一つの指令から別の指令にジャンプする。

プログラミング言語は、後に解釈またはコンパイルして機械が実行できる指令に変換できる、より人間友好的な層で手順を記述するものである。

自然言語では、例えば:

このカードデッキを並べよ。

多くの解釈の余地がある。

それは、指令を受ける人間がカードとは何か、並べるとは何か、どの順序基準を使用するか、どのカードが他のカードよりも高いか、スーツが重要か、どのくらいの時間や労力が許容されるかについての知識を前提としている。

プログラミングでは、そのような決定はシステムのどこかで定義しなければなりません。

我々は常に手ですべての細節を書くわけではありません。なぜなら、プログラミング言語、ライブラリ、フレームワーク、および抽象化がすでに多くの決定を包摂しているからです。しかし、あるレベルで誰かがそれらの決定を正確に定義しなければなりませんでした。

自然言語とプログラミング言語は、同じものよりも良いまたは悪いわけではありません。それらは異なるツールです。AIがもはやプログラミングを学ばない必要がないかどうかを尋ねる問いは、なぜか拡大鏡を知っているからといって、顕微鏡を学ばない必要がないかどうかを尋ねるような問いに似ています。

ソフトウェア会社でソフトウェアがどのように作られるか

ソフトウェア会社では、ほぼすべてのことが自然言語から始まります。

クライアントが何を望んでいるかを説明します:

アプリケーションを管理するためにアプリが必要です。

または:

商品を販売するためのウェブサイトが必要です。

または:

項目を管理するためのシステムが必要です。

その最初の説明はほとんど正確ではありません。それはクライアントが自分の表現を悪くしているせいではなく、それがあまりに多くのドメイン知識をいくつかの文に圧縮しているためです。

その知識には習慣、例外、ビジネスルール、好み、エッジケース、以前の問題、そして明確な決定が含まれます。

その理由から、中間層が存在します。

製品マネージャー、ビジネスアナリスト、または機能アナリストがクライアントに話をして、質問をします。曖昧さを発見し、最初の要請をはっきりとした要件に変換します。

その後、テクノロジーリーダー、アーキテクト、またはプログラマがその要件をより具体的な技術的な決定に変換し、最終的にコードになります。

各レイヤーが問題を発見したり、変更を提案したりするかもしれません。しかし、どのレイヤーも上位レベルでされた決定を静黙的に上書きすべきではありません。

プログラマが矛盾を見つけた場合は、それを報告すべきです。製品マネージャーが要件を再構成した場合、その再構成は元のクライアントの要望に追跡されるべきです。

コードにアイデアを翻訳する3つの方法

以下のダイアグラムは、伝統的なソフトウェア開発、直接LLMベースのコード生成、そして提案された明示的な中間層を持つワークフローの3つのワークフローを比較しています。

伝統的なプロセス

クライアント
↓
アナリスト/PM
↓
テクノロジーレッド
↓
プログラマ
↓
コード

メソロジーや方法論のないLLM

ユーザー
↓
プロンプト
↓
LLM
↓
コード

ブラックボックスアプローチ

ユーザー
↓
自然言語
↓
擬コード+ボックス
↓
スキルに従ったLLM
↓
追跡可能なコード

人間がLLMを使ってコードを生成するときにはどうなるのか

人間が直接LLMとやり取りしてコードを生成するとき、中間層は完全に消失しない。それは明示的になくなる。

モデルはプロンプトを解釈し、欠損している詳細を補う、指示を優先させ、実装の決定を行う。しかし、多くの決定はどこにも記録されていない。

それが故に、多くのAIコードツールにはプランモードのようなものがあります。考えることを実装する前にモデルに求めることです。

問題は、このプランがしばしば一時的であることです。会話の中で閉じ込められ、最終コードと分離されたり、ユーザーによって無視されたりすることがあります。

ユーザーがプログラマーである場合、彼らは多くの時間をスペック、要件ドキュメント、アーキテクチャノート、コンテキストファイルを書くことに費やすことがよくあります。しかし、これは別の問題を引き起こします。数十の散らかりファイルが存在し、それぞれの権限、優先度、新鮮さが異なるからです。

仕様を慎重に書いても、問題はなくなりません。

LLMは矛盾を生じたり、空白を埋めたり、要件と一致しない決定を下すことがあります。これは自然言語の曖昧さから生じる場合もあります。また、モデルがファイルを読み込む順序によって生じる場合もあります。また、指示が衝突する場合もあります。そして、時にはLLMは確率的なシステムであるためです。

問題はLLMが決定を下すことではありません。

問題は、LLMがどの決定を下したのか、なぜその決定を下したのか、そしてそれらが人間がすでに決めたことと矛盾しているのかどうかを知ることではありません。

疑似コードとしての中間レイヤー

疑似コードは、伝統的なソフトウェア開発において、クライアント、マネージャー、アーキテクト、プログラマー間で分散される役割を果たします。

それは純粋な自然言語ではありません。なぜなら構造があるからです。

しかし、実行可能なコードでもありません。なぜなら、まだ意図、モジュール、責任について語ることができますから。

この中間レイヤーはLLMにとって特に役立ちます。なぜなら、LLMではしばしば過度に融合される二つの瞬間を分離するためです。

  1. システムが何をすべきかを決める;
  2. それをどのように実装するかを決める。

例えば:

IF user selects a subscription plan
  CALL create_checkout_session

IF payment succeeds
  CALL activate_subscription
  CALL send_confirmation_email
ELSE
  CALL show_payment_error

この疑似コードでは、Stripe、Paddle、Mercado Pago、独自のAPI、特定のライブラリのどれを使うかはまだ決めていません。

しかし、構造は定義しています。

  • プランがある。
  • 決済セッションがある。
  • サブスクリプションを有効にする手順がある。
  • 支払いが成功した場合の処理がある。
  • エラーが発生した場合の処理がある。

これは、次のような曖昧な依頼よりもすでにはるかに具体的です。

アプリに支払い機能を追加して。

それでも、最終的な実装コードほど固定されてはいません。

提案:Black Box Approach

提案する方法はシンプルです。

システムの各部分を、明確な境界、中間的な疑似コード、明示された意思決定の出所を持ち、段階的に詳細化できるブラックボックスとして扱う。

各アプリのプロジェクトには、次のものを含む blackboxes/ フォルダーを用意します。

  • YAMLで記述したボックス。
  • 共有するデータ構造。
  • 明示的に呼び出されたときにLLMが従うスキルファイル。
  • 意思決定の出所を記録するログ。
  • 必要に応じて、ボックスのツリーを表示する可視化ツール。

目的はコードを置き換えることではありません。

人間の意図からコードへの変換を、見えるようにし、編集できるようにし、検証できるようにすることです。

解決しようとしていること

1. 人間の判断とAIの判断

すべてのボックスとデータフィールドで、意思決定の出所を記録します。

  • human_defined
  • ai_defined
  • mixed

人間が確認した内容を、AIが黙って単なる推測に戻すことはできません。

たとえば、LLMがStripeを使うと決めた場合、その判断は最初は ai_defined として記録できます。

人間がその判断を確認すると、人間が承認した判断になります。それ以降、モデルは変更を明示的に提案しない限り、Stripeを別のサービスに置き換えるべきではありません。

2. 中間層としての疑似コード

ボックスには、複数の記述方法を組み合わせた疑似コードを使います。

CALL create_checkout_session
IF payment succeeds
WHILE there are pending invoices

システムを分解するために必要な構造は用意しますが、準備ができる前に実装の細部に埋もれてしまうほどの構造は持たせません。

3. モジュールの境界

一度に一つのブラックボックスを詳細化していきます。

上位のボックスは読み取り専用の参考情報です。変更できるのは選択した部分のツリーだけです。

これは、次のような依頼があったときに重要です。

支払いの流れを修正して。

関連する変更が明示され、承認されていない限り、この依頼によってLLMが認証層、データベースのスキーマ、通知システムまで書き換えてよいことにはなりません。

4. 人間が理解できる表示

可視化ツールなら、何十もの散在する仕様ファイルよりも、ボックスのツリーをずっと簡単に表示できます。

狙いは単なる文書化ではありません。意思決定の境界を保つことです。

最小限の例

支払いの流れを表すブラックボックスを簡略化すると、次のようになります。

box: payment_flow
goal: allow the user to pay for a subscription
provenance: human_defined

inputs:
  - selected_plan
  - user_id

pseudocode:
  - IF user selects plan
  - CALL create_checkout_session
  - IF payment succeeds
  - CALL activate_subscription
  - ELSE show_error_message

decisions:
  payment_provider:
    value: Stripe
    provenance: ai_defined
    status: needs_human_confirmation

constraints:
  - Do not modify auth_flow without human approval
  - Do not store sensitive card data

重要なのはYAMLそのものではありません。

重要なのは、すべての判断に記録する場所があることです。何を人間が指定し、何をAIが推測し、何がまだ確認を必要としているかを、システムが把握できます。

中間層は官僚的な手続きではありません。AIが曖昧さを、見えない判断に変えてしまうのを防ぐ方法です。

今、プログラミングを学ぶとはどういうことか

プログラミングを学ぶことは、もはや構文を暗記したり、すべての関数をゼロから書いたりすることではないのかもしれません。

それは、精度の異なる段階で考える方法を学ぶことなのかもしれません。

自然言語だけで十分なとき、疑似コードが必要なとき、実行可能なコードが必要なとき、そしてシステム全体に影響するため判断を記録すべきときを見極めることです。

プログラミングを学ぶと、次のことも身につきます。

  • 問題を分解する方法。
  • 例外的なケースを見つける方法。
  • データをモデル化する方法。
  • 生成されたコードを批判的に読む方法。
  • デバッグする方法。
  • 選択肢の利点と欠点を評価する方法。
  • メモリ、遅延、セキュリティ、並行処理、権限、費用といった制約について考える方法。

AIはコード生成を大幅に加速できます。

しかし、AIが生成するコードが増えるほど、そのコードを読み、疑問を持ち、実装すべき人間の判断と結び直すことが重要になります。

結論

「今でもプログラミングを学ぶ必要があるのか?」という問いの立て方自体が適切ではないのかもしれません。

プログラミングが構文を暗記し、コードの全行を手で書くことを意味するなら、今後はそうした作業が減っていくでしょう。

しかし、曖昧な意図が、判断し行動を実行する形式的なシステムへどう変わるのかを理解することを意味するなら、答えは明らかに「はい」です。

AIはコードを書けます。

それでも、変換を管理する人は必要です。

変換を管理するには、両方の言語を少なくとも部分的には理解する必要があります。


この記事で説明したBlack Box Approachは、概念実証の実験です。github.com/amiune/theblackboxapproach