以下の内容はhttps://blog.nflabs.jp/entry/2026/03/26/170000より取得しました。


プロンプトで制御できないLLMをParlantで統制する

概要

LLMエージェントをプロンプトだけで制御していると、同じ質問に対して応答がぶれたり、意図した動作を守れなかったりすることがあります。
本記事では、ParlantのGuidelineベース制御を用いて、こうした課題にどう対処できるかを紹介します。あわせて、プロダクション運用を見据えたMongoDB+Qdrantによるカスタムストア構成の実装方法も解説します。

この記事では次の内容を扱います。

  • プロンプトの限界とGuidelineベース制御のメリット
  • Parlantの内部動作(GuidelineMatcher、Journey、Relationship)
  • MongoDB+Qdrantによるプロダクション構成の実装

1. はじめに

1.1 なぜParlantを調査したか

Purple Flairには、ユーザーの学習を支援するアドバイス機能があります。この機能にLLMを活用していますが、運用する中で以下の課題が顕在化しました。

  • 意図しない回答の生成: ヒントを与えるべき場面で、問題の答えそのものを教えてしまう
  • 再現性の欠如: 同じ入力でも異なる出力が返るため、テストやデバッグが難しい
  • 監査性の不足: なぜその応答が生成されたのか、判断根拠を追跡できない
  • 統制の難しさ: プロンプトの変更が予期しない副作用を引き起こす

これらはプロンプトベースの制御に起因する問題です。たとえば「ヒントを与えるべき場面では答えを教えない」というルールをプロンプトに書いても、会話が長くなるとその指示が埋もれてしまったり、状況に応じて動的に適用するのが難しかったりします。
こうした問題に対処するため、LLMの振る舞いをより明示的に制御しやすいフレームワークであるParlantを調査しました。

Parlantは、Guideline・Relationship・Journeyなどの構造化された概念を通じて、エージェントの振る舞いをより明示的に制御しやすくするフレームワークです。エージェントの応答を一貫性・追跡可能性・安全性の観点から扱いやすくするよう設計されています。エージェントに守らせたいルールをGuidelineとして定義し、現在の会話コンテキストに基づいて条件に合致するGuidelineだけを選択して適用することで、会話が長くなっても指示が埋もれず、状況に応じて動的に適用できるようになります。

本記事では、Parlantの内部動作を理解した上で、プロダクション環境に適したストレージ構成(MongoDB+Qdrant)を実装する方法を解説します。

1.2 Parlantとは

Parlantは、ガイドラインベース会話エージェントを構築するためのフレームワークで、Guideline, Relationship, Journey, Glossary, Toolsなどの概念を用いてエージェントを構成します。*1*2
特にGuidelineは、「条件(condition)」と「行動(action)」のペアで定義され、各応答生成で関連するものだけが評価・適用されます。
これにより、どのルールが適用されたかを追跡でき、要件変更時も影響範囲を特定しやすくなります。

1.3 Parlantの基本的な動作

Parlantの基本的な動作は、Guidelineのマッチング、Journeyによる状態遷移管理、Glossaryによる用語補完といった仕組みに支えられています。

ガイドラインマッチング機構

エージェントがメッセージを受信すると、GuidelineMatcherが現在の会話コンテキストをもとに、適用可能なGuidelineを選択します。このマッチングは、LLMの文脈理解能力を利用して行われます。*3
Guidelineはconditionとactionから構成され、条件に合致したものだけが応答生成コンテキストに取り込まれます。公式ドキュメントでは、conditionは会話全体を踏まえて関連性を判定し、actionはマッチした際にエージェントの振る舞いやツール呼び出しを方向づけるものとして説明されています。*4
重要なのは、これらの評価が応答生成の前に行われる点です。Parlantは、現在の会話に当てはまるGuideline、必要に応じて実行したツールの結果、Glossary、会話履歴を揃えたうえで応答を生成します。一方で、ツールを実行した結果として、新たに当てはまるGuidelineが出てくることがあります。そのため、「どのGuidelineが当てはまるかの評価 → ツール実行 → 再評価」は1回で終わらず、複数回繰り返されることがあります。*5*6*7*8

