iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
AI 自動化

協定、框架、架構:一條龍搞懂 AI Agent 是怎麼被造出來的系列 第 4

Day 4:裝飾器、Context Manager、Generator,手刻一個 @tool 出來

  • 分享至 

  • xImage
  •  

前面三天把型別、Pydantic 和 async 補起來之後,今天來到 Python 地基的最後一天。

接下來寫 MCP 時,會一直看到這三種東西:

@mcp.tool()

async with ...

async for ...

第一次看到 SDK 的寫法,很容易覺得:

這些東西到底在背後做了什麼?

所以今天不急著碰 MCP SDK。

先自己拆一次。


裝飾器其實沒有那麼神秘

最簡單的 Decorator,本質上就是:

吃一個 function,再回傳一個 function。

例如:

def timed(fn):
    def wrapper(*args, **kwargs):
        print("開始執行")
        result = fn(*args, **kwargs)
        print("執行完成")
        return result

    return wrapper

當我們寫:

@timed
def hello():
    print("hello")

其實差不多等於:

hello = timed(hello)

所以 @xxx 本身沒有什麼魔法。

它只是讓我們可以在 function 外面再包一層行為。

這也開始有點像之後會看到的:

@mcp.tool()

那我們自己刻一個 @tool

今天真正想做的是這個:

@tool
def read_file(
    path: str,
    max_lines: int = 200
) -> str:
    """讀取專案內的一個檔案"""

希望加上 @tool 之後,可以自動知道:

工具名稱
參數名稱
參數型別
預設值
哪些參數必填
工具說明

做法其實沒有想像中複雜。

Python 有一個很好用的東西:

inspect.signature()

它可以直接把 function 的參數讀出來。

例如:

sig = inspect.signature(read_file)

就能知道:

path       → str
max_lines  → int,預設 200

接著再用 Pydantic 動態建立 Model:

model = create_model(
    "read_file_params",
    path=(str, ...),
    max_lines=(int, 200)
)

最後:

schema = model.model_json_schema()

就拿到 JSON Schema 了。

所以簡化後的 @tool 大概就是:

def tool(fn):
    sig = inspect.signature(fn)
    fields = {}

    for name, param in sig.parameters.items():
        annotation = param.annotation

        if param.default is inspect.Parameter.empty:
            fields[name] = (annotation, ...)
        else:
            fields[name] = (
                annotation,
                param.default
            )

    model = create_model(
        f"{fn.__name__}_params",
        **fields
    )

    schema = model.model_json_schema()

    REGISTRY[fn.__name__] = {
        "name": fn.__name__,
        "description": inspect.getdoc(fn) or "",
        "schema": schema,
        "handler": fn
    }

    return fn

做的事情其實就三步:

讀 Function Signature
        ↓
建立 Pydantic Model
        ↓
產生 JSON Schema

是不是突然就沒那麼神秘了。


跟 Day 2 的結果比一次

Day 2 我們是直接定義:

class ReadFileParams(BaseModel):
    path: str
    max_lines: int = 200

今天則是:

@tool
def read_file(
    path: str,
    max_lines: int = 200
):
    ...

最後把兩邊產生的 Schema 拿來比較。

實測:

properties 完全相同? ✓ 是
required 相同?       True

這就是我今天最想確認的事情。

前幾天看起來像是在學:

Type Hint
Pydantic
Decorator

現在其實已經慢慢接在一起了:

Python Function
      ↓
Type Hint
      ↓
Decorator 讀取 Signature
      ↓
Pydantic
      ↓
JSON Schema
      ↓
Tool

等到 Day 10 真正寫:

@mcp.tool()

至少已經知道其中一部分到底在做什麼。


Context Manager:負責開場跟收場

另一個之後很常看到的是:

async with ...

Context Manager 最重要的用途其實很單純:

確保資源有正確地建立,也有正確地關掉。

例如:

@asynccontextmanager
async def lifespan():
    print("啟動")

    try:
        yield
    finally:
        print("關閉")

使用時:

async with lifespan():
    print("程式執行中")

概念大概就是:

yield 之前
→ 建立資源

yield
→ 程式執行

yield 之後
→ 清理資源

就算中間發生錯誤,finally 還是會跑。

到了 Day 10 寫 MCP Server 時,會再看到同樣的概念用在 Server 的生命週期。


Generator:一次拿一點,不用全部等完

最後一個是 Generator。

例如:

def countdown(n):
    while n > 0:
        yield n
        n -= 1

呼叫:

gen = countdown(3)

這時其實還沒有把:

3
2
1

全部算好。

而是你每次要求下一個值,它才繼續往下跑。

next(gen)

得到:

3

再一次才是:

2

這就是 streaming 很重要的一個概念:

不用等全部結果完成,可以邊產生邊處理。

LLM 串流也是一樣。

不是等整段回答生成完成才一次丟回來,而是:

產生一小段
↓
送出
↓
再產生一小段
↓
再送出

如果是 Async Generator,就會看到:

async for chunk in stream():
    print(chunk)

這種寫法後面也會一直出現。


Day 4 小結

今天其實是在把幾個看起來有點「魔法」的 Python 語法拆掉:

Decorator
→ 幫 function 加能力

Context Manager
→ 管理資源的建立與清理

Generator
→ 一次產生一部分結果

而最重要的是,我們真的自己刻出了一個:

@tool

它做的核心流程就是:

inspect
   ↓
讀取 function signature
   ↓
Pydantic
   ↓
JSON Schema
   ↓
註冊成 Tool

等到 Day 10 使用 MCP SDK 時,再回頭看:

@mcp.tool()

應該就不會覺得它是一個黑盒子了。


明天:Day 5

下一篇:

用 uv 建專案 + 呼叫第一支 LLM API

前四天都還在準備地基。

明天終於要真的把模型接進來。

會從最基本的請求開始,建立後面整個系列都會共用的 LLM 呼叫層。

也就是從 Day 5 開始,我們不只是在準備零件。

專案要正式開始動了。


上一篇
Day 3:async / await 與 asyncio,順便量出這張卡的天花板
下一篇
Day 5:用 uv 建專案 + 呼叫第一支 LLM API
系列文
協定、框架、架構:一條龍搞懂 AI Agent 是怎麼被造出來的10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言