4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【コンテキストエンジニアリングシリーズ】2)コーディングエージェントのメモリ設計 - 長期記憶システムの実装

Last updated at Posted at 2025-10-12

AIエージェントに「忘れない記憶」を実装する実践ガイド


📚 この記事の位置づけ

本記事は、コンテキストエンジニアリングシリーズの続編です。

前提知識として、以下の記事を先にお読みいただくことをおすすめします:

本記事では、外部化したコンテキストを効果的に管理・検索する長期記憶システムの実装を扱います。


目次

  1. エージェントの「記憶」問題
  2. 長期記憶システムとは何か
  3. メモリアーキテクチャの設計
  4. ベクトルデータベースの選定と実装
  5. セマンティック検索の実装
  6. 決定事項の索引化戦略
  7. 時系列での記憶管理
  8. 実装例: フルスタック記憶システム
  9. パフォーマンス最適化
  10. 運用とメンテナンス

エージェントの「記憶」問題

想像してみてください。あなたのプロジェクトには、何百もの技術的決定、何千行ものコード、そして数え切れない議論の履歴があります。しかし、AIエージェントに「3ヶ月前になぜこの設計にしたの?」と尋ねても、「すみません、わかりません」という答えしか返ってこない——これが現在の多くの開発現場で起きている現実です。

AIエージェントのコンテキストウィンドウは、人間の短期記憶のように一時的です。セッションが終われば、そこで交わされたすべての会話、決定の背景、失敗から学んだ教訓は跡形もなく消え去ります。200Kトークンという大容量のコンテキストウィンドウを持つ最新モデルでも、この「記憶の揮発性」という本質的な問題は解決できません。

以降では、なぜ単なるMarkdownファイルでは不十分なのか、そして真の「長期記憶」システムがどのようにこれらの課題を解決するのかを、具体的なシーンを交えて見ていきます。

コンテキストウィンドウの限界

現代のLLMが持つコンテキストウィンドウは、どれだけ大容量でも3つの根本的な制約から逃れられません。トークン数の上限、セッション終了時の消失、そしてコストの増大です。

具体的な問題シーン

実際の開発現場で起こる3つの典型的なシナリオを見てみましょう。これらは架空の話ではなく、コンテキストウィンドウの限界が引き起こす現実の問題です。

シーン1: 「3ヶ月前の決定」が失われる

// 3ヶ月前のあなた
あなた: "認証はJWT、有効期限15分で実装して"
エージェント: "実装しました!"
// → context.mdに記録せず

// 今日のあなた
あなた: "認証機能の仕様を教えて"
エージェント: "現在のコードを見ると...(推測で答える)"
あなた: "なんで15分って決めたんだっけ?"
エージェント: "すみません、わかりません" 😓

根本原因: 決定の背景や理由が記録されていない

シーン2: 「過去の失敗」を繰り返す

// 1ヶ月前
あなた: "非同期処理でこういう実装にして"
エージェント: "実装しました"
// → 後日、レースコンディションで障害発生
// → 解決策を見つけて修正
// → 教訓が記録されていない

// 今日
あなた: "別の機能で非同期処理を実装して"
エージェント: "こういう実装にします"
// → 同じパターンで実装
あなた: "またレースコンディション... 😱\

根本原因: 過去の失敗と解決策が検索可能な形で保存されていない

シーン3: 「関連する過去の議論」が見つからない

あなた: "決済処理のエラーハンドリングどうする?"

エージェント: "一般的には..."

あなた: "いや、2ヶ月前にStripe APIの制約で議論したはず"

# 実際には .ai/discussions/payment-errors.md に詳細な議論がある
# でもエージェントは見つけられない 🔍

根本原因: セマンティックな検索ができていない

なぜMarkdownファイルだけでは不十分なのか

多くのチームが.ai/ディレクトリにMarkdownファイルを置いてコンテキストを管理していますが、これだけでは記憶システムとして機能しません。検索性、関連性の把握、スケーラビリティの3つの壁が立ちはだかります。

具体例:

# Markdownでの検索の限界
$ grep -r "認証" .ai/
# → 「認証」という文字列を含むファイルが100個ヒット
# → どれが今の質問に関連するかわからない 🤷‍♂️

# 欲しいのは
「JWTのトークン有効期限を決定した際の議論」を検索
# → セマンティックな意味で関連する情報を取得

長期記憶システムとは何か

長期記憶システムは、AIエージェントに「忘れない記憶」を与える画期的なアーキテクチャです。

人間の脳が短期記憶と長期記憶を使い分けるように、AIエージェントにも2種類の記憶が必要です。コンテキストウィンドウは「今、目の前で起きていること」を処理する短期記憶。そして長期記憶システムは「過去に学んだすべてのこと」を永続的に保存し、必要な時に瞬時に呼び出すための基盤となります。

このシステムの革新性は、単なるデータの保存にとどまりません。セマンティック検索により、キーワードではなく「意味」で情報を検索できます。「JWTのトークン有効期限」というキーワードがなくても、「認証の設計でセキュリティとUXのバランスを考慮した議論」という意味的な関連性から、過去の決定事項を発見できるのです。

ここからは、人間の記憶メカニズムとの類似性、RAG(Retrieval-Augmented Generation)アーキテクチャの仕組み、そして実際のプロジェクトでどのように機能するかを詳しく掘り下げていきます。

人間の記憶モデルとの類似

長期記憶システムの設計は、人間の脳の記憶メカニズムから多くの示唆を得ています。感覚記憶→短期記憶→長期記憶という階層構造は、AIエージェントの記憶設計にも応用できます。

長期記憶システムの3つの機能

記憶システムの本質は、「記録する」「検索する」「想起する」という3つの機能の統合です。これらが有機的に連携することで、AIエージェントは過去の知識を効果的に活用できます。

1. 記録(Write): 情報を構造化して保存

// ✅ 構造化された記録
{
  id: "decision-001",
  type: "architectural_decision",
  title: "JWT認証の採用",
  content: "セッションベースではなくJWT認証を採用",
  context: {
    reason: "水平スケールのため",
    alternatives: ["session-based", "OAuth only"],
    date: "2024-10-12",
    participants: ["@alice", "@bob"]
  },
  tags: ["authentication", "jwt", "architecture"],
  related: ["decision-002", "issue-234"]
}

// ❌ 非構造化な記録(検索困難)
"今日、認証の方式について話した。JWTにすることにした。"

2. 検索(Search): 意味的に関連する情報を取得

// セマンティック検索
query: "トークンの有効期限を変更したい理由"

// → ベクトル空間で類似度計算
// → 関連する決定・議論・実装を返す

results: [
  {
    type: "decision",
    title: "JWT有効期限を15分に変更",
    similarity: 0.92,  // 高い関連性
    snippet: "セキュリティ強化のため..."
  },
  {
    type: "discussion",
    title: "リフレッシュトークンの運用",
    similarity: 0.85,
    snippet: "有効期限とUXのトレードオフ..."
  }
]

3. 想起(Recall): 適切なタイミングで関連情報を提供

// ユーザーの質問から自動的に関連コンテキストを検索
ユーザー: "決済処理のエラーハンドリングを実装したい"

システム:
1. 質問をベクトル化
2. 関連する記憶を検索
   - 過去の決済エラー議論
   - Stripe APIの制約
   - 既存のエラーハンドリングパターン
3. エージェントのコンテキストに自動追加
4. エージェントが適切な回答を生成

RAG(Retrieval-Augmented Generation)アーキテクチャ

RAGは、記憶システムとAIエージェントを結びつける重要なパターンです。ユーザーの質問に対して、まず関連する記憶を検索(Retrieval)し、それをコンテキストに加えて回答を生成(Generation)します。


メモリアーキテクチャの設計

優れた長期記憶システムは、適切に設計されたアーキテクチャの上に成り立ちます。ここでは、スケーラブルで保守性の高い記憶システムを構築するための設計原則を解説します。

記憶システムの設計で最も重要なのは、分離された責務を持つレイヤー構造です。プレゼンテーション層(CLI/UI)、アプリケーション層(検索・索引化ロジック)、ストレージ層(データ永続化)の3層に分けることで、それぞれを独立して進化させることができます。また、将来的にベクトルDBを変更したり、UIをWebベースに移行する際も、他の層に影響を与えません。

さらに、記憶の構造化されたデータモデルが重要です。単なるテキストの羅列ではなく、ID、タイプ、メタデータ、関連性、ベクトル表現など、検索と管理に最適化された構造で情報を保存します。これにより、「認証に関する決定」「過去3ヶ月の重要な議論」「特定のコードパターンに関連する教訓」といった多様な検索が可能になります。

実装の詳細に入る前に、まずはシステム全体の青写真を描いていきましょう。

レイヤー構造

記憶システムは、プレゼンテーション層、アプリケーション層、ストレージ層の3層に分けて設計します。この分離により、各層を独立して変更・拡張でき、長期的な保守性が確保されます。

データモデル

記憶の構造を定義するデータモデルは、システム全体の基盤となります。TypeScriptのインターフェースで型安全性を確保しつつ、将来の拡張にも対応できる柔軟な設計を目指します。

// メモリエントリの基本構造
interface MemoryEntry {
  // 識別情報
  id: string;                    // UUID
  type: MemoryType;              // 記憶の種類
  
  // コンテンツ
  title: string;
  content: string;
  summary?: string;              // 自動生成された要約
  
  // メタデータ
  metadata: {
    created_at: Date;
    updated_at: Date;
    created_by: string;
    tags: string[];
    category: string;
    importance: number;          // 1-10
  };
  
  // 関連性
  relations: {
    parent?: string;             // 親記憶
    children?: string[];         // 子記憶
    related: string[];           // 関連記憶
    supersedes?: string;         // 上書きする記憶
  };
  
  // ベクトル(検索用)
  embedding?: number[];          // 768次元ベクトル
  
  // コンテキスト
  context: {
    project: string;
    domain?: string;
    file_path?: string;
    code_snippet?: string;
  };
}

// 記憶の種類
enum MemoryType {
  DECISION = 'decision',              // 技術的決定
  DISCUSSION = 'discussion',          // 議論・会話
  PATTERN = 'pattern',                // コードパターン
  LESSON = 'lesson',                  // 学習・教訓
  SPECIFICATION = 'specification',    // 仕様
  TROUBLESHOOTING = 'troubleshooting' // 問題解決
}

ディレクトリ構造

記憶システムのファイル配置は、保守性と拡張性を考慮して設計します。.ai/memory/以下に、インデックス、エントリ、スクリプトを整理して配置します。

