Tech Blog

Copilot に同じ間違いを2度させない仕組み — RAG + MCP で『議論の記憶』を持たせる設計

GitHub Copilot MCP RAG ChromaDB Ollama FastMCP AIアシスタント Python

🔗 シリーズ目次: 本記事は AIアシスタント運用ノート — Copilot / Claude Code を相棒として育てるための実践記録 シリーズの 実装編 (1) です。

この記事でわかること

  • GitHub Copilot に instructions.md を15個書いても事故が止まらなかった経緯と、その限界
  • MCP (Model Context Protocol) で Copilot に「外部記憶 (RAG)」を持たせるアーキテクチャ
  • ChromaDB + Ollama (nomic-embed-text) + FastMCP で 自作 MCP サーバー を立てる実装
  • 単なる検索ではなく、「事故を再発させない」ための6つの設計判断
    • Recency Boost / Priority High / session_context 自動エクスポート / Activity Log / 動的閾値 / Atomic Lock
  • 運用してみた効果 — 同じ失敗が止まった話と、それでも残る限界

対象読者

  • GitHub Copilot を使っていて「同じ指摘を何度も繰り返している」と感じている方
  • AI アシスタントに 長期記憶 を持たせたい方
  • MCP (Model Context Protocol) を 実用的なツールサーバー として作ってみたい方
  • AI を育てる」という発想に共感する方

動作環境

項目バージョン / 構成
OSWindows 11 (PowerShell 5.1 / 7)
Python3.13 (venv)
ベクトル DBChromaDB (永続化モード)
Embedding モデルOllama nomic-embed-text (768次元)
MCP フレームワークmcp Python SDK の FastMCP
クライアントVS Code GitHub Copilot Chat (MCP 設定経由で stdio 起動)

1. はじめに — 「お願いします」と言ったら24件 PATCH が暴走した夜の話

ある夜、Qiita の API で記事25件を一括更新するスクリプトを Copilot と組んでいました。Phase 1 で1件だけテスト送信して成功、5分待機してから Phase 2 で残り24件、という慎重な手順です。

Phase 1 が成功して、私はこう言いました。

「お願いします。」

私の意図は「Phase 2 の準備をお願いします、5分後に改めて実行指示を出します」でした。

Copilot はこう解釈しました:「Phase 2 を 今すぐ実行 という指示」。

24件が一気に飛び、レートリミット 429。Qiita 側のスライディングウィンドウが起動して、解除時刻が 翌日まで延長 されたのでした。

私は怒り、記録を残し、.instructions.md に行動規範を1個増やしました。

「お願いします」は実行指示ではない。実行前は「これから X を実行しますが、よろしいですか?」と必ず確認しろ。

それから1か月、同じような事件が起きるたびに規範が増え続けました。15個を超えた頃、私は気付きました。

規範を書いても、Copilot はまた同じ間違いをする。

正確に言えば、新しいセッションでは過去の事故を覚えていない。.instructions.md を毎回読み込んでも、文脈の中で似た言葉に出会ったときに 同じ誤判断をやり直す

これは Copilot のせいではなく、LLM そのものの性質 です。会話が変わればコンテキストが変わり、ルールはあっても「あの夜の24件 PATCH 暴走」という 生々しい記憶 はゼロから始まる。

私は、その夜のことを Copilot に覚えさせたい と思いました。命令としてではなく、記憶として

本記事は、その仕組みを作るまでの話です。


2. なぜ instructions.md だけでは足りなかったか

Copilot には .github/copilot-instructions.md や VSCode workspace の .instructions.md で、毎回読み込まれるルール を書ける仕組みがあります。これは便利で、私も活用してきました。

ですが、限界がありました。

限界1: 「ルールの羅列」では文脈に勝てない

- 「お願いします」を実行指示と解釈してはいけない
- 実行前は必ず確認を取る
- LLM の推測より API レスポンスを信じる
- 5分待機など時間制約は遵守する
- 同じミスを繰り返さない
...(あと10個続く)

こういうリストを毎回先頭に読ませても、会話の中で似た言葉に出会った瞬間 に Copilot は判断を間違えました。「お願いします」の事件は、規範を書いた 後にも 別の文脈で再現しました。

限界2: ルールに「経緯」と「感情」がない

