Từ trước đến nay, chúng ta lập trình các hệ thống AI theo kiểu "hỏi gì đáp nấy" — LLM nhận vào một Prompt tĩnh và sinh ngay ra câu trả lời cuối cùng. Nhưng nếu câu hỏi yêu cầu giải quyết một bài toán phức tạp đòi hỏi phải tính toán số liệu và tra cứu dữ liệu ngoài thời gian thực, LLM thô chắc chắn sẽ bó tay.

Bước tiến hóa tiếp theo của ứng dụng trí tuệ nhân tạo là AI Agent (Đại lý AI). Thay vì trả lời ngay, Agent sử dụng LLM làm "bộ não" lập kế hoạch, tự chia nhỏ tác vụ lớn thành các bước nhỏ, tự lựa chọn và kích hoạt các công cụ (Tools) lập trình bên ngoài, đọc kết quả trả về, tự điều chỉnh và lặp lại cho đến khi hoàn thành mục tiêu. Bài học này sẽ hướng dẫn bạn giải phẫu khung tư duy ReAct (Reasoning + Acting), cách thiết kế cấu trúc sử dụng công cụ (Tool Use), và tự tay xây dựng một AI Agent tự động giải toán và tra cứu thông tin mà không dùng bất kỳ framework cồng kềnh nào.

✅ Bạn cần gì trước khi bắt đầu
Phần mềm: Ollama kèm một mô hình chat (ollama pull qwen2.5:7b). Không cần mô hình nhúng — bài này không truy xuất tài liệu.

Kiến thức cần có: Bài 12 cho chu trình Tool Call bốn bước — ReAct chính là chu trình đó lặp lại nhiều lần, với mô hình tự quyết định khi nào dừng. Bài 11 cho mảng messages phân vai, và đặc biệt là cạm bẫy "ghép lịch sử thành một chuỗi thô": mục 17.4 sẽ cho thấy chính bản đầu tiên của dự án này mắc đúng lỗi đó.

17.1 Định nghĩa AI Agent: Khác gì LLM thô?

Một mô hình LLM thuần túy giống như một bộ não bị cô lập: cực kỳ thông minh về mặt ngôn ngữ nhưng không có tay chân để tương tác với thế giới xung quanh, và không có khả năng tự suy ngẫm để sửa lỗi trong quá trình suy luận.

Một AI Agent (Hệ tác nhân AI) là một cấu trúc phần mềm bao quanh LLM, trang bị cho nó 3 thành phần cốt lõi:

  • Planning (Lập kế hoạch): Khả năng chia nhỏ mục tiêu phức tạp thành chuỗi các bước hành động logic.
  • Memory (Bộ nhớ): Lưu trữ lịch sử suy luận ngắn hạn và thông tin dài hạn để giữ ngữ cảnh nhất quán.
  • Tools (Công cụ): Các đoạn code Python, API web, hoặc cơ sở dữ liệu giúp LLM kết nối với thế giới thực (ví dụ: máy tính toán, bộ tìm kiếm Google, API thời tiết).
⚠️ Cạm bẫy: Lạm dụng AI Agent cho tác vụ tĩnh
Lập trình Agent đi kèm chi phí và độ trễ cực cao. Một vòng lặp tư duy của Agent có thể tốn từ 3 đến 10 lượt gọi API LLM liên tục. Nếu bạn chỉ cần phân loại email hoặc tóm tắt một đoạn văn bản ngắn, hãy sử dụng LLM thô hoặc một hàm Structured Output đơn thuần thay vì dựng Agent. Chỉ dùng Agent khi luồng xử lý không tất định (non-deterministic) và kết quả bước sau phụ thuộc trực tiếp vào kết quả chạy thử của bước trước.

17.2 Vòng lặp tư duy ReAct (Reasoning + Acting)

Khung tư duy ReAct (Reasoning and Acting) là phương pháp thiết lập prompt hướng dẫn LLM hoạt động theo vòng lặp 4 bước tuần hoàn:

🔄 Chu trình vòng lặp ReAct:
  1. 1. Thought (Suy nghĩ): LLM phân tích trạng thái hiện tại của nhiệm vụ và tự trả lời câu hỏi: "Tôi cần làm gì tiếp theo? Tôi có cần gọi công cụ nào không?".
  2. 2. Action (Hành động): LLM sinh ra một câu lệnh gọi công cụ theo đúng định dạng quy định, ví dụ: Action: calculate[2 + 2].
  3. 3. Observation (Quan sát): Hệ thống Python bắt giữ câu lệnh trên, thực thi hàm cục bộ bên ngoài và trả về kết quả thô cho LLM đọc dưới dạng: Observation: 4.
  4. 4. Lặp lại (Repeat): LLM đọc kết quả quan sát, suy nghĩ tiếp (Thought) để quyết định gọi tiếp công cụ khác hoặc đưa ra câu trả lời cuối cùng (Final Answer: ...).

