---
# System prepended metadata

title: Day 3 - Context Engineering
tags: [AI_Agent]

---


## 課程筆記：第三天 - 情境工程、會話與記憶 (Context Engineering, Sessions & Memory)

歡迎來到課程的第三天。繼第一天定義了代理 (Agent)，第二天為其提供了工具（手
和眼）之後，今天我們將專注於為代理建立「記憶」。本筆記涵蓋了白皮書中關於情境工程、短期會話 (Sessions) 和長期記憶 (Memory) 的核心概念，以及來自 Notebook LM 團隊和 ADK 框架專家的深度解析。

---

### 1. 白皮書總覽：賦予代理記憶
* **核心問題**：大型語言模型 (LLM) 根本上是「**無狀態的**」。智慧代理的目標是在每一次對話時，動態地組合出「**完美的上下文視窗**」。
* **會話 (Sessions)**：
    * **定義**：會話是「**短期工作台**」，用於儲存當前的即時對話歷史。
    * **挑戰**：「**上下文溢位**」。隨著對話變長，上下文視窗會被填滿，導致成本上升或超載。
    * **策略**：「**壓縮策略**」，例如遞迴摘要 (recursive summarization) 或基於 token 的截斷 (truncation)。
* **長期記憶 (Long-Term Memory)**：
    * **定義**：一個用於儲存「**持久性知識**」的「**有組織的檔案櫃**」。
    * **記憶 vs. RAG**：標準 RAG 檢索的是「**靜態、全域的事實**」（如研究圖書館員）；而記憶是「**動態的、用戶特定的**」，並會隨時間演變（如個人助理）。
* **記憶 ETL 流程**：
    1.  **提取 (Extract)**：使用 LLM 從嘈雜的對話中提取關鍵事實。
    2.  **鞏固 (Consolidate)**：解決新舊資訊之間的衝突。
    3.  **合併 (Merge)**：刪除重複內容，保持知識庫的清新。
* **記憶的種類**：
    * **陳述性記憶 (Declarative Memory)**：知道「**什麼**」（事實）。
    * **程序性記憶 (Procedural Memory)**：知道「**如何**」（如何做某事）。
* **生產注意事項**：
    * **延遲**：記憶的生成必須是「**非阻塞的背景操作**」，以避免應用程式延遲。
    * **隱私**：必須嚴格拒絕 PII（個人可識別資訊）以維護用戶信任。

---

### 2. 專家 Q&A 重點
* **Steven Johnson (NotebookLM 創辦人)**
    * **NotebookLM 作為上下文 UI**：NotebookLM 本質上是一個「**上下文管理的消費者介面**」。用戶可以上傳來源，並透過「**聚焦**」(focus) 功能，手動控制模型當下應該關注哪些特定文件。
    * **大型上下文與 RAG**：目前 NotebookLM 已使用 10M token 的上下文視窗。如果來源超過此限制，系統會自動在背景執行 RAG，包括生成「**查詢的替代版本**」以檢索最佳的相關段落。
    * **代理的未來**：希望 AI 不僅是被動使用工具，而是能「**主動建議並觸發**」工具。例如：「嘿，聽起來你正在做...，你應該建立一個...格式的音訊總覽。需要我幫你做嗎？」。
    * **記憶的敘事性**：代理需要理解專案的「**時間敘事**」。如果代理只知道所有事實，但沒有時間感，它可能會提出一年前就解決的老問題。
* **Julia (ADK 框架)**
    * **ADK 的新上下文結構**：ADK 將上下文乾淨地分離為三個組件：
        1.  **靜態指令 (Static Instruction)**：核心身份、系統指令、安全策略（可被快取）。
        2.  **用戶訊息 (End User Message)**：原始、乾淨的用戶輸入，便於審計和評估。
        3.  **回合指令 (Turn Instruction)**：代表「**應用程式的聲音**」，是後端為**每一回合**動態生成的轉向指令。
    * **上下文快取 (Context Caching)**：將上下文儲存起來，未來可直接注入，無需再次呼叫 LLM，既「**更快又更便宜**」。
    * **壓縮的權衡**：這是在「**成本與上下文品質**」之間的權衡。
        * **截斷 (Truncation)**：成本效益高，但有「**風險**」，可能刪除關鍵資訊。
        * **壓縮 (Compaction)**：如遞迴摘要，品質高，但需要「**額外一次 LLM 呼叫**」來生成摘要。