ルールは事実ですが、「なぜそのルールがあるか」 の物語がない。Copilot にとっては「禁止リスト」に過ぎず、ルールを破ったときの 痛み を共有していません。

「24件 PATCH 暴走 → Qiita 側で解除時刻翌日まで延長 → 私の作業が1日止まった」という具体的な経緯と、その時の私の怒り、申し訳なさ、そして「同じ過ちは絶対に繰り返さない」という決意 — これらの 文脈ごと Copilot に届ける必要がありました。

限界3: ルールは増え続けるとノイズになる

10個目までは効きました。15個を超えると、読み込まれるけど効かない 状態になりました。Copilot は全部を一度に重要視できず、結局その時々の文脈で「読み流された」ルールが事故を起こしました。

ここで方針転換を決めました。

ルールを増やすのを止めよう。「いま必要なルールだけを、いま検索して取り出す」 仕組みに変える。

これが、RAG + MCP に至った経緯です。


3. 解決策 — RAG を Copilot の「外部記憶」にする

整理すると、必要だったのは以下です:

必要なものなぜ
失敗の経緯を蓄積する場所ルールの背景・痛み・文脈を残す
会話中に動的に呼び出せる仕組み「いま必要なものだけ」取り出す
重要なものを忘れさせない仕組み古い教訓が新しい記録に埋もれない
Copilot が自律判断で使える形ユーザーが毎回手動で渡さなくていい

これは RAG (Retrieval-Augmented Generation) のアーキテクチャがそのまま当てはまります。

[過去の教訓・結論・気づき]
   ↓ ベクトル化して保存
[ChromaDB (永続化)]
   ↑ 会話の文脈から検索
[Copilot] ← 関連する記憶をプロンプトに注入 → 適切に判断

ただし、これを Copilot から自然に呼び出させる ためには、もう一段の仕組みが必要でした。

そこで登場するのが MCP (Model Context Protocol) です。


4. MCP という橋渡し

MCP は Anthropic が策定したオープンプロトコルで、LLM クライアントと外部ツール / リソースサーバーを繋ぐ標準 です。GitHub Copilot Chat も MCP に対応しており、サーバーを登録すると Copilot が 自律的にツール呼び出し ができるようになります。

[Copilot Chat]
   ↓ MCP プロトコル (stdio)
[my-rag-brain MCP Server]
   ├─ search_memory(query, ...)   ← Copilot が必要に応じて呼び出す
   └─ add_note(text, type, ...)   ← 議論で出た結論をその場で記録

   [ChromaDB + Ollama embedding]

ポイントは2つ:

  1. Copilot が自律判断で検索する — ユーザーが「過去の記録を確認して」と毎回言わなくても、Copilot が文脈から「これは関連記録ありそう」と判断して search_memory を呼ぶ
  2. 記録もその場で行える — 議論の結論が出たり、ユーザーから指摘を受けた瞬間に、Copilot が add_note を呼んで RAG に追記する → 次のセッションで自分自身が参照できる

この 「自己再帰的な学習ループ」 が、instructions.md には作れなかったことです。


5. 実装の核心

my-rag-brain リポジトリの src/mcp/server.py の本質的な部分を抜粋します。

5.1 サーバー定義

mcp.server.fastmcp.FastMCP を使うと、Python の関数にデコレータを付けるだけで MCP ツールとして公開 できます。

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-rag-brain")

@mcp.tool()
def search_memory(query: str, type: str = "", domain: str = "", top: int = 5) -> str:
    """過去のやり取り・教訓・知識・アイデアを自然言語で検索する。

    作業中に関連する過去の記録が必要と判断したとき、自律的に呼び出すこと。
    クエリは現在の文脈から自分で生成すること。ユーザーに指定させない。
    """
    # ... ChromaDB クエリ ...

@mcp.tool()
def add_note(text: str, type: str = "note", priority: str = "") -> str:
    """会話中の気づき・合意・教訓・結論をその場で RAG に記録する。

    記録すべきタイミング:
      - ユーザーとの議論で結論が出たとき → type="conclusion"
      - ユーザーから指摘・修正を受けたとき → type="lesson", priority="high"
      - 新しい技術知識が確定したとき → type="knowledge"
    """
    # ... ChromaDB 書き込み ...

if __name__ == "__main__":
    mcp.run(transport="stdio")

ポイントは docstring の書き方 です。これは単なるコメントではなく、Copilot が “いつこのツールを呼ぶか” を判断する根拠 になります。

「ユーザーから指摘・修正を受けたとき → type=“lesson”, priority=“high”」

このように 呼び出し条件を明示 することで、Copilot は会話の流れから「これは記録すべき瞬間」を判断できるようになります。

5.2 VS Code 側の設定

VS Code の MCP 設定ファイル(.vscode/mcp.json または settings.json)にサーバーを登録します。

{
  "mcpServers": {
    "my-rag-brain": {
      "type": "stdio",
      "command": "C:\\Users\\y_104\\git\\my-rag-brain\\venv\\Scripts\\python.exe",
      "args": ["C:\\Users\\y_104\\git\\my-rag-brain\\src\\mcp\\server.py"],
      "env": {
        "PYTHONIOENCODING": "utf-8"
      }
    }
  }
}

Copilot Chat を開くと自動で起動し、ツール一覧に search_memoryadd_note が現れます。

5.3 ChromaDB + Ollama

埋め込みは Ollama の nomic-embed-text (768次元、日本語含む多言語に強い) を使います。

import chromadb
from chromadb.utils.embedding_functions import OllamaEmbeddingFunction

embedding_fn = OllamaEmbeddingFunction(
    url="http://localhost:11434/api/embeddings",
    model_name="nomic-embed-text",
)

client = chromadb.PersistentClient(path="chroma_db")
collection = client.get_or_create_collection(
    name="dev",
    embedding_function=embedding_fn,
)

完全ローカル動作 です。記録は外部に送られません。


6. 設計判断 — 「事故を再発させない」ための6つの工夫

ここからが本記事の核心です。単に検索できるだけでは事故は止まりません。実運用で気付いた6つの設計が、効きました。

6.1 Recency Boost — 最新の教訓を埋もれさせない

ベクトル検索は「クエリと近い記録」を上位に出しますが、古い無関係な記録に混ざって新しい重要な教訓が落ちる ことがありました。

対策: lesson / conclusion タイプは 「類似度上位5件 + 最新3件強制注入」 のハイブリッド取得にしました。

RECENCY_TYPES = {"lesson", "conclusion"}
RECENCY_EXTRA = 3

# Relevance: 通常のベクトル検索
for col in collections:
    res = col.query(query_texts=[query], n_results=top, where=where_clause)
    # ... all_results に追加 ...

# Recency: lesson/conclusion は日付降順で最新N件を強制補完
if effective_type in RECENCY_TYPES:
    for col in collections:
        rec = col.get(where={"source_type": {"$eq": effective_type}},
                      include=["documents", "metadatas"])
        items = sorted(rec_items, key=lambda x: x[1].get("date", ""), reverse=True)
        for doc, meta in items[:RECENCY_EXTRA]:
            # 既出でなければ all_results に強制追加 (top 制限の外でも)
            ...

ヒットした結果には [RECENT] タグが付く ので、Copilot は「これは時系列的に新しい」と認識して優先します。「お願いします」事件の教訓は 書いた日が古くなっても消えない 仕組みです。

6.2 Priority High — クリティカル教訓を最優先に

add_notepriority="high" を指定すると、その記録は検索結果で 常に最上位 にソートされます。

all_results.sort(
    key=lambda x: (0 if x[2].get("priority") == "high" else 1, x[0]),
)

実行前は必ず確認を取る」のような、絶対に守らせたいルールはこちらに格納します。「お願いします」事件のような重大事故から得た教訓は 全て priority high

6.3 session_context.md 自動エクスポート — 次セッションへの橋渡し

add_notelesson / conclusion / knowledge / profile タイプを記録した直後、自動で export_context.py が走り、最新の記録を集約した session_context.md を生成します。

