AI-Native Solutions Architect

Advanced Prompting: Chain of Thought, Tree of Thoughts, ReAct cho lập trình

7/19/2026 · 15p đọc


title: "Advanced Prompting: Chain of Thought, Tree of Thoughts, ReAct cho lập trình"
series: "AI-Native Solutions Architect: Từ Coder đến Kiến trúc sư AI"
season: "Season 1 — AI-Native Coding & Tooling"
order: 3
audience: "Software Engineer hướng tới Solutions Architect"
reading_time: "12 phút"
tags: ["prompt-engineering", "chain-of-thought", "tree-of-thoughts", "react", "llm-reasoning", "ai-agent", "solutions-architect"]

Advanced Prompting: Chain of Thought, Tree of Thoughts, ReAct cho lập trình

Bạn gõ "viết giúp tôi hàm rate limiter cho API" vào Cursor hay Copilot Chat. Ba giây sau có code: một Map đếm request, so sánh với ngưỡng, trả 429 nếu vượt. Nhìn qua thì ổn. Nhưng nó không xử lý concurrent request, không tính đến việc scale ngang nhiều instance, không nói gì về sliding window hay token bucket, và không hỏi bạn cần rate limit theo IP hay theo user ID. Nó chỉ trả lời câu hỏi đầu tiên xuất hiện trong "đầu" — pattern phổ biến nhất trong training data cho từ khóa "rate limiter".

Đây không phải lỗi của model. Đó là hệ quả trực tiếp của cách bạn hỏi. LLM là một cỗ máy dự đoán token tiếp theo, và mặc định nó tối ưu cho phản hồi nhanh, ăn khớp bề mặt với prompt — giống một junior engineer trả lời phỏng vấn bằng giải pháp đầu tiên nghĩ ra thay vì dừng lại phân tích trade-off. Với bug đơn giản hay boilerplate, phản xạ đó đủ dùng. Với bài toán có nhiều ràng buộc cạnh tranh nhau — đúng vs. nhanh vs. dễ bảo trì vs. chịu tải — bạn cần ép model đi qua một quy trình suy luận có cấu trúc trước khi chốt câu trả lời.

Đó là lý do Solutions Architect cần vượt qua "prompt hội thoại" để nắm vững ba kỹ thuật đến từ chính literature về LLM reasoning: Chain-of-Thought, Tree-of-Thoughts, và ReAct. Đây không phải mẹo vặt — đây là cách bạn kiểm soát quá trình suy luận của model thay vì chỉ kiểm soát câu hỏi đưa vào. Bài này đi thẳng vào cách áp dụng ba kỹ thuật đó cho một bài toán kỹ thuật cụ thể: thiết kế thuật toán rate-limiting cho API.

Vấn đề

Khi bạn ném một yêu cầu mơ hồ, nhiều ràng buộc vào một prompt ngắn, model có hai lựa chọn ngầm: (1) chọn diễn giải phổ biến nhất và trả lời ngay, hoặc (2) hỏi lại để làm rõ. Phần lớn thời gian, đặc biệt với các model được tune để "hữu ích" và phản hồi nhanh, nó chọn (1). Kết quả là code chạy được nhưng sai ngữ cảnh — đúng cú pháp, sai bài toán.

Vấn đề trầm trọng hơn ở ba loại bài toán mà kỹ sư hay giao cho AI một cách hời hợt:

Thiết kế thuật toán có nhiều ràng buộc. Rate limiting là ví dụ kinh điển: bạn cần cân bằng giữa độ chính xác (không cho vượt quota), độ trễ (không làm chậm hot path), khả năng scale (nhiều instance cùng đọc/ghi state), và chi phí vận hành (Redis thêm round-trip, in-memory thì không consistent). Không có "câu trả lời đúng" duy nhất — chỉ có các đánh đổi khác nhau tùy ngữ cảnh (traffic pattern, SLA, budget). Model trả lời phản xạ sẽ chọn một thuật toán (thường là token bucket vì nó phổ biến trong tutorial) mà không hề nói cho bạn biết tại sao không chọn sliding window log hay leaky bucket.

Debug lỗi khó. Một stack trace hiếm khi tự nói lên nguyên nhân gốc. Model phản xạ sẽ nhìn dòng lỗi cuối cùng và đề xuất fix bề mặt (thêm null check, catch exception) mà không truy ngược chuỗi nhân quả.