Ưu điểm lớn nhất của ReAct là khả năng Tự sửa sai (Self-Correction). Nếu một công cụ bị trả về kết quả lỗi (ví dụ: máy tính báo lỗi chia cho 0 hoặc API web lỗi kết nối), bước Observation chứa thông báo lỗi sẽ được nạp lại vào ngữ cảnh. LLM sẽ đọc lỗi đó ở bước Thought tiếp theo và tự động thay đổi đối số truyền vào hoặc chuyển sang gọi một công cụ dự phòng.

⚡ Cạm bẫy vòng lặp vô hạn (Infinite Loop) & Max Iterations
Khi LLM gặp một câu hỏi quá khó hoặc công cụ liên tục trả về kết quả lỗi không mong muốn, mô hình có thể bị "kẹt suy nghĩ" và liên tục lặp đi lặp lại hành vi gọi cùng một công cụ với cùng một tham số sai. Điều này gây tốn kém tài nguyên API cực lớn và treo ứng dụng. Khắc phục: Bắt buộc phải cài đặt một biến đếm giới hạn số vòng lặp tối đa (Max Iterations, thường là 5 lượt). Nếu vượt quá số lượt này mà Agent chưa đưa ra được Final Answer, hệ thống phải ngắt luồng và trả về cảnh báo.
⚠️ Cạm bẫy bảo mật: lọc theo ký tự không đủ để thay thế eval()
Bản đầu tiên của dự án này dùng eval() để tính biểu thức, và tự bảo vệ bằng một dòng lọc ký tự: re.sub(r'[^0-9+\-*/().\s]', '', expression). Với chuỗi tấn công kinh điển __import__('os').system('ls') thì bộ lọc đó hoạt động — nó biến chuỗi thành '().()', một biểu thức sai cú pháp. Nhưng bộ lọc giữ lại dấu *, mà hai dấu * liền nhau là phép lũy thừa. Chuỗi 9**9**9 đi qua bộ lọc nguyên vẹn — và eval() sẽ ngồi tính một con số khoảng 370 triệu chữ số, treo tiến trình vô thời hạn. Đây không phải giả thuyết: chương trình dưới đây in ra đúng chuỗi sau khi lọc để bạn tự thấy.

Bài học tổng quát: danh sách cấm (blacklist) ký tự luôn thua danh sách cho phép (whitelist). Phiên bản mới bỏ hẳn eval(), thay bằng cách phân tích biểu thức thành cây cú pháp (ast.parse) rồi chỉ duyệt qua đúng bốn phép toán được phép. Cái gì không nằm trong danh sách cho phép thì không chạy được, kể cả những thứ bạn chưa từng nghĩ tới.

17.3 Tool Use: Thiết lập mô tả công cụ cho LLM

Làm thế nào để LLM biết hệ thống có những công cụ gì để gọi? Chúng ta phải mô tả chi tiết danh sách công cụ trong System Prompt gửi cho LLM. Bản mô tả bắt buộc phải chứa:

  • Tên công cụ (Tool Name): Định danh duy nhất để LLM sinh ra chính xác (ví dụ: calculator).
  • Chức năng (Description): Giải thích rõ công cụ dùng để làm gì và khi nào nên dùng nó.
  • Schema đối số (Arguments): Đặc tả kiểu dữ liệu đầu vào cần truyền.

17.4 Dự án thực hành bài 17: Tự lập trình ReAct Agent từ số 0

Dự án tự viết một Agent ReAct độc lập, không dùng LangChain hay bất kỳ framework nào — đúng để bạn thấy vòng lặp thật sự chỉ là một vòng for quanh một lời gọi API, cộng với một biểu thức chính quy bắt lệnh gọi công cụ.

Agent có hai công cụ: get_stock_price (tra giá cổ phiếu, dữ liệu cố định để chạy lại cho ra cùng kết quả) và calculate (tính số học). Câu hỏi thử nghiệm cố ý cần cả hai và theo đúng thứ tự: phải tra giá trước, rồi mới nhân được với số lượng.

