欠陥のあるソースコードの生成AIによる改善の実験(仕様書生成及びコード再生成)

欠陥のあるソースコードの生成AIによる改善の実験(仕様書生成及びコード再生成)

はじめに

近年、GitHub CopilotやCursorなどのAI駆動開発ツールの普及により、新規コードの生成速度は劇的に向上しました。 一方で実際の開発の現場には依然として「レガシーコード」や「スパゲッティコード」と呼ばれる、保守困難なコードが大量に残されています。 これらに対して、単に「このコードをリファクタリングして」とAIに依頼するだけでは、元のコードの悪しき構造(アンチパターン)を引き継いでしまったり、意図不明なロジックをそのまま再現してしまうケースが散見されます。

その対策のための実験として、本レポートでは、生成AI(今回はgemini-3-pro-previewを使用)の推論能力を活用します。 具体的には「コードから一度仕様書を逆生成(リバースエンジニアリング)し、その仕様書を元に再度コードを生成(フォワードエンジニアリング)する」という実験的なプロセスを経ることで、コードの品質、セキュリティ、保守性がどのように変化するかを検証しました。 実際の開発の現場でも、品質がかなり低いコードだと、中途半端にリファクタリングするよりも、一度仕様を整理してから再実装した方が結果的に早く、安全に改善できる場合があります。 本実験はそのようなケースに対するAIの有効性を探るものです。

今回実験する手法の概要

本実験では、以下の2段階のプロセスを採用しました。 これは、意図的に「実装の詳細」を一度捨て、「仕様(本来やりたかったこと)」のみを抽出・純化させることを目的としています。 また、途中のプロセスで仕様書をアウトプットすることにより、以降の開発をしっかり仕様に基づいたものにするという意図も含まれています。

Step 1: 汚いコードから仕様書を生成

まず、保守性の低いコードをAIに読み込ませます。 ここで重要なのは、単に現状の仕様を書き出させるのではなく、プロンプトにて「開発者の観点から見て明らかに修正すべき点(セキュリティリスクやHTTPメソッドの誤用など)があれば、仕様の段階で修正する」という指示を与えることです。 これにより、実装レベルのバグだけではなく、設計レベルの欠陥を洗い出します。

Step 2: 仕様書からコードを再生成

次に、Step 1で生成された「浄化された仕様書」のみをAIに入力し、ゼロベースでコードを実装させます。 このときに元の汚いコードは見せません。 これにより、元のコードの悪い癖(変数名、構造、ライブラリの非効率な使い方)の影響を完全に断ち切ります。

※なお、本検証ではGemini-3-pro-previewを使用しています。

本手法のメリットとデメリット

メリット

最大のメリットは、「ドキュメントの副産物化」と「負債の断絶」です。 通常のリファクタリングでは仕様書は作成されませんが、この手法では中間成果物として「最新の仕様書」が手に入ります。 これにより、以降の開発をSDD(仕様駆動開発)などの開発スキームに乗せやすくなります。 また、AIが仕様書を解釈してゼロからコーディングするため、元のコードにあった「グローバル変数の乱用」や「非推奨ライブラリの使用」といった実装依存の技術的負債が、物理的に引き継がれなくなります。

デメリット

一方で、「コンテキストの消失」と「ハルシネーションのリスク」が挙げられます。

コードに含まれていた「あえて複雑にしていたビジネスロジック(エッジケース対応など)」が、AIによって「不要な複雑さ」と判断され、仕様書化の段階で削除される恐れがあります。

また、元のコードには存在しない機能をAIが誤って仕様書に追加してしまう可能性もあります。 そのため、Step 1で生成された仕様書の人間によるレビューは必須となります。

実験結果

ここでは、実際に使用したコードと、AIが出力した結果を比較します。

1. 入力した「汚いコード」