この設計により、すべての指示を長い固定プロンプトに埋め込むのではなく、応答生成する上で本当に関係するルールだけを絞って扱いやすくなります。結果として、振る舞いの制御性や追跡可能性を高めやすくなります。プロンプトベースのアプローチで起きがちな「指示の埋没」や「意図しない動作」も、こうした動的なコンテキスト選択によって緩和しやすくなります。

Journeyによる状態遷移管理

Journeyは、複数ステップからなる対話フローを状態遷移図として表現します。各Journeyは以下の要素で構成されます。*9

  • Conditions: Journeyをアクティブ化する条件
  • Nodes(JourneyNode): 対話の各段階を表すノード。アクション、使用するツール、メタデータを持つ
  • Edges(JourneyEdge): ノード間の遷移条件を定義するエッジ。source、target、conditionを持つ

JourneyNextStepSelectionアルゴリズムは、現在の会話コンテキストとユーザの入力に基づいて、次に進むべき状態を決定します。ステップの完了判定では、CUSTOMER_DEPENDENT、REQUIRES_AGENT_ACTION、TOOL_EXECUTIONの3種類が考慮されます。なお、Journeyの状態マッチング全体は、Guidelineのマッチングと並行して実行されるよう最適化されています。*10

  • CUSTOMER_DEPENDENT: ユーザからの情報提供が必要なステップ。ユーザが要求された情報を提供した時点で完了とみなされます。
  • REQUIRES_AGENT_ACTION: エージェントが何かを伝える、または特定のアクションを実行する必要があるステップ。記述されたアクションが実行された時点で完了とみなされます。
  • TOOL_EXECUTION: 外部ツールの実行が必要なステップ。ツールが実行された時点で完了とみなされます。

Glossaryによる用語補完

Glossaryは、エージェントが扱う用語の定義を保存し、会話中に関連する用語を参照できるようにする仕組みです。*11
イメージとしては、会話に応じて関連情報を外部から取り出して文脈に補うという点で、RAGに近い役割を持ちます。ただし、一般的なRAGのように幅広い知識を検索するというよりは、用語定義を一貫して補完するための仕組みとして使われます。

1.4 関係性モデルと補足

コンテキスト管理と注意機構の最適化

LLMのコンテキストウィンドウは有限であるため、Parlantは現在の会話に関連するGuidelineやJourneyを選択して応答生成に利用します。これにより、コンテキストを必要なものに絞り込み、関連する指示を扱いやすくします。

この最適化は、以下のような流れで行われます。

  • 予測フェーズ: 現在のコンテキストから、アクティブ化される可能性の高いJourneyを予測
  • 並列マッチング: 予測されたJourneyの状態マッチングとGuidelineのマッチングを並列実行

予測から外れたJourneyがアクティブ化された場合には、追加の状態マッチングが行われます。

関係性モデルと優先順位制御

GuidelineやJourneyの間には、Relationshipという概念で関係性を定義できます。関係性には以下の6種類があります。*12*13

  • Entailment: 一方が他方を含意する関係(SOURCEがアクティブ化されたとき、TARGETも常にアクティブ化される)
  • Priority: 一方が他方より優先される関係(例:ユーザが不満を表明した場合のエスカレーション)
  • Dependency: 一方が他方に依存する関係(TARGETもアクティブ化されていない限り、SOURCEを非アクティブ化する)
  • Disambiguation: 複数のターゲットが同時にアクティブ化された場合に、どのアクションを取るべきかをユーザに確認する関係
  • Reevaluation: TARGETツールが実行されたとき、レスポンス前にSOURCEガイドラインを再評価する関係
  • Overlap: SOURCEとTARGETのツールが両方評価される場合に、競合を防ぐため同じバッチで評価する関係

これらの関係性を定義することで、複数のGuidelineやJourneyが同時に関わる場面でも、優先順位や依存関係を明示的に扱いやすくなります。

1.5 まとめ

