跳轉到內容

實作課程 3 — 工具

目標: 替換靜態提示上下文以使用真實Python函式模型可以 在需要時呼叫 @define_tool, Pydantic引數中繼資料和 client.create_session(..., tools=[...]).

時間: ~20分鐘

必要條件: 實作課程 2 完成。

第1步 — 為什麼工具

在實作課程 2中,您透過傳送零售事實在系統訊息中接地了助理。這適用於小例子,但有三個問題:

  1. 上下文是靜態的——它只知道你前端貼上的內容
  2. 模型必須猜測哪些事實重要
  3. 每個事實都會消耗令牌,即使答案不需要它們

一個工具改變了問題的形狀。你不需要指望提示包含正確的資料,而是註冊一個Python函式。模型決定何時需要那個函式,然後SDK呼叫它,接收結果,然後寫最終答案。

第2步 — 檢查工具形狀

開啟 sdk_labs/tools_sample.py 和找到 get_customer_total.

一個Copilot SDK工具開始作為一個普通的Python函式,帶有Pydantic引數模型:

class GetCustomerTotalParams(BaseModel):
    """Parameter schema sent to the model.

    Where C# reads ``[Description]`` attributes off the method signature, Python
    describes parameters with a Pydantic model — the field descriptions are what
    the model sees.
    """

    customer_id: Annotated[str, Field(description="Customer identifier, for example C003")]


@define_tool(description="Gets the total amount a given retail customer has spent.")
def get_customer_total(params: GetCustomerTotalParams, _invocation: ToolInvocation) -> str:
    matches = [t for t in TRANSACTIONS if t[0].casefold() == params.customer_id.casefold()]

    if not matches:
        return f"No transactions found for {params.customer_id}."

    total = sum(t[1] for t in matches)
    print(f"  [tool] get_customer_total({params.customer_id}) -> ${total:,.2f}")
    return f"{params.customer_id} has {len(matches)} transactions totalling ${total:,.2f}."

所需的簽名是 (params: SomePydanticModel, _invocation: ToolInvocation) -> str.

與C#的不同之處在於中繼資料。.NET使用 [Description] 在方法和引數上使用屬性。Python使用:

  • @define_tool(description=...) 工具描述
  • Field(description=...) Pydantic模型欄位上的描述

這些描述就是提供給模型的 API 文件。描述若含糊不清,可能導致模型未使用工具或傳入錯誤引數,因此請比照公開 API 的標準撰寫。

第3步 — 註冊工具

範例建立一個使用者端,選擇一個模型,然後在建立工作階段時註冊工具:

async with CopilotClient() as client:
    model_id = await model_picker.pick(client, requested_model_id)
    if model_id is None:
        return 1

    session = await client.create_session(
        model=model_id,
        streaming=False,
        tools=[get_customer_total],
        # Required in Python, unlike .NET: the runtime asks permission before
        # invoking a custom tool, and with no handler the call is denied and
        # the model reports a permission error instead of an answer.
        on_permission_request=PermissionHandler.approve_all,
    )

tools=[get_customer_total] 行是"模型有些文字上下文"和"模型可以要求主機應用程式實際做工作"之間的區別。

model_picker 保持 claude-haiku-4.5 作為這些實作課程的首選模型,但會退回到你的帳戶可用的實體模型。覆蓋它用:

uv run python -m sdk_labs tools --model gpt-5

第4步 — 不要跳過權限處理程式

⚠️ 這一點確實與 .NET 範例不同。

在Python中,自訂工具呼叫除非你透過 on_permission_request 建立工作階段時傳遞。如果沒有它,模型會收到一個權限失敗,而不是工具結果,然後以錯誤回答。

對於允許每個請求的實驗範例,使用:

on_permission_request=PermissionHandler.approve_all

對於生產程式碼,提供一個策略函式。第8步涵蓋安全注意事項。

第5步 — 執行它

src/AgentOrchestrator-python:

uv run python -m sdk_labs tools

預期輸出:

== Lab 03: tools ==

Model: claude-haiku-4.5
Prompt: How much has customer C003 spent in total?

  [tool] get_customer_total(C003) -> $1,700.00

Assistant: Customer C003 has spent a total of **$1,700.00** across 2 transactions.

注意,這裡不像.NET轉錄那樣,Python範例沒有空的第一行。第一個助理事件只攜帶工具請求,因此故意跳過內容為空的助理訊息。 Assistant: 1 tools_sample.py

第6步 — 跟蹤發生了什麼

