Có bao nhiêu cách chạy một AI Agent?
Góc nhìn của một backend engineer sau vài tháng nhét agent vào production
Mở đầu: agent thực ra rất nhỏ
Trước khi bàn "chạy ở đâu", cần thống nhất "chạy cái gì". Bỏ hết marketing đi, một AI agent chỉ là vòng lặp này:
async function agentLoop(messages: Message[], tools: Tool[]) {
while (true) {
const res = await llm.createMessage({ messages, tools });
messages.push({ role: 'assistant', content: res.content });
if (res.stop_reason !== 'tool_use') return res;
const toolResults = await Promise.all(
res.content
.filter((b) => b.type === 'tool_use')
.map(async (b) => ({
type: 'tool_result' as const,
tool_use_id: b.id,
content: await executeTool(b.name, b.input),
})),
);
messages.push({ role: 'user', content: toolResults });
}
}
Hai mươi dòng. Chạy được. Và cũng chính vì nó nhỏ như vậy nên câu hỏi thú vị không phải "agent là gì", mà là:
Vòng lặp này sống ở đâu, ai giữ state của nó, và chuyện gì xảy ra khi process chết ở vòng thứ bảy?
Bài này đi qua bốn trục quyết định: runtime, kiến trúc loop, cách kích hoạt, và cách cấp tool. Bốn trục này gần như độc lập với nhau, bạn có thể mix tuỳ ý, và phần lớn tranh cãi về "agent framework nào tốt nhất" thật ra là do người ta đang nói chuyện ở bốn trục khác nhau mà không biết.
Trục 1: Runtime — vòng lặp sống ở đâu
1.1. CLI trên máy dev
Claude Code, Codex CLI, aider, Cursor Agent. Vòng lặp chạy trong một process ngắn hạn ngay trên máy bạn.
Đặc điểm: có quyền truy cập filesystem và shell thật, state nằm trong RAM, người dùng ngồi ngay đó để duyệt từng hành động.
Hợp với: coding, refactor, migration, script hoá công việc lặp đi lặp lại.
Không hợp với: bất cứ thứ gì multi-tenant. Không có isolation giữa các user, không có audit trail, không scale ngang được. Nếu bạn thấy mình đang viết wrapper HTTP quanh một CLI agent để phục vụ nhiều người dùng, dừng lại, đó là dấu hiệu chọn sai runtime.
Cạm bẫy: trust boundary. Agent có shell nghĩa là prompt injection từ nội dung một file bất kỳ trong repo cũng có thể thành rm -rf. Sandbox và allowlist không phải là tuỳ chọn.
1.2. Long-running service trong container
ECS Fargate, EKS, EC2, hoặc bất cứ chỗ nào giữ được một process sống lâu. Vòng lặp nằm trong service của bạn, kết quả stream ra client qua SSE hoặc WebSocket.
@Sse('chat/:id/stream')
stream(@Param('id') id: string): Observable<MessageEvent> {
return this.agentService.run(id).pipe(
map((chunk) => ({ data: chunk })),
);
}
Đặc điểm: không giới hạn thời gian chạy thực tế, giữ được connection, dễ debug vì mọi thứ trong một process.
Hợp với: interactive agent, chat, các loop chạy từ 30 giây đến vài phút.
Cạm bẫy lớn nhất: state nằm trong RAM của một pod cụ thể. User đóng laptop, mở lại, load balancer route sang pod khác, và toàn bộ hội thoại biến mất. Deploy giữa lúc có 50 loop đang chạy thì mất cả 50. Đây là lý do phần lớn team bắt đầu từ mô hình này rồi sớm muộn cũng phải chuyển sang mô hình queue bên dưới.
1.3. Serverless
Lambda, Cloud Run, Vercel Functions.
Đặc điểm: scale về 0, trả tiền theo request, và bị chặn cứng bởi timeout. Lambda tối đa 15 phút. Một agent loop gọi 12 lần tool, mỗi lần chờ LLM 8 giây, đã ăn hết 2 phút chỉ riêng phần suy nghĩ.
Hợp với: từng bước riêng lẻ trong workflow, không phải cả vòng lặp. Ví dụ Lambda làm tool executor, còn loop điều phối nằm chỗ khác.
Cạm bẫy: streaming qua Lambda function URL thì được, nhưng qua API Gateway thì buffer. Ai từng debug "tại sao SSE không ra chữ nào cho đến khi xong" đều đã gặp chuyện này.
1.4. Queue và worker pool
Dispatcher nhận request, đẩy job vào Redis Streams hoặc SQS, worker pool tiêu thụ và ghi từng chunk kết quả ngược lại stream. Client đọc stream đó, không đọc trực tiếp từ worker.
Client ──HTTP──▶ Dispatcher ──XADD──▶ redis stream: job:{id}
│
┌──────┴──────┐
worker A worker B
│
XADD chunks ──▶ stream: out:{id}
│
Client ◀────────── SSE ─────────────────────────────┘
Đây là mô hình duy nhất trong danh sách này cho ra durability thật:
- Worker chết giữa chừng thì message chưa
XACK, consumer group giao lại cho worker khác. - Client rớt mạng thì reconnect với
Last-Event-ID, serverXRANGEtừ cursor đó và replay phần đã lỡ. - Deploy không mất job, chỉ mất connection.
// Client reconnect: replay từ cursor rồi mới stream tiếp
async *resume(jobId: string, lastEventId?: string) {
const from = lastEventId ?? '0';
const missed = await this.redis.xrange(`out:${jobId}`, from, '+');
for (const [id, fields] of missed) {
yield { id, data: fields[1] };
}
yield* this.subscribe(jobId);
}
Cạm bẫy: phức tạp hơn hẳn. Bạn phải tự lo idempotency (worker chạy lại một job đã gọi tool "gửi email" thì sao?), phải nghĩ về ordering, phải có TTL cho stream kẻo Redis phình. Đừng bắt đầu từ đây nếu chưa thật sự cần.
1.5. Managed agent runtime
Bedrock AgentCore, OpenAI Responses/Agents API, Vertex AI Agent Engine, Claude Agent SDK. Nhà cung cấp lo hộ vòng lặp, session state, sandbox, đôi khi cả memory.
Đặc điểm: đi từ 0 đến demo trong một buổi chiều. Session dài hơn hẳn serverless, có sẵn observability.
Cạm bẫy: ba thứ thường vỡ khi lên production nghiêm túc.
- BYOK. Nếu sản phẩm của bạn cho khách hàng dùng API key của chính họ, phần lớn managed runtime không có chỗ nhét key theo tenant vào runtime của nhà cung cấp.
- Lock-in ở tầng state. Session state nằm trong hệ thống của họ, migrate đi rất đau.
- Kiểm soát loop. Bạn muốn chèn một bước validate giữa tool call và execution? Tuỳ runtime, có thể không có hook nào cho việc đó.
Kiểm tra tài liệu mới nhất trước khi chốt, mảng này thay đổi từng quý.
Bảng so sánh nhanh
| Runtime | Thời gian chạy | Durable? | Resume? | Multi-tenant | Độ phức tạp |
|---|---|---|---|---|---|
| CLI | Phiên làm việc | Không | Không | Không | Rất thấp |
| Container | Không giới hạn thực tế | Không | Không | Có | Thấp |
| Serverless | ≤ 15 phút | Không | Không | Có | Thấp |
| Queue + worker | Không giới hạn | Có | Có | Có | Cao |
| Managed | Theo nhà cung cấp | Có | Có | Tuỳ | Trung bình |
Trục 2: Kiến trúc vòng lặp
Runtime trả lời "chạy ở đâu". Trục này trả lời "chạy như thế nào".
2.1. Single-loop ReAct
Đúng đoạn code ở đầu bài. Một model, một context window, lặp đến khi không còn tool call.
Đây là mặc định đúng cho khoảng 80% trường hợp. Dễ debug vì chỉ có một luồng suy luận để đọc, chi phí dễ dự đoán, không có chuyện agent này chờ agent kia.
Giới hạn: context window. Loop dài, tool trả về nhiều dữ liệu, và đến vòng thứ 20 thì nửa context là output của mấy lần grep không còn liên quan.
2.2. Planner → worker → synthesizer
Một model cắt việc thành các nhánh độc lập, fan-out cho worker pool chạy song song, rồi một model khác gộp kết quả.
┌──▶ worker 1 (context riêng) ──┐
planner ──plan──────┼──▶ worker 2 (context riêng) ──┼──▶ synthesizer ──▶ output
└──▶ worker 3 (context riêng) ──┘
Lợi: giảm latency đuôi khi các nhánh thật sự độc lập. Ba nhánh mỗi nhánh 20 giây chạy song song ra 20 giây thay vì 60. Mỗi worker có context window riêng nên không bị tràn.
Hại: tốn token hơn nhiều, vì planner và synthesizer đều phải đọc lại đề bài. Và nó chỉ có lợi khi các nhánh độc lập thật. Nếu worker 2 cần kết quả của worker 1 thì fan-out chẳng giúp được gì, chỉ thêm một hop.
Câu hỏi cần trả lời trước khi chọn: bạn có thể viết ra được cái plan bằng tay không? Nếu có, có lẽ bạn cần workflow cố định ở mục 2.4 chứ không cần planner LLM.
2.3. Multi-agent / subagent
Mỗi agent có system prompt, tool set và context riêng, gọi nhau như gọi tool.
Lợi ích thật sự nằm ở cô lập context, không phải ở "chuyên môn hoá". Một subagent chuyên đọc log có thể ngốn 100k token rồi trả về ba dòng tóm tắt cho agent cha, phần rác không bao giờ chạm vào context chính.
Hại: lỗi lan truyền rất khó truy vết. Agent cha nhận một tóm tắt sai từ agent con, và bạn phải đọc ngược ba tầng trace để biết chỗ nào bịa ra. Đầu tư vào tracing trước khi đầu tư vào tầng thứ ba.
2.4. Workflow cố định
Step Functions, Temporal, LangGraph với graph tĩnh. Các bước do con người định nghĩa, LLM chỉ điền vào chỗ trống.
Đây là lựa chọn bị đánh giá thấp nhất. Nếu quy trình đã biết trước, ví dụ "đọc ticket → tìm code liên quan → sinh patch → chạy test → mở PR", thì để LLM tự quyết thứ tự chẳng thêm giá trị gì, chỉ thêm chỗ để sai. Đổi lại bạn được retry theo từng bước, replay, và observability gần như miễn phí.
Nguyên tắc thực dụng: dùng agent ở chỗ không đoán trước được đường đi, dùng workflow ở mọi chỗ còn lại. Rất nhiều hệ thống gọi là "agent" thực ra là workflow có một bước gọi LLM.
Trục 3: Cách kích hoạt
Trục này quyết định nhu cầu persistence, và người ta hay bỏ quên nó.
- Interactive (SSE/WebSocket): có người ngồi chờ. Cần streaming, cần resume, cần cancel. Đắt nhất về mặt hạ tầng.
- Cron / scheduled: không ai chờ. Không cần streaming, chỉ cần ghi kết quả và báo khi xong. Đơn giản hơn hẳn.
- Event-driven (webhook): PR mở ra, message Slack, file upload lên S3. Cần idempotency vì webhook được gửi lại là chuyện bình thường.
- CI pipeline: agent chạy trong GitHub Actions, có timeout tự nhiên và log sẵn. Sandbox tốt để thử nghiệm trước khi đưa vào sản phẩm.
Một agent thường phải phục vụ nhiều kiểu trigger. Tách phần lõi (loop + tool) khỏi phần transport ngay từ đầu, đừng để logic SSE lẫn vào trong loop.
Trục 4: Cấp tool cho agent bằng cách nào
4.1. Function calling trực tiếp
Schema hardcode trong code, executeTool là một switch. Nhanh, ít tầng, không có gì bí ẩn.
Giới hạn: tool schema chiếm chỗ trong mọi request. Ba mươi tool là vài nghìn token trả tiền cho mỗi lượt, kể cả lượt chỉ chào hỏi.
4.2. MCP qua stdio
MCP server chạy như child process, giao tiếp qua stdin/stdout. Hợp với CLI và môi trường local, vì tool chạy cùng máy với agent, thừa hưởng quyền của user.
4.3. MCP remote qua HTTP
Server độc lập, có OAuth, nhiều client dùng chung. Đây là hướng đúng khi tool cần chia sẻ giữa nhiều agent, nhiều team, hoặc nhiều tenant.
Phần khó không nằm ở protocol mà ở authorization. Vài câu hỏi phải trả lời trước khi mở một MCP server ra ngoài:
- Token của user cuối hay của service? Nếu là user, đường đi từ agent đến MCP server đến API đích giữ được identity đó không?
- Scope tối thiểu là gì? Một tool đọc lịch không có lý do gì cần quyền ghi Drive.
- Consent hiển thị cho user lúc nào, và họ có hiểu mình đang đồng ý cho cái gì không?
Đây là chỗ mà "làm cho chạy" và "làm cho an toàn" cách nhau rất xa.
4.4. Code execution thay cho tool
Thay vì khai báo 40 tool, cho agent một sandbox và một SDK, để nó viết code gọi API. Schema từ vài nghìn token xuống còn một tool duy nhất, và agent có thể compose (lọc, gộp, lặp) mà không cần round-trip qua model sau mỗi bước.
Đổi lại bạn phải có sandbox thật sự cách ly. Agent sinh code là agent có thể sinh code gọi ra internet, đọc biến môi trường, hoặc lặp vô hạn.
Những thứ thật sự quyết định lựa chọn
Sau khi đi qua bốn trục, những câu hỏi dưới đây mới là thứ tách một prototype khỏi một hệ thống dùng được.
Durability. Process chết ở vòng thứ bảy thì sao? Nếu câu trả lời là "user gõ lại", bạn đang chạy prototype, và điều đó hoàn toàn ổn cho đến khi nó không ổn nữa.
Resume. User đóng tab rồi mở lại có thấy tiếp không? Muốn có tính năng này thì output phải nằm ở nơi có thể replay theo cursor, không thể là buffer trong RAM.
Idempotency. Retry là chuyện chắc chắn xảy ra. Tool nào có side effect thì phải có idempotency key, nếu không một cú retry sẽ gửi email hai lần.
Observability. Token vào ra mỗi bước, tool nào được gọi với input gì, tại sao loop dừng. Không có ba thứ này thì mọi bug đều thành chuyện đoán mò. Log toàn bộ conversation là chưa đủ, bạn cần trace có cấu trúc.
Chi phí. Vòng lặp gửi lại toàn bộ lịch sử mỗi lượt. Chi phí tăng theo bình phương độ dài hội thoại, không phải tuyến tính. Đặt trần số vòng lặp và trần token ngay từ ngày đầu, không phải sau khi nhận hoá đơn.
Multi-tenant key. Nếu khách hàng dùng key của họ, key phải được mã hoá khi lưu, chỉ giải mã đúng lúc gọi API, và không bao giờ xuất hiện trong log hay trong trace. Chuyện này ảnh hưởng ngược lên lựa chọn runtime, vì managed runtime thường không cho bạn chỗ để làm việc đó.
Chọn thế nào
Một đường đi thực dụng:
- Bắt đầu bằng single-loop trong một container, tool khai báo trực tiếp, trigger là SSE. Ít tầng nhất, học được nhiều nhất về bài toán của bạn.
- Khi loop bắt đầu chạy quá một phút hoặc user phàn nàn mất hội thoại: chuyển output sang stream có cursor để resume được. Chưa cần đổi kiến trúc loop.
- Khi deploy làm mất job: đưa loop vào worker pool sau queue.
- Khi context tràn hoặc latency đuôi quá tệ: mới tính đến fan-out hoặc subagent. Không phải trước đó.
- Ở bất kỳ bước nào, nếu bạn viết được plan bằng tay: bỏ agent, dùng workflow.
Điều dễ nhầm nhất là tưởng agent phức tạp hơn thì tốt hơn. Phần lớn giá trị nằm ở một vòng lặp đơn giản có tool tốt và observability tử tế. Phần phức tạp còn lại là để giải quyết những vấn đề bạn chưa chắc đã có.
Nếu bạn đang chạy agent theo cách khác với năm kiểu ở trên, mình rất muốn nghe.