0

Before You Build an AI Agent, Try an If-Statement First

Tuần này trên Dev.to có một bài viết khá thẳng: Half the AI agents in production are if-statements with a GPU bill. Đọc xong mình vừa cười vừa hơi chột dạ, vì chính team mình cũng từng làm y như vậy. Đầu năm nay tụi mình build một "AI agent" để phân loại ticket support. Nó có tool calling, có memory, có vòng lặp reasoning đầy đủ. Ba tháng sau mình ngồi phân tích log thì thấy khoảng 70% ticket có thể phân loại bằng vài regex và một bảng keyword. Mỗi request tốn 2-4 giây và vài cent, trong khi một câu if chạy chưa tới 1ms và gần như không tốn gì.

Bài này không nhằm nói AI vô dụng. Mình chỉ muốn chia sẻ một cách làm thực tế hơn: rule trước, LLM sau, agent là lựa chọn cuối cùng. Kèm theo là cách đo để biết hệ thống của bạn đang nằm ở đâu.

Agent thật sự là gì, và khi nào bạn không cần nó

Định nghĩa agent mỗi người nói một kiểu, nhưng về mặt kỹ thuật thì mình hiểu thế này: agent là hệ thống mà LLM tự quyết định bước tiếp theo trong một vòng lặp. Nó chọn tool nào để gọi, gọi khi nào, và lúc nào thì dừng.

Nếu flow của bạn luôn là "nhận input → phân loại → gọi đúng 1 API → trả kết quả" thì đó không phải agent. Đó là một pipeline có một bước dùng LLM. Và đa số bài toán business thực tế đều có dạng này.

Mình hay tự hỏi 3 câu trước khi viết dòng code nào:

  1. Input có cấu trúc hay gần có cấu trúc không? Ví dụ như email có subject cố định, form có dropdown, log có format rõ ràng. Nếu có thì rule xử lý được phần lớn.
    1. Số nhánh xử lý có hữu hạn không? Nếu chỉ có 5-10 loại action thì một bảng mapping là đủ.
    1. Sai thì mất gì? Nếu sai một lần là mất tiền hoặc mất data, bạn cần logic deterministic mà test được, chứ đừng để LLM "suy nghĩ".
