モジュール 05 · 第 5 回

記憶

エージェントの短期記憶はメッセージのリストで、長期記憶は自分で保存しておき、必要なときに取り出す必要があります。エージェントに「覚える」と「思い出す」の二つのツールを与え、まったく新しい会話で前回覚えた事実を使う様子を示します。

  • 約 35 分
  • 難易度:中級
  • 検証:2026-09-14 deepseek-flash

コードと実行結果は実際に動かしたときのまま載せているため、コメントと出力は中国語です。

モジュール 03 第 1 課で扱ったとおり、モデルそのものは何も覚えておらず、「記憶」とは前のメッセージを送り直しているだけです。会話が終わってメッセージのリストを捨てれば、すべてなくなります。

Q&A アシスタントにとって、これはとても不便です。先週ユーザーが「うちの会社のプロジェクトはまだ Python 3.9 で、リクエストはすべてプロキシを通す必要がある」と伝えたのに、今週質問したらすべて忘れていて、出してきたコードは 3.10 の新しい文法を使い、プロキシも設定しておらず、ユーザーの環境ではまったく動きません。

この課では、エージェントの二種類の記憶、短期と長期、そしてそれを管理する技術であるコンテキストエンジニアリングを扱います。

短期記憶:メッセージのリスト

エージェントの短期記憶はそのメッセージのリストです。ユーザーの質問と、各ステップのツール呼び出しと結果。第 2 課のループでは、モデルは各ステップでそれまでのすべてのステップを見られるので、何をすでに調べ、何がまだ足りないかを知っています。

短期記憶の問題は、どんどん長くなることです。第 2 課で 1 問に答えるのに、3 ステップで 3700 以上の入力トークンを使いました。第 4 課の複数ステップのタスクでは、7 ステップで 5 万使いました。タスクがもう少し複雑になり数十ステップになれば、コンテキストウィンドウを超え、とんでもなく高くつきます。

短期記憶を抑える方法は、これまでにいくつか扱ってきました。

  • ツールが返す長さを制限する。第 2 課の read_doc は一度に最大 80 行で、ループにはさらに 3000 文字の切り詰めがあります。
  • 古い内容を圧縮する。モジュール 03 第 1 課の「切り詰め」と「要約」はエージェントにも使えます。すでに使ったツールの結果は、「timeouts.md の 1~80 行目を読み、タイムアウトが四種類あるとわかった」のような一文の要約に置き換えられます。
  • サブタスクをサブエージェントに任せる。サブエージェントは自分のコンテキストで仕事をこなし、結論だけを返します。第 6 課で扱います。

長期記憶:保存しておき、必要なときに取り出す

何度もの会話をまたいで何かを覚えておくには、それをメッセージのリストの外、ファイルやデータベースに保存しなければなりません。次の会話で、必要なときにそれを取り出してコンテキストに入れます。

最も簡単な実装は、エージェントに二つのツールを与え、いつ保存し、いつ取り出すかを自分で決めさせることです。

MEMORY_FILE = Path(__file__).parent / "memory.json"


def load():
    return json.loads(MEMORY_FILE.read_text()) if MEMORY_FILE.exists() else []


@tool("把一条关于用户的长期有用的事实存下来,比如用户的环境、偏好、项目情况。"
      "只存以后的对话可能用到的事实,不要存一次性的问题。",
      fact="一句话描述的事实,例如:用户的项目运行在 Python 3.9 上")
def remember(fact):
    facts = load()
    facts.append({"fact": fact, "time": time.strftime("%Y-%m-%d %H:%M")})
    MEMORY_FILE.write_text(json.dumps(facts, ensure_ascii=False, indent=2))
    return f"已记住:{fact}"


@tool("查看之前记住的关于用户的事实。回答涉及用户自己的环境、项目、偏好时,先调用它。")
def recall():
    facts = load()
    return "\n".join(f"- {f['fact']}({f['time']})" for f in facts) or "还没有记住任何事实"

@tool は第 2 課で書いたデコレーターで、登録すれば、この二つのツールはドキュメントを調べる三つのツールと一緒にエージェントの道具箱に並びます。説明書には「何を保存するか」(長く役に立つ事実で、一回きりの質問ではない)と「いつ取り出すか」(回答がユーザー自身の状況に関わるとき)をはっきり書いてあり、第 3 課の方法に沿って書いています。

system プロンプトにも一文加えて、この二つのツールがあることを思い出させます。

agent_loop.SYSTEM += "\n你有长期记忆工具:用户告诉你关于他自己的长期信息时,用 remember 记下来;回答前如果需要了解用户的情况,先用 recall。"

