Tóm tắt nhanh: Gọi công cụ bất đồng bộ (async tool call) cho AI agent chia nhỏ một lệnh gọi run() dạng blocking thành một giao ước (contract) đơn giản: start() trả về mã định danh tác vụ (job handle) ngay lập tức, status() báo cáo tiến độ, stop() dùng để hủy, còn kết quả sẽ được trả về sau dưới dạng một sự kiện mà agent đọc được ở lượt (turn) tiếp theo. Nhờ đó, agent vẫn có thể tiếp tục trò chuyện và nhận chỉ dẫn mới trong lúc các tác vụ kéo dài đang chạy.
Phần lớn các agent hiện nay đều gọi công cụ rồi đứng chờ kết quả. Nếu bạn hỏi câu thứ hai trong lúc bộ kiểm thử đang chạy mất 6 phút, câu hỏi đó sẽ phải nằm chờ trong hàng đợi. Điều này chẳng thành vấn đề với một lượt tra cứu mất 200 ms, nhưng lại cực kỳ bất tiện đối với quy trình triển khai (deploy) hay thu thập dữ liệu (crawl).
Một bài viết gần đây của đội ngũ DeepSeek Harness, “Exploring Super-agent Design”, đã gọi giải pháp khắc phục này là “full-duplex” (song công toàn phần): thay thế lệnh run() blocking bằng start() và stop(). Hướng dẫn này sẽ đi sâu vào khía cạnh vòng lặp của agent (agent loop): giao ước của công cụ, cách kết quả được chuyển ngược trở lại, cơ chế hủy tác vụ và các API mà mỗi công cụ cần có. Để tìm hiểu khía cạnh HTTP của một lệnh gọi kéo dài, hãy đọc bài viết AI Agents and Long-Running API Calls: Polling vs Webhooks.
Async tool call trong vòng lặp agent là gì?
Async tool call (gọi công cụ bất đồng bộ) là việc gọi một công cụ mà trong đó nó trả về một handle thay vì kết quả cuối cùng. Vòng lặp agent sẽ ghi nhận handle này, kết thúc lượt của mình và sẵn sàng tiếp nhận tương tác từ người dùng. Khi công việc hoàn tất, runtime sẽ đẩy một sự kiện hoàn thành vào cuộc trò chuyện, và mô hình sẽ đọc nó ở lượt tiếp theo như bất kỳ tin nhắn nào khác.
Điểm khác biệt nằm ở chỗ ai là bên phải chờ: với run() thì mô hình phải chờ, còn với start() thì runtime sẽ đảm nhận việc đó.
Blocking run() |
Async start() |
|
|---|---|---|
| Giá trị trả về | Kết quả cuối cùng | Job id, “Started” |
| Trạng thái của agent khi đang thực hiện | Bị đóng băng | Vẫn trò chuyện, lập kế hoạch, khởi chạy thêm các job khác |
| Tin nhắn mới từ người dùng | Bị xếp hàng chờ cho đến khi công cụ trả kết quả | Được xử lý ngay |
| Hủy bỏ | Hủy toàn bộ lượt xử lý | stop(job_id) |
Bài viết của DeepSeek mô tả đây là một kiến trúc thiết kế chứ không phải một tính năng đóng gói sẵn chỉ cần bật tắt. Bài viết cho biết full-duplex đã được triển khai trong DeepSeek Harness nhưng không được bật theo mặc định, và một số thành phần nằm trong một plugin nguyên mẫu tên là dsh-superagent-prototype mà các tác giả khẳng định không phải là sản phẩm chính thức của DeepSeek. Hãy xem đây như một kiến trúc tham khảo để học hỏi. Để biết thêm thông tin nền tảng, hãy xem what DeepSeek Harness is và bài viết đi kèm về how to build a super agent.
Vấn đề của cơ chế blocking: một công cụ chậm làm đóng băng toàn bộ agent
Hãy hình dung một coding agent được yêu cầu chạy bộ kiểm thử (mất 5 phút) và đồng thời trả lời một câu hỏi về lỗi import thất bại. Với các lệnh gọi blocking, câu hỏi của bạn sẽ phải đứng chờ sau bộ kiểm thử dù nó chẳng cần bất kỳ dữ liệu nào từ quá trình chạy đó. Khi ấy, ba vấn đề sau sẽ phát sinh:
- Độ trễ bị cộng dồn. Mỗi công cụ chạy chậm sẽ cộng toàn bộ thời gian thực thi của nó vào mọi tác vụ xếp sau.
- Chỉ dẫn mất đi tính thời điểm. Khi quá trình chạy được hai phút, bạn nhắn “thôi bỏ qua các bài test e2e đi”. Agent sẽ không thể tiếp nhận câu nói đó cho đến khi quá trình chạy kết thúc.
- Nguy cơ timeout. Các gateway thường sẽ ngắt kết nối nếu bị giữ mở suốt nhiều phút, do đó một lệnh
run()kéo dài có thể bị thất bại ngay cả khi tác vụ thực tế đã hoàn thành thành công.
Bài viết gọi đặc tính bạn mong muốn là “productive” (năng suất): agent tiếp nhận chỉ dẫn tiếp theo trước khi chỉ dẫn trước đó chạy xong. Và async tool chính là giải pháp giúp bạn đạt được điều này.

