上一章我們把 query_database 和 skills 接進 Agent,也看到 ReAct loop 怎麼運作。
ReAct loop 很簡單:
Model 思考
↓
呼叫 Tool
↓
Observe Tool Result
↓
再思考
↓
Repeat
create_deep_agent 本身已經提供這個 loop。
真正需要設計的,是 Agent 在每一輪會看到什麼:
今天主要處理兩個問題:
目前整個設計大概有四個主要元件:
| 元件 | 解決的問題 |
|---|---|
query_database |
讓 Agent 透過 SQL 存取資料 |
SKILL.md |
提供 dataset / domain knowledge |
SkillEnforcerMiddleware |
確保 Agent 使用 tool 前先讀 skill |
max_result_tokens |
避免過大的 tool result 進入 context |
目前相關結構:
config.yml
│
├── agent/
│ ├── __init__.py
│ │ └── build_agent()
│ ├── tools.py
│ │ └── query_database / format_result
│ └── middleware.py
│ └── SkillEnforcerMiddleware
│
├── skills/
│ ├── query_database/
│ │ └── SKILL.md
│ └── trend-report/
│ └── SKILL.md
│
├── datasources/
│ └── postgres.py
│
├── scripts/
│ └── stages.py
│
└── tests/
先把兩個不同層級的限制放進 config:
agent:
model: anthropic:claude-sonnet-4-6
skills_dir: skills
max_result_tokens: 5000
database:
max_rows: 1000
timeout: 10.0
這兩個 limit 解決的是不同問題:
| 限制什麼 | 在哪一層 | 保護什麼 | |
|---|---|---|---|
max_rows |
row 數量 | datasource layer | database / Python memory |
max_result_tokens |
result 大小 | tool layer | model context |
query_database 是 Agent 存取 PostgreSQL 的入口:
@tool
async def query_database(sql: str) -> str:
"""Run one read-only PostgreSQL query and return rows as JSON.
Aggregate in SQL rather than fetching unnecessary raw rows.
Large results are refused so the query can be narrowed.
"""
try:
rows = await _query_database(sql)
except (TooManyRows, asyncpg.PostgresError) as e:
return f"ERROR: {e}"
return format_result(
rows,
cfg.agent.max_result_tokens,
)
這裡有幾個重要設計。
Model 會看到 tool 的:
所以 docstring 不只是 documentation。
它也會影響 Agent:
什麼時候呼叫 tool
↓
應該傳什麼 input
↓
應該怎麼使用這個 tool
例如 SQL 寫錯:
ERROR: column "view" does not exist
我們把它當成 tool result 回給 Agent。
這樣 ReAct loop 可以繼續:
ACT
wrong SQL
OBSERVE
database error
ACT
fix SQL and retry
而不是因為 exception 直接結束整個 run。
Agent 使用的是只有 SELECT 權限的 PostgreSQL role。
所以 database safety 不依賴 model 有沒有乖乖遵守 prompt。
Tool result 會進入 conversation history。
如果一次把很大的 query result 放進去,後面的每一次 model call 都會帶著這些內容。
所以我不希望 flow 是:
Database
↓
SELECT *
↓
大量 JSON
↓
Model 自己算
而是:
Database
↓
Filter / Aggregate in SQL
↓
Small Result
↓
Model Reasoning
簡單來說:
讓 database 做 computation,讓 model 做 reasoning。
Datasource layer 已經有:
max_rows: 1000
如果 query 超過 1,000 rows,就直接拒絕。
但 row count 不代表 result size。
同樣都是 1,000 rows:
3 columns
→ 約 19,500 tokens
all columns
→ 約 166,000 tokens
所以只限制 rows 還不夠。
max_result_tokens在 tool layer,我再加一個 result-size check:
def format_result(
rows: list[dict],
max_tokens: int,
) -> str:
content = json.dumps(
rows,
ensure_ascii=False,
)
tokens = estimate_tokens(content)
if tokens <= max_tokens:
return content
return (
f"ERROR: result is ~{tokens:,} tokens "
f"({len(rows):,} rows x {len(rows[0])} columns), "
f"over the allowed limit. "
f"Nothing was returned; narrow the query "
f"to what the answer needs."
)
如果 result 太大,不會 truncate,而是直接 refuse。
這是刻意的。
假設原本有 10,000 rows,但 system 偷偷只回前 1,000 rows:
10,000 rows
↓
truncate
↓
1,000 rows
↓
Agent thinks this is complete
這反而會讓模型拿到不完整的資料做分析而導致錯誤的結論。
所以我們選擇:
Too large
↓
Refuse
↓
Tell Agent the size
↓
Agent rewrites SQL
↓
Filter / Aggregate
這仍然是 ReAct。
Error message 本身就是新的 observation。
Deep Agents 本身也會處理過大的 tool result。
如果 result 大到一定程度,完整內容不會直接留在 context,而是被移到 file,只留下較小的內容給 model。
所以目前大概有:
Skill
→ 引導 Agent aggregate
Tool description
→ 再提醒一次
max_rows
→ 控制 database 回傳 row 數
max_result_tokens
→ 控制進入 context 的 result 大小
Deep Agents safeguard
→ 最後一道防線
對 SQL tool 來說,我會希望在 max_result_tokens 這層就先擋下來。
因為重新下更精確的 SQL,通常比把大量資料存起來再讓 Agent 分段讀更合理。
接下來是另一個問題。
我們希望 Agent 在 query database 前,先知道資料本身的特性。
例如:
一列 != 一支影片
這種知識不適合放在 tool implementation 裡。
所以放進:
skills/query_database/SKILL.md
Skill 本身是一個 folder 加上一個 SKILL.md。
Deep Agents 會使用 progressive disclosure:
Skill name + description
↓
Agent 判斷是否需要
↓
read_file(SKILL.md)
↓
完整內容進 context
所以 description 很重要。
目前這個 skill 主要放的是資料本身的事實,例如:
Skill 告訴 Agent:
資料代表什麼。
問題是:
把 skill 放在那裡,不代表 Agent 一定會讀。
就算 description 寫:
Read this before using query_database.
本質上還是 prompt。
Model 還是可以直接呼叫:
query_database(...)
如果 skill 只是 suggestion,這沒什麼問題。
但如果 skill 裡面是 tool 使用前一定需要知道的 domain knowledge,就不太一樣。
所以我希望:
No Skill
↓
No Tool
這就是 SkillEnforcerMiddleware 的用途。
預期 flow:
Model calls query_database
↓
Has query_database skill been read?
├── Yes → execute
└── No
↓
Block
↓
Ask Agent to read skill
↓
read_file(SKILL.md)
↓
Retry query_database
Middleware 不是讓 Agent crash。
它只是回傳一個新的 observation:
Skill check failed:
read /skills/query_database/SKILL.md first.
接著 Agent 可以自己修正下一步:
ACT
query_database(...)
OBSERVE
Skill check failed
ACT
read_file("/skills/query_database/SKILL.md")
OBSERVE
<skill content>
ACT
query_database(...)
我們沒有離開 ReAct loop。
只是開始用 code 控制:
Agent 在什麼條件下才能執行某些 action。