project-root/
├── .ai/
│   ├── memory/
│   │   ├── config.json           # 記憶システム設定
│   │   ├── index/                # インデックスファイル
│   │   │   ├── vector.index      # ベクトルインデックス
│   │   │   ├── metadata.db       # メタデータDB(SQLite)
│   │   │   └── embeddings/       # エンベディングキャッシュ
│   │   ├── entries/              # 記憶エントリ(JSON)
│   │   │   ├── decisions/
│   │   │   ├── discussions/
│   │   │   ├── patterns/
│   │   │   ├── lessons/
│   │   │   └── specs/
│   │   └── archive/              # アーカイブ
│   │
│   ├── scripts/
│   │   ├── memory-index.ts       # インデックス作成
│   │   ├── memory-search.ts      # 検索CLI
│   │   └── memory-analyze.ts     # 分析ツール
│   │
│   └── team/                     # 既存のコンテキスト
│       └── ...
│
└── package.json

ベクトルデータベースの選定と実装

長期記憶システムの心臓部となるのが、ベクトルデータベースです。これは従来のRDBMSやNoSQLとは全く異なる、ベクトル空間での類似度計算に特化したデータベースです。

なぜベクトルDBが必要なのか?答えは「意味の数値化」にあります。テキストを768次元や1536次元のベクトル(数値の配列)に変換することで、2つの文章がどれだけ似ているかを数学的に計算できます。これにより、「JWT認証の有効期限」という検索クエリから、たとえ「トークン」「期限」という単語が含まれていなくても、セマンティックに関連する「アクセストークンのライフタイム設計」という記憶を発見できるのです。

市場には多様なベクトルDBが存在しますが、選択を誤ると後々の移行コストが膨大になります。Chroma、Qdrant、Pinecone、Milvus、FAISS——それぞれに強みがあり、プロジェクトの規模、チーム構成、プライバシー要件によって最適解は変わります。

まずは主要なベクトルDBの比較から始め、個人・小規模チームに最適なChromaを使った具体的な実装手順、既存のMarkdownファイルをインデックス化するスクリプトまで、すぐに使える実装例を順を追って見ていきましょう。

ベクトルDBの選択肢

市場には様々なベクトルDBが存在し、それぞれ異なる特性を持ちます。プロジェクトの規模、チーム構成、インフラ要件に応じて最適な選択が必要です。

DB 特徴 ローカル スケール コスト
Chroma シンプル、Python/TS対応 無料
Qdrant 高速、Rust製 無料
Weaviate 機能豊富、GraphQL 無料
Pinecone マネージド、簡単 有料
Milvus エンタープライズ向け 無料
FAISS Meta製、超高速 無料

推奨: 個人・小規模チーム向け

個人開発や小規模チームには、セットアップが簡単でローカル完結できるChromaを推奨します。プライバシーを守りながら、本番レベルの機能を無料で利用できます。

選択: Chroma

理由:

  • ✅ ローカルで完結(プライバシー保護)
  • ✅ TypeScriptのSDKが充実
  • ✅ セットアップが簡単
  • ✅ SQLiteベースで軽量
  • ✅ メタデータフィルタリングが強力

Chromaのセットアップ

Chromaは、npmパッケージとしてインストールするか、Dockerコンテナで起動できます。開発環境ではDockerを推奨します。永続化やポート設定も簡単です。

# インストール
npm install chromadb

# または Docker で起動
docker pull chromadb/chroma
docker run -p 8000:8000 chromadb/chroma

初期化スクリプト

記憶システムの初期化スクリプトは、コレクションの作成、エンベディング関数の設定、既存ファイルのインデックス化を自動化します。一度書けば、いつでも再実行可能です。

// .ai/scripts/memory-index.ts

import { ChromaClient, OpenAIEmbeddingFunction } from 'chromadb';
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'glob';

// Chroma クライアント初期化
const client = new ChromaClient({
  path: 'http://localhost:8000', // ローカルインスタンス
});

// エンベディング関数(OpenAI)
const embedder = new OpenAIEmbeddingFunction({
  openai_api_key: process.env.OPENAI_API_KEY!,
  model_name: 'text-embedding-3-small', // 安価で高性能
});

// コレクション作成
async function createCollection() {
  const collection = await client.getOrCreateCollection({
    name: 'project_memory',
    embeddingFunction: embedder,
    metadata: {
      description: 'プロジェクトの長期記憶',
      created_at: new Date().toISOString(),
    },
  });
  
  return collection;
}

// メモリエントリをインデックスに追加
async function indexMemory(entry: MemoryEntry) {
  const collection = await createCollection();
  
  // ドキュメントとして追加
  await collection.add({
    ids: [entry.id],
    documents: [entry.content],
    metadatas: [{
      type: entry.type,
      title: entry.title,
      created_at: entry.metadata.created_at.toISOString(),
      tags: entry.metadata.tags.join(','),
      importance: entry.metadata.importance,
      category: entry.metadata.category,
    }],
  });
  
  console.log(`✅ Indexed: ${entry.title}`);
}

// 既存のMarkdownファイルをインデックス化
async function indexExistingFiles() {
  const files = await glob('.ai/team/**/*.md');
  
  for (const file of files) {
    const content = await fs.readFile(file, 'utf-8');
    const title = path.basename(file, '.md');
    
    const entry: MemoryEntry = {
      id: generateId(file),
      type: MemoryType.SPECIFICATION,
      title,
      content,
      metadata: {
        created_at: new Date(),
        updated_at: new Date(),
        created_by: 'system',
        tags: extractTags(content),
        category: extractCategory(file),
        importance: 8,
      },
      relations: {
        related: [],
      },
      context: {
        project: 'main',
        file_path: file,
      },
    };
    
    await indexMemory(entry);
  }
  
  console.log(`\
✅ Indexed ${files.length} files`);
}

// ユーティリティ関数
function generateId(filePath: string): string {
  return `memory-${filePath.replace(/[^a-z0-9]/gi, '-')}`;
}

function extractTags(content: string): string[] {
  // Markdownのヘッダーからタグを抽出
  const headers = content.match(/^#{1,3} (.+)$/gm) || [];
  return headers
    .map(h => h.replace(/^#+\\s+/, '').toLowerCase())
    .slice(0, 5);
}

function extractCategory(filePath: string): string {
  const parts = filePath.split('/');
  return parts[parts.length - 2] || 'general';
}

// 実行
if (require.main === module) {
  indexExistingFiles()
    .then(() => console.log('✅ Indexing complete'))
    .catch(err => console.error('❌ Error:', err));
}

package.json にスクリプト追加

{
  "scripts": {
    "memory:index": "tsx .ai/scripts/memory-index.ts",
    "memory:search": "tsx .ai/scripts/memory-search.ts",
    "memory:add": "tsx .ai/scripts/memory-add.ts"
  }
}

セマンティック検索の実装

セマンティック検索は、長期記憶システムの最も強力な機能です。従来のgrepや全文検索では、「JWT」というキーワードでしか「JWT」を含む文書を見つけられませんでした。しかしセマンティック検索では、「トークンベース認証のセキュリティ設計」という問い合わせから、「JWT」という単語が一切含まれていない「アクセス制御の実装パターン」という文書も発見できます。

この魔法のような検索がどのように機能するのか?秘密はベクトル空間での類似度計算にあります。検索クエリと記憶された文書がそれぞれベクトルに変換され、多次元空間上での「距離」が計算されます。距離が近いほど、意味的に関連性が高いと判断されるのです。

しかし、単純なベクトル検索だけでは不十分です。実際のプロジェクトでは、「認証に関する決定事項」「過去3ヶ月の重要度8以上の記録」「エラーハンドリングのパターン」といった複雑な条件での検索が必要になります。そのため、セマンティック検索とメタデータフィルタリングを組み合わせたハイブリッド検索が威力を発揮します。

基本的な検索の実装から、実践的なCLIツール、そして高度な検索テクニックまで、段階的に実装していきましょう。

基本的な検索フロー

セマンティック検索の流れは、クエリのベクトル化、類似度計算、結果の整形という3つのステップで構成されます。このフローを理解することが、効果的な検索実装の第一歩です。

検索スクリプトの実装

実用的な検索スクリプトには、メタデータフィルタリング、結果の整形、CLIインターフェースが必要です。以下の実装は、コマンドラインから即座に使える完全な検索ツールです。

// .ai/scripts/memory-search.ts

import { ChromaClient, OpenAIEmbeddingFunction } from 'chromadb';
import chalk from 'chalk';

const client = new ChromaClient({ path: 'http://localhost:8000' });
const embedder = new OpenAIEmbeddingFunction({
  openai_api_key: process.env.OPENAI_API_KEY!,
  model_name: 'text-embedding-3-small',
});

interface SearchOptions {
  query: string;
  limit?: number;           // 結果数(デフォルト: 5)
  type?: string;            // 記憶タイプでフィルタ
  tags?: string[];          // タグでフィルタ
  minImportance?: number;   // 重要度でフィルタ
  dateFrom?: Date;          // 日付範囲
  dateTo?: Date;
}

async function searchMemory(options: SearchOptions) {
  const collection = await client.getCollection({
    name: 'project_memory',
    embeddingFunction: embedder,
  });
  
  // メタデータフィルタの構築
  const where: any = {};
  
  if (options.type) {
    where.type = options.type;
  }
  
  if (options.minImportance) {
    where.importance = { $gte: options.minImportance };
  }
  
  if (options.dateFrom || options.dateTo) {
    where.created_at = {};
    if (options.dateFrom) {
      where.created_at.$gte = options.dateFrom.toISOString();
    }
    if (options.dateTo) {
      where.created_at.$lte = options.dateTo.toISOString();
    }
  }
  
  // セマンティック検索実行
  const results = await collection.query({
    queryTexts: [options.query],
    nResults: options.limit || 5,
    where: Object.keys(where).length > 0 ? where : undefined,
  });
  
  return formatResults(results);
}

function formatResults(results: any) {
  const formatted = [];
  
  for (let i = 0; i < results.ids[0].length; i++) {
    formatted.push({
      id: results.ids[0][i],
      document: results.documents[0][i],
      metadata: results.metadatas[0][i],
      distance: results.distances[0][i],
      similarity: 1 - results.distances[0][i], // 類似度(0-1)
    });
  }
  
  return formatted;
}

// CLI表示
async function searchCLI(query: string, options: Partial<SearchOptions> = {}) {
  console.log(chalk.blue('\
🔍 Searching memories...\
'));
  
  const results = await searchMemory({ query, ...options });
  
  if (results.length === 0) {
    console.log(chalk.yellow('No results found.'));
    return;
  }
  
  results.forEach((result, index) => {
    const similarity = (result.similarity * 100).toFixed(1);
    const importanceStars = ''.repeat(Math.ceil(result.metadata.importance / 2));
    
    console.log(chalk.bold(`\
${index + 1}. ${result.metadata.title}`));
    console.log(chalk.gray(`   ID: ${result.id}`));
    console.log(chalk.gray(`   Type: ${result.metadata.type}`));
    console.log(chalk.gray(`   Similarity: ${similarity}%`));
    console.log(chalk.gray(`   Importance: ${importanceStars}`));
    console.log(chalk.gray(`   Created: ${new Date(result.metadata.created_at).toLocaleDateString()}`));
    
    if (result.metadata.tags) {
      const tags = result.metadata.tags.split(',').map((t: string) => chalk.cyan(`#${t}`)).join(' ');
      console.log(chalk.gray(`   Tags: ${tags}`));
    }
    
    // スニペット表示(最初の200文字)
    const snippet = result.document.substring(0, 200).replace(//g, ' ');
    console.log(`   ${snippet}...`);
    console.log(chalk.blue(`   → .ai/memory/entries/${result.metadata.type}s/${result.id}.json`));
  });
  
  console.log('');
}

// コマンドライン引数の処理
if (require.main === module) {
  const args = process.argv.slice(2);
  
  if (args.length === 0) {
    console.log('Usage: npm run memory:search \"your query\"');
    console.log('Options:');
    console.log('  --type=decision        Filter by type');
    console.log('  --tags=auth,jwt        Filter by tags');
    console.log('  --importance=8         Minimum importance');
    console.log('  --limit=10             Number of results');
    process.exit(1);
  }
  
  const query = args[0];
  const options: Partial<SearchOptions> = {};
  
  // オプション解析
  args.slice(1).forEach(arg => {
    if (arg.startsWith('--type=')) {
      options.type = arg.split('=')[1];
    } else if (arg.startsWith('--limit=')) {
      options.limit = parseInt(arg.split('=')[1]);
    } else if (arg.startsWith('--importance=')) {
      options.minImportance = parseInt(arg.split('=')[1]);
    } else if (arg.startsWith('--tags=')) {
      options.tags = arg.split('=')[1].split(',');
    }
  });
  
  searchCLI(query, options)
    .then(() => process.exit(0))
    .catch(err => {
      console.error(chalk.red('Error:'), err);
      process.exit(1);
    });
}

export { searchMemory, searchCLI };

使用例

# 基本的な検索
npm run memory:search "JWT認証の有効期限"

# 出力:
# 🔍 Searching memories...
# 
# 1. JWT認証の採用
#    ID: memory-decision-001
#    Type: decision
#    Similarity: 94.2%
#    Importance: ⭐⭐⭐⭐⭐
#    Created: 2024-10-12
#    Tags: #authentication #jwt #architecture
#    
#    セッションベースではなくJWT認証を採用。アクセストークン15分、
#    リフレッシュトークン7日。理由: 水平スケールのため...
#    → .ai/memory/entries/decisions/memory-decision-001.json

# タイプでフィルタ
npm run memory:search "エラー処理" --type=pattern

# 重要度でフィルタ
npm run memory:search "セキュリティ" --importance=8

# 複数条件
npm run memory:search "データベース" --type=decision --importance=7 --limit=10

高度な検索機能

基本的なセマンティック検索に加え、ハイブリッド検索、関連記憶の探索、時系列検索、クラスター分析といった高度な機能により、記憶システムの価値が飛躍的に向上します。

// .ai/scripts/memory-search-advanced.ts

// 1. ハイブリッド検索(セマンティック + キーワード)
async function hybridSearch(query: string) {
  // セマンティック検索
  const semanticResults = await searchMemory({ query, limit: 10 });
  
  // キーワード検索(メタデータ)
  const collection = await client.getCollection({
    name: 'project_memory',
    embeddingFunction: embedder,
  });
  
  const keywordResults = await collection.query({
    queryTexts: [query],
    nResults: 10,
    where: {
      $or: [
        { title: { $contains: query } },
        { tags: { $contains: query } }
      ]
    }
  });
  
  // スコアで統合
  return mergeResults(semanticResults, keywordResults);
}

// 2. 関連記憶の探索
async function findRelated(memoryId: string, depth: number = 2) {
  const visited = new Set<string>();
  const results: MemoryEntry[] = [];
  
  async function traverse(id: string, currentDepth: number) {
    if (visited.has(id) || currentDepth > depth) return;
    visited.add(id);
    
    const entry = await loadMemory(id);
    results.push(entry);
    
    // 関連記憶を再帰的に探索
    for (const relatedId of entry.relations.related) {
      await traverse(relatedId, currentDepth + 1);
    }
  }
  
  await traverse(memoryId, 0);
  return results;
}

// 3. 時系列での検索
async function searchByTimeline(
  query: string,
  startDate: Date,
  endDate: Date,
  granularity: 'day' | 'week' | 'month' = 'week'
) {
  const results = await searchMemory({
    query,
    dateFrom: startDate,
    dateTo: endDate,
  });
  
  // 時系列でグループ化
  return groupByTime(results, granularity);
}

// 4. クラスター分析
async function clusterMemories(queries: string[]) {
  const allResults = await Promise.all(
    queries.map(q => searchMemory({ query: q, limit: 20 }))
  );
  
  // 類似度行列を作成してクラスタリング
  return performClustering(allResults);
}

決定事項の索引化戦略

プロジェクトで最も価値の高い情報は何でしょうか?それは間違いなくなぜその決定をしたのかという記録です。

「なぜReactではなくVueを選んだのか」「なぜマイクロサービスではなくモノリスにしたのか」「なぜPostgreSQLではなくMongoDBを採用したのか」——これらの決定の背景にある理由、検討した代替案、予想される影響を記録することで、プロジェクトは「なぜ」に答えられる組織になります。

Architecture Decision Record(ADR)は、こうした技術的決定を構造化して記録するための標準的なフォーマットです。しかし、多くのプロジェクトでADRが形骸化するのは、記録するプロセスが煩雑だからです。手動でファイル名を決め、テンプレートをコピーし、番号を採番し、リンクを更新する——これらの作業が障壁となり、結局誰も記録しなくなります。

そこで、ADRの作成・索引化を完全に自動化する実装を紹介します。インタラクティブなCLIで質問に答えるだけで、自動採番され、メタデータが付与され、ベクトルDBにインデックスされたADRが生成されます。さらに、重要度の自動計算、関連記録の自動リンク、検索可能な形での保存まで、実用的な実装パターンを見ていきます。

ADR(Architecture Decision Record)の自動インデックス化

ADRの作成から索引化までを完全自動化することで、記録のハードルを劇的に下げます。インタラクティブなプロンプトに答えるだけで、構造化されたADRが生成されます。

// .ai/scripts/memory-add.ts

import { v4 as uuidv4 } from 'uuid';
import fs from 'fs/promises';
import path from 'path';
import { indexMemory } from './memory-index';

interface ADRTemplate {
  title: string;
  status: 'proposed' | 'accepted' | 'rejected' | 'deprecated' | 'superseded';
  context: string;
  decision: string;
  consequences: string;
  alternatives?: string;
  references?: string[];
}

async function addDecision(adr: ADRTemplate) {
  // ADR番号を自動採番
  const adrNumber = await getNextADRNumber();
  const id = `adr-${String(adrNumber).padStart(3, '0')}`;
  
  // MemoryEntryを作成
  const entry: MemoryEntry = {
    id,
    type: MemoryType.DECISION,
    title: `ADR-${adrNumber}: ${adr.title}`,
    content: formatADR(adr),
    summary: generateSummary(adr),
    metadata: {
      created_at: new Date(),
      updated_at: new Date(),
      created_by: process.env.USER || 'unknown',
      tags: extractDecisionTags(adr),
      category: 'architecture',
      importance: calculateImportance(adr),
    },
    relations: {
      related: adr.references || [],
    },
    context: {
      project: 'main',
      file_path: `.ai/team/adr/${id}.md`,
    },
  };
  
  // Markdownファイルを作成
  await saveADRFile(entry);
  
  // ベクトルDBにインデックス
  await indexMemory(entry);
  
  // JSONとしても保存(詳細メタデータ)
  await saveMemoryJSON(entry);
  
  console.log(`✅ Created ${id}: ${adr.title}`);
  return entry;
}

function formatADR(adr: ADRTemplate): string {
  return `# ${adr.title}

## ステータス
${adr.status}

## コンテキスト
${adr.context}

## 決定内容
${adr.decision}

## 結果
${adr.consequences}

${adr.alternatives ? `## 検討した代替案${adr.alternatives}` : ''}
${adr.references ? `## 参考資料${adr.references.map(r => `- ${r}`).join('')}` : ''}`;
}

function generateSummary(adr: ADRTemplate): string {
  // LLMを使って要約を生成(オプション)
  return `${adr.title}. ${adr.decision.substring(0, 100)}...`;
}

function extractDecisionTags(adr: ADRTemplate): string[] {
  const tags = new Set<string>();
  
  // タイトルからキーワード抽出
  const keywords = adr.title
    .toLowerCase()
    .split(/\\s+/)
    .filter(w => w.length > 3);
  
  keywords.forEach(k => tags.add(k));
  
  // ステータスもタグに
  tags.add(adr.status);
  
  return Array.from(tags);
}

function calculateImportance(adr: ADRTemplate): number {
  // ステータスベースでスコアリング
  const statusScore = {
    accepted: 10,
    proposed: 7,
    deprecated: 5,
    rejected: 3,
    superseded: 4,
  };
  
  let score = statusScore[adr.status];
  
  // 長さベースで調整(詳細な議論ほど重要)
  if (adr.context.length > 500) score += 1;
  if (adr.alternatives) score += 1;
  
  return Math.min(score, 10);
}

async function getNextADRNumber(): Promise<number> {
  const adrDir = '.ai/team/adr';
  const files = await fs.readdir(adrDir);
  
  const numbers = files
    .filter(f => f.match(/^\\d{3}-/))
    .map(f => parseInt(f.substring(0, 3)));
  
  return numbers.length > 0 ? Math.max(...numbers) + 1 : 1;
}

async function saveADRFile(entry: MemoryEntry): Promise<void> {
  await fs.writeFile(
    entry.context.file_path!,
    entry.content,
    'utf-8'
  );
}

async function saveMemoryJSON(entry: MemoryEntry): Promise<void> {
  const dir = `.ai/memory/entries/${entry.type}s`;
  await fs.mkdir(dir, { recursive: true });
  
  const jsonPath = path.join(dir, `${entry.id}.json`);
  await fs.writeFile(
    jsonPath,
    JSON.stringify(entry, null, 2),
    'utf-8'
  );
}

// CLI
if (require.main === module) {
  // インタラクティブモード
  import('inquirer').then(async ({ default: inquirer }) => {
    const answers = await inquirer.prompt([
      {
        type: 'input',
        name: 'title',
        message: 'ADRのタイトル:',
      },
      {
        type: 'list',
        name: 'status',
        message: 'ステータス:',
        choices: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'],
        default: 'proposed',
      },
      {
        type: 'editor',
        name: 'context',
        message: 'コンテキスト(背景・課題):',
      },
      {
        type: 'editor',
        name: 'decision',
        message: '決定内容:',
      },
      {
        type: 'editor',
        name: 'consequences',
        message: '結果・影響:',
      },
      {
        type: 'confirm',
        name: 'hasAlternatives',
        message: '代替案を記録しますか?',
        default: false,
      },
    ]);
    
    if (answers.hasAlternatives) {
      const altAnswer = await inquirer.prompt([
        {
          type: 'editor',
          name: 'alternatives',
          message: '検討した代替案:',
        },
      ]);
      answers.alternatives = altAnswer.alternatives;
    }
    
    await addDecision(answers);
  });
}

使用例

# インタラクティブにADRを作成
npm run memory:add

# または引数で指定
npm run memory:add -- \
  --title="Redis導入" \
  --status="accepted" \
  --context="セッション管理とキャッシュが必要" \
  --decision="Redis 7.xを導入する"

時系列での記憶管理

プロジェクトは時間とともに進化します。3ヶ月前の決定が今日の実装に影響し、今日の議論が3ヶ月後の設計を決定づけます。この時間軸での文脈を理解することで、「なぜこうなったのか」を本質的に把握できるようになります。

記憶を時系列で管理する価値は、単なる履歴管理にとどまりません。例えば、「認証システムの設計がどのように進化したか」を時系列で追うことで、初期のシンプルな実装から、セキュリティ要件の追加、パフォーマンス最適化、マイクロサービス化といった変遷の物語が見えてきます。これは、新しいメンバーのオンボーディングや、リファクタリングの判断、アーキテクチャの見直しで極めて重要な情報となります。

さらに、月次サマリーや統計情報により、チームの活動パターンを可視化できます。「決定事項が集中している時期」は重要な設計フェーズを示し、「トラブルシューティングが多い時期」は技術的負債の蓄積を示唆します。

タイムラインビューの実装、月次ダイジェストの生成、統計分析の方法まで、時系列での記憶管理を実現する具体的な実装を順に見ていきましょう。

タイムラインビュー

時系列で記憶を可視化することで、プロジェクトの進化の過程を追えます。日付でグループ化し、記憶のタイプや重要度とともに表示することで、歴史を一目で把握できます。

// .ai/scripts/memory-timeline.ts

interface TimelineEntry {
  date: Date;
  entries: MemoryEntry[];
}

async function getTimeline(
  startDate: Date,
  endDate: Date,
  filters?: {
    type?: MemoryType;
    minImportance?: number;
  }
): Promise<TimelineEntry[]> {
  const collection = await client.getCollection({
    name: 'project_memory',
    embeddingFunction: embedder,
  });
  
  // 日付範囲で検索
  const where: any = {
    created_at: {
      $gte: startDate.toISOString(),
      $lte: endDate.toISOString(),
    },
  };
  
  if (filters?.type) {
    where.type = filters.type;
  }
  
  if (filters?.minImportance) {
    where.importance = { $gte: filters.minImportance };
  }
  
  const results = await collection.get({
    where,
    limit: 1000,
  });
  
  // 日付でグループ化
  return groupByDate(results);
}

function groupByDate(results: any): TimelineEntry[] {
  const groups = new Map<string, MemoryEntry[]>();
  
  results.ids.forEach((id: string, index: number) => {
    const date = new Date(results.metadatas[index].created_at);
    const dateKey = date.toISOString().split('T')[0];
    
    if (!groups.has(dateKey)) {
      groups.set(dateKey, []);
    }
    
    groups.get(dateKey)!.push({
      id,
      type: results.metadatas[index].type,
      title: results.metadatas[index].title,
      content: results.documents[index],
      metadata: results.metadatas[index],
      relations: { related: [] },
      context: { project: 'main' },
    });
  });
  
  // 日付順にソート
  return Array.from(groups.entries())
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([date, entries]) => ({
      date: new Date(date),
      entries,
    }));
}

// タイムライン表示
async function displayTimeline(
  startDate: Date,
  endDate: Date,
  filters?: any
) {
  const timeline = await getTimeline(startDate, endDate, filters);
  
  console.log(chalk.blue('📅 Timeline View'));
  console.log(chalk.gray(`Period: ${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}`));
  
  timeline.forEach(({ date, entries }) => {
    console.log(chalk.bold(`${date.toLocaleDateString()} (${entries.length} entries)`));
    console.log(chalk.gray(''.repeat(60)));
    
    entries.forEach(entry => {
      const icon = getTypeIcon(entry.type);
      const importance = ''.repeat(Math.ceil(entry.metadata.importance / 2));
      
      console.log(`  ${icon} ${entry.title} ${importance}`);
      console.log(chalk.gray(`     ${entry.type}${entry.metadata.tags?.split(',').slice(0, 3).join(', ')}`));
    });
  });
  
  console.log('');
}

function getTypeIcon(type: string): string {
  const icons: Record<string, string> = {
    decision: '📋',
    discussion: '💬',
    pattern: '🔧',
    lesson: '💡',
    specification: '📄',
    troubleshooting: '🔍',
  };
  return icons[type] || '📝';
}

// 月次サマリー
async function monthlyDigest(year: number, month: number) {
  const startDate = new Date(year, month - 1, 1);
  const endDate = new Date(year, month, 0);
  
  const timeline = await getTimeline(startDate, endDate);
  
  // 統計情報
  const stats = {
    totalEntries: 0,
    byType: {} as Record<string, number>,
    byImportance: {} as Record<number, number>,
    topTags: {} as Record<string, number>,
  };
  
  timeline.forEach(({ entries }) => {
    entries.forEach(entry => {
      stats.totalEntries++;
      
      // タイプ別カウント
      stats.byType[entry.type] = (stats.byType[entry.type] || 0) + 1;
      
      // 重要度別カウント
      const imp = Math.ceil(entry.metadata.importance / 2);
      stats.byImportance[imp] = (stats.byImportance[imp] || 0) + 1;
      
      // タグカウント
      entry.metadata.tags?.split(',').forEach((tag: string) => {
        stats.topTags[tag] = (stats.topTags[tag] || 0) + 1;
      });
    });
  });
  
  console.log(chalk.blue(`📊 Monthly Digest: ${year}${month}月`));
  
  console.log(chalk.bold('📈 統計'));
  console.log(`  総記憶数: ${stats.totalEntries}`);
  console.log(`  記録日数: ${timeline.length}`);
  console.log();
  
  console.log(chalk.bold('📋 タイプ別'));
  Object.entries(stats.byType)
    .sort(([, a], [, b]) => b - a)
    .forEach(([type, count]) => {
      const icon = getTypeIcon(type);
      console.log(`  ${icon} ${type}: ${count}`);
    });
  console.log();
  
  console.log(chalk.bold('🏷️  人気タグ TOP10'));
  Object.entries(stats.topTags)
    .sort(([, a], [, b]) => b - a)
    .slice(0, 10)
    .forEach(([tag, count]) => {
      console.log(`  #${tag}: ${count}`);
    });
  console.log();
}

使用例

# 今月のタイムライン
npm run memory:timeline -- --month=current

# 特定期間
npm run memory:timeline -- --from=2024-10-01 --to=2024-10-31

# 決定事項のみ
npm run memory:timeline -- --type=decision --month=current

# 月次サマリー
npm run memory:digest -- --year=2024 --month=10

実装例: フルスタック記憶システム

ここまで、長期記憶システムの各コンポーネント——ベクトルDB、セマンティック検索、ADR索引化、時系列管理——を個別に解説してきました。しかし、これらを統合し、実際のプロジェクトで使える本番レベルのシステムを構築するには、さらなる設計が必要です。

本セクションで紹介するMemorySystemクラスは、すべての機能を統合したオールインワンソリューションです。このクラスは:

  • CRUD操作: 記憶の追加、検索、更新、削除の基本機能
  • キャッシング: 頻繁にアクセスされる記憶をメモリにキャッシュしてパフォーマンス向上
  • イベント駆動: 記憶の追加や更新時にイベントを発火し、外部システムと連携
  • トランザクション: 複数の記憶を一括で更新する際の整合性保証
  • インポート/エクスポート: 既存のMarkdownファイルからの移行や、バックアップ機能
  • 分析機能: 記憶のクラスター分析、関連性グラフの生成

実際のプロジェクトでは、これをベースに独自の要件を追加していくことになります。以下では、拡張性と保守性を考慮した実装パターンを具体的なコードとともに示していきます。

システム全体構成

MemorySystemクラスは、すべての機能を統合した中核クラスです。イベント駆動、キャッシング、トランザクション管理を実装し、本番環境での使用に耐える堅牢性を持ちます。

// .ai/scripts/memory-system.ts

import { ChromaClient } from 'chromadb';
import { EventEmitter } from 'events';

class MemorySystem extends EventEmitter {
  private client: ChromaClient;
  private collection: any;
  private cache: Map<string, MemoryEntry>;
  
  constructor(config: MemoryConfig) {
    super();
    this.client = new ChromaClient({ path: config.chromaUrl });
    this.cache = new Map();
  }
  
  async initialize() {
    this.collection = await this.client.getOrCreateCollection({
      name: 'project_memory',
      embeddingFunction: embedder,
    });
    
    this.emit('initialized');
  }
  
  // 記憶を追加
  async remember(entry: Partial<MemoryEntry>): Promise<MemoryEntry> {
    const fullEntry: MemoryEntry = {
      id: entry.id || uuidv4(),
      type: entry.type!,
      title: entry.title!,
      content: entry.content!,
      summary: entry.summary || await this.generateSummary(entry.content!),
      metadata: {
        created_at: new Date(),
        updated_at: new Date(),
        created_by: process.env.USER || 'unknown',
        tags: entry.metadata?.tags || [],
        category: entry.metadata?.category || 'general',
        importance: entry.metadata?.importance || 5,
      },
      relations: entry.relations || { related: [] },
      context: entry.context || { project: 'main' },
    };
    
    // ベクトルDBに保存
    await this.collection.add({
      ids: [fullEntry.id],
      documents: [fullEntry.content],
      metadatas: [this.serializeMetadata(fullEntry.metadata)],
    });
    
    // ファイルシステムに保存
    await this.saveToFile(fullEntry);
    
    // キャッシュ
    this.cache.set(fullEntry.id, fullEntry);
    
    this.emit('remembered', fullEntry);
    return fullEntry;
  }
  
  // 記憶を検索
  async recall(query: string, options: SearchOptions = {}): Promise<MemoryEntry[]> {
    const results = await this.collection.query({
      queryTexts: [query],
      nResults: options.limit || 5,
      where: this.buildWhereClause(options),
    });
    
    return this.hydrateResults(results);
  }
  
  // 記憶を更新
  async update(id: string, updates: Partial<MemoryEntry>): Promise<MemoryEntry> {
    const existing = await this.get(id);
    
    const updated: MemoryEntry = {
      ...existing,
      ...updates,
      metadata: {
        ...existing.metadata,
        ...updates.metadata,
        updated_at: new Date(),
      },
    };
    
    // ベクトルDBを更新
    await this.collection.update({
      ids: [id],
      documents: [updated.content],
      metadatas: [this.serializeMetadata(updated.metadata)],
    });
    
    // ファイルシステムを更新
    await this.saveToFile(updated);
    
    // キャッシュを更新
    this.cache.set(id, updated);
    
    this.emit('updated', updated);
    return updated;
  }
  
  // 記憶を削除
  async forget(id: string): Promise<void> {
    await this.collection.delete({ ids: [id] });
    await this.deleteFile(id);
    this.cache.delete(id);
    this.emit('forgotten', id);
  }
  
  // 関連する記憶を取得
  async getRelated(id: string, depth: number = 1): Promise<MemoryEntry[]> {
    const entry = await this.get(id);
    const related: MemoryEntry[] = [];
    
    // 直接の関連
    for (const relatedId of entry.relations.related) {
      const relatedEntry = await this.get(relatedId);
      related.push(relatedEntry);
    }
    
    // セマンティック検索で類似記憶も取得
    const semanticResults = await this.recall(entry.title, { limit: 5 });
    related.push(...semanticResults.filter(r => r.id !== id));
    
    return this.deduplicateById(related);
  }
  
  // 分析機能
  async analyze(): Promise<MemoryAnalytics> {
    const allMemories = await this.getAllMemories();
    
    return {
      totalCount: allMemories.length,
      byType: this.countByType(allMemories),
      byCategory: this.countByCategory(allMemories),
      averageImportance: this.calculateAverageImportance(allMemories),
      topTags: this.getTopTags(allMemories, 20),
      timeline: this.buildTimeline(allMemories),
      relationships: this.analyzeRelationships(allMemories),
    };
  }
  
  // エクスポート
  async export(format: 'json' | 'markdown' = 'json'): Promise<string> {
    const allMemories = await this.getAllMemories();
    
    if (format === 'json') {
      return JSON.stringify(allMemories, null, 2);
    } else {
      return this.convertToMarkdown(allMemories);
    }
  }
  
  // インポート
  async import(data: string, format: 'json' | 'markdown' = 'json'): Promise<number> {
    let memories: MemoryEntry[];
    
    if (format === 'json') {
      memories = JSON.parse(data);
    } else {
      memories = await this.parseMarkdown(data);
    }
    
    for (const memory of memories) {
      await this.remember(memory);
    }
    
    return memories.length;
  }
  
  // プライベートメソッド
  private async generateSummary(content: string): Promise<string> {
    // 簡易要約(実際にはLLM使用推奨)
    return content.substring(0, 200) + '...';
  }
  
  private serializeMetadata(metadata: any): any {
    return {
      ...metadata,
      created_at: metadata.created_at.toISOString(),
      updated_at: metadata.updated_at.toISOString(),
      tags: metadata.tags.join(','),
    };
  }
  
  private buildWhereClause(options: SearchOptions): any {
    const where: any = {};
    
    if (options.type) where.type = options.type;
    if (options.minImportance) where.importance = { $gte: options.minImportance };
    
    return Object.keys(where).length > 0 ? where : undefined;
  }
  
  private async saveToFile(entry: MemoryEntry): Promise<void> {
    const dir = `.ai/memory/entries/${entry.type}s`;
    await fs.mkdir(dir, { recursive: true });
    
    const jsonPath = path.join(dir, `${entry.id}.json`);
    await fs.writeFile(jsonPath, JSON.stringify(entry, null, 2));
  }
  
  private async deleteFile(id: string): Promise<void> {
    // 実装省略
  }
  
  private deduplicateById(entries: MemoryEntry[]): MemoryEntry[] {
    const seen = new Set<string>();
    return entries.filter(entry => {
      if (seen.has(entry.id)) return false;
      seen.add(entry.id);
      return true;
    });
  }
  
  private countByType(memories: MemoryEntry[]): Record<string, number> {
    // 実装省略
    return {};
  }
  
  // ... その他のプライベートメソッド
}

// 使用例
async function example() {
  const system = new MemorySystem({
    chromaUrl: 'http://localhost:8000',
  });
  
  await system.initialize();
  
  // 記憶を追加
  const decision = await system.remember({
    type: MemoryType.DECISION,
    title: 'Redis導入',
    content: 'キャッシュとセッション管理のためにRedis 7.xを導入する決定',
    metadata: {
      tags: ['redis', 'cache', 'session'],
      category: 'infrastructure',
      importance: 9,
    },
  });
  
  // 検索
  const results = await system.recall('キャッシュ戦略', {
    type: MemoryType.DECISION,
    minImportance: 7,
  });
  
  console.log(`Found ${results.length} related memories`);
  
  // 関連記憶を取得
  const related = await system.getRelated(decision.id);
  console.log(`${related.length} related memories found`);
  
  // 分析
  const analytics = await system.analyze();
  console.log(analytics);
}

パフォーマンス最適化

どれだけ優れたアーキテクチャでも、検索に5秒かかるシステムは誰も使いません。記憶システムの実用性は、レスポンス速度で決まります。

実際のプロジェクトでは、記憶の数は数百、数千と増えていきます。エンベディングの生成だけでも1クエリあたり100ms以上かかり、ベクトル検索のレイテンシも無視できません。1000件の記憶を持つプロジェクトで、毎回すべてをスキャンしていては、検索のたびに数秒待たされることになります。

パフォーマンス最適化の鍵は、適切なキャッシング戦略効率的なインデックス管理です。頻繁にアクセスされる記憶をメモリにキャッシュし、同じクエリのエンベディングを再計算せず、バッチ処理で効率的にインデックスを更新する——これらの技術により、1000件以上の記憶を持つシステムでも、検索を100ms以下に抑えることができます。

2層キャッシング(メモリエントリとエンベディング)、バッチ処理、インデックス最適化、パフォーマンス監視まで、実践的な最適化テクニックを順に見ていきましょう。

キャッシング戦略

2層キャッシング(メモリエントリとエンベディング)により、検索速度を劇的に向上させます。頻繁にアクセスされるデータはメモリに保持し、DBアクセスを最小化します。

// .ai/scripts/memory-cache.ts

import NodeCache from 'node-cache';

class MemoryCache {
  private cache: NodeCache;
  private embeddingCache: NodeCache;
  
  constructor() {
    // メモリエントリのキャッシュ(10分)
    this.cache = new NodeCache({
      stdTTL: 600,
      checkperiod: 120,
      maxKeys: 1000,
    });
    
    // エンベディングのキャッシュ(1時間)
    this.embeddingCache = new NodeCache({
      stdTTL: 3600,
      checkperiod: 300,
      maxKeys: 5000,
    });
  }
  
  // メモリエントリ取得(キャッシュ優先)
  async get(id: string, loader: () => Promise<MemoryEntry>): Promise<MemoryEntry> {
    const cached = this.cache.get<MemoryEntry>(id);
    if (cached) return cached;
    
    const entry = await loader();
    this.cache.set(id, entry);
    return entry;
  }
  
  // エンベディング取得(キャッシュ優先)
  async getEmbedding(
    text: string,
    generator: (text: string) => Promise<number[]>
  ): Promise<number[]> {
    const cacheKey = this.hashText(text);
    const cached = this.embeddingCache.get<number[]>(cacheKey);
    if (cached) return cached;
    
    const embedding = await generator(text);
    this.embeddingCache.set(cacheKey, embedding);
    return embedding;
  }
  
  // キャッシュクリア
  clear() {
    this.cache.flushAll();
    this.embeddingCache.flushAll();
  }
  
  // 統計情報
  stats() {
    return {
      memory: {
        keys: this.cache.keys().length,
        hits: this.cache.getStats().hits,
        misses: this.cache.getStats().misses,
      },
      embedding: {
        keys: this.embeddingCache.keys().length,
        hits: this.embeddingCache.getStats().hits,
        misses: this.embeddingCache.getStats().misses,
      },
    };
  }
  
  private hashText(text: string): string {
    // シンプルなハッシュ関数
    let hash = 0;
    for (let i = 0; i < text.length; i++) {
      const char = text.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return hash.toString(16);
  }
}

バッチ処理

大量の記憶を一度にインデックス化する際は、バッチ処理が不可欠です。100件ずつまとめて処理することで、API呼び出しのオーバーヘッドを削減し、処理時間を大幅に短縮できます。

// 大量のメモリを一括インデックス
async function batchIndex(entries: MemoryEntry[], batchSize: number = 100) {
  const collection = await getCollection();
  
  for (let i = 0; i < entries.length; i += batchSize) {
    const batch = entries.slice(i, i + batchSize);
    
    await collection.add({
      ids: batch.map(e => e.id),
      documents: batch.map(e => e.content),
      metadatas: batch.map(e => serializeMetadata(e.metadata)),
    });
    
    console.log(`Indexed ${i + batch.length}/${entries.length}`);
  }
}

インデックス最適化

時間とともにインデックスは断片化し、検索パフォーマンスが低下します。月次での再構築により、常に最適な状態を保ち、高速な検索を維持できます。

// 定期的なインデックス再構築
async function rebuildIndex() {
  console.log('🔄 Rebuilding index...');
  
  // 既存のコレクションを削除
  await client.deleteCollection({ name: 'project_memory' });
  
  // 新しいコレクションを作成
  const collection = await client.createCollection({
    name: 'project_memory',
    embeddingFunction: embedder,
    metadata: { rebuilt_at: new Date().toISOString() },
  });
  
  // すべてのメモリエントリを読み込み
  const entries = await loadAllMemoryEntries();
  
  // バッチでインデックス
  await batchIndex(entries);
  
  console.log('✅ Index rebuilt successfully');
}

// 月次で実行
// cron: 0 2 1 * * (毎月1日 午前2時)

運用とメンテナンス

長期記憶システムは「作って終わり」ではありません。データは日々増え続け、インデックスは断片化し、ディスク容量は圧迫されていきます。適切な運用とメンテナンスがなければ、いずれシステムは劣化し、検索速度の低下、インデックスの不整合、最悪の場合はデータ損失につながります。

運用の本質は、問題が起きる前に検知し、自動的に修復することです。ヘルスチェックで異常を早期発見し、バックアップで万が一に備え、定期メンテナンスでシステムを最適な状態に保つ——これらの仕組みを最初から組み込むことで、記憶システムは何年にもわたって安定稼働します。

現実のプロジェクトでは、記憶システムの障害はAIエージェントの機能停止を意味します。「3ヶ月前の決定が見つからない」「検索が遅すぎて使えない」「インデックスが壊れて再構築が必要」——これらの問題を未然に防ぐには、プロアクティブな運用戦略が不可欠です。

自動化されたヘルスチェック、堅牢なバックアップ・リストア戦略、cronによるメンテナンスタスク、モニタリングとアラート設定まで、実運用に必要なすべての要素を具体的に実装していきます。

ヘルスチェック

定期的なヘルスチェックにより、システムの状態を監視し、問題を早期発見できます。ベクトルDB接続、インデックス整合性、ディスク使用量、API状態を自動チェックします。

// .ai/scripts/memory-health.ts

async function healthCheck(): Promise<HealthReport> {
  const report: HealthReport = {
    status: 'healthy',
    checks: [],
    timestamp: new Date(),
  };
  
  // 1. ベクトルDBの接続確認
  try {
    await client.heartbeat();
    report.checks.push({
      name: 'VectorDB Connection',
      status: 'ok',
      message: 'Connected successfully',
    });
  } catch (error) {
    report.status = 'unhealthy';
    report.checks.push({
      name: 'VectorDB Connection',
      status: 'error',
      message: error.message,
    });
  }
  
  // 2. インデックスの整合性確認
  const collection = await client.getCollection({ name: 'project_memory' });
  const count = await collection.count();
  
  const fileCount = (await glob('.ai/memory/entries/**/*.json')).length;
  
  if (count === fileCount) {
    report.checks.push({
      name: 'Index Consistency',
      status: 'ok',
      message: `${count} entries indexed`,
    });
  } else {
    report.status = 'warning';
    report.checks.push({
      name: 'Index Consistency',
      status: 'warning',
      message: `Mismatch: ${count} indexed vs ${fileCount} files`,
      suggestion: 'Run: npm run memory:reindex',
    });
  }
  
  // 3. ディスク使用量確認
  const indexSize = await getDirectorySize('.ai/memory/index');
  const entriesSize = await getDirectorySize('.ai/memory/entries');
  
  report.checks.push({
    name: 'Disk Usage',
    status: 'ok',
    message: `Index: ${formatBytes(indexSize)}, Entries: ${formatBytes(entriesSize)}`,
  });
  
  // 4. エンベディングAPIの確認
  try {
    const testEmbedding = await embedder.generate(['test']);
    report.checks.push({
      name: 'Embedding API',
      status: 'ok',
      message: 'API responding normally',
    });
  } catch (error) {
    report.status = 'unhealthy';
    report.checks.push({
      name: 'Embedding API',
      status: 'error',
      message: error.message,
      suggestion: 'Check OPENAI_API_KEY environment variable',
    });
  }
  
  return report;
}

interface HealthReport {
  status: 'healthy' | 'warning' | 'unhealthy';
  checks: HealthCheck[];
  timestamp: Date;
}

interface HealthCheck {
  name: string;
  status: 'ok' | 'warning' | 'error';
  message: string;
  suggestion?: string;
}

async function getDirectorySize(dir: string): Promise<number> {
  let size = 0;
  const files = await glob(`${dir}/**/*`);
  
  for (const file of files) {
    const stats = await fs.stat(file);
    if (stats.isFile()) {
      size += stats.size;
    }
  }
  
  return size;
}

function formatBytes(bytes: number): string {
  if (bytes < 1024) return bytes + ' B';
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
  if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
  return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}

// CLI表示
if (require.main === module) {
  healthCheck()
    .then(report => {
      console.log(chalk.blue('\n🏥 Memory System Health Check\n'));
      console.log(chalk.gray(`Timestamp: ${report.timestamp.toISOString()}\n`));
      
      report.checks.forEach(check => {
        const statusIcon = {
          ok: chalk.green(''),
          warning: chalk.yellow('⚠️'),
          error: chalk.red(''),
        }[check.status];
        
        console.log(`${statusIcon} ${check.name}`);
        console.log(chalk.gray(`   ${check.message}`));
        
        if (check.suggestion) {
          console.log(chalk.yellow(`   💡 ${check.suggestion}`));
        }
        console.log();
      });
      
      const statusIcon = {
        healthy: chalk.green('✅ Healthy'),
        warning: chalk.yellow('⚠️  Warning'),
        unhealthy: chalk.red('❌ Unhealthy'),
      }[report.status];
      
      console.log(chalk.bold(`\n総合ステータス: ${statusIcon}\n`));
      
      process.exit(report.status === 'unhealthy' ? 1 : 0);
    })
    .catch(err => {
      console.error(chalk.red('Health check failed:'), err);
      process.exit(1);
    });
}

バックアップとリストア

記憶システムのデータ損失は、組織の知識の喪失を意味します。定期的なバックアップとテスト済みのリストア手順により、万が一に備えます。

// .ai/scripts/memory-backup.ts

import tar from 'tar';
import path from 'path';

class MemoryBackup {
  private backupDir = '.ai/memory/backups';
  
  async backup(): Promise<string> {
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const backupName = `memory-backup-${timestamp}`;
    const backupPath = path.join(this.backupDir, `${backupName}.tar.gz`);
    
    console.log(chalk.blue('🔄 Creating backup...'));
    
    // バックアップディレクトリを作成
    await fs.mkdir(this.backupDir, { recursive: true });
    
    // tar.gzで圧縮
    await tar.create(
      {
        gzip: true,
        file: backupPath,
        cwd: '.ai',
      },
      ['memory/entries', 'memory/index']
    );
    
    const stats = await fs.stat(backupPath);
    console.log(chalk.green(`✅ Backup created: ${backupPath}`));
    console.log(chalk.gray(`   Size: ${formatBytes(stats.size)}`));
    
    // 古いバックアップを削除(30日以上前)
    await this.cleanOldBackups(30);
    
    return backupPath;
  }
  
  async restore(backupPath: string): Promise<void> {
    console.log(chalk.blue(`🔄 Restoring from ${backupPath}...`));
    
    // 現在のデータをバックアップ
    await this.backup();
    
    // 既存データを削除
    await fs.rm('.ai/memory/entries', { recursive: true, force: true });
    await fs.rm('.ai/memory/index', { recursive: true, force: true });
    
    // バックアップから復元
    await tar.extract({
      file: backupPath,
      cwd: '.ai',
    });
    
    console.log(chalk.green('✅ Restore completed'));
    console.log(chalk.yellow('⚠️  Please reindex: npm run memory:reindex'));
  }
  
  async list(): Promise<BackupInfo[]> {
    const files = await glob(path.join(this.backupDir, '*.tar.gz'));
    
    const backups: BackupInfo[] = [];
    
    for (const file of files) {
      const stats = await fs.stat(file);
      backups.push({
        path: file,
        name: path.basename(file),
        size: stats.size,
        created: stats.birthtime,
      });
    }
    
    return backups.sort((a, b) => b.created.getTime() - a.created.getTime());
  }
  
  private async cleanOldBackups(daysToKeep: number): Promise<void> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
    
    const backups = await this.list();
    let deletedCount = 0;
    
    for (const backup of backups) {
      if (backup.created < cutoffDate) {
        await fs.unlink(backup.path);
        deletedCount++;
      }
    }
    
    if (deletedCount > 0) {
      console.log(chalk.gray(`   Cleaned ${deletedCount} old backup(s)`));
    }
  }
}

interface BackupInfo {
  path: string;
  name: string;
  size: number;
  created: Date;
}

// CLI
if (require.main === module) {
  const command = process.argv[2];
  const backupManager = new MemoryBackup();
  
  switch (command) {
    case 'create':
      backupManager.backup();
      break;
      
    case 'list':
      backupManager.list().then(backups => {
        console.log(chalk.blue('\n📦 Available Backups\n'));
        backups.forEach((backup, i) => {
          console.log(`${i + 1}. ${backup.name}`);
          console.log(chalk.gray(`   Created: ${backup.created.toLocaleString()}`));
          console.log(chalk.gray(`   Size: ${formatBytes(backup.size)}\n`));
        });
      });
      break;
      
    case 'restore':
      const backupPath = process.argv[3];
      if (!backupPath) {
        console.error('Usage: npm run memory:backup restore <backup-path>');
        process.exit(1);
      }
      backupManager.restore(backupPath);
      break;
      
    default:
      console.log('Usage:');
      console.log('  npm run memory:backup create');
      console.log('  npm run memory:backup list');
      console.log('  npm run memory:backup restore <backup-path>');
      process.exit(1);
  }
}

定期メンテナンスタスク

運用を自動化するため、npmスクリプトとcronを組み合わせて定期メンテナンスを設定します。ヘルスチェック、バックアップ、インデックス再構築、クリーンアップを定期実行します。

// package.json
{
  "scripts": {
    "memory:health": "tsx .ai/scripts/memory-health.ts",
    "memory:backup": "tsx .ai/scripts/memory-backup.ts",
    "memory:reindex": "tsx .ai/scripts/memory-index.ts --rebuild",
    "memory:cleanup": "tsx .ai/scripts/memory-cleanup.ts",
    "memory:maintenance": "npm run memory:health && npm run memory:backup create && npm run memory:cleanup"
  }
}
# crontab設定例

# 毎日午前3時: ヘルスチェック
0 3 * * * cd /path/to/project && npm run memory:health

# 毎週日曜午前2時: バックアップ
0 2 * * 0 cd /path/to/project && npm run memory:backup create

# 毎月1日午前1時: インデックス再構築
0 1 1 * * cd /path/to/project && npm run memory:reindex

# 毎月15日午前4時: クリーンアップ
0 4 15 * * cd /path/to/project && npm run memory:cleanup

ベストプラクティス

長期記憶システムを成功させるか失敗させるかは、日々の運用における小さな選択の積み重ねで決まります。適切なメタデータの付与、記憶同士の関連付け、定期的なメンテナンス——これらのベストプラクティスを守ることで、システムは何年にもわたって高い価値を提供し続けます。

ここでは、実際のプロジェクトで学んだ「やるべきこと」と「やってはいけないこと」を整理します。これらは理論ではなく、実戦で得た教訓です。

多くのプロジェクトでは、最初は記憶システムを熱心に使っても、徐々に形骸化していきます。その理由は、不適切な運用慣行にあります。メタデータが雑、重要度がバラバラ、関連付けがない、検索しても見つからない——こうした「技術的負債」が蓄積すると、誰も記憶システムを信頼しなくなります。

逆に、ベストプラクティスを守ったプロジェクトでは、記憶システムは「なくてはならないインフラ」になります。新しいメンバーは過去の議論を即座に理解し、ベテランは意思決定の根拠を明確に説明でき、チーム全体の知識レベルが底上げされるのです。

✅ DO: やるべきこと

1. 記憶には必ず構造化メタデータを付与

// ✅ Good
const memory: MemoryEntry = {
  id: 'decision-015',
  type: MemoryType.DECISION,
  title: 'JWT有効期限を15分に変更',
  content: '...',
  metadata: {
    created_at: new Date(),
    updated_at: new Date(),
    created_by: '@alice',
    tags: ['jwt', 'security', 'authentication'],
    category: 'security',
    importance: 9,
  },
  relations: {
    related: ['decision-001', 'issue-234'],
  },
};

// ❌ Bad
const memory = {
  content: 'JWT有効期限を15分にした',
  // メタデータなし → 検索困難
};

2. 関連する記憶同士をリンク

// ✅ Good: 決定と実装をリンク
{
  id: 'decision-020',
  type: MemoryType.DECISION,
  title: 'GraphQL導入',
  relations: {
    related: [
      'pattern-graphql-resolver',  // 実装パターン
      'lesson-graphql-n+1',        // 学習事項
      'spec-graphql-schema'        // 仕様
    ]
  }
}

// ❌ Bad: 孤立した記憶
{
  id: 'decision-020',
  title: 'GraphQL導入',
  relations: { related: [] }  // リンクなし
}

3. 重要度を適切に設定

// 重要度の基準
const importanceGuide = {
  10: 'プロジェクトの根幹に関わる決定(アーキテクチャ選定等)',
  8-9: 'システム全体に影響する決定(認証方式、DB選定等)',
  6-7: 'モジュール単位の重要な決定',
  4-5: '機能レベルの実装判断',
  1-3: '細かい実装の記録',
};

// ✅ Good
{
  title: 'Clean Architecture採用',
  importance: 10,  // プロジェクトの根幹
}

{
  title: 'エラーメッセージの文言変更',
  importance: 2,   // 細かい変更
}

4. 定期的にインデックスを最適化

# 月次メンテナンス
npm run memory:health          # ヘルスチェック
npm run memory:backup create   # バックアップ
npm run memory:reindex         # インデックス再構築
npm run memory:cleanup         # 不要データ削除

5. 検索クエリは具体的に

// ✅ Good: 具体的なクエリ
await searchMemory({
  query: 'JWT認証のトークン有効期限を決定した背景と理由',
  type: MemoryType.DECISION,
  minImportance: 7,
});

// ❌ Bad: 曖昧すぎるクエリ
await searchMemory({
  query: '認証',  // 広すぎる
});

❌ DON'T: やってはいけないこと

1. エンベディングAPIキーをコミットしない

# .env
OPENAI_API_KEY=sk-...

# .gitignore
.env
.ai/memory/index/*  # ベクトルインデックスもgitignore

2. 大きすぎる記憶を作らない

// ❌ Bad: コンテンツが長すぎる
{
  content: '10000行のコード全文...',  // 長すぎる
}

// ✅ Good: 要約とリンク
{
  content: '認証モジュールの実装。詳細は src/auth/ を参照',
  context: {
    file_path: 'src/auth/auth.service.ts',
    code_snippet: '// 重要な部分だけ抜粋',
  },
}

3. 機密情報を記憶に含めない

// ❌ Bad: APIキーやパスワード
{
  content: 'Stripe API Key: sk_live_xxxxx',  // 絶対NG!
}

// ✅ Good: 参照のみ
{
  content: 'Stripe APIキーは .env の STRIPE_API_KEY を使用',
}

4. すべてを記憶しようとしない

// 記憶すべきもの
- 技術的な決定とその理由
- 過去の失敗と解決策
- 重要なコードパターン
- チーム合意事項

// 記憶不要なもの
- コード全体Gitで管理
- 日々の些細な変更
- 個人的なメモ
- 一時的な実験

トラブルシューティング

長期記憶システムを実装・運用していると、避けられない問題に遭遇します。検索結果が期待と違う、パフォーマンスが低下する、インデックスが壊れる——これらは誰もが経験する典型的な課題です。

重要なのは、問題が起きた時にパニックにならず、体系的にトラブルシューティングすることです。ほとんどの問題には既知の原因と解決策があり、適切な診断手順を踏めば短時間で解決できます。

実際のプロジェクトで頻繁に遭遇する4つの問題と、その具体的な解決方法を見ていきましょう。それぞれの問題について、症状の特定、根本原因の診断、段階的な対策まで、実践的なガイドを提供します。

問題1: 検索結果が不正確

セマンティック検索で期待した結果が出ない場合、原因は主に3つあります。エンベディングモデルの選択、メタデータの不足、クエリの曖昧さです。

症状:

$ npm run memory:search "JWT認証"
# → 全く関係ないRedisの記憶が出てくる

原因と対策:

  1. エンベディングモデルの問題
// 原因: 古いモデルを使用
model_name: 'text-embedding-ada-002'  // 旧モデル

// 対策: 最新モデルに変更
model_name: 'text-embedding-3-small'  // 新モデル

// インデックス再構築が必要
npm run memory:reindex
  1. メタデータ不足
// 原因: タグやカテゴリがない
{
  content: 'JWT認証を導入',
  metadata: { tags: [] }  // タグなし
}

// 対策: 適切なメタデータを追加
{
  content: 'JWT認証を導入',
  metadata: {
    tags: ['jwt', 'authentication', 'security'],
    category: 'security',
  }
}
  1. クエリが曖昧
// 原因: 単語だけのクエリ
query: 'JWT'

// 対策: 文章で具体的に
query: 'JWT認証のトークン有効期限を決定した理由'

問題2: インデックスとファイルの不一致

ファイルシステムのJSONファイルとベクトルDBのインデックスがずれると、検索結果に矛盾が生じます。定期的な整合性チェックと再インデックスで解決します。

症状:

$ npm run memory:health
# ⚠️ Index Consistency: Mismatch: 50 indexed vs 55 files

対策:

# ステップ1: バックアップ
npm run memory:backup create

# ステップ2: インデックス再構築
npm run memory:reindex

# ステップ3: 再確認
npm run memory:health

問題3: パフォーマンス低下

記憶の数が増えるにつれ、検索が遅くなる場合があります。インデックスサイズの確認、古いデータのアーカイブ、キャッシュの活用で改善できます。

症状:

  • 検索に10秒以上かかる
  • メモリ使用量が異常に高い

対策:

  1. インデックスサイズの確認
$ du -sh .ai/memory/index
# 500MB以上なら要最適化
  1. 古い記憶をアーカイブ
// .ai/scripts/memory-cleanup.ts

async function archiveOldMemories(monthsOld: number = 12) {
  const cutoffDate = new Date();
  cutoffDate.setMonth(cutoffDate.getMonth() - monthsOld);
  
  const collection = await client.getCollection({ name: 'project_memory' });
  
  const oldMemories = await collection.get({
    where: {
      created_at: { $lt: cutoffDate.toISOString() },
      importance: { $lt: 7 },  // 重要度7未満のみ
    },
  });
  
  // アーカイブディレクトリに移動
  for (const id of oldMemories.ids) {
    const entry = await loadMemory(id);
    await moveToArchive(entry);
    await collection.delete({ ids: [id] });
  }
  
  console.log(`Archived ${oldMemories.ids.length} old memories`);
}
  1. キャッシュの活用
// 頻繁に検索するクエリをキャッシュ
const popularQueries = [
  '認証の実装方法',
  'エラーハンドリングのパターン',
  'データベース設計',
];

// 起動時にウォームアップ
async function warmupCache() {
  for (const query of popularQueries) {
    await searchMemory({ query, limit: 5 });
  }
}

問題4: ベクトルDBが起動しない

Chromaへの接続エラーは、Dockerコンテナの停止やポート競合が原因です。コンテナの状態確認と再起動で多くの場合解決します。

症状:

$ npm run memory:search "test"
# Error: Failed to connect to Chroma at http://localhost:8000

対策:

  1. Dockerコンテナの確認
# コンテナ状態確認
docker ps -a | grep chroma

# 起動していない場合
docker start chroma

# または新規作成
docker run -d \
  --name chroma \
  -p 8000:8000 \
  -v $(pwd)/.ai/memory/index:/chroma/data \
  chromadb/chroma
  1. ポート競合の確認
# ポート8000が使用中か確認
lsof -i :8000

# 別のポートを使用
docker run -p 8001:8000 chromadb/chroma

# config更新
// .ai/memory/config.json
{
  "chromaUrl": "http://localhost:8001"
}

実践的なユースケース

ここまで、長期記憶システムの理論と実装を詳しく解説してきました。しかし、「実際のプロジェクトでどう使うの?」という疑問が残っているかもしれません。

日々の開発フローの中で記憶システムをどう活用するか、3つの具体的なシナリオを通して見ていきましょう。新機能開発、デバッグ、コードレビュー——それぞれの場面で、長期記憶システムがどのように開発者の判断を支援し、品質を向上させるかが明らかになります。

これらのユースケースは、単なる理論上の想定ではありません。実際のプロジェクトで繰り返し直面する、リアルな開発シーンです。記憶システムを導入することで、これらのタスクの所要時間が半分以下になり、意思決定の品質が飛躍的に向上します。

ユースケース1: 新機能開発の記憶活用

// シナリオ: 決済機能を新規開発

// ステップ1: 関連する過去の記憶を検索
const paymentMemories = await searchMemory({
  query: '決済処理 エラーハンドリング Stripe連携',
  type: MemoryType.PATTERN,
  limit: 10,
});

// ステップ2: 関連する決定事項も取得
const decisions = await searchMemory({
  query: '決済 外部API 設計決定',
  type: MemoryType.DECISION,
  minImportance: 7,
});

// ステップ3: 過去の失敗例を確認
const lessons = await searchMemory({
  query: '決済 失敗 トラブル',
  type: MemoryType.LESSON,
});

// ステップ4: コンテキストを整理してエージェントに渡す
const context = `
# 決済機能開発のコンテキスト

## 過去の実装パターン
${paymentMemories.map(m => `- ${m.title}: ${m.summary}`).join('\n')}

## 関連する技術的決定
${decisions.map(d => `- ${d.title}`).join('\n')}

## 注意すべき過去の失敗
${lessons.map(l => `- ${l.title}`).join('\n')}

## 要件
新しい決済プロバイダー(PayPay)の統合を実装してください。
`;

// ステップ5: エージェントに依頼
// Claude Codeなどで上記コンテキストを使用

ユースケース2: デバッグ時の記憶参照

// シナリオ: 本番で認証エラーが発生

// ステップ1: エラーに関連する記憶を時系列で検索
const recentAuthIssues = await searchByTimeline(
  '認証 エラー トークン',
  new Date('2024-09-01'),
  new Date(),
  'week'
);

// ステップ2: 過去の似た問題を検索
const similarIssues = await searchMemory({
  query: 'JWT トークン 有効期限切れ 本番環境',
  type: MemoryType.TROUBLESHOOTING,
});

// ステップ3: 解決策の記憶を確認
if (similarIssues.length > 0) {
  const solution = similarIssues[0];
  console.log(`過去に似た問題がありました: ${solution.title}`);
  console.log(`解決策: ${solution.content}`);
  
  // 関連する変更も確認
  const related = await findRelated(solution.id);
  console.log('関連する変更:', related.map(r => r.title));
}

// ステップ4: 今回の解決策を記録
await memorySystem.remember({
  type: MemoryType.TROUBLESHOOTING,
  title: '本番環境でJWTトークン有効期限エラー',
  content: `
## 問題
本番環境でユーザーのトークンが突然無効になる問題が発生。

## 原因
サーバー時刻がずれていた(NTP同期の問題)。

## 解決策
1. NTPサーバーとの同期を強制
2. トークン有効期限に30秒のバッファを追加
3. モニタリングアラートを追加

## 再発防止
- 定期的なサーバー時刻チェックを自動化
- トークン検証時にクロックスキューを考慮
  `,
  metadata: {
    tags: ['jwt', 'authentication', 'production', 'troubleshooting'],
    category: 'security',
    importance: 9,
  },
  relations: {
    related: similarIssues.map(s => s.id),
  },
});

ユースケース3: コードレビュー時の記憶活用

// シナリオ: Pull Requestのレビュー

// ステップ1: PRの変更内容を分析
const prChanges = await analyzePullRequest('PR-123');

// ステップ2: 関連する過去の決定を検索
for (const change of prChanges.files) {
  const memories = await searchMemory({
    query: `${change.module} 設計 アーキテクチャ`,
    type: MemoryType.DECISION,
  });
  
  if (memories.length > 0) {
    console.log(`\n${change.file} に関連する決定:`);
    memories.forEach(m => {
      console.log(`  - ${m.title}: ${m.summary}`);
    });
  }
}

// ステップ3: コーディング規約との整合性確認
const conventions = await searchMemory({
  query: prChanges.languages.join(' ') + ' コーディング規約',
  type: MemoryType.SPECIFICATION,
});

// ステップ4: 過去の似たPRのレッスンを参照
const lessons = await searchMemory({
  query: `${prChanges.module} 実装 注意点`,
  type: MemoryType.LESSON,
});

// ステップ5: レビューコメントを生成
const reviewComments = generateReviewComments(
  prChanges,
  memories,
  conventions,
  lessons
);

まとめ

ここまで長期記憶システムの設計から実装、運用まで、包括的に解説してきました。

「AIエージェントに記憶を与える」——一見シンプルに聞こえるこのコンセプトは、実は現代のソフトウェア開発における最も重要な課題の1つを解決します。それは、**「組織の知識をどう保存し、必要な時に引き出すか」**という、何十年も前から存在する本質的な問題です。

Markdownファイル、Wiki、ドキュメント——これらは静的で、検索も限定的で、AIエージェントが効果的に活用するには不十分でした。しかし、ベクトルDB、セマンティック検索、構造化されたメタデータを組み合わせた長期記憶システムは、この問題に対する決定的な解答を提供します。

このシステムを導入したプロジェクトでは、「3ヶ月前の決定」を即座に参照でき、「過去の失敗」から学び、「関連する議論」を自動的に発見できます。AIエージェントは、真の意味で「忘れない」パートナーとなるのです。

最後に、これまでの内容を振り返り、システムの価値と実装のステップをまとめておきましょう。

長期記憶システムの価値

実装の5つのステップ

ステップ1: 基本セットアップ(1日)

# 1. Chromaのインストール
docker run -d -p 8000:8000 chromadb/chroma

# 2. 必要なパッケージ
npm install chromadb openai

# 3. ディレクトリ構造作成
mkdir -p .ai/memory/{entries,index,backups}

# 4. 環境変数設定
echo "OPENAI_API_KEY=your-key" > .env

ステップ2: 既存コンテキストのインデックス化(半日)

# スクリプトをコピー
cp templates/memory-index.ts .ai/scripts/

# 既存のMarkdownファイルをインデックス
npm run memory:index

ステップ3: 検索機能の実装(半日)

# 検索スクリプトをコピー
cp templates/memory-search.ts .ai/scripts/

# 検索を試す
npm run memory:search "認証の実装方法"

ステップ4: 記憶追加フローの整備(1日)

# ADR作成スクリプト
cp templates/memory-add.ts .ai/scripts/

# 試しにADRを作成
npm run memory:add

ステップ5: 運用体制の構築(1日)

# ヘルスチェック・バックアップ設定
cp templates/memory-health.ts .ai/scripts/
cp templates/memory-backup.ts .ai/scripts/

# crontabに登録
crontab -e

成功の鍵

1. 習慣化

✅ 重要な決定をしたら即座に記録
✅ 問題を解決したら必ず記憶に追加
✅ 週次で記憶システムをレビュー

2. 適切な粒度

✅ すべてを記録しようとしない
✅ 重要度7以上のものに集中
✅ 詳細はコードやドキュメントへのリンクで

3. チームでの活用

✅ 記憶の追加をPRプロセスに組み込む
✅ 定期的な記憶レビュー会を開催
✅ 検索の使い方をチームで共有

4. 継続的な改善

✅ 月次でシステムの使用状況を分析
✅ 検索精度を定期的に評価
✅ 記憶の品質をモニタリング

次のステップ

さらに学ぶための資料

関連記事(コンテキストエンジニアリングシリーズ)

📚 シリーズトップページ

  1. コーディングエージェント時代のコンテキストエンジニアリング実践ガイド

    • 個人利用の基礎(前提知識)
  2. コンテキストウィンドウの処理フローと動作メカニズム 完全解説

    • 技術的詳細(深い理解)
  3. プロンプトエンジニアリング2.0 - コンテキストを制する者がAIを制する

    • コンテキスト管理 + プロンプト設計の統合
  4. コーディングエージェントのメモリ設計 - 長期記憶システムの実装

    • 外部化したコンテキストの管理・検索
  5. チーム開発のためのコンテキスト共有戦略

    • チーム活用(組織展開)
  6. コンテキスト駆動開発(CDD) - AIファーストな開発手法

    • 開発手法としての体系化
  7. マルチエージェント時代のコンテキストオーケストレーション

    • 複数エージェントの協調動作
  8. デバッグ駆動コンテキストエンジニアリング

    • トラブルシューティングに特化

参考リソース

ベクトルDB

RAG実装

メモリパターン

発展的なトピック

1. マルチモーダル記憶

// 画像・図表も記憶に含める
interface MultimodalMemory extends MemoryEntry {
  attachments: {
    type: 'image' | 'diagram' | 'video';
    url: string;
    embedding: number[];  // CLIP等でエンベディング
  }[];
}

2. エージェント間の記憶共有

// 複数のエージェントで記憶を共有
class SharedMemorySystem {
  async sync(agentId: string): Promise<void> {
    // 中央の記憶システムと同期
  }
}

3. 記憶の自動要約・分類

// LLMを使った自動メタデータ生成
async function enrichMemory(entry: MemoryEntry): Promise<MemoryEntry> {
  const llm = new OpenAI();
  
  // タグ自動生成
  const tags = await llm.complete(`
    この記憶に適切なタグを5個生成してください:
    ${entry.content}
  `);
  
  // 要約生成
  const summary = await llm.complete(`
    以下を200文字で要約してください:
    ${entry.content}
  `);
  
  return {
    ...entry,
    summary,
    metadata: {
      ...entry.metadata,
      tags: tags.split(',').map(t => t.trim()),
    },
  };
}

「忘れない」エージェントと共に、より高品質な開発を!

長期記憶システムを実装することで、あなたのAIエージェントは:

  • 📚 過去の知識を忘れない
  • 🔍 必要な情報を即座に見つける
  • 🎯 文脈に応じた最適な提案をする
  • 🚀 チーム全体の生産性を向上させる

ぜひ今日から実装を始めてみてください!


付録:規模別テンプレート(すぐ導入できる雛形)

ここまでお読みいただき、ありがとうございました。

この記事は約10万文字にわたり、長期記憶システムの設計から実装まで詳しく解説してきました。「理論はわかったけど、実際に何から始めればいいの?」という疑問をお持ちかもしれません。

そこで、プロジェクト規模別にすぐ使えるディレクトリ構成とファイルテンプレートを用意しました。

このセクションの使い方

  • 個人開発者: 小規模テンプレートで30分でセットアップ完了
  • 小〜中規模チーム: 中規模テンプレートでインデックス分割と自動化を実現
  • 大規模組織: 大規模テンプレートでマイクロサービス分割とガバナンス確保
  • 段階的成長: 小規模から始めて、チーム拡大に応じて段階的にスケールアップ

ヒント: まずは小規模テンプレートから始めて、実際に動かしながら自分のプロジェクトに合わせてカスタマイズすることをおすすめします。


小規模(個人/PoC)

.ai/
  memory/
    config.yaml        # プロバイダ/APIキー/インデックス名
    schema.json        # 最小スキーマ(id, title, content, tags)
  scripts/
    ingest.ts          # md/コードを一括投入
    search.ts          # CLI検索(topK, filters)
  • 推奨: 単一インデックス、tagsとpathのみで運用
  • 目標: セットアップ30分、運用は手動で十分

中規模(6-20人チーム)

.ai/
  memory/
    configs/
      default.yaml
      code.yaml        # コード系(型/関数シグネチャ重視)
      docs.yaml        # ドキュメント系(段落単位の分割)
    schema/
      base.json
      code.json        # language, symbol, importGraph
      docs.json        # section, heading, anchors
  pipelines/
    nightly.yaml       # 再インデックス/リンク切れ検知
  scripts/
    ingest-batch.ts
    validate-links.ts
    search.ts
  • 推奨: インデックス分割(code/docs)、夜間再構築、リンク検証
  • 目標: ヒット率向上と遅延のバランス最適化

大規模(21人〜/複数リポジトリ)

.ai/
  registry/
    sources.yaml       # リポジトリ/ブランチ/パスの宣言
  memory/
    shards/            # シャーディング(サービス/ドメイン)
    schema/            # 共通+シャード特有の属性
  pipelines/
    incremental.yaml   # 変更検知で差分インデックス
    quality.yaml       # 重複/矛盾/古さの自動指標
  scripts/
    orchestrate.ts     # 並列シャード更新
    query-router.ts    # シャード横断検索/集約
  • 推奨: シャーディング/クエリルーター/増分更新、品質ゲート
  • 目標: スケール時のコスト最適化とSLA維持

観測メトリクスと改善ループ(計測して回す)

コア指標(最低限)

  • 検索の正確性: Recall@k, MRR(週次)
  • 応答遅延: p50/p90(ms)
  • コスト: 1リクエストあたり(トークン/円)
  • 新鮮度: 更新からの経過時間(avg/max)

目安(初期ターゲット)

  • Recall@5 ≥ 0.75 / MRR ≥ 0.55
  • p90 レイテンシ ≤ 1200ms
  • 主要知識の新鮮度 ≤ 7日

週次改善ループ

  1. 指標確認 → 2) ボトルネック特定 → 3) 施策 → 4) 再測定

例)Recallが低い → スキーマにdomain, layer追加 → 分割粒度を段落→文に見直し → topK/filters再調整

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?