LattixのアーキテクチャルールをAIに渡して既存コードに機能の追加実装をさせてみた

Lattix

はじめに

前回の記事では、既存コードに発生しているアーキテクチャルール違反をLattixで検出し、その違反レポートとルールレポートをAIに渡して修正させる、という検証をおこないました。つまり、「違反が起きた後にAIで直す」アプローチです。

今回は視点を変えて、「そもそも違反を起こさせない」アプローチを試します。新しい機能を追加する際に、仕様書と一緒にアーキテクチャルールをAIに渡しておけば、最初からルールに沿った実装をしてくれるのか――という検証です。

既存のソフトウェアに新しい機能を追加する場面を考えてみます。AIコーディングツールに追加仕様を伝えれば、動くコードをすぐに生成してくれます。しかし、「動く」ことと「既存の設計ルールを守っている」ことは別の話です。特にレイヤードアーキテクチャのように依存方向のルールがある場合、機能追加の実装時にAIがルールを破ってしまうことはないでしょうか。

そこで今回は、組み込みソフトウェアのサンプルコードに対して、同じ機能追加仕様を2通りの方法で実装させてみました。

AIに渡す情報パターンAパターンB
追加仕様書の添付ありあり
Lattixルールレポート(CSV)の添付なしあり

それぞれの実装をLattixで解析し、ルール違反の有無を比較してみます。

環境

ソフトウェアバージョン
Lattix2025.1.7(Architect + LDC)
SciTools Understand7.2 (Build 1252)
Python3.12
VS Code最新版
GitHub Copilot最新版(Agent Mode)
AI モデルClaude Opus 4.6

今回使用したサンプルコードの説明

組み込みファームウェアのサンプルを使用します。UART通信で外部からコマンドを受信し、LED制御や状態応答をおこなう構成です。

レイヤー構成

4層のレイヤー構造で設計されています。

+----------+
|  app     |  最上位 : アプリケーション制御
+----------+
|  middle  |  通信・LED制御ミドルウェア
+----------+
|  driver  |  HWアクセスドライバ
+----------+
|  lib     |  最下位 : 共通ライブラリ(型・CRC・バッファ)
+----------+

ソース構成

embedded_sample/
├── main.c                     エントリポイント
├── app/
│   ├── app_main.c/.h         アプリ初期化・メインループ
│   └── cmd_handler.c/.h      コマンド解釈
├── middle/
│   ├── comm_manager.c/.h     UART通信パケット送受信
│   └── led_controller.c/.h   LED制御
├── driver/
│   ├── uart_drv.c/.h         UARTドライバー
│   ├── gpio_drv.c/.h         GPIOドライバー
│   └── timer_drv.c/.h        タイマードライバー
└── lib/
    ├── common_types.h         型定義
    ├── ringbuffer.c/.h        リングバッファー
    └── crc.c/.h               CRC16計算

アーキテクチャルール

Lattixで以下のレイヤールールを設定しています。上位レイヤーから下位レイヤーへの依存は許可(CAN_USE)、逆方向の依存は禁止(CANNOT_USE)です。

禁止ルール(CANNOT_USE)意味
middle → appミドルウェアからアプリへの逆依存禁止
driver → appドライバーからアプリへの逆依存禁止
driver → middleドライバーからミドルウェアへの逆依存禁止
lib → appライブラリからアプリへの逆依存禁止
lib → middleライブラリからミドルウェアへの逆依存禁止
lib → driverライブラリからドライバーへの逆依存禁止

今回注目するのは driver → middle の禁止ルールです。

準備

事前に用意したものを整理します。

1. サンプルソースコード

今回使用したサンプルプロジェクト一式(元コード、追加仕様書、ルール定義CSV、パターンA/B実装済みコード)をzipで配布しています。手元で再現しながら読み進める場合はこちらからダウンロードしてご利用ください。

上記のレイヤー構成を持つ組み込みサンプル(embedded_sample/)を用意しました。

2. Lattixプロジェクトとルール設定

サンプルコードをUnderstandで解析し、Lattixでプロジェクトを作成したうえで、12件のレイヤールール(CAN_USE 6件 + CANNOT_USE 6件)を設定済みです。

3. ルールレポート(CSV)

Lattixからエクスポートした現在のルール一覧です。パターンBではこれをAIに渡します。

依存元,ルールタイプ,依存先,ルールの深さ
app,CAN_USE,middle,2
app,CAN_USE,driver,2
app,CAN_USE,lib,2
middle,CAN_USE,driver,2
middle,CAN_USE,lib,2
driver,CAN_USE,lib,2
middle,CANNOT_USE,app,2
driver,CANNOT_USE,app,2
driver,CANNOT_USE,middle,2
lib,CANNOT_USE,app,2
lib,CANNOT_USE,middle,2
lib,CANNOT_USE,driver,2

