實作課程 3 — 工具¶
目標: 替換靜態提示上下文以使用真實Python函式模型可以
在需要時呼叫 @define_tool, Pydantic引數中繼資料和
client.create_session(..., tools=[...]).
時間: ~20分鐘
必要條件: 實作課程 2 完成。
第1步 — 為什麼工具¶
在實作課程 2中,您透過傳送零售事實在系統訊息中接地了助理。這適用於小例子,但有三個問題:
- 上下文是靜態的——它只知道你前端貼上的內容
- 模型必須猜測哪些事實重要
- 每個事實都會消耗令牌,即使答案不需要它們
一個工具改變了問題的形狀。你不需要指望提示包含正確的資料,而是註冊一個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 作為這些實作課程的首選模型,但會退回到你的帳戶可用的實體模型。覆蓋它用:
第4步 — 不要跳過權限處理程式¶
⚠️ 這一點確實與 .NET 範例不同。
在Python中,自訂工具呼叫除非你透過 on_permission_request
建立工作階段時傳遞。如果沒有它,模型會收到一個權限失敗,而不是工具結果,然後以錯誤回答。
對於允許每個請求的實驗範例,使用:
對於生產程式碼,提供一個策略函式。第8步涵蓋安全注意事項。
第5步 — 執行它¶
從 src/AgentOrchestrator-python:
預期輸出:
== 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步 — 跟蹤發生了什麼¶
有四個移動部分:
- 請求總花費給客戶。
C003 - 模型決定註冊的工具是回答的正確方式。
- SDK呼叫Python函式,產生
[tool]的 - 的結果被反饋給模型,模型寫最終答案的
事件 [tool] get_customer_total(C003) -> $1,700.00 不是模擬輸出。它由真正的 get_customer_total 函式列印,SDK正在處理模型的工具呼叫。
傳回一個正常的字串,而不是一個自訂SDK結果物件:
第7步 — 理解事件交付¶
範例仍然使用你在實作課程 02中看到的事件模型。Python事件是隻推送回呼: session.on(handler) 註冊處理程式並傳回一個取消訂閱的可呼叫物件。沒有非同步迭代器。
SessionEvent 有一個資料類別,欄位如 data, id, timestamp, type. type 是一個 SessionEventType 列舉,所以Python根據
evt.type。這與.NET的一個子類模式根本不同。
共享 IdleWaiter 在
sdk_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 為了:
重新執行範例並檢查助理報告沒有發現交易。
然後嘗試一個不需要零售資料的提示詞:
模型應該直接回答。因為不需要客戶查詢,所以不應該出現的行。
[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] 為什麼在實際執行的主機上需要驗證權限掛鉤
💡 延伸挑戰¶
新增一個傳回客戶購買的產品類別工具,例如 Electronics 和 Fashion 用於 C003.
使用第二個Pydantic引數模型或重用 GetCustomerTotalParams,給工具一個精確的描述,旁邊註冊 get_customer_total,然後問:
檢查模型是否呼叫一個工具、兩個工具還是直接回答。