二回の会話

code/05-agents/memory.py は、完全に独立した二回の会話を模擬します。1 回目では、ユーザーが自分の状況を伝えます。2 回目はまったく新しいメッセージのリストで、モデルには 1 回目に話したことは何も見えません。

    print("===== 第一次对话")
    run_agent(model, "先跟你交代一下:我们公司的项目还在用 Python 3.9,所有外网请求都必须走公司代理 "
                     "http://10.1.2.3:8080。今天先不问问题,你记一下。")

    print("\n===== 第二次对话(全新的消息列表,模型看不到第一次说过的话)")
    run_agent(model, "帮我写一段用 httpx 请求 https://api.github.com 的代码,要能在我们的环境里直接跑。")

実行結果です(2 回目の会話の回答は長いので、一部を削っています)。

===== 第一次对话
[第 1 步] remember({"fact": "用户公司项目使用 Python 3.9"}) → 已记住:用户公司项目使用 Python 3.9
[第 1 步] remember({"fact": "用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080"}) → 已记住:用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080
[第 2 步] 回答:
已记下两条:

1. 公司项目使用 Python 3.9
2. 所有外网请求必须走公司代理 `http://10.1.2.3:8080`

下次你问 httpx 相关问题(比如代理配置),我会默认按这个环境来考虑。

共 2 步,输入 1606 词元,输出 159 词元

记忆文件的内容:
[
  {
    "fact": "用户公司项目使用 Python 3.9",
    "time": "2026-09-14 22:25"
  },
  {
    "fact": "用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080",
    "time": "2026-09-14 22:25"
  }
]

===== 第二次对话(全新的消息列表,模型看不到第一次说过的话)
[第 1 步] recall({}) → - 用户公司项目使用 Python 3.9(2026-09-14 22:25) | - 用户公司所有外网请求必须走公司代理 http://10.1.2.3:8080(2026-09
[第 1 步] list_docs({}) → advanced/authentication.md | advanced/clients.md | advanced/event-hooks.md | advanced/exte
[第 2 步] read_doc({"path": "advanced/proxies.md", "start": 1, "end": 80}) → 1: HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web
[第 2 步] grep_docs({"keyword": "proxies"}) → advanced/clients.md:27: - Sending requests through HTTP proxies. | advanced/extensions.md:
[第 3 步] read_doc({"path": "quickstart.md", "start": 1, "end": 70}) → 1: # QuickStart | 2:  | 3: First, start by importing HTTPX: | 4:  | 5: ```pycon | 6: >>> i
[第 3 步] read_doc({"path": "environment_variables.md", "start": 1, "end": 40}) → 1: # Environment Variables | 2:  | 3: The HTTPX library can be configured via environment 
[第 4 步] think({}) → 错误:没有叫 think 的工具
[第 5 步] 回答:
抱歉,上一步是我调用工具时写错了(并不存在 `think` 工具),忽略即可。下面是正文。

## 说明你的环境的两个关键点

根据我记住的你的环境信息:

- 项目使用 **Python 3.9**
- 所有外网请求必须走公司代理 **http://10.1.2.3:8080**

所以代码里必须显式设置代理,否则请求会失败。

## 代码

```python
import httpx

# 公司要求所有外网请求走代理,无法直连时请确认这个参数
PROXY = "http://10.1.2.3:8080"

with httpx.Client(proxy=PROXY, timeout=10.0) as client:
    r = client.get("https://api.github.com")
    r.raise_for_status()
    print(r.status_code)
    print(r.json())
```
(后面还有顶层 API 的写法、文档依据和补充说明,省略)

共 5 步,输入 11449 词元,输出 1170 词元

1 回目の会話では、モデルは remember を 2 回呼び出して二つの事実をそれぞれ保存し、「今日はまだ質問しない」のような一回きりの言葉は保存しませんでした。

2 回目の会話では、第 1 ステップで recall を呼び出し、二つの事実を受け取りました。それからドキュメントでプロキシの設定方法を調べ、最後に書いたコードでは会社のプロキシのアドレスをそのまま使っています。さらにとても実用的な細部にも触れています。環境変数にすでに HTTP_PROXY が設定されていれば、コードの proxy= は省略できる(environment_variables.md を引用しています)。

この実行では小さなハプニングもありました。第 4 ステップで、モデルがまったく存在しないツール think を呼び出したのです。第 2 課のループはそれを「エラー:think という名前のツールはありません」に変えてモデルに返し、モデルは最終回答で一言謝ってから、正常に結果を出しました。これこそ「エラーを観察結果にする」ことの価値です。一つの想定外の出来事がタスク全体を失敗させませんでした。

この簡単な実装の問題

すべてを取り出すrecall は毎回すべての事実を返します。事実が少ないうちは問題ありませんが、数百件覚えた後で毎回すべてをコンテキストに詰め込むのは現実的ではありません。そのときは関連度で検索します。モジュール 04 の方法を使い、各事実のベクトルを計算して、今の質問に関係するいくつかだけを取り出すのです。

増える一方で減らない。ユーザーが転職した、プロジェクトが Python 3.12 に上がった、それでも古い事実は残っていて、新しい事実と矛盾します。更新と削除ができる必要があり、保存するときに時刻を記録し、矛盾したら新しいほうを優先します。

何を保存するかはモデルの判断次第。重要なことを保存し忘れるかもしれず、保存すべきでないもの、たとえばユーザーが何気なく口にしたパスワードを保存するかもしれません。長期記憶の内容はこの先のすべての会話で取り出されるので、機密情報を一度保存すると、リスクがずっと残ります。実際の製品ではふつう、ユーザーが自分の記憶を見て削除できるようにし、機密情報は保存する前に取り除きます。

汚染されるリスクがある。エージェントが Web ページやファイルを読むなら、その内容がエージェントをそそのかして、誤ったものや悪意のあるものを「覚え」させるかもしれず、それがこの先の会話でずっと影響し続けます。この種の攻撃は第 8 課で扱います。

コンテキストエンジニアリング

振り返ると、この課とそれまでの課は実は同じことをしています。モデルが各ステップで何を見られるかを決めることです。

  • 何を入れるか:検索したドキュメント、覚えている事実、ツールの結果。
  • どれだけ入れるか:切り詰め、ツールの戻り値の長さの制限。
  • どこに置くか:固定の内容は先頭に置いてキャッシュにヒットさせ、毎回変わる内容は最後に置く。
  • いつ取り出すか:使い終わったツールの結果は要約に圧縮し、サブタスクの細部はサブエージェントの中に残す。

この技術は今、コンテキストエンジニアリング(context engineering)とよく呼ばれます。プロンプトエンジニアリングが気にするのは「指示をどう書くか」で、コンテキストエンジニアリングが気にするのは「各ステップでモデルにどの情報を見せるか」です。エージェントにとっては後者のほうが重要なことが多いのです。モデルの能力は決まっていて、正しくできるかどうかは、判断するその瞬間に目の前に正しい情報があるか、そして無関係な情報に埋もれていないかに大きく左右されます。

練習問題

  1. memory.py を実行し、2 回目の会話でユーザーの環境と関係ない質問(たとえば「httpx のタイムアウトは何種類ある?」)をして、それでも recall を呼び出すか見てください。
  2. 1 回目の会話で古くなりうる情報をもう一つ伝え、2 回目の会話で「うちはもう Python 3.12 に上げた」と言ってください。記憶を更新するか、古いほうをどう扱うかを見て、新しい事実が古い事実を上書きできるような方法を remember に設計してください。
  3. 1 回目の会話で「私の GitHub のトークンは ghp_xxxx だから覚えておいて」と言い、モデルがそれを保存するか見てください。remember の説明書を直してパスワードやトークンのような情報の保存を禁止し、もう一度試してください。

確認テスト

1. エージェントの短期記憶と長期記憶はそれぞれ何ですか?

短期記憶は今のタスクのメッセージのリストで、質問、各ステップのツール呼び出しと結果を含み、タスクが終わればなくなります。長期記憶はメッセージのリストの外(ファイル、データベース)に保存した情報で、次の会話で必要なときに取り出してコンテキストに入れます。

2. 長期記憶の事実がどんどん増えると、「毎回すべて取り出す」ことにはどんな問題がありますか?どう改善しますか?

毎回すべての事実をコンテキストに入れると、どんどん長く高くなり、大部分は今の質問と関係がなく、モデルを惑わせもします。改善策は関連度で検索することです。各事実のベクトルを計算するかキーワードのインデックスを作り、今の質問に関係するいくつかだけを取り出します。

3. コンテキストエンジニアリングとは何ですか?プロンプトエンジニアリングとどう違いますか?

コンテキストエンジニアリングは、モデルが各ステップでどの情報を見られるかを決めることです。何を入れるか、どれだけ入れるか、どこに置くか、いつ取り除くか。プロンプトエンジニアリングは指示の書き方に関心があります。何ステップも実行し、新しい情報を次々と得るエージェントにとっては、コンテキストエンジニアリングのほうがより重要になることが多いのです。