Khi xây dựng các hệ tác nhân AI phức tạp trong thực tế, các framework cũ (như LangChain Chains) bộc lộ hạn chế lớn: chúng chỉ hỗ trợ các luồng xử lý tuyến tính một chiều đơn giản. Nếu Agent cần thực hiện các vòng lặp phi tuyến, tự kiểm tra mã nguồn, rẽ nhánh điều kiện dựa theo kết quả chạy thử, hoặc tạm ngắt để chờ sự phê duyệt của con người, chúng ta cần một kiến trúc đồ thị trạng thái có hướng.

Giải pháp toàn diện là LangGraph — thư viện chuyên dụng để xây dựng các AI Agent dưới dạng đồ thị có trạng thái (Stateful Multi-Agent Graphs). Bài học này sẽ giúp bạn hiểu sâu sắc nguyên lý hoạt động của cấu trúc Node - Edge, cơ chế quản lý trạng thái chung (State Management), kỹ thuật ngắt tương tác con người (Human-in-the-loop), và tự tay lập trình chu trình Agent tự viết code - tự kiểm thử - tự sửa lỗi tuần hoàn.

18.1 Sự ra đời của Graph-based Agent

Một luồng công việc AI thực tế thường không đi theo đường thẳng. Ví dụ, trong quy trình viết code: Mô hình viết code ➔ Biên dịch thử ➔ Nếu lỗi, quay lại sửa code kèm thông báo lỗi ➔ Nếu thành công, chuyển tiếp đi lưu trữ. Đây là một đồ thị có chứa vòng lặp (Cyclic Graph).

LangGraph mô hình hóa Agent dưới dạng một Đồ thị (Graph):

  • Nodes (Nút): Đại diện cho các bước xử lý, là các hàm Python nhận vào trạng thái hiện tại, thực hiện tác vụ (gọi LLM, chạy code) và trả về thông tin cập nhật.
  • Edges (Cạnh): Xác định hướng đi tiếp theo giữa các nút. Có hai loại cạnh: Cạnh cố định (chuyển tiếp thẳng từ nút A sang B) và Cạnh điều kiện (Conditional Edges - rẽ nhánh dựa theo kết quả logic của nút vừa chạy).
⚠️ Cạm bẫy: Spaghetti Graph (Đồ thị mạng nhện)
Khi thiết kế đồ thị cho Agent, rất dễ rơi vào cạm bẫy vẽ quá nhiều nút liên kết chéo, tạo thành một ma trận spaghetti phức tạp cực kỳ khó gỡ lỗi và kiểm soát trạng thái. Quy tắc thiết kế sạch: Chia nhỏ đồ thị khổng lồ thành các đồ thị con (Sub-graphs) độc lập, mỗi sub-graph chịu trách nhiệm hoàn thành một tác vụ chuyên biệt duy nhất.

18.2 Quản lý Trạng thái chung (State Management)

Trái tim của LangGraph là đối tượng State (Trạng thái). State là một cấu trúc dữ liệu chung (Pydantic Model hoặc TypedDict) được truyền đi xuyên suốt qua toàn bộ các nút của đồ thị.

Đặc tính của quản lý trạng thái trong LangGraph:

  • Đồng bộ trạng thái: Mỗi nút khi chạy xong sẽ trả về các trường dữ liệu cập nhật. Hệ thống tự động ghi đè hoặc cộng dồn (Reducer) các trường này vào State chung.
  • Reducer Function: Cho phép định nghĩa cách cập nhật dữ liệu đặc thù. Ví dụ, đối với lịch sử trò chuyện chat, ta dùng reducer operator.add để tự động nối đuôi (append) các tin nhắn mới vào danh sách cũ thay vì ghi đè xóa sạch lịch sử.
