上一篇看完 commerce-agents 的 Runtime 與 MCP,這篇來看它另一個很值得拆的設計:Memory。
假設乘客第一次跟客服說:
我長途航班通常坐走道,而且不要紅眼班機。
幾個月後又來問:
幫我找下週去東京的班機。
Agent 如果要記得前面的偏好,最簡單的方法是把以前的聊天紀錄全部找回來。
但 commerce-agents 不是這樣做。
它會把真正值得留下的資訊整理成:
{
"key": "seat_preference",
"value": "Prefers aisle seats",
"category": "preference"
}
也就是一筆 MemoryFact。
這篇只回答三個問題:
核心程式主要在:
commerce-common/
└── commerce_common/
├── memory.py
└── types.py
一筆 Memory 的資料結構很簡單:
class MemoryFact(BaseModel):
key: str
value: str
category: MemoryCategory # category 只有preference | constraint | context
updated_at: datetime | None
source_session_id: str | None
例如航空客服可能存成:
seat_preference
→ Prefers aisle seats
→ preference
overnight_flight
→ Avoids overnight flights
→ constraint
travel_party
→ Usually travels with spouse
→ context
它不是把這段原始對話整段保存。
我上次坐靠窗,
後來覺得進出很麻煩,
以後長途班機還是幫我找走道。
這是這套 Memory 最重要的地方。
commerce-agents 不是看到 User 說什麼就全部存,而是先用 Memory Extraction Prompt 判斷。
共用規則放在:
commerce-common/commerce_common/memory.py
MEMORY_EXTRACTION_TEMPLATE
它要求一筆長期 Memory 大致符合三個條件:
使用者自己明確說過
+
過一段時間仍可能成立
+
下次服務時仍可能有用
所以:
「我通常坐走道。」
→ 存
「我不搭紅眼班機。」
→ 存
「我通常和先生一起旅行。」
→ 可以存
但:
「這次改成明天晚上九點。」
→ 不存
只是這次改票條件
「CI100 現在還有三個位置。」
→ 不存
這是查詢結果
「他看起來應該比較喜歡靠窗。」
→ 不存
這是 Model 推論
Shopping Agent 再把這個通用規則具體化。
shopping-agent/core/shopping_agent/memory.py
SHOPPING_MEMORY_EXTRACTION_PROMPT
例如它允許記:
seat / room preference
size
budget
travel companion
preferred maker
但不記:
商品搜尋結果
商店政策
本次操作內容
Model 自己推論的資訊
完整流程是:
Conversation
↓
memory_model
↓
Extraction Prompt
↓
Candidate Fact
↓
validate_fact()
↓
MemoryWriteFilter
↓
duplicate check
↓
MemoryStore
它會先讀目前已經保存的 Fact,再讓 Model 判斷這次有沒有新東西。
例如目前已有:
seat_preference = Prefers aisle seats
Model 又產生:
seat_preference = Likes aisle seats
程式會把它視為同一件事,不再多存一筆。
如果原本是:
seat_preference = Prefers window seats
User 這次明確說:
以後還是幫我選走道。
新的 Fact 就沿用:
seat_preference
更新成:
Prefers aisle seats
每次 extraction 預設最多產生三筆新 Fact:
max_new_facts = 3
避免一輪聊天被拆成大量 Memory。
Repo 沒有指定一定要用哪個 Database。
它只定義:
class MemoryStore(Protocol):
Storage 要提供:
get_facts(subject_id)
upsert_facts(subject_id, facts)
search_facts(subject_id, query)
delete_fact(subject_id, key)
clear(subject_id)
purge_generation(subject_id)
Repo 內建兩個簡單版本。
測試用:
InMemoryMemoryStore
資料只存在 process 裡。
Demo 則可以用:
JsonFileMemoryStore
Retail demo 實際設定:
memory_store=JsonFileMemoryStore(
DATA_DIR / ".memory-store.json"
)
如果正式服務使用 PostgreSQL,可以自己做:
class PostgresMemoryStore:
async def get_facts(...):
...
async def upsert_facts(...):
...
async def search_facts(...):
...
底層可以只是:
memory_fact
subject_id
key
value
category
updated_at
source_session_id
它也沒有把某個 User 的全部 Fact 一次送給 Model。
先取一小部分:
tier_one()
規則是:
所有 constraint
+
最近更新的 Fact
數量由memory_tier_one_cap控制。
例如乘客有二十筆 Memory,這次可能直接帶:
Avoids overnight flights
Prefers aisle seats
Usually travels with spouse
在 Shopping Agent 的 SDK runtime 中,這些資料是在 get_preferences() 時一起加入:
facts = await self._memory.tier_one(
self._session.user_id
)
payload["saved_memory"] = [
memory_fact_payload(f)
for f in facts
]
其他較舊的資訊則等真的需要時才:
recall_memories
例如:
我爸上次坐什麼位置比較方便?
Agent 才去查相關 Memory。
因此它的讀取策略其實很簡單:
常用的重要 Fact
→ 每次帶
其他 Fact
→ 需要時才查
Repo 還有:
shopping-agent/
└── skills/
└── memory-personalization/
└── SKILL.md
負責告訴 Agent:什麼時候應該使用 Memory?
例如:
這次 User 已經明確指定需求
→ 這次的需求優先
Memory 已經在 profile 中
→ 不用再 recall
舊 Memory 只有真的會影響選擇
→ 才查
所以兩者責任很清楚:
Extraction Prompt
→ 決定「記什麼」
Memory Skill
→ 決定「什麼時候用」
commerce-agents 的 Shopping Agent 可以直接跑在 Claude Agent SDK。
建立方式例如:
options = ClaudeAgentOptions(
system_prompt=...,
mcp_servers=...,
allowed_tools=...,
tools=["Skill"],
skills=...,
model=config.model,
)
Claude Agent SDK 負責:
Agent loop
Model call
Skill
Tool call
Hook
但 Customer Memory 還是使用 Commerce 自己的:
Claude Agent SDK
│
▼
ShoppingToolset
│
▼
ShoppingToolExecutor
│
├── save_memory
└── recall_memories
│
▼
MemoryRuntime
│
▼
MemoryStore
也就是:
Claude Agent SDK
→ 負責 Agent 執行
commerce-agents
→ 負責 User Memory
Claude API 也有 Memory Tool,但抽象方式不一樣。
Claude Memory Tool 比較像:
/memories/
├── preferences.md
├── project.md
└── notes.md
Claude 可以操作這些 Memory file。
而 commerce-agents 使用的是:
subject_id
key
value
category
所以差異可以簡化成:
| Claude Memory Tool | commerce-agents |
|---|---|
| File-like Memory | Structured Fact |
| Claude 組織內容 | Schema 固定 |
| 通用 Agent Memory | Customer / Merchant Memory |
| Application 提供 storage | Application 一樣提供 storage |
對 Commerce application 來說,後者比較容易控制:
這筆 Memory 是誰的?
這個欄位能不能存?
多久過期?
是不是 constraint?
所以 Repo 沒有直接用一份自由格式的 Memory file 保存 Customer preference。
Agent SDK runtime 已經可以使用:
save_memory
recall_memories
但 README 特別標示:
Memory extraction
→ not applied here
也就是說:
User:
我通常不搭紅眼班機。
Agent 沒有主動 save_memory
SDK runtime 預設不會在 Turn 結束後,自動再問一次:
這輪有沒有值得留下的 Fact?
如果產品希望有這種 Auto Learn,Host 要在 Turn 完成後另外執行:
await toolset.memory.extract(
anthropic_client,
subject_id=user_id,
session_id=session_id,
transcript=transcript,
)
完整流程才會變成:
Claude Agent SDK
↓
Agent reply
↓
Host
↓
MemoryRuntime.extract()
↓
MemoryStore
這是使用 Agent SDK 版本時需要額外注意的地方。
如果把 Shopping Agent 改成航空客服,也不用重新設計整套 Memory。
主要只換:
MemoryStore
→ 正式 DB
Extraction Prompt + Skill
→ 航空業的記憶規則
Config / Filter
→ 保存量、期限與敏感資料
這也是這個 Repo 的 Memory 最值得參考的地方:
它不是讓 Agent 自由寫一份「記憶筆記」,而是先把對話整理成固定格式的 Fact,再由 Runtime 控制這筆 Fact 能不能被保存與使用。
shopping-agent/skills/memory-personalization/SKILL.md — Memory 使用規則shopping-agent/runtime-agent-sdk/README.md — Claude Agent SDK runtime 與 Memory 的整合方式