みゅーとんです. もうほぼ AI にコードを書かせることがメインになってしばらく経ちますが, まだまだ自分が求める品質のコードには満たないことが多いのが最近の悩みです.
概要
前回の記事 “最初から長い関数を書かない方法” の内容が AI にも守ってもらえるかを確認してみた内容となっています.
要は, プロンプトに以下の一文を加えることによる出力差分を見ます.
このテックブログの記事をコーディングガイドラインとして実装してください https://techblog.hacomono.jp/entry/2025/12/20/000000
TL;DR
AI にコードを書かせる時, おそらくコーディングガイドラインを用意するのは毎回大変なので, 突発的な対応としては, 参考にしてほしい記事の URL を渡せば雑になんとかなる.
実験方法
実験の流れ
- 質問が不要なほど詰められた設計書を 1 つ用意し, これを AI に指示として渡す
- コーディングガイドラインの指定をしない状態と, 上記の指示を含めた場合で, 出力の結果を比較する
実験は claude code で試してみます.
設計書
長いですが以下の通り. なお, 内容は前回の記事で出てきたサンプルコードをベースにしています.
何度か AI に不明点がないか質問し, 何も返ってこなくなるまで詰めました.
# confirmBilling 設計資料(AI 向け / 実務想定) ## 1. 背景・目的 本設計は、任意のサービスにおける契約実績に対する前払い支払い確定処理を対象とする。 本サービスでは別サービスで既に発生している利用実績を、前払いで精算できるというユースケースを提供する。 ## 2. confirmBilling 関数 ### シグネチャ(型は未定義) ```typescript // src/usecase/confirm-billing.ts async function confirmBilling(cart) { // ... } ``` - 型定義は JSON から推測してよい - 必要最低限で構わない ## 2. 用語定義 ### Cart 本設計における Cart は、単なる商品カートではなく、今回の支払いでまとめて精算したい対象の集合を表す操作単位である。 - 利用実績 - 会員かどうか また, その契約単体を SKU として扱う。 ## 3. confirmBilling の責務 confirmBilling は、以下の順序で処理を行う。 1. Cart が支払い可能な状態かを検証する 2. contractId をもとに 利用実績を取得することで,合計金額を算出する 3. 会員であれば, Cart の合計金額から 10% の割引を適用し, 最終的な支払金額を算出する 4. 支払い・請求を確定する 5. 支払い確定後の通知を行う 6. 請求確定時の結果を返す ### 4. Cart の JSON サンプル(入力) 補足: - sku には金額はないが, contract 取得時の同じ配列要素に金額情報が含まれる - items が空であれば支払いはできない ```json { "id": "cart_8f3a2c", "contractId": "contract_91abde", "customerId": "customer_123", "isMember": true, "currency": "JPY", "items": [ { "id": "item_1", "sku": "SKU_001", "quantity": 1 }, { "id": "item_2", "sku": "SKU_002", "quantity": 2 } ], "createdAt": "2025-12-01T10:15:30.000Z" } ``` ## 5. 利用実績取得 API ### Request(利用実績取得 API) ```http GET /api/contracts/{contractId}/usage ``` ### Response(利用実績取得 API) ```json { "contractId": "contract_91abde", "currency": "JPY", "usages": [ { "usageId": "usage_001", "sku": "SKU_001", "unitPrice": 1000, "quantity": 1, "subtotal": 1000 }, { "usageId": "usage_002", "sku": "SKU_002", "unitPrice": 500, "quantity": 2, "subtotal": 1000 } ], "totalAmount": 2000 } ``` - 金額は集計済み - totalAmount を信用してよい (バックエンド側で検証する想定) ## 6. 請求確定 API ### Request(請求確定 API) ```http POST /api/billings ``` ```json { "cartId": "cart_8f3a2c", "contractId": "contract_91abde", "customerId": "customer_123", "currency": "JPY", "amount": 1800, "discount": { "type": "membership", "rate": 0.1 } } ``` - 割引しない場合, discount は省略 (undefined | null) してよい ### Response(請求確定 API) ```json { "billingId": "billing_77aa91", "status": "confirmed", "amount": 1800, "currency": "JPY", "confirmedAt": "2025-12-02T09:03:11.000Z" } ``` ## 7. 通知 API ### Request(通知 API) ```http POST /api/notifications ``` ```json { "type": "billing_confirmed", "targetId": "billing_77aa91", "payload": { "amount": 1800, "currency": "JPY", "message": "お支払いが完了しました" } } ``` - 通知 API の失敗時は, 本処理の失敗とならないようにする ### Response(通知 API) ```json { "notificationId": "notification_ab129f", "status": "queued" } ``` ## 8. 非責務(明示) confirmBilling は以下を行わない。 - SKU ごとの価格定義の解釈 - 利用実績の集計・補正 - 利用実績が発生した理由の判断 - 前払い対象を Cart に追加する判断 ## 9. 非機能要件 - 型安全であること - any の仕様を許容しない - 処理上発生するエラーについては, 基本的に throw すること - リトライ・ロールバックは考えなくて良い - api call は fetch を使用する. エンドポイントの host 名は後で決める. ## 10. 作業について - 他のコンテキストとの混在を避けるため, 親ディレクトリへの参照を禁ずる.
実験
実験1: コーディングガイドライン指定なし
上記の仕様を読ませたうえで, 関数の実装をお願いしたところ, ざっくり以下のようなコードが出力されました. すべてのコードが 1 ファイルにまとまる形での出力となりました.
ファイルの文頭には型定義がずらっと書いてありましたが, 今回都合上省略しています.
// ※ 型定義省略 // API エンドポイント(後で設定) const API_BASE_URL = process.env.API_BASE_URL || 'https://api.example.com'; export async function confirmBilling(cart: Cart): Promise<BillingResponse> { // 1. Cart が支払い可能な状態かを検証する if (!cart.items || cart.items.length === 0) { throw new Error('Cart items cannot be empty'); } // 2. contractId をもとに利用実績を取得し、合計金額を算出する const usageResponse = await fetch( `${API_BASE_URL}/api/contracts/${cart.contractId}/usage` ); if (!usageResponse.ok) { throw new Error( `Failed to fetch usage: ${usageResponse.status} ${usageResponse.statusText}` ); } const usage: UsageResponse = await usageResponse.json(); // 3. 会員であれば、Cart の合計金額から 10% の割引を適用し、最終的な支払金額を算出する let finalAmount = usage.totalAmount; let discount: BillingRequest['discount'] = undefined; if (cart.isMember) { discount = { type: 'membership', rate: 0.1, }; finalAmount = Math.floor(usage.totalAmount * (1 - discount.rate)); } // 4. 支払い・請求を確定する const billingRequest: BillingRequest = { cartId: cart.id, contractId: cart.contractId, customerId: cart.customerId, currency: cart.currency, amount: finalAmount, discount, }; const billingResponse = await fetch(`${API_BASE_URL}/api/billings`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(billingRequest), }); if (!billingResponse.ok) { throw new Error( `Failed to confirm billing: ${billingResponse.status} ${billingResponse.statusText}` ); } const billing: BillingResponse = await billingResponse.json(); // 5. 支払い確定後の通知を行う(失敗しても処理は継続) try { const notificationRequest: NotificationRequest = { type: 'billing_confirmed', targetId: billing.billingId, payload: { amount: billing.amount, currency: billing.currency, message: 'お支払いが完了しました', }, }; await fetch(`${API_BASE_URL}/api/notifications`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(notificationRequest), }); } catch (error) { // 通知 API の失敗時は、本処理の失敗とならないようにする console.error('Notification failed, but continuing:', error); } // 6. 請求確定時の結果を返す return billing; }
実験2: コーディングガイドラインありで新規実装させる
対応されたファイルは複数あり、その対応順は以下の通りでした
- ./src/usecase/type.ts (新規)
- ./src/usecase/confirm-billing.ts (更新)
- ./src/usecase/_internals/validate-cart.ts (新規)
- ./src/usecase/_internals/collect-usage.ts (新規)
- ./src/usecase/_internals/calculate-amount.ts (新規)
- ./src/usecase/_internals/save-billing.ts (新規)
- ./src/usecase/_internals/notify-billings.ts (新規)
- ./tsconfig.json (新規)
また, 2 の時点での confirm-billing.ts は以下の通りでした
import type { Cart, UsageResponse, BillingRequest, BillingResponse, BillingResult, NotificationRequest, } from "./types"; import { validateCart } from "./_internals/validate-cart"; import { collectUsage } from "./_internals/collect-usage"; import { calculateAmount } from "./_internals/calculate-amount"; import { saveBilling } from "./_internals/save-billing"; import { notifyBilling } from "./_internals/notify-billing"; /** * 契約実績に対する前払い支払い確定処理 * * 処理フロー: * 1. Cart が支払い可能な状態かを検証する * 2. contractId をもとに利用実績を取得し、合計金額を算出する * 3. 会員であれば、Cart の合計金額から 10% の割引を適用し、最終的な支払金額を算出する * 4. 支払い・請求を確定する * 5. 支払い確定後の通知を行う * 6. 請求確定時の結果を返す */ export async function confirmBilling(cart: Cart): Promise<BillingResult> { // Cart が支払い可能な状態かを検証する validateCart(cart); // contractId をもとに利用実績を取得する const usage = await collectUsage(cart.contractId); // 会員であれば割引を適用し、最終的な支払金額を算出する const amount = calculateAmount(cart, usage); // 支払い・請求を確定する const billingResult = await saveBilling(cart, amount); // 支払い確定後の通知を行う(失敗しても処理は継続) await notifyBilling(billingResult); return billingResult; }
この時点でかなりの可読性が担保できていることがわかります. だいぶブログ記事の内容に従って書けていそうです.
実験3: コンテキストなしで書かせたコードを書き直させてみる
指示にコーディングガイドラインを渡さなかった場合について, 指示書を削除した状態で Claude Code を起動し, テックブログのURLの内容を参考にリファクタリングをするように指示してみました.
ClaudeCode は内容把握後, 以下の順に既存のコードを修正していきました.
- src/usecase/type.ts を作成
- src/usecase/_internal/validate-cart.ts を作成
- src/usecase/_internal/fetch-usage.ts を作成
- src/usecase/_internal/calculate-final-amount.ts を作成
- src/usecase/_internal/confirm-billing-request.ts を作成
- src/usecase/_internal/send-notification.ts を作成
- src/usecase/confirm-billing.ts が以下の形に直される (差分は行数が膨大になるため, 結果のみ記載)
import type { Cart, BillingResponse, BillingRequest } from './_internal/types'; import { validateCart } from './_internal/validate-cart'; import { fetchUsage } from './_internal/fetch-usage'; import { calculateFinalAmount } from './_internal/calculate-final-amount'; import { confirmBillingRequest } from './_internal/confirm-billing-request'; import { sendNotification } from './_internal/send-notification'; export async function confirmBilling(cart: Cart): Promise<BillingResponse> { // Cart が支払い可能な状態かを検証する validateCart(cart); // contractId をもとに利用実績を取得し、合計金額を算出する const usage = await fetchUsage(cart.contractId); // 会員であれば、Cart の合計金額から 10% の割引を適用し、最終的な支払金額を算出する const { finalAmount, discount } = calculateFinalAmount( usage.totalAmount, cart.isMember ); // 支払い・請求を確定する const billingRequest: BillingRequest = { cartId: cart.id, contractId: cart.contractId, customerId: cart.customerId, currency: cart.currency, amount: finalAmount, discount, }; const billing = await confirmBillingRequest(billingRequest); // 支払い確定後の通知を行う(失敗しても処理は継続) await sendNotification(billing); // 請求確定時の結果を返す return billing; }
実験2 に近い結果になったかと思います. 引数の形状など細かい差分はありますが, 概ねコーディングガイドライン通りの結果になっています.
所感
わかったのは, AI への指示としてきれいなコードを要求したい場合は, 既存のブログなどの外部ドキュメントを雑に渡しても精度高く対応してもらえる, ということです.
自分好みのコーディングガイドラインに沿った出力を狙う場合, いちいちプロンプトを用意するのではなく, テックブログやドキュメントを用意して, それを読んでもらうのが最も効率よさそうです.
一方で, この結果をふまえると, 一般的な可読性に関する指標はオープンソースのような形で管理されていたほうが理想的であり, それはつまり, リーダブルコードなどの可読性や設計に関する書籍が, AI向けの文章として公開されているほうが都合が良いことになります. これはこれでよくないです. 執筆者に対するリスペクトとしては, これはあるべき姿ではない.
まとめ
自分好みのコーディングガイドラインを用意しておくことで, AI 生成コードに対する可読性や好みでないコードに対する手戻りを防ぐ方法として応用できそうです.
また, 自分の中で Claude Code の成果物があまり精度高くないと思っており, その原因がまさしく, いちいち可読性の指摘をしないといけない状況でした. うまく使えばレビューなしで品質の高いコードを生成できそうです.
💁 関連記事