* **Jay Alamar & Kimberley (記憶架構與評估)**
    * **混合式 RAG**：Jay 指出，僅有向量資料庫是不夠的，一個好的檢索系統最好是「**關鍵字搜尋**」和「**向量搜尋**」的**混合系統**。
    * **圖譜資料庫 (Graph DB)**：Kimberley 補充，圖譜在「**鞏固**」記憶時特別有用，因為它能讓記憶之間產生關聯，而不只是孤立的實體。
    * **RAG 評估 (Jay)**：
        1.  **檢索 (Retrieval)**：使用傳統資訊檢索指標（如相關性）。
        2.  **生成 (Generation)**：使用「**LLM 作為裁判**」。RAGAs 等函式庫提供如「**忠實度**」(Faithfulness) 等指標，以評估答案是否忠於提供的上下文。
    * **防止記憶被汙染 (Kimberley)**：
        1.  **信任來源**：始終優先採用「**高信任度的資料**」（例如，用戶在 CRM 中明確上傳的資料 > 對話中間接推斷的偏好）。
        2.  **嚴格定義**：明確定義「**什麼該被儲存**」。Agent Memory Bank 支援「客製化」(customization) 功能。
        3.  **防護**：使用如 **Model Armor** 這樣的工具來檢測「**提示注入**」攻擊，防止惡意資訊被存入記憶。
        4.  **將記憶作為工具**：Kimberley 與 Julia 都提到，可以讓代理「**決定**」何時檢索或儲存記憶，而不是總是將記憶塞入上下文中。

---

### 3. 程式實驗 (Code Lab) 演練
* **Notebook 1：會話 (Sessions) - 短期記憶**
    * **核心問題**：API 呼叫是「**天生無狀態的**」。
    * **ADK 方案**：
        * `InMemorySessionService`：提供會話管理，但如果服務重啟，記憶就會消失。
        * `DatabaseSessionService`：將會話歷史寫入持久性儲存（如磁碟或資料庫），使會話得以保留。
    * **上下文壓縮 (Context Compaction)**：
        * ADK 提供了 `EventCompactionConfig`，它能自動將冗長的會話歷史壓縮（例如總結），以節省 token。
* **Notebook 2：記憶 (Memory) - 長期記憶**
    * **核心差異**：會話 (Session) 用於「**單次對話**」；記憶 (Memory) 用於「**跨多個對話**」。
    * **ADK 方案**：
        * `MemoryService`：管理長期記憶，可對接 `VertexAIMemoryBank` 等後端。
        * **儲存**：使用 `AddSessionToMemory` 函式將會話中的關鍵資訊寫入長期記憶。
        * **檢索**：提供一個名為 `loadMemory` 的「**工具**」給代理，讓代理能**自行決定**何時去查詢長期記憶。
        * **自動化**：使用 ADK 的「**回呼**」(Callbacks) 功能（例如 `after_agent_runs`），可以在對話結束後自動觸發記憶儲存，無需手動呼叫。

---

### 4. 隨堂測驗 (Pop Quiz)
* **Q1**: 哪個比喻最能描述會話 (Session) 和記憶 (Memory) 的區別？
    * **B**: 會話是「**臨時工作台**」，記憶是「**有組織的檔案櫃**」。
* **Q2**: 什麼是情境工程 (Context Engineering)？
    * **B**: 在 LLM 上下文視窗中「**動態組合和管理資訊**」的過程。
* **Q3**: 陳述性記憶 (Declarative) 和程序性記憶 (Procedural) 的關鍵功能區別是什麼？
    * **C**: 陳述性是知道「**什麼**」(knowing what)，程序性是知道「**如何**」(knowing how)。
* **Q4**: 記憶管理器中「記憶鞏固」(Memory Consolidation) 的主要目標是什麼？
    * **A**: 將新資訊與現有記憶「**合併**」，同時**解決衝突並刪除重複資料**。
* **Q5**: RAG 與記憶 (Memory) 在代理系統中的根本區別是什麼？
    * **C**: RAG 扮演「**研究圖書館員**」（知道全域事實），記憶扮演「**個人助理**」（知道用戶）。

---
# Kaggle 實作 - 從提示到行動

好的，這份筆記是針對 Kaggle 課程第三天的**第一個程式實驗 (Code Lab 3a)**，主題為「**會話管理 (Agent Sessions)**」。

-----

在今天的課程中，我們將學習如何建立「**有狀態的代理**」(stateful agents)。

### 1\. 核心問題：LLM 的「無狀態性」

當您與 AI 模型進行 API 呼叫時，這些呼叫天生就是「**無狀態的**」(stateless)。這意味著第二次的呼叫，完全不知道第一次呼叫發生了什麼事。

這會導致糟糕的使用者體驗，因為使用者必須不斷重複自己。

### 2\. 什麼是「會話 (Session)」？