上記は見やすくサブシステム名で記載していますが、実際のCSVでは依存元・依存先がLattixのパーティション名形式(例: C:.work.work_ltxFunc.work_ltxapi.ldz.embedded_sample.app)になっています。実際の内容はダウンロードzipに含まれる layer_rules.csv を参照してください。AIにはこのLattix出力形式(Lattixのパーティション名形式)をそのまま渡しましたが、特別な説明を加えなくてもルールの内容を読み取って実装に反映していました。

4. 追加仕様書(マークダウン)

以下の内容追加仕様A_UART受信割り込み駆動化.md として用意しました(後述)。

実装と検証

同じ追加仕様に対して、以下の2パターンでCopilotに実装を依頼し、結果を比較します。

AIに渡す情報パターンAパターンB
追加仕様書の添付ありあり
Lattixルールレポート(CSV)の添付なしあり

追加する機能

以下の仕様を追加実装します。

UART受信割り込み駆動化

現在のUART受信はポーリング方式のため、データ取りこぼしを防ぐために割り込み駆動に変更する。

  • UART受信割り込みハンドラー uart_rx_isr() を実装する
  • 割り込みで受信したデータを即座に comm_manager へ渡し、パケット組み立てに使えるようにする
  • 既存の comm_recv_packet() のインターフェースは変更しない
  • 受信バッファオーバーフロー時はデータを破棄し、STATUS_ERROR を返せるようにする

この仕様書には、レイヤールールについての言及はありません。このあと説明するパターンBでだけ、ルールレポートによりレイヤールールを明示的に与えています。

パターンA: ルール情報なしで実装

パターンAでは、GitHub Copilot(Agent Mode)に追加仕様書だけを渡して実装を依頼します。

使用したプロンプト

embedded_sampleについて、追加仕様A_UART受信割り込み駆動化.md
の内容に沿って、仕様追加を実装してください。

なお、実装されたコードは
embedded_sample_feature_A フォルダに配置してください。

ワークスペース内に追加仕様書のmdファイルと元のソースコードがある状態で、プロンプトを入力しています。

以下のコードが生成されました。

// driver/uart_drv.c
#include "uart_drv.h"
#include "../lib/ringbuffer.h"
#include "../middle/comm_manager.h"  // ← driver層からmiddle層をinclude

/* UART receive interrupt handler */
void uart_rx_isr(void)
{
    if (!uart_initialized) return;
    u8 data = 0; /* In real HW: data = UART_DATA_REG; */
    comm_push_rx_byte(data);  // ← middle層のAPIを直接呼び出し
}

機能的には正しく動作するコードです。ISRで受信したデータを即座にcomm_managerのバッファーに格納しています。

AIの思考回路

生成されたコードについて、設計判断の理由をCopilotに説明させたところ、以下のような回答でした。

  1. 「割り込みで受信したデータを即座に comm_manager へ渡す」と書いてある
  2. comm_manager は middle 層にある
  3. ISR は driver/uart_drv.c に実装する
  4. → driver から middle の関数を呼べばいい(もっともシンプル)

パターンB: ルール情報ありで実装

パターンBでは、同じ追加仕様書に加えて、Lattixからエクスポートしたルールレポート(CSV)もCopilotに渡して実装を依頼します。

使用したプロンプト

embedded_sampleについて、追加仕様A_UART受信割り込み駆動化.md
の内容に沿って、仕様追加を実装してください。
制約条件:layer_rules.csvはアーキテクチャルールです。これを守って実装してください。

なお、実装されたコードは
embedded_sample_feature_A_with_rules フォルダに配置してください。

パターンAとの違いは、「制約条件:layer_rules.csvはアーキテクチャルールです。これを守って実装してください。」の一文を追加しただけです。ワークスペース内にlayer_rules.csvが置いてあるので、Copilotがそれを読み取って制約として反映してくれます。

以下のコードが生成されました。

// driver/uart_drv.c
#include "uart_drv.h"
#include "../lib/ringbuffer.h"
// ★ middle層のヘッダーは一切includeしない

static ringbuffer_t uart_rx_buf;  // driver層ローカルバッファ

/* UART receive interrupt handler */
void uart_rx_isr(void)
{
    if (!uart_initialized) return;
    u8 data = 0; /* In real HW: data = UART_DATA_REG; */
    ringbuffer_put(&uart_rx_buf, data);  // 自層のバッファに格納
}

middle層(comm_manager)は、driver層の既存API uart_recv_byte() / uart_is_rx_available() を使ってデータを取得します。依存の方向は middle → driver(許可方向)です。

AIの思考回路

生成されたコードについて、設計判断の理由をCopilotに説明させたところ、以下のような回答でした。

  1. 「割り込みで受信したデータを即座に comm_manager へ渡す」と書いてある
  2. しかし、driver → middle は CANNOT_USE(禁止)
  3. driver 層から middle 層の関数を直接呼ぶことはできない
  4. → driver 層内にローカルバッファーを持ち、自層内で完結させる
  5. middle 層が driver 層のAPIをポーリングしてデータを取得する設計にする

Lattixで解析する

それぞれの実装をUnderstandで静的解析し、Lattixでルール違反をチェックします。

