Web開発
LLMのコードスタイルとトークンコストについて私が発見したこと
What I'm Finding About LLM Code Style and Token Costs (jimmont.com)
要約
本記事では、LLM(大規模言語モデル)が生成するコードが、モダンなWeb API標準と比較して冗長で古いパターンに偏りがちであり、その結果、トークンコストが大幅に高くなる問題について考察しています。著者は、クエリパラメータの解析やフォームデータの処理など、いくつかの具体的なコードパターンにおいて、LLMが非効率な実装を選択しがちであることを示しています。この問題を解決するためには、LLMにプラットフォームが提供するネイティブなWeb APIの使用を明示的に指示することが、トークン使用量の最適化とコード品質の向上につながると提言しています。
全文翻訳
LLMのコードスタイルとトークンコストについて私が発見したこと。それを共有するために出力トークンを消費する。価格が高騰する前に。ジム。事の始まり。この1年、私はClaudeを使って機能を作成し、レビューしてきた。トークン消費とレガシーパターンの間の緊張は注目に値する。何かが完成したと思うと、問題(リグレッション、エッジケースなど)が浮上する。その間ずっと、ゆっくりと着実に、そして自然に最終的なフルレート料金への移行が進んでいるのを目の当たりにしている。この現象と並行して、私はモダンなWeb開発の実用的な最先端に留まるための蓄積された推進力を感じている。ほぼ遍在する機能がコード行を削減し、品質を向上させるスイートスポットで、私はいつも不思議に思っている。なぜあの出力が得られたのか?何年も前から利用可能だったはずなのに、なぜあのコード行が現れたのか?私は通常、Claudeがせいぜいジュニアレベルであり、面接で問われる百科事典的知識の有用な近似であるという観察可能な事実でそれを片付けてしまう。しかし、何かを進めようとする中で、私は自分の実践を見直し、そのとんでもないトークン使用量がどこから来ているのかを探っている。それらのすべてが出力トークンであり、API料金では入力トークンよりも数倍(3~5倍!)高価なものだ。より長く、より脆く、より安全でないパターンで、プラットフォームが既に解決済みの問題(しばしば何年も前に)を解決しているのだ。まるでWeb Componentsなどを開発したライアン・ダール、そしてアレックス・ラッセル、ディミトリ・グラズコフ(その他多数)がWebプラットフォーム全体を再び素晴らしいものにしたまさにその時に、トークンからいくらかの利益を絞り出すために、Webプラットフォーム全体を後退させようとする陰謀があるかのように想像してしまうほどだ。だから、陰謀説のために、これが私が発見したことだ。
言語を使用し、タイポグラフィをデザインし、初期にプログラミングを行い、絵を描くなど多くの多様な趣味を持つ人間としての私の背景から、私はタブのようなものを実に素晴らしい革新だと考えている。インデントを文字通り1文字に減らすことができるのであって、誰かに定義を尋ねたり使用許可を得たりする必要のある抽象化ではない。(私はソフトウェアコミュニティ全体の排他的な態度を評価するにはあまりにも平等主義者すぎるのだろう)。私は人間を大切にし、物事が何らかの倹約的な基準内で機能することを望んでいる。そして、何かを4倍したり、任意の数で乗算したりするのは、私にとっては本当に意味をなさない。話は長くなるが、これはおそらく、実際のメディアで実際の言語を扱い、何が機能し、何が機能しないかについて意見を持つ人物の視点を確立するだろう。その部分はそれ自体を物語る傾向がある。私がこれを述べるのは、純粋に実用的な観点から私が調査した事柄に色合いを与えているからだ。私は誰もがタブを使うべきだという特定の立場を主張しているわけではない(それが自明であるにもかかわらず)。私は、私が抱いていた意見を形成した背景を開示している。常に経済的な議論が心の中にあり、それが今、実際のAPIコストとして現れているのだ。慣習に関する私の意見は本記事の主題ではない。トークン使用量の最適化こそが、私がここで共有したかったことだ。だからあなたも恩恵を受けられるだろう。もし複数のスペースを使い続けたいのなら、文献によればそれは問題ないとされており、LLMもそれ以上のことを知らない、と自分に言い聞かせよう。
地球上で最も簡単なトークン最適化はすでにランタイムにある。DenoやCloudflare Workersのようなランタイムは、Web APIサーフェス(URL、URLSearchParams、fetch、FormData、Headers、Request、Response、AbortController、ReadableStream、cryptoなど)をネイティブに実装している。これらはブラウザで実行されるものと同じオブジェクトだ。これはDenoが意図的に行ったアーキテクチャ上の選択であり、WinterCGがランタイム間の最小共通APIサーフェスとして形式化しているものであり、重要な実用的な結果をもたらす。すなわち、同じAPIサーフェスがブラウザとサーバーサイドの両方のコードをカバーするのだ。翻訳レイヤーも、シムも、適応コストも不要。プラットフォームはすでに、多くの問題を正しく、安全に、そして依存関係なしに解決している。Denoは、不足しているものがありクロスプラットフォームソリューションが必要な場合に、標準ライブラリを含んでいる点で特に注目に値する。LLMは、あなたが明示しない限り、あなたの環境についてこれを知らない。そのトレーニングコーパスは、これらのAPIが普及する前のNode.jsコード(require('url')、querystring.parse()、Expressミドルウェアパターン、カスタムタイムアウトラッパー付きのaxios、フォーム解析用のmulterなど)に支配されている。それらのパターンが、モデルが学習した中で統計的に優勢なのだ。モデルはそれらのパターンに手を伸ばす。モデルがデフォルトで選択するものと、プラットフォームがすでに提供しているものとの間のギャップに、出力トークンのコストの大部分が存在する。
パターン別の大きさ。私はこれを進めるにあたり、トークン経済学を推定してきた。これらは概算であり、正式な研究ではなくパターンの実際の長さに基づいているが、比率は十分に一貫性があり有用だ。
クエリパラメータの解析
```javascript
// モデルのデフォルト — 手動解析(約140トークン)
const parts = rawUrl.split('?');
const pairs = parts[1] ? parts[1].split('&') : [];
const params = {};
pairs.forEach(p => {
const [k, v] = p.split('=');
params[decodeURIComponent(k)] = decodeURIComponent(v);
});
```
```javascript
// Web API(約12トークン)
const params = Object.fromEntries(new URL(rawUrl).searchParams);
```
およそ140トークン対12トークン。発生ごとに約90%の削減だ。手動バージョンは、不正なキーでサイレントに失敗し、繰り返しパラメーターの場合、最後の値以外のすべてをサイレントに破棄し、キーが__proto__である場合はプロトタイプ汚染のベクトルとなる。ネイティブバージョンは、仕様によってこれらすべてを処理する。
フォームデータ
```javascript
// モデルのデフォルト — フィールドごとの状態管理(3フィールドのフォームで200+トークン)
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [role, setRole] = useState('');
const handleChange = (e) => setFields({ ...fields, [e.target.name]: e.target.value });
```
```javascript
// Web API(約14トークン)
const data = Object.fromEntries(new FormData(event.target));
```
モデルはすべてのフィールドに対して状態追跡と変更ハンドラを生成する。ネイティブバージョンは、フォーム全体を1回の呼び出しで取り込む。フィールド数に応じて約200~250トークン対14トークンとなり、ネイティブバージョンは20フィールドでも同じコストでスケーリングする。
フェッチのライフサイクルとキャンセル
```javascript
// モデルのデフォルト(約90トークン)
let timer;
const controller = new AbortController();
timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
```
```javascript
// Web API(約12トークン)
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
```
手動バージョンは、リファクタリング中にfinallyパスが省略されるとタイマーがリークする可能性がある。ネイティブバージョンは管理すべきライフサイクルがない。
失敗分離を伴う並列非同期処理
```javascript
// モデルのデフォルト(約100トークン)
let anyFailed = false;
const results = await Promise.all(
tasks.map(t => t.catch(e => {
anyFailed = true;
return null;
}))
);
if (anyFailed) { /* now what? */ }
```
```javascript
// Web API(約10トークン)
const results = await Promise.allSettled(tasks);
```
Promise.allSettled()は、各タスクに対して「fulfilled」または「rejected」の.statusと、対応する値または理由を含む構造化された結果を返す。手動バージョンではエラーの詳細が失われ、使用ごとに新しいアドホックなステータス規則が発明される。
UIコンポーネント
```javascript
// モデルのデフォルト — カスタムモーダル(JSライフサイクル管理で約250トークン)
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// ... aria属性、キーボードトラップ、背景クリックハンドラ ...
```
```html
// セマンティックHTML(約25トークン)
<dialog ref={ref}>...</dialog>
// ブラウザがフォーカストラップ、Escapeキー、アクセシビリティツリー、背景を処理
```
<dialog>は2022年以降、すべての主要ブラウザでサポートされている。<details>/<summary>によるアコーディオン、ネイティブな<form>の制約検証(required、type="email"、pattern、minlength)など、これらは決して難解なものではない。モデルは、トレーニングデータに存在するJavaScript実装に頼る。指示されない限り、これを続けるだろう。Denoの完全なリクエストハンドラ。複合