iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0

上一章我們把 query_database 和 skills 接進 Agent,也看到 ReAct loop 怎麼運作。

ReAct loop 很簡單:

Model 思考
   ↓
呼叫 Tool
   ↓
Observe Tool Result
   ↓
再思考
   ↓
Repeat

create_deep_agent 本身已經提供這個 loop。

真正需要設計的,是 Agent 在每一輪會看到什麼:

  • tool description
  • skill
  • tool result
  • error message
  • middleware 回傳的訊息

今天主要處理兩個問題:

  1. 怎麼避免一次 database query 把大量資料塞進 context。
  2. 怎麼確保 Agent 在使用 tool 前,真的有讀到需要的 skill。

目前整個設計大概有四個主要元件:

元件 解決的問題
query_database 讓 Agent 透過 SQL 存取資料
SKILL.md 提供 dataset / domain knowledge
SkillEnforcerMiddleware 確保 Agent 使用 tool 前先讀 skill
max_result_tokens 避免過大的 tool result 進入 context

Architecture

目前相關結構:

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

先把兩個不同層級的限制放進 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

Database Tool

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,
    )

這裡有幾個重要設計。

Tool description 也是 Prompt

Model 會看到 tool 的:

  • name
  • description
  • input schema

所以 docstring 不只是 documentation。

它也會影響 Agent:

什麼時候呼叫 tool
↓
應該傳什麼 input
↓
應該怎麼使用這個 tool

Error 不直接讓 Agent Crash

例如 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。

Safety 放在 Database

Agent 使用的是只有 SELECT 權限的 PostgreSQL role。

所以 database safety 不依賴 model 有沒有乖乖遵守 prompt。

不要把整個 Query Result 塞進 Context

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。

只靠 max_rows 是不夠的

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 還有最後一道防線

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 分段讀更合理。

Skill

接下來是另一個問題。

我們希望 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 主要放的是資料本身的事實,例如:

  • 一列代表一支影片在某一天 trending
  • 40,949 rows,但只有 6,351 distinct videos
  • per-video analysis 要先 collapse 成一支影片一列
  • time-based analysis 則保留每日資料
  • views distribution 很 skewed
  • 有些 category sample size 很小

Skill 告訴 Agent:

資料代表什麼。

但 Skill 還是可能被跳過

問題是:

把 skill 放在那裡,不代表 Agent 一定會讀。

就算 description 寫:

Read this before using query_database.

本質上還是 prompt。

Model 還是可以直接呼叫:

query_database(...)

如果 skill 只是 suggestion,這沒什麼問題。

但如果 skill 裡面是 tool 使用前一定需要知道的 domain knowledge,就不太一樣。

所以我希望:

No Skill
   ↓
No Tool

SkillEnforcerMiddleware

這就是 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。


上一篇
[Day 10] Tool、Skill 與 Domain Knowledge
系列文
AaaS from Scratch: 從一次性定義,到規模化分析 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言