Quyết định kiến trúc. "Nên dùng message queue hay gọi API trực tiếp?" là câu hỏi không có câu trả lời đúng tuyệt đối — nó phụ thuộc vào throughput, consistency requirement, độ phức tạp vận hành chấp nhận được. Model phản xạ sẽ chọn một phương án nghe "best practice" nhất mà không phân tích trade-off theo ngữ cảnh cụ thể của bạn.

Cái chung của cả ba: bài toán cần suy luận nhiều bước hoặc so sánh nhiều phương án trước khi chốt. Nếu bạn không yêu cầu rõ trong prompt, model sẽ bỏ qua bước đó — không phải vì nó "không biết", mà vì bạn không tạo áp lực buộc nó phải thể hiện suy luận trước khi kết luận.

Kỹ thuật cốt lõi

Ba kỹ thuật dưới đây đều xuất phát từ nghiên cứu về LLM reasoning (Wei et al. 2022 cho CoT, Yao et al. 2023 cho ToT và ReAct), nhưng điều quan trọng với vai trò Solutions Architect không phải là thuộc paper — mà là biết khi nào dùng cái nào khi làm việc với AI coding assistant.

1. Chain-of-Thought (CoT) — suy luận tuần tự trước khi kết luận

CoT ép model trình bày các bước suy luận trung gian trước khi đưa ra câu trả lời cuối. Về bản chất, bạn đang khai thác một đặc tính của kiến trúc transformer autoregressive: model dự đoán token tiếp theo dựa trên toàn bộ context đã sinh ra trước đó — kể cả context do chính nó vừa viết. Nếu bạn để nó viết ra bước suy luận trước, nó sẽ "tự neo" (self-condition) vào lập luận đó, giảm khả năng nhảy thẳng đến một mẫu code quen thuộc mà bỏ qua ràng buộc riêng của bài toán.

flowchart LR
    A["Prompt: yêu cầu trực tiếp"] --> B["Model dự đoán pattern quen thuộc"] --> C["Code output (có thể sai ngữ cảnh)"]

    D["Prompt: yêu cầu CoT"] --> E["Bước 1: phân tích ràng buộc"] --> F["Bước 2: liệt kê lựa chọn kỹ thuật"] --> G["Bước 3: chọn & giải thích"] --> H["Code output (bám ngữ cảnh)"]

CoT phù hợp khi bài toán có một hướng giải quyết tương đối rõ nhưng nhiều chi tiết dễ bỏ sót — ví dụ viết một thuật toán cụ thể, tính toán complexity, hoặc trace qua một logic nhiều nhánh. Nó không phù hợp khi bạn thực sự cần so sánh nhiều kiến trúc khác nhau — đó là lúc cần ToT.

2. Tree-of-Thoughts (ToT) — khám phá song song nhiều nhánh giải pháp

CoT đi theo một đường thẳng suy luận. ToT mở rộng thành một cây: tại mỗi bước, model sinh ra nhiều nhánh suy luận khác nhau, tự đánh giá từng nhánh, rồi mới chọn nhánh tốt nhất (hoặc kết hợp). Trong nghiên cứu gốc, ToT dùng search có backtracking thực sự qua nhiều lệnh gọi model. Trong thực hành hàng ngày với chatbot/coding assistant, bạn không cần implement search engine — bạn có thể mô phỏng hiệu ứng đó bằng một prompt duy nhất yêu cầu model tự sinh N phương án, tự phản biện, rồi chọn.

flowchart TD
    Root["Bài toán: rate limiting"] --> A["Nhánh A: Token Bucket"]
    Root --> B["Nhánh B: Sliding Window Log"]
    Root --> C["Nhánh C: Fixed Window Counter"]
    A --> AE["Đánh giá: burst tolerance tốt, tốn state"]
    B --> BE["Đánh giá: chính xác nhất, tốn memory"]
    C --> CE["Đánh giá: rẻ nhất, có edge burst ở biên window"]
    AE & BE & CE --> Pick["So sánh chéo -> chọn theo ngữ cảnh"]