react_agent.py
"""Lesson 17 project: a ReAct agent written by hand, with its two sharp edges.

Run:  python3 react_agent.py
Needs: Ollama with a chat model (`ollama pull qwen2.5:7b`).

Two things this file takes seriously that a first ReAct implementation usually
does not: the agent's own output must go back as an `assistant` message rather
than as user text, and the tool that evaluates arithmetic must never be `eval`.
"""

import ast
import json
import operator
import re
import urllib.error
import urllib.request

OLLAMA = "http://localhost:11434"
CHAT_PREFERRED = ["qwen2.5:14b-instruct", "qwen2.5:7b", "llama3.1", "llama3.2",
                  "qwen2.5-coder:7b"]


# ---------------------------------------------------------------------------
# 1. The tools the agent is allowed to call
# ---------------------------------------------------------------------------

# Only these node types are allowed through. Note that ast.Pow is NOT here:
# the model can write 9**9**9, which passes a character filter untouched and
# then hangs the process for hours inside eval(). Blocking it is not paranoia.
SAFE_OPERATORS = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.USub: operator.neg,
}


def _evaluate(node):
    if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
        return node.value
    if isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPERATORS:
        return SAFE_OPERATORS[type(node.op)](_evaluate(node.left),
                                             _evaluate(node.right))
    if isinstance(node, ast.UnaryOp) and type(node.op) in SAFE_OPERATORS:
        return SAFE_OPERATORS[type(node.op)](_evaluate(node.operand))
    raise ValueError("unsupported expression")


def calculate(expression):
    """Evaluate arithmetic without eval(): parse, then walk a whitelist."""
    try:
        return str(_evaluate(ast.parse(expression, mode="eval").body))
    except ZeroDivisionError:
        return "Division by zero."
    except (SyntaxError, ValueError, TypeError):
        return f"Cannot evaluate '{expression}' - only + - * / are supported."


def get_stock_price(symbol):
    """Look up a share price. Fixed data, so the run stays reproducible."""
    prices = {"AAPL": "185.50 USD", "GOOGL": "172.30 USD",
              "MSFT": "420.10 USD", "TSLA": "175.20 USD"}
    return prices.get(symbol.strip().upper(),
                      f"No price on file for '{symbol}'.")


TOOL_MAP = {"calculate": calculate, "get_stock_price": get_stock_price}


# ---------------------------------------------------------------------------
# 2. The system prompt that defines the ReAct protocol
# ---------------------------------------------------------------------------

SYSTEM_PROMPT = """Bạn là một AI Agent hoạt động theo vòng lặp ReAct (Thought -> Action -> Observation).
Bạn được cung cấp các công cụ sau:

1. get_stock_price[symbol]: Lấy giá cổ phiếu của một mã chứng khoán. Ví dụ: get_stock_price[AAPL]
2. calculate[expression]: Thực hiện phép tính số học. Ví dụ: calculate[150 * 1.1]

Quy trình làm việc của bạn:
Bước 1: Suy nghĩ về câu hỏi của người dùng (Thought: ...)
Bước 2: Nếu cần dùng công cụ, hãy xuất ra: Action: ten_cong_cu[tham_so] rồi DỪNG LẠI.
        Tuyệt đối không tự viết dòng Observation - hệ thống sẽ cung cấp nó.
Bước 3: Sau khi nhận được Observation, tiếp tục suy nghĩ (Thought: ...) để quyết
        định hành động tiếp theo hoặc đưa ra câu trả lời cuối cùng.
Bước 4: Khi đã có câu trả lời, hãy xuất ra: Final Answer: [câu trả lời của bạn].

Bắt đầu!
"""

# Belt and braces for step 2. A well-behaved model stops on its own, but a stop
# sequence makes an invented Observation impossible rather than merely unlikely
# - the same distinction as the logit mask in Lesson 12.
STOP_SEQUENCES = ["Observation:"]


# ---------------------------------------------------------------------------
# 3. Talking to Ollama - the pattern from Lesson 13
# ---------------------------------------------------------------------------


def installed_models():
    try:
        with urllib.request.urlopen(f"{OLLAMA}/api/tags", timeout=5) as response:
            return [m["name"] for m in json.loads(response.read())["models"]]
    except urllib.error.URLError:
        return []


def pick(names, preferred):
    for wanted in preferred:
        for name in names:
            if name == wanted or name.startswith(wanted):
                return name
    return None


