AIエージェントデータモデラー現る！

## この記事の概要

通称AIエージェント実践本「LangChainとLangGraphによるRAG・AIエージェント実践入門」の勉強会を開催しています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/50951100-0b1d-e930-d253-863b2d07f827.jpeg" width=20% />

---
AIエージェント編の開催に向けて、「**AIエージェントデザインパターンを試してみた！**」というものになります。

具体的には、[書籍](https://gihyo.jp/book/2024/978-4-297-14530-9)の第12章 「LangChain/LangGraphで実装するエージェントデザインパターン」をベースとして、より実践的な内容にカスタマイズしてみました。

---
## どんなものを作ったか？
この記事では、データモデリングを行うAIエージェントにカスタマイズしてみました。

書籍の内容は「汎用的なAIエージェント」を作るというものになっています。
これを少しカスタマイズして「**データモデリングに特化したAIエージェント**」としています。

---
## 書籍：第12章のおさらい

第12章では以下のデザインパターンをあつかっています。

- パッシブゴールクリエイター (Passive Goal Creator)
- プロンプト/レスポンス最適化 (Prompt/Response Optimizer)
- シングルパスプランジェネレーター (Single-Path Plan Generator)
- マルチパスプランジェネレーター (Multi-Path Plan Generator)
- セルフリフレクション (Self-Reflection)
- クロスリフレクション (Cross-Reflection)
- 役割ベースの協調 (Role-Based Cooperation)

ここでは詳細は触れませんので、書籍の第11章、12章をご参照ください。

---
## デザインパターンの選定

今回は「シングルパスプランジェネレーター (Single-Path Plan Generator)」を採用しました。

第11章での説明によるとシングルパスプランジェネレーターは以下のような特徴をもっています。
> シングルパスプランジェネレーター（図11.6）はユーザーの目標を達成するための一連の手順や行動計画を生成するパターンです。このパターンは、比較的単純なタスクや、明確な手順が存在する問題に対して効果的です。
(第11章 エージェントデザインパターン 5.シングルパスプランジェネレーターから引用)

データモデリングは明確な手順が存在するため、このパターンが有効であると考えました。

---
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/53879532-838c-ed41-2835-b5f13b1b9a09.png)

(第11章 エージェントデザインパターン 5.シングルパスプランジェネレーターから引用)


---
## カスタマイズの内容と方針

プログラムの構造には基本的に手を入れずに、一部のプロンプトのみを変更しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/1ed52a00-2828-1a6c-7aff-2d8878367f84.png)

(第12章 12.5 シングルパスプランジェネレーターの図表を引用し補記)

---
## カスタマイズ内容の詳細

### PassiveGoalCreatorのプロンプト変更