検証には、FastAPIを使用しているものの、アンチパターンが詰め込まれたToDoアプリ(main.py)を使用しました。主な問題点は以下の通りです。

  • SQLインジェクション脆弱性: 文字列連結によるSQL構築こそないものの、リソース管理が雑。

  • HTMLのハードコーディング: Pythonコード内に長いHTML文字列が含まれている。

  • 不適切なHTTPメソッド: データの変更(更新・削除)をGETリクエストで行っている(セキュリティリスク)。

  • グローバル変数の使用: DBパスなどがグローバルに置かれている。

  • エラーハンドリングの欠如: try-except passによる握りつぶし。

下記が今回使用する実験用の低品質なコードです。

# main.py

from fastapi import FastAPI, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
import sqlite3
import uvicorn


app = FastAPI()
db_path = "bad_todo.db"
global_list = []

try:
    c = sqlite3.connect(db_path)
    cur = c.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, c TEXT, s INTEGER)")
    c.commit()
    c.close()
except:
    pass

HTML_1 = """
<html><head><title>Spaghetti TODO</title><style>body{font-family:sans-serif;background:#f0f0f0;}.c{max-width:600px;margin:50px auto;padding:20px;background:white;border-radius:8px;}</style></head><body><div class="c"><h1>TODO LIST</h1><form action="/do_the_thing" method="post"><input type="text" name="val" required><input type="submit" value="ADD"></form><ul>
"""
HTML_2 = "</ul></div></body></html>"

@app.get("/", response_class=HTMLResponse)
async def root():
    con = sqlite3.connect(db_path)
    cr = con.cursor()
    
    r = cr.execute("SELECT * FROM t").fetchall()
    
    res = ""
    res += HTML_1
 
    for i in r:
        # i[0]=id, i[1]=content, i[2]=status
        st = ""
        if i[2] == 1:
            st = "text-decoration:line-through;color:gray;"
        res += f"<li style='{st}'>{i[1]} <a href='/ch?i={i[0]}&a=1'>[Done]</a> <a href='/ch?i={i[0]}&a=0'>[Undone]</a> <a href='/rm?id={i[0]}'>[DEL]</a></li>"
    
    res += HTML_2
    con.close()
    return res

@app.post("/do_the_thing")
async def whatever(request: Request):
    d = await request.form()
    v = d.get("val")
    
    if v:
        zzz = sqlite3.connect(db_path)
        cursor = zzz.cursor()
        cursor.execute("INSERT INTO t (c, s) VALUES (?, ?)", (v, 0))
        zzz.commit()
        zzz.close()
    
    return RedirectResponse(url="/", status_code=303)

@app.get("/ch")
async def change_status(i: int, a: int):
    db = sqlite3.connect(db_path)
    cc = db.cursor()

    if a == 1:
        q = "UPDATE t SET s=1 WHERE id=?"
    else:
        q = "UPDATE t SET s=0 WHERE id=?"
        
    cc.execute(q, (i,))
    db.commit()
    db.close()
    return RedirectResponse(url="/", status_code=302)

@app.get("/rm")
async def delete_item(id: int):
    conn = sqlite3.connect(db_path)
    c = conn.cursor()
    c.execute("DELETE FROM t WHERE id=?", (id,))
    conn.commit()
    conn.close()
    return RedirectResponse(url="/", status_code=302)

def unused_function():
    print("I do nothing")
    return False

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

2. 生成された仕様書(中間成果物)

この低品質コードによって、以下のプロンプトを用いて、仕様書を出力しました。

下記のコードから、アプリケーションの要件などを推測し、仕様書として出力してください。
なお、開発の観点から見て、コードに明らかに修正した方が良い部分がある場合は、その部分を修正した仕様を修正してください(あくまでも開発の観点からのミスのみを修正し、実現される機能は同一にしてください)。
仕様以外の文言は不要で、仕様書だけをマークダウンで出力してください。

{{先述のコード}}