結果の比較

DSMの比較

追加実装前と、各パターン実装後のDSMを比較します。

  • 追加実装前のDSM

追加実装前のDSM

  • パターンA実装後のDSM

パターンA実装後のDSM

  • パターンB実装後のDSM
パターンB実装後のDSM

ルール違反の件数

パターンA(ルールなし)パターンB(ルールあり)
driver → middle 違反2件0件
その他のレイヤー間違反0件0件

パターンA: 検出された違反の詳細

#違反元依存先依存タイプ行番号
1driver/uart_drv.cmiddle/comm_manager.hInclude3
2driver/uart_drv.cmiddle/comm_manager.cの関数Invoke44, 70, 85

driver層のファイルからmiddle層のヘッダーをインクルードし、middle層の関数を呼び出しているため、CANNOT_USEルールに違反しています。

パターンB: 違反なし

レイヤー間のルール違反は0件です。driver層はlib層にのみ依存し、middle層はdriver層のAPIを正方向で呼び出しています。

依存関係の比較図

パターンA(違反あり):

 middle(comm_manager)
     ▲
     │ ★ CANNOT_USE 違反!
     │
 driver(uart_drv)──→ lib(ringbuffer)

パターンB(違反なし):

 middle(comm_manager)
     │
     │ CAN_USE ✓(許可方向)
     ▼
 driver(uart_drv)──→ lib(ringbuffer)

なぜこうなるのか

設計思想の違い

同じ機能要件でも、バッファーをどのレイヤーに持たせるかという設計判断で依存方向が変わります。

観点パターンA(ルール違反あり)パターンB(ルール違反なし)
受信バッファーの所有者middle層driver層
ISRのデータ格納先middle層のバッファーに直接書き込みdriver層のローカルバッファー
データ取得の方向driverがmiddleのAPIを呼ぶ(逆依存)middleがdriverのAPIを呼ぶ(正方向)
層間インターフェースmiddleが「書き込みAPI」を公開driverが「読み出しAPI」を公開

AIの傾向

各パターンでAIに回答させた「AIの思考回路」を見ると、以下のことが考察されます。

  • 仕様書に「comm_managerへデータを渡す」と書かれていれば、AIは素直にdriver層からmiddle層の関数を呼ぶコードを生成している。これは仕様を最短経路で実現する合理的な判断だが、レイヤー制約を考慮していない
  • 一方、ルール情報を渡すと、AIは制約を満たす別の設計パターン(ローカルバッファー+ポーリング方式)を選択した。機能的にはどちらも同等だが、アーキテクチャの健全性には大きな違いがある

まとめ

今回の検証を通じて、AIにコーディングを任せる際に意識しておきたいことが見えてきました。

  • AIは「動くコード」を書くのは得意だが、「設計意図」は伝えなければ汲み取らない
    仕様書だけ渡すと、AIは最短経路で機能を実現しようとする。既存コードの暗黙のルール(レイヤー構造、依存方向の制約など)は、明示しなければ無視される可能性がある
  • 「ルールを守って」と一言添えるだけで、AIの設計判断は変わる
    今回の実験では、プロンプトにたった一文追加しただけでルール違反がゼロになった。AIに渡す情報の質が、生成されるコードの設計品質を左右する
  • AIの出力は「レビュー対象」であり「完成品」ではない
    AIが生成したコードが正しいかどうかは、人間(またはツール)が確認する必要がある。Lattixのようなアーキテクチャ解析ツールは、その確認を自動化・定量化する手段として有効

今後AIに実装を任せる場面では、仕様書と一緒にアーキテクチャルールを伝えることで、設計品質を維持しながら開発速度を上げる運用が期待できそうです。

AIにコーディングを任せる際は、ぜひLattixでアーキテクチャの管理もしていきましょう。

余談:VS Codeだけで完結させたくなった話

今回の検証では「Understand解析→Lattixプロジェクト作成→ルール違反確認」という一連の流れを、変更元・パターンA・パターンBそれぞれのプロジェクトに対して実行しました。複数のプロジェクトでこの手順を繰り返すと、毎回LattixのGUIを開いてプロジェクトを更新し、レポートを確認して…という手順が地味に手間になってきます。特にAIコーディングはVS Code上でおこなっているのに、確認のためだけに別のアプリケーションに切り替えるのは流れが途切れます。

そこで、Lattix Python APIを活用してVS Code上からチャットベースでUnderstand解析・Lattix解析([Lattixで解析する]の画像)・DSMの表示・並び替え・ルールの出力などをおこなえるようにしたCopilot用Skillsを作成し、今回の検証でも活用しています。

チャットで「DSMを表示して」と入力すればマトリクスが表示され、「ルール違反を確認して」と言えば違反レポートが出力されます。LattixのGUIを開かなくても、コーディング中のVS Code上でアーキテクチャの確認が完結するため、今回のような「実装→解析→確認」の繰り返しがスムーズにおこなえました。

こちらについては別の記事で改めてご紹介する予定です。