ジェネラティブエージェンツの西見です。
Claude Codeなどのコーディングエージェントを活用するためには、的確な指示だけでなく、エージェントが生成したコードの誤りを自律的に検知・修正する仕組みが重要となります。誤り検知には自動テストやLinterが有効ですが、本記事では、仕様書とコードの「意味的な整合性」を検証するツール「Semcheck」に着目し、その性能を複数のLLMで比較評価します。
Semcheckとは
Semcheckは、LLMを利用して、仕様書(Markdown形式)とソースコード間の意味的な整合性を検証するGo言語製のツールです。構文やスタイルを対象とする従来の静的解析ツールとは異なり、「仕様書で定義された要求事項がコードに正しく実装されているか」という観点から検証を行う点に特徴があります。
OpenAI、Anthropic、Gemini、Ollamaなど複数のLLMプロバイダーに対応しており、YAML形式の設定ファイルでチェック内容を定義できます。また、GitHub ActionsなどのCI/CDパイプラインへの統合や、変更されたファイルのみを対象とするプレコミットモードもサポートしており、効率的な検証が可能です。
検証内容
本検証では、FastAPIで実装したユーザー管理APIを対象とします。仕様書に対し、意図的に4種類の仕様違反をコードに埋め込み、各LLMがこれらをどの程度検出できるかを評価しました。
仕様書(fastapi_spec.md)
# FastAPI User Management API Specification ## Overview This API provides user management functionality with CRUD operations. ## Endpoints ### GET / - Returns JSON object with API status - Response: {"message": "User Management API is running"} ### GET /users/{user_id} - Returns user information by ID - Returns 404 if user not found - Response: User object ### POST /users - Creates a new user - Request body: UserCreate schema - Response: Created User object ### PUT /users/{user_id} - Updates existing user - Request body: UserCreate schema - Response: Updated User object ### DELETE /users/{user_id} - Deletes a user (required endpoint) - Returns 204 on success ## Data Models ### User - id: integer (required) - name: string (required) - email: string (required) - age: integer (optional) ### UserCreate - name: string (required) - email: string (required) - age: integer (optional)
正常な実装(仕様通り)
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional, List app = FastAPI(title="User Management API", version="1.0.0") class User(BaseModel): id: int name: str email: str age: Optional[int] = None # 仕様通り: オプショナル class UserCreate(BaseModel): name: str email: str age: Optional[int] = None # 仕様通り: オプショナル users_db: List[User] = [] next_id = 1 @app.get("/") async def root(): return {"message": "User Management API is running"} # 仕様通り: JSON形式 @app.get("/users/{user_id}", response_model=User) async def get_user(user_id: int): for user in users_db: if user.id == user_id: return user raise HTTPException(status_code=404, detail="User not found") # 仕様通り: 404エラー @app.post("/users", response_model=User) async def create_user(user: UserCreate): global next_id new_user = User(id=next_id, **user.dict()) users_db.append(new_user) next_id += 1 return new_user @app.put("/users/{user_id}", response_model=User) async def update_user(user_id: int, user: UserCreate): for idx, existing_user in enumerate(users_db): if existing_user.id == user_id: updated_user = User(id=user_id, **user.dict()) users_db[idx] = updated_user return updated_user raise HTTPException(status_code=404, detail="User not found") @app.delete("/users/{user_id}", status_code=204) async def delete_user(user_id: int): for idx, user in enumerate(users_db): if user.id == user_id: users_db.pop(idx) return raise HTTPException(status_code=404, detail="User not found")
仕様違反を埋め込んだ実装
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List app = FastAPI(title="User Management API", version="1.0.0") class User(BaseModel): id: int name: str email: str age: int # 🚨 仕様違反1: Optional[int]であるべき class UserCreate(BaseModel): name: str email: str age: int # 🚨 仕様違反1: Optional[int]であるべき users_db: List[User] = [] next_id = 1 @app.get("/") async def root(): return "API is running" # 🚨 仕様違反2: JSON形式であるべき @app.get("/users/{user_id}", response_model=User) async def get_user(user_id: int): for user in users_db: if user.id == user_id: return user raise HTTPException(status_code=500, detail="Internal server error") # 🚨 仕様違反3: 404であるべき @app.post("/users", response_model=User) async def create_user(user: UserCreate): global next_id new_user = User(id=next_id, **user.dict()) users_db.append(new_user) next_id += 1 return new_user @app.put("/users/{user_id}", response_model=User) async def update_user(user_id: int, user: UserCreate): for idx, existing_user in enumerate(users_db): if existing_user.id == user_id: updated_user = User(id=user_id, **user.dict()) users_db[idx] = updated_user return updated_user raise HTTPException(status_code=404, detail="User not found") # 🚨 仕様違反4: DELETEエンドポイントが削除されている(仕様書では必須)
検証対象の仕様違反
| 仕様違反の種類 | 内容 | 影響 |
|---|---|---|
| 型定義の不整合 | ageフィールドが必須(仕様ではOptional) |
クライアントAPIの互換性問題 |
| レスポンス形式違反 | ルートエンドポイントが文字列を返す(仕様ではJSON) | クライアントのパースエラー |
| HTTPステータス違反 | ユーザー不在時に500を返す(仕様では404) | エラーハンドリングの不整合 |
| エンドポイント欠落 | DELETEエンドポイントが未実装(仕様では必須) | 機能の欠落 |
検証は、再現性を確認するため、FastAPI、Django、Flaskの3つのフレームワークで同様のテストケースを用意し、合計で12パターン(誤った仕様4つ × 3フレームワーク)で実施しました。
検証結果
検証は以下の環境で実施しました。
Mac Studio (2025) Apple M3 Ultra Mem: 512GB Storage: 4TB SSD macOS 15.5
モデル別検出精度
3パターンのコード(フレームワーク)に対して10回ずつ検出を実行したところ得られた結果をまとめたものが、次の表です。
| モデル | 仕様違反検出率 | 誤検出率(正常実装) | 平均実行時間 |
|---|---|---|---|
| gpt-4.1 | 100% | 0% | 5.05秒 (±2.10秒) |
| gpt-4.1-mini | 100% | 0% | 8.42秒 (±3.48秒) |
| Gemini 2.5 Pro | 40% | 0% | 23.36秒 (±8.94秒) |
| Gemini 2.5 Flash | 100% | 0% | 12.60秒 (±2.10秒) |
| Claude Sonnet 4 | 100% | 0% | 8.21秒 (±1.27秒) |
| DeepSeek-R1-0528-Qwen3-8B | 100% | 40% | 11.07秒 (±5.13秒) |
なぜかGemini 2.5 Proだけが40%という結果になってしまっていますが、次の表で示すローカルLLMで全て検出率が0%になってしまったことからも、Semcheck内で利用されているプロンプトとの相性問題もありそうです。DeepSeek-R1-0528-Qwen3-8Bは、今回検証したローカルLLMの中で唯一適切な出力を返したLLMですが、誤検出が40%出ていました。運用を考える場合は、ローカルLLMよりフロンティアモデルを利用する方が良さそうです。
| モデル | 仕様違反検出率 | 誤検出率(正常実装) | 平均実行時間 |
|---|---|---|---|
| DeepSeek-R1-Distill-Qwen-14B | 0% | 0% | 12.83秒 (±1.57秒) |
| Gemma 3n E4B | 0% | 0% | 7.60秒 (±0.29秒) |
| Qwen3 8B | 0% | 0% | 7.37秒 (±0.42秒) |
| Phi-4-reasoning-plus 14B | 0% | 0% | 10.65秒 (±2.75秒) |
| Gemma3 12B | 0% | 0% | 6.24秒 (±0.58秒) |
また、利用モデルによる違いは見られましたが、利用フレームワークによる検出率の違いは見られませんでした。
検出時の様子
検出に成功した場合は、次のように具体的な修正案も提示されます。
gpt-4.1による検出例(型定義の不整合)
🚨 ERROR: 'age' field is required in User and UserCreate models, but should be optional as per specification. Confidence: 1.0 The specification states that the 'age' field in both User and UserCreate models is optional (i.e., not required), but in the implementation, 'age' is defined as a required field. This will cause the API to reject requests that do not include 'age', which is not compliant with the specification. Suggestion: Change 'age: int' to 'age: Optional[int] = None' in both models.
DeepSeek R1 8Bによる検出例(エンドポイント欠落)
🚨 ERROR: DELETE /users/{user_id} endpoint is missing, but required by the specification.
Confidence: 0.9
The specification explicitly requires a DELETE /users/{user_id} endpoint for
deleting users, but the implementation does not provide this endpoint at all.
まとめ
実のところ、ローカルLLMで上手く仕様書との不整合が検出できたら良いなと思って始めた検証でしたが、正直設定値を間違えたかなと何回も見直すぐらいに上手く行かないモデルが多数でした。特に「DeepSeek-R1-0528-Qwen3-8B」は100%検出するのに、「DeepSeek-R1-Distill-Qwen-14B」は全く検出できない理由は、中で利用されているプロンプトなどを見ないと、ちょっと分からないなという感じです。
とはいえ、スペック駆動のAIコーディングを行う際には、かなり便利なライブラリなのではと思いました。モデル選定の際にご参考いただければ幸いです。