有四個移動部分:

  1. 請求總花費給客戶。 C003
  2. 模型決定註冊的工具是回答的正確方式。
  3. SDK呼叫Python函式,產生 [tool]
  4. 的結果被反饋給模型,模型寫最終答案的

事件 [tool] get_customer_total(C003) -> $1,700.00 不是模擬輸出。它由真正的 get_customer_total 函式列印,SDK正在處理模型的工具呼叫。

傳回一個正常的字串,而不是一個自訂SDK結果物件:

return f"{params.customer_id} has {len(matches)} transactions totalling ${total:,.2f}."

第7步 — 理解事件交付

範例仍然使用你在實作課程 02中看到的事件模型。Python事件是隻推送回呼: session.on(handler) 註冊處理程式並傳回一個取消訂閱的可呼叫物件。沒有非同步迭代器。

SessionEvent 有一個資料類別,欄位如 data, id, timestamptype. type 是一個 SessionEventType 列舉,所以Python根據 evt.type。這與.NET的一個子類模式根本不同。

共享 IdleWaitersdk_labs/_common.py 保持範例從不等待永遠進入空閒狀態。

第8步 — 透過權限控制工具執行

SDK 透過在程序內暴露一個權限掛鉤 on_permission_request. 一個自訂策略可以檢查請求並傳回一個決策物件 從 copilot.rpc, 例如:

from copilot.rpc import PermissionDecisionApproveOnce, PermissionDecisionReject

# Not re-exported at the package root in SDK 1.0.9 — import it from the module.
from copilot.session import PermissionInvocation

參考診斷實作:

def on_permission_request(
    request: PermissionRequest, invocation: PermissionInvocation
) -> PermissionRequestResult:
    # `kind` is a class attribute on each request type ("shell", "custom-tool",
    # "write", …), where the C# version reads a `Kind` property off one type.
    kind = getattr(request, "kind", "")
    tool_name = getattr(request, "tool_name", "") or ""
    print(f"  [permission] requested: kind={kind} tool={tool_name or '(n/a)'}")

    # Policy: allow reads, refuse anything destructive.
    if "delete" in f"{kind} {tool_name}".casefold():
        print("  [permission] -> REJECTED by policy")
        return PermissionDecisionReject(
            feedback="Destructive operations are not permitted in this demo."
        )

    print("  [permission] -> approved once")
    return PermissionDecisionApproveOnce()

Python 優勢:有 沒有 GHCP001 抑制 步驟。 .NET 專案 必須抑制實驗 API 建置錯誤以使用權限決定。 copilot.rpc 直接從 Python 中暴露,無需選擇。

⚠️ 權限處理程式注意事項:此處理程式會針對 自訂工具 觸發,但測試時並未觀察到它針對 shell 指令觸發。執行權限診斷時,模型會執行 echo 並回報輸出,但不會列印 [permission] 行,因為主機 Copilot CLI 已核准 shell 操作。實作課程 03 的工具需要 on_permission_request;請先在你的環境中驗證行為,再依賴這項機制。

參考程式碼位於 sdk_labs/permissions_sample.py. 對於 shell-hook 管理方案的替代方案,請參閱 額外的治理掛鉤.

第9步 — 實驗

嘗試一個不存在的客戶,例如 C999. 更改提示 tools_sample.py 為了:

await session.send(
    "How much has customer C999 spent in total? Use the available tool."
)

重新執行範例並檢查助理報告沒有發現交易。

然後嘗試一個不需要零售資料的提示詞:

await session.send("In one short sentence, define average order value.")

模型應該直接回答。因為不需要客戶查詢,所以不應該出現的行。 [tool] [x] 為什麼工具比填充系統訊息的動態資料更好

✅ 檢查點

現在你可以解釋:

  • [x] 如何描述一個Python工具
  • [x] 如何描述Pydantic @define_tool(description=...) 描述一個Python工具
  • [x] 如何描述Pydantic Field(description=...) 描述工具引數
  • [x] 如何描述Pydantic tools=[get_customer_total] 使函式可用給模型
  • [x] 為什麼Python自訂工具需要 on_permission_request
  • [x] 為什麼Python事件分支於 evt.type
  • [x] 為什麼在實際執行的主機上需要驗證權限掛鉤

💡 延伸挑戰

新增一個傳回客戶購買的產品類別工具,例如 ElectronicsFashion 用於 C003.

使用第二個Pydantic引數模型或重用 GetCustomerTotalParams,給工具一個精確的描述,旁邊註冊 get_customer_total,然後問:

Which categories has customer C003 bought from, and how much have they spent?

檢查模型是否呼叫一個工具、兩個工具還是直接回答。