Mở đầu: Track C bắt đầu — tự tay viết framework agent từ số 0
6 bài trước (Track A) nói về cách một AI product ra đời ở tầm tổ chức. Từ bài này, series chuyển hẳn góc nhìn: không còn là "quy trình công nghiệp" nữa, mà là kỹ sư hệ thống tự viết — xây dựng từng viên gạch của một framework agent thu nhỏ, giống việc tự implement một LangChain/LangGraph mini bằng JS thuần. Bài 7 này là viên gạch đầu tiên: tool-calling — cơ chế cho phép agent làm những việc mà bản thân model ngôn ngữ không tự làm được.
Toàn bộ code trong Track C (Bài 7–9) nằm trong 1 file dùng chung
aisys-agent-kernel.js — mỗi bài mở rộng thêm 1 phần, và các bài sau tái sử dụng nguyên phần
đã viết ở bài trước (không viết lại). Bài 7 xây phần ToolRegistry.
1. Vì Sao Agent Cần Tool
Một model ngôn ngữ thuần chỉ có thể sinh văn bản dựa trên những gì nó đã học được lúc huấn luyện — nó không thể tính toán chính xác số học lớn, không biết thời tiết hôm nay, không đọc được file trên máy bạn, và kiến thức của nó luôn có một "điểm dừng" (knowledge cutoff). Tool-calling giải quyết đúng giới hạn này: cho phép model yêu cầu gọi một hàm/API bên ngoài để lấy dữ liệu thật hoặc thực hiện hành động thật, rồi dùng kết quả đó để trả lời chính xác hơn.
2. Tool Schema (JSON Schema)
Để model "biết" cách gọi một tool, tool đó cần một schema mô tả rõ tên, mục đích, và kiểu dữ liệu từng tham số — thường ở định dạng gần giống JSON Schema:
const calculatorTool = defineTool({
name: "calculator",
description: "Tính biểu thức số học cơ bản (+ - * / và dấu ngoặc)",
schema: {
type: "object",
required: ["expression"],
properties: {
// "pattern" đóng vai trò SANDBOX — chỉ cho phép ký tự số học, xem mục 3.
expression: { type: "string", pattern: "^[0-9+\\-*/(). ]+$" },
},
},
execute: (args) => safeCalculate(args.expression),
});
Schema này phục vụ 2 việc: (1) cho model biết CHÍNH XÁC cách gọi tool (tên tham số, kiểu dữ liệu), và (2) cho runtime một quy tắc để validate — kiểm tra tham số trước khi thực thi, thay vì tin tưởng mù quáng bất kỳ thứ gì model sinh ra.
3. Validate Input & Sandbox Hoá Side-Effect
Đây là mục quan trọng nhất về mặt an toàn: model có thể bị dẫn dắt (qua prompt injection, hoặc đơn giản là "ảo giác") để sinh ra tham số độc hại. Runtime tuyệt đối KHÔNG được tin tưởng input từ model mà không kiểm tra:
function validateArgs(schema, args) {
for (const key of schema.required || []) {
if (!(key in args)) throw new ToolValidationError(`Thiếu tham số bắt buộc: "${key}"`);
}
for (const [key, value] of Object.entries(args)) {
const propSchema = schema.properties[key];
if (!propSchema) throw new ToolValidationError(`Tham số không được khai báo: "${key}"`);
if (propSchema.pattern && !new RegExp(propSchema.pattern).test(value)) {
// Chặn NGAY tại đây — dù model có sinh ra "1); require('fs')..." cũng
// không thể lọt qua vì không khớp pattern chỉ cho phép ký tự số học.
throw new ToolValidationError(`Tham số "${key}" vi phạm sandbox pattern`);
}
}
}
execute vẫn dùng cơ chế nguy hiểm
bên trong. Ví dụ: dù đã validate expression chỉ chứa ký tự số học, nếu tool calculator
triển khai bằng eval(expression) hay new Function(expression) — một pattern
validate lỏng lẻo (hoặc một lỗi ở chính regex) vẫn có thể dẫn tới thực thi mã tuỳ ý. Đây là lý do demo ở
mục 5 dùng một bộ tính toán TỰ VIẾT (recursive-descent parser), không dùng eval/Function
ở bất kỳ đâu — sandbox thật sự nằm ở việc loại bỏ hoàn toàn khả năng thực thi mã tuỳ ý, không chỉ dựa
vào 1 lớp validate đầu vào.
4. Tool Registry Pattern
Khi có nhiều tool, agent cần một nơi tập trung để đăng ký, tra cứu, và gọi tool theo tên — gọi là tool registry. Registry giúp thêm tool mới mà không cần sửa logic agent:
class ToolRegistry {
constructor() { this.tools = new Map(); }
register(tool) { this.tools.set(tool.name, tool); }
get(name) { return this.tools.get(name); }
list() { return [...this.tools.values()]; }
execute(name, args) {
const tool = this.get(name);
if (!tool) throw new ToolValidationError(`Không tìm thấy tool: "${name}"`);
validateArgs(tool.schema, args); // luôn validate TRƯỚC khi thực thi
return tool.execute(args);
}
}
Đây chính xác là class dùng trong demo bên dưới, và sẽ được Agent (Bài 9) gọi lại nguyên vẹn — không viết lại logic registry ở bài sau.
5. Thực hành tương tác: Tool Registry Playground
Chọn 1 preset bên dưới (hoặc tự chọn tool + nhập tham số) để xem registry validate rồi thực thi. Đặc biệt thử preset "🧪 Thử tấn công" — một chuỗi cố tình chứa mã độc hại, và xem sandbox pattern chặn nó lại trước khi chạm tới bất kỳ hàm thực thi nào:
Schema của tool đang chọn
Tool đã đăng ký trong registry
import { defineTool, ToolRegistry, safeCalculate } from "./aisys-agent-kernel.js";
export function createDemoRegistry() {
const registry = new ToolRegistry();
registry.register(defineTool({ name: "calculator", /* ... */ execute: (a) => safeCalculate(a.expression) }));
registry.register(defineTool({ name: "dictionary", /* ... */ execute: (a) => DICTIONARY[a.term] }));
return registry;
}
Bài 8 sẽ mở rộng chính file aisys-agent-kernel.js này với Memory và Prompt Template — hai
viên gạch tiếp theo agent cần trước khi có thể tự chạy vòng lặp suy luận đầy đủ ở Bài 9.
Tải file code thực hành minh họa bài học
File JavaScript aisys-agent-kernel.js — phần ToolRegistry/safeCalculate
dùng chung cho cả Track C, cùng aisys-tool-lab.js wiring demo của bài này:
📖 Tài liệu tham khảo
- Function calling / tool use: OpenAI — Function Calling Guide — mô tả cơ chế tool schema tương tự mục 2.
- JSON Schema: json-schema.org — Understanding JSON Schema — chuẩn mô tả dữ liệu dùng làm nền cho tool schema.
- Sandboxing mã không tin cậy: MDN — Never use eval()! — lý do kỹ thuật đứng sau cạm bẫy ở mục 3.
Bình luận