1章では、プロンプトベースの制御が抱える課題と、Parlantがそれをどのように解決するかを整理しました。プロンプトで制御するアプローチには、再現性の欠如・監査性の不足・統制の難しさという根本的な限界があります。会話が長くなると指示が埋もれ、状況に応じた動的なルール適用が難しくなります。

Parlantは、以下の仕組みによってこの課題に対処します。

  • Guideline: conditionとactionのペアでルールを明示的に定義し、各会話で関連するものだけをコンテキストに取り込みます
  • Journey: 複数ステップの対話フローを状態遷移図として管理し、会話の進行を構造的に追跡します
  • Relationship: 複数のGuidelineが競合する場合の優先順位や依存関係を明示的に定義できます
  • Glossary: 用語の意味をベクトル検索で一貫して参照できるようにします

これらにより、どのルールがなぜ適用されたかを追跡でき、要件変更時の影響範囲も特定しやすくなります。

次章では、このParlantをプロダクション環境で運用するために必要な、ストレージ構成の考え方と実装方法を解説します。

2. カスタムストア構成の背景

2.1 なぜプロダクション向けのストア構成が必要か

Parlantはデフォルトでインメモリやファイルベースのストアで動作しますが、プロダクション環境では以下の理由からストア構成を見直す必要があります。

  • 永続性: インメモリストアはプロセス終了時にデータが失われる
  • スケーラビリティ: ファイルベースは単一インスタンスでの運用を前提としている
  • 可用性: 複数インスタンス構成やフェイルオーバーには外部ストアが必要になる

Parlantはストレージ層を抽象化しているため、要件に応じて適切なストア実装を選択できます。

2.2 Parlantのストレージ設計

Parlantは設計上、ドキュメントストアとベクトルストアを別々のインターフェースとして分離しています。これは、それぞれのストアが異なるデータ特性と操作要件を持つためです。

  • DocumentDatabase: Agent, Guidelineなどの構造化データを扱うCRUD操作。トランザクションや整合性制約が重要なデータを保存します。
  • VectorDatabase: Glossary, Journeyなどのベクトル埋め込みと類似検索。高次元ベクトル空間での近傍探索が主な用途です。

この分離設計により、以下のメリットが得られます。

  • 最適なストアの選択: 構造化データにはMongoDB、ベクトル検索にはQdrantなど、用途に最適なストアを独立して選択できます
  • 独立したスケーリング: ドキュメント保存とベクトル検索の負荷特性に応じて、別々にスケールできます
  • 技術的柔軟性: それぞれのストアに最適な実装を採用でき、将来的な技術変更にも対応しやすくなります

この分離により、ドキュメントストアとベクトルストアのそれぞれの実装が必要になります。次節では、Parlantが提供しているストア実装を紹介します。

2.3 提供されているストア実装

Parlantは以下のストア実装を提供しています。

DocumentDatabase

  • TransientDocumentDatabase: インメモリ実装*14
  • JSONFileDocumentDatabase: ファイルベース実装*15
  • MongoDocumentDatabase: MongoDB実装*16
  • SnowflakeDocumentDatabase: Snowflake実装*17

VectorDatabase

  • TransientVectorDatabase: インメモリ実装*18
  • ChromaDatabase: Chroma実装*19
  • QdrantDatabase: Qdrant実装*20

今回の構成では、DocumentDatabaseの実装としてMongoDBを、VectorDatabaseの実装としてQdrantを採用しました。
AgentStoreなどのビジネスロジック層は、DocumentDatabaseを通じてデータを永続化します。したがって、DocumentDatabaseの実装を差し替えることで、コードを変更せずにストレージを切り替えられます。

2.4 モジュール差し替え機構

ストア実装を切り替えるには、Parlantのモジュール差し替え機構を使います。
Parlantは内部でlagomのDIコンテナを利用しており、モジュールの差し替えは主に次の3つのライフサイクルフックで行います。*21

  • configure_module: DIコンテナに型を登録するフェーズです。どのインターフェースに対して、どの実装を使うかをここで決めます。DB接続の生成など、初期化時に必要なI/O処理もこの段階で実行できます。
  • initialize_module: configure_moduleの後に、追加の初期化が必要な場合に使います。Parlant本体の初期化が完了した後に実行されるため、他の初期化処理との順序が重要な場合に利用します。
  • shutdown_module: 終了時の後片付けを行うフェーズです。たとえば、DBコネクションやクライアントをクローズする処理をここに書きます。

通常は、configure_moduleで登録と接続準備を行い、shutdown_moduleで終了処理を書く構成で十分です。一方で、初期化順序に依存するケースではinitialize_moduleが必要になります。たとえば後述するGlossaryStoreのように、Parlantコアの初期化後でないと正しく組み立てられない処理ではinitialize_moduleが必要になります。

次章では、この仕組みを使ってMongoDB + Qdrant構成を実装する方法を説明します。

3. カスタムストアの実装

今回は、Parlantが提供するStore層の実装はできるだけ再利用し、DocumentDatabase/VectorDatabaseのバックエンドだけを差し替えます。
そのため、アプリケーション側のコードは大きく変えずに、永続化に関するコードだけをプロダクション向けに置き換えられます。

3.1 モジュールファイルの書き方

以下にagent用のカスタムモジュールの例を示します。

# custom_modules/mongo_agent_store.py
from contextlib import AsyncExitStack
import os
from typing import Tuple

from lagom import Container
from pymongo import AsyncMongoClient

from parlant.adapters.db.mongo_db import MongoDocumentDatabase
from parlant.core.agents import AgentDocumentStore, AgentStore
from parlant.core.common import IdGenerator
from parlant.core.loggers import Logger

_exit_stack = AsyncExitStack()

MONGO_URL_ENV = "PARLANT_MONGO_URL"
ALLOW_MIGRATION_ENV = "PARLANT_MONGO_ALLOW_MIGRATION"


def _parse_bool_env(name: str, default: bool = False) -> bool:
    value = os.environ.get(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


async def configure_module(container: Container) -> Container:
    mongo_url = os.environ.get(MONGO_URL_ENV)
    if not mongo_url:
        raise RuntimeError(
            f"{MONGO_URL_ENV} is required to enable MongoDB-backed persistence."
        )

    database_name = os.environ.get("PARLANT_MONGO_DB_AGENTS", "parlant_agents")
    allow_migration = _parse_bool_env(ALLOW_MIGRATION_ENV, default=False)

    mongo_client = await _exit_stack.enter_async_context(AsyncMongoClient(mongo_url))
    document_db = await _exit_stack.enter_async_context(
        MongoDocumentDatabase(
            mongo_client=mongo_client,
            database_name=database_name,
            logger=container[Logger],
        )
    )

    agent_store = await AgentDocumentStore(
        id_generator=container[IdGenerator],
        database=document_db,
        allow_migration=allow_migration,
    ).__aenter__()

    container = container.clone()
    container[AgentDocumentStore] = agent_store
    container[AgentStore] = agent_store
    return container


async def shutdown_module() -> None:
    await _exit_stack.aclose()

カスタムモジュールは、2.4で説明したライフサイクルフックを持つPythonファイルです。2つのフックはそれぞれ呼ばれるタイミングが異なります。

フック タイミング やること
configure_module 起動時 DIコンテナへの型バインド、DB接続
shutdown_module 終了時 コネクション解放

実装のポイントは以下の通りです。

  • 既存のAgentDocumentStoreを再利用: Parlantが提供する実装をそのまま使い、バックエンドだけをMongoDBに差し替えています
  • container.clone(): 元のコンテナを変更せず、新しい定義を追加したクローンを返します
  • AgentStoreとAgentDocumentStoreの両方を登録: インターフェース型と具象型の両方を登録することで、どちらの型でも注入可能になります
  • AsyncExitStackによる接続管理: コネクションのライフサイクルを管理し、shutdown_moduleで一括解放します
3.2 QdrantによるGlossaryStoreの実装例

GlossaryStoreはベクトル検索(VectorDatabase)とドキュメント保存(DocumentDatabase)の両方を使用します。Parlantは、QdrantDatabaseとGlossaryVectorStoreを提供しているので、これらを組み合わせて実現します。

GlossaryStoreの差し替えは、他のストアより複雑です。Parlantのコア初期化処理で先にデフォルトのGlossaryStoreが定義されてしまうため、configure_moduleでプレースホルダーを登録しておき、initialize_moduleで実体を差し替えるという2段階の処理が必要です。

# custom_modules/qdrant_glossary_store.py
from contextlib import AsyncExitStack
import os
from pathlib import Path
from typing import Type, Tuple

from lagom import Container
from pymongo import AsyncMongoClient

from parlant.adapters.db.mongo_db import MongoDocumentDatabase
from parlant.adapters.vector_db.qdrant import QdrantDatabase
from parlant.core.common import IdGenerator
from parlant.core.glossary import GlossaryStore, GlossaryVectorStore
from parlant.core.loggers import Logger
from parlant.core.nlp.embedding import Embedder, EmbedderFactory, EmbeddingCache
from parlant.core.nlp.service import NLPService
from parlant.core.tracer import Tracer

_exit_stack = AsyncExitStack()

MONGO_URL_ENV = "PARLANT_MONGO_URL"
ALLOW_MIGRATION_ENV = "PARLANT_MONGO_ALLOW_MIGRATION"

_glossary_store_instance: GlossaryVectorStore | None = None


def _parse_bool_env(name: str, default: bool = False) -> bool:
    value = os.environ.get(name)
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "on"}