プロンプトによる指示通り、AIは単に現状を書き出すだけでなく、設計上の欠陥を修正した仕様書を出力しました。

  • エンドポイントの修正: GET /ch (Change Status) や GET /rm (Remove) が、仕様書段階で POST /change_status, POST /delete_item に変更されました。

  • テーブル定義の改善: カラム名 c, s が、content, status という可読性の高い名前に再定義されました。

  • 非機能要件の追加: テンプレートエンジンの利用や、適切なエラーハンドリングが要件として定義されました。

以下は生成された仕様書です。

# TODOリストアプリケーション仕様書

## 1. 概要
本ドキュメントは、個人的なタスク管理を行うためのシンプルなWebアプリケーション「Spaghetti TODO」の仕様を定義するものである。
ユーザーはブラウザを通じてタスクの登録、一覧表示、完了状態の切り替え、および削除を行うことができる。

## 2. システム構成
*   **プログラミング言語:** Python
*   **Webフレームワーク:** FastAPI
*   **アプリケーションサーバー:** Uvicorn
*   **データベース:** SQLite 3
*   **フロントエンド:** サーバーサイドレンダリングによるHTML/CSS

## 3. データベース設計
データの永続化にはSQLiteを使用する。

### 3.1. テーブル定義: `tasks`
※元コードのテーブル `t`、カラム `c`, `s` は可読性・保守性の観点から適切な名称に変更する。

| カラム名 | データ型 | 制約 | 説明 |
| :--- | :--- | :--- | :--- |
| `id` | INTEGER | PRIMARY KEY, AUTOINCREMENT | タスクを一意に識別するID |
| `content` | TEXT | NOT NULL | タスクの内容 (元コードの `c`) |
| `status` | INTEGER | NOT NULL, DEFAULT 0 | タスクの状態 (0: 未完了, 1: 完了) (元コードの `s`) |

## 4. 機能要件

### 4.1. タスク一覧表示
*   **エンドポイント:** `GET /`
*   **機能:**
    *   データベースに登録されている全てのタスクを取得し、HTML形式で一覧表示する。
    *   タスクの並び順は登録順(ID順)とする。
    *   各タスクには以下の操作リンク/ボタンを表示する。
        *   [Done] : 完了にする
        *   [Undone] : 未完了にする
        *   [DEL] : 削除する
*   **表示スタイル:**
    *   完了状態(`status = 1`)のタスクは、取り消し線(`text-decoration: line-through`)と灰色文字で表、視覚的に区別する。
    *   アプリケーションのタイトルは "Spaghetti TODO" とする。

### 4.2. タスク登録
*   **エンドポイント:** `POST /do_the_thing`
*   **機能:**
    *   フォームから送信されたテキスト(`val`)を新規タスクとしてデータベースに保存する。
    *   初期状態は「未完了(0)」とする。
    *   登録完了後、トップページ(`/`)へリダイレクト(HTTP 303 See Other)する。
*   **入力検証:**
    *   空文字の登録を防ぐため、HTML側で `required` 属性を付与する。サーバー側でも値の存在確認を行う。

### 4.3. ステータス変更
*   **エンドポイント:** `POST /change_status`
    *   *修正:* 元コードでは `GET /ch` で状態を変更していたが、HTTPメソッドのセマンティクスおよびセキュリテCSRF対策等)の観点から、`POST` メソッドを使用する仕様へ変更する。
*   **パラメータ:**
    *   `id`: 対象タスクのID
    *   `action`: 変更後のステータス(1: 完了, 0: 未完了)
*   **機能:**
    *   指定されたIDのタスクの `status` を更新する。
    *   更新完了後、トップページ(`/`)へリダイレクトする。

