上一篇我們整理了 DeepAgent 五塊拼圖裡的前兩塊:
- Tools — 讓 Agent 能對外採取行動的手
- Backends — 決定 Agent 工作空間長什麼樣子、資料存在哪裡的地基
有了行動力與安全的工作空間,這篇接著補完剩下三塊:
一樣全部搭配 Ollama 地端模型 ornith-1.5:9b 示範,把整套地端 DeepAgent 架構補齊。
Skills:賦予 Agent 操作技能的能力Tools 是「動作」,Skills 則是「知識 + 流程」。一個 Skill 是一個資料夾,裡面放一份 SKILL.md(含 YAML frontmatter 的操作說明),可以再搭配 scripts/、references/、assets/ 等輔助資源。
Skills 最關鍵的設計理念,是避免把所有指令一次塞進 system prompt 造成 context 膨脹。它分三個層級載入:
| 層級 | 載入內容 | 時機 |
|---|---|---|
| 1. Metadata | SKILL.md frontmatter 的 name、description |
Agent 啟動時,所有已配置 skill 都會載入 |
| 2. Instructions | SKILL.md 完整內文 |
該 skill 被觸發時才讀取 |
| 3. Resources | scripts/、references/、assets/ 底下的檔案 |
Agent 依指令需要時才讀取 |
也就是說,Agent 啟動時只看得到每個 Skill 的「名字+一句話描述」,直到任務內容命中某個描述,才會用 read_file 把完整的 SKILL.md 讀進來。
目錄結構範例:
skills/
└── langgraph-docs/
├── SKILL.md
├── scripts/
│ └── fetch_docs.py
└── references/
└── api-patterns.md
SKILL.md 範例:
---
name: langgraph-docs
description:
當使用者詢問 LangGraph 相關問題時使用本技能,
透過抓取官方文件來提供準確且最新的回答。
---
# langgraph-docs
## 使用方式
1. 用 fetch_url 工具讀取 https://docs.langchain.com/llms.txt
2. 依問題挑選 2-4 篇最相關的文件連結
3. 抓取內容後綜合回答,優先給直接答案,避免整段照抄
掛載到 Agent:
from deepagents import create_deep_agent
from deepagents.backends.filesystem import FilesystemBackend
backend = FilesystemBackend(root_dir="./my-project")
agent = create_deep_agent(
model="ollama:ornith-1.5:9b",
backend=backend,
skills=["./my-project/skills/"],
)
description 要具體:這是 Agent 挑選 skill 時唯一看得到的資訊,寫清楚「做什麼」與「何時用」,並帶入使用者可能提到的關鍵字。
SKILL.md 本文控制在 500 行以內,frontmatter 建議在 5,000 tokens 以下,細節內容拆到 references/。
技能數量寧少勿多:描述重疊的 skill 越多,Agent 選錯或猶豫的機率越高,必要時合併成一個涵蓋多個子任務的 skill。
| Skills | Memory | Tools | |
|---|---|---|---|
| 目的 | 依需求載入的能力 | 啟動時就載入的持久上下文 | 程式化可呼叫的動作 |
| 載入時機 | Agent 判斷相關時才讀 | Agent 啟動時載入 | 每一輪都可用 |
| 格式 | 具名目錄下的 SKILL.md |
AGENTS.md 檔案 |
綁定在 Agent 上的函式 |
| 適用時機 | 指令具任務特化性、內容可能很長 | 內容永遠相關(專案慣例、偏好設定) | 需要程式化動作,或無法透過檔案系統存取 |
小結:如果你發現自己一直在跟 Agent 重複交代同一套步驟,那就是把它蒸餾成 Skill 的訊號。地端場景下,把公司內部 SOP、故障排查流程封裝成 Skill,讓小模型也能穩定重現複雜流程。
Memory:建立 Agent 的記憶能力DeepAgent 把長期記憶做成「檔案系統」的延伸:Agent 讀寫記憶就像讀寫檔案,實際存在哪裡則交給 Backend 決定。這一節專注在跨對話持久的長期記憶;單一對話內的短期記憶(訊息歷史、暫存檔案)則由 Agent state 自動管理。
Agent-scoped Memory:讓 Agent 有自己的人格把 namespace 設成 (assistant_id,),代表所有使用者、所有對話共用同一份記憶檔案——Agent 因此能累積出一致的「人設」與跨使用者學到的通用知識。
from deepagents import create_deep_agent
from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
agent = create_deep_agent(
model="ollama:ornith-1.5:9b",
memory=["/memories/AGENTS.md"],
skills=["/skills/"],
backend=CompositeBackend(
default=StateBackend(),
routes={
"/memories/": StoreBackend(namespace=lambda rt: (rt.server_info.assistant_id,)),
"/skills/": StoreBackend(namespace=lambda rt: (rt.server_info.assistant_id,)),
},
),
)
User-scoped Memory:每位使用者的獨立記憶把 namespace 換成 (rt.server_info.user.identity,),每位使用者就各自擁有一份記憶檔,彼此的偏好與歷史不會外洩:
backend=CompositeBackend(
default=StateBackend(),
routes={
"/memories/": StoreBackend(namespace=lambda rt: (rt.server_info.user.identity,)),
},
)
Episodic Memory:搜尋過去的對話除了「事實型」記憶(存在 AGENTS.md 裡的偏好、規則),DeepAgent 也能利用 checkpointer 保存的完整對話歷史做「情節型」記憶 — 讓 Agent 回想起「上次是怎麼解決這個問題的」,而不只是「學到了什麼結論」:
from langgraph_sdk import get_client
from langchain.tools import tool, ToolRuntime
client = get_client(url="<DEPLOYMENT_URL>")
@tool
async def search_past_conversations(query: str, runtime: ToolRuntime) -> str:
"""搜尋過去與這位使用者相關的對話"""
user_id = runtime.server_info.user.identity
threads = await client.threads.search(metadata={"user_id": user_id}, limit=5)
results = [await client.threads.get_history(thread_id=t["thread_id"]) for t in threads]
return str(results)
| 方式 | 優點 | 缺點 |
|---|---|---|
| Hot path(對話當下寫入) | 記憶立即可用,對使用者透明 | 增加延遲,Agent 要邊做事邊記錄 |
| 背景整併(sleep-time compute) | 不拖慢當下對話,可跨多輪對話彙整 | 記憶要等下一次對話才生效,需要第二個 Agent |
多數場景 hot path 就夠用;若使用量大、想降低延遲或提升記憶品質,可以另外部署一個「整併 Agent」,用 cron job 定期讀取近期對話、萃取重點、合併回記憶檔。
組織層級的政策(合規規則、共用知識庫)建議設為唯讀,只由應用程式碼寫入,避免透過共享狀態被 prompt injection:
permissions=[
FilesystemPermission(operations=["write"], paths=["/policies/**"], mode="deny"),
]
小結:地端部署常見的節省成本手法,是把「使用者個人偏好」設成 user-scoped、可寫;把「公司規範、SOP」設成 organization-scoped、唯讀。兩者混搭在同一個
CompositeBackend裡,各自路由到不同的儲存空間。
Summarization Middleware:壓縮當前 Session 的上下文地端模型(尤其是 9B 這個量級)的 context window 通常比雲端旗艦模型小得多。當對話輪數一多、工具呼叫結果一長,很容易撞到上限。SummarizationMiddleware 就是為了解決這個問題:當接近 token 上限時,自動把較舊的訊息壓縮成摘要,同時保留最近的訊息原文。
from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
agent = create_agent(
model="ollama:ornith-1.5:9b",
tools=[your_weather_tool, your_calculator_tool],
middleware=[
SummarizationMiddleware(
model="ollama:ornith-1.5:9b",
trigger=("tokens", 4000),
keep=("messages", 20),
),
],
)
trigger 決定「什麼時候開始壓縮」,keep 決定「壓縮後留下多少」,兩者都支援三種度量單位:
fraction:模型 context 視窗的比例(0–1)tokens:絕對 token 數messages:訊息則數trigger 還支援組合邏輯:
# OR:tokens >= 3000 或 messages >= 6,符合其一即觸發
SummarizationMiddleware(
model="ollama:ornith-1.5:9b",
trigger=[("tokens", 3000), ("messages", 6)],
keep=("messages", 20),
)
# AND:tokens >= 4000 且 messages >= 10 同時成立才觸發
SummarizationMiddleware(
model="ollama:ornith-1.5:9b",
trigger={"tokens": 4000, "messages": 10},
keep=("messages", 20),
)
因為 ornith-1.5:9b 這類地端模型的可用 context 通常較小,實務上有幾個要注意的地方:
fraction 條件依賴模型 profile 資料。若 Ollama 模型沒有對應的 profile,建議改用 tokens 或手動指定 custom_profile,避免比例計算失準。trigger 的閾值要抓得比模型上限更保守,因為摘要本身也要消耗一次模型呼叫,若壓縮太晚觸發,可能連摘要這次呼叫都會超出上限。ornith-1.5:9b 之外再跑一個更小的模型),把「摘要」與「主任務」的算力分開,減少地端 GPU/CPU 負擔。除了 Summarization,官方也提供 ContextEditingMiddleware(搭配 ClearToolUsesEdit),策略是直接清掉較舊的工具呼叫結果,而不是生成摘要:
from langchain.agents.middleware import ContextEditingMiddleware, ClearToolUsesEdit
middleware=[
ContextEditingMiddleware(
edits=[ClearToolUsesEdit(trigger=2000, keep=3)],
),
]
兩者可以同時使用:ContextEditingMiddleware 負責清掉不再需要的工具原始輸出(例如超長的搜尋結果),SummarizationMiddleware 負責把對話本身濃縮成摘要——分進合擊,讓地端小模型也能撐住長對話。
小結:地端模型的 context window 是稀缺資源,Summarization 與 Context Editing 是延長 Agent「有效對話長度」最直接的手段,建議一開始就把這兩層中介層納入預設架構,而不是等到爆 context 才補上。
這兩篇涵蓋了 Tools、Backends、Skills、Memory、Summarization,也對應到打造一個生產級地端 Agent 的五個問題: