昨天講了 Write——把不急著用的資訊先存到 context window 之外。但存起來的東西,需要在對的時機被拿回來用。今天要談的 Select,處理的正是這個問題:什麼時候、該挑哪些內容,放回這一輪的 context 裡。
Select 的核心動作是「從已經存起來的資訊裡,挑出這次真正需要的部分,拉回目前的 context」。這跟單純「把所有存過的東西都塞回去」不一樣,如果昨天的 scratchpad 存了 50 筆筆記、長期記憶存了使用者過去三個月的所有偏好,不會希望每次呼叫都把這 50 筆筆記、三個月的紀錄全部塞進 prompt,那樣只是把 Write 解決掉的問題,又原封不動地搬回來而已。
Select 常見會發生在幾種情境:
任務進行到一半,可能只需要讀回「跟目前這一步相關」的那幾筆暫存筆記,而不是整份 scratchpad:
def select_relevant_notes(scratchpad, task_id, current_step):
all_notes = scratchpad.get(task_id, [])
# 實務上可能會用關鍵字比對、或再呼叫一次模型判斷相關性
return [note for note in all_notes if current_step in note]
長期記憶裡可能累積了大量跟使用者相關的資訊,但這次對話未必每一項都用得到。挑選的方式通常分幾種:情境式的(這次任務曾經處理過的具體案例)、規則式的(固定要遵守的偏好或限制)、事實式的(單純的背景資訊)。實際該挑哪一種、挑多少,取決於這次任務的性質。
Select 不只用在挑資料,也用在挑工具。如果工具太多、工具描述彼此重疊(例如同時有「查詢訂單」「查詢訂單明細」「查詢訂單狀態」這幾個名稱相近的工具),模型光是要從選項裡挑工具,就容易搞混。
解法概念上跟RAG很像:把工具描述當成被檢索的內容,先用語意搜尋,從全部工具裡挑出跟這次任務最相關的幾個,只把這幾個工具的定義送進 tools 參數,而不是每次都把全部工具塞給模型:
def select_relevant_tools(all_tools, user_query, top_k=5):
# 概念示意:依照 user_query 跟工具描述的語意相似度排序,
# 只挑出最相關的幾個工具定義送給模型
scored = [(tool, similarity(user_query, tool["description"])) for tool in all_tools]
scored.sort(key=lambda pair: pair[1], reverse=True)
return [tool for tool, _ in scored[:top_k]]
這其實就是把 RAG 的概念,套用在「挑選工具」這件事上,只是被檢索的對象從文件片段換成了工具描述。有研究指出,這種做法能明顯改善模型選對工具的準確率,工具數量越多、描述越相似,這個問題就越明顯。
有些問題,模型自己知道的知識並不夠用,需要額外查詢外部資料,例如公司的內部文件、即時更新的資訊。這種「先判斷這次需要什麼資料、再去檢索、把檢索到的片段當成這次的 context 餵給模型」的做法,也是 Select 的一種,只是換成從外部知識庫裡挑,而不是從自己存的 scratchpad 或記憶裡挑。
Select 做得好不好,直接決定了模型這一輪看到的東西夠不夠用、準不準。挑太少,模型可能缺乏關鍵資訊;挑太多、挑錯方向,模型看到的反而是雜訊,跟昨天完全沒做 Write/Select、什麼都往 context 裡塞的情況沒有本質差異,只是換了一種塞法。這也是為什麼「挑選的邏輯本身」——用什麼方式判斷相關性、用什麼順序排列——往往比「有沒有做 Select 這個動作」更重要。
Select 是 Write 的另一半:Write 決定什麼先存起來,Select 決定這一輪要把什麼拉回來。兩者合起來,才能讓 context window 裡隨時放的都是「這一步真正需要」的內容,而不是「反正都存過、乾脆全部塞回去」。
明天要談的是另一個角度:就算 Select 做得再精準,留在 context 裡的內容能不能再想辦法變小?這就是 Compress 要處理的問題。