Tính bất biến cho tác nhân AI: ngăn thử lại tạo giao dịch trùng lặp
Tác nhân của bạn gọi endpoint thanh toán, máy chủ đã ghi nhận giao dịch nhưng phản hồi bị timeout trên đường về. Không thấy mã 200, tác nhân thử lại — và khách hàng bị tính phí hai lần dù log không báo lỗi rõ ràng.
Đây là khác biệt quan trọng giữa tác nhân và API client thông thường: người dùng thường chờ hoặc kiểm tra trước khi bấm lại; tác nhân có thể thử lại 3–4 lần trong vài giây. Chính sách retry giúp tác nhân bền bỉ hơn, nhưng cũng làm tăng nguy cơ ghi dữ liệu trùng lặp.
Giải pháp là tính bất biến (idempotency): một yêu cầu lặp lại phải tạo ra cùng kết quả với một yêu cầu duy nhất.
Bài viết này tập trung vào cách:
- Tạo khóa bất biến mà tác nhân có thể tái sử dụng khi retry.
- Lưu và xử lý khóa đúng ở phía máy chủ.
- Kiểm thử để chứng minh lần gọi thứ hai không tạo hiệu ứng mới.
- Theo dõi lần chạy đã thực hiện thao tác nào.
Để hiểu rộng hơn về các lỗi tác nhân trong môi trường thực tế, xem bài viết về độ tin cậy của tác nhân AI trong production.
Apidog hữu ích ở giai đoạn kiểm thử: bạn có thể gửi cùng một request nhiều lần, lưu kịch bản và chạy nó trong CI để phát hiện hồi quy.
Vì sao tác nhân dễ tạo bản sao hơn con người?
Ba đặc điểm khiến lưu lượng từ tác nhân dễ sinh thao tác trùng lặp:
Retry mạnh mẽ
Framework tác nhân thường retry khi gặp lỗi mạng tạm thời. Backoff và circuit breaker là cần thiết, nhưng chúng cũng làm tăng số lần một request đến máy chủ. Xem thêm hướng dẫn về khôi phục lỗi cho tác nhân.Timeout mang tính mơ hồ
Một504từ proxy có thể nghĩa là máy chủ chưa xử lý request, hoặc đã xử lý xong nhưng phản hồi bị mất. Tác nhân thường retry thay vì gọi API để kiểm tra trạng thái trước.Retry toàn bộ workflow
Nếu bước 1 tạo đơn hàng và bước 4 thất bại, việc khởi động lại workflow có thể tạo đơn hàng thứ hai. Với tác nhân đa bước, ranh giới retry không luôn rõ ràng vì mô hình có thể quyết định bắt đầu lại từ đâu.
Vấn đề không phải tác nhân gửi request sai. Chúng gửi request đúng — nhiều hơn một lần.
Tính bất biến thực sự đảm bảo điều gì?
Một thao tác là bất biến khi gọi nhiều lần có cùng tác động với gọi một lần.
Theo RFC 9110, GET, PUT và DELETE là các phương thức bất biến. POST không bất biến, nên thường được dùng cho các thao tác rủi ro như tạo đơn hàng, gửi email hoặc bắt đầu chuyển khoản.
Phân biệt hai khái niệm:
-
Bất biến không đồng nghĩa với an toàn.
DELETElà bất biến: gọi năm lần vẫn chỉ xóa một tài nguyên. Nhưng nó vẫn là thao tác phá hủy. Với tác nhân, quyền đọc và quyền ghi cần được tách riêng bằng API key có đặc quyền tối thiểu. - Bất biến không yêu cầu phản hồi giống byte-for-byte. Lần gọi lại có thể trả về phản hồi đã lưu hoặc mã trạng thái khác. Điều quan trọng là trạng thái máy chủ không đổi: một giao dịch, một đơn hàng, một email.
Dùng Idempotency-Key cho các thao tác POST
Mô hình phổ biến là client tạo khóa và gửi kèm request. Máy chủ lưu khóa cùng kết quả xử lý; request sau dùng cùng khóa sẽ nhận lại kết quả đó thay vì thực hiện công việc lần nữa.
Stripe đã phổ biến cách tiếp cận này trong tài liệu về idempotent request. IETF cũng đang chuẩn hóa header Idempotency-Key.
POST /v1/payments HTTP/1.1
Host: api.yourservice.com
[REDACTED CREDENTIAL] [REDACTED]...
Idempotency-Key: 9f2b7c14-6d3a-4b18-9d55-1e2a7c0b4f31
Content-Type: application/json
{
"amount": 4900,
"currency": "usd",
"customer_id": "cus_8812",
"description": "Pro plan, August"
}
Khóa thường là UUID. Máy chủ cần lưu:
- Khóa.
- Dấu vân tay của payload.
- Trạng thái xử lý.
- Mã trạng thái và response body.
- Thời điểm hết hạn.
Tạo khóa mà tác nhân thực sự tái sử dụng
Sai lầm phổ biến là để tool wrapper tạo UUID mới cho từng HTTP attempt. Khi đó retry dùng khóa khác và hoàn toàn mất tác dụng.
Quy tắc: tạo khóa khi tác nhân quyết định thực hiện một thao tác logic, rồi tái sử dụng khóa đó cho mọi lần thử lại của thao tác ấy.
import uuid
class PaymentTool:
def __init__(self, client):
self.client = client
self._keys = {}
def charge(self, task_id, step_id, amount, customer_id):
# One key per (task, step). Retries of the same step reuse it.
op = f"{task_id}:{step_id}"
if op not in self._keys:
self._keys[op] = str(uuid.uuid4())
return self.client.post(
"/v1/payments",
headers={"Idempotency-Key": self._keys[op]},
json={"amount": amount, "customer_id": customer_id},
)
Để khóa tồn tại qua lần khởi động lại tiến trình, dùng khóa xác định từ ID tác vụ, ID bước và payload:
import hashlib
def idempotency_key(task_id: str, step_id: str, payload: dict) -> str:
raw = f"{task_id}|{step_id}|{sorted(payload.items())}"
return hashlib.sha256(raw.encode()).hexdigest()[:32]
Không tạo khóa từ timestamp hoặc giá trị ngẫu nhiên được sinh lại khi retry. Nếu tác nhân khởi động một tác vụ mới thực sự, task_id mới sẽ tạo ra khóa mới — đúng với ý nghĩa nghiệp vụ.
Máy chủ cần làm gì?
Máy chủ không chỉ tra cứu khóa; nó phải xử lý cả tính nhất quán và cạnh tranh đồng thời.
Claim khóa trước khi làm việc
Chèn khóa vào bảng có unique constraint trước khi thực hiện thao tác ghi.Từ chối cùng khóa nhưng payload khác
Nếu request hash khác, trả về422. Không âm thầm trả response cũ vì điều đó che giấu lỗi client.Xử lý request đang chạy
Nếu request đầu tiên với cùng khóa chưa hoàn thành, trả409để client back off thay vì cạnh tranh.Lưu kết quả hoàn tất
Lưu status code và response body, sau đó trả lại đúng kết quả đã lưu cho các request sau.
CREATE TABLE idempotency_records (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL,
state TEXT NOT NULL, -- in_progress | completed
response_status INT,
response_body JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL
);
Đặt TTL cho khóa. 24 giờ thường đủ cho hầu hết retry window thực tế và cũng là mặc định Stripe sử dụng. Giữ khóa vĩnh viễn chỉ làm bảng dữ liệu phình to.
Kiểm thử: lần gọi thứ hai không được thay đổi gì
Happy path không đủ để chứng minh idempotency hoạt động, vì hai giao dịch thành công đều có thể trả 200.
Thay vào đó, hãy kiểm thử dựa trên trạng thái:
- Response body lần hai phải chứa cùng ID tài nguyên như lần đầu.
-
GETdanh sách tài nguyên sau đó chỉ trả về một bản ghi. - Số dư, bộ đếm hoặc side effect chỉ thay đổi một lần.
Trong Apidog, tạo một kịch bản gồm:
- Gửi
POSTvớiIdempotency-Keycố định. - Gửi lại request hoàn toàn giống hệt.
- Lưu ID từ response đầu tiên và xác minh response thứ hai trả lại cùng ID.
- Gọi endpoint liệt kê tài nguyên và xác minh chỉ có một bản ghi.
Lưu kịch bản này và chạy trong CI khi thay đổi payment flow. Cách làm này cũng phù hợp với kiểm thử hợp đồng API.
Đừng bỏ qua hai trường hợp sau:
-
Cùng khóa, payload khác: phải nhận
422, không phải success âm thầm. - Request trùng lặp đồng thời: gửi hai request cùng lúc và xác minh chỉ một request thực hiện thao tác. Kiểm thử tuần tự sẽ không phát hiện unique constraint bị thiếu.
Nếu payment API chưa tồn tại, hãy tạo mock có ngữ nghĩa idempotency ngay từ đầu, bao gồm 422 khi payload không khớp. Điều này cho phép bạn luyện retry logic của tác nhân trước khi chạm vào production. Xem thêm lý do tác nhân nên dùng mock API thay vì môi trường production.
Khi không thể thêm khóa bất biến
Nếu API bên thứ ba không hỗ trợ idempotency, ưu tiên các phương án sau:
Thiết kế thao tác vốn đã bất biến
DùngPUT /orders/{client_order_id}thay vìPOST /ordersnếu bạn kiểm soát API. Client chọn resource ID, nên retry cùng request không tạo đơn hàng mới.Kiểm tra trước khi ghi
Cho tác nhân tìm bản ghi có natural key tương ứng trước khi tạo. Cách này yếu hơn do vẫn có race condition, nhưng giảm rủi ro timeout-retry phổ biến.Khử trùng lặp ở downstream
Với event hoặc message, gắn message ID ổn định và yêu cầu consumer bỏ qua bản sao. Đây là thực hành chuẩn trong event-driven system; xem hướng dẫn thiết kế webhook đáng tin cậy.Yêu cầu phê duyệt của con người
Với thao tác không thể đảo ngược và không thể làm bất biến, dùng approval gate. Đây là mô hình phù hợp khi chi phí của thao tác trùng lặp quá cao; xem thêm về hàng rào bảo vệ cho tác nhân AI.
Theo dõi lần chạy nào đã tạo dữ liệu
Idempotency ngăn bản sao nhưng không cho biết attempt nào đã tạo bản ghi. Vì vậy, hãy log task_id, step_id, idempotency key và kết quả của từng attempt.
Nếu tác nhân chạy trong coding runtime, nền tảng cũng nên liên kết thao tác ghi với task và execution run tương ứng. Ví dụ, Sharkly gắn từng lần chạy với Tác vụ nguồn, trạng thái thực thi và kết quả, giúp truy ngược thao tác lặp lại về một run cụ thể thay vì một retry ẩn danh.
Checklist trước khi triển khai
- [ ] Mọi tool ghi dữ liệu mà tác nhân gọi đều yêu cầu
Idempotency-Key. - [ ] Tool wrapper tạo khóa từ task và step, không phải từ từng retry.
- [ ] Máy chủ claim khóa trước khi thực hiện công việc.
- [ ] Cùng khóa nhưng payload khác trả về lỗi.
- [ ] Request trùng lặp đồng thời được xử lý bằng unique constraint ở database.
- [ ] Có kịch bản kiểm thử xác minh request thứ hai không tạo side effect.
- [ ] Kịch bản được chạy trong CI.
- [ ] Khóa có TTL và dữ liệu hết hạn được dọn dẹp.
Câu hỏi thường gặp
Có cần khóa bất biến cho tool chỉ đọc không?
Không. GET đã an toàn và bất biến. Retry chỉ làm tăng độ trễ, không tạo side effect. Chỉ dùng khóa cho thao tác tạo, tính phí, gửi hoặc thay đổi trạng thái.
Nên tạo khóa trong tác nhân hay tool wrapper?
Trong tool wrapper, dựa trên ID tác vụ và ID bước. Không để mô hình tự tạo khóa: mô hình có thể tạo giá trị mới khi retry hoặc tạo va chạm giữa các tác vụ.
Request lặp lại nên trả status code nào?
Trả lại status code đã lưu từ request gốc. Nếu request đầu trả 201, request lặp lại cũng nên trả 201 cùng response body. Có thể thêm header Idempotent-Replay: true để hỗ trợ gỡ lỗi.
Nên giữ khóa bao lâu?
24 giờ bao phủ gần như mọi retry window. Giữ lâu hơn hiếm khi hữu ích và làm tăng kích thước bảng không giới hạn. Retry sau TTL nên được coi là thao tác mới.
Idempotency có thay thế transaction không?
Không. Idempotency ngăn request trùng lặp tạo side effect trùng lặp; transaction đảm bảo một request đơn lẻ là atomic. Bạn cần cả hai, và nên claim idempotency key trong cùng transaction với thao tác nghiệp vụ khi có thể.
Kiểm thử mà không có nhà cung cấp thanh toán thật như thế nào?
Trỏ tác nhân tới mock triển khai Idempotency-Key, bao gồm cả 422 khi payload không khớp. Bạn có thể tải Apidog để đặt mock và kịch bản kiểm thử retry trong cùng một dự án.


Top comments (0)