async def configure_module(container: Container) -> Container:
    container = container.clone()

    def _lazy_glossary_store(c: Container) -> GlossaryVectorStore:
        if _glossary_store_instance is None:
            raise RuntimeError("GlossaryStore not yet initialized.")
        return _glossary_store_instance

    container[GlossaryStore] = _lazy_glossary_store
    container[GlossaryVectorStore] = _lazy_glossary_store
    return container


async def initialize_module(container: Container) -> None:
    global _glossary_store_instance
    embedder_factory = EmbedderFactory(container)

    async def get_embedder_type() -> Type[Embedder]:
        return type(await container[NLPService].get_embedder())

    qdrant_url = os.getenv("QDRANT_URL")
    qdrant_api_key = os.getenv("QDRANT_API_KEY")

    if qdrant_url and qdrant_api_key:
        qdrant_db = QdrantDatabase(
            logger=container[Logger],
            tracer=container[Tracer],
            url=qdrant_url,
            api_key=qdrant_api_key,
            embedder_factory=embedder_factory,
            embedding_cache_provider=lambda: container[EmbeddingCache],
        )
        )
    else:
        raise RuntimeError("QDRANT_URL and QDRANT_API_KEY is required.")

    vector_db = await _exit_stack.enter_async_context(qdrant_db)

    mongo_url = os.environ.get(MONGO_URL_ENV)
    if not mongo_url:
        raise RuntimeError(
            f"{MONGO_URL_ENV} is required to enable MongoDB-backed persistence."
        )

    database_name = os.environ.get("PARLANT_MONGO_DB_GLOSSARY", "parlant_glossary")
    allow_migration = _parse_bool_env(ALLOW_MIGRATION_ENV, default=False)

    mongo_client = await _exit_stack.enter_async_context(AsyncMongoClient(mongo_url))
    document_db = await _exit_stack.enter_async_context(
        MongoDocumentDatabase(
            mongo_client=mongo_client,
            database_name=database_name,
            logger=container[Logger],
        )
    )

    _glossary_store_instance = await GlossaryVectorStore(
        id_generator=container[IdGenerator],
        vector_db=vector_db,
        document_db=document_db,
        embedder_type_provider=get_embedder_type,
        embedder_factory=embedder_factory,
        allow_migration=allow_migration,
    ).__aenter__()


async def shutdown_module() -> None:
    await _exit_stack.aclose()

実装のポイントは以下の通りです。

  • プレースホルダーパターン: configure_moduleで遅延評価関数を登録し、実際の初期化はinitialize_moduleで行います
  • Embedderの注入: GlossaryVectorStoreはベクトル埋め込みを生成するため、EmbedderFactoryとembedder_type_providerが必要です
