Copilot に同じ間違いを2度させない仕組み — RAG + MCP で『議論の記憶』を持たせる設計
🔗 シリーズ目次: 本記事は 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 を育てる」という発想に共感する方
動作環境
| 項目 | バージョン / 構成 |
|---|---|
| OS | Windows 11 (PowerShell 5.1 / 7) |
| Python | 3.13 (venv) |
| ベクトル DB | ChromaDB (永続化モード) |
| 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つ:
- Copilot が自律判断で検索する — ユーザーが「過去の記録を確認して」と毎回言わなくても、Copilot が文脈から「これは関連記録ありそう」と判断して
search_memoryを呼ぶ - 記録もその場で行える — 議論の結論が出たり、ユーザーから指摘を受けた瞬間に、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_memory と add_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_note に priority="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_note で lesson / 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.Event の is_set() / set() で排他していたのですが、TOCTOU (Time-Of-Check to Time-Of-Use) な書き方をしてしまい、瞬間的に2プロセス同時起動するケースが残りました。
対策: threading.Lock の acquire(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 に手書きで規範を追加していた頃は、事件のたびに私が:
- 何が起きたか整理
- なぜ起きたか分析
- ルールに昇格させる文言を作る
.instructions.mdを編集- 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 品質、教訓の “現役期間”、ローカルリソース、個人化の壁。
- 関係の質が 教育から協働へ 変わった。これが一番の収穫だった気がしている。
関連記事:
- GitHub Copilot を1か月、Claude Code を2日使ってみた — コーディングパートナーとエージェントは、別物だった — このMCP仕組みを Claude Code 移行前に作っていた話との繋がり
これからも教訓を蓄積していきます。明日もまた、何かしら笑える失敗が起きるはずなので。