start, status, stop: giao ước của async tool
Thay vì chỉ cung cấp một công cụ duy nhất cho mỗi tính năng tốn thời gian, hãy cung cấp cho mô hình ba công cụ riêng biệt với cấu trúc dữ liệu trả về gọn nhẹ.
{
"name": "start_test_run",
"description": "Start the test suite. Returns immediately with a job_id. The result arrives later as a job_completed event; do not wait for it.",
"input_schema": {
"type": "object",
"properties": {
"suite": { "type": "string", "enum": ["unit", "e2e", "all"] },
"idempotency_key": { "type": "string" }
},
"required": ["suite", "idempotency_key"]
}
}
start_test_run chỉ trả về {"job_id": "job_8f2", "status": "queued"} mà không có gì khác. get_job_status(job_id) trả về queued, running, done hoặc failed, kèm theo một ghi chú tiến độ ngắn gọn. stop_job(job_id) sẽ gửi yêu cầu hủy và trả về trạng thái đạt được tại thời điểm đó.
Hai nguyên tắc giúp mô hình hoạt động chuẩn xác: hãy ghi rõ “do not wait” (không chờ đợi) trong phần mô tả, nếu không nó sẽ liên tục gọi get_job_status trong một vòng lặp dồn dập; và tuyệt đối không trả về dữ liệu kết quả từ start, nếu không mô hình sẽ phải phỏng đoán xem nó đang nhận được cấu trúc dữ liệu nào.
Cách kết quả được chuyển ngược trở lại vòng lặp
Để mô hình tự polling chỉ là phương án dự phòng, chứ không phải giải pháp tối ưu. Thiết kế tốt hơn là để runtime theo dõi các tác vụ và đẩy một sự kiện hoàn tất vào cuộc trò chuyện dưới dạng một tin nhắn mới:
[event] job_completed job_id=job_8f2 tool=start_test_run status=done
summary: 412 passed, 3 failed (auth/session.test.ts)
details: get_job_result(job_8f2)
Hãy giữ cho sự kiện thật ngắn gọn và đặt toàn bộ dữ liệu chi tiết phía sau một công cụ truy xuất (fetch tool), để tránh việc một tệp log dài 40.000 dòng bị nhồi thẳng vào context.
Tiếp theo là gom nhóm (batching). Bài viết của DeepSeek mô tả một bộ định tuyến (router) có nhiệm vụ thu thập các sự kiện kích hoạt (trigger) xuất hiện trong một lượt và gộp chúng vào lần gọi mô hình tiếp theo, với đầu vào dài và đầu ra ngắn được tích hợp vào giai đoạn prefill. Trong thử nghiệm của họ, số trigger tăng từ 1 lên 40 mỗi phút, kích thước batch trung bình tăng từ 1,0 lên 2,8, và thời gian đưa ra mỗi quyết định giảm từ 6,4 giây xuống còn 1,6 giây. Đó là các số liệu thử nghiệm riêng trong bài viết chứ không phải benchmark tổng quát, nhưng rõ ràng một lệnh gọi xử lý ba sự kiện cùng lúc sẽ tối ưu hơn hẳn ba lệnh gọi riêng lẻ cho từng sự kiện. Một quy tắc đơn giản: trong lúc mô hình đang tạo phản hồi, hãy đưa các sự kiện và tin nhắn của người dùng vào hàng đợi; khi lượt xử lý kết thúc, hãy gửi tất cả cùng nhau theo đúng thứ tự xuất hiện.