🔢 Ví dụ tính tay Reducer operator.add (đã kiểm chứng)
Giả sử trường lịch sử chat trong State hiện có ["Xin chào"]. Một nút vừa chạy xong và trả về tin nhắn mới ["Tôi cần giúp đỡ"]. Chạy thử bằng Python (đã kiểm chứng):
  • Với Reducer operator.add: kết quả là ["Xin chào", "Tôi cần giúp đỡ"] — lịch sử cũ được giữ nguyên, tin nhắn mới được nối đuôi vào.
  • Nếu KHÔNG dùng Reducer (ghi đè mặc định): kết quả chỉ còn ["Tôi cần giúp đỡ"] — toàn bộ lịch sử trò chuyện trước đó bị xóa sạch ngay lập tức.
Đây chính là lý do khai báo đúng Reducer cho từng trường trong State là bắt buộc: mặc định LangGraph ghi đè (overwrite), nên bất kỳ trường nào cần "cộng dồn" theo thời gian (lịch sử chat, log lỗi tích lũy) đều phải khai báo Reducer tường minh, nếu không dữ liệu quan trọng sẽ âm thầm biến mất sau mỗi lượt chạy nút.

18.3 Cơ chế Con người phê duyệt (Human-in-the-loop)

Trong môi trường sản xuất của doanh nghiệp, việc để AI tự trị hoàn toàn 100% đưa ra các hành động quan trọng (như gửi email trực tiếp cho khách hàng, thanh toán hóa đơn tài chính, hoặc lưu trực tiếp file code lên server chạy thật) là cực kỳ nguy hiểm.

Cơ chế Human-in-the-loop (Con người phê duyệt) cho phép ta đặt một điểm ngắt (Interrupt) ngay trước khi đồ thị chuyển tới các nút nhạy cảm. Đồ thị sẽ tự động đóng băng trạng thái hiện tại vào đĩa cứng (State Persistence). Hệ thống tạm dừng, gửi thông báo cho người quản trị. Sau khi con người nhấn "Phê duyệt" hoặc chỉnh sửa trực tiếp nội dung trong State, đồ thị mới được kích hoạt chạy tiếp tục từ điểm ngắt.

⚡ Ranh giới an toàn: Chỉ phê duyệt sau khi kiểm định
Cơ chế Human-in-the-loop là chốt chặn bảo mật cuối cùng chống lại sự mất kiểm soát của LLM (Hallucination nguy hại). Một mô hình Agent viết code tốt bắt buộc phải có bước chạy thử trong môi trường hộp cát cô lập (Sandbox) để tự phát hiện lỗi cú pháp trước khi gửi đơn yêu cầu con người phê duyệt lưu file.

18.4 Dự án thực hành bài 18: Đại lý tự động viết - chạy thử - sửa lỗi code

Dự án thực hành của bài học này là xây dựng một Graph Engine có trạng thái giả lập bằng Python để triển khai luồng Agent giải toán thông minh:

Sơ đồ Đồ thị hoạt động:
StartNode Coder (Viết hàm Python) ➔ Node Tester (Chạy thử Unit Test) ➔ Rẽ nhánh điều kiện:
  • Nếu chạy Test lỗi: Quay lại Node Coder kèm Log lỗi để sửa (tối đa 3 lần).
  • Nếu chạy Test thành công: Chuyển sang Node Approver (Human-in-the-loop).
Node Approver ➔ Chờ người dùng nhấn "Phê duyệt" ➔ Xuất file code hoàn chỉnh.
langgraph_agent.py
import urllib.request
import json
import sys
import re

# 1. Định nghĩa cấu trúc State (Trạng thái đồ thị)
class AgentState:
    def __init__(self, task_description):
        self.task = task_description
        self.code = ""
        self.test_log = ""
        self.error_count = 0
        self.approved = False
 