ToT phù hợp cho quyết định kiến trúc có đánh đổi thực sự — nơi không có phương án "đúng tuyệt đối", chỉ có phương án phù hợp với ràng buộc cụ thể (SLA, traffic, ngân sách vận hành). Cái giá phải trả: tốn nhiều token hơn CoT vì model phải viết ra nhiều nhánh song song. Dùng ToT cho bài toán chỉ có một cách làm hợp lý là lãng phí — đây chính là cạm bẫy sẽ nói ở phần sau.

3. ReAct (Reasoning + Acting) — xen kẽ suy luận và hành động thực

CoT và ToT đều diễn ra hoàn toàn "trong đầu" model — dựa trên thông tin có sẵn trong prompt, không kiểm tra gì bên ngoài. ReAct khác về bản chất: model xen kẽ giữa bước suy luận (reasoning — "tôi nghĩ cần kiểm tra X") và bước hành động (acting — gọi một tool cụ thể: đọc file, chạy lệnh, query database), sau đó quan sát kết quả (observation) rồi suy luận tiếp dựa trên dữ liệu thực vừa thu được. Đây chính là vòng lặp nền tảng đứng sau mọi AI coding agent hiện đại (Cursor Agent, Claude Code, Cline, Windsurf) — dù mỗi công cụ có cách implement và UX riêng, tất cả đều dựa trên vòng lặp Thought → Action → Observation lặp lại.

sequenceDiagram
    participant U as Kỹ sư
    participant A as AI Agent
    participant T as Tool/Filesystem

    U->>A: "Đề xuất cấu hình rate limit cho endpoint /api/orders"
    A->>A: Thought: cần biết config hiện tại trước khi đề xuất
    A->>T: Action: đọc file config rate-limit hiện tại
    T-->>A: Observation: đang dùng fixed window, limit 100 req/phút
    A->>A: Thought: cần biết traffic pattern thực tế
    A->>T: Action: grep log tìm peak request rate
    T-->>A: Observation: peak 340 req/phút vào giờ cao điểm
    A->>A: Thought: fixed window đang gây false-reject ở peak
    A->>U: Đề xuất: chuyển sang token bucket, burst=150, refill=200/phút

Khác biệt cốt lõi so với CoT/ToT: ReAct grounding suy luận vào dữ liệu thực của hệ thống thay vì chỉ suy luận trên giả định. Đây là lý do một AI agent đọc đúng file .env, đúng schema DB, đúng log thực tế sẽ cho đề xuất sát thực tế hơn nhiều so với việc bạn hỏi suông "nên cấu hình rate limit thế nào".

Thực hành

Áp dụng cả ba kỹ thuật cho cùng một bài toán: thiết kế thuật toán rate-limiting cho một API endpoint xử lý đơn hàng (POST /api/orders), chạy trên nhiều instance sau load balancer, dùng Redis làm shared state.

Prompt 1 — Chain-of-Thought (khi cần suy luận từng bước một thuật toán cụ thể)

Dùng khi bạn đã quyết định hướng đi (ví dụ: đã chọn thuật toán token bucket) và cần model triển khai đúng, không bỏ sót edge case.

Tôi cần triển khai rate limiting cho endpoint POST /api/orders bằng
thuật toán Token Bucket, dùng Redis làm shared state vì service chạy
trên nhiều instance sau load balancer.

Hãy suy luận từng bước trước khi viết code, theo đúng thứ tự sau —
không nhảy thẳng vào code:

Bước 1: Liệt kê các race condition có thể xảy ra khi nhiều instance
cùng đọc/ghi token count cho cùng một key trong Redis, và giải thích
tại sao GET-rồi-SET đơn thuần không an toàn.

Bước 2: Nêu cơ chế Redis đảm bảo atomicity cho use case này (ví dụ
Lua script hoặc lệnh atomic có sẵn), giải thích tại sao chọn cơ chế đó.

Bước 3: Xác định các tham số cần cấu hình (bucket capacity, refill
rate, refill interval) và đề xuất giá trị hợp lý cho traffic trung
bình 50 req/phút/user, cho phép burst tối đa 20 request liên tiếp.

Bước 4: Xử lý edge case: user mới chưa có key trong Redis, key hết
hạn (TTL), và Redis tạm thời unavailable (fallback behavior — fail
open hay fail closed, giải thích lựa chọn).

Chỉ sau khi trình bày đủ 4 bước trên, hãy viết code TypeScript
(dùng ioredis) triển khai middleware NestJS cho rate limiter này.

Prompt 2 — Tree-of-Thoughts (khi cần so sánh nhiều phương án kiến trúc)

Dùng khi bạn chưa chốt thuật toán và cần đánh giá đánh đổi giữa các lựa chọn dựa trên ràng buộc thực tế của hệ thống.

Tôi cần chọn thuật toán rate-limiting cho endpoint POST /api/orders.

Bối cảnh:
- Service chạy 4-8 instance sau load balancer (auto-scale theo tải).
- Đã có Redis cluster dùng chung cho session, có thể tận dụng thêm.
- SLA yêu cầu p99 latency của endpoint dưới 150ms, rate limit check
  không được cộng thêm quá 5ms.
- Traffic có tính burst rõ rệt vào giờ mở bán flash sale.
- Business chấp nhận rủi ro "hơi lỏng tay" trong burst ngắn, nhưng
  tuyệt đối không chấp nhận reject nhầm user hợp lệ trong traffic bình thường.

Đề xuất 3 cách tiếp cận khác nhau cho rate limiting (ví dụ: Token
Bucket, Sliding Window Log, Sliding Window Counter, Fixed Window
Counter — chọn 3 trong số này hoặc thuật toán khác bạn thấy phù hợp).

Với mỗi cách tiếp cận, phân tích theo đúng 4 tiêu chí sau, mỗi tiêu
chí 2-3 câu:
1. Độ chính xác trong việc kiểm soát burst so với SLA đề ra
2. Chi phí tính toán và độ trễ thêm vào request (ước lượng, không
   cần benchmark thật)
3. Độ phức tạp khi triển khai đúng trên nhiều instance dùng chung Redis
4. Rủi ro lớn nhất nếu chọn phương án này mà sai (failure mode)

Sau khi phân tích cả 3, hãy so sánh chéo trong một bảng markdown,
rồi chọn phương án tốt nhất cho bối cảnh trên và giải thích rõ TẠI
SAO hai phương án còn lại bị loại — không chỉ nói phương án được
chọn tốt ở đâu.

Luồng ReAct — AI agent tự đọc cấu hình thật trước khi đề xuất

Khác với hai prompt trên (thuần suy luận dựa trên mô tả bằng lời), ReAct chỉ có ý nghĩa khi AI thực sự có quyền gọi tool (đọc file, chạy lệnh) — đây là cách các coding agent như Claude Code, Cursor Agent vận hành. Dưới đây là prompt khởi tạo và luồng các bước agent tự thực hiện:

Hệ thống hiện tại đã có rate limiting sơ khai cho các endpoint khác.
Trước khi đề xuất cấu hình cho POST /api/orders, hãy tự kiểm tra
codebase thực tế thay vì giả định — đọc file cấu hình, xem log traffic
gần nhất, rồi mới đề xuất. Với mỗi bước, nói rõ bạn đang làm gì và tại
sao, trước khi thực hiện.

Luồng thực thi ReAct mà agent sẽ tự đi qua (mỗi dòng là một vòng Thought → Action → Observation):

Thought 1: Cần biết rate limiter hiện có đang cấu hình thế nào
           trước khi đề xuất thay đổi cho endpoint mới.
Action 1:  Đọc file code/be/src/common/guards/rate-limit.guard.ts
Observation 1: Guard hiện dùng @nestjs/throttler, cấu hình global
           TTL=60s, limit=20 request/phút cho MỌI endpoint — không
           override riêng theo route.

Thought 2: 20 req/phút là quá thấp cho checkout flow thật (user có
           thể retry khi thanh toán lỗi). Cần xem traffic thực tế
           của /api/orders để không đề xuất mù quáng.
Action 2:  Chạy lệnh grep log 7 ngày gần nhất để lấy phân phối
           request/phút/user cho route /api/orders
Observation 2: p95 = 4 req/phút/user, nhưng có spike tới 18
           req/phút/user trong 30 giây đầu flash sale (do retry
           storm khi FE bị timeout).

Thought 3: Giới hạn global 20 req/phút đang sát ngưỡng spike thực tế
           — rủi ro reject nhầm đúng lúc traffic cao nhất. Cần
           override riêng cho route này, tách khỏi cấu hình global.
Action 3:  Đọc thêm file code/be/src/modules/orders/orders.controller.ts
           để xem endpoint đã có decorator @Throttle riêng chưa.
Observation 3: Chưa có override, đang dùng nguyên default global.

Kết luận (sau 3 vòng Thought-Action-Observation):
Đề xuất thêm @Throttle({ default: { ttl: 60_000, limit: 40 } })
riêng cho OrdersController, và bổ sung xử lý idempotency-key ở
tầng service để giảm retry storm gốc rễ thay vì chỉ nới rate limit.

Điểm mấu chốt: agent không đề xuất ngay từ Thought 1 — nó buộc mình phải quan sát dữ liệu thật (file config, log) trước khi đưa kết luận ở Thought 3. Nếu bạn chỉ hỏi suông "nên cấu hình rate limit /api/orders bao nhiêu", model sẽ đoán một con số hợp lý về mặt lý thuyết nhưng có thể sai hoàn toàn so với traffic pattern thật của hệ thống bạn.

Cạm bẫy thường gặp

Dùng ToT cho bài toán chỉ có một cách làm hợp lý. Nếu bạn cần viết một hàm validate email hay parse JSON, yêu cầu model "đề xuất 3 cách tiếp cận rồi so sánh" chỉ tạo ra 3 phương án gần như giống hệt nhau về bản chất, tốn gấp 3 lần token và thời gian đọc mà không thêm giá trị. ToT chỉ đáng giá khi thực sự có nhiều phương án khác biệt về đánh đổi — quyết định kiến trúc, chọn thuật toán, chọn công nghệ. Trước khi viết prompt ToT, tự hỏi: "có ít nhất 2 phương án hợp lý mà người có kinh nghiệm cũng tranh cãi không?" — nếu không, dùng CoT hoặc thậm chí prompt trực tiếp là đủ.

Nhầm lẫn "prompt dài" với "prompt có cấu trúc suy luận tốt". Thêm 500 từ ngữ cảnh nền, quy tắc code style, lịch sử dự án vào prompt không tự động khiến model suy luận sâu hơn — nó chỉ thêm context để tham chiếu. CoT hoạt động vì nó ép model sinh ra các bước trung gian, tạo self-conditioning; một prompt dài nhưng chỉ mô tả bối cảnh mà không yêu cầu trình bày từng bước suy luận vẫn có thể nhận lại câu trả lời phản xạ. Điểm khác biệt nằm ở cấu trúc yêu cầu ("hãy làm bước 1, rồi bước 2, rồi mới kết luận"), không nằm ở độ dài.

Tin ReAct output như một chân lý chỉ vì agent "đã đọc file". Agent đọc đúng file không đảm bảo nó diễn giải đúng — nó có thể đọc đúng file config nhưng bỏ sót một điều kiện biên trong log, hoặc gộp nhầm hai route có tên gần giống nhau. Quan sát thực (Observation) là input tốt hơn giả định thuần túy, nhưng vẫn cần bạn review lại chuỗi Thought-Action-Observation trước khi apply thay đổi vào production — đặc biệt với các quyết định ảnh hưởng đến hạ tầng thật (rate limit sai có thể chặn nhầm user thật hoặc mở cửa cho abuse).

🧭 Góc nhìn Solutions Architect
Trước khi viết prompt, tự hỏi: bài toán này có thực sự cần model suy luận nhiều bước hay so sánh nhiều phương án, hay tôi chỉ đang phức tạp hóa một việc đơn giản? Nếu dùng ReAct qua agent, tôi có đủ tự tin rằng các Observation mà agent thu thập được là đầy đủ và đại diện, hay agent đang dừng lại ở bước quan sát đầu tiên "trông có vẻ đủ"? Và cuối cùng — nếu hai kỹ sư senior khác nhau review cùng bối cảnh này, họ có thực sự bất đồng về phương án, hay tôi đang dùng Tree-of-Thoughts chỉ vì nó nghe "advanced"?

🔗 Bài viết liên quan


Bài trước: Context Engineering: .cursorrules và codebase map · Bài tiếp theo: AI-Assisted Debugging: đọc stack trace và log

Advanced Prompting: Chain of Thought, Tree of Thoughts, ReAct cho lập trình