Tưởng tượng bạn đang debug một lỗi production. Bạn không ngồi mò code blind. Bạn nhìn stack trace, nghĩ: “có thể do null pointer ở đây”, chạy lệnh: kubectl logs pod-xyz | grep ERROR, đọc output: thấy NullPointerException ở line 42, rồi nghĩ tiếp: “vậy là input từ upstream thiếu field”. Rồi lại chạy lệnh tiếp.
Chu kỳ đó: nghĩ, hành động, quan sát, nghĩ tiếp. ReAct là cách đưa chu kỳ đó vào agent.
Bài bài 3 đã cover control loop cơ bản: LLM nhận input, gọi tool, nhận result, lặp lại. Bài này đi sâu hơn vào phần reasoning: vì sao thêm một bước “thought” trước mỗi action lại giúp agent ít sai hơn, và với modern model như Claude Sonnet 4.6, liệu bạn còn cần explicit ReAct không.
Phần 1: ReAct paper và intuition
Paper gốc: ReAct: Synergizing Reasoning and Acting in Language Models, Yao et al., 2022.
Ý tưởng cốt lõi chỉ có một dòng: interleave reasoning traces với action execution.
Trước ReAct, có hai luồng nghiên cứu riêng biệt:
- Chain-of-Thought (CoT): LLM nghĩ từng bước, không có action thật. Tốt cho toán, logic, nhưng không tương tác được với thế giới ngoài.
- Action-only agent: LLM gọi tool trực tiếp, không giải thích tại sao. Fast, nhưng dễ gọi tool sai khi task phức tạp.
ReAct ghép cả hai. Format cụ thể trong paper:
Thought: Tôi cần tìm năm sinh của Marie Curie trước.
Action: search("Marie Curie date of birth")
Observation: Marie Curie was born on November 7, 1867.
Thought: Được rồi. Giờ tôi cần tính tuổi khi bà nhận Nobel Prize năm 1903.
Action: calculate("1903 - 1867")
Observation: 36
Thought: Marie Curie nhận Nobel Prize năm 1903 khi 36 tuổi. Đủ thông tin để trả lời.
Action: finish("Marie Curie nhận Nobel Prize năm 36 tuổi")
Mỗi Thought không phải output cho user. Đó là scratch space riêng của LLM, nơi nó lên kế hoạch trước khi commit vào một action.
Phần 2: Vì sao thought trước action thực sự giúp ích
Intuition đơn giản: LLM không giỏi undo.
Khi một action đã được thực thi, kết quả nằm trong conversation history. LLM phải tiếp tục từ đó. Nếu action sai, agent phải tốn thêm action để undo hoặc sửa, tốn token, tốn latency, và đôi khi gây side effect không thu hồi được.
Thought buộc LLM “nói to lên” plan trước khi thực thi. Một số cơ chế hoạt động ở đây:
1. Grounding: Viết thought ra khiến LLM “nhìn thấy” reasoning của mình và có thể catch mâu thuẫn trước khi commit vào action. Giống như dev viết comment trước khi viết code, đôi khi nhận ra plan không hợp lý ngay lúc viết comment.
2. Disambiguation: Khi task mơ hồ, thought giúp LLM narrow down ý nghĩa của task trước khi chọn tool. “User hỏi ‘delete user 123’, có thể là soft delete hoặc hard delete. Tôi sẽ dùng soft_delete vì đây là production.” Không có thought, LLM chọn tool theo pattern matching thuần túy.
3. Error recovery: Khi một action fail, thought ở bước tiếp theo phân tích tại sao fail và plan recovery. Không có thought, LLM có xu hướng retry y chang action cũ.
4. Context management: Task dài có nhiều step, thought giúp LLM track được “mình đang ở đâu” trong plan. Giảm hiện tượng LLM “quên” intermediate result sau nhiều action.
Paper gốc benchmark trên HotpotQA và FEVER, cho thấy ReAct vượt CoT và action-only trên cả hai. Nhưng đó là năm 2022 với GPT-3 era. Với model hiện đại, cần đánh giá lại.
Phần 3: Implementation, explicit ReAct với Python
Cách implement ReAct truyền thống: prompt engineering. Bạn dạy LLM format Thought/Action/Observation trong system prompt, parse output bằng regex hoặc structured output.
import anthropic
import re
client = anthropic.Anthropic()
SYSTEM_PROMPT = """Bạn là một agent giải quyết task bằng cách suy nghĩ từng bước.
Với mỗi bước, format response theo đúng cấu trúc:
Thought: [lý do bạn làm bước này]
Action: [tên_tool]([args])
Khi đã có đủ thông tin để trả lời:
Thought: [kết luận]
Final Answer: [câu trả lời cuối]
Tools có sẵn:
- search(query): tìm kiếm thông tin
- calculate(expression): tính toán số học
"""
TOOLS_MOCK = {
"search": lambda q: f"Kết quả tìm kiếm cho '{q}': [mock data]",
# NOTE: expression_parser thay thế eval() để an toàn hơn trong production
"calculate": lambda expr: str(simple_calc(expr)),
}
def simple_calc(expr):
"""Safe arithmetic parser. Chỉ chấp nhận số, +, -, *, /."""
import ast
tree = ast.parse(expr, mode="eval")
# Validate: chỉ cho phép số và toán tử cơ bản
allowed = (ast.Expression, ast.BinOp, ast.Constant,
ast.Add, ast.Sub, ast.Mult, ast.Div)
for node in ast.walk(tree):
if not isinstance(node, allowed):
raise ValueError(f"Biểu thức không hợp lệ: {type(node).__name__}")
return ast.literal_eval(ast.unparse(tree))
def parse_action(text):
"""Parse 'tool_name(args)' từ Action line."""
match = re.match(r"(\w+)\((.+)\)", text.strip())
if match:
return match.group(1), match.group(2).strip("\"'")
return None, None
def react_loop(task, max_steps=8):
messages = [{"role": "user", "content": task}]
for step in range(max_steps):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=SYSTEM_PROMPT,
messages=messages,
)
output = response.content[0].text
messages.append({"role": "assistant", "content": output})
print(f"\n--- Step {step + 1} ---\n{output}")
if "Final Answer:" in output:
final = output.split("Final Answer:")[-1].strip()
return final
# Parse action từ output
for line in output.split("\n"):
if line.startswith("Action:"):
action_text = line.replace("Action:", "").strip()
tool_name, args = parse_action(action_text)
if tool_name and tool_name in TOOLS_MOCK:
obs = TOOLS_MOCK[tool_name](args)
messages.append({
"role": "user",
"content": f"Observation: {obs}"
})
break
return "Max steps reached"
result = react_loop("Tính 17 nhân 23 cộng 456")
print(f"\nKết quả: {result}")
Approach này hoạt động, nhưng có vấn đề:
- Parse regex dễ vỡ khi LLM output không theo đúng format
- Không tận dụng được native tool use của Anthropic API
- Thought nằm trong text, không phải structured field, khó log và trace
Phần 4: ReAct với native tool use
Cách hiện đại hơn: dùng Anthropic native tool use, để thinking xảy ra trong model, chỉ explicit hoá qua system prompt hướng dẫn LLM “think before act”.
import anthropic
client = anthropic.Anthropic()
TOOLS = [
{
"name": "web_search",
"description": "Tìm kiếm thông tin trên web. Dùng khi cần fact, số liệu, hoặc thông tin bên ngoài context hiện tại.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query rõ ràng, cụ thể"
}
},
"required": ["query"]
}
},
{
"name": "calculate",
"description": "Tính toán biểu thức số học. Input là Python expression hợp lệ.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Arithmetic expression, e.g. '(17 * 23) + 456'"
}
},
"required": ["expression"]
}
},
]
SYSTEM = """Trước mỗi tool call, hãy giải thích ngắn gọn tại sao bạn chọn tool đó và
bạn kỳ vọng nhận được gì. Điều này giúp track reasoning và debug khi cần.
Format: "[lý do] -> gọi [tên_tool]"
Ví dụ: "Cần tính tổng hai số -> gọi calculate"
"""
def execute_tool(name, args):
"""Mock execution. Production: thay bằng real implementation với input validation."""
if name == "web_search":
return f"Search results for '{args['query']}': [relevant information here]"
if name == "calculate":
import ast
try:
tree = ast.parse(args["expression"], mode="eval")
return str(ast.literal_eval(ast.unparse(tree)))
except Exception as e:
return f"Error: {e}"
return f"Unknown tool: {name}"
def react_agent(task, max_iterations=10):
messages = [{"role": "user", "content": task}]
for i in range(max_iterations):
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM,
tools=TOOLS,
messages=messages,
)
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
text_blocks = [b.text for b in response.content if hasattr(b, "text")]
return "\n".join(text_blocks)
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f" Tool: {block.name}({block.input})")
result = execute_tool(block.name, block.input)
print(f" Result: {result}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
return "Max iterations exceeded"
answer = react_agent("Chu vi hình tròn có bán kính 7 là bao nhiêu? Dùng pi = 3.14159")
print(f"\nAnswer: {answer}")
Approach này sạch hơn: LLM tự quyết định khi nào cần reasoning step, tool use có schema validation built-in, observation là tool result structured.
Phần 5: Modern model đã tự reasoning, bạn còn cần explicit ReAct không?
Đây là câu hỏi thực tế nhất khi build agent năm 2026.
Claude Sonnet 4.6 (và các frontier model hiện tại) có extended thinking và internal chain-of-thought. Model không phải chờ bạn nhắc “hãy nghĩ từng bước”. Nó đã làm vậy internally trước khi output.
Vậy explicit ReAct vẫn cần không? Câu trả lời phụ thuộc vào context:
Khi explicit ReAct vẫn có giá trị:
- Trace và debug: Thought được in ra rõ ràng, giúp bạn thấy tại sao agent quyết định gọi tool nào. Internal CoT của model không expose ra cho developer.
- Multi-step task phức tạp với nhiều tool: Explicit thought buộc model “commit” vào một plan trước khi execute. Giảm hiện tượng model “random walk” qua các tools.
- Domain-specific reasoning: Khi task cần expertise cụ thể (y tế, pháp lý, tài chính), thought template có thể hướng model theo checklist nhất định.
- Weaker model: Nếu bạn dùng Haiku 3.5 hoặc model nhỏ hơn để tiết kiệm cost, explicit ReAct bù đắp cho reasoning capability yếu hơn.
Khi có thể bỏ qua explicit ReAct:
- Single-step task: Query tìm kiếm rồi trả lời thẳng. Không cần format Thought/Action/Observation cho task đơn giản.
- Claude Sonnet 4.6 trở lên với extended thinking: Model tự handling reasoning internally. Thêm explicit ReAct có thể conflict với internal CoT, tạo ra redundancy hoặc format mismatch.
- Latency sensitive: Thought step tốn thêm tokens. Nếu mỗi token là 10ms, thêm Thought dài 100 tokens là thêm 1s latency.
- Cost sensitive: Thought tokens cũng là tokens. Ứng dụng high volume thì chi phí thought accumulated lên nhanh.
Thực tế khi dùng Claude Sonnet 4.6: Với native tool use và một system prompt rõ ràng về goal, model đã thực hiện implicit ReAct không cần bạn force format. Nếu muốn trace, dùng betas=["interleaved-thinking-2025-05-14"] để expose thinking blocks thay vì engineer format Thought/Action bằng tay.
Phần 6: So sánh với basic tool use loop
| Basic tool use loop | ReAct explicit | Modern model + native tools | |
|---|---|---|---|
| Reasoning | Implicit trong LLM | Explicit Thought text | Internal CoT, optional expose |
| Trace | Tool calls + results | Thought + Action + Observation | Thinking blocks (nếu enable) |
| Overhead | Thấp nhất | Medium (thêm thought tokens) | Thấp, thinking billed riêng |
| Phù hợp | Simple task, latency critical | Debug, complex multi-step, weaker model | Production với frontier model |
| Risk | Không giải thích reasoning | Thought quá dài đốt token | Internal CoT không inspectable |
Phần 7: Pitfall thực tế
Pitfall 1: Thought dài đốt token
Lần đầu tôi implement explicit ReAct, tôi không cap độ dài của thought. Agent tôi xây cho một task refactor codebase đã viết thought dài 800 token cho mỗi action. Task 20 action tốn thêm 16.000 token chỉ cho thought. Với Opus 4.7, đó là thêm $0.24 cho mỗi task run. Nhân với 500 run một ngày là $120/ngày tiêu vào thought.
Fix: thêm instruction trong system prompt: “Thought không quá 50 words. Ngắn gọn, đủ ý.”
Pitfall 2: Observation quá dài flood context
Tool trả về full JSON 50KB. Agent đọc observation, tiếp tục, đến bước 8 thì context đã 180K token, latency tăng, cost tăng, và model bắt đầu “quên” thông tin ở đầu conversation.
Fix: truncate observation trước khi append vào history. Quyết định bao nhiêu là đủ phụ thuộc task. Với search result, 500 chars đầu thường đủ. Với code file, chỉ lấy relevant function, không cả file.
def truncate_observation(obs, max_chars=500):
if len(obs) > max_chars:
return obs[:max_chars] + f"... [truncated, {len(obs)} chars total]"
return obs
Pitfall 3: Thought không match action
Model viết “Tôi sẽ tìm kiếm thông tin X” nhưng sau đó gọi tool Y. Format không enforce constraint, chỉ là text. Với explicit ReAct bằng prompt engineering, bạn đang tin tưởng model tự consistent.
Fix: với task critical, dùng structured output để force thought schema trước khi action, hoặc validate thought text match tool được gọi sau đó. Hoặc chuyển sang native tool use và bỏ explicit Thought format.
Pitfall 4: Retry loop vì thought quá optimistic
Thought: “Thử lại sẽ được”. Action: retry với args y chang. Observation: fail lại. Lặp vô hạn.
LLM có xu hướng optimistic trong thought. Nó viết “lần này sẽ thành công” ngay cả khi không có cơ sở.
Fix: detect retry pattern trong code (không trong prompt): nếu cùng tool và cùng args được gọi 2 lần liên tiếp, inject một message vào conversation: “Action này đã được thực thi với kết quả tương tự. Xem xét approach khác.”
Cheatsheet
| Concept | Mô tả |
|---|---|
| Thought | Scratch space, không phải output. LLM lên kế hoạch trước khi action. |
| Action | Tool call với args cụ thể. Commit point, khó undo. |
| Observation | Kết quả tool trả về. LLM dùng để plan step tiếp theo. |
| Cycle | Thought > Action > Observation > Thought > … cho đến Final Answer |
| Explicit ReAct | Prompt engineering, parse text. Debug tốt, overhead cao. |
| Implicit ReAct | Native tool use, model tự reason. Cleaner, less inspectable. |
| Khi nào dùng explicit ReAct | Khi nào skip |
|---|---|
| Debug và trace quan trọng | Production với frontier model |
| Weaker model (Haiku) | Latency critical |
| Complex multi-step, nhiều tool | Simple single-step task |
| Domain với checklist cứng | Cost sensitive |
| Pitfall | Fix |
|---|---|
| Thought dài đốt token | Cap 50 words trong system prompt |
| Observation flood context | Truncate trước khi append |
| Thought không match action | Native tool use thay vì text format |
| Optimistic retry loop | Detect pattern trong code, inject warning |
Lời kết
ReAct là một pattern đơn giản với tác động lớn: thêm “nghĩ trước khi làm” vào agent loop. Paper gốc năm 2022 chứng minh điều đó trên GPT-3. Năm 2026, frontier model đã internalize reasoning, nhưng explicit ReAct vẫn có chỗ đứng khi trace và debug là yêu cầu.
Quan trọng hơn là hiểu tại sao nó hoạt động: không phải vì “thought là magic”. Mà vì commit vào kế hoạch trước khi execute giảm irreversible mistake, và observation rõ ràng sau mỗi action giúp agent track state chính xác hơn.
Bài 7, Plan-and-Execute: tách planning khỏi execution, đi xa hơn một bước: thay vì think-then-act trong cùng một LLM call, tách hẳn ra hai phase với hai LLM call riêng. Planner tạo full plan trước, executor chạy từng step. Tradeoff khác, failure mode khác.
Bài 10, Chain-of-Thought so với structured reasoning, sẽ quay lại câu hỏi “thought đóng góp gì” từ góc nhìn mechanistic: khi nào CoT thực sự giúp model reason tốt hơn, khi nào chỉ là expensive decoration.