ハクソク

世界を動かす技術を、日本語で。

ユーザーが受け入れ基準を最初に定義することでLLMの効果が最大化される

概要

  • LLM生成のRust製データベースは、SQLiteと比べて極端に遅いことが判明
  • コードは正しく動作し、テストも合格するが、根本的な設計ミスで実用性に乏しい
  • 性能劣化の原因は、クエリプランナーやトランザクション管理など複数のバグと安全志向の設計判断
  • LLMは「もっともらしさ」を優先し、本質的な要件や最適化を見落とす傾向
  • 表面上は正しいが本質的に不適切な実装が生まれるリスクを検証

LLM生成Rustデータベースの性能問題

  • SQLiteとLLM生成Rust実装で100件の主キー検索を比較
    • SQLite:0.09 ms
    • LLM Rust版:1,815.43 ms(約20,171倍遅い)
  • Turso/libsqlとは無関係なプロジェクトであり、TursoはSQLiteのCコードベースをフォークしたもの
  • TursoのベンチマークはSQLiteの1.2倍程度で、成熟したフォークとして妥当な性能

コードの外観と実態

  • Rust実装は動作し、テストも合格
    • SQLiteファイルフォーマットを読み書き可能
    • READMEにはMVCC、C API互換、ファイル互換性を謳う
  • 実際は著しく遅く、運用に耐えない

LLM生成コードの失敗パターン

  • LLMは「もっともらしさ」を最優先
    • 正しく見えるが実用性や最適化が欠如
  • 受け入れ基準を事前に明確化することが重要
  • 他の研究(METR, GitClear)でも同様の傾向が確認されている

主な性能劣化の原因

  • 主キー検索がB-tree検索にならない
    • id INTEGER PRIMARY KEYがrowidとして認識されず、全件スキャン(O(n²))
    • SQLiteはWHERE id = NでO(log n)のB-tree検索を実現
    • Rust実装はWHERE rowid = ?のみ高速経路
  • INSERT時に毎回fsyncを実行
    • トランザクション外のINSERTごとにフル同期
    • SQLiteはfdatasync(メタデータ同期省略)をデフォルト使用し高速化

細かな性能低下要因

  • ASTクローンの多用:SQLパース済みASTを毎回複製・再コンパイル
  • ヒープアロケーション乱用:キャッシュヒットでも4KB Vec<u8>を新規確保
  • スキーマの毎回リロード:autocommitごとに全CREATE TABLEを再パース
  • ホットパスでの不要なフォーマット:毎回SQL文字列へ変換
  • オブジェクトの逐次生成破棄:トランザクションやVDBEなどを毎回生成・破棄

安全志向の設計判断の弊害

  • Rust所有権や安全性を重視した結果、極端な性能低下
  • SQLiteの高速化はC言語だけでなく、26年の最適化の積み重ね

過剰設計の別事例

  • ビルドアーティファクト削除のための巨大なRustプロジェクト
    • 82,000行、192依存、36,000行のダッシュボードなど
    • 実際にはcronの1行ジョブで十分
    • cargo-sweepやOS標準の回復手段も未活用

LLM生成コードの本質的な問題

  • 「求められたもの」を忠実に生成するが、「本当に必要なもの」とは限らない
  • 表面的には正しく、テストもパスするが、本質的要件や最適化を満たさない
  • 問題の本質は構文や文法の誤りではなく、要件や最適化の見落とし

まとめと教訓

  • LLM活用時は受け入れ基準・ベンチマーク・設計意図の明確化が不可欠
  • 「見た目が正しい」だけではなく、「本当に正しい」かを検証する仕組みの重要性
  • 安全志向や最新技術の導入が、必ずしも実用性や効率性に直結しない事例

Hackerたちの意見

