Model đề nghị, code của bạn thực thi
Model chỉ thấy phần khai báo của tool, không thấy phần thực thi. Một tool call là yêu cầu có cấu trúc mà code của bạn kiểm tra, kiểm quyền, rồi quyết định chạy.
Lời gọi tool trông như model đang hành động
Đây là thứ bạn thấy trong log khi model “gọi tool”:
{ "role": "assistant", "tool_calls": [ { "id": "call_a7f3", "type": "function", "function": { "name": "write_file", "arguments": "{\"path\":\"/etc/hosts\",\"content\":\"127.0.0.1 api.internal\"}" } } ]}Trông như model vừa ghi vào /etc/hosts. Nó không viết gì cả. Nó vừa đề nghị một thao tác ghi, dưới dạng JSON.
Không có dòng nào trong payload đó chạy được. Không có hàm, không có đường dẫn thực thi, không có gì để thực thi. Đó là dữ liệu mô tả một hành động, và mọi thứ xảy ra sau đó là quyết định của code bạn.
Đây là ranh giới tin cậy quan trọng nhất trong một harness. Nó thường bị hiểu sai theo hướng nguy hiểm: người ta tưởng model có quyền, nên đi tìm cách chặn model. Thực tế quyền nằm ở phía chạy lời gọi.
Mười hai trường, model thấy bốn
Trong harness tôi đọc, một tool được đăng ký là một object có 12 trường, chia làm hai nhóm rõ rệt. Nhóm thứ nhất hướng ra model:
| Trường | Vai trò |
|---|---|
name | Tên tool, model dùng để gọi |
description | Mô tả để model biết khi nào nên gọi |
parameters | JSON Schema của tham số |
deferLoading | Cờ nạp chậm định nghĩa tool vào ngữ cảnh |
Nhóm thứ hai ở lại phía hệ thống: 8 trường, gồm output, execute, projectContent, finalizeContent, timeoutMs, isConcurrencySafe, presentCall, presentResult.
Tám trường đó gồm:
- hàm thực thi,
- hàm kết xuất nội dung,
- ngân sách thời gian,
- bộ phân loại chạy song song,
- hai hàm trình bày giao diện.
Không trường nào là dữ liệu. Không trường nào tuần tự hoá được thành JSON để gửi đi.
Và tài liệu của chính harness đó nói thẳng điều này:
A ToolSchema (the model-facing fields) plus a mandatory canonical output declaration, the execute function, host-only scheduler metadata, an optional final-content callback, and optional UI presenters. The registry’s schemas() builds the model-facing ToolSchema[] by an explicit allowlist — output/execute/projectContent/finalizeContent/timeoutMs/isConcurrencySafe/presentCall/presentResult must never leak into a model request.— dsh, docs/subsystems/tools.md
“Host-only” là từ khoá. Metadata đó thuộc về máy chủ, và ranh giới không phải một quy ước — nó là một hàm chỉ định rõ từng trường.
private schemaOf(definition: ToolDefinition, detachParameters: boolean): ToolSchema { const { name, description, parameters, deferLoading } = definition const detached = detachParameters ? snapshotJsonValue(parameters) : parameters if (detached === undefined) { throw new Error(`tool "${name}" parameters must be lossless JSON before schema projection`) } return { name, description, parameters: detached, ...deferLoading === true ? { deferLoading } : {}, }}Đọc hàm này chậm một chút, vì toàn bộ luận điểm của bài nằm ở đây. Nó destructure bốn trường rồi trả về bốn trường. Không có spread ...definition ở đâu cả.
Nghĩa là execute không bị lọc bỏ. Nó chưa bao giờ được đọc tới. Muốn gửi hàm thực thi lên wire thì phải sửa chính hàm này. Không ai sửa nhầm một hàm có tên schemaOf mà không biết mình đang làm gì.
Xem dữ liệu diagram
type: layers
title: Ranh giới tin cậy cắt ngang một tool
desc: Bốn trường hướng ra model nằm trên đường ranh giới; tám trường còn lại không bao giờ rời khỏi tiến trình
nodes:
- id: wire
label: Bốn trường gửi cho model
kind: external
- id: boundary
label: Ranh giới tin cậy
kind: security
focal: true
- id: host
label: Tám trường ở lại máy chủ
kind: store
- id: exec
label: execute chạy trong tiến trình của bạn
focal: true
edges:
- [wire, boundary]
- [boundary, host]
- [host, exec]Ba thứ chỉ code của bạn biết
Thời hạn chạy
timeoutMs khai báo ngân sách thời gian cho một lời gọi, và tài liệu ghi rõ nó không bao giờ được gửi cho model. Model không biết tool của bạn có hạn 5 giây hay 5 phút.
Hệ quả: model không thể tự điều chỉnh chiến lược theo ngân sách thời gian. Nếu bạn cần model biết, bạn phải nói trong description. Nhưng khi đó model biết một con số bạn kể, không phải con số hệ thống đang thực sự áp.
Quyền chạy song song
isConcurrencySafe là một hàm phân loại đồng bộ, chỉ trả true mới được xếp vào nhóm chạy song song. Mặc định là không — thiếu trường, hàm ném lỗi, hay trả về bất cứ thứ gì khác true đều bị coi là loại trừ.
Đây là mặc định đúng. Nó bảo vệ bạn khỏi một lớp lỗi khó tái hiện: hai lời gọi tool cùng sửa một tài nguyên. Model không thấy cờ này, nên model không thể tự bật nó lên.
Cách kết quả được kể lại
render và presentCall biến giá trị thô thành nội dung cho model và thành thẻ cho giao diện. Cùng một kết quả, hai cách kể khác nhau — và cả hai đều do bạn viết.
Điểm quan trọng: model đọc bản kể lại, không đọc giá trị gốc. Bạn kiểm soát được model thấy gì về những gì vừa xảy ra.
Từ đề nghị đến thực thi
Khi model muốn gọi tool, toàn bộ chu trình đi qua một đường biên tin cậy rõ ràng:
Xem dữ liệu diagram
type: sequence
title: Ranh giới tin cậy giữa đề nghị và thực thi
desc: Model chỉ phát ra JSON đề nghị; code của bạn kiểm tra quyền, thực thi và trả kết quả
nodes:
- id: model
label: Model
kind: external
- id: host
label: Code của bạn
focal: true
- id: tool
label: Hàm thực thi
kind: security
edges:
- [model, host]
- [host, tool]
- [tool, host]
- [host, model]Chu trình này có bốn nhịp dứt khoát:
- Model phát ra đề nghị: Một chuỗi JSON thuần túy chứa tên tool và tham số.
- Code của bạn kiểm soát: Kiểm tra quyền, áp đặt thời hạn (timeout) — model hoàn toàn không thể tự ý vượt rào.
- Thực thi cô lập: Hàm thực thi chỉ chạy trong tiến trình của bạn khi được cấp phép.
- Đóng gói trả về: Kết quả được định dạng thành message và đưa vào lịch sử hội thoại cho lượt kế tiếp.
Đó là thiết kế đúng. Nếu kiểm quyền nằm trong thân tool, mỗi tool phải tự nhớ kiểm, và một tool quên là một lỗ hổng. Đặt ở ngoài thì mọi tool đều đi qua cùng một cổng.
Ghi chú thứ hai: chỉ chặng tools/execute mới được thay signal huỷ. Các chặng khác không chạm được vào nó. Đây là chi tiết nhỏ nhưng là dạng ràng buộc mà bạn chỉ làm đúng được nếu thiết kế ranh giới trước.
Khi model gửi tham số sai
Đây là chỗ tôi thấy hay nhất, và cũng là chỗ dễ làm sai nhất.
Model đoán sai tên tham số là chuyện thường. Câu hỏi kiến trúc là: lỗi đó đi đâu?
Câu trả lời đúng là nó trở thành một kết quả tool. Không phải ngoại lệ làm sập vòng lặp, không phải log câm. Harness tôi đọc ném hai loại lỗi có tên riêng. ToolArgsError báo tham số không khớp schema, còn ToolOutputError báo giá trị trả về sai kiểu.
Tài liệu ghi rõ cả hai đi qua đường tool-error bình thường.
Nghĩa là model đọc được thông báo lỗi và tự sửa ở lượt sau. Vòng lặp không chết vì một tham số viết sai.
Làm được ngay
- Tách phần khai báo khỏi phần thực thi bằng kiểu dữ liệuĐừng dựa vào quy ước. Viết một hàm dựng phần gửi đi theo allowlist, destructure đúng những trường được phép, và không spread object gốc.
- Đặt cổng kiểm quyền ngoài toolAllow, deny, ask phải là một chặng mà mọi tool đều đi qua. Kiểm quyền trong thân tool là kiểm quyền sẽ bị quên.
- Trả lỗi tham số về như một kết quảMột tham số sai là dữ liệu cho lượt sau, không phải ngoại lệ. Log nó, và gửi nó về cho model đọc.
Nguyên tắc
Đọc schemaOf của một harness là cách nhanh nhất để biết nó có ranh giới thật hay chỉ có quy ước. Ranh giới thật thì nhìn thấy được trong vài dòng code.
Model đề nghị. Code của bạn thực thi. Nếu hai việc đó nằm cùng một chỗ, bạn không có hệ thống agent — bạn có một shell có giao diện chat.— tusgino
- Một lời gọi tool là JSON, không phải hành động. Không có dòng nào trong payload đó tự chạy.
- Phần model thấy và phần hệ thống giữ là hai nhóm trường khác nhau; ranh giới nên là một hàm allowlist, không phải một quy ước trong tài liệu.
- Đặt kiểm quyền (allow/deny/ask) ngoài tool, ở một chặng mà mọi lời gọi đều đi qua.
- Trả lỗi tham số về như kết quả tool để model tự sửa, đừng để nó làm sập vòng lặp.
- Nếu model chạy được thứ nó đề nghị, bạn không có hệ thống agent.