模組 05 · 第 5 課

記憶

智慧體的短期記憶就是訊息列表,長期記憶要自己存下來、需要時再取出。給智慧體加上"記住"和"回想"兩個工具,演示它在一次全新的對話裡用上了上次記住的事實。

  • 約 35 分鐘
  • 難度:進階
  • 實測:2026-09-14 deepseek-flash

程式碼和執行結果保留原樣(簡體中文),與實際執行時完全一致。

第 03 模組第 1 課講過,模型本身什麼都不記得,"記憶"只是把之前的訊息重新發一遍。對話結束,訊息列表一扔,一切都沒了。

對一個答疑助手來說,這很不方便。使用者上週告訴過它"我們公司的專案還在用 Python 3.9,所有請求都要走代理",這週再問問題,它全忘了,給出的程式碼用了 3.10 的新語法,也沒有配代理,在使用者的環境里根本跑不起來。

這一課講智慧體的兩種記憶:短期的和長期的,以及管理它們的一門手藝,叫上下文工程。

短期記憶:訊息列表

智慧體的短期記憶就是它的訊息列表:使用者的問題、每一步的工具呼叫和結果。第 2 課的迴圈裡,模型每一步都能看到之前所有的步驟,所以它知道已經查過什麼、還缺什麼。

短期記憶的問題是會越來越長。第 2 課回答一個問題,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 模擬兩次完全獨立的對話。第一次,使用者交代自己的情況;第二次是一個全新的訊息列表,模型看不到第一次說過的任何話:

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

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

執行結果(第二次對話的回答較長,刪減了一部分):

===== 第一次对话
[第 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 词元

第一次對話,模型呼叫了兩次 remember,把兩條事實分別存下來,沒有把"今天先不問問題"這種一次性的話也存進去。

第二次對話,它第一步就呼叫了 recall,拿到了這兩條事實。然後去文件裡查怎麼配置代理,最後寫出的程式碼直接用上了公司的代理地址。它還提到了一個很實用的細節:如果環境變數裡已經設定了 HTTP_PROXY,程式碼裡的 proxy= 可以省略(它引用了 environment_variables.md)。

這次執行裡還出了一個小插曲:第 4 步,模型呼叫了一個根本不存在的工具 think。第 2 課的迴圈把它變成了一條"錯誤:沒有叫 think 的工具"交還給模型,模型在最終回答裡道了個歉,然後正常給出了結果。這正是"把錯誤變成觀察結果"的價值:一個意外沒有讓整個任務失敗。

這個簡單實現的問題

全部取出recall 每次把所有事實都返回。事實少的時候沒問題;記了幾百條之後,每次都全部塞進上下文就不現實了。那時要按相關性檢索:用第 04 模組的辦法,給每條事實算向量,只取出和當前問題相關的幾條。

只增不減。使用者換了工作、專案升級到了 Python 3.12,舊的事實還在裡面,而且和新的事實互相矛盾。需要能更新和刪除,存的時候記下時間,衝突時以新的為準。

存什麼全靠模型判斷。它可能漏存重要的事,也可能把不該存的存下來,比如使用者隨口說的密碼。長期記憶裡的內容會在以後的每次對話中被取出,一旦存了敏感資訊,風險會一直存在。實際產品裡,通常要讓使用者能看到、能刪除自己的記憶,敏感資訊在存之前就要過濾掉。

有被汙染的風險。如果智慧體會讀網頁、讀檔案,那裡面的內容可能誘導它"記住"一些錯誤的或者惡意的東西,這些東西會在以後的對話裡一直影響它。第 8 課會講這類攻擊。

上下文工程

回頭看,這一課和前面幾課其實在做同一件事:決定模型每一步能看到什麼

  • 放什麼進去:檢索到的文件、記住的事實、工具的結果。
  • 放多少:截斷、限制工具返回的長度。
  • 放在哪裡:固定的內容放在開頭,能命中快取;每次變化的內容放在最後。
  • 什麼時候拿出去:用過的工具結果壓縮成摘要,子任務的細節留在子智慧體裡。

這門手藝現在常被叫作上下文工程(context engineering)。提示詞工程關注的是"怎麼寫指令",上下文工程關注的是"每一步給模型看哪些資訊"。對智慧體來說,後者往往更重要:模型的能力是固定的,它能不能做對,很大程度上取決於它在做決定的那一刻,眼前有沒有正確的資訊,以及有沒有被太多無關的資訊淹沒。

練習

  1. 執行 memory.py,然後在第二次對話裡問一個和使用者環境無關的問題(比如"httpx 的超時有幾種"),看看它還會不會呼叫 recall
  2. 在第一次對話裡再告訴它一條會過時的資訊,然後在第二次對話裡說"我們已經升級到 Python 3.12 了"。看看它會不會更新記憶,舊的那條怎麼處理。給 remember 設計一個辦法,讓新事實能覆蓋舊事實。
  3. 在第一次對話裡說"我的 GitHub token 是 ghp_xxxx,記一下",看看模型會不會把它存下來。修改 remember 的說明書,禁止它存密碼、token 這類資訊,再試一次。

自測

1. 智慧體的短期記憶和長期記憶分別是什麼?

短期記憶是當前任務的訊息列表,包括問題、每一步的工具呼叫和結果,任務結束就沒有了。長期記憶是存到訊息列表之外(檔案、資料庫)的資訊,下次對話時在需要的時候取出來放進上下文。

2. 長期記憶裡的事實越來越多之後,"每次全部取出"會有什麼問題?怎麼改進?

每次都把所有事實放進上下文,會越來越長、越來越貴,大部分內容和當前問題無關,還會干擾模型。改進辦法是按相關性檢索:給每條事實算向量或者建關鍵詞索引,只取出和當前問題相關的幾條。

3. 什麼是上下文工程?它和提示詞工程有什麼區別?

上下文工程是決定模型每一步能看到哪些資訊:放什麼、放多少、放在哪裡、什麼時候移除。提示詞工程關注怎麼寫指令。對需要多步執行、不斷獲取新資訊的智慧體來說,上下文工程往往更關鍵。

提問與討論

這一課沒看懂的地方,在這裡問。看到別人的問題,也歡迎你來回答。

提問 +3 點,回答別人 +6 點。內容經審核後公開。

正在載入討論…