はじめに
はじめまして、NECソリューションイノベータ株式会社で技術探索チームとして日々業務をしております、茶碗谷廉太朗です。
AIエージェント関連の様々な記事が出ていますが、複数のLLMを組み合わせ仕組みの記事が面白そうでしたので、それ関連で簡単ですが試してみました。
今回はpromptflowを利用したAIエージェントの仕組みを理解するのを目的にしているので、細かい中身の精査やLLMのカスタマイズについてはこだわっていませんので、そこの部分はご了承ください。
参考記事
以下の記事を参考に今回構築しましたので、ご参考にしてください。
- 参考資料
- AIエージェントの構築の仕組み関連
- promptflow関連
事前準備
以下の事前準備が必要です。
- promptflowの構築
- 今回はローカル環境で動くように構築しました。
- LLMの用意
- 今回はAzureOpenAIでchatGPT4oを事前に用意して下記の2つを準備しました。
- APIキー
- エンドポイント
- 今回はAzureOpenAIでchatGPT4oを事前に用意して下記の2つを準備しました。
注意点(私が理解していない部分があっただけかも知れないですが、備忘録的に残しておきます)
以下のdeployment_nameは、入力候補が出てくるのですが。。。 「手動です!!」
スクショのようにAzureOpenAIの 名前(モデル名の横) を手打ちで入力するとうまくいきました。
今回のフロー図
以下のようなフロー図と、最初のユーザー入力で構築しました。
- input
- ユーザー希望を入力
それぞれのLLMの内容
- LLM_requirements
assistant:
あなたはシステムアナリストとして、ユーザーの入力から要件を定義するエキスパートです。
user:
以下のユーザー入力から、要件を定義してください:
{{question}}
answer:
承知しました。入力内容から要件を定義します。
# 機能要件
- 機能1
- 詳細な説明
- 制約条件
# 非機能要件
- パフォーマンス要件
- セキュリティ要件
- 品質要件
# 技術要件
- 使用技術
- アーキテクチャ
- フレームワーク
# UI要件
- UI/UX要件
- レスポンシブ対応
- アクセシビリティ
- LLM_review
system:
あなたは要件定義のレビューアーとして、要件の品質を確認し改善点を指摘するエキスパートです。
user:
以下の要件定義をレビューし、改善点を指摘してください:
{{requirements}}
assistant:
承知しました。要件定義のレビューを行います。
# 機能の明確性
- 各機能の目的は明確か
- 機能の範囲は適切に定義されているか
- 機能間の依存関係は明確か
- エッジケースは考慮されているか
# 非機能要件の網羅性
- パフォーマンス要件は具体的か
- セキュリティ要件は十分か
- 品質要件は測定可能か
- スケーラビリティは考慮されているか
- 保守性への考慮はあるか
# 改善提案
- 不足している要件
- より具体化すべき点
- リスクとなる可能性がある要件
- その他の改善点
- LLM_generatecode
system:
あなたは熟練のソフトウェアエンジニアとして、要件定義からコードを生成するエキスパートです。
user:
以下の要件定義とレビュー結果に基づき、コードを生成してください:
要件定義:
{{requirements}}
レビュー結果:
{{review}}
assistant:
承知しました。要件とレビュー結果を分析し、コードを生成します。
# コード生成の方針
- 要件定義に基づいた実装
- レビュー結果で指摘された改善点の反映
- 適切なコーディング規約の適用
- エラーハンドリングの実装
- セキュリティ対策の実装
# 生成するコードの構成
- ディレクトリ構造
- 各ファイルの役割
- 依存関係
- 設定ファイル
# 実装の詳細
[ここに生成されたコードが入ります]
# 実装の説明
- 各機能の実装方針
- インターフェース定義
- データモデルの説明
- エラーハンドリング方針
- セキュリティ対策の詳細
- LLM_component
system:
あなたは設計のエキスパートとして、生成されたコードから再利用可能なコンポーネントを抽出する専門家です。
user:
以下のコードから、再利用可能なコンポーネントを抽出してください:
{{code}}
assistant:
承知しました。コードを分析し、再利用可能なコンポーネントを抽出します。
# コンポーネント抽出の基準
- 再利用性の高い機能
- 共通のUIパターン
- ビジネスロジックの分離
- 状態管理の分離
# 抽出されたコンポーネント
[ここに抽出されたコンポーネントのコードが入ります]
# コンポーネント設計
- コンポーネントの責務
- インターフェース定義
- プロパティの設計
- イベントハンドリング
# リファクタリング方針
- コードの重複除去
- 依存関係の整理
- テスト容易性の向上
- パフォーマンスの最適化
# 使用方法の説明
- 各コンポーネントの利用方法
- カスタマイズのオプション
- 依存関係の解決方法
- エラーハンドリング
出力の詳細
- output(フローの最後での出力)の出力
- 各LLMからoutputに繋いでいるので、LLMごとの出力が分けられて表示されている。
- どの出力を、outputとして出力させるかは調整可能です。
output全文
4つのLLMごとに出力がまとめられて最終的な出力に出てきます。
以下では、それぞれの出力を見てみます。
requirements
要件定義
以下は、ユーザーからの入力に基づいて定義された要件です。
機能要件
機能1: ユーザー作成
-
詳細な説明:
新しいユーザーを登録するための画面を提供します。この画面では、以下の情報を入力できます。- ユーザーの名前
- メールアドレス
- パスワード
- プロファイル画像のアップロード
- ユーザーの役割選択
-
制約条件:
- ユーザー名とメールアドレスは既に存在しないものでなければならない。
- パスワードは以下の条件を満たす必要がある。
- 最低8文字
- 少なくとも1つの大文字
- 少なくとも1つの小文字
- 少なくとも1つの数字
- 少なくとも1つの特殊文字
非機能要件
-
パフォーマンス要件:
- ユーザー作成処理は3秒以内に完了すること。
-
セキュリティ要件:
- ユーザーのパスワードは暗号化してデータベースに保存する。
- TLSを使用してデータを転送する。
-
品質要件:
- ユーザーインターフェースは直感的で使いやすいデザインとする。
- 一般的なブラウザ互換性を確保する。
技術要件
-
使用技術:
- フロントエンド: React または Vue.js
- バックエンドAPI: Node.js または Django
-
アーキテクチャ:
- RESTful API または GraphQL をバックエンドで使用する。
- マイクロサービスアーキテクチャを採用する。
-
フレームワーク:
- フロントエンド: React 推奨
- バックエンド: Express または Django
UI要件
-
UI/UX要件:
- ユーザー登録画面は使いやすいナビゲーションと明確なフィードバックを提供する。
- ユーザーが入力エラーを簡単に理解できるようにする。
-
レスポンシブ対応:
- スマートフォン・タブレット・デスクトップなど、異なるデバイスで適切に表示されること。
-
アクセシビリティ:
-
WCAG基準に従い、以下をサポートする。
- スクリーンリーダー
- キーボードナビゲーション
-
WCAG基準に従い、以下をサポートする。
要件レビュー
機能要件
1. 説明の明確性
- ユーザー作成の機能要件は基本的に明確ですが、「ユーザーの役割」の詳細が欠けています。
- 役割ごとに特定の権限やアクセス制限がある場合は、それを明示する必要があります。
2. 制約条件の明確化
- ユーザー名とメールアドレスが既に存在しないことを確認する要件は実装上の重要な要素です。
- このチェックにおける競合条件(レースコンディション)への対策についても検討すると良いでしょう。
3. エッジケースの考慮
- ユーザー名やメールアドレスのフォーマットのルールやバリデーション(例:最大長、使用可能な文字セットなど)が記載されていません。
- 詳細なバリデーション要件を追加すると、仕様の曖昧さを減らせます。
非機能要件
1. パフォーマンス要件
- 「3秒以内に完了」の基準がやや曖昧です。
- ロード時間か、バックエンド処理時間か、具体的に示した方が良いでしょう。
- 負荷がかかった状況(例:同時接続数)も記載すると、パフォーマンス目標がより明確になります。
2. セキュリティ要件
- パスワードの暗号化方式(例:ハッシュ関数の種類)を具体的に指定すると良いでしょう。
- さらに、ユーザー認証後のセッション管理についても触れると、セキュリティをより強化できます。
3. 品質要件
- 「直感的で使いやすい」という表現はやや主観的です。
- ユーザビリティの具体的な指標(例:学習容易性、効率性、メンタルモデルの一致度など)を設定すると、評価が容易になります。
技術要件
1. 使用技術の選択肢
- 複数の技術選択肢が提示されていますが、プロジェクトの特性やチームのスキルに基づいて、デフォルトまたは推奨の選択を設けると、技術選定のブレを減らせます。
2. アーキテクチャの詳細
- マイクロサービスアーキテクチャを用いる理由や、それに伴うインフラ要件(例:コンテナ化、オーケストレーションツールなど)についても説明があると理解が深まります。
UI要件
1. UI/UX要件の具体性
- 「使いやすいナビゲーション」との記述がありますが、具体的な指標(例:クリック数の最小化、視認性、アクセシビリティ基準など)を追加すると、設計の目標がより具体的になります。
2. アクセシビリティ
- WCAGのどのレベルを達成するべきか(例:A、AA、AAA)の指標を明示すると、開発者への指示が明確になります。
その他の改善点
1. テスト要件
- 各要件に対して、どのように検証するかテストケースの概要を載せると、要件の達成度が測定可能になります。
2. 将来的な拡張性
- 要件に基づくだけでなく、将来の拡張を考慮した設計要件があると、変更への耐性が強化されます。
まとめ
これらの観点から要件を見直し、追加や修正を行うことで、より具体的で実現可能な要件定義を形成できるでしょう。
Code生成
以下は要件とレビュー結果を反映した簡単なユーザー作成機能のバックエンドコードです。バックエンドにはNode.jsとExpressを使用します。フロントエンドはReactを想定していますが、今回はバックエンドのみを実装します。
以下は要件とレビュー結果を反映した簡単なユーザー作成機能のバックエンドコードです。バックエンドにはNode.jsとExpressを使用します。フロントエンドはReactを想定していますが、今回はバックエンドのみを実装します。
ディレクトリ構造
/user-registration-app
|-- /backend
|-- /config
|-- db.js
|-- /controllers
|-- userController.js
|-- /models
|-- User.js
|-- /routes
|-- userRoutes.js
|-- /utils
|-- validateInput.js
|-- server.js
|-- package.json
server.js
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');
const { dbConnect } = require('./config/db');
require('dotenv').config();
const app = express();
// Middleware
app.use(express.json());
// Database Connection
dbConnect();
// Routes
app.use('/api/users', userRoutes);
// Server Start
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
config/db.js
const mongoose = require('mongoose');
const dbConnect = () => {
mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Database connected'))
.catch(error => console.error('Database connection error:', error));
};
module.exports = { dbConnect };
models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcrypt');
const UserSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' },
profileImage: { type: String },
}, {
timestamps: true
});
// Password hashing middleware
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
module.exports = mongoose.model('User', UserSchema);
controllers/userController.js
const User = require('../models/User');
const { validateUser } = require('../utils/validateInput');
exports.registerUser = async (req, res) => {
const { username, email, password, role, profileImage } = req.body;
// Validate input
const validationError = validateUser(req.body);
if (validationError) return res.status(400).json({ error: validationError });
try {
// Check for existing user
const existingUser = await User.findOne({ $or: [{ username }, { email }] });
if (existingUser) return res.status(400).json({ error: 'Username or Email already exists' });
// Create new user
const user = new User({ username, email, password, role, profileImage });
await user.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
};
routes/userRoutes.js
const express = require('express');
const { registerUser } = require('../controllers/userController');
const router = express.Router();
router.post('/register', registerUser);
module.exports = router;
utils/validateInput.js
const validateUser = ({ username, email, password }) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
if (!username) return 'Username is required';
if (!email || !emailRegex.test(email)) return 'Valid email is required';
if (!password || !passwordRegex.test(password)) return 'Password must be at least 8 characters long and include uppercase, lowercase, numeric and special characters';
return null;
};
module.exports = { validateUser };
package.json
{
"name": "user-registration-app-backend",
"version": "1.0.0",
"description": "User registration service",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"bcrypt": "^5.0.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^5.13.3"
}
}
実装の説明
- ユーザー入力バリデーション: パスワードやメールフォーマットの検証。
- パスワードのハッシュ化: ユーザー作成時にパスワードをbcryptでハッシュ化。
- エラーハンドリング: 既存ユーザーの確認や入力バリデーションエラーのハンドリング。
- セキュリティ: 簡単なAPIエンドポイントを持ち、データベースとの安全な通信。
- フロントエンドとの接続: フロントエンドからリクエストを受けてユーザー登録を行う。
全てのコードに基づいて、適切な環境変数の設定などが必要です。テスト駆動での開発や、さらなる詳細設計が求められる場合、SwaggerやJestなども検討すると良いでしょう。
コンポーネント化
以下は、再利用可能なコンポーネントとして抽出できる部分です。
1. データベース接続 (dbConnect
)
再利用性
データベースへの接続は、他のプロジェクトやモジュールでも必要になる一般化された機能です。
config/db.js
const mongoose = require('mongoose');
const dbConnect = (uri) => {
mongoose.connect(uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('Database connected'))
.catch(error => console.error('Database connection error:', error));
};
module.exports = { dbConnect };
改善ポイント: 関数の引数にURIを受け取るようにし、汎用化しました。
2. ユーザー入力バリデーション (validateUser
)
再利用性
入力バリデーションは、ほぼすべてのフォーム処理で必要になるため、再利用が容易です。
utils/validateInput.js
const validateUser = ({ username, email, password }) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
if (!username) return 'Username is required';
if (!email || !emailRegex.test(email)) return 'Valid email is required';
if (!password || !passwordRegex.test(password)) return 'Password must be at least 8 characters long and include uppercase, lowercase, numeric and special characters';
return null;
};
module.exports = { validateUser };
利用方法: validateUser
関数を利用して、入力されたユーザーデータをバリデートする。エラーがある場合にはその詳細を返します。
3. パスワードハッシュ化ロジック
再利用性
パスワードのハッシュ化は、セキュアなアプリケーションにおいて不可欠な部分で、他のユーザー関連モデルでも再利用できます。
models/User.js
const bcrypt = require('bcrypt');
const hashPassword = async (password) => {
return await bcrypt.hash(password, 12);
};
module.exports = { hashPassword };
改善ポイント: パスワードのハッシュ化を他のモデルでも再利用できるよう、共通のユーティリティ関数に分離しました。
4. サーバー設定
再利用性
基本的なサーバーの設定は、多くのアプリケーションで共通するため、テンプレートとして再利用できます。
server.js
const express = require('express');
const startServer = (routes, port) => {
const app = express();
app.use(express.json());
app.use('/api', routes);
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
};
module.exports = { startServer };
改善ポイント: サーバー起動に必要な部分を関数化し、ルーティングとポートを引数として受け取るようにしました。
まとめ
これらのコンポーネントは、異なるプロジェクトやモジュールでの再利用を容易にします。また、各関数やモジュールは独立してテスト可能であり、アプリケーションの拡張時にも役立ちます。
今回試した背景や実際に触った感想
promptflowをなぜ使用したか
- 複雑なフローや、多岐にわたるAIでのチャット処理は目的に応じて、フローを変えた方がいい。
- 一括で最終ゴールを出力しようとすると、うまくいかない。
- コード生成などを、AIで自動化して行う→ブラックボックス的になる部分の、品質的な担保の部分で使えそう。
- 根拠や、本当に正しく出力されているのか。
- promptflowなら、入力と出力がログとして出力されるので品質チェックに活かせそうと感じた。
- 複雑なフローや条件分岐でも、しっかりと正しく入力が渡されて出力が何かが視覚的に確認できる。
私なりの仮説
- AIエージェントの構築では、複雑な処理や、ユーザーの求める内容によっての条件分岐での内部的処理をして適した応答を返す方が、回答の精度が高くなる。
- AIエージェント→人間の思考回路と近い論理で、フローを組むとより人と同じレベルのAIというものに近づきそう。
- ユーザー入力→これがどんな質問なのか→どういう情報が必要か→どんな処理をするべきか
- LLMを分散して、それぞれを連鎖させる方式が良さそう。
- AIにパーソナリティを与えるという点で、カスタマイズすることも考えるとそっちの設計の方が向いていそう。
- AIエージェント→人間の思考回路と近い論理で、フローを組むとより人と同じレベルのAIというものに近づきそう。
- まだ、簡単なプロセスかつ簡単なインプットで試しただけなのでまだまだ要検証。。。
- 開発スキルは高度なものは要求されないので、誰でも簡単に作れる。
- pythonも組み込めて、そこで多少の開発スキルが必要だが全体構成やLLMの組み込みは視覚的に構築できるので良かった。
- promptflowでのAIエージェント構築は開発スキルよりも、AIに与えるデータの部分の構築(各LLMのに与えるプロンプトや学習させたいデータの整理、それらを学習させる部分など)が大事そうと感じた。