LLM(ChatGPT5)と共同でPythonを書いていて気付かされたこと
最近ChatGPTにPythonコードを書かせていると意外で想像を超えるようなコードを出してくることが有り、自分の見識の狭さを知らされました。それについてとプログラミング言語と及びそれを使う人の変化についての私見を書き連ねます。あとに進むに連れハルシネーションが増大していきます。
Dataclass、型ヒントの多用
Pythonの動的型付言語としての柔軟性は辞書型(dict),tuple,可変長引数、lambda関数の簡便な使い勝手によく現れています。辞書やtupleに変数追加も容易なこともあって*argsや**kwargsを使った関数、クラスの引数は長大になりがちです。また出力は動的にtupleを作ることで複数変数を返すこともできます。これらの安易な利用が可読性を下げて行ってしまう問題が有りますが、最近では辞書(dict)ではなくdataclassを使う方法が一般的になりつつあります。
10年以上前のJava,C++のClass、構造体定義に見られた煩雑さを取り除き、デフォルト値、型ヒント、メソッドのなどの動的定義ができます。しかし最近のJava,C++、そしてRustはそうでもなくstructに後からメソッドを追加するような書き方が主流になっているような感じではあります。
ChatGPTが書いたコードの1プログラム内で閉じる処理ですら関数間の値の受け渡しでdataclassが使われ、人間やAIが後でプログラムを修正、機能追加する場合は最初に想定された制約を知る、思い出すのを容易にします。書き捨てのような使い勝手をしていて一見無駄ですがあとからインターフェース仕様とみなすことも容易だと思います。
def ot_allocate_bits_for_model(
model: nn.Module,
dataloader,
loss_fn,
device: torch.device,
config: q.OTQuantConfig = q.OTQuantConfig(),
) -> q.OTQuantResult:
# 1) 感度推定
(中略)
return q.OTQuantResult(
assignment=q.QuantAssignment(layer_names=layer_names, bit_indices=bit_indices),
P=P.detach().cpu(),
target_counts=counts, cost_matrix=C.detach().cpu() )
@dataclass
class OTQuantConfig:
bits: List[int] = None # 例: [2,4,6,8]
avg_bits: float = 6.0 # 目標平均
epsilon: float = 0.01 # Sinkhorn 温度
sinkhorn_iters: int = 500
sens_batches: int = 1 # 感度推定に使うバッチ数
def __post_init__(self):
if self.bits is None:
self.bits = [2,4,6,8]
もともとはOTQuantConfigはsinkhorn_fisher.py内で定義されていたものを切り出したものです。当初のコードでは一回しか使われない想定でしたが、データとしてのまとまりがあり類似の関数を作る際に切り出すことができ使い勝手が良かったです。
一方でitertoolsは意外と使われませんでした。コードの簡潔さと有名でない関数を使うことによる可読性の低下のバランスがLLMの学習したコードの範囲内、すなわちインターネットの総意的にはここなのだと思います。
C++でいうalgorithmも同様な機能をもっているものの個人的には高度すぎる関数はなかなか覚えづらく使いこなせず、同じような傾向になるのかもしれません。
型ヒントは静的型付け言語で見られるようなコンパイル、実行時に型の不整合を検出して
想定外であれば停止するのとは異なりあくまでコーディング、メンテナンス時のデータや関数の機能を思い出すヒントという位置づけです。LLMが学習、検索した既存のプログラムを使い回す場合も伝統的なコンパイラとは異なり、どうも人間と同じような類推でプロジェクト、レポジトリ内外で呼び出せる関数を検索しているようです。これはプログラム生成に失敗している場合でも人間による解読と修正が容易になっているという点が有ります。
動的にモジュールのメンバーを呼び出そうとする
驚いたのはgithubに置いたコードのリンクを示しその中の関数、クラスを組み合わせたコードを書いてくださいとChatGPT5に言ったときに以下のような予想外のコードを書いてきました。
def eval_S(S: List[List[str]], log_steps: bool) -> Tuple[List[List[str]], Optional[List[int]]]:
"""
Evaluate each S-expression to its result tokens (ss).
If steps-aware API exists, also return steps per sample.
"""
eval_with_steps = smart_getattr(evl_mod, [
"eval_list_with_steps",
"evaluate_list_with_steps",
"eval_with_steps",
])
if eval_with_steps is not None:
try:
ss, steps = eval_with_steps(S)
return ss, (steps if log_steps else None)
except Exception:
pass
eval_list = smart_getattr(evl_mod, [
"eval_list",
"evaluate_list",
"evalall",
"evaluate",
"main",
])
if eval_list is None:
fail_with_attributes(evl_mod, "S-expression evaluation")
ss = eval_list(S)
return ss, None
def smart_getattr(mod, candidates: Sequence[str]) -> Optional[Callable]:
"""Return first existing attribute (callable) by trying candidates in order."""
for name in candidates:
if hasattr(mod, name):
fn = getattr(mod, name)
if callable(fn):
return fn
return None
def fail_with_attributes(mod, purpose: str) -> None:
attrs = [a for a in dir(mod) if not a.startswith("_")]
raise AttributeError(
f"Cannot find a suitable function for {purpose} in module {mod.__name__}.\n"
f"Available attributes:\n- " + "\n- ".join(attrs)
)
モジュールを動的にimportしたあとsmart_attrで”それっぽい”名前の関数を検索し、なければ例外で終了します。渡したコードの全ての内容を全て覚えて理解しているわけではなさそうなのでこのような手法を使うことになったようです。ここでも型ヒントがあればヒントになりますし、Pythonではdataclass,C++ではheaderファイルを読み込まればインターフェースの情報のみがLLMに伝わり品質、効率の点ではまともなプログラムが生成されるでしょう。
短く書くための糖衣構文や(Rustなどの)マクロを使うコードよりは、冗長でもすべて明示してあるコードが、今後は圧倒的に「良いコード」とされていくでしょう。
と書かれていますが、どれほどLLMが進歩しても有限のコンテキスト長に少しでも情報を乗せるために糖衣構文や使い勝手の良いマクロはむしろ多用され、事実上の言語標準になっていく可能性すらあると思います(あるいはよく使われる定型的な記述が1トークンとみなされるでしょうが、そうなったらその単位が(マクロとして)人間の書くプログラミング言語に逆輸入されそうです。)。
動的型付けよりも静的型付けの言語がますます好まれるようになり、最終的なE2Eテストを行いやすいアーキテクチャが好まれるようになり、中長期的にはcoding agentにとって良いコードとは何かという議論が盛んに行われるようになるでしょう。
あまり静的型付けの言語での実用的なプログラムをLLMに書かせたことはないのですが、上記のようにPythonも静的型付け言語に近づく傾向が明確に見られます(静的型付けを学習した結果が転移しているのかもしれません)。さらにJava,Rustでも有名ライブラリとその公開される型、インターフェースを多用した記述が主流となり、LLMに使ってもらえるかが人気ライブラリ評価の点で重要になってきそうです。
ただしそれはソフトウェアモジュールの組み合わせとしてのプログラミングの容易さの観点であり、限られたコンテキスト長、会話の記録の中で異なるプログラミング言語、パラダイムシフトを使った大規模ソフトウェアシステムを作るのに必須であろう部品、モジュールへの設計の分割とその組み合わせとしての全体設計、必要なインターフェースの定義という仕事は残ります。
そのようなアーキテクト的な部分もAIに職を取られてしまうのか、既存の教科書に載っているような有名なソフトウェアアーキテクチャと計算機の物理的限界、物理法則や法律、各種業界慣習をいい感じに組み合わせた”クリエイティブ”でかつ部分、全体テストを行いやすいような全体設計をどれほどできるのかは非常に気になります。
前出のブログでは
設計はコーディングよりも重要になっていきます。書きながら全体の設計を頭の中でしていくスタイルのソフトウェアエンジニアリングは廃れてゆき、いかにcoding agentにわかりやすい設計書を書けるかがソフトウェアエンジニアリングの重要なスキルになっていきます。
coding agentのマネージメントスキルも重要です。もうすでに、coding agentのタスク管理、指示出し、成果物の確認だけで、仕事時間のほぼ全てを使っています。
とも書かれています。複数人でのソフトウェア開発ではリーダー格のエンジニアには当然必要とされるスキルですが、これからは新卒に近いジュニアエンジニアもAIの管理が主要な仕事になって来ると思われます。
少し前〜現代のWeb系ソフトウェアエンジニアがアセンブリどころかC言語も読み書きできない、設計の上流工程に携わるシステムエンジニアがプログラミング言語に触れる機会が少ないと言ったことが弊害として語られることが多かったですが、これからはソフトウェアエンジニアが設計とAIとの会話、管理にのみ注力し、コーディングの品質とそれによる性能への影響に無頓着になりがちなのが問題視されてくるかもしれません。
生成AI時代のPython, Programming言語?
ここでは豊富な既存ライブラリを持つ動的型付け、GCのあるスクリプトの代表としてPythonの事例を書きました。
最も極端な考えは自然言語でプログラミングを行うこと、あるいは勝手に会話やドキュメントを読み取ってその内容に沿って動作するソフトウェアにしてくれることであり、想定外利用、物理故障も含んだ異常な状態を含んだ網羅的な条件の整理とテスト実行までLLMが行ってくれるのが理想です。
プログラミング本体とそのテストコードを同時に生成することには両者の知識が混ざり合ってテストが甘くなるのではないかという懸念が有ります。プログラミング本体とテストコードの対を学習させたLLMを作成する、専門に特化したコーダーLLMとテスターLLMのGAN的な対決により品質を高めるなどの方法も考えられますがこれらは大規模データの学習を伴うプロジェクトになりそうです。
製品コードまたは仕様書とテストコードどちらかの設計資産が豊富にあるのであればそれをプロンプト、RAGに用いてもう片方を生成するという手法もあるかもしれません。
それとは対極に静的検証、自動証明支援との組み合わせによってテストに依存せずに生産性、品質を向上させられるのではないかという期待があります。
しかしC++,Java,Rustのようなより静的型が充実した言語では厳密な型にしたがって設計要素を多分に含んだコーディングが行われるので完全に静的検証に頼ることは少なくプログラマーが適切と思ったレベルで静的検証を行いつつ開発しているとも言え、むしろそちらの方が現在までのプログラミング、ソフトウェア開発の主流派です。
さて、流行る形式手法の条件はざっくりいうと以下の3点にまとめられます。
検証力・表現力は限定されているがちゃんと動く
テストと相補的である
DXを向上させる
じつはこのような条件を満たしている形式手法が既にあります。
それは「型システム」と呼ばれています。
特にTypeScriptやRustでは型の強さと型推論のバランスの取れた型システムが採用されており、
流行っていることは疑いの余地はないでしょう。
静的検証が活躍しているのは、普通のプログラミング言語とは異なる特性が求められるハードウェア記述言語、システムや通信プロトコルの仕様記述言語、COBOLなど古い言語の再利用、移植時の品質保証、航空宇宙、原子力、自動車などのような高い安全性が求められる分野のようです。
まとめ?と所感
生成AIの進化によって既存のプログラミング言語の進化は止まる、あるいは人間がプログラムを書かなくなるのではないかという予想が有ります。
生成AIは自分のような古い知識しか持たない人間にプログラミング言語の最新の機能を自分が必要とする事例に基づいて示してくれるので、短期的にはプログラマーのスキルの底上げに繋がりそうです。それによって各プログラミング言語の新機能の追加が加速する可能性も有ります。Pythonの例をみると業務ドメインとプログラミング言語の資料領域があまり相関していないせいか似たような記述になり異なるプログラミング言語の文法や機能が収斂してく可能性も考えられます。
異なるプログラム、システム間をつなぐMCPが注目されていますがシステムの一部品として完結する部分はたいてい単一のプログラミング言語で書かれ、物理学や医学、機械、金融などの各産業固有の高度な専門知識とそのからの推論が必要です。その専門知識に基づいた、推論も行うことで生成AIは各分野の専門家に驚異、脅威をもたらしていますが、人間の持つ最先端の知見をソフトウェアに実装しようという意図を簡単に実現してくれるのもまた生成AIです。各分野の専門家がそれぞれの専門用語を駆使した自然言語でプロンプトを書き、そこそこの品質のコード、ソフトウェアを生成、実行結果を得られるようになってきています。
その専門家(医者、法律家、数学者、物理学者、プラントエンジニア等々…)の中にプログラマーが入ることはできるのでしょうか。大学以上の専門教育の中では知識だけでなくそれぞれの分野特有の思考や推論の方法を学びます。そのようなものがソフトウェアエンジニアリング、ソフトウェア工学と呼ばれる分野には十分に存在するのでしょうか。
あるとすると応用数学の諸分野、データサイエンス中でも特に統計学とAIそのもののアーキテクチャ、性能、数学的性質に関する知識と実践、ハードウェア寄りの知識(電気回路設計、半導体等の物性、データセンターでのノード間通信、電力、伝熱工学、光、量子コンピュータに使う光学、物理学の知識と実践)、ソフトウェアの品質保証などを兼ね備えた比較的高度な人材が挙げられるかもしれません。特にソフトウェアの品質保証はAIが生成したコードの最終的な妥当性の判断に必ず必要になりますが、これが独立した分野なのか、あるいは他の分野の専門知識(人命のコストは限りなく高いが金融や他のビジネスでは製品不具合コストとテストコストはトレードオフ)と密着して分離不可能なのか、工業で用いられる伝統的な統計学から独立したものなのかは自分には判断が付きかねます。
Discussion