Durable Execution và Triển Khai Agent Lên Production

Agent của bạn chạy 40 phút, gọi 30 tool, gửi hai email và một lệnh hoàn tiền. Ở phút thứ 38, pod bị evict khi deploy. Process chết, state trong RAM mất, và lần retry tiếp theo gửi lại đúng hai email đó cùng một lệnh hoàn tiền. Đây không phải bài toán prompt; đây là bài toán durable execution.

Cập nhật 2026

  • Durable execution đã thành hạ tầng phổ biến: Temporal, DBOS, Restate, Inngest. Bạn không cần tự viết journal.
  • Framework cũng có checkpoint riêng: LangGraph 1.2.20 (checkpointer + store), Microsoft Agent Framework 1.25.0 (FileCheckpointStorage), OpenAI Agents 0.24.0 (RunState serialize ra JSON), PydanticAI 2.66.0 (Temporal/DBOS/Prefect/Restate và StepPersistence).
  • Queue vẫn là at-least-once. Không có engine nào biến nó thành exactly-once; idempotency vẫn là việc của bạn.
  • Kiểm soát chi phí và kill switch là một phần của thiết kế deploy, không phải tính năng thêm sau.

Durable execution giải quyết bài toán gì

Một agent loop bình thường là một hàm chạy trong một process: gọi model, gọi tool, lặp. Nếu process chết, toàn bộ tiến trình biến mất. Durable execution đưa phần điều phối ra khỏi process: mỗi bước được ghi vào một journal, và khi worker mới tiếp quản, nó phát lại các bước đã hoàn thành rồi tiếp tục từ bước kế tiếp.

Ba yêu cầu để việc phát lại đúng:

  • Bước điều phối deterministic — quyết định bước nào tiếp theo không phụ thuộc now(), random hay network. Thời gian và số ngẫu nhiên lấy từ engine.
  • Side effect nằm trong activity/step — gọi model, gọi tool, ghi DB là các bước riêng, có thể retry độc lập. Không trộn vào logic điều phối.
  • Kết quả bước được lưu — phát lại không gọi lại model. Nếu không lưu, bạn trả tiền cho cùng một prompt hai lần và nhận output khác.

Điểm thứ ba quan trọng với chi phí: replay một workflow 30 bước không được gọi model 30 lần. Engine lưu output của activity đã xong, nên replay chỉ chạy các bước còn thiếu.

Journal hay snapshot?

Hai cách lưu state, và chúng khác nhau ở chỗ nào đắt:

  • Event journal — ghi từng bước đã xảy ra, phát lại tuần tự. Temporal và các engine workflow dùng hướng này. Replay rẻ vì chỉ phát lại quyết định, không gọi lại model; nhưng journal dài dần và cần chính sách lưu trữ.
  • Snapshot — lưu toàn bộ state tại một thời điểm. Checkpointer của LangGraph, checkpoint file của MAF, RunState của OpenAI Agents đều là snapshot ở các mức khác nhau. Khôi phục nhanh, nhưng snapshot lớn và schema thay đổi giữa các bản deploy là vấn đề thật.

Trong thực tế bạn thường dùng cả hai: journal cho điều phối, snapshot cho state hội thoại. Điều cần tránh là lưu snapshot mỗi bước mà không có version — hai tuần sau, một task cũ được resume với schema mới và fail ở chỗ khó debug nhất.

Idempotency: chìa khoá của mọi thứ

Queue đảm bảo ít nhất một lần, nghĩa là có thể nhiều lần. Cách duy nhất để sống chung là mỗi side effect có một khoá duy nhất và tầng ghi nhận nó. Bộ ba thực dụng: (task_id, step_id, tool_call_id).

-- File: migrations/001_idempotency.sql
CREATE TABLE side_effect (
  task_id      uuid        NOT NULL,
  step_id      text        NOT NULL,
  tool_call_id text        NOT NULL,
  tool_name    text        NOT NULL,
  result       jsonb,
  created_at   timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (task_id, step_id, tool_call_id)
);

Trước khi thực thi, worker INSERT ... ON CONFLICT DO NOTHING. Nếu insert thất bại vì khoá đã tồn tại, bước đó đã chạy — đọc result và bỏ qua. Nếu insert thành công, chạy tool rồi cập nhật result.