そう、信頼できるテキスト予測ってまさにその通りだよね。でも、著者がプロンプトにベンチマークを含めてたかどうか気になるな。隠れた要件を隠しておくのはちょっとフェアじゃないよ。
これを「隠れた要件」とするのは危険な道だね。Claude Codeや似たようなツールを使った経験から言うと、「隠れた要件」には以下が含まれるかもしれない。* DESIGN.mdが最新であることを確認する * ソースを変更した後はテストを書いたり更新したりして、ちゃんと通るか確認する * ユニットテストだけじゃなくて統合テストも追加する * 現在のタスクに関係ないコードはリファクタリングしない … これらはプロジェクトや言語特有の指示じゃなくて、ソフトウェアエンジニアリングでは常識や良いプラクティスとされてることなんだけど、時々コーディングエージェントにそれを守るようにお願いしなきゃいけなかったこともあったよ。(TypeScriptのコードベースで「any」を使わないように何回強調しなきゃいけなかったか知りたい?)人々は単にこれがコーディングツールの限界だって認めるべきだと思うし、それでも意味のある議論はできるはず。
調べたところ、問題のSQLiteの再実装はFrankensqliteで、数日前にHacker Newsに載ってた(ただしフラグが付いてたけど): https://news.ycombinator.com/item?id=47176209
彼らのデフォルトの解決策は、掘り続けることなんだ。どんどんコードが増えていく悪循環を生むよ。もしあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになる。コードが遅いって言ったら、最適化された高速パス(もっとコード)、専門的なルーチン(もっとコード)、カスタムデータ構造(さらにもっとコード)を追加しようとする。そして、そのコードが生み出した問題を修正するために、さらにフラクタルにコードが増えていく。バグがあるって文句を言ったら、バグごとに10個の特注テストが必要になるよ。さらに、前のフレームワークが目的に合わなかったら、新しいモッキングフレームワークが毎回作られるし。重複を統一してほしいって言ったら、「問題ないよ、新しいメタモック抽象アダプターフレームワークがあって、すべての機能セットのスーパーセットに加えて、古いコードと新しいコード用の2つの新しいメタモックドライバーもあるよ!新しいアダプターのテストを書いてほしいなら教えてね。」って言うんだ。
> 重複を統一してほしいって言ったら、「問題ないよ、新しいメタモック抽象アダプターフレームワークがあって、すべての機能セットのスーパーセットに加えて、古いコードと新しいコード用の2つの新しいメタモックドライバーもあるよ!新しいアダプターのテストを書いてほしいなら教えてね。」って言うんだ。実際には5つの重複セクションのうち3つしか移行してなくて、今は死んでるコードは一切削除してないのに。
上から下に進めることを強くおすすめするよ。コーディングを始める前に、ちゃんとしたアーキテクチャをアウトラインするようにさせるんだ。それから、もしモジュールの一つが混乱し始めたら、そのモジュールのためにクリーンなコンテキストから始めて、悪い経験から得た注意点や教訓を取り入れる。LLMは、君が指摘した理由から、同じコードを作業したり再作業したりするのがまだ得意じゃない。でも、実装プロセスを何度も繰り返して正しくする「グラウンドホッグデイ」的なアプローチには結構向いてるよ。
コードの削除やクリーンアップのコーパスで訓練されたLLMがあってもいいかもね。
だから、ほとんどのプログラマーの労働力を置き換える準備ができていないって言う人たちに混乱するんだよ。
私の感覚では、コード生成は早いけど、その後に実装が適切で正しいか、しっかりテストされているか、正しい前提に基づいているか、技術的負債を生まないかを確認するのに数時間かかるんだよね。手動でコーディングする時も同じことをやるけど、AIツールが悪いコードを出力するスピードを考えると、もっと重要になってくる。
> もし彼らがあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになるよ。プランモード使ってる?昔はあまり良くないアプローチをして問題を掘り下げることが多かったけど、計画を立てることでそれがなくなった気がする。
皮肉を言うつもりはないけど、敬意を表して言うと…これはスキルの問題だよ。ツールなんだから。すごく効果的で能力のあるツールだと思う。なんでこんなに多くの人が似たような体験を語る中で、私の体験がこんなに違うのか分からないけど…ほぼ毎回、入力が出力を決めるって結論に至る。 > もし彼らがあまり良くないアプローチで何かを実装したら、後で制限にぶつかるたびにワークアラウンドや冗長なコードを追加し続けることになる。そう、プロンプトや指示が広すぎて、どうやってやるべきかを示すガードレールやガイドラインがないと、こうなるよね。プランモードを使ってないなら、それはスキルの問題。実装が始まる前に、すべてを整理しておく必要がある。もし実装が「あまり良くない」アプローチで行われたら、それはあなたの責任だよ。 > コードが遅いって言ったら、ふぅ。分かった。コードが遅いって言わないでしょ。自分の同僚に「ねぇ、君のコード遅いよ」って言って、いい結果が期待できる?コードのベンチマークをお願いして、どうやって最適化できるかを聞くんだ。それからその選択肢について話し合う(ここで前の段落の部分をやって、あまり良くないアプローチにならないようにする)まで進めて、気に入ったアプローチにたどり着いて、モデルが状況を理解していることを示したら、計画を受け入れてモデルに作業を始めさせる。この時点で、基本的にはアプローチを指示して、何もバカなことをしないようにしているはず。そうしたら、実行するだけで、あなたが設定した計画のパラメータや範囲内に収まる(もしバグがあるって言う代わりに具体的にバグについて言ったり、どう解決してほしいかを言わないと、脱線することもあるけど)。 > バグごとに10個の特注テストを持つこともできるし、前のフレームワークが目的に合わなかったら新しいモッキングフレームワークを毎回作ることもある。この点では、モデルが非常に無能だということには同意する。テストやテスト環境、モッキングに関して何が問題なのかを誰かが研究する必要がある。これに対する解決策は、掘り下げたり、無限に回ったりする問題の解決策と同じだよ…プロンプトや会話の初めに、最終結果に対する期待を述べる。出力を早めに定義して、その後に文脈を説明したり提供したりする。プロンプトや会話の早い段階で「要件」を設定すればするほど、より強固になる。テストについても全く同じ。自分でテストを書いて、モデルにテストから機能を構築させるか、モデルに計画された出力の一部としてテストを最初に構築させてから、事前定義されたテストから機能を埋め込ませる。テストシステムや環境の設定については非常に具体的に説明し、テスト関連の問題にぶつかった時にはモデルにそのことと解決策をTESTING.mdドキュメントにメモさせる。AGENTS.mdやCLAUDE.mdなどには、モデルがテストを扱う場合はTESTING.mdドキュメントを参照するように指示しておく。個人的には、機能に焦点を当てて、統合して動作する状態まで持っていって、ステージングや本番環境にプッシュする準備ができたら、モデルにその動作するシステムやソリューション、機能について分析させてテストを書かせる。一般的に、モデルに対するテスト環境のメモは、使用中の基本的なテストフローやプロセス、フレームワークを説明する段落のようなもので、どう動いてほしいかを伝える。慣習に従えば従うほど、うまくいくよ。そして、プランモードを使って。
この理由で、GitHub Copilotのチェックポイント復元/フォーク会話機能を多用してる。ほとんどの場合、何かが脱線するよりも巻き戻した方がいいから。
一回のセッションで回復できないほど悪化させないようにしよう。定期的に技術的負債を整理するためのセッションを行うことが大事だよ(ドキュメントも含めてね)。
動いている時にコードのリファクタリングを頼むのが解決策なのかな。
この記事は素晴らしいね。ブログの記事の見出しも面白いけど、間違ってる。LLMは一般的に、妥当なコードを書くわけじゃない(原則として)。彼らはただ、トレーニングデータで見たコード(クラスター)に(意味的に)似たコードを書くんだ。それがRLHFやRLVRによって制限されていない限り。これは覚えておくのが難しくないし、生成的LLMが実際に何をしているのかを簡略化した正しい説明だよ、単純すぎる比喩に頼る必要はない。
その通り。分布外の領域に入ってしまうのも簡単だよ。いくつかのtree-sitterクエリを頼んで、Gemini 3、Opus 4.5、GLM 5が新しい指示を幻覚するのを見てみて。
確か、トレーニングデータの中で一番多いのはPythonだよね。その次がWeb技術(HTML、JS/TS、CSS)。これは開発者が一番多いことに対応してる。多くの人が一つの技術にキャリアを捧げてるし、私たちはソフトウェア開発全般を指すのに同じ言語を使い続けてる。これによって同じコミュニティの一員になれるけど、誤解の元にもなってる。一部の人は、物事をその本質で考えず、業界のリーダーを見て自分たちが何を考えるべきかを判断する傾向がある。そういう人たちは、開発タスクを解決する知能エージェントについて一貫して話してる。同じ抽象的な言語を使って、統一感のあるイメージを持たせてるんだ。これが、共通の問題を解決している私たちに、業界が終わったと信じさせる原因になってる。
100%そう思う。自分がLLMより賢いと思って、何を求めているかを知っていると思っているけど、実際はそうじゃない。LLMに自分が達成したいことに基づいて解決策を考える余地を与えてあげて。要件を伝えるけど、自分が考えた解決策を出させようとしない方がいい。そうすると、返答が強制的になって質が下がるから。
完全に運転してる人に依存してる。
現在、LLMを使ってクエリを書いてる。たくさんのテストを書かせて、差分テストを行ってコードとテストを正しくしてから、クエリを最適化させてる。バックエンドで動かすために最適化は必須だし(大きなテーブルでたくさんの行を処理してるから)、テストがなければ全く機能しない。テストだけじゃなくて、かなりのカバレッジが必要だよ。もしエッジケースがカバーされていなかったら、最適化中に洗い流される可能性が高いから(そもそもコードが正しいかどうかもあるけど)。過去にはエッジケースを手動で追加しなきゃいけなかったけど、時間が経つにつれてワークフローは改善されてきた。プランナーは使ってないけど、自分のワークフローを設定してる(テストを修正したり、差分テスト中にコードを修正するためには、コンテキストを隔離したエージェントが必要だから)。もしプランナーが広範なテストカバレッジやパフォーマンスフィードバックループ(あるいは非常に攻撃的でよく知られた最適化)を追加できたら、うまくいくかもしれない。
LLMが生成したコードを見てると、ある意味では正しい(テストに合格する)けど、要件を満たしてない(めっちゃ遅い)っていうのが面白い。プライマリーキーを特定するのにis_ipkを使わず、すべてのステートメントでfsyncを使ってる。こういう大きなプロジェクトでは、どんなに優秀でも、コードの行数が多すぎて、ちゃんと読み解くのが難しいんだよね。著者がこのプロジェクトを読むために時間をかけたのはすごいと思う。ほとんどの人は絶対にやらないし(もちろん著者も含めて)。今のLLMはオートコンプリートとして使うのが一番いいと思う。コードの塊が小さくて、書いてる時に慎重にレビューできるから、Claudeは大体合ってる(時々ひどく間違えるけど)。オートコンプリートだとそれを見つけやすい。そうやって、ほぼ設計通りに動いて、人間の負担も完全に管理可能になるし、生成されたコードの理解も深まる。オートコンプリートの時に30%くらいの確率でミスをするけど、これは結構大きい(ミスが必ずしもバグとは限らないけど、見栄えが悪いコード、遅いコード、重複したコード、間違ったコードなど)。AIが大部分のコードを生成するのは、計画や監視に時間がかかるし、レビューやメンテナンス、診断が難しくなる。パフォーマンス向上にはあまりつながらないと思う。元のコードのライセンスを無視して、トレーニングデータにすでにあるコードを生成したいだけなら別だけど。
ちょっとした指摘/質問なんだけど、「LLM」って生のAPIコールで得られるものだよね?claude.aiやchatgpt.com、Claude Code、Windsurf、Cursor、Excel Claudeプラグインなどのハーネスを使ってLLMを利用している場合、LLMを使っているわけじゃなくて、もっと別のものを使ってるってことだよね?よく聞く例が「LLMは記憶や時間の理解がないから___」なんだけど、エージェントには様々なレベルの記憶がある。これを会議やランダムなコメントで説明しようとしてるんだけど、もし私が大きく外れていなければ、正しい用語は何になるべきだと思う?LLMベースのエージェント?
全然外れてないよ。私の考え方はこうだね: - LLM = モデルそのもの(ステートレス、ツールなし、ただのテキスト入出力) - LLM + システムプロンプト + 会話履歴 = チャットボット(ほとんどの人がChatGPTやClaudeなどでやりとりするもの) - LLM + ツール + メモリ + オーケストレーション = エージェント(アクションを取れる、状態を保持できる、APIを使える) 誰かが「LLMには記憶がない」と言った場合、生のモデルについては正しいけど、Claude CodeやCursorはエージェントだから、コンテキストやツールへのアクセスがあって、インタラクションを通じて状態を維持できる。業界ではその最後のカテゴリーに「エージェントシステム」や単に「エージェント」という用語が定着しつつあって、中間のものには「チャットボット」や「アシスタント」が使われてる。混乱の原因は、製品名(ChatGPTやClaude)がこれらの境界を曖昧にしているからで、人々は「LLM」と言うとき、全体のスタックを指していることが多い。
> ちょっとした指摘/質問なんだけど、LLMは生のAPIコールで得られるものだよね?LLMとやり取りするには、何らかのハーネスが必要だよ。普通のWeb API(特にホスティングされた商業システムのためのもの)は、LLMをラップした非最小限のハーネスで、ツールの組み込み、ツールコールの解釈、APIコールのコンテキスト構造をプロンプトに変換するための「プロンプトテンプレート」としてローカルツールチェーンで公開されているものの適用を行う(場合によっては、バックエンドでプロンプトを構築するために使われる会話状態の管理をサポートすることもある)。 > もしclaude.aiやchatgpt.com、Claude Code、Windsurf、Cursor、Excel Claudeプラグインなどのハーネスを使ってLLMを利用している場合、LLMを使っているわけじゃなくて、もっと別のものを使ってるってことだよね?基本的に、あなたがソフトウェアスタック全体を書いている人で、消費しているのがモデルの重みだけ、あるいはトークン化の前に何の変換もされない本当に最小限のハーネスを使っている場合を除いて、常にLLM以上のものを使っていることになるよ(設定とプロンプトを受け取って、トークンからテキストに戻す以外に変換やフィルタリングを行わずに結果を返す)。でも、そういう複雑なフロントエンドを使っている場合(WebでもCLIでも他の何かでも)、プロバイダーのWeb APIを使っている時よりも、LLMの上にかなり多くのものを使っている可能性が高いね。
> バイブスだけじゃ足りないよ。正しいって何を意味するのか定義して、それを測定しないと。ほぼその通りだね。私はこれをずっと主張してきた。自動化には意図が必要で、比較には測定が必要だよ。爆風半径やリスクプロファイルも、事前にどれだけカバーする必要があるかを理解するために重要だ。著者が評価について言及してるけど、この文脈ではAI評価(AI evals)と呼ばれることが多い。私が見たいのは、これらの評価が実際に証明可能なユーザーストーリーの共通言語になることだ。そうすれば、科学者やビジネスの人、ソフトウェア開発者など、異なる役割間の断絶がなくなる。私たちが共通の言語を話せて、どんなバックグラウンドを持っていても簡単に書いたりメンテナンスできるようになれば、コラボレーションがしやすくなって、人々を力づけて、制御を失わずに素早く動けるようになるよ。 - [1] https://ai-evals.io/ (または実用的なリポジトリ:https://github.com/Alexhans/eval-ception)
人間もこういう風に働くのが一番いいよね。