Vì sao nên bắt đầu bằng ngữ cảnh tác nhân thay vì quy tắc?
Phần lớn các nhóm nhận ra tác nhân AI trong lưu lượng của họ thường sẽ tìm đến một quy tắc trước. Chặn một user agent, thêm một bước thử thách, siết chặt rate limit. Vấn đề là một quy tắc được viết trước khi bạn hiểu lưu lượng có xu hướng chạm nhầm người. Trợ lý hợp lệ bị chặn, người dùng thật bị thử thách, còn phần tự động hóa mà bạn thực sự lo ngại vẫn tiếp tục đi qua vì nó chưa từng tự nhận diện ngay từ đầu.
Ngữ cảnh tác nhân là bước đứng trước các quy tắc. Với mỗi yêu cầu, bạn ghi lại những gì ứng dụng của bạn có thể quan sát về ai hoặc cái gì đang hành động: một người tương tác, một người đang làm việc cùng trợ lý AI, một tác nhân tự động, hoặc một thứ mà bạn chưa thể phân biệt. Bạn ghi lại bằng chứng đứng sau nhận định đó, rồi giữ nó bên cạnh kết quả của yêu cầu. Sau một hoặc hai tuần làm vậy, bạn có thứ mà không mẫu quy tắc nào cho bạn được: một bản đồ lưu lượng của chính bạn. Nếu bạn vẫn đang quyết định liệu tác nhân có thực sự truy cập trang của bạn hay không, hãy bắt đầu bằng cách biết lưu lượng website có đến từ tác nhân AI hay không.
Hướng dẫn này xây dựng lớp đó trong một ứng dụng Node.js bằng middleware thuần. Nó hoạt động với Express, và các ý tưởng tương tự cũng áp dụng cho Fastify, Koa, Hono hoặc một route handler của Next.js.
Những gì bạn sẽ xây dựng
Đến cuối cùng bạn sẽ có:
- Một middleware trích xuất bằng chứng tác nhân từ mỗi yêu cầu.
- Một phân loại nhỏ, rõ ràng với cơ sở được nêu ra cho mọi kết quả.
- Nhật ký quan sát có cấu trúc, không bao giờ làm thay đổi cách yêu cầu được xử lý.
- Một cách gắn kết quả vào request để các handler phía sau có thể đọc.
Không có gì ở đây chặn một yêu cầu. Điều đó là có chủ đích.
Bước 1: Máy chủ của bạn thực sự nhìn thấy được bằng chứng gì?
Máy chủ nhìn thấy ít hơn nhiều so với điều mọi người thường nghĩ. Trước khi viết code, hãy liệt kê các tín hiệu có sẵn ở biên của bạn:
- Chữ ký yêu cầu. Một số client tự động ký yêu cầu bằng HTTP Message Signatures ([RFC 9421]), gửi các header
Signature-InputvàSignature, thường đi kèm một headerSignature-Agenttrỏ đến khóa đã được công bố của nhà vận hành, như mô tả trong bản thảo giao thức IETF Web Bot Auth. - Danh tính tự khai báo. Client có thể tự đặt tên trong header
User-Agent, trong headerFrom, hoặc thông qua metadata giao thức như tên client của MCP. Client MCP báo cáo một tênclientInfodo chính nó chọn trong initialize request của bản sửa đổi 2025-11-25, và bản sửa đổi 2026-07-28 mang danh tính client dưới dạng metadata theo từng request. Đây là các lời khai báo, và bất kỳ ai cũng có thể sao chép chúng. - Ngữ cảnh xác thực. Yêu cầu có mang session cookie, bearer token hay API key hay không, và đó là loại thông tin xác thực nào.
- Hình thái giao thức. Header Accept, việc client yêu cầu HTML hay JSON, việc nó tải tài nguyên của trang hay không, và cách nó xử lý redirect và cookie.
- Khai báo của ứng dụng. Sản phẩm của chính bạn có thể biết những điều mà mạng không thể biết, chẳng hạn một người dùng chọn tùy chọn "AI assisted", hoặc một yêu cầu đến từ tính năng trợ lý tích hợp sẵn của bạn.
Mỗi tín hiệu có độ mạnh khác nhau. Một chữ ký được xác thực thì có thể kiểm tra. Một chuỗi user agent chỉ là một lời khai báo. Một cookie bị thiếu chỉ đơn giản là bị thiếu. Giữ chúng tách biệt là toàn bộ mục đích của bài tập này.
Bước 2: Mô hình hóa kết quả một cách rõ ràng
Hãy định nghĩa cấu trúc của một kết quả tác nhân trước khi viết logic phát hiện. Một mô hình nhỏ, có kiểu rõ ràng sẽ giúp mọi người thẳng thắn hơn về ý nghĩa của một phân loại. Bốn class dùng ở đây được giải thích trong Human, AI-assisted or agent: a practical taxonomy for application traffic.
export type ActorClass = "human" | "human_ai" | "agent" | "unknown";
export type EvidenceBasis = "verified" | "declared" | "inferred" | "unknown";
export interface ActorEvidence {
signaturePresent: boolean;
signatureAgent?: string;
declaredClient?: string;
credentialType: "session" | "api_key" | "bearer" | "none";
acceptsHtml: boolean;
appDeclaredAssist: boolean;
}
export interface ActorResult {
actor: ActorClass;
basis: EvidenceBasis;
reasons: string[];
evidence: ActorEvidence;
observedAt: string;
}Hai trường quan trọng nhất. actor là class. basis nói vì sao bạn tin điều đó. Một kết quả tác nhân dựa trên chữ ký được xác thực và một kết quả tác nhân dựa trên chuỗi user agent đều là "agent", nhưng về sau chúng cần được xử lý rất khác nhau, vì vậy bạn không bao giờ được gộp chúng lại.
Bước 3: Trích xuất bằng chứng trong middleware
Bước trích xuất đọc header và trạng thái của request. Nó chưa quyết định gì cả.
import type { Request, Response, NextFunction } from "express";
const KNOWN_AGENT_TOKENS = ["bot", "crawler", "spider", "agent", "gpt", "claude"];
export function extractEvidence(req: Request): ActorEvidence {
const ua = (req.get("user-agent") || "").toLowerCase();
const auth = req.get("authorization") || "";
const hasSession = Boolean(req.cookies?.session);
let credentialType: ActorEvidence["credentialType"] = "none";
if (hasSession) credentialType = "session";
else if (auth.toLowerCase().startsWith("bearer ")) credentialType = "bearer";
else if (req.get("x-api-key")) credentialType = "api_key";
const declaredToken = KNOWN_AGENT_TOKENS.find((t) => ua.includes(t));
return {
signaturePresent: Boolean(req.get("signature") && req.get("signature-input")),
signatureAgent: req.get("signature-agent") || undefined,
declaredClient: declaredToken ? ua.slice(0, 200) : undefined,
credentialType,
acceptsHtml: (req.get("accept") || "").includes("text/html"),
appDeclaredAssist: req.get("x-app-assist") === "1",
};
}Một vài lưu ý về đoạn code này:
signaturePresentchỉ ghi nhận rằng các header chữ ký tồn tại. Việc xác thực là một bước riêng, được đề cập trong hướng dẫn về xác minh các yêu cầu tác nhân đã ký.- Danh sách token user agent được cố ý giữ đơn giản. Nó tạo ra một lời khai báo, không bao giờ là một sự xác minh.
x-app-assistđại diện cho bất kỳ thứ gì frontend của bạn tự gửi khi người dùng làm việc thông qua tính năng AI tích hợp sẵn. Hãy thay nó bằng cơ chế thực tế của bạn.
Bước 4: Phân loại với cơ sở được nêu rõ
Bây giờ hãy chuyển bằng chứng thành kết quả. Giữ các quy tắc ngắn gọn, sắp xếp từ bằng chứng mạnh nhất đến yếu nhất, và luôn rơi xuống Unknown.
export async function classify(
evidence: ActorEvidence,
verifySignature: () => Promise<boolean>,
): Promise<ActorResult> {
const reasons: string[] = [];
const observedAt = new Date().toISOString();
if (evidence.signaturePresent && (await verifySignature())) {
reasons.push("request signature validated");
return { actor: "agent", basis: "verified", reasons, evidence, observedAt };
}
if (evidence.appDeclaredAssist) {
reasons.push("application reported its built-in assistant");
return { actor: "human_ai", basis: "declared", reasons, evidence, observedAt };
}
if (evidence.declaredClient) {
reasons.push("client declared itself as automated");
return { actor: "agent", basis: "declared", reasons, evidence, observedAt };
}
if (evidence.credentialType === "session" && evidence.acceptsHtml) {
reasons.push("interactive browser session");
return { actor: "human", basis: "inferred", reasons, evidence, observedAt };
}
reasons.push("insufficient evidence");
return { actor: "unknown", basis: "unknown", reasons, evidence, observedAt };
}Hãy lưu ý điều mà đoạn này không làm. Nó không bao giờ nâng một lời khai báo thành một sự xác minh. Nó không bao giờ gán Human cho một yêu cầu không có tín hiệu tương tác. Khi không thể kết luận, nó nói Unknown. Unknown là một câu trả lời hợp lệ và hữu ích, đồng thời cho bạn biết chính xác giới hạn hiển thị của bạn nằm ở đâu. Chúng tôi trình bày đầy đủ lập luận này trong vì sao Unknown là một kết quả hợp lệ cho lưu lượng AI.
Bước 5: Gắn vào chế độ quan sát
Gắn middleware sớm, đính kết quả vào request, và ghi log. Đừng rẽ nhánh dựa trên nó.
import express from "express";
import cookieParser from "cookie-parser";
const app = express();
app.use(cookieParser());
app.use(async (req, res, next) => {
try {
const evidence = extractEvidence(req);
const result = await classify(evidence, async () => false);
res.locals.actor = result;
res.on("finish", () => {
logActor({
route: req.route?.path ?? req.path,
method: req.method,
status: res.statusCode,
actor: result.actor,
basis: result.basis,
reasons: result.reasons,
});
});
} catch {
res.locals.actor = undefined;
}
next();
});Ba chi tiết sau làm cho cách này an toàn:
- Khối
tryđảm bảo rằng một lỗi trong phát hiện sẽ không bao giờ ảnh hưởng đến phản hồi. Quan sát phải fail open. - Việc ghi log diễn ra ở
finish, vì vậy bạn thu được mã trạng thái thực và sau đó có thể so sánh các phân loại với kết quả. verifySignatuređược dựng tạm thànhfalsecho đến khi bạn triển khai xác thực thực sự. Điều đó giữ các yêu cầu đã ký trong nhóm declared hoặc unknown thay vì thổi phồng chúng.
Bước 6: Ghi log để phân tích, không chỉ để gỡ lỗi
Hãy viết các log có thể tổng hợp được. Chỉ cần một dòng có cấu trúc cho mỗi yêu cầu là đủ.
function logActor(entry: Record<string, unknown>) {
process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}Đừng đưa dữ liệu cá nhân vào các log này. Bạn cần route, class, basis, reasons và outcome. Bạn không cần toàn bộ địa chỉ IP, cookie thô hoặc body của request để hiểu bức tranh lưu lượng của mình. Nếu bạn giữ bất kỳ định danh nào để đối chiếu, hãy băm nó với một salt luân phiên và đặt thời gian lưu giữ ngắn.
Bước 7: Đọc dữ liệu trước khi hành động
Hãy chạy ở chế độ quan sát đủ lâu để thấy được các mẫu hằng tuần bình thường. Sau đó trả lời vài câu hỏi:
- Route nào thu hút tác nhân? Tài liệu, danh mục và các endpoint tìm kiếm thường có. Các luồng tài khoản và thanh toán thường thì không nên.
- Tỷ lệ kết quả agent được xác minh so với khai báo là bao nhiêu? Tỷ lệ đó cho bạn biết mức độ có thể dựa vào danh tính an toàn đến đâu khi viết chính sách.
- Unknown tập trung ở đâu? Tỷ lệ Unknown cao trên một route thường có nghĩa là bạn đang thiếu một tín hiệu ở đó, ví dụ một trang không bao giờ chạy code frontend của bạn.
- Kết quả có khác nhau theo class không? So sánh tỷ lệ lỗi, các bước chuyển đổi hoặc báo cáo lạm dụng giữa các class. Đây là nơi các ý tưởng chính sách thực tế xuất hiện.
Các câu trả lời thường sẽ khiến bạn ngạc nhiên. Các nhóm thường phát hiện rằng một phần lớn của "vấn đề bot" thực ra là tự động hóa hợp lệ, vốn sẽ sử dụng một endpoint có cấu trúc nếu nó tồn tại.
Bước 8: Cung cấp kết quả cho phần còn lại của ứng dụng
Khi dữ liệu đã chảy vào, hãy để các handler đọc res.locals.actor cho các mục đích không mang tính thực thi: gắn nhãn phân tích, chọn định dạng phản hồi, hoặc ghi nhận rằng một ticket hỗ trợ đến từ một phiên AI-assisted. Những cách dùng này cải thiện trải nghiệm mà không khiến ai bị khóa truy cập.
app.get("/docs/:page", (req, res) => {
const actor = res.locals.actor as ActorResult | undefined;
if (actor?.actor === "agent" && req.accepts("text/markdown")) {
return res.type("text/markdown").send(renderMarkdown(req.params.page));
}
return res.render("doc", { page: req.params.page });
});Việc thực thi, chẳng hạn như challenge, rate limit hoặc kiểm tra bước cao hơn, sẽ đến sau, sau khi bạn đã đo lường các false positive. Hướng dẫn về triển khai chính sách nhận biết tác nhân bao phủ quy trình đó.
Bạn nên tránh những sai lầm nào?
- Xem user agent như danh tính. Đây là header dễ thay đổi nhất. Hãy ghi lại nó như một lời khai báo. Ngay cả các nhà vận hành AI cũng nói vậy: hướng dẫn của Anthropic cho trình duyệt agent do họ lưu trữ lưu ý rằng bất kỳ ai cũng có thể gửi cùng một header User-Agent và khuyến nghị kiểm tra các địa chỉ IP đã công bố trước khi coi lưu lượng là thật.
- Mặc định là Human. Nếu code của bạn đặt Human bất cứ khi nào không tìm thấy tín hiệu bot, số liệu Human của bạn sẽ bị phồng lên và các chính sách sẽ tin vào lưu lượng sai.
- Trộn lẫn class và basis. Luôn lưu cả hai. Một dashboard chỉ hiển thị "Agent" sẽ che khuất sự khác biệt giữa một nhà vận hành được xác minh và một chuỗi bị sao chép.
- Chặn trong khi quan sát. Ngay cả một lần chặn tạm thời cũng làm sai số đo của bạn, vì client bị chặn sẽ thay đổi hành vi.
Proof of Actor phù hợp ở đâu
Middleware ở trên là một điểm khởi đầu vững chắc, và nó cho thấy mô hình cốt lõi: bằng chứng trước tiên, một cơ sở rõ ràng cho mỗi kết quả, Unknown được giữ hiển thị, và các quyết định được để lại cho ứng dụng của bạn. Proof of Actor xây dựng trên cùng mô hình đó với độ phủ tín hiệu sâu hơn trên server, edge, browser và bối cảnh MCP, cùng với phân tích cho phép bạn Detect, Understand và Route một cách tự tin.
Nếu bạn muốn được hỗ trợ để đưa điều này tiến xa hơn trên một ứng dụng production, đăng ký quyền truy cập sớm. Để xem toàn bộ luồng quyết định, hãy đọc Cách Proof of Actor hoạt động.
Nguồn
- RFC 9421: HTTP Message Signatures: tiêu chuẩn đứng sau các yêu cầu đã ký
- draft-ietf-webbotauth-httpsig-protocol: bản thảo IETF Web Bot Auth xác định header
Signature-Agent(đang trong quá trình hoàn thiện tính đến tháng 11 năm 2026) - RFC 9110: HTTP Semantics, User-Agent and From: các header nhận diện client do chính client khai báo
- MCP specification 2025-11-25: Lifecycle: request
initializevàclientInfo - MCP specification 2026-07-28: Versioning and Compatibility: metadata theo từng request về phiên bản, danh tính và năng lực
- Anthropic: how Anthropic-hosted browsers identify themselves: token user agent, địa chỉ IP đã công bố và cảnh báo rằng header có thể bị sao chép