# 2. Định nghĩa các Nodes (Nút xử lý hành động)
def coder_node(state: AgentState):
    print("\n[Node Coder]: Đang viết code Python để giải quyết bài toán...")
    
    url = "http://localhost:11434/api/chat"
    
    # Prompt yêu cầu sinh code sạch, không chứa markdown giải thích ngoài khối code
    prompt = f"""Bạn là một lập trình viên Python chuyên nghiệp. Hãy viết duy nhất một hàm Python có tên là `solve_problem()` giải quyết yêu cầu sau:
Yêu cầu: {state.task}
 
Lưu ý: Chỉ trả về duy nhất code Python bên trong cặp thẻ markdown ```python và ```. Không giải thích gì thêm.
"""
    # Nếu có lỗi từ lượt chạy trước, đưa log lỗi vào prompt để Coder sửa
    if state.test_log:
        prompt += f"\nLần chạy trước bị lỗi Unit Test sau đây, hãy sửa lại code:\nLog lỗi: {state.test_log}"
 
    payload = {
        "model": "llama3",
        "messages": [{"role": "user", "content": prompt}],
        "stream": False
    }
    
    try:
        data = json.dumps(payload).encode("utf-8")
        req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
        with urllib.request.urlopen(req) as response:
            res = json.loads(response.read().decode("utf-8"))
            content = res["message"]["content"]
            
            # Trích xuất đoạn code Python nằm giữa ```python ... ```
            code_match = re.search(r'```python(.*?)```', content, re.DOTALL)
            if code_match:
                state.code = code_match.group(1).strip()
            else:
                state.code = content.strip()
    except Exception as e:
        # Fallback giả lập sinh code nếu không có Ollama chạy local
        state.code = "def solve_problem():\n    return 100 / 0 # Cố tình tạo lỗi chia cho 0"
        
    print("-> Code đã sinh ra:\n", state.code)
    return state
 
def tester_node(state: AgentState):
    print("\n[Node Tester]: Đang chạy thử nghiệm code vừa sinh...")
    
    # Tạo môi trường chạy code độc lập cục bộ
    local_env = {}
    try:
        # Biên dịch và thực thi chuỗi code Python
        exec(state.code, local_env)
        
        # Kiểm tra xem hàm solve_problem có tồn tại không
        if "solve_problem" in local_env:
            # Chạy thử hàm giải bài toán
            result = local_env["solve_problem"]()
            state.test_log = "SUCCESS"
            print(f"-> Chạy thử thành công! Kết quả hàm trả về: {result}")
        else:
            state.test_log = "Lỗi: Không tìm thấy định nghĩa hàm solve_problem()."
            state.error_count += 1
            print("-> Thất bại: Thiếu định nghĩa hàm.")
    except Exception as e:
        state.test_log = f"Runtime Error: {str(e)}"
        state.error_count += 1
        print(f"-> Thất bại! Phát hiện lỗi khi chạy thử: {state.test_log}")
        
    return state
 
def human_approval_node(state: AgentState):
    print("\n[Node Human-in-the-loop]: Đang chờ phê duyệt từ người quản trị...")
    print(f"Mã nguồn đề xuất:\n{state.code}")
    
    # Dừng luồng chờ input từ bàn phím người dùng
    user_input = input("Bạn có phê duyệt lưu file code này không? (y/n): ").strip().lower()
    
    if user_input == 'y':
        state.approved = True
        print("-> Đã được con người phê duyệt!")
    else:
        state.approved = False
        print("-> Bị con người từ chối.")
        
    return state
 
# 3. Lập trình Stateful Graph Engine điều khiển luồng rẽ nhánh điều kiện
def run_stateful_agent_graph(task_description):
    # Khởi tạo trạng thái ban đầu của Graph
    state = AgentState(task_description)
    
    max_retries = 3
    
    # Bắt đầu luồng chạy
    while True:
        # Bước 1: Gọi Node Coder
        state = coder_node(state)
        
        # Bước 2: Gọi Node Tester
        state = tester_node(state)
        
        # Cạnh điều kiện (Conditional Edge): Kiểm tra kết quả chạy thử
        if state.test_log == "SUCCESS":
            print("\n>> Điều kiện đạt! Chuyển tiếp tới bước Phê duyệt.")
            break
        else:
            if state.error_count >= max_retries:
                print(f"\n>> Đạt giới hạn sửa lỗi tối đa ({max_retries} lần). Kết thúc đồ thị thất bại.")
                return
            print(f"\n>> Phát hiện lỗi. Tự động quay lại Node Coder để sửa lại (Lần sửa thứ {state.error_count}).")
            
    # Bước 3: Điểm ngắt Human-in-the-loop
    state = human_approval_node(state)
    
    # Bước 4: Kiểm tra kết quả phê duyệt cuối cùng
    if state.approved:
        # Xuất file code ra ổ cứng
        filename = "solved_output.py"
        with open(filename, "w", encoding="utf-8") as f:
            f.write(state.code)
        print(f"\n🎉 Thành công: Đã ghi nhận mã nguồn vào tệp '{filename}'!")
    else:
        print("\n❌ Thất bại: Mã nguồn bị hủy bỏ do con người từ chối phê duyệt.")
 
