はじめに
これは会話型のマルチエージェントシステムです。各エージェント(エンティティ)が専門領域を持ち、RAG やモデレーション(ガードレール)を使いながら応答を作ります。現状はターン制で順にエージェントを動かす(=ターン式AIチャット)実装になっています。
Source
TODO
アーキテクチャ概要
- コア概念:複数エージェント(Entity)が行動順序(Turn)に従って会話を生成
- 検索/知識基盤:RAG 材料(fixtures/rag_material.json 等)
- 安全性:静的ガードレール(禁止ワード等)+動的ガードレール(OpenAI Moderation API 想定)
- 永続化:ActionHistory / Message 等のモデルで会話履歴を管理
典型的な処理フロー(1ターンのライフサイクル)
- ユーザーがテキストを送信(
IndexView
経由でリクエストを受ける) - InputProcessor がガードレールチェックを実施
ユーザー入力に対して安全性を審査し、問題なければ加工済みメッセージを返す(危険ならエラー応答) - TurnManagement で「現在のターン」のエンティティを取得
- ユーザーに発言権がない場合はエラー応答
- ContextAnalyzer で文脈を整形し、RAGを参照してLLM応答を生成
- 必要に応じて外部情報や素材もRAG処理経由で参照
- TurnManagementRepository を通じて Message を保存し、ActionHistory を更新(done=True)
- 次の未処理 ActionHistory を取得し、ユーザーに次のエンティティ情報を返却
主要コンポーネント
1. Factory層(ファクトリー層)
Factory層の役割とイメージ
Factory層は、「入力された情報やタイプに応じて、最適な値オブジェクトやドメインオブジェクトを "工場のように自動で組み立てて返却する" 役割」を持ちます。
現実の「部品工場」のように、依頼内容(例: 製品の種類、スペック)に応じて、必要なパーツや工程を選び、完成品(インスタンス)として提供します。
この層を用いることで、
- どの型を返すべきか、呼び出し元(サービスやユースケース)側が意識しなくてよくなる
- 新しい型やロジックの追加・差し替えもfactoryの内部だけで完結できる
- 同じインターフェースで多様なValue ObjectやEntityを動的に生成でき、保守性と拡張性が向上する
たとえばRAG(検索拡張生成)素材のように、外部ソースや対象ごとに異なるメタデータ形式が存在する場合にも、「material_type」というキー情報を渡すだけで、自動的に該当するクラスのインスタンスを組み立てることができます。
Factory層利用の具体的なシナリオ例
- Slack/Gmail/マニュアル/PDF/Google Map等、素材タイプごとに違うメタデータを、そのまま呼び出し元が意識せず取得したい
- 複数の値オブジェクトがバラバラに増えても、利用側のコードを変更せずに拡張したい
- Factoryの実装を差し替えるだけで、システム全体の動作や扱う型を容易に増やせる柔軟な構造にしたい
このように、「種類」や「スペック」(material_typeや各種パラメータ)を渡すだけで、最適な値オブジェクトがインスタンス化されて戻ってきます。
具体実装例としては、「RagMetadataFactory」などが存在し、create(material_type, metadata_dict)
のようなシンプルなI/Fで呼び出せます。
2. Repository層(リポジトリ層)
Repository層の役割とイメージ
Repository層は、「ドメインオブジェクトや値オブジェクトを データストアからの入出力という観点で一元管理・取得・永続化する倉庫のような役割』」を持ちます。
現実の「倉庫・書庫」のように、依頼内容(例: どの型や条件で情報を取り出したいか)に応じて、必要なデータを各ストレージから探索し、適切な形で提供します。
この層を用いることで、
- データベースやAPIなど物理的なデータソースアクセスを、ドメインロジックやサービス側から隠蔽できる
- 取得条件や保存処理の共通化・再利用が可能となり、クリーンアーキテクチャやDDDの原則に沿った保守性の高いコードとなる
- 仮にDBや外部サービス、検索エンジン(例: RAGベクトル検索)が将来変更されても、Repositoryの実装を差し替えるだけで、サービスや呼び出し側への影響が最小限となる
たとえば「RAG素材」や「アクション履歴」など異なるデータ種別が増減しても、Repository経由で取り出すことで、上位層はデータ取得手段や保存先を意識せず利用できます。
Repository層利用の具体的なシナリオ例
-
material_type
を指定するだけで、RagMaterialテーブルに格納されている知識ソースをまとめて取得できる - 会話のターン管理(発言順やアクション履歴など)のために、特定条件で履歴や進行状況を取得・追加・更新できる
- ドメインサービスやユースケースで「どこに・どの形式・どんな条件で」保存/取得するか意識せず、RepositoryのI/F(例:
find_by_type
,save_action_history
)経由でデータを扱いたい - 将来的に「全文検索」「ベクトル検索API」「データベース/NoSQL/キャッシュ」など、物理層の実装が変更・追加されても、Repositoryの差し替えだけで対応可能
このように、「どのように保存・取得するか?」のロジックや接続先への依存コードはRepository層に集約され、 利用側が「何を(どんな条件・型)取り扱うか」だけを意識すればよくなります。
具体実装例としては、「ContextAnalyzerRepository」「TurnManagementRepository」などが存在し、get_rag_source_merged(material_type)
や find_next_turn_entity()
のようなシンプルで意図の明確なI/Fを持ちます。
3. Service層(サービス層)
Service層の役割とイメージ
Service層は、複数のリポジトリやドメインオブジェクトを横断的かつ一括して扱い、
アプリケーションにおける業務ロジックを“シナリオ単位”で記述・集約する層です。
現実で言えば「調理場」や「指示を出して工程を束ねるマネージャー」のようなもので、
「この操作をこういう順番・条件でやって、結果をこうまとめなさい」と全体進行を担います。
この層を使うことで、
- 複数のデータ取得・保存・ビジネス判定などを「1サービス=1業務シナリオ」としてまとめられる
- 上位のView・Controller層からは「業務目的」単位でサービスを呼び出すだけになり、コードの責務分担・再利用性・テスト容易性が高まる
- 将来的に業務フローや外部API連携が変わっても、Serviceの修正だけでUIやRepository変更を最小限にできる
たとえば「会話のターン進行」「ユーザー入力処理」「文脈再構成」「アクション履歴まとめ表示」など複数テーブル・多段階処理・ビジネスルールが絡む単位を、Service層に集約します。
Service層利用の具体的なシナリオ例
-
InputProcessor
を通じ、入力テキストのサニタイズ・ガードレール判定・各種LLM/外部サービス連携まで一括実行 -
TurnManagementService
で、ワークフロー内のターン数更新・シミュレーション・リセット・進行状況記録をまとめて一元管理 -
ContextAnalyzerService
で、発話内容から文脈や思考タイプの分類、キーワード抽出、関連エンティティの再構成を一括実施 - ViewやController層で「どの順番・どの条件で個別Repositoryを使うか」「外部API結果のサニタイズ/統合」など面倒なビジネスロジックをすべてService層で肩代わりしてもらうことで、呼び出し側をシンプルにできる
このように「どのような順序・組合せで業務処理を行うか?」のロジックや外部インタフェースとの実装はService層に集約し、上位層は「何をしたいか(どのシナリオ型Serviceを使うか)」だけ意識すればよくなります。
具体実装例としては、「InputProcessor」「TurnManagementService」「ContextAnalyzerService」などがあり、process_input(input_text, entity)
やprogress_turn(entity_id)
のような意図が明解で使いやすいI/Fを持ちます。
4. ValueObject層(値オブジェクト層)
ValueObject層の役割とイメージ
ValueObject層は、業務上の意味や制約付きの「値」を、不変(immutable)なデータ構造として厳密に表現・管理する 層です。「単なる型付きデータ」ではなく、「このデータがこの形でまとまっていること自体が重要な意味を持つ」場面で活用します。現実で言えば、“特定のルールが課されたIDカード”や“検証・変換済みの伝票”のように、「これさえあれば、ドメイン的に“意味”や“正しさ”が担保できる」 ものです。
この層を用いることで、
- 業務で多用される「まとまった情報」や「値の集合」に、一貫した制約・検証ロジックを組み込める
- たとえばレビューのメタデータや入力チェック結果など、「一度生成したら中身を書き換えない」意図が明確になる
- 検証済み・型安全なValueObjectを受け取ることで、サービス層やビジネスロジック側の実装簡素化・品質向上が実現できる
たとえば「Googleレビューメタ情報」「PDFメタデータ」「ガードレール検証結果」「エンティティの一意性情報」など、意味付きの値集合を毎回辞書やプリミティブ型でやり取りせず、ValueObjectインスタンス化して扱うことで、再利用・テスト・将来の拡張にも強くなります。
ValueObject層利用の具体的なシナリオ例
- Google Mapsレビューなどの情報を、「評価」「緯度経度」「著者名」「レビュー日時」など業務的必要情報を1つのVOで保持・検証
→GoogleMapsMetadata(rating, latitude, longitude, author_name, review_date, location_name)
- PDF/RAG素材のソースについて「ファイルパス」など必須情報を、ただのstrではなく必ず存在を検証したVOとして流通
- 入力ガードレール判定(禁止ワードOK/NGや違反カテゴリ等)のチェック結果をVOで一括返却
→ 呼び出し側はblocked
やviolation_categories
だけ判定すればよい - Entity/進行状況など、「この時点でのスナップショット」を意味する小さな値集合もVOで管理
- VOの
from_dict()/to_dict()
でAPI⇔ドメイン変換や永続化時の変換も一元化できる - 上位層・サービス層は**「すでに業務意味的に正しいデータしか受け取らない」**前提で安心して処理可能
- ValueObjectに検証や変換(例:日付のiso変換、範囲チェック)が組み込まれているため、
不正なデータを早期に拒否できるし、変更・拡張もVOだけ修正すれば全体に反映できる
このように、「複数値を束ね、意味・制約を保証するもの」はValueObjectとして実装・流通させることで、
「業務ロジックの安全性」と「保守性」、および「ドキュメント性」の向上が期待できます。
具体例には「GoogleMapsMetadata」「PdfSourceMetadata」「EntityVO」「GuardrailResult」「InputProcessorConfig」などがあり、from_dict()
/to_dict()
などのI/Fやバリデーションが組み込まれています。