

# LangGraph エージェントのセマンティック長期メモリ
<a name="ddb-langgraph-memory"></a>

LangGraph は 2 種類のエージェント状態を分離します。短期的な状態は会話スレッド自体であり、チェックポインターがスレッドを再開、再生、復旧できるように保持します (「[DynamoDB を LangGraph エージェントのチェックポイントストアとして使用する](ddb-langgraph-checkpoint.md)」を参照)。長期メモリはエージェントがスレッド間で保持する知識であり、LangGraph はそれをストアとしてモデル化します。ストアとは、エージェントが意図的に書き込み、後で読み返すための名前空間付きキーバリューサーフェスです。

[langgraph-checkpoint-aws](https://pypi.org/project/langgraph-checkpoint-aws/) パッケージの `DynamoDBStore` クラスは、そのストアの DynamoDB 実装です。階層的な名前空間、古くなったメモリを期限切れにする 有効期限 (TTL)、および基本的なフィルタリングを使用して、キーと値の側を処理します。ベクトルインデックスが設定されている場合 (「[DynamoDB でのベクトルインデックスの使用](VectorSearch.md)」を参照)、その `search()` メソッドはセマンティック検索を実行します。メモリは書き込み時に埋め込まれ、非同期的にインデックス化され、同じテーブルから意味に基づいて類似度順に呼び出されます。プロビジョニングが必要な個別のベクトルデータベースも、データをコピーするパイプラインも不要です。

## 前提条件
<a name="langgraph-memory-prerequisites"></a>
+ DynamoDB テーブルを作成し、Amazon Bedrock モデルを呼び出すアクセス許可を持つ AWS アカウント。
+ お使いのリージョンの Amazon Bedrock の埋め込みモデルへのアクセス。例としては、Amazon Titan Text Embeddings V2 などがあります。
+ Python 3.10 以降、`langgraph-checkpoint-aws` 1.2.2 以降および `boto3` 1.43.64 以降 (以前の `boto3` バージョンには `SearchVectors` オペレーションがありません)

pip を使用してライブラリをインストールします。

```
pip install langgraph langgraph-checkpoint-aws langchain-aws
```

## ストアをセットアップする
<a name="langgraph-memory-setup"></a>

`index` ブロックを使用してストアを設定し、`setup()` を呼び出します。

```
from langchain_aws import BedrockEmbeddings
from langgraph_checkpoint_aws import DynamoDBStore

store = DynamoDBStore(
    table_name="support-agent-memory",
    region_name="us-east-1",
    index={
        "embed": BedrockEmbeddings(model_id="amazon.titan-embed-text-v2:0"),
        "dims": 1024,
        "fields": ["text"],
        "distance_function": "COSINE",
    },
)

store.setup()
```

`index` ブロックの 4 つの設定について理解しておく価値があります。
+ `embed` は、LangChain の埋め込みオブジェクト、または文字列のリストをベクトルのリストにマッピングするプレーン呼び出し可能オブジェクトを受け取ります。
+ `dims` はモデルの出力サイズと一致している必要があります。Titan Text Embeddings V2 は、デフォルトで 1,024 次元を返します。これらが一致しない場合、ストアはインデックスが使用できないベクトルを書き込むのではなく、最初の書き込み時に両方の数値を示すディメンション不一致エラーを発生させます。
+ `fields` は、値のどの部分を埋め込むかを選択します。ここでは、`text` フィールドのみが埋め込まれます。デフォルトの `["$"]` は、値全体を JSON にシリアル化して埋め込みます。これは便利ですが、ブックキーピング属性も埋め込まれます。
+ `distance_function` のデフォルトは `COSINE` であり、 `EUCLIDEAN` および `DOT_PRODUCT` も受け入れます。

`setup()` は、テーブルが存在しない場合はテーブルを作成し、ベクトルインデックスをアタッチして、設定されている場合は有効期限 (TTL) を有効にします。既存のテーブルでは不足しているものだけが追加されるため、既存の `DynamoDBStore` デプロイでは、テーブルを再作成したり項目を移行したりすることなくセマンティック検索を採用できます。`setup()` は、インデックスが `ACTIVE` を報告し、バックフィルが完了するまで返らないことに注意してください。バックフィル中に `ACTIVE` を報告するインデックスは、 `SearchVectors` 呼び出しを拒否します。新しい空のテーブルでは、待機時間は短くなります。既存の項目を含むテーブルへの後付けには、バックフィルにかかる時間と同じだけの時間がかかります。

## メモリの書き込み
<a name="langgraph-memory-write"></a>

書き込みは通常の `put()` です。埋め込みは自動的に行われます。

```
namespace = ("memories", "acme-corp", "user-8812")

store.put(namespace, "mem-1", {
    "text": "Account runs a proxy that closes idle sockets after 60 seconds",
    "source": "ticket-4471",
})
store.put(namespace, "mem-2", {
    "text": "Customer prefers email, asked not to be called by phone",
    "source": "ticket-4471",
})
store.put(namespace, "mem-3", {
    "text": "Uses a custom build of the SDK pinned to version 2.14",
    "source": "ticket-4502",
})
```

## 意味によるリコール
<a name="langgraph-memory-recall"></a>

お客様が新しい会話を開始し、「約 1 分後に接続が何度も切れてしまう」と話したとします。

```
results = store.search(
    namespace,
    query="connection drops after a minute of inactivity",
    limit=3,
)

for item in results:
    print(round(item.score, 3), item.value["text"])
```

出力:

```
0.324 Account runs a proxy that closes idle sockets after 60 seconds
0.039 Customer prefers email, asked not to be called by phone
0.031 Uses a custom build of the SDK pinned to version 2.14
```

関連するメモリが最初にランク付けされ、クエリ内のどのキーとも一致しません。`SearchItem.score` はLangGraph の規則に従っており、値が高いほど関連性が高いことを示します。一方、DynamoDB は距離を返すため、値が低いほど近いことを示します。そのため、ストアは値を次のように変換します。`COSINE` の場合は `1 - distance`、`EUCLIDEAN` の場合は `1 / (1 + distance)`、`DOT_PRODUCT` の場合はスコアをそのまま使用します。絶対スコアに基づいてメモリがプロンプトに挿入するのに十分な関連性があるかどうかを判断する場合は、そのしきい値を独自のデータと選択した距離関数に対して調整してください。

## 検索スキーマを使用したメモリのスコープ
<a name="langgraph-memory-scoping"></a>

`DynamoDBStore` がベクトルインデックスを作成するとき、インデックスの検索スキーマで、テーブルのパーティションキーを `HASH` 要素として宣言します。この要素が存在するため、すべての検索でこの要素に対する条件を指定する必要があり、ストアはその条件として名前空間を指定します。名前空間のタプルは結合されてパーティションキーの値になるため、すべてのセマンティック検索は、正確に 1 つの名前空間に限定されます。これには、次の 3 つの結果があります。
+ **分離は構造的なものであり、忘れずに記述する必要があるフィルターではありません。**検索は別の名前空間のメモリに到達できないため、多数のテナントにサービスを提供するストアには、別のテナントのメモリを返すクエリ形式が存在しません。これはクエリのスコープ設定であり、認可ではありません。テーブルに対する `dynamodb:SearchVectors` 権限を持つプリンシパルは、任意の名前空間の値を直接検索できます。そのため、呼び出し元がどの名前空間を読み取れるかを決定することは、引き続き IAM とアプリケーションの認可レイヤーの役割です。
+ **リコールのコストは、テーブル全体ではなく 1 ユーザーのメモリ量に応じて決まります。**検索処理は、製品全体が保持するメモリの数ではなく、その 1 つの名前空間が保持するメモリの量によって制限されます。そのため、規模が拡大しても、レイテンシーとベクトル検索コストの両方を小さく、安定した状態に保つことができます。
+ **セマンティック検索は、プレフィックスではなく、完全一致の名前空間を対象とします。**検索は、指定した名前空間だけに到達し、その下位の名前空間には到達しません。`("memories", "acme-corp")` を検索しても、パーティションキーの値が異なるため、`("memories", "acme-corp", "user-8812")` には到達しません。名前空間がリコールのスコープとなるため、1 回の検索でどの範囲を参照させたいかに合わせて名前空間を選択してください。

以下の表は、名前空間の構成をどのように選択するかをまとめたものです。


| 名前空間の構成 | 1 回の `search()` で到達する範囲 | この構成を選択する場合 | 
| --- | --- | --- | 
| `("memories", user_id)` | そのユーザーのメモリ | ユーザーごとにリコールを行うシングルテナント製品 | 
| `("memories", tenant_id, user_id)` | そのテナントにおける、そのユーザー | マルチテナント (一般的なケース) | 
| `("memories", tenant_id, user_id, agent_name)` | そのユーザーに関する 1 つのエージェントのメモ | 複数の専門エージェントが互いのメモを読み取るべきではない場合 | 
| `("account_facts", tenant_id)` | テナント全体のナレッジ | アカウント内のすべてのユーザーに適用されるファクト | 

すべてのメモリを 1 つの名前空間に配置してメタデータでフィルタリングすることは避けてください。フィルターは、DynamoDB が名前空間全体で最も近い一致を選択した後に適用され、1 回の検索で返される一致は最大 100 件です。そのため、ある利用量の多いテナントが一般的なクエリの上位 100 件を占めてしまうと、利用量の少ないテナントのメモリが存在しているにもかかわらず、そのテナントの検索では要求した件数より少ない結果しか返されなくなります。正確性を左右するものについては、フィルターよりも名前空間のスコープを優先してください。リコールの境界よりも細かくスコープを設定することも、正当な設計方法です。例えば、ユーザー固有のコンテキストとアカウント全体のコンテキストの両方を必要とするエージェントは、両方の名前空間を検索して結果を統合します。

## 考慮事項
<a name="langgraph-memory-considerations"></a>
+ インデックスは、グローバルセカンダリインデックスと同じモデルである、結果整合性を持ちます。`put()` の直後に実行した `search()` では、新しいメモリがまだ含まれていない場合があります。エージェントが同じターン内でメモリを書き込み、その後すぐにそのメモリをリコールする場合は、代わりにキーによって読み戻してください。
+ 1 回の検索で返される一致は最大 100 件です。`limit` と `offset` の合計がこの上限を超えるリクエストは、上限を超えた分について何も返さずに済ませるのではなく、明確なエラーを返して拒否されます。
+ 有効期限 (TTL) を使用して古いメモリを期限切れにし、読み取り時の更新を有効にすると、エージェントが実際にリコールするメモリの有効期限が延長されるため、アクティブに使用中のメモリがひっそりと消えることはありません。
+ ベクトル検索の失敗はエラーとして発生するため、アプリケーション側で対応できます。ストアが失敗時に空のリストを返す場合、スロットリング、権限の問題、そして本当に結果が空だったケースがすべて同じように見えてしまいます。
+ ベクトル操作は独自の単位で課金され、基本テーブルの読み取りおよび書き込みリクエストユニットとは別に計測されます。また、どちらもディメンション数に応じてスケールします。埋め込みのディメンション数が小さいほど、すべての書き込みとすべての検索でコストが低くなるため、リコールの品質を維持できる最小のディメンション数を使用してください。
+ 設定した `fields` にテキストがない状態で書き込まれたメモリは、埋め込みなしで保存され、セマンティック検索の結果には表示されません。このような場合、ストアは警告をログに記録します。

## その他のリソース
<a name="langgraph-memory-resources"></a>
+ [GitHub の DynamoDBStore ドキュメント](https://github.com/langchain-ai/langchain-aws/blob/main/libs/langgraph-checkpoint-aws/langgraph_checkpoint_aws/store/dynamodb/DynamoDBStore.md)
+ [PyPI の langgraph-checkpoint-aws](https://pypi.org/project/langgraph-checkpoint-aws/)
+ [LangGraph ドキュメント](https://langchain-ai.github.io/langgraph/)