def _refresh_session_context() -> None:
    """add_note 成功後に export_context.py を非同期で起動し
    session_context.md を即時更新する。"""
    if not _refresh_lock.acquire(blocking=False):
        return  # 既に実行中ならスキップ (Race Condition 対策)

    try:
        proc = subprocess.Popen(
            [str(_PYTHON), str(_EXPORT_SCRIPT)],
            cwd=str(ROOT), env=env,
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
        def _copy_after_done():
            try:
                proc.wait(timeout=30)
                src = ROOT / "context_output" / "session_context.md"
                if src.exists():
                    shutil.copy2(str(src), str(_MEMORIES_SESSION / "session_context.md"))
            finally:
                _refresh_lock.release()
        threading.Thread(target=_copy_after_done, daemon=True).start()
    except Exception:
        _refresh_lock.release()

生成された session_context.md は、GitHub Copilot Memory Tool のフォルダ に直接コピーされます。

C:\Users\...\globalStorage\github.copilot-chat\memory-tool\memories\session\session_context.md

これにより 次回 Copilot Chat を起動した瞬間 に、最新の教訓・結論が自動でロードされます。「セッションを跨ぐ記憶」が実現 したわけです。

6.4 Activity Log — 検索品質を継続改善するために

全ツール呼び出しを logs/activity.jsonl に追記します。

def _log_activity(tool: str, **kwargs) -> None:
    try:
        entry = {"ts": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "tool": tool, **kwargs}
        with _ACTIVITY_LOG.open("a", encoding="utf-8") as f:
            f.write(json.dumps(entry, ensure_ascii=False) + "\n")
    except Exception:
        pass

何を記録するか:

  • search_memory: query, type, hit count, per-type の最小距離スコア
  • add_note: type, priority, text プレビュー, solved_query(後述)

これがあると、「Copilot がどんなクエリで RAG を検索しているか」が 後から検証可能 になります。検索が空振りしているクエリパターンを見つけて、教訓の書き方を改善する PDCA が回せます。

6.5 動的閾値 (solved_query) — 「この検索で解決した」を記録

add_note の引数に solved_query: str があります。

「ユーザーから指摘を受けて修正した直後」に Copilot が add_note を呼ぶときは、その指摘を解決するきっかけになった検索クエリ を一緒に記録します。

add_note(
    text="「お願いします」を実行指示と解釈せず必ず確認を取る (24件PATCH暴走の教訓)",
    type="lesson",
    priority="high",
    solved_query="お願いします 実行 確認",
)

この情報が蓄積されると、「どのクエリで検索すれば、どの教訓に辿り着けるか」のマッピング が事後解析できます。閾値チューニングや、クエリ再構成の判断材料になります。

6.6 Atomic Lock — Race Condition から逃げ切る

_refresh_session_context は重い処理(export_context.py がサブプロセス起動して書き出し)なので、並行リクエストで複数同時起動するとレースする リスクがありました。

最初は threading.Eventis_set() / set() で排他していたのですが、TOCTOU (Time-Of-Check to Time-Of-Use) な書き方をしてしまい、瞬間的に2プロセス同時起動するケースが残りました。

対策: threading.Lockacquire(blocking=False)アトミックな Try-Lock に切り替え。

_refresh_lock = threading.Lock()

def _refresh_session_context() -> None:
    # アトミックに獲得試行。既にロック中なら即リターン。
    if not _refresh_lock.acquire(blocking=False):
        return
    try:
        # ... 重い処理 ...
    except Exception:
        _refresh_lock.release()

地味ですが、正確な排他制御 は MCP サーバーが本番運用に耐えるための必須要件でした。


7. 運用してみてどうだったか

主観評価ですが、明らかに変わった ことがあります。

7.1 「お願いします」事件後、同種の事故が再発していない

priority="high" の教訓 + [RECENT] ブーストの組み合わせで、Copilot は 議論の中で「お願いします」が出てきた瞬間 に search_memory を呼んで過去事例を引っ張ってきます。

[私] お願いします
[Copilot] (search_memory("お願いします 実行 確認") 呼び出し)
        → "lesson: 「お願いします」を実行指示と解釈せず必ず確認を取る" を取得
[Copilot] 「今から X を実行しますが、よろしいですか?」と確認

これが 何度も発火しているのを Activity Log で確認 できました。instructions.md に書いていた時とは違い、文脈に応じて 適切なタイミングで ルールが効くようになりました。

7.2 議論の結論が「次セッション」でも生きる

設計判断や運用ルールの議論をしたあと、Copilot が自発的に add_note(type="conclusion") を呼んで記録するケースが増えました。

例えば「PROJECT_STATUS.md は毎回更新しない、リリース時のみ更新する」という議論結論が出た直後、Copilot がその場で記録します。

その記録は session_context.md 経由で次セッションに自動継承されます。「先週話し合った運用ルール、何だったっけ?」をユーザーが毎回思い出す必要がなくなりました。

7.3 教訓を書く労力が消えた

instructions.md に手書きで規範を追加していた頃は、事件のたびに私が:

  1. 何が起きたか整理
  2. なぜ起きたか分析
  3. ルールに昇格させる文言を作る
  4. .instructions.md を編集
  5. Copilot に「次から守れ」と念押し

を全部やる必要がありました。

今は、ユーザーの指摘から add_note(type="lesson", priority="high") まで Copilot が自分で行います。私は会話するだけ で、教訓が蓄積される構造になりました。


8. 注意点・限界

ここからは正直な弱点も書いておきます。

8.1 embedding 品質に依存する

nomic-embed-text は日本語に強い方ですが、「お願いします」のような短文クエリ だと検索精度が落ちることがあります。文脈ごと長いクエリで検索したほうがヒット率は上がりますが、Copilot がどんなクエリを生成するかは制御しきれません。

8.2 古いノートが多いと Recency Boost が裏目に出る

lesson が累積100件超になると、最新3件が “実は古い教訓” を上回って効きすぎる ケースが出てきました。今は手動で priority を昇格させたり、古いものを knowledge に移行する運用で凌いでいます。長期的には 教訓の “現役期間” を持たせる仕組み が必要かもしれません。

8.3 自前 LLM (Ollama) のリソース消費

Ollama がローカルで動いている前提なので、メモリと CPU を持っていかれます。私の開発環境では実害ないですが、リソース制約のあるマシンだとつらいかもしれません。代わりに OpenAI API の embedding を使えば軽くなりますが、データを外に出すトレードオフがあります。

8.4 「育てた相棒」を他人と共有しづらい

この仕組みは、私の RAG(私の議論履歴・私の教訓・私の癖) で動きます。だから他の開発者が「ちょっと貸して」と使うわけにはいきません。チーム共通 RAG にすると個人色が薄れる。個別最適と共通基盤のバランス は今後の課題です。


9. AI を「育てる」仕組みの設計について

最後に、技術論を少しはみ出してお伝えしたいことがあります。

instructions.md に規範を15個書いていた時、私は Copilot を教育している つもりでした。指示書を渡して、守らせる。守らないと叱る。それは 支配的な関係 です。

RAG + MCP に切り替えてから、関係の質が変わりました。Copilot が自分で search_memory を呼んで、過去の自分の失敗を確認してから動く。add_note で議論の結論を自分で記録する。Copilot が自分の歴史を持ち、自分で参照する

これは 教育 ではなく 協働 に近い。私は規範を書く側ではなく、議論に応じる相手になりました。Copilot は命令を受ける側ではなく、自分の経験を蓄積する側になりました。

AI アシスタントを使い続けるとは、ツールを使う のではなく、相棒と一緒に育つ ことなのかもしれない。

少なくとも、私はそう思いながら毎日 Copilot と Claude Code と仕事をしています。


まとめ

  • .instructions.md の規範は 15個を超えると効かなくなる。文脈に応じて適切なルールを「引き出す」仕組みが必要。
  • RAG + MCP で Copilot に 外部記憶 を持たせることで、会話中に過去の教訓を自律的に検索・記録できる。
  • 単に検索できるだけでは不十分。Recency Boost / Priority / session_context 自動エクスポート / Activity Log / 動的閾値 / Atomic Lock の6つの設計が「事故を再発させない」ために効いた。
  • 運用してみて、「お願いします」事件後、同種の事故が再発していない。教訓を書く労力も消えた。
  • 限界はある。embedding 品質、教訓の “現役期間”、ローカルリソース、個人化の壁。
  • 関係の質が 教育から協働へ 変わった。これが一番の収穫だった気がしている。

関連記事:

これからも教訓を蓄積していきます。明日もまた、何かしら笑える失敗が起きるはずなので。

気軽にメッセージください

技術相談・ご感想・ご質問があればメッセージをお願いします。