3.3 モジュールの読み込み方

作ったモジュールはparlant-serverの--moduleオプションで読み込みます。*22

  • 1つだけ読み込む場合
uv run parlant-server run --openai --module custom_modules.mongo_agent_store
  • 複数読み込む場合
uv run parlant-server run --openai \
  --module custom_modules.mongo_agent_store \
  --module custom_modules.qdrant_glossary_store

これにより、Parlant起動時にカスタムモジュールが読み込まれ、各モジュールで定義したストア(MongoDBやQdrant)がDIコンテナに登録されます。エージェントがAgent、Guideline、Glossaryなどのデータを参照・保存する際は、これらのカスタムストアが使用されます。

今回の構成では、各モジュール(mongo_agent_store、qdrant_glossary_store)は独立して動作するため、読み込み順序は任意です。

4. まとめ

本記事では、プロンプトだけでは制御しきれないLLMの振る舞いを、ParlantのGuidelineベース制御で統制するアプローチを紹介しました。ルールを明示的に定義し、条件に応じて適用することで、プロンプトベースでは難しかった再現性・監査性・統制を実現できます。また、プロダクション環境での運用を見据え、MongoDB+Qdrantによるカスタムストア構成の実装方法も解説しました。Parlantのモジュール差し替え機構を活用することで、コアを触らずにストレージバックエンドを切り替えられます。

ParlantのGuidelineベース制御と柔軟なストレージ構成により、LLMエージェントの振る舞いをより確実に制御し、プロダクション環境で運用できるシステムを構築できます。本記事が、LLMエージェントの統制と運用の課題に取り組む際の参考になれば幸いです。

*1:GitHub - emcie-co/parlant: The conversational control layer for customer-facing AI agents - Parlant is a context-engineering framework optimized for controlling customer interactions. · GitHub

*2:Motivation | Parlant

*3:parlant/docs/concepts/customization/guidelines.md at release/3.2.2 · emcie-co/parlant · GitHub

*4:Guidelines | Parlant

*5:parlant/src/parlant/core/engines/alpha/engine.py at release/3.2.2 · emcie-co/parlant · GitHub

*6:parlant/src/parlant/core/engines/alpha/engine.py at release/3.2.2 · emcie-co/parlant · GitHub

*7:parlant/src/parlant/core/engines/alpha/engine.py at release/3.2.2 · emcie-co/parlant · GitHub

*8:parlant/src/parlant/core/engines/alpha/engine.py at release/3.2.2 · emcie-co/parlant · GitHub

*9:Journeys | Parlant

*10:parlant/src/parlant/core/engines/alpha/guideline_matching/generic/journey/journey_next_step_selection.py at release/3.2.2 · emcie-co/parlant · GitHub

*11:Glossary | Parlant

*12:Relationships | Parlant

*13:parlant/src/parlant/core/relationships.py at release/3.2.2 · emcie-co/parlant · GitHub

*14:parlant/src/parlant/adapters/db/transient.py at release/3.2.2 · emcie-co/parlant · GitHub

*15:parlant/src/parlant/adapters/db/json_file.py at release/3.2.2 · emcie-co/parlant · GitHub

*16:parlant/src/parlant/adapters/db/mongo_db.py at release/3.2.2 · emcie-co/parlant · GitHub

*17:parlant/src/parlant/adapters/db/snowflake_db.py at release/3.2.2 · emcie-co/parlant · GitHub

*18:parlant/src/parlant/adapters/vector_db/transient.py at release/3.2.2 · emcie-co/parlant · GitHub

*19:parlant/src/parlant/adapters/vector_db/chroma.py at release/3.2.2 · emcie-co/parlant · GitHub

*20:parlant/src/parlant/adapters/vector_db/qdrant.py at release/3.2.2 · emcie-co/parlant · GitHub

*21:Getting Started - Lagom - Dependency Injection Container

*22:parlant/src/parlant/bin/server.py at release/3.2.2 · emcie-co/parlant · GitHub




以上の内容はhttps://blog.nflabs.jp/entry/2026/03/26/170000より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14