「會話」就是解決方案。在 ADK (Agent Development Kit) 中：

  * **會話 = 對話 (Conversation)**。
  * **會話是「短期記憶」**。
  * 一個會話包含：
      * **事件 (Events)**：如用戶輸入、代理回應、工具呼叫等。
      * **狀態 (State)**：一個「便條紙」(scratch pad)，用於儲存鍵值對（例如：`user_name: "Sam"`）。

-----

### 3\. 實驗 1：建立一個「有狀態」的代理 (In-Memory)

在第一個實驗中，我們使用「記憶體內會話服務」，這非常適合快速原型設計。

#### 程式碼講解

1.  **建立代理**：首先，和第一天一樣建立一個標準代理。
2.  **實例化服務**：接著，我們實例化 `InMemorySessionService`。
3.  **啟動運行器**：將這個服務放進我們的代理運行器 (Runner) 中。

<!-- end list -->

```python
# 範例程式碼概念 (基於逐字稿描述)

# 1. 建立代理
my_agent = adk.Agent(...)

# 2. 實例化「記憶體內」會話服務
session_service = adk.InMemorySessionService()

# 3. 將服務注入運行器
runner = adk.SessionRunner(
    agent=my_agent,
    session_service=session_service
)

# 4. 產生一個會話 ID
session_id = runner.create_session()
```

#### 測試狀態

  * **第一次互動 (在同一個 session\_id 中)**：
      * **使用者**：「嗨，我叫 Sam。美國的首都是哪裡？」
      * **代理**：（回答問題...）
  * **第二次互動 (在同一個 session\_id 中)**：
      * **使用者**：「你好，我叫什麼名字？」
      * **代理**：「你告訴過我你叫 Sam。」

**結果**：代理記住了我們的名字！ 但這有一個問題：

  * **限制**：`InMemorySessionService` 是「非持久性的」。如果你重新啟動 Kaggle 核心 (Kernel)，所有記憶都會消失。

-----

### 4\. 實驗 2：持久性會話 (Database)

為了在代理重啟後仍保留對話紀錄，我們需要將會話儲存到「持久性」的地方，例如磁碟或雲端資料庫。

#### 程式碼講解

這次的程式碼幾乎完全相同，我們只需要替換掉「服務」即可：

```python
# 範例程式碼概念 (基於逐字稿描述)

# 1. 建立代理 (同上)
my_agent = adk.Agent(...)

# 2. 實例化「資料庫」會話服務
# 這會將會話寫入本地的 SQLite 檔案
session_service = adk.DatabaseSessionService(db_path="my_sessions.db")

# 3. 將服務注入運行器 (同上)
runner = adk.SessionRunner(
    agent=my_agent,
    session_service=session_service
)
```

**結果**：現在，即使你重新啟動筆記本，只要使用相同的 `session_id`，代理仍然會記得你叫 Sam。

-----

### 5\. 實驗 3：上下文壓縮 (Context Compaction)

隨著對話越來越長，將「整個」對話歷史記錄不斷發送給模型，會變得非常昂貴且緩慢。

**解決方案**：ADK 提供了「**上下文壓縮**」(Context Compaction) 功能，它會像「摘要」一樣，將舊的對話事件壓縮起來。

#### 程式碼講解

我們只需在配置中啟用它：

```python
# 範例程式碼概念 (基於逐字稿描述)

# 1. 定義壓縮設定
compaction_config = adk.EventCompactionConfig(
    compaction_fn=adk.compaction.LLMSummarizer(),
    max_turns=10,       # 每 10 輪對話壓縮一次
    max_tokens=2048     # 壓縮到 2048 個 token 以下
)

# 2. 將設定應用到服務中
session_service = adk.DatabaseSessionService(
    db_path="my_sessions.db",
    compaction_config=compaction_config
)
```

**結果**：ADK 會自動在背景壓縮舊的對話，確保我們不會因為 token 過多而使上下文溢位。

-----

### 6\. 實驗 4：會話狀態 (Session State)

有時候，我們不需要記住整個對話，只需要記住幾個「**有意義的變數**」（例如：`user_name`, `user_location`, `preferences`）。

這就是「**狀態 (State)**」的作用，它就像是會話專屬的「便條紙」。

#### 程式碼講解

筆記本中的練習會教您如何建立工具或邏輯，以便在對話中提取這些變數，並將它們儲存在 `session.state` 中，供以後的對話使用。

**總結**：

  * **會話 (Sessions)** 用於管理「對話」或「短期記憶」。
  * 使用 `DatabaseSessionService` 來實現**持久性**。
  * 使用 `Context Compaction` 來**節省成本和 token**。