Thứ tự thực thi, hủy tác vụ và khóa idempotent
Thứ tự thực thi. Các job có thể hoàn thành theo bất kỳ thứ tự nào. Nếu job B phụ thuộc vào job A, hãy thiết lập quan hệ phụ thuộc thật rõ ràng: hoặc chỉ khởi chạy B sau khi nhận được sự kiện hoàn tất của A, hoặc cấu hình để API chấp nhận tham số depends_on.
Hủy tác vụ. Lệnh stop chỉ là một yêu cầu chứ không phải một sự đảm bảo chắc chắn. Hãy trả về trạng thái kết thúc thực tế (cancelled, hoặc done nếu tác vụ đã hoàn thành trước đó) và để mô hình thông báo trung thực. Tiện ích mở rộng Tasks của MCP cũng áp dụng nguyên tắc tương tự: tài liệu của họ mô tả việc hủy mang tính phối hợp (cooperative), nghĩa là một tác vụ vẫn có thể rơi vào một trạng thái kết thúc khác ngoài cancelled.
Khóa idempotent (Idempotency key). Các mô hình có cơ chế thử lại (retry), vì vậy một phản hồi bị rớt có thể kích hoạt lệnh start lần thứ hai. Hãy bắt buộc phải có idempotency_key trong mỗi lệnh start và cấu hình để API trả về job hiện có nếu gặp lại khóa đó. Nếu không có cơ chế này, một yêu cầu như “chạy migration” có thể sẽ bị thực thi hai lần.
Kết quả từng phần (Partial result). Cung cấp các kết quả sớm (chẳng hạn như 50 trang đầu tiên được crawl) thông qua get_job_result(job_id, cursor) thay vì phải chờ đợi cho đến khi toàn bộ tác vụ kết thúc.
Kiểm soát dung lượng context. Hãy tóm tắt các job đã hoàn thành thành một dòng duy nhất ngay khi mô hình đã xử lý xong chúng.
Thời gian chờ (Timeout). Thiết lập thời hạn (deadline) cho mỗi job và phát ra sự kiện job_timed_out khi vượt quá thời gian này.
API nền tảng cần được thiết kế như thế nào
Một async tool chỉ thực sự hiệu quả khi có một job API chất lượng đứng phía sau: tiếp nhận công việc, trả về một ID, và cho phép phía gọi kiểm tra trạng thái. RFC 9110 định nghĩa 202 Accepted là “yêu cầu đã được chấp nhận để xử lý, nhưng quá trình xử lý vẫn chưa hoàn tất”, và tài liệu tham khảo 202 của MDN sẽ trình bày chi tiết về cách áp dụng trong thực tế.
Một job API tối giản cho công cụ start():
| Lệnh gọi | Phản hồi | Được sử dụng bởi |
|---|---|---|
POST /jobs (với header Idempotency-Key) |
202 + {"job_id", "status":"queued"} + Location: /jobs/{id} |
start |
GET /jobs/{id} |
200 + queued, running, done hoặc failed |
status, job watcher |
GET /jobs/{id}/result |
200 kèm dữ liệu output, hoặc 409 nếu chưa hoàn thành |
fetch tool |
DELETE /jobs/{id} |
202 đã yêu cầu hủy, sau đó trả về 404 hoặc 409 ở các lệnh gọi sau |
stop |
| Callback URL tùy chọn | POST tới bên gọi khi hoàn tất |
job watcher |
Callback chính là cơ chế giúp runtime chủ động đẩy các sự kiện thay vì phải liên tục polling. Nếu bạn phân vân giữa hai phương án, hãy xem xét các điểm đánh đổi trong bài viết webhooks vs polling, còn khía cạnh API hướng sự kiện (event-driven API) được đề cập chi tiết trong bài viết what is AsyncAPI.
Nếu các công cụ của bạn hỗ trợ giao thức MCP, hiện đã có một tiêu chuẩn dành riêng cho việc này. Đặc tả kỹ thuật của MCP (phiên bản 2026-07-28) đã đưa Tasks vào như một tiện ích mở rộng tùy chọn: máy chủ có thể phản hồi tools/call bằng một CreateTaskResult chứa taskId, client sẽ poll tasks/get (hoặc lắng nghe notifications/tasks), và tasks/cancel sẽ gửi yêu cầu dừng. Các trạng thái gồm có working, input_required, completed, failed và cancelled. Cả client lẫn server đều phải bật tính năng này, và mức độ hỗ trợ từ host có thể khác nhau, vì vậy hãy kiểm tra kỹ client của bạn trước khi đưa vào sử dụng chính thức.
Để coding agent xây dựng và kiểm thử job API với Apidog CLI
Job API là phần mà bạn hoàn toàn có thể kiểm chứng trước khi bất kỳ agent nào bắt đầu tương tác với nó. Hãy giao bản thiết kế và các bài kiểm thử cho coding agent của bạn để nó trực tiếp điều khiển Apidog CLI. Bạn chỉ cần viết một prompt duy nhất; agent sẽ tự động soạn thảo đặc tả (spec), viết các test case và tiến hành chạy kiểm thử.
Quá trình thiết lập chỉ gồm ba câu lệnh:
npm install -g apidog-cli@latest
apidog auth login --with-token <YOUR_TOKEN>
npx -y skills add https://apidog.com # pick your agent when prompted
Sau đó, hãy đưa cho agent của bạn prompt này:
In Apidog project <projectId>, design the job API a start() tool needs:
POST /jobs returns 202 with job_id, GET /jobs/{id} returns queued|running|done|failed,
DELETE /jobs/{id} cancels. Create the endpoints, then create test cases that assert:
202 and a job_id on create, a status that reaches done within 60 seconds,
the same job_id when the Idempotency-Key repeats, and 409 or 404 on GET result after cancel.
Run them against the staging environment <envId> and summarize failures.
Agent sẽ đọc cấu trúc payload từ CLI, xác thực tính hợp lệ, sau đó tiến hành tạo và chạy kiểm thử:
apidog cli-schema get endpoint-create
apidog cli-schema validate endpoint-create --file post-jobs.json
apidog endpoint create --project <projectId> --file post-jobs.json
apidog cli-schema get test-case-create
apidog cli-schema validate test-case-create --file case-create-202.json
apidog test-case create --project <projectId> --file case-create-202.json
apidog run --test-case <caseId> --project <projectId> -e <envId> -r cli,json
Bộ xuất báo cáo (reporter) cli sẽ in kết quả đạt (pass) và thất bại (fail) cho từng assertion ngay trong terminal, còn báo cáo định dạng json sẽ cung cấp dữ liệu có cấu trúc để agent có thể đọc lại và phân tích. Khi các test case đều đã vượt qua, hãy chạy lại lệnh tương tự trong quy trình CI với tùy chọn -r junit; hướng dẫn quy trình làm việc của Apidog CLI agent sẽ giải thích chi tiết về chu trình này, và bài viết Apidog CLI in Claude Code sẽ hướng dẫn cách thiết lập agent. Gói miễn phí của Apidog hỗ trợ tối đa 4 người dùng.
Khi nào nên dùng async tool, và khi nào không nên
Nên dùng async khi:
- Một công cụ thường xuyên mất hơn vài giây để hoàn thành (như build phần mềm, chạy kiểm thử, crawl dữ liệu, render, hoặc deploy).
- Người dùng có nhu cầu tiếp tục trò chuyện với agent trong khi tác vụ đang chạy.
Nên giữ nguyên blocking khi:
- Lệnh gọi trả về kết quả chỉ trong vòng một đến hai giây.
- Bước tiếp theo dù sao cũng không thể bắt đầu nếu chưa có kết quả.
- Hệ thống phía sau không hỗ trợ một job API thực sự. Việc tạo một async tool "giả vờ" nhưng bên trong vẫn bị block chỉ làm tăng độ phức tạp một cách vô ích.
Một phân chia thực tế: hãy giữ các thao tác đọc (read) ở dạng blocking, và chuyển các thao tác ghi (write) kéo dài cũng như các tác vụ tính toán nặng sang dạng async.
Câu hỏi thường gặp (FAQ)
Sự khác biệt giữa async tool call và parallel tool call là gì? Parallel tool call (gọi công cụ song song) sẽ chạy nhiều công cụ cùng lúc trong một lượt duy nhất, và mô hình vẫn phải chờ cho đến khi tất cả các công cụ hoàn thành. Trong khi đó, async tool call (gọi công cụ bất đồng bộ) trả về ngay lập tức và chỉ hoàn tất ở các lượt sau đó, nhờ vậy mô hình vẫn có thể tiếp tục trò chuyện trong lúc các tác vụ đang chạy.
Tôi có bắt buộc phải sử dụng MCP để xây dựng async tool không? Không. Bất kỳ công cụ nào trả về một job ID kết hợp với một runtime có khả năng đẩy sự kiện hoàn tất đều có thể hoạt động tốt. Tiện ích mở rộng Tasks tùy chọn của MCP chỉ giúp chuẩn hóa handle, việc polling và các lệnh gọi hủy nếu cả client và server của bạn đều hỗ trợ nó.
Làm thế nào để agent biết khi nào một job hoàn tất? Runtime sẽ thông báo cho agent. Một tiến trình theo dõi tác vụ (job watcher) sẽ poll job API hoặc nhận một callback, sau đó thêm một sự kiện hoàn tất ngắn gọn vào cuộc trò chuyện. Mô hình sẽ đọc sự kiện này ở lượt tiếp theo.
Điều gì sẽ xảy ra nếu người dùng hủy tác vụ giữa chừng? Agent sẽ gọi lệnh stop(job_id), API ghi nhận yêu cầu và job sẽ báo cáo trạng thái kết thúc thực tế của nó. Nếu công việc đã kịp hoàn thành trước khi yêu cầu hủy được ghi nhận, agent nên thông báo đúng như vậy thay vì tuyên bố rằng tác vụ đã bị dừng.
Chế độ full-duplex của DeepSeek có phải là tính năng có thể bật lên dùng ngay không? Không phải dưới dạng một tính năng sản phẩm thương mại. Bài viết cho biết nó đã được triển khai trong DeepSeek Harness nhưng bị tắt theo mặc định, và plugin liên quan chỉ là một bản nguyên mẫu mà các tác giả khẳng định không phải là sản phẩm chính thức của DeepSeek. Bạn nên coi đây là một kiến trúc tham khảo.