flowchart TD
    A[Bài toán mới] --> B{Input có cấu trúc?}
        B -->|Có| C[Rule / Regex / Lookup table]
            B -->|Không| D{Số nhánh hữu hạn?}
                D -->|Có| E[LLM classify 1 lần + structured output]
                    D -->|Không| F{Cần nhiều bước, tự chọn tool?}
                        F -->|Không| E
                            F -->|Có| G[Agent loop + guardrails]
                            ```
                            
                            ## Pattern: Rule-first, LLM-fallback
                            
                            Đây là pattern mình dùng nhiều nhất. Rule rẻ và nhanh nên cho chạy trước. Chỉ khi không rule nào match thì mới gọi LLM. Ví dụ bằng Python 3.12 với Pydantic 2.x:
                            
                            ```python
                            import re
                            import time
                            import logging
                            from pydantic import BaseModel, ValidationError
                            
                            log = logging.getLogger('router')
                            
                            class Route(BaseModel):
                                category: str
                                    source: str  # 'rule' hoặc 'llm'
                                    
                                    RULES = [
                                        (re.compile(r'(hoàn tiền|refund|trả lại tiền)', re.I), 'billing'),
                                            (re.compile(r'(quên mật khẩu|reset password|không đăng nhập)', re.I), 'auth'),
                                                (re.compile(r'(đơn hàng|order|giao hàng|shipping)', re.I), 'shipping'),
                                                ]
                                                
                                                ALLOWED = {'billing', 'auth', 'shipping', 'other'}
                                                
                                                def route_ticket(text: str, call_llm) -> Route:
                                                    start = time.perf_counter()
                                                        for pattern, category in RULES:
                                                                if pattern.search(text):
                                                                            log.info('route source=rule cat=%s ms=%.1f', category,
                                                                                                 (time.perf_counter() - start) * 1000)
                                                                                                             return Route(category=category, source='rule')
                                                                                                             
                                                                                                                 # Fallback: chỉ gọi LLM khi rule không match
                                                                                                                     raw = call_llm(
                                                                                                                             'Phân loại ticket vào đúng 1 trong: billing, auth, shipping, other. '
                                                                                                                                     'Chỉ trả về JSON dạng {"category": "..."}.\n\n' + text
                                                                                                                                         )
                                                                                                                                             try:
                                                                                                                                                     result = Route.model_validate_json(raw.replace('}', ', "source": "llm"}', 1))
                                                                                                                                                             if result.category not in ALLOWED:
                                                                                                                                                                         raise ValueError(result.category)
                                                                                                                                                                             except (ValidationError, ValueError):
                                                                                                                                                                                     result = Route(category='other', source='llm')
                                                                                                                                                                                     
                                                                                                                                                                                         log.info('route source=llm cat=%s ms=%.1f', result.category,
                                                                                                                                                                                                      (time.perf_counter() - start) * 1000)
                                                                                                                                                                                                          return result
                                                                                                                                                                                                          ```
                                                                                                                                                                                                          
                                                                                                                                                                                                          Có vài điểm mình muốn nhấn mạnh:
                                                                                                                                                                                                          
                                                                                                                                                                                                          - **Output của LLM luôn phải được validate.** Nó hoàn toàn có thể trả về `"Billing "` hay `"thanh toán"`. Không validate thì bug sẽ lọt xuống downstream mà bạn không hề biết.
                                                                                                                                                                                                          - **Luôn có giá trị mặc định an toàn** (ở đây là `other`) để chuyển cho người xử lý.
                                                                                                                                                                                                          - **Log field `source` ngay từ ngày đầu.** Phần tiếp theo sẽ cần tới nó.
                                                                                                                                                                                                          
                                                                                                                                                                                                          ```mermaid
                                                                                                                                                                                                          sequenceDiagram
                                                                                                                                                                                                              participant U as User
                                                                                                                                                                                                                  participant R as Router
                                                                                                                                                                                                                      participant L as LLM
                                                                                                                                                                                                                          participant H as Handler
                                                                                                                                                                                                                              U->>R: Ticket text
                                                                                                                                                                                                                                  alt Rule match
                                                                                                                                                                                                                                          R->>H: category (source=rule, ~1ms)
                                                                                                                                                                                                                                              else Không match
                                                                                                                                                                                                                                                      R->>L: Prompt classify
                                                                                                                                                                                                                                                              L-->>R: JSON
                                                                                                                                                                                                                                                                      R->>R: Validate + default
                                                                                                                                                                                                                                                                              R->>H: category (source=llm, ~2s)
                                                                                                                                                                                                                                                                                  end
                                                                                                                                                                                                                                                                                  ```
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  ## Đo trước khi tin: log cho bạn biết sự thật
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  Chạy khoảng một tuần rồi mở log ra xem. Phần này là phần quan trọng nhất, vì nó cho bạn biết LLM có thực sự cần thiết hay không. Dùng `jq` 1.7 nếu log là JSON, còn log text như ở trên thì `grep` và `awk` là đủ:
                                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                  ```bash
                                                                                                                                                                                                                                                                                  # Tỷ lệ request đi qua rule và qua LLM
                                                                                                                                                                                                                                                                                  grep 'route source=' app.log \
                                                                                                                                                                                                                                                                                    | awk '{for(i=1;i<=NF;i++) if($i ~ /^source=/) print $i}' \
                                                                                                                                                                                                                                                                                      | sort | uniq -c | sort -rn
                                                                                                                                                                                                                                                                                      
                                                                                                                                                                                                                                                                                      # Trong các case đi qua LLM, category nào xuất hiện nhiều nhất?
                                                                                                                                                                                                                                                                                      grep 'route source=llm' app.log \
                                                                                                                                                                                                                                                                                        | grep -o 'cat=[a-z]*' | sort | uniq -c | sort -rn
                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                        # Latency p95 của nhánh LLM
                                                                                                                                                                                                                                                                                        grep 'route source=llm' app.log | grep -o 'ms=[0-9.]*' \
                                                                                                                                                                                                                                                                                          | cut -d= -f2 | sort -n \
                                                                                                                                                                                                                                                                                            | awk '{a[NR]=$1} END {print "p95:", a[int(NR*0.95)]}'
                                                                                                                                                                                                                                                                                            ```
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            Nếu bạn thấy LLM trả về `shipping` cho 40% số case fallback thì gần như chắc chắn rule `shipping` đang thiếu keyword. Lấy mẫu vài chục ticket đó, đọc bằng mắt rồi thêm rule. Cứ lặp lại như vậy mỗi tuần, tỷ lệ gọi LLM sẽ giảm dần mà chất lượng vẫn giữ nguyên.
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            Team mình sau 4 vòng như vậy đã giảm từ 100% request gọi LLM xuống còn khoảng 18%. Chi phí API giảm hơn 5 lần, p50 latency từ 2.1s xuống dưới 10ms. Thứ quý nhất lại là **test được**: rule thì viết unit test bằng `pytest` bình thường, không cần mock LLM hay chấp nhận kết quả "lúc đúng lúc sai".
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            ## Khi nào thật sự cần agent loop
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            Mình không chống agent. Có những bài toán mà agent đáng đồng tiền:
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            - **Số bước không biết trước.** Ví dụ debug một incident, cần đọc log, query DB, xem metric, rồi quyết định đào tiếp chỗ nào.
                                                                                                                                                                                                                                                                                            - **Không gian tool lớn và kết hợp linh hoạt.** Ví dụ coding assistant cần đọc file, chạy test, sửa code, rồi chạy lại.
                                                                                                                                                                                                                                                                                            - **Input hoàn toàn tự do** mà rule không cover nổi.
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            Kể cả khi đó, bạn vẫn nên bọc agent bằng những rule cứng. Đây là ví dụ guardrail đơn giản bằng Node.js 22 và Zod 3.x:
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            ```javascript
                                                                                                                                                                                                                                                                                            import { z } from 'zod';
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            const MAX_STEPS = 8;
                                                                                                                                                                                                                                                                                            const ALLOWED_TOOLS = new Set(['search_logs', 'query_metrics', 'read_file']);
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                            const ToolCall = z.object({
                                                                                                                                                                                                                                                                                              tool: z.string(),
                                                                                                                                                                                                                                                                                                args: z.record(z.unknown()),
                                                                                                                                                                                                                                                                                                });
                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                export async function runAgent(task, llmStep, tools) {
                                                                                                                                                                                                                                                                                                  const history = [];
                                                                                                                                                                                                                                                                                                    for (let step = 0; step < MAX_STEPS; step++) {
                                                                                                                                                                                                                                                                                                        const raw = await llmStep(task, history);
                                                                                                                                                                                                                                                                                                            if (raw.done) return raw.answer;
                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                const parsed = ToolCall.safeParse(raw);
                                                                                                                                                                                                                                                                                                                    if (!parsed.success || !ALLOWED_TOOLS.has(parsed.data.tool)) {
                                                                                                                                                                                                                                                                                                                          history.push({ error: 'invalid tool call, try again' });
                                                                                                                                                                                                                                                                                                                                continue; // không bao giờ execute tool lạ
                                                                                                                                                                                                                                                                                                                                    }
                                                                                                                                                                                                                                                                                                                                        const result = await tools[parsed.data.tool](parsed.data.args);
                                                                                                                                                                                                                                                                                                                                            history.push({ call: parsed.data, result });
                                                                                                                                                                                                                                                                                                                                              }
                                                                                                                                                                                                                                                                                                                                                throw new Error(`Agent vượt quá ${MAX_STEPS} bước, chuyển cho người xử lý`);
                                                                                                                                                                                                                                                                                                                                                }
                                                                                                                                                                                                                                                                                                                                                ```
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                Nhìn kỹ thì trong agent này vẫn đầy `if`: giới hạn số bước, whitelist tool, validate schema. Agent tốt không phải là agent không có if-statement. Agent tốt là agent mà **những quyết định quan trọng nằm trong code, còn LLM chỉ lo phần thật sự cần đến ngôn ngữ tự nhiên**.
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                ## Kết luận
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                Một bài khác đang trending trên HN là *The problem is not the AI code, but nobody knows anything anymore*. Mình nghĩ nó liên quan trực tiếp đến chuyện này. Khi mọi quyết định đều giao cho LLM, không ai trong team còn hiểu vì sao hệ thống lại hành xử như vậy. Rule thì đọc được, review được, test được.
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                Những việc bạn có thể làm ngay tuần này:
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                1. **Thêm field `source` vào log** cho mọi chỗ đang gọi LLM, và phân biệt rõ rule với llm.
                                                                                                                                                                                                                                                                                                                                                2. **Chạy script `grep`/`awk` ở trên** sau một tuần để xem phân bố thực tế.
                                                                                                                                                                                                                                                                                                                                                3. **Mỗi tuần chuyển top pattern từ nhánh LLM sang rule**, kèm unit test.
                                                                                                                                                                                                                                                                                                                                                4. **Luôn validate output của LLM** bằng Pydantic hoặc Zod, và luôn có giá trị default an toàn.
                                                                                                                                                                                                                                                                                                                                                5. **Chỉ dùng agent loop khi số bước thật sự không biết trước**, và lúc đó phải có `MAX_STEPS` cùng whitelist tool.
                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                Đừng ngại viết if-statement. Nó không làm sản phẩm của bạn kém "AI" đi. Nó chỉ làm sản phẩm chạy nhanh hơn, rẻ hơn, và giúp bạn ngủ ngon hơn khi đang on-call.

All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.