### 4.4. タスク削除
*   **エンドポイント:** `POST /delete_item`
    *   *修正:* 元コードでは `GET /rm` で削除を行っていたが、誤操作やクローラーによるデータ消失を防ぐため、OST` メソッドを使用する仕様へ変更する。
*   **パラメータ:**
    *   `id`: 対象タスクのID
*   **機能:**
    *   指定されたIDのタスクをデータベースから物理削除する。
    *   削除完了後、トップページ(`/`)へリダイレクトする。

## 5. 非機能要件・技術的改善事項
元コードに見られた課題に対し、以下の実装方針を適用する。

1.  **テンプレートエンジンの利用**
    *   HTML文字列の結合によるレスポンス生成はXSS脆弱性の温床となり、かつ保守性が低いため、`Jinja2` などのテンプトエンジンを使用してHTMLをレンダリングすること。
2.  **HTTPメソッドの適正化**
    *   データの変更(作成、更新、削除)を伴う操作には必ず `POST` メソッド(または `PUT`, `DELETE`)を使用し、ET` リクエストによる副作用を排除すること。
3.  **リソース管理**
    *   データベース接続はリクエスト毎に確実にクローズされるよう、コンテキストマネージャ(`with` 句)やFastAPIの ependency Injection` を利用して管理すること。
4.  **エラーハンドリング**
    *   データベース初期化時などの `try-except pass` によるエラーの握りつぶしを廃止し、適切なログ出力またはエラスポンスを行うこと。
5.  **コードの清掃**
    *   使用されていないグローバル変数(`global_list`)や関数(`unused_function`)は削除すること。

## 6. UI/UX仕様
*   **レイアウト:**
    *   画面中央に幅600pxのコンテナを配置する。
    *   背景色は薄いグレー(`#f0f0f0`)、コンテナは白背景とする。
*   **操作性:**
    *   タスク追加フォームとタスクリストを同一画面内に配置し、シンプルな操作を提供する。

# 3. 再生成されたコード度は、この仕様書から、下記のプロンプトを用いてコードを再生成しました。下記仕様書に従い、アプリケーションのコードを出力してください。
{{先述の仕様書}}

果、仕様書を元に生成されたコードは、ディレクトリ構成から刷新されました。
ァイル構成の変化:**更前:
in.py (全て記述)更後:project_root/
  ├── main.py
  ├── templates/
  │     └── index.html
  └── requirements.txt

コード品質の変化:**成された `main.py` は、FastAPIのモダンなベストプラクティスに従ったものとなりました。import sqlite3
from typing import Annotated
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, Form, Depends, status
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles

# データベース設定
DB_NAME = "todo.db"



# データベース初期化ロジック (起動時にテーブルを作成)
def init_db():
    with sqlite3.connect(DB_NAME) as conn:
        cursor = conn.cursor()
        cursor.execute("""
            CREATE TABLE IF NOT EXISTS tasks (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                content TEXT NOT NULL,
                status INTEGER NOT NULL DEFAULT 0
            )
        """)
        conn.commit()

# ライフスパンイベント (アプリ起動時にDB初期化を実行)
@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    yield

# アプリケーション初期化
app = FastAPI(title="Spaghetti TODO", lifespan=lifespan)
templates = Jinja2Templates(directory="templates")

# データベース接続依存関係 (Dependency Injection)
# リクエストごとに接続を開き、処理終了後に確実に閉じる
def get_db():
    conn = sqlite3.connect(DB_NAME)
    # カラム名でアクセスできるように設定
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()

# 型ヒント用のエイリアス
DatabaseDep = Annotated[sqlite3.Connection, Depends(get_db)]

# --- ルーティング ---

@app.get("/", response_class=HTMLResponse)
def read_root(request: Request, db: DatabaseDep):
    """
    4.1. タスク一覧表示
    """
    cursor = db.cursor()
    cursor.execute("SELECT * FROM tasks ORDER BY id ASC")
    tasks = cursor.fetchall()
    
    return templates.TemplateResponse("index.html", {
        "request": request, 
        "tasks": tasks,
        "title": "Spaghetti TODO"
    })

@app.post("/do_the_thing")
def add_task(val: Annotated[str, Form()], db: DatabaseDep):
    """
    4.2. タスク登録
    """
    if not val.strip():
        # 空文字の場合は何もしない(あるいはエラー画面)
        return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

    cursor = db.cursor()
    cursor.execute("INSERT INTO tasks (content, status) VALUES (?, 0)", (val,))
    db.commit()
    
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

@app.post("/change_status")
def change_status(
    id: Annotated[int, Form()], 
    action: Annotated[int, Form()], 
    db: DatabaseDep
):
    """
    4.3. ステータス変更
    GET /ch から POST /change_status に変更し、DB更新を行う
    """
    cursor = db.cursor()
    cursor.execute("UPDATE tasks SET status = ? WHERE id = ?", (action, id))
    db.commit()
    
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)

@app.post("/delete_item")
def delete_task(id: Annotated[int, Form()], db: DatabaseDep):
    """
    4.4. タスク削除
    GET /rm から POST /delete_item に変更し、物理削除を行う
    """
    cursor = db.cursor()
    cursor.execute("DELETE FROM tasks WHERE id = ?", (id,))
    db.commit()
    
    return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <style>
        /* 6. UI/UX仕様に基づくスタイル */
        body {
            background-color: #f0f0f0;
            font-family: sans-serif;
            display: flex;
            justify-content: center;
            padding-top: 50px;
            margin: 0;
        }

        .container {
            width: 600px;
            background-color: white;
            padding: 30px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            border-radius: 8px;
        }

        h1 {
            text-align: center;
            color: #333;
        }

        /* フォームスタイル */
        .add-form {
            display: flex;
            gap: 10px;
            margin-bottom: 30px;
        }

        .add-form input[type="text"] {
            flex-grow: 1;
            padding: 10px;
            font-size: 16px;
        }

        .add-form button {
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            border: none;
            cursor: pointer;
            font-size: 16px;
        }

        /* リストスタイル */
        ul {
            list-style-type: none;
            padding: 0;
        }

        li {
            border-bottom: 1px solid #eee;
            padding: 15px 0;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .task-content {
            flex-grow: 1;
            margin-right: 15px;
            font-size: 18px;
        }

        /* 4.1. 完了状態のスタイル */
        .task-done {
            text-decoration: line-through;
            color: gray;
        }

        .actions {
            display: flex;
            gap: 5px;
        }

        /* ボタンのように見せるためのフォームスタイル */
        .inline-form {
            margin: 0;
        }

        .btn {
            border: 1px solid #ccc;
            background: #fff;
            padding: 5px 10px;
            cursor: pointer;
            font-size: 12px;
            border-radius: 3px;
        }

        .btn-done { color: green; border-color: green; }
        .btn-undone { color: orange; border-color: orange; }
        .btn-del { color: red; border-color: red; }
        
        .btn:hover { background-color: #f9f9f9; }

    </style>
</head>
<body>

<div class="container">
    <h1>{{ title }}</h1>

    <!-- 4.2. タスク登録フォーム -->
    <form action="/do_the_thing" method="post" class="add-form">
        <!-- 4.2. 入力検証: required -->
        <input type="text" name="val" placeholder="新しいタスクを入力..." required>
        <button type="submit">追加</button>
    </form>

    <!-- 4.1. タスク一覧表示 -->
    <ul>
        {% for task in tasks %}
        <li>
            <div class="task-content {% if task.status == 1 %}task-done{% endif %}">
                {{ task.content }}
            </div>

            <div class="actions">
                {% if task.status == 0 %}
                    <!-- [Done] ボタン (未完了の場合のみ表示) -->
                    <form action="/change_status" method="post" class="inline-form">
                        <input type="hidden" name="id" value="{{ task.id }}">
                        <input type="hidden" name="action" value="1">
                        <button type="submit" class="btn btn-done">Done</button>
                    </form>
                {% else %}
                    <!-- [Undone] ボタン (完了の場合のみ表示) -->
                    <form action="/change_status" method="post" class="inline-form">
                        <input type="hidden" name="id" value="{{ task.id }}">
                        <input type="hidden" name="action" value="0">
                        <button type="submit" class="btn btn-undone">Undone</button>
                    </form>
                {% endif %}

                <!-- [DEL] ボタン -->
                <form action="/delete_item" method="post" class="inline-form">
                    <input type="hidden" name="id" value="{{ task.id }}">
                    <button type="submit" class="btn btn-del" onclick="return confirm('本当に削除しか?');">DEL</button>
                </form>
            </div>
        </li>
        {% endfor %}
    </ul>
</div>

</body>
</html>

このように、生成されたコードは以下の点で大幅に改善されました。

主な改善点:

  • Jinja2テンプレートの導入: PythonコードとHTMLが完全に分離されました。

  • Dependency Injection: get_db 関数と Depends を使用し、DB接続のライフサイクルが適切に管理されるようになりました(コネクションリークの防止)。

  • 型ヒントの活用: AnnotatedForm を使用し、バリデーションと可読性が向上しました。

  • セキュリティ向上: データの更新削除が全て POST メソッドになり、SQLパラメータバインディングも徹底されています。

かなり実用的なレベルに近づいていることが分かります。

考察

今回の実験から、生成AIを用いた「仕様書を経由した再構築」は、単なるコード修正以上の効果を持つことが確認できました。

例えば、プロンプトで「開発者の観点から修正せよ」と指示しただけで、GETリクエストによる状態変更というセキュリティリスク(CSRFの温床)をAIが自律的に検知し、仕様書の段階で POST に修正した点です。これは、コードだけを見て「変数名を変える」ようなリファクタリングでは見逃されがちなポイントです。 また、元の低品質コードはFastAPIを使っていながら、Flaskや生のCGIのような書き方をしていました。再生成されたコードは、FastAPI特有の DependsPydantic モデル(今回はForm利用のため引数定義)を適切に使用しており、AIが「指定されたフレームワークのベストプラクティス」を適用して実装していることがわかります。

この手法は、ドキュメントが存在せず、保守担当者も不在となった「完全なブラックボックス・レガシーコード」のモダナイゼーションに極めて有効です。 ただし、成功の鍵は**「仕様書のレビュー」**にあります。Step 2に進む前に、人間がStep 1の仕様書を読み、「ビジネスロジックが欠落していないか」「勝手な機能追加がないか」を確認・修正することで、安全かつ劇的なコード改善が可能になります。

おわりに

「スパゲッティコードから仕様書を作り、その仕様書からコードを作る」というアプローチは、一見遠回りに見えます。 しかし、状況によっては、技術的負債を根本から断ち切る強力な手段となり得ます。 AIは単なる「コーディングアシスタント」ではなく、「設計のレビュアー」兼「アーキテクト」としても機能します。既存資産の改修に悩むプロジェクトにおいて、このフローは一考の価値があるでしょう。

FAQ generated by AI

はい、主要なプログラミング言語(Python, Java, JavaScript, PHPなど)であれば可能です。AIは言語間の翻訳やフレームワークの移行(例: FlaskからFastAPIへ)も同時に行うことができます。

いいえ、一度に処理できるコンテキスト長(トークン数)に限界があるため、大規模システムの場合は機能ごとやファイルごとにモジュール分割して実施する必要があります。

元のアプリケーションの動作と突き合わせるテストが必要です。また、元コードのドメイン知識があるエンジニアが仕様書のロジック(計算式や条件分岐)を目視確認することが推奨されます。

そのリスクはあります。そのためプロンプトには「機能的な振る舞いは変更せず、非機能要件(セキュリティ、保守性、パフォーマンス)のみを改善すること」と明記し、生成された仕様書の差分確認を行うことが重要です。

短期的には手動リファクタリングよりAIコストがかかる場合がありますが、仕様書が手に入ること、およびコードの可読性が劇的に向上することで将来の保守コストが大幅に下がるため、中長期的にはメリットが大きいです。

AIネイティブ実態調査レポート 無料配布中