【脱・Office】NotebookLM × Dify × Copilotで実現する、要件定義からAIエージェント実装までの一気通貫パイプライン
はじめに
こんにちは、藤野 拓也です。
私は自他ともに認めるめんどくさがり屋で、根底には常に「いかに無駄を省いて楽をするか」という哲学があります。普段はドローンで自動通勤する妄想をしながら自宅のRTX 4080でAIをぶん回したり、窓から見える山の景観をぼーっと眺めたりしていますが、本業ではAIエージェントの導入支援やテクニカルディレクションの上流工程を担っています。
そんな私が日々の業務でどうしても忌避してしまうのが、WordやExcelを開いてちまちま体裁を整える従来のドキュメント作成です。「AIを活用するシステムを作っている側の人間が、レガシーな手法で疲弊していてどうするんだ?」という葛藤が常にありました。私のPCにはそもそもOffice製品を入れておらず、ドキュメントはすべてMarkdownとGitで一元管理して、極限までスピード優先で動きたい派です。
そこで今回は、現場の不満から私の理想を詰め込み、「上流の要件定義から下流のエージェント実装まで、いかに一気通貫でサボれる(自動化できる)か」を突き詰めて手元で構築・検証した、次世代のAI駆動開発パイプラインのPoC(概念実証)をご紹介します。
【今回想定するお題】
今回は具体例として、非エンジニアの担当者からチャットで以下のような「ふんわりとした要求仕様メモ」がポンと投げられた状態を想定してください。
<要求仕様> 業務アサインエージェント
・社員のこれまでの業務内容、スキル、成果物、キャリア希望を総合的に判断し、業務にアサインが可能かを判断するAIエージェント
・マッチング確率を%で表示。希望度は別で小・中・大のような表示、優先度も別で表示
まだ実務の全社標準としてゴリゴリ運用しているわけではなく、あくまで「現時点で私が考える最強のアーキテクチャ提案」ですが、顧客のカオスな要望を「NotebookLM」に投入してMarkdown仕様書化し、それを「Dify」や「GitHub Copilot」と連携させる構成は、圧倒的なスピードでセキュアなプロトタイプを形にするための大きなヒントになるはずです。
1. 提案するアーキテクチャ:AIに適材適所で働いてもらう
このワークフローのキモは、「すべてのハブをMarkdownにすること」、そして「アプリケーションの型安全(型定義)」と「AIの推論ロジック(プロンプト)」を綺麗に分離することにあります。
今回は、単なるおもちゃのPoCではなく、KMS(鍵管理)やDLP(機密情報検査)を備えたエンタープライズ環境への導入を想定し、以下のようなセキュアなアーキテクチャを設計しました。
このアーキテクチャを実現するため、以下の3つのフェーズで開発を進めます。
- Phase 1(思考・分析): 議事録や資料を NotebookLM に投入し、矛盾検知と要件整理を行う。
- Phase 2(要件定義): NotebookLM から、AIとCopilotが読みやすい構造化された Markdown仕様書 を出力。
- Phase 3(実装・エージェント化): Markdownをハブにして、Dify でAIエージェントをAPI化し、GitHub Copilot でバックエンドの型定義を自動生成する。
2. NotebookLMで「仕様の矛盾検知」を自動化する
プロジェクト初期の要求仕様は、さまざまな資料に散らばっていることが多く、これを手作業で整理するのは膨大な時間を消費します。そこで、関連資料をすべてNotebookLMにアップロードし、単なる要約ツールではなく「テクニカルディレクションの壁打ち相手」として活用します。
NotebookLMは与えられたソースに厳密にグラウンディングするため、「既存APIとの矛盾点」や「通信瞬断時の例外処理の漏れ」などを、ハルシネーションなしでソリッドに指摘してくれます。
今回のターゲットは「業務アサイン判定AIエージェント」です。NotebookLMとの壁打ちを経て、以下のようなMarkdownの要件定義書(抜粋)を出力させ、VS CodeのGitリポジトリに保存します。このMarkdownが、すべての実装の「マスターデータ」となります。
# 業務アサインエージェント 要件定義書
## 1. システム概要とAIエージェントの役割
本システムは、社員のスキル、過去の業務実績、成果物、およびキャリアに対する希望を多角的に分析し、
特定の業務(プロジェクト・タスク)へのアサイン妥当性を自動判定するAIエージェントである。
## 2. 入力データ定義(Input Schema)
### 2.1 社員情報(Employee Profile)
- 社員ID (文字列)
- 案件概要 (テキスト)
- 過去の業務内容 (テキスト)
- 保有スキル (文字列の配列)
- 主要な成果物 (テキストまたはURL)
- キャリア希望 (テキスト)
### 2.2 案件・業務情報(Assignment Requirement)
- 案件ID (文字列)
- 要求される業務内容 (テキスト)
- 必須スキル・歓迎スキル (文字列の配列)
- プロジェクトの優先度 (文字列: 高、中、低)
## 3. 出力データ定義(Output Schema)
- アサイン可否 (真偽値)
- マッチング確率 (数値: 0〜100)
- 希望度 (文字列: 大、中、小)
- 優先度 (文字列: 高、中、低)
- マッチング理由 (テキスト)
## 4. 判定ロジックおよび制約条件(Constraints)
- 総合的判断の原則: スキル一致数だけでなく、定性的な経験値を総合評価すること。
- 独立評価の原則: 「マッチング確率」「希望度」「優先度」は独立して評価すること。
- ハルシネーションの排除: 推測や補完で判定根拠に含めてはならない。
## 5. 例外設計(Exception Handling)
- データ不足時の挙動: 入力されたテキスト量が不足している場合は判定を強制終了(フォールバック)し、不足項目を明示するエラーを返すこと。
## 6. エージェント評価指標(Evaluation Metrics)
- Precision(適合率): 人間の判断とAIの判定の一致率。
- Recall(再現率): アサイン成功実績をAIが網羅できたかの割合。
3. DifyによるAIエージェントの「インスタンス化」
このMarkdown要件定義をベースに、エンタープライズアーキテクチャ図の「VPC内オーケストレーション」にあたる「Dify」上にAIエージェントを構築します。
Difyを使うことで、プロンプトやモデル設定(Vertex AI等)がカプセル化され、外部から叩ける「独立したAPIエンドポイント」が生成されます。
※なぜわざわざDifyを挟むのか?
一言で言えば、「システムのコードを一切いじらずに、プロンプトの微調整やAIの精度(Precision / Recall)向上に向けたチューニングを、GUI上で安全に行うため」です。
NotebookLMで整理した「4章の判定ロジック」をDifyのシステムプロンプトに移植し、OpenAIのStructured Outputs機能等を用いて「3章の出力データ定義」通りの厳格なJSONを返却するように設定しておくことで、エンジニアと非エンジニアが安全に分業できる環境が整います。
4. GitHub Copilotによる「堅牢なバックエンド」の自動生成
Dify側にAIの脳ができたら、次はVS Codeを開き、GitHub Copilotを使って「DifyのAPIを安全に叩き、フロントエンドにデータを返すためのバックエンドコード(schemas.ts)」を一瞬で生成させます。
Copilot Chatに #file:要件定義書.md を読み込ませ、「Zodスキーマと、データ不足時の例外処理を含めた評価関数を出力して」と指示するだけで、以下のような実務レベルの堅牢なTypeScriptコードが生成されます。
import { z } from 'zod';
const MIN_TEXT_LENGTH = 10;
type PreferenceLevel = '大' | '中' | '小';
type PriorityLevel = '高' | '中' | '低';
// ==========================================
// 1. 入力データ定義 (Input Schema)
// ==========================================
export const EmployeeProfileSchema = z.object({
employeeId: z.string().min(1, "社員IDは必須です"),
pastExperience: z.string().describe("過去の業務内容(プロジェクト履歴、担当フェーズ、役割の詳細)"),
projectSummary: z.string().describe("案件概要(案件の背景、目的、期待成果)"),
skills: z.array(z.string()).describe("保有スキル(言語、フレームワーク、ツール、ドメイン知識)"),
achievements: z.string().describe("主要な成果物(テキストまたはURL)"),
careerAspirations: z.string().describe("キャリア希望(今後挑戦したい領域など)"),
});
export type EmployeeProfile = z.infer<typeof EmployeeProfileSchema>;
export const AssignmentRequirementSchema = z.object({
assignmentId: z.string().min(1, "案件IDは必須です"),
description: z.string().describe("要求される業務内容(タスクの詳細、担当する役割)"),
requiredAndPreferredSkills: z.array(z.string()).describe("必須・歓迎スキル"),
priority: z.enum(['高', '中', '低']).describe("プロジェクトの優先度"),
});
export type AssignmentRequirement = z.infer<typeof AssignmentRequirementSchema>;
// ==========================================
// 2. 出力データ定義 (Output Schema)
// ==========================================
export const AgentResultSchema = z.object({
isAssignable: z.boolean().describe("アサイン可否の判定フラグ"),
matchProbability: z.number().min(0).max(100).describe("マッチング確率 (0-100%)"),
preferenceLevel: z.enum(['大', '中', '小']).describe("希望度(社員のキャリア希望との合致度)"),
priorityLevel: z.enum(['高', '中', '低']).describe("優先度(案件優先度および緊急度)"),
matchReason: z.string().describe("マッチング理由(判定理由、評価・不足要素の詳細)"),
});
export type AgentResult = z.infer<typeof AgentResultSchema>;
// ==========================================
// 3. 例外設計 (Exception Handling)
// ==========================================
export const AssignmentEvaluationErrorSchema = z.object({
errorCode: z.literal('INSUFFICIENT_INPUT'),
message: z.string(),
missingFields: z.array(z.string()).min(1),
});
export type AssignmentEvaluationErrorPayload = z.infer<typeof AssignmentEvaluationErrorSchema>;
export class AssignmentEvaluationError extends Error {
readonly payload: AssignmentEvaluationErrorPayload;
constructor(payload: AssignmentEvaluationErrorPayload) {
super(payload.message);
this.name = 'AssignmentEvaluationError';
this.payload = payload;
}
}
// Dify API等の外部評価器を注入するための型
export type AssignmentEvaluator = (prompt: string) => Promise<string | AgentResult | unknown>;
// ==========================================
// 4. メイン評価ロジック
// ==========================================
export async function evaluateAssignment({
employeeProfile,
assignmentRequirement,
evaluator,
}: {
employeeProfile: EmployeeProfile;
assignmentRequirement: AssignmentRequirement;
evaluator: AssignmentEvaluator;
}): Promise<AgentResult> {
const parsedEmployee = EmployeeProfileSchema.parse(employeeProfile);
const parsedAssignment = AssignmentRequirementSchema.parse(assignmentRequirement);
// データ不足チェック(要件5.1 フォールバックの実装)
const missingFields = collectInsufficientFields(parsedEmployee, parsedAssignment);
if (missingFields.length > 0) {
throw new AssignmentEvaluationError({
errorCode: 'INSUFFICIENT_INPUT',
message: '判定に必要な入力情報が不足しているため、評価を実行できません。',
missingFields,
});
}
// Dify側がプロンプトを持たない構成の場合はここで構築して渡すことも可能
const prompt = buildEvaluateAssignmentPrompt(parsedEmployee, parsedAssignment);
// evaluator関数内でDifyのAPI等をコールする
const rawResponse = await evaluator(prompt);
// 余計なMarkdown記法などをサニタイズしてJSON化
const normalizedResponse = normalizeAgentResponse(rawResponse);
// 最終的なZod検証
return AgentResultSchema.parse(normalizedResponse);
}
// --- ヘルパー関数群 ---
function collectInsufficientFields(emp: EmployeeProfile, req: AssignmentRequirement): string[] {
const missing: string[] = [];
if (isBlank(emp.pastExperience, MIN_TEXT_LENGTH)) missing.push('employeeProfile.pastExperience');
if (emp.skills.length === 0 || emp.skills.every(isBlank)) missing.push('employeeProfile.skills');
if (isBlank(emp.achievements, MIN_TEXT_LENGTH)) missing.push('employeeProfile.achievements');
if (isBlank(emp.careerAspirations, MIN_TEXT_LENGTH)) missing.push('employeeProfile.careerAspirations');
if (isBlank(req.projectSummary, MIN_TEXT_LENGTH)) missing.push('assignmentRequirement.projectSummary');
if (isBlank(req.description, MIN_TEXT_LENGTH)) missing.push('assignmentRequirement.description');
if (req.requiredAndPreferredSkills.length === 0 || req.requiredAndPreferredSkills.every(isBlank)) {
missing.push('assignmentRequirement.requiredAndPreferredSkills');
}
return missing;
}
function isBlank(value: string, minLength = 1): boolean {
return value.trim().length < minLength;
}
function normalizeAgentResponse(rawResponse: string | AgentResult | unknown): unknown {
if (typeof rawResponse === 'string') {
const sanitized = rawResponse.replace(/^```json\s*/i, '').replace(/^```\s*/i, '').replace(/```$/i, '').trim();
return JSON.parse(sanitized);
}
return rawResponse;
}
// (buildEvaluateAssignmentPromptの実装は省略)
要件定義書に記載した「5.1 データ不足時の挙動」が collectInsufficientFields 関数として、見事に型安全なTypeScriptコードへとマッピングされています。仕様書と睨めっこしながら手作業で型定義やエラーハンドリングを書いていたあのリードタイムが、完全にゼロになります。
5. 実際にPoCを動かしてみる(実行エントリーポイント)
型定義と評価ロジックが完成したので、実際にこれを動かしてPoCの挙動を確認します。
「DifyのAPIを叩く処理」をモック化して注入(Dependency Injection)することで、APIキーの準備すら後回しにして、まずはローカル環境で爆速でテストを回します。
先ほどの schemas.ts を呼び出す実行スクリプト(index.ts)を作成します。
import { evaluateAssignment, EmployeeProfile, AssignmentRequirement } from './schemas';
// ==========================================
// 1. テスト用のダミーデータ(通常はフロントやDBから取得)
// ==========================================
const dummyEmployee: EmployeeProfile = {
employeeId: "EMP-001",
pastExperience: "某大手銀行の基幹システムリプレイス案件にて、要件定義から基本設計までを担当。その後、Reactを用いた社内ツールのフロントエンド開発を半年経験。",
skills: ["TypeScript", "React", "Java", "要件定義"],
achievements: "社内向けタスク管理ツールのフロントエンド構築",
careerAspirations: "今後はフロントエンドの技術を極めつつ、テックリードとしてチームを牽引したい。",
};
const dummyRequirement: AssignmentRequirement = {
assignmentId: "PRJ-999",
description: "新規SaaSプロダクトのフロントエンド(Next.js/TypeScript)のリードエンジニア。アーキテクチャ設計から実装、コードレビューまでを担当。",
requiredAndPreferredSkills: ["TypeScript", "React", "Next.js", "テックリード経験"],
priority: "高",
};
// ==========================================
// 2. Dify呼び出しのモック(AIの脳の代役)
// ==========================================
const mockDifyEvaluator = async (prompt: string) => {
console.log("🚀 Dify APIにリクエストを送信中... (Mock)");
// 実際はここで fetch('https://api.dify.ai/v1/workflows/run', ...) を実行する
// 今回はAIが返してきた体で、Markdownのコードブロック付きのJSONを返す
return `
\`\`\`json
{
"isAssignable": true,
"matchProbability": 85,
"preferenceLevel": "大",
"priorityLevel": "高",
"matchReason": "React/TypeScriptのスキルおよびフロントエンド開発経験が案件要件に合致。キャリア希望である『テックリード』とも方向性が一致しているため、希望度も大と判定。"
}
\`\`\`
`;
};
// ==========================================
// 3. PoCの実行
// ==========================================
async function runPoC() {
try {
console.log("検証開始...");
const result = await evaluateAssignment({
employeeProfile: dummyEmployee,
assignmentRequirement: dummyRequirement,
evaluator: mockDifyEvaluator, // モックを注入
});
console.log("✅ 検証成功!Zodによる型検証を通過した結果:");
console.log(result);
} catch (error: any) {
console.error("❌ エラー発生:", error.message);
if (error.payload?.missingFields) {
console.error("不足している項目:", error.payload.missingFields);
}
}
}
runPoC();
このスクリプトを ts-node index.ts 等で実行すると、AIが返してきた(と仮定した)文字列のMarkdown記法が綺麗にサニタイズされ、Zodの厳格な型チェックを通過した安全なJSONオブジェクトとして出力されます。
もしダミーデータの pastExperience を数文字に削って実行すれば、LLMに無駄なリクエストが飛ぶ前に AssignmentEvaluationError が発火し、システムが安全に停止することも確認できます。
AIというブラックボックスを、TypeScriptの堅牢な型システムの中に安全に閉じ込める。この一連の動作が確認できれば、あとは mockDifyEvaluator の中身を本物のAPIリクエストに差し替えるだけで、エンタープライズ品質のバックエンドが完成します。
6. NotebookLMを用いた「インタラクティブなチームレビュー」
要件定義をチームに共有する際、静的なPDFやMarkdownを配るだけのレビューでは、後から前提条件の確認や仕様の認識ズレが発覚し、多大なコミュニケーションコストがかかるケースがあります。
そこで、NotebookLMのプロジェクト(ノートブック)自体をチームに共有します。メンバーは「ネットワーク瞬断時の挙動はどこに定義されてる?」「データ不足時の例外処理の仕様は?」とNotebookLMに直接質問してファクトチェックができるため、レビューの精度とスピードが劇的に向上し、より本質的なアーキテクチャやセキュリティの議論に集中できます。
まとめ
上流工程の「思考・分析」はNotebookLMに任せ、AIエージェントの「インスタンス化・運用」はDifyが担い、「実装・出力」はCopilotが加速させる。そして、それらをMarkdownとGitで一元管理する。
このパイプラインを構築すれば、テクニカルディレクションからプロダクションコードへの落とし込みまでのスピードは格段に跳ね上がります。「実物(PoC)を見てから仕様をブラッシュアップする」という超高速なイテレーションを回せるようになるので、皆さんもぜひ日々の非効率を徹底的に排除し、開発のスピードを最大化するために試してみてください。