前一天我們在 Memory Extraction 與 Embedding 之間加入了:
apply_memory_policy()
現在只有符合產品範圍、有未來用途,而且沒有被 Hard Rule 擋下的 Candidate,才會進入:
Embedding
→ LongTermMemoryStore
但通過 Memory Policy 的資料,重要程度仍然可能完全不同。
例如:
使用者希望在三個月後通過 B2 檢定。
以及:
使用者今天完成了五題單字練習。
兩筆資料都可能值得保存,卻不代表它們在未來應該具有相同優先順序。
第一筆會影響未來數個月的學習安排;第二筆則比較像一次學習進度。當兩者都和目前問題有關,而 Context 又只能放入有限數量的 Memory 時,Memora 應該優先取回哪一筆?
今天要替通過 Policy 的 Memory 加上:
importance_score
讓 Long-term Memory 不再只有「保存或拒絕」,還能表示:
這筆資料對未來的英文學習有多重要?
先把目前出現過的幾種判斷分開:
| 判斷 | 回答的問題 | 產生時間 |
|---|---|---|
memory_type |
這是 Semantic 還是 Episodic Memory? | Memory Extraction |
should_store |
這筆資料是否允許保存? | Memory Policy |
importance_score |
通過 Policy 後,它有多重要? | 寫入前 |
score |
它和目前 Query 有多相似? | Retrieval 時 |
例如:
使用者今天完成了五題英文練習。
可能得到:
memory_type = episodic
should_store = true
importance_score = 2
這代表它是一筆允許保存、但重要度較低的 Episodic Memory。
另一筆:
使用者希望在三個月後通過 B2 檢定。
可能是:
memory_type = semantic
should_store = true
importance_score = 5
Memory Policy 決定它能不能進入 Store;Importance Score 則描述它進入 Store 後的相對價值。因此 Importance 不是另一個 Allow/Reject Gate。即使分數只有 1,只要已經通過 Policy,目前仍然可以保存。
Day 18 已經透過 Embedding 計算 Semantic Similarity。
假設目前 Query 是:
幫我安排 B2 檢定的讀書計畫。
下面這筆 Memory 可能同時具有高 Importance 與高 Relevance:
使用者希望在三個月後通過 B2 檢定。
但如果 Query 改成:
幫我複習上次的機場英文。
同一筆 Memory 雖然仍然很重要,和這次問題的直接關聯卻沒有那麼高。
兩者最大的差別是:
Importance
不依賴目前 Query
在 Memory 建立時產生
Relevance
依賴目前 Query
每次 Retrieval 時重新計算
所以不能只依照 Importance 取回資料,否則最重要的 Memory 可能每一輪都被放進 Context,即使它和目前問題無關。
Importance 並不是客觀真理,它取決於 Application 的目的。Memora 是英文學習助手,所以今天使用 1~5 分:
| 分數 | 定義 | 例子 |
|---|---|---|
| 1 | 未來價值很低,但仍通過 Policy | 使用者完成一次很小的練習 |
| 2 | 可能在短期內有幫助 | 使用者今天練習過機場單字 |
| 3 | 對後續學習有明確用途 | 使用者經常混淆兩個文法 |
| 4 | 會持續影響教學安排 | 使用者主要想加強商務英文 |
| 5 | 核心且長期的學習資訊 | 使用者預計三個月後參加 B2 檢定 |
這個分數衡量的是:
這筆 Memory 對未來英文學習的預期價值。
它不是:
內容有多感人
模型有多確定
資料有多敏感
句子有多長
也不能直接根據 memory_type 決定。
Semantic Memory 不一定比較重要:
使用者偶爾喜歡看英文迷因。
可能只有 2 分。
Episodic Memory 也不一定比較不重要:
使用者第一次成功完成全英文面試練習。
可能得到 4 分。
Day 21 完成後的 Write Path 是:
Memory Extraction
→ MemoryCandidate
→ Memory Policy
→ accepted_memories
→ Embedding
→ Chroma
今天要把 Importance Scoring 放在:
Memory Policy
與
Embedding
之間
變成:
Memory Extraction
→ MemoryCandidate
→ Memory Policy
→ accepted_memories
→ Importance Scoring
→ ScoredMemoryCandidate
→ Embedding
→ EmbeddedMemoryCandidate
→ Chroma
這個位置有兩個好處。
第一,被 Policy 拒絕的 Candidate 不需要浪費一次 Importance 評分。
第二,Importance Score 可以在建立 Embedding 前加入資料物件,接著和 memory_type 一樣一路傳到 Chroma Metadata。
MemoryCandidateDay 21 的 MemoryCandidate 已經負責:
抽取內容
判斷類型
提供 Policy Recommendation
原本是:
class MemoryCandidate(BaseModel):
content: str
memory_type: ExtractedMemoryType
should_store: bool
policy_reason: MemoryPolicyReason
今天不直接替它加入 importance_score。如果直接家的話連被 Policy 拒絕的 Candidate 都必須產生 Importance,資料階段也會混在一起。因此新增一個通過 Policy 後才會建立的 Model:
class ScoredMemoryCandidate(BaseModel):
content: str
memory_type: ExtractedMemoryType
importance_score: int = Field(
ge=1,
le=5
)
資料流會更清楚:
MemoryCandidate
尚未確定能不能保存
ScoredMemoryCandidate
已通過 Policy,並完成重要度評分
先把原本的 Pydantic Import:
from pydantic import BaseModel
改成:
from pydantic import BaseModel, Field
Field(ge=1, le=5) 會限制分數只能介於 1~5。
為了讓 LLM 一次評估多筆 Memory,先建立兩個 Output Models:
class ImportanceAssessment(BaseModel):
candidate_index: int = Field(ge=0)
importance_score: int = Field(
ge=1,
le=5
)
class ImportanceAssessmentResult(BaseModel):
assessments: list[ImportanceAssessment]
這裡使用 candidate_index,而不是要求模型把整段 Memory 原文再輸出一次。
例如 Application 傳入:
[
{
"candidate_index": 0,
"content": "The user wants to pass B2 in three months.",
"memory_type": "semantic"
},
{
"candidate_index": 1,
"content": "The user completed five vocabulary questions today.",
"memory_type": "episodic"
}
]
模型只需要回傳:
{
"assessments": [
{
"candidate_index": 0,
"importance_score": 5
},
{
"candidate_index": 1,
"importance_score": 2
}
]
}
真正的 content 與 memory_type 仍然由 Application 保存,不讓模型在評分過程中重新改寫 Memory。
新增一個專門評估重要度的 Prompt:
IMPORTANCE_PROMPT = """
You evaluate the importance of long-term memories for
a personal English learning assistant.
Each candidate has already passed the memory policy.
Do not decide whether it should be stored again.
Assign one importance_score from 1 to 5:
1:
Very low future value. A minor learning event that is
unlikely to affect future assistance.
2:
Limited future value. It may help with a nearby follow-up
conversation.
3:
Clearly useful learning context, such as a recurring
difficulty or meaningful progress.
4:
Important information that should influence future
teaching or learning plans.
5:
Core long-term information, such as a major learning goal,
an upcoming examination, or an enduring learning need.
Importance is not semantic relevance to the current query.
Importance is not confidence, sensitivity, or memory type.
A semantic memory is not automatically important.
An episodic memory is not automatically unimportant.
An explicit request to remember something does not
automatically receive a score of 5.
Return exactly one assessment for every candidate_index.
Preserve each candidate_index.
"""
這個 Prompt 刻意不再判斷:
should_store
policy_reason
因為那些工作已經在 Day 21 完成。
每個 Function 只處理一個明確責任:
apply_memory_policy()
→ 能不能存
score_memory_importance()
→ 有多重要
score_memory_importance()Day 19 的 UserProfileStore 已經使用 json。如果程式目前仍然放在同一個檔案,可以直接沿用:
import json
接著新增:
def score_memory_importance(
candidates: list[MemoryCandidate]
) -> tuple[list[ScoredMemoryCandidate], int]:
if not candidates:
return [], 0
candidate_payload = [
{
"candidate_index": index,
"content": candidate.content,
"memory_type": candidate.memory_type
}
for index, candidate in enumerate(candidates)
]
response = client.responses.parse(
model=MODEL,
instructions=IMPORTANCE_PROMPT,
input=json.dumps(
candidate_payload,
ensure_ascii=False
),
text_format=ImportanceAssessmentResult
)
result = response.output_parsed
if result is None:
raise RuntimeError(
"Importance assessment returned no result."
)
assessments_by_index = {}
for assessment in result.assessments:
candidate_index = assessment.candidate_index
if candidate_index in assessments_by_index:
raise ValueError(
"Duplicate candidate_index in "
"importance assessment."
)
assessments_by_index[candidate_index] = (
assessment.importance_score
)
expected_indexes = set(
range(len(candidates))
)
if (
set(assessments_by_index)
!= expected_indexes
):
raise ValueError(
"Importance assessment does not match "
"the input candidates."
)
scored_memories = [
ScoredMemoryCandidate(
content=candidate.content,
memory_type=candidate.memory_type,
importance_score=(
assessments_by_index[index]
)
)
for index, candidate in enumerate(candidates)
]
return (
scored_memories,
response.usage.total_tokens
)
這裡除了使用 Structured Output 限制分數範圍,也檢查:
是否少評一筆
是否多出不存在的 Index
是否出現重複 Index
假設輸入有三筆 Candidate,模型卻只回傳 0 和 1,Application 不會靜默把第三筆弄丟,而是直接拋出錯誤。
EmbeddedMemoryCandidateDay 20 的 EmbeddedMemoryCandidate 是:
class EmbeddedMemoryCandidate(BaseModel):
content: str
memory_type: ExtractedMemoryType
embedding: list[float]
今天加入:
class EmbeddedMemoryCandidate(BaseModel):
content: str
memory_type: ExtractedMemoryType
importance_score: int = Field(
ge=1,
le=5
)
embedding: list[float]
接下來建立 Embedding Object 時,從 ScoredMemoryCandidate 取得分數:
embedded_memories = [
EmbeddedMemoryCandidate(
content=memory_item.content,
memory_type=memory_item.memory_type,
importance_score=(
memory_item.importance_score
),
embedding=embedding
)
for memory_item, embedding in zip(
scored_memories,
memory_embeddings
)
]
目前欄位會依序流動:
ScoredMemoryCandidate.importance_score
→ EmbeddedMemoryCandidate.importance_score
→ Chroma Metadata
Day 21 已經要求自動抽取與手動 remember 共用相同的 Policy。
今天可以再把通過 Policy 後的流程整理成:
def store_accepted_memories(
candidates: list[MemoryCandidate],
source: str
) -> tuple[list[str], int]:
if not candidates:
return [], 0
(
scored_memories,
importance_tokens
) = score_memory_importance(candidates)
memory_contents = [
memory_item.content
for memory_item in scored_memories
]
(
memory_embeddings,
embedding_tokens
) = create_embeddings(memory_contents)
if (
len(memory_embeddings)
!= len(scored_memories)
):
raise RuntimeError(
"Embedding count does not match "
"the number of memories."
)
embedded_memories = [
EmbeddedMemoryCandidate(
content=memory_item.content,
memory_type=memory_item.memory_type,
importance_score=(
memory_item.importance_score
),
embedding=embedding
)
for memory_item, embedding in zip(
scored_memories,
memory_embeddings
)
]
memory_ids = long_term_memory.add(
memories=embedded_memories,
source=source
)
total_tokens = (
importance_tokens
+ embedding_tokens
)
return memory_ids, total_tokens
這個 Function 沒有取代:
score_memory_importance()
create_embeddings()
LongTermMemoryStore.add()
它只是把三個既有步驟串在一起,讓自動與手動寫入不必複製相同程式。
accepted_memories自動 Memory Extraction 完成後,Day 21 已經有:
(
accepted_memories,
rejected_memories
) = apply_memory_policy(memory_candidates)
原本後面直接建立 Embedding。今天改成:
(
stored_memory_ids,
storage_tokens
) = store_accepted_memories(
candidates=accepted_memories,
source="automatic"
)
memory.add_token_usage(storage_tokens)
如果 accepted_memories 是空 List,Function 會直接回傳:
[], 0
因此不會呼叫 Importance API、Embeddings API 或 Chroma。
手動 remember 通過 Policy 後,也走同一個 Function:
(
accepted_memories,
rejected_memories
) = apply_memory_policy([memory_candidate])
if not accepted_memories:
print(
"Memory rejected by policy:",
rejected_memories[0].policy_reason
)
continue
(
stored_memory_ids,
storage_tokens
) = store_accepted_memories(
candidates=accepted_memories,
source="manual"
)
memory.add_token_usage(storage_tokens)
print(
f"Stored {len(stored_memory_ids)} memory."
)
現在兩個入口會共用:
Memory Policy
→ Importance Scoring
→ Embedding
→ LongTermMemoryStore
手動 remember 不會自動得到 5 分,而是使用相同標準評估。
接著修改 Day 20 的 LongTermMemoryStore.add()。
原本 Metadata 是:
{
"source": source,
"created_at": created_at,
"memory_type": memory.memory_type
}
現在加入:
{
"source": source,
"created_at": created_at,
"memory_type": memory.memory_type,
"importance_score": (
memory.importance_score
)
}
完整位置仍然在原本的 metadatas:
self.collection.add(
ids=memory_ids,
documents=[
memory.content
for memory in memories
],
embeddings=[
memory.embedding
for memory in memories
],
metadatas=[
{
"source": source,
"created_at": created_at,
"memory_type": memory.memory_type,
"importance_score": (
memory.importance_score
)
}
for memory in memories
]
)
一筆新的 Chroma Record 現在可能是:
Document
The user wants to pass B2 in three months.
Embedding
[0.021, -0.037, ...]
Metadata
source = automatic
created_at = 2026-09-12T03:20:00+00:00
memory_type = semantic
importance_score = 5
Importance 不需要放進 Embedding。
Embedding 表示文字語意;Importance 是 Application 要使用的 Metadata。
Day 21 以前寫入的 Record 沒有:
importance_score
因此新增:
DEFAULT_IMPORTANCE_SCORE = 3
以及:
def read_importance_score(
metadata: dict
) -> int:
value = metadata.get(
"importance_score"
)
if isinstance(value, bool):
return DEFAULT_IMPORTANCE_SCORE
if isinstance(value, (int, float)):
score = int(value)
if (
value == score
and 1 <= score <= 5
):
return score
return DEFAULT_IMPORTANCE_SCORE
這裡使用 3 作為中立值。
它不代表舊 Memory 真的被評為 3 分,只是避免所有舊資料因為缺少欄位而無法讀取。
接著修改兩個讀取 Models:
class StoredMemory(BaseModel):
memory_id: str
content: str
memory_type: StoredMemoryType
importance_score: int = Field(
ge=1,
le=5
)
source: str
created_at: str
class MemorySearchResult(BaseModel):
memory_id: str
content: str
memory_type: StoredMemoryType
importance_score: int = Field(
ge=1,
le=5
)
source: str
created_at: str
distance: float
score: float
在 list_all() 建立 StoredMemory 時加入:
importance_score=read_importance_score(
metadata
)
search() 建立 MemorySearchResult 時也加入相同欄位。
如此一來:
Day 22 新資料
→ 使用實際 Importance Score
Day 21 以前的舊資料
→ 暫時使用預設值 3
不需要刪除原本的 Chroma Collection。
memories Command 可以多印一行:
print(
f" Importance: "
f"{stored_memory.importance_score}/5"
)
結果可能是:
1. 使用者希望三個月後通過 B2 檢定。
Type: semantic
Importance: 5/5
Source: automatic
2. 使用者今天完成五題單字練習。
Type: episodic
Importance: 2/5
Source: automatic
3. 使用者偏好簡短例句。
Type: unclassified
Importance: 3/5
Source: automatic
第三筆的 3/5 可能只是舊資料的 Compatibility Default,所以除錯時不能把它解讀成真正的 LLM 評分結果。
search <query> 也可以顯示:
print(
f" Importance: "
f"{search_result.importance_score}/5"
)
目前 search Command 仍然可以維持原本的 Vector Search 順序;真正的綜合排序會放在自動 Retrieval 的 Application Layer。
目前 Day 18 的 Retrieval 主要依照:
result.score
也就是 Semantic Similarity。
今天先加入 Importance,但不加入 Day 23 才要討論的 Recency。
先定義:
RETRIEVAL_CANDIDATE_K = 8
RETRIEVAL_LIMIT = 3
RELEVANCE_WEIGHT = 0.8
IMPORTANCE_WEIGHT = 0.2
Day 18 原本可能只取得三筆結果:
RETRIEVAL_TOP_K = 3
今天將它拆成:
先從 Chroma 取得 8 筆 Candidates
再重新排序並保留 3 筆
如果一開始只向 Chroma 取得三筆,Application 就沒有足夠的 Candidates 可以重新排序。
Semantic Similarity 與 Importance 的分數範圍不同:
Similarity
大致使用 0~1
Importance
使用 1~5
不能直接相加:
0.8 + 5
否則 Importance 會完全蓋過 Similarity。
先將 Importance 轉成 0~1:
def normalize_importance(
importance_score: int
) -> float:
return (
importance_score - 1
) / 4
轉換結果是:
| Importance | Normalized |
|---|---|
| 1 | 0.00 |
| 2 | 0.25 |
| 3 | 0.50 |
| 4 | 0.75 |
| 5 | 1.00 |
接著建立綜合分數:
def calculate_retrieval_score(
result: MemorySearchResult
) -> float:
relevance_score = max(
0.0,
min(1.0, result.score)
)
importance_score = normalize_importance(
result.importance_score
)
return (
RELEVANCE_WEIGHT
* relevance_score
+ IMPORTANCE_WEIGHT
* importance_score
)
目前使用:
Relevance = 80%
Importance = 20%
這不是通用最佳比例,只是方便觀察效果的起始值。真正的權重需要使用實際 Query 與 Memory 測試後再調整。
這裡有一個很重要的順序。
不能直接讓高 Importance 補掉極低的 Relevance,否則:
使用者三個月後要考 B2
可能在每一個不相關問題中都被取回。
因此 Day 18 的 Semantic Threshold 仍然保留。
修改 retrieve_relevant_memories():
def retrieve_relevant_memories(
query: str
) -> tuple[list[MemorySearchResult], int]:
(
candidates,
embedding_tokens
) = semantic_search(
query=query,
top_k=RETRIEVAL_CANDIDATE_K
)
relevant_candidates = [
result
for result in candidates
if result.score
>= RETRIEVAL_MIN_SCORE
]
ranked_memories = sorted(
relevant_candidates,
key=calculate_retrieval_score,
reverse=True
)
return (
ranked_memories[
:RETRIEVAL_LIMIT
],
embedding_tokens
)
流程是:
Chroma Vector Search
→ 取得 8 筆 Candidates
→ 排除不相關結果
→ 計算 Relevance + Importance
→ 重新排序
→ 取前 3 筆
LongTermMemoryStore.search() 仍然只負責 Vector Database 查詢。
Importance Re-ranking 放在:
retrieve_relevant_memories()
因為它屬於 Application Retrieval Policy,不需要塞進 Database Class。
假設取得三筆 Candidates:
| Memory | Similarity | Importance |
|---|---|---|
| A:最近做過機場單字練習 | 0.82 | 2 |
| B:三個月後要參加 B2 檢定 | 0.70 | 5 |
| C:長期目標是通過 B2 檢定 | 0.20 | 5 |
Memory A 的計算是:
Importance 2
→ Normalized Importance = 0.25
Retrieval Score
= 0.8 × 0.82 + 0.2 × 0.25
= 0.706
Memory B:
Importance 5
→ Normalized Importance = 1.00
Retrieval Score
= 0.8 × 0.70 + 0.2 × 1.00
= 0.760
因此 B 的 Semantic Similarity 雖然略低,加入 Importance 後可能排在 A 前面。但 Memory C 的 Similarity 只有 0.20。如果目前的:
RETRIEVAL_MIN_SCORE = 0.45
它會在 Re-ranking 前被排除。所以高 Importance 只能調整「已經相關」的 Candidates,不能讓完全不相關的 Memory 強行進入 Context。
假設 Store 裡有一筆錯誤 Memory:
使用者的英文程度是 C1。
而它的 Importance 是:
5
這不代表它有 100% 機率正確,只代表 Application 認為「英文程度」這類資訊很重要。
因此以下概念不能混在一起:
Importance
這筆資料有多值得被使用
Confidence
這筆資料有多可信
Correctness
這筆資料是否符合現在的事實
如果重要但錯誤的 Memory 被頻繁取回,反而可能造成更嚴重的影響。這也是為什麼 Day 24 還需要處理:
Deduplication
Update
Contradiction
今天只增加排序訊號,不假設 Store 中的內容永遠正確。
今天的 Importance Score 在 Memory 建立時產生,寫入後暫時維持不變。
例如:
使用者下週要參加英文面試。
建立時可能得到:
importance_score = 5
但面試結束半年後,這筆 Memory 對目前任務的價值可能已經降低。
Importance 本身無法表達這種時間變化。
因此明天會再加入:
Recency
Memory Decay
Forgetting
目前的 Retrieval Ranking 只有:
Relevance + Importance
Day 23 才會變成:
Relevance + Importance + Recency
今天不先加入時間公式,避免 Importance 和 Recency 的責任混在一起。
可以先輸入:
You:
請記住,我希望三個月後通過 B2 檢定。
預期流程是:
Memory Extraction
→ semantic
Memory Policy
→ accepted
→ explicit_request
Importance Scoring
→ 5
Embedding
→ 建立向量
Chroma
→ 寫入 importance_score = 5
接著輸入:
You:
我今天完成了五題單字練習。
可能得到:
memory_type = episodic
importance_score = 2
再輸入:
You:
memories
確認新資料都有:
Type
Importance
Source
Created at
最後可以測試:
You:
幫我安排接下來的英文學習。
觀察 retrieve_relevant_memories() 是否先排除不相關內容,再讓重要的學習目標在相關 Candidates 中獲得較高排序。
由於 Importance 是 LLM 評分,不保證每次都完全一致。真正的 Application 應準備固定測試集,例如二十到五十筆不同 Memory,觀察不同 Prompt 與模型版本的分數分布,而不是只測一個例子。
今天沒有重新設計 Day 21 的 Memory Policy,也沒有更換 Embedding、Chroma 或 User Profile。
沿用原本的:
MemoryCandidate
apply_memory_policy()
accepted_memories
create_embeddings()
LongTermMemoryStore
retrieve_relevant_memories()
build_background_messages()
新增的是:
ImportanceAssessment
ImportanceAssessmentResult
ScoredMemoryCandidate
importance_score
score_memory_importance()
store_accepted_memories()
calculate_retrieval_score()
資料寫入流程現在是:
Memory Extraction
→ Memory Policy
→ Importance Scoring
→ Embedding
→ Long-term Memory Store
資料讀取流程則是:
Semantic Search
→ Relevance Threshold
→ Relevance + Importance
→ Re-ranking
→ Short-term Context
今天最重要的觀念是:
Importance Score 不是用來取代 Relevance,而是讓多筆都與目前問題相關的 Memory,能依照長期價值重新排序。
現在 Memora 已經能回答三個問題:
這是哪一種 Memory?
這筆 Memory 能不能保存?
通過 Policy 後,它有多重要?
但重要的 Memory 不一定永遠重要,普通的 Memory 也不一定永遠沒有用。時間會持續改變一筆資料應該被想起的機會。
下一篇會從今天的 Retrieval Ranking 繼續修改,加入:
last_accessed_at
Recency Score
Memory Decay
Forgetting Condition
到時候要處理的是:
剛剛使用過的 Memory 是否應該比較容易再次被取回?
很久沒有使用的資料應該如何降低權重?
高 Importance 能不能減緩衰退?
Forgetting 是降低分數,還是真的刪除資料?
今天讓 Memory 有不同的重要程度;明天則要讓這些分數開始受到時間影響。