Nhưng có một lỗ hổng kinh điển: worker chết sau khi tool đã chạy và trước khi result được ghi. Khi đó khoá tồn tại với result rỗng. Cần thêm trạng thái và một bước reconcile: hoặc tool viết kết quả theo cùng khoá (ví dụ payment API nhận idempotency key), hoặc worker tra cứu side effect thật trước khi retry.

ALTER TABLE side_effect ADD COLUMN status text NOT NULL DEFAULT 'in_flight';
-- in_flight | done | failed

Quy tắc: retry chỉ khi bước idempotent, và idempotent phải được chứng minh ở tầng tool, không phải giả định ở tầng agent.

Heartbeat, lease và resumability

Task dài cần biết worker nào đang giữ nó. Mô hình lease đơn giản: mỗi task có một dòng khoá, worker gia hạn định kỳ; reaper lấy lại task khi heartbeat quá hạn.

-- File: migrations/002_lease.sql
CREATE TABLE task_lease (
  task_id      uuid PRIMARY KEY,
  worker_id    text        NOT NULL,
  leased_until timestamptz NOT NULL,
  heartbeat_at timestamptz NOT NULL
);

Lấy task bằng FOR UPDATE SKIP LOCKED, đặt leased_until = now() + interval '60 seconds'. Worker chạy một vòng heartbeat mỗi 15–20 giây. Reaper chạy mỗi phút và đưa các task có leased_until đã qua trở lại queue với attempt count tăng. Không có attempt count, task độc hại sẽ quay vòng mãi mãi.

Heartbeat không chỉ để giữ lease. Nó còn là tín hiệu observability: task không heartbeat trong 2 phút là dấu hiệu worker kẹt ở một tool call, không phải đang suy nghĩ. Phân biệt được hai trạng thái này tiết kiệm rất nhiều thời gian debug.

Resumability là mức cao hơn: không chỉ chạy lại task mà tiếp tục đúng bước. Ở đây framework checkpoint có ích: LangGraph checkpointer lưu state graph theo thread; MAF lưu checkpoint ra file; OpenAI Agents serialize RunState; PydanticAI có StepPersistence. Nhưng checkpoint của framework vẫn cần một nơi lưu bền và một chiến lược version hoá — schema state đổi giữa hai lần deploy là lỗi thường gặp.

Retry: đúng loại lỗi, đúng số lần

  • 429 — đọc Retry-After, backoff có jitter, tôn trọng header. Đây là lỗi tạm thời, retry được.
  • 5xx — retry có giới hạn. Nhưng nếu tool đã tạo side effect, phải reconcile trước.
  • 400/401/403 — không retry. Retry chỉ tạo thêm log và có thể bị khoá tài khoản.
  • Timeout — không tự động retry nếu chưa biết tool đã chạy hay chưa. Đây là trường hợp cần idempotency key nhất.

Đặt trần cứng cho số attempt theo từng bước và theo toàn task. Một vòng lặp retry không trần là LLM10 unbounded consumption với hoá đơn thật.

Kill switch và budget

Kill switch đúng nghĩa không phải nút bấm dừng process — process chết thì side effect đã chạy vẫn tồn tại. Kill switch phải tước quyền tool: thu hồi token ngắn hạn, chặn egress, và từ chối mọi bước mới cho task đó. Task đang ngủ vẫn phải chết khi tỉnh.

Budget nên kiểm tra trước mỗi bước, không phải sau khi task xong:

# File: worker/budget.py — logic thuần, không phụ thuộc framework
class BudgetExceeded(Exception):
    pass

def check_budget(spent_usd: float, limit_usd: float, steps: int, max_steps: int):
    if spent_usd >= limit_usd:
        raise BudgetExceeded(f"cost {spent_usd:.2f} >= {limit_usd:.2f}")
    if steps >= max_steps:
        raise BudgetExceeded(f"steps {steps} >= {max_steps}")

Hai trần: trần theo task và trần toàn hệ thống theo ngày. Trần toàn hệ thống là thứ cứu bạn khi một bug làm mọi task chạy 200 bước cùng lúc. Ghi nhận input, cached input, output và reasoning riêng — reasoning token bị tính như output, nên một task “chỉ suy luận” vẫn có thể đắt hơn task sinh văn bản.

Deployment topology

Topology tối thiểu cho agent chạy dài:

client -> stateless API (auth, validate, enqueue)
                     |
                     v
                queue / durable engine
                     |
        +------------+------------+
        v                         v
  worker pool (lease,          scheduler (timeout,
  heartbeat, budget)           reaper, retry)
        |
        +--> model API (key theo tenant)
        +--> tool gateway (egress allowlist, idempotency)
        +--> artifact store (kết quả lớn, ngoài context)

API không giữ state; nó chỉ validate, xác thực và đưa task vào queue. Worker là nơi duy nhất chạy agent loop, và có thể scale ngang vì lease nằm trong DB/engine. Artifact lớn — file, transcript, PDF — để ngoài context và chỉ truyền handle; nếu nhét vào prompt, mỗi bước lại trả tiền cho cùng một khối token.

Một chi tiết dễ sai: worker không nên gọi model bằng credential dài hạn của hệ thống. Token nên ngắn hạn, scope theo tenant và task, inject ở biên. Khi kill switch bật, thu hồi token là hành động có hiệu lực ngay, khác với việc chờ process tự dừng.

Observability và alerting

Theo dõi theo task, không chỉ theo request:

  • Task success — nhị phân hoặc rubric; đếm theo loại task.
  • Cost per task — tách input, cached, output, reasoning. So với budget đặt ra.
  • Steps per task và tool-call accuracy — số bước tăng bất thường là dấu hiệu loop.
  • p50/p95 latency — p95 mới là thứ người dùng cảm nhận.
  • Retry rate và dead-letter count — retry storm thường đến từ một tool chậm, không phải từ model.

OTel GenAI vẫn ở trạng thái Development, attribute còn đổi; hãy pin phiên bản semantic convention và đừng xây dashboard dựa trên tên attribute chưa ổn định. Cảnh báo tối thiểu nên có: task không heartbeat quá 2 phút, tổng chi phí giờ vượt ngưỡng, tỉ lệ retry vượt ngưỡng, dead-letter tăng.

Khi nào không cần durable execution

Nếu task dưới 30 giây, không có side effect ngoài, và chạy lại từ đầu không tốn gì, một request-response bình thường là đủ. Durable execution thêm một hệ thống phải vận hành, một schema state phải version, và một tầng debug mới. Đừng trả giá đó cho một agent trả lời câu hỏi.

Tình huống Chọn
Task ngắn, không side effect Request-response
Task dài, một process, chấp nhận chạy lại Queue + idempotency
Task nhiều bước, cần resume đúng chỗ Durable engine hoặc framework checkpoint
Nhiều tenant, cần budget và kill switch Durable engine + tool gateway

Checklist production

  1. Mọi tool có side effect nhận idempotency key; khoá ghi vào bảng riêng với trạng thái in_flight/done/failed.
  2. Có lease + heartbeat + reaper; task kẹt được lấy lại và có attempt count.
  3. Retry phân loại theo lỗi; tôn trọng Retry-After; không retry 400/401/403.
  4. Budget theo task và theo ngày, kiểm tra trước mỗi bước; max steps và timeout cứng.
  5. Kill switch tước token và chặn egress, không chỉ dừng process.
  6. API stateless, worker scale ngang; artifact lớn để ngoài context và truyền handle.
  7. Secret ngắn hạn, scope theo tenant/task; không có credential dài hạn trong worker.
  8. Checkpoint/state có version; deploy mới không đọc state cũ với schema khác.
  9. Trace theo task id xuyên API, worker, tool; log có task id ở mọi dòng.
  10. Có dead-letter queue và runbook cho task kẹt, retry storm, budget breach.

Hướng tới 2027

Durable execution đang trở thành mặc định thay vì lựa chọn: PydanticAI đưa Temporal/DBOS/Prefect/Restate vào capabilities, MAF có checkpoint, LangGraph có checkpointer. Xu hướng tiếp theo là chuẩn hoá state của agent để có thể di chuyển giữa engine — hiện chưa có chuẩn chung, nên pin engine và viết abstraction mỏng quanh nó thay vì rải API khắp code.

Đọc tiếp: tối ưu chi phí theo token, giám sát agent với Langfuse, Helicone và OpenLLMetry, đánh giá agent, và thiết kế tool và xử lý lỗi.