```python
prompt = ChatPromptTemplate.from_template(
    "ユーザーの入力を分析し、明確で実行可能な目標を生成してください。\n"
    "要件:\n"
    "1. エンティティを抽出する。(提示されたユースケースの名詞、動詞に着目)\n"
    "2. 洗い出したエンティティを[リソース]と[イベント]に分類する。イベントに分類する基準は属性に”日時・日付（イベントが実行された日時・日付）”を持つものである。\n"
    "3. イベントエンティティには1つの日時属性しかもたないようにする。\n"
    "4. リソースに隠されたイベントを抽出する。（リソースに更新日時をもちたい場合にはイベントが隠されている可能性がある）\n"
    "  例）社員情報（リソース）の更新日時がある場合には、社員異動（イベントエンティティ）を抽出する。\n"
    "5. エンティティ間の依存度が強すぎる場合には、交差エンティティ（関連エンティティ）を導入する。（カーディナリティが多対多の関係を持つような場合に導入する）\n"
    "ユーザーの入力: {query}"
)
```
モデリングの手順は川島氏の「[イミュータブルデータモデル](https://scrapbox.io/kawasima/%E3%82%A4%E3%83%9F%E3%83%A5%E3%83%BC%E3%82%BF%E3%83%96%E3%83%AB%E3%83%87%E3%83%BC%E3%82%BF%E3%83%A2%E3%83%87%E3%83%AB)」を参考にしました。

---
### TaskExecutorでAmazon Bedrock KnowlegeBaseを参照するよう変更

元々は、タスク実行時にTavilyでのインターネットサーチのみをするようになっていました。
しかし、いまいちモデリングに関係ない情報を取ってきてしまうため、KnowledeBaseにモデリングに関する情報を与え、それを参照するように変更しました。
以下は、変更した部分のコード（TaskExecutorのコンストラクタ）です。

```python
class TaskExecutor:
    def __init__(self, llm: ChatOpenAI):
        self.llm = llm

        retriever = AmazonKnowledgeBasesRetriever(
            knowledge_base_id=settings.KNOWLEDGE_BASE_ID,
            retrieval_config={"vectorSearchConfiguration": {"numberOfResults": 10}},
        )
        tool = create_retriever_tool(
            retriever,
            "BedrockKnowledgeBasesRetriever",
            "With this tool, you can search the Amazon Bedrock KnowledgeBase to gain insights into Immutable Data Modeling.",
        )

        self.tools = [
            tool,
            TavilySearchResults(max_results=3)
        ]

```


---
### ResultAggregatorのプロンプト変更

書籍では、「ユーザー入力」を渡しておらず、「与えられた目標」のみ渡しています。
何回か試したところ、「与えられた目標」の内容によって出力が大きく揺らぐため、「ユーザー入力」も渡すようにしました。

```python
prompt = ChatPromptTemplate.from_template(
    "ユーザー入力:\n{query}\n\n"
    "与えられた目標:\n{optimized_goal}\n\n"
    "調査結果:\n{results}\n\n"
    "ユーザー入力と与えられた目標に対し、調査結果を用いて、以下の指示に基づいてレスポンスを生成してください。\n"
    "Mermeid形式のER図のコードは、省略せずにそのまま出力してください。\n"
    "Mermeid形式のER図のコードに限り、英語で出力してください。\n"
    "{response_definition}"
)
```

---
## 実行してみる

最初の実行コマンドに渡すプロンプトが少々長くなるためテキストファイルに記載します。

```text
以下のユースケースに対して、RDBMSを前提としたテーブル設計をおこなってください。

## ユースケース :
「請求期日が到来した場合、顧客に請求書を送付する。期日までに入金がない場合には、確認状を送付する。」

```

実行方法

```bash
uv run python -m single_path_plan_generation.main --task "$(cat task2.txt)"
```

---
## 実行結果

```markdown
以下に、請求書管理システムのためのデータベース設計を示します。各エンティティの役割、必要なカラム、外部キーの設定、イベントエンティティの日時属性、交差エンティティの導入理由について説明します。

### 1. 顧客テーブル
- **役割**: 顧客情報を管理します。
- **カラム**:
  - 顧客ID (主キー)
  - 顧客名
  - 住所
  - 電話番号
  - メールアドレス
  - 登録日
  - ステータス（アクティブ/非アクティブ）
- **外部キー**: なし

### 2. 請求書テーブル
- **役割**: 請求書の詳細を管理します。
- **カラム**:
  - 請求書番号 (主キー)
  - 発行日
  - 顧客ID (外部キー)
  - 請求金額
  - 支払期日
  - ステータス（未払い/支払い済み）
  - 支払い方法
- **外部キー**: 顧客ID -> 顧客テーブル

### 3. 入金テーブル
- **役割**: 入金情報を管理します。
- **カラム**:
  - 入金ID (主キー)
  - 入金日
  - 顧客ID (外部キー)
  - 入金額
  - 入金方法（現金、銀行振込、クレジットカードなど）
  - 入金ステータス
- **外部キー**: 顧客ID -> 顧客テーブル

### 4. 請求期日イベントテーブル
- **役割**: 請求書の期日イベントを管理します。
- **カラム**:
  - イベントID (主キー)
  - 請求書番号 (外部キー)
  - 期日
  - 通知ステータス（通知済み/未通知）
- **外部キー**: 請求書番号 -> 請求書テーブル
- **日時属性**: 期日はUTCで管理し、通知のタイミングを一貫して管理します。

### 5. 請求書送付イベントテーブル
- **役割**: 請求書の送付イベントを管理します。
- **カラム**:
  - イベントID (主キー)
  - 請求書番号 (外部キー)
  - 送付日
  - 送付方法（メール、郵送など）
  - 送付ステータス（送付済み/未送付）
- **外部キー**: 請求書番号 -> 請求書テーブル
- **日時属性**: 送付日はUTCで管理し、送付の履歴を正確に記録します。

### 6. 確認状送付イベントテーブル
- **役割**: 確認状の送付イベントを管理します。
- **カラム**:
  - イベントID (主キー)
  - 請求書番号 (外部キー)
  - 送付日
  - 送付方法（メール、郵送など）
  - 送付ステータス（送付済み/未送付）
- **外部キー**: 請求書番号 -> 請求書テーブル
- **日時属性**: 送付日はUTCで管理し、確認状の送付履歴を正確に記録します。

### ER図 (Mermaid形式)
mermaid
erDiagram
    CUSTOMER {
        int CustomerID PK
        string Name
        string Address
        string Phone
        string Email
        date RegistrationDate
        string Status
    }
    INVOICE {
        int InvoiceNumber PK
        date IssueDate
        int CustomerID FK
        float Amount
        date DueDate
        string Status
        string PaymentMethod
    }
    PAYMENT {
        int PaymentID PK
        date PaymentDate
        int CustomerID FK
        float Amount
        string PaymentMethod
        string PaymentStatus
    }
    DUE_DATE_EVENT {
        int EventID PK
        int InvoiceNumber FK
        date DueDate
        string NotificationStatus
    }
    INVOICE_SEND_EVENT {
        int EventID PK
        int InvoiceNumber FK
        date SendDate
        string SendMethod
        string SendStatus
    }
    CONFIRMATION_SEND_EVENT {
        int EventID PK
        int InvoiceNumber FK
        date SendDate
        string SendMethod
        string SendStatus
    }

    CUSTOMER ||--o{ INVOICE : has
    CUSTOMER ||--o{ PAYMENT : makes
    INVOICE ||--o{ DUE_DATE_EVENT : triggers
    INVOICE ||--o{ INVOICE_SEND_EVENT : sends
    INVOICE ||--o{ CONFIRMATION_SEND_EVENT : confirms


この設計により、請求書管理システムの各エンティティが明確に定義され、外部キーによって関連付けられています。イベントエンティティには日時属性が設定されており、交差エンティティは必要に応じて導入されています。
```


---

## ER図

![tmp63_320ei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130262/f3c1c135-225d-f725-0999-e21c89764602.png)

---
## LangSmithのログ

https://smith.langchain.com/public/f985cc32-9c87-4b18-98b0-1ed1d877c2ee/r

---
## Sourcecode

https://github.com/tis-abe-akira/agentic-data-modeler

---
## 学んだこと
「**自分なりの意図をもって改造するとすごい気づきが多かった**」

- 試行錯誤が大事
    - この記事では結論だけ掲載しているが、モデリング手順をどの段階で記載するか？又は、ネット記事を参照して手順を考えてもらうか？など色々試行錯誤
    - 結果として、冒頭のpassive_goal_creatorで指定するのが一番安定していた
- 今回はモデリングに特化したプロンプトを明示した
    - 特化型が良いか？汎用型が良いか？というのは悩ましい
    - しかし汎用型はビッグテックが出してくるChatアプリで解決可能な場合が多い
    - 差別化するならば、特化型のエージェントにするのが良さそう（仮説）
- 特化型エージェントを作るのに「AIエージェント実践本」はベースとして大変役立つ！

---
## 今後の課題

より実践的なデータモデラーとするためには、以下のような機能改善が必要と考えます。

- インターネットサーチではなく、プロジェクトの規約を用いてタスク実行するようにする
    - ネーミングルール
    - エンティティ抽出のルール
- 出力は、Mermeid記法のER図を出すところまでとしているが、より実装に近い機能も搭載する
    - テーブルの作成
    - ダミーのテストデータ投入
- UIの改善
    - 画面構築をしてより簡単に使えるようにする
    - プロンプトを外部から注入できるようにする
    - Human-in-the-Loopにより人間の介入を可能とする

---
## まとめ

今回は、少しの変更により「**AIエージェントデータモデラ―！**」の基盤部分ができました。
自分のニーズに合わせて変更しつつ、書籍を読み進めると理解が進みます。
また、実践的な内容の自分用エージェントが作れるということも体感できました。

皆さんも、ぜひ色々と活用して「AIエージェント開発」を一緒に盛り上げていきましょう！