def chat(messages, model, stop=None):
    """One call. No try/except swallowing: a broken call must stop the agent.

    The earlier version of this project returned the error text as if it were
    the model's answer, so the loop went on to parse "[connection error]" as a
    Thought and carried on for five iterations.
    """
    payload = {"model": model, "messages": messages, "stream": False,
               "options": {"temperature": 0.0, **({"stop": stop} if stop else {})}}
    request = urllib.request.Request(
        f"{OLLAMA}/api/chat", data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(request) as response:
            return json.loads(response.read())["message"]["content"].strip()
    except urllib.error.HTTPError as exc:
        detail = json.loads(exc.read() or b"{}").get("error", "no detail")
        raise RuntimeError(f"Ollama answered HTTP {exc.code}: {detail}") from None
    except urllib.error.URLError as exc:
        raise RuntimeError(f"cannot reach Ollama at {OLLAMA} - {exc.reason}") from None


# ---------------------------------------------------------------------------
# 4. The ReAct control loop
# ---------------------------------------------------------------------------

ACTION_PATTERN = re.compile(r"Action:\s*(\w+)\[(.*?)\]", re.S)


def run_react_agent(question, model, max_iterations=5, use_roles=True):
    """Drive the Thought/Action/Observation loop until a Final Answer.

    use_roles=False reproduces the original mistake - see main().
    """
    print(f"Question: {question}")
    messages = [{"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": question}]
    flat_context = question

    for step in range(1, max_iterations + 1):
        if use_roles:
            reply = chat(messages, model, stop=STOP_SEQUENCES)
        else:
            # Everything the agent ever said, glued into one user message.
            reply = chat([{"role": "system", "content": SYSTEM_PROMPT},
                          {"role": "user", "content": flat_context}],
                         model, stop=STOP_SEQUENCES)
        print(f"\n  --- step {step} ---")
        for line in reply.splitlines():
            if line.strip():
                print(f"  {line}")

        messages.append({"role": "assistant", "content": reply})
        flat_context += f"\n{reply}"

        if "Final Answer:" in reply:
            answer = reply.split("Final Answer:")[-1].strip()
            print(f"\n  RESULT: {answer}")
            return answer, messages

        match = ACTION_PATTERN.search(reply)
        if not match:
            print("\n  The model produced neither an Action nor a Final Answer.")
            return None, messages

        tool_name, argument = match.group(1), match.group(2)
        tool = TOOL_MAP.get(tool_name)
        observation = (tool(argument) if tool
                       else f"Error: no tool named '{tool_name}'.")
        print(f"  [tool] {tool_name}({argument!r}) -> {observation}")

        # The observation is new information from outside, so it is a user turn.
        messages.append({"role": "user", "content": f"Observation: {observation}"})
        flat_context += f"\nObservation: {observation}"

    print(f"\n  Gave up after {max_iterations} iterations.")
    return None, messages


# ---------------------------------------------------------------------------
# 5. Demonstrations
# ---------------------------------------------------------------------------


def demo_calculator_safety():
    """The tool the agent is allowed to call must survive hostile input."""
    print("=== Why the calculator does not use eval() ===")
    cases = ["185.50 * 12", "1 / 0", "9**9**9", "__import__('os').system('ls')"]
    for expression in cases:
        stripped = re.sub(r"[^0-9+\-*/().\s]", "", expression)
        survives = "**" in stripped
        print(f"  {expression!r}")
        print(f"    after the old character filter: {stripped!r}"
              f"{'   <-- ** SURVIVES' if survives else ''}")
        print(f"    this version returns: {calculate(expression)}")
    print("  A filter that keeps '*' cannot stop '**'. 9**9**9 has around 370")
    print("  million digits, so eval() on it hangs the agent indefinitely.\n")


def show_message_roles(messages):
    print("=== The message array the agent built ===")
    for message in messages:
        first_line = message["content"].splitlines()[0] if message["content"] else ""
        print(f"  {message['role']:<9} | {first_line[:58]}")
    roles = [m["role"] for m in messages]
    print(f"  assistant turns recorded: {roles.count('assistant')}")
    print("  Without them the model reads its own reasoning as if the user had")
    print("  typed it - the exact mistake Lesson 11 warns about.\n")


def main():
    names = installed_models()
    if not names:
        print("Ollama is not reachable. Start it, then run this again.")
        return
    model = pick(names, CHAT_PREFERRED)
    if not model:
        print("No suitable chat model found. Try: ollama pull qwen2.5:7b")
        return
    print(f"model: {model}\n")

    demo_calculator_safety()

    question = ("Nếu tôi mua 12 cổ phiếu AAPL thì tôi cần trả tổng cộng "
                "bao nhiêu tiền?")
    print("=== ReAct loop, with proper message roles ===")
    answer, messages = run_react_agent(question, model)
    print()
    show_message_roles(messages)

    print("=== The same question, with the history flattened into one string ===")
    print("  (this is what the first version of this project did)")
    run_react_agent(question, model, use_roles=False)


if __name__ == "__main__":
    main()

Công cụ của Agent phải chịu được đầu vào thù địch

Trước khi chạy vòng lặp, chương trình chạy bốn biểu thức qua cả bộ lọc ký tự cũ lẫn bộ tính toán mới:

Terminal
=== Why the calculator does not use eval() ===
  '185.50 * 12'
    after the old character filter: '185.50 * 12'
    this version returns: 2226.0
  '1 / 0'
    after the old character filter: '1 / 0'
    this version returns: Division by zero.
  '9**9**9'
    after the old character filter: '9**9**9'   <-- ** SURVIVES
    this version returns: Cannot evaluate '9**9**9' - only + - * / are supported.
  "__import__('os').system('ls')"
    after the old character filter: '().()'
    this version returns: Cannot evaluate '__import__('os').system('ls')' - only + - * / are supported.
  A filter that keeps '*' cannot stop '**'. 9**9**9 has around 370
  million digits, so eval() on it hangs the agent indefinitely.

Dòng <-- ** SURVIVES là toàn bộ luận điểm của cạm bẫy ở mục 17.2. Bộ lọc chặn được cuộc tấn công mà tác giả nghĩ tới (__import__), và để lọt cuộc tấn công mà tác giả không nghĩ tới. Đó là bản chất của mọi danh sách cấm.

Vòng lặp chạy thật

Với câu hỏi cần hai công cụ nối tiếp nhau, Agent tự đi hết ba bước:

Terminal
=== ReAct loop, with proper message roles ===
Question: Nếu tôi mua 12 cổ phiếu AAPL thì tôi cần trả tổng cộng bao nhiêu tiền?

  --- step 1 ---
  Thought: Để trả lời được câu hỏi này, trước tiên tôi cần lấy giá hiện tại của cổ phiếu AAPL bằng cách sử dụng công cụ get_stock_price.
  Action: get_stock_price[AAPL]
  [tool] get_stock_price('AAPL') -> 185.50 USD

  --- step 2 ---
  Thought: Giờ tôi đã có được giá hiện tại của cổ phiếu AAPL là 185.50 USD, tôi sẽ tính tổng số tiền cần trả cho việc mua 12 cổ phiếu này.
  Action: calculate[185.50 * 12]
  [tool] calculate('185.50 * 12') -> 2226.0

  --- step 3 ---
  Thought: Bây giờ tôi đã có kết quả từ phép tính, tổng số tiền cần trả cho việc mua 12 cổ phiếu AAPL là 2226.0 USD.
  Final Answer: Tổng cộng bạn sẽ phải trả 2226.0 USD để mua 12 cổ phiếu AAPL.

  RESULT: Tổng cộng bạn sẽ phải trả 2226.0 USD để mua 12 cổ phiếu AAPL.

Đọc kỹ bước 2: mô hình không tự nhân 185.50 với 12 trong đầu. Nó viết ra Action: calculate[185.50 * 12] và chờ. Đó chính là điểm mấu chốt của ReAct — mô hình ngôn ngữ vốn tính toán không đáng tin (Bài 11), nên việc đúng đắn là để nó quyết định phải tính gì rồi giao phép tính cho Python.

Vai trò tin nhắn: chỗ bản đầu tiên của dự án này làm sai

Bản đầu tiên lưu toàn bộ lịch sử suy luận vào một chuỗi Python rồi gửi lên dưới dạng một tin nhắn user duy nhất. Nghĩa là mọi câu mô hình từng nói đều quay lại với nó dưới danh nghĩa lời của người dùng — đúng cạm bẫy Bài 11 mô tả. Bản mới ghi lại đúng vai trò:

Terminal
=== The message array the agent built ===
  system    | Bạn là một AI Agent hoạt động theo vòng lặp ReAct (Thought
  user      | Nếu tôi mua 12 cổ phiếu AAPL thì tôi cần trả tổng cộng bao
  assistant | Thought: Để trả lời được câu hỏi này, trước tiên tôi cần l
  user      | Observation: 185.50 USD
  assistant | Thought: Giờ tôi đã có được giá hiện tại của cổ phiếu AAPL
  user      | Observation: 2226.0
  assistant | Thought: Bây giờ tôi đã có kết quả từ phép tính, tổng số t
  assistant turns recorded: 3
  Without them the model reads its own reasoning as if the user had
  typed it - the exact mistake Lesson 11 warns about.
🔬 Trung thực về phép so sánh này
Chương trình chạy cả hai phiên bản trên cùng câu hỏi để bạn tự đối chiếu. Trên máy viết bài, với mô hình 14B và một nhiệm vụ chỉ ba bước, cả hai đều ra đáp án đúng — tôi không dựng được một thất bại để trưng ra, và sẽ không bịa một cái.

Lý do vẫn nên làm đúng vai trò là ở chỗ khác: mảng messages đúng vai là thứ bạn có thể nối thẳng vào cơ chế Function Calling gốc của API (Bài 12), có thể cắt tỉa theo lượt khi vượt ngân sách token (Bài 11), và có thể lưu lại làm nhật ký kiểm toán. Một chuỗi phẳng thì không làm được gì trong ba việc đó. Chi phí để làm đúng bằng không; hãy làm đúng ngay từ đầu.

Cách chạy dự án này trên máy bạn

  1. Bật Ollama, tải một mô hình chat (ollama pull qwen2.5:7b), rồi chạy python3 react_agent.py. Phần kiểm tra máy tính chạy được cả khi không có Ollama.
  2. Chuỗi suy luận trên máy bạn sẽ khác về câu chữ (mô hình khác nhau diễn đạt khác nhau), nhưng hình dạng phải giống: tra giá trước, tính sau, rồi mới Final Answer.
  3. Rồi thử phá nó theo ba cách:
    • Bỏ stop=STOP_SEQUENCES trong run_react_agent. Với mô hình tốt thì không đổi gì — nhưng với mô hình nhỏ hơn, nó có thể tự viết luôn dòng Observation: và bịa ra kết quả công cụ. Chuỗi dừng biến chuyện đó thành bất khả thay vì ít khả năng, đúng tinh thần mặt nạ logits ở Bài 12.
    • Đổi max_iterations=5 thành 1. Agent bị cắt ngang giữa chừng và in ra dòng "Gave up" — đây là lưới an toàn mà cạm bẫy vòng lặp vô hạn ở mục 17.2 nói tới, giờ bạn thấy nó kích hoạt.
    • Hỏi một câu cần công cụ mà Agent không có, ví dụ "thời tiết Hà Nội hôm nay thế nào?". Xem mô hình xử lý ra sao: nó gọi bừa một công cụ không tồn tại (và nhận lại Error: no tool named ...), hay nó thừa nhận không làm đượ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: Tự viết trọn vòng lặp ReAct bằng Python thuần — không LangChain — và thấy Agent tự nối hai công cụ theo đúng thứ tự phụ thuộc.
  • Đạt được: Ghi lịch sử Agent bằng mảng messages đúng vai trò, thay vì ghép thành một chuỗi thô như cạm bẫy Bài 11.
  • Đạt được: Thấy vì sao lọc ký tự không thay được whitelist: 9**9**9 đi qua bộ lọc nguyên vẹn và treo eval(). Bản mới dùng ast.parse và chỉ cho phép bốn phép toán.
  • Đạt được: Biết ba lưới an toàn bắt buộc của một Agent: giới hạn số vòng lặp, chuỗi dừng, và công cụ không tin đầu ra của mô hình.

Cầu nối bài tiếp theo: Vòng lặp for ở trên đủ cho một Agent tuyến tính. Nhưng khi luồng công việc có nhánh, có vòng quay lại, và có chỗ cần người phê duyệt trước khi đi tiếp thì một vòng for không đủ nữa. Bài 18 mô hình hóa Agent thành đồ thị trạng thái với LangGraph.

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

File Python react_agent.py — mã nguồn xây dựng Agent tư duy ReAct gọi công cụ tính toán và tra cứu giá cổ phiếu cục bộ (chạy python react_agent.py):

Tải về react_agent.py

📖 Tài liệu tham khảo

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

Bài 16: RAG Nâng Cao: Query Translation & Cross-Encoder Reranking Bài 18: Đại lý có trạng thái với LangGraph Quay lại Lộ trình Kỹ Sư AI Thực Chiến

Bình luận