if __name__ == "__main__":
    print("=== Trình mô phỏng Đồ thị Agent có trạng thái ===")
    
    # Bài toán lập trình yêu cầu Agent thực hiện
    problem = "Hãy viết một hàm giải phép chia lấy phần dư của 17 cho 5."
    
    # Kích hoạt đồ thị chạy
    run_stateful_agent_graph(problem)
⚠️ Lỗi phát hiện khi chạy thử thật: thiếu import re khiến Agent luôn thất bại âm thầm
Chạy thử chính xác đoạn code gốc phát hiện ra một lỗi nghiêm trọng: hàm coder_node gọi re.search(...) để trích xuất code Python từ phản hồi Ollama, nhưng phần import ở đầu file chỉ có urllib.request, json, sys — thiếu hẳn import re.

Hậu quả (đã kiểm chứng bằng Python): khi Ollama không chạy, lỗi kết nối xảy ra trước khi chạm tới dòng re.search, nên bug này bị che khuất hoàn toàn — mọi thứ trông như vẫn hoạt động "bình thường" nhờ cơ chế fallback. Nhưng khi Ollama chạy thành công và trả về code hợp lệ, dòng re.search sẽ ném ra NameError: name 're' is not defined, bị khối except Exception ở ngay bên dưới nuốt lặng lẽ, rồi âm thầm thay code thật của LLM bằng hàm giả cố tình lỗi (100 / 0). Kết quả: Agent sẽ luôn thất bại sau đúng 3 lần thử, bất kể LLM viết code đúng hay sai — vì code thật không bao giờ được dùng tới. Đã thêm import re vào phần đầu file để khắc phục triệt để. Đây là minh chứng rõ ràng cho nguyên tắc: khối except Exception quá rộng có thể che giấu cả những lỗi lập trình cơ bản nhất, khiến hệ thống "trông như chạy được" trong khi thực chất đã hỏng từ gốc.

Tóm tắt bài học & Cầu nối kiến thức

🔑 Bài học đạt được:
  • Đạt được: Thiết kế luồng xử lý Agent dưới dạng đồ thị có trạng thái (State Graph) bằng LangGraph.
  • Đạt được: Quản lý State tập trung và tích hợp cơ chế con người kiểm duyệt (Human-in-the-loop) để tăng độ an toàn.

Cầu nối bài tiếp theo: Đối với những bài toán chuyên biệt cần giọng điệu hoặc tri thức đặc thù mà RAG và Prompting không giải quyết triệt để, chúng ta cần tinh chỉnh trực tiếp vào trọng số mô hình bằng kỹ thuật Fine-tuning & LoRA ở Bài số 19.

Tải file code thực hành minh họa bài học

File Python langgraph_agent.py — mã nguồn xây dựng Graph Engine tự động hóa chu trình Coder viết code, Tester chạy kiểm định và Approver duyệt lưu file (chạy python langgraph_agent.py):

Tải về langgraph_agent.py

📖 Tài liệu tham khảo

Bài viết liên quan trong series

Bài 17: AI Agents & Vòng lặp ReAct Bài 19: Tinh chỉnh mô hình (Fine-tuning LLM) Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận