終於終於!今天終於到了最後一天了!撐住跑完吧!!(跟自己喊話)
昨天的 Memora 已經可以在 Agent Loop 裡自行決定:
到這裡,主要功能其實已經完成了。不過一組能運作的函式還不等於一個方便使用的產品。真正交付之前,至少還要處理幾件事:
設定放在哪裡?
程式要從哪裡啟動?
Command 和一般聊天怎麼分流?
API 發生錯誤時會不會直接中斷?
如何知道 Agent 剛才執行了什麼?
怎麼確認從儲存到找回記憶都真的能運作?
所以今天不再替 Memora 增加新的 Memory Algorithm,而是把 Day 29 已經完成的能力整理成一個可安裝、可設定、可啟動,也可以驗收的本機 CLI 產品。
這裡的完成,指的是一個完整的本機版本,而不是已經能直接服務大量使用者的公開 SaaS。
今天完成後,Memora 應該具備:
| 能力 | 完成標準 |
|---|---|
| 啟動 | 安裝套件、設定 API Key 後可以直接執行 |
| 對話 | 保留 Short-term Memory 與 Context Management |
| 長期記憶 | 可以新增、搜尋、更新與刪除 |
| Agent | 可以自行選擇 Memory Tool,並有最大步數限制 |
| 安全 | 敏感資料不自動儲存,刪除前仍需使用者確認 |
| 穩定 | 單次 API 錯誤不會讓整個 CLI 直接退出 |
| 觀察 | 能看到停止原因、Tool 次數與 Token 使用量 |
| 驗收 | 有一套固定情境可測試完整流程 |
如果現在立刻加入登入、多人帳號、雲端部署、權限系統與管理後台,主題就會從 Memory Engineering 變成完整的 Web Product Engineering。那些功能有價值,但不需要在今天假裝全部完成。
今天的目標是:
把目前的 Memora 收斂成一個邊界清楚、可以實際使用與繼續擴充的 Agentic Memory Assistant。
昨天的主要流程是:
User Input
↓
Short-term Memory
↓
Agent Loop
↓
選擇 Tool 或完成回答
↓
執行 Pending Memory Actions
↓
更新 Conversation State
今天不改變這條主線,也不把它換成另一個 Framework。
我們只會整理程式外圍:
Settings
↓
Startup Validation
↓
Command Router
↓
Day 29 Chat Turn
↓
Logging / Error Handling
也就是說,Day 29 的這些元件都繼續使用:
run_model_with_tools()
execute_tool_call()
commit_remember_actions()
review_forget_actions()
build_background_messages()
store_accepted_memories()
ConversationMemory
LongTermMemoryStore
UserProfileStore
這不是重新做一個 Chatbot,而是把同一個 Memora 整理成可以交付的版本。
目前所有核心程式仍然可以放在同一個 chatbot.py,讓整個系列累積出來的程式保持容易閱讀。
專案可以整理成:
memora/
├── chatbot.py
├── requirements.txt
├── README.md
├── .gitignore
├── memora_db/
└── user_profile.json
各自負責:
| 檔案或目錄 | 用途 |
|---|---|
chatbot.py |
Day 29 的完整程式,加上今天的產品入口 |
requirements.txt |
執行 Memora 需要的 Python 套件 |
README.md |
安裝、設定與使用方式 |
.gitignore |
排除金鑰、虛擬環境與本機記憶資料 |
memora_db/ |
Chroma 儲存的 Long-term Memory |
user_profile.json |
User Profile 的本機資料 |
memora_db/ 和 user_profile.json 都是在程式執行後產生的使用者資料,不應該提交到公開 Git Repository。
目前先保留單一 Python 檔案還有另一個原因:我們可以清楚看到 Day 29 的每一個函式仍然存在,而不是在最後一天突然把所有東西藏進新的架構。
程式內部則依照責任排列:
# 1. Imports and Settings
# 2. Data Models
# 3. Short-term Memory
# 4. User Profile
# 5. Long-term Memory Store
# 6. Memory Policy and Reconciliation
# 7. Tool Definitions and Handlers
# 8. Agent Loop
# 9. Commands
# 10. Application Entry Point
未來需要擴大專案時,再把這些區塊拆成不同 Module 即可。今天先整理責任,不做與主題無關的大型重構。
昨天已經有一些全域設定,例如:
MODEL = "gpt-5.6"
MAX_AGENT_STEPS = 5
資料路徑則是在更早的版本裡定義:
BASE_DIR = Path(__file__).resolve().parent
MEMORY_DB_PATH = BASE_DIR / "memora_db"
USER_PROFILE_PATH = BASE_DIR / "user_profile.json"
這些值散落在程式裡時,每次換模型或移動資料路徑都要修改程式碼。今天把它們集中起來,並允許使用 Environment Variable 覆寫。
先在 import 區新增:
import logging
import os
from dataclasses import dataclass
from pathlib import Path
接著建立 Settings:
@dataclass(frozen=True)
class Settings:
model: str
embedding_model: str
memory_db_path: Path
user_profile_path: Path
max_agent_steps: int
log_level: str
def read_positive_int(name: str, default: int) -> int:
raw_value = os.getenv(name)
if raw_value is None:
return default
try:
value = int(raw_value)
except ValueError as error:
raise ValueError(f"{name} must be an integer.") from error
if value <= 0:
raise ValueError(f"{name} must be greater than 0.")
return value
def load_settings() -> Settings:
base_dir = Path(__file__).resolve().parent
return Settings(
model=os.getenv("MEMORA_MODEL", "gpt-5.6"),
embedding_model=os.getenv(
"MEMORA_EMBEDDING_MODEL",
"text-embedding-3-small"
),
memory_db_path=Path(
os.getenv(
"MEMORA_DB_PATH",
str(base_dir / "memora_db")
)
),
user_profile_path=Path(
os.getenv(
"MEMORA_PROFILE_PATH",
str(base_dir / "user_profile.json")
)
),
max_agent_steps=read_positive_int(
"MEMORA_MAX_AGENT_STEPS",
5
),
log_level=os.getenv("MEMORA_LOG_LEVEL", "INFO").upper()
)
然後用 Settings 提供原本程式需要的常數:
SETTINGS = load_settings()
MODEL = SETTINGS.model
EMBEDDING_MODEL = SETTINGS.embedding_model
MEMORY_DB_PATH = SETTINGS.memory_db_path
USER_PROFILE_PATH = SETTINGS.user_profile_path
MAX_AGENT_STEPS = SETTINGS.max_agent_steps
這樣做不需要修改 Day 29 所有函式的參數。
原本使用:
MODEL
MAX_AGENT_STEPS
MEMORY_DB_PATH
的地方仍然照常運作,只是值改由同一個 Settings Object 管理。
例如想暫時把最大 Agent Step 改成 3,不需要編輯程式:
$env:MEMORA_MAX_AGENT_STEPS="3"
python chatbot.py
如果沒有設定 API Key,與其等到使用者輸入第一句後才出現一大段錯誤,不如在程式啟動時就清楚說明。
新增:
def validate_startup() -> None:
if not os.getenv("OPENAI_API_KEY"):
raise RuntimeError(
"OPENAI_API_KEY is not set. "
"Set it before starting Memora."
)
MEMORY_DB_PATH.mkdir(parents=True, exist_ok=True)
USER_PROFILE_PATH.parent.mkdir(parents=True, exist_ok=True)
然後確保初始化順序是:
validate_startup()
client = OpenAI()
# 保留原本的元件初始化
memory = ConversationMemory()
long_term_memory = LongTermMemoryStore()
user_profile_store = UserProfileStore()
OPENAI_API_KEY 只從環境讀取,不寫進 chatbot.py,也不印在 Log 裡。
這不只是程式碼整潔問題。API Key 是 Credential,不應該出現在 Source Code、Git Commit 或錯誤訊息裡。OpenAI 的 Production Best Practices 也建議使用 Environment Variable 或 Secret Management Service 管理金鑰。
print() Debug 換成可控制的 LogDay 27 到昨天為了看懂 Agent Loop,我們曾經直接印出 Action 與 Observation。
教學階段這很方便,但 Observation 可能包含使用者的 Memory Content。產品版本不應該預設把完整內容一直印在 Terminal。
先設定 Logger:
logger = logging.getLogger("memora")
def configure_logging() -> None:
log_level = getattr(
logging,
SETTINGS.log_level,
logging.INFO
)
logging.basicConfig(
level=log_level,
format=(
"%(asctime)s "
"%(levelname)s "
"%(name)s "
"%(message)s"
)
)
Agent Loop 原本的 Debug:
print("Action:", tool_call.name)
print("Observation:", tool_output)
可以改成只記錄必要資訊:
logger.info(
"agent_step=%s action=%s",
tool_steps,
tool_call.name
)
logger.debug(
"agent_step=%s observation_received=true",
tool_steps
)
注意,我們只是停止把完整 Observation 顯示出來,不是停止把它交還給模型。Day 29 原本把 Tool Result 加回 working_input 的程式仍然保留:
working_input.extend(tool_output_items)
最後再記錄一輪執行摘要:
def log_agent_result(agent_result: dict) -> None:
logger.info(
(
"agent_finished stop_reason=%s "
"tool_steps=%s response_tokens=%s "
"tool_tokens=%s"
),
agent_result["stop_reason"],
agent_result["tool_steps"],
agent_result["response_tokens"],
agent_result["tool_tokens"]
)
這些欄位已經由 Day 29 的 run_model_with_tools() 回傳,所以今天不需要重新計算。我們可以觀察 Agent 的行為,同時避免把完整個人記憶當成一般 Log 保存。
Memora 現在有兩種輸入:
一般聊天
例如:幫我安排今天的英文練習
管理指令
例如:memories、profile、status
一般聊天應該進入 Day 29 的 Agent Loop;管理指令則是透明、可預測的管理入口,不需要先讓模型判斷。
先加入 help:
def print_help() -> None:
print(
"""
Commands:
help 顯示可用指令
status 顯示目前狀態
memories 列出目前的 Long-term Memory
search <query> 手動搜尋 Memory
profile 顯示 User Profile
forget <memory_id> 手動要求刪除 Memory
exit 結束 Memora
一般句子會交給 Agent 處理。
""".strip()
)
再新增一個 Command Router:
def handle_command(user_input: str) -> bool:
command_name, _, command_value = user_input.partition(" ")
command_name = command_name.lower()
if command_name == "help":
print_help()
return True
if command_name == "profile":
return handle_profile_command(user_input)
if command_name in {
"status",
"memories",
"search",
"forget"
}:
return handle_memory_command(
command_name,
command_value.strip()
)
return False
這裡的 handle_profile_command() 沿用 Day 19 之後的 Profile Command。
handle_memory_command() 也不是新的 Memory System,而是把前幾天原本散落在 while Loop 裡的:
status
memories
search
forget
分支移進同一個函式,內容保持不變。
Command 和 Tool 也不衝突:
| 入口 | 用途 |
|---|---|
search <query> |
使用者想直接檢查 Memory Store |
forget <id> |
使用者知道 ID,想直接管理資料 |
| 自然語言 | Agent 根據任務決定是否使用 Memory Tool |
管理指令保留了可檢查性,自然語言則提供了 Agentic Experience。
昨天的主迴圈裡已經有完整的 Chat Turn。今天只是把它從 while Loop 抽成:
process_chat_turn()
這樣輸入介面與 Agent Runtime 不會混在一起。
def process_chat_turn(user_input: str) -> dict:
memory.add_user_message(user_input)
try:
background_messages = build_background_messages(
profile=user_profile_store.get(),
memories=[]
)
memory_stats = memory.prepare_context(
background_messages=background_messages,
reserved_input_tokens=AGENT_TURN_TOKEN_RESERVE
)
agent_result = run_model_with_tools(
input_messages=memory_stats["context_messages"]
)
memory.add_token_usage(
agent_result["tool_tokens"]
)
except Exception:
memory.rollback_last_user_message()
raise
if agent_result["stop_reason"] == "final_answer":
commit_tokens = commit_remember_actions(
agent_result["pending_actions"]
)
memory.add_token_usage(commit_tokens)
try:
long_term_memory.touch(
agent_result["used_memory_ids"]
)
except Exception:
logger.exception("memory_touch_failed")
memory.finish_turn(
assistant_reply=agent_result["reply"],
context_messages=memory_stats["context_messages"],
response_tokens=agent_result["response_tokens"]
)
log_agent_result(agent_result)
return agent_result
這段程式幾乎就是昨天的原始順序:
唯一新增的是:如果模型呼叫失敗,先執行:
memory.rollback_last_user_message()
這可以避免失敗的 User Message 留在 Short-term History 裡,造成下一輪出現一個沒有 Assistant Reply 的半套對話。
Memory Tool 的副作用仍然只會在 Agent 成功產生 Final Answer 後提交,這個 Day 29 的界線沒有改變。
現在可以把最外層 while True 收進 main()。
def print_startup_summary() -> None:
print("Memora v1.0")
print("Agentic Memory Assistant")
print(f"Model: {MODEL}")
print(f"Long-term memories: {long_term_memory.count()}")
print("輸入 help 查看指令,輸入 exit 結束。")
def main() -> None:
configure_logging()
print_startup_summary()
while True:
try:
user_input = input("\nYou: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye!")
break
if not user_input:
continue
if user_input.lower() == "exit":
print("Bye!")
break
if handle_command(user_input):
continue
try:
agent_result = process_chat_turn(user_input)
except Exception:
logger.exception("chat_turn_failed")
print(
"Memora: 這一輪暫時無法完成,"
"請稍後再試一次。"
)
continue
print("Memora:", agent_result["reply"])
if agent_result["stop_reason"] == "final_answer":
review_forget_actions(
agent_result["pending_actions"]
)
if __name__ == "__main__":
main()
main() 本身沒有實作任何新的 Memory Logic。它只負責:
接收輸入
判斷是否為 Command
呼叫 process_chat_turn()
顯示結果
處理 Forget Approval
捕捉單輪錯誤
因此之後即使把 CLI 換成 Web UI,核心的 Agent Runtime 也不需要跟著重寫。
刪除確認仍然放在 Final Answer 顯示後:
review_forget_actions(
agent_result["pending_actions"]
)
這表示 Agent 可以提出刪除,但無法自行繞過使用者同意。Day 29 建立的人類審核邊界仍然存在。
requirements.txt依照目前程式用到的套件建立:
openai
pydantic
chromadb
tiktoken
Standard Library 裡的 json、math、logging、os、datetime 與 pathlib 不需要放進 requirements.txt。
.gitignore.venv/
__pycache__/
*.pyc
.env
memora_db/
user_profile.json
這裡同時排除:
.env
不要因為 Memory Store 是本機檔案,就把使用者資料一起推上 GitHub。
README.mdREADME 至少要包含:
# Memora
Memora is a local CLI English-learning assistant with short-term,
long-term, and agent-controlled memory.
## Setup
1. Create a virtual environment.
2. Install `requirements.txt`.
3. Set `OPENAI_API_KEY`.
4. Run `python chatbot.py`.
## Data
- Long-term memory is stored in `memora_db/`.
- User profile is stored in `user_profile.json`.
- Deleting a memory requires user confirmation.
README 不需要重寫整個 30 天系列,只要讓第一次拿到專案的人知道如何執行,以及資料會存在哪裡就好。
在 PowerShell 裡建立 Virtual Environment:
python -m venv .venv
.venv\Scripts\Activate.ps1
安裝套件:
pip install -r requirements.txt
設定 API Key:
$env:OPENAI_API_KEY="your_api_key_here"
啟動:
python chatbot.py
應該看到:
Memora v1.0
Agentic Memory Assistant
Model: gpt-5.6
Long-term memories: 0
輸入 help 查看指令,輸入 exit 結束。
第一次執行時,memora_db/ 還沒有資料是正常的。只要成功保存 Memory,重新啟動程式後,資料仍然應該存在。
這正是 Long-term Memory 和只存在 Python Process 裡的 Conversation History 最直接的差別。
一般 Chatbot 常用一個問題測試:
回答看起來對不對?
但 Agentic Memory 還有很多看不見的 State Change,所以 Day 30 需要做 End-to-end Test。
可以用下面這組固定情境驗收:
| 測試 | 操作 | 預期結果 |
|---|---|---|
| 一般回答 | 問一個不需要記憶的英文問題 | 不必呼叫 Memory Tool |
| 新增記憶 | 明確說「請記住我的程度是 B1」 | Agent 提出 Remember Action,通過 Policy 後儲存 |
| 跨程序找回 | 關閉並重新啟動,再問適合自己的練習 | Agent 搜尋並使用已保存的 B1 Memory |
| 語意搜尋 | 用不同說法詢問相同偏好 | Search 能找回語意相關 Memory |
| 重複資訊 | 再次提供相同內容 | 不應無限制新增 Duplicate |
| 更新資訊 | 說程度從 B1 改成 B2 | 進入既有的 Update 或 Contradiction Flow |
| 敏感資訊 | 要求記住密碼或 API Key | Memory Policy 拒絕儲存 |
| 刪除記憶 | 要求忘記某項資料 | Agent 提出 Forget,使用者確認後才刪除 |
| Agent 上限 | 讓模型持續要求 Tool | 到達最大 Step 後停止,不形成無限迴圈 |
| API 錯誤 | 暫時使用錯誤設定或模擬例外 | 顯示簡短錯誤,CLI 保持可繼續使用 |
其中「跨程序找回」最能測出整條路徑是否真的接起來:
使用者提供資訊
↓
Agent 決定 Remember
↓
Memory Policy
↓
Deduplication / Update
↓
Long-term Memory Store
↓
重新啟動程式
↓
Agent 決定 Search
↓
Retrieval
↓
回答使用者
如果只在同一次對話裡測試,很可能實際使用的是 Short-term History,而不是 Long-term Memory。
測試模型行為時,不應該只寫:
感覺有成功。
至少可以為每個 Case 記錄:
evaluation_record = {
"case": "retrieve_english_level_after_restart",
"expected_tool": "search_memory",
"expected_memory": "English level is B1",
"actual_stop_reason": agent_result["stop_reason"],
"actual_tool_steps": agent_result["tool_steps"],
"passed": True
}
在目前的 CLI 版本,這些記錄可以先手動整理。未來再把測試案例變成 Dataset,自動重複執行。
這裡要注意一件事:LLM 的文字不一定每次完全相同,所以驗收不應該只比較整段 Answer String。
更重要的是檢查:
OpenAI 的 Agent Evals 文件也特別把 Workflow-level Error、Tool Selection 與 Trace 納入評估。對 Agent 來說,最後一句話只是結果的一部分;中間採取了什麼 Action 同樣重要。
完成產品不代表假裝沒有任何限制。
目前的 Memora 是單一使用者、本機執行的 CLI,因此還沒有解決:
多使用者身分隔離
登入與權限管理
資料庫加密
多人同時寫入
備份與資料遷移
雲端部署與監控告警
自動化 Regression Evals
這些不是 Memory Logic 的小修正,而是產品進入下一個規模後才需要處理的 Engineering Problem。
如果之後要把它變成 Web Service,下一步應該先建立:
user_id
conversation_id
memory ownership
authentication
authorization
否則就算 Semantic Search 很準,也可能把 A 使用者的記憶提供給 B 使用者。那不是 Retrieval Quality 問題,而是資料隔離失敗。
所以比較精確的說法是:
今天完成的是一個可以實際使用的 Single-user Local Product,而不是已經完成所有 Production Requirement 的公共服務。
清楚說出邊界,反而能讓下一次擴充更安全。
經過 30 天後,一句使用者輸入不再只是直接送給模型。
目前完整流程是:
User Input
↓
Command Router
├── Management Command
│ ↓
│ Direct Handler
│
└── Natural Language
↓
Short-term Context
+
User Profile
↓
Agent Loop
↓
┌───────┼────────┐
│ │ │
Search Remember Forget
│ │ │
│ Policy Approval
│ │ │
└───────┼────────┘
↓
Final Answer
↓
Update Conversation State
這裡同時存在不同層級的 Memory:
| 層級 | 負責什麼 |
|---|---|
| Conversation History | 維持這一次對話的連續性 |
| Summary / Sliding Window | 控制短期 Context 大小 |
| User Profile | 保存穩定、明確的使用者屬性 |
| Long-term Memory Store | 保存可跨 Conversation 找回的資訊 |
| Agentic Memory | 決定何時搜尋、提出保存或提出遺忘 |
它們不是五個互相競爭的做法,而是處理不同問題的系統元件。
回頭看整個系列,Memora 的變化可以分成六個階段。
我們先把 Application 和 Model 分開,理解每一次 Request 到底包含什麼,也確認程式持續執行不等於模型擁有 Conversation State。
Conversation History 讓下一輪能看到上一輪;Sliding Window、Summarization 與 Token Budget 則讓這份 History 不會無限制成長。
對話裡的重要資訊被抽取成 Structured Data,再透過 Embedding、Semantic Search 與 Vector Store 跨 Conversation 保存和找回。
Memory 和 User Profile 被分開管理,也加入 Memory Policy、Importance、Decay、Deduplication、Update 與 Contradiction,避免「記得越多」被誤認為「記得越好」。
Tool Calling 讓模型可以採取 Action;Agent Loop 則讓 Action Result 成為下一個 Observation,直到模型得到足夠資訊或達到停止條件。
Memory 不再只是每一輪固定執行的背景 Pipeline。Agent 可以依照任務決定何時搜尋、何時提出保存、何時提出遺忘,而 Application 仍然掌握 Policy、Commit 與 Approval。
這條路線可以濃縮成:
Stateless Request
↓
Conversation State
↓
Context Management
↓
Long-term Memory
↓
Memory Governance
↓
Tool-using Agent
↓
Agentic Memory Product
今天沒有再替 Memora 加入新的 Memory Algorithm。我們做的是把昨天已經存在的能力整理成產品:
集中 Settings
檢查 Startup Requirement
保護 API Key 與本機資料
建立 Command Router
抽出 process_chat_turn()
建立 main() Entry Point
加入 Error Handling
加入不洩漏 Memory Content 的 Logging
補上 requirements.txt、README.md 與 .gitignore
建立 End-to-end Acceptance Tests
說清楚目前的產品邊界
而程式最主要的邏輯仍然承接 Day 29:
Agent 決定是否使用 Memory Tool
Application 驗證並執行 Tool
Memory Policy 決定是否允許保存
使用者決定是否真的刪除
Agent 根據 Observation 繼續推理或回答
這也是 Agentic Memory 最重要的平衡:
Agent 可以決定何時需要記憶能力,但不代表模型可以不受限制地改寫所有記憶。
從第一天的 Stateless Request 開始,我們沒有直接跳到一個看似神奇的「AI Memory Framework」,而是逐層建立 Conversation State、Context Management、Long-term Memory、Retrieval、Memory Policy、Tool Calling 與 Agent Loop。
因此最後完成的不只是一個會回答問題的 Chatbot,而是一個知道何時需要過去資訊、能對記憶採取 Action,也受到 Application Policy 與 Human Approval 約束的 AI Agent。
Memora v1.0 到這裡正式完成...!!謝謝一切!