Chapter 6 — Building the Autonomous ReAct Loop Engine
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 1–5 เราเตรียมอวัยวะครบแล้ว: สมองกล (Ollama Client), ระบบส่งคำสั่ง (JSON Parser & Dispatcher), ดวงตา (File Reader), และ มือเท้า (Maven Test Runner) แต่สังเกตไหมว่าสคริปต์ที่เราเขียนใน Day 4 และ 5 มนุษย์ยังต้องเป็นคน "สั่งมือ (Hardcode Turn 1 ➔ Turn 2)" ให้มันอยู่!
Day 6 คือจุดเปลี่ยนครั้งยิ่งใหญ่ที่สุดในหลักสูตร: เราจะสร้าง Autonomous ReAct Loop Engine (Reason ➔ Act ➔ Observe) เป็นลูป while หรือ for ที่ทำงานอัตโนมัติ โดยที่มนุษย์ป้อนโจทย์เพียงครั้งเดียว เช่น "ช่วยตรวจสอบและแก้โค้ดที่มีปัญหาให้เทสผ่านทั้งหมด" แล้วปล่อยให้ Agent ตัดสินใจเองว่าจะรันเทสกี่รอบ อ่านไฟล์ไหนกี่ครั้ง จนกว่าจะบรรลุเป้าหมาย (final_answer) โดยมี Guardrail จำกัดจำนวนรอบสูงสุด (Max Iterations) ป้องกันไม่ให้ AI วนลูปไม่รู้จบและสูบแบตเตอรี่ Mac M3!
โค้ดประจำ Day 6 จะต่อยอดจาก se_agent/ โดยเพิ่มโมดูลแกนกลาง core/engine.py และสคริปต์รันจำลองการคิดและทำงานวนลูปอัตโนมัติ test_react_loop.py:
ทำหน้าที่เป็น State Machine Controller ถือประวัติการสนทนา (Conversation Memory) วนลูปส่งให้ Ollama ตรวจจับว่าโมเดลขอใช้ Action หรือส่ง final_answer ถ้าขอใช้ Action จะสั่ง Dispatcher รัน Tool แล้วนำ Observation แปะต่อท้ายเพื่อวนลูปต่ออัตโนมัติ
สคริปต์รันบน Terminal เพื่อทดสอบส่งโจทย์ซับซ้อน: "ช่วยตรวจโปรเจกต์ sample_maven_project รันเทส ส่องดูโค้ดที่มีบั๊ก แล้วสรุปผลให้ฟัง" โดยดูการทำงานแบบ Real-time ทีละ Step จนจบงานอัตโนมัติ
โค้ดในชีวิตจริงไม่ได้แก้จบใน 1 ขั้นตอน การแก้บั๊กต้องเริ่มจากรันเทส ➔ ดูผลพัง ➔ ส่องหาบรรทัดโค้ด ➔ ทำความเข้าใจ ➔ แก้ไข ➔ รันซ้ำจนเขียว การถาม-ตอบรอบเดียวจึงทำได้แค่เดาและมักจะตอบผิด
โมเดลจะนำผลลัพธ์ (Observation) จากการรันเทสกลับมาเป็นข้อมูลดิบเพื่อคิดต่อว่า "อ๋อ มันพังตรงฟังก์ชัน applyDiscount งั้นก้าวต่อไปฉันจะขอเปิดอ่าน Calculator.java" เกิดเป็นปัญญาประดิษฐ์ที่ปรับตัวตามผลลัพธ์จริงในเครื่องได้เอง!
import json
from typing import List, Dict, Any, Optional
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser, JSONDecodeError
from core.dispatcher import ToolDispatcher, ToolExecutionError
class MaxIterationsReachedError(Exception):
"""Exception เมื่อลูปทำงานเกินขีดจำกัดสูงสุดที่ตั้งไว้ (Guardrail)"""
pass
class ReActAgentEngine:
"""
แกนกลางควบคุมลูป ReAct (Reason ➔ Act ➔ Observe) แบบอัตโนมัติ
"""
def __init__(
self,
client: OllamaLLMClient,
dispatcher: ToolDispatcher,
max_iterations: int = 6
):
self.client = client
self.dispatcher = dispatcher
self.max_iterations = max_iterations
self.messages: List[Dict[str, str]] = []
def run(self, user_prompt: str, system_prompt: str) -> str:
"""
เริ่มลูป ReAct อัตโนมัติ: ป้อนโจทย์ ➔ AI ตัดสินใจคิดและทำวนลูปจนจบงาน
"""
# 1. กำหนดประวัติบทสนทนาเริ่มต้น
self.messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
# 2. เริ่มลูปตามจำนวนรอบสูงสุดที่กำหนดไว้
for step in range(1, self.max_iterations + 1):
print(f"\n🔄 [Step {step}/{self.max_iterations}] Agent กำลังประมวลผล...")
# ยิงเรียก Ollama พร้อมเปิดใช้งาน json_mode=True
raw_response = self.client.chat(self.messages, temperature=0.1, json_mode=True)
try:
# ถอดรหัส JSON จากคำตอบของโมเดล
decision = StructuredOutputParser.parse(raw_response)
except JSONDecodeError as err:
print(f"⚠️ โมเดลส่ง JSON ไม่สมบูรณ์: {err}")
# Self-Correction: ส่งข้อความแจ้งให้โมเดลแก้ไวยากรณ์ในรอบถัดไป
self.messages.append({"role": "assistant", "content": raw_response})
self.messages.append({
"role": "user",
"content": "Error: Your previous response was not valid JSON. Please respond strictly in the defined JSON schema."
})
continue
# ดึงการใช้เหตุผล (Thought) ออกมาแสดง
thought = decision.get("thought", "ไม่มี Thought ระบุ")
print(f"💡 Thought: {thought}")
# 3. ตรวจสอบเงื่อนไขการจบงาน (Termination Condition)
if "final_answer" in decision:
final_answer = decision["final_answer"]
print(f"🎯 Final Answer: {final_answer}")
return final_answer
# 4. หากโมเดลต้องการเรียกใช้งาน Tool (Action)
if "tool" in decision:
tool_name = decision.get("tool")
arguments = decision.get("arguments", {})
print(f"🛠️ Action: เรียกใช้ Tool '{tool_name}' ด้วยค่า: {arguments}")
try:
# สั่ง Dispatcher ทำงานจริงในเครื่อง
observation = self.dispatcher.dispatch(tool_name, arguments)
except ToolExecutionError as err:
observation = f"Tool Execution Error: {str(err)}"
except Exception as err:
observation = f"System Error while executing tool: {str(err)}"
# ตัดทอน Observation เล็กน้อยตอนแสดงผลในคอนโซล
obs_summary = str(observation).strip()
if len(obs_summary) > 200:
print(f"👁️ Observation: {obs_summary[:200]}... [ตัดทอน]")
else:
print(f"👁️ Observation:\n{obs_summary}")
# 5. ป้อนประวัติกลับเข้าสู่ Memory เพื่อให้โมเดลมองเห็นผลลัพธ์ในรอบถัดไป
self.messages.append({"role": "assistant", "content": raw_response})
self.messages.append({
"role": "user",
"content": f"Observation from {tool_name}:\n{observation}"
})
else:
# กรณีโมเดลไม่ส่งทั้ง tool และ final_answer
self.messages.append({"role": "assistant", "content": raw_response})
self.messages.append({
"role": "user",
"content": "Invalid State: You must provide either an 'action' with a 'tool' or a 'final_answer'."
})
# 6. ถ้าวนลูปครบแล้วยังไม่จบงาน ให้โยน Guardrail Exception
raise MaxIterationsReachedError(
f"Agent reached maximum iterations limit ({self.max_iterations}) without completing the task."
)
ชำแหละไวยากรณ์สำคัญใน core/engine.py
for step in range(1, self.max_iterations + 1):
การใช้ for พร้อมกำหนด max_iterations เป็น Safety Guardrail สำคัญ ป้องกันโมเดลวนลูปไม่รู้จบ (Infinite Loop) หากเจอบั๊กที่ไม่สามารถแก้ได้
if "final_answer" in decision: return final_answer
Termination Condition เมื่อโมเดลตัดสินใจว่าได้ข้อมูลครบถ้วนแล้วและทำงานสำเร็จ มันจะส่งคีย์ final_answer มาเพื่อหลุดออกจากลูปทันที
self.messages.append({"role": "user", "content": f"Observation from..."})
การอัปเดต Conversation History ในหน่วยความจำ ทำให้โมเดลใน Step ถัดไปสามารถ "รับรู้" ผลลัพธ์จากการรัน Tool ใน Step ก่อนหน้าได้แบบต่อเนื่อง
from core.client import OllamaLLMClient
from core.dispatcher import ToolDispatcher
from core.engine import ReActAgentEngine, MaxIterationsReachedError
from core.tools.file_reader import read_file
from core.tools.test_runner import run_maven_test
def main():
print("🚀 เริ่มทดสอบ Day 6: Autonomous ReAct Loop Engine บน Mac M3...")
# 1. ติดตั้ง Dispatcher และจดทะเบียน Tool ทั้งหมดที่มี
dispatcher = ToolDispatcher()
dispatcher.register("read_file", read_file)
dispatcher.register("run_maven_test", run_maven_test)
client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
if not client.check_health():
print("❌ ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b กรุณาตรวจสอบคำสั่ง 'ollama list'")
return
# 2. System Prompt ระบุไวยากรณ์ ReAct (Thought ➔ Tool/Final Answer)
system_prompt = (
"You are an Autonomous Software Engineering Agent.\n"
"You operate in a strict ReAct loop (Reason, Act, Observe).\n"
"You have access to tools to inspect files and run automated tests.\n\n"
"Available Tools:\n"
"1. run_maven_test(project_dir: string): Runs Maven unit tests.\n"
"2. read_file(filepath: string): Reads file content safely from the workspace.\n\n"
"Response Rules:\n"
"- When you need to take an action, reply with:\n"
"{\n"
' "thought": "Your reasoning why you take this step",\n'
' "tool": "tool_name",\n'
' "arguments": { ... }\n'
"}\n\n"
"- When the task is completely finished, reply with:\n"
"{\n"
' "thought": "Summary of what was achieved",\n'
' "final_answer": "Detailed conclusion and analysis for the human user"\n'
"}\n\n"
"Respond ONLY with valid JSON."
)
# 3. ป้อนโจทย์ระดับสูงเพียง 1 ครั้ง
user_prompt = (
"ในโฟลเดอร์ sample_maven_project มีบั๊กแฝงอยู่ "
"ช่วยรันเทสเพื่อหาจุดพัง จากนั้นเปิดอ่านโค้ดที่เกี่ยวข้องเพื่อวิเคราะห์ว่าต้องแก้สูตรอย่างไรจึงจะถูกต้อง"
)
print(f"💬 โจทย์จากผู้ใช้: {user_prompt}\n")
# 4. สั่ง Engine เริ่มทำงานวนลูปอัตโนมัติ (ตั้งขีดจำกัดไม่เกิน 5 รอบ)
engine = ReActAgentEngine(client=client, dispatcher=dispatcher, max_iterations=5)
try:
final_result = engine.run(user_prompt=user_prompt, system_prompt=system_prompt)
print("\n==================================================")
print("🎉 ผลลัพธ์สุดท้ายจาก Autonomous Agent:")
print("==================================================")
print(final_result)
print("==================================================")
print("✨ สำเร็จ 100%! Agent ของคุณสามารถคิดและลงมือทำวนลูปจนจบงานได้เองโดยมนุษย์ไม่ต้องช่วยสั่งทีละสเต็ป!")
except MaxIterationsReachedError as err:
print(f"\n🛑 Guardrail Alert: {err}")
except Exception as err:
print(f"\n❌ Error: {err}")
if __name__ == "__main__":
main()
เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อทดสอบ Autonomous ReAct Loop Engine ทันที:
cd ~/se_agent
source .venv/bin/activate
touch core/engine.py test_react_loop.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/engine.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน test_react_loop.py
python test_react_loop.py
สังเกตการไหลของลูปใน Terminal:
👉 Step 1: AI สั่ง run_maven_test ➔ ผลลัพธ์: เทส testApplyDiscount ล้มเหลว (Expected: 90.0, Actual: 80.0)
👉 Step 2: AI คิดต่อทันทีโดยไม่ต้องถามมนุษย์ สั่ง read_file("sample_maven_project/src/main/java/Calculator.java") ➔ เห็นโค้ด return price - (discount * 2.0);
👉 Step 3: AI เห็นข้อมูลครบถ้วน ส่ง final_answer สรุปวิธีแก้โค้ดเป็น return price - discount; และจบงานอย่างสมบูรณ์แบบ 100%!
ข้อที่ 1: คำว่า "ReAct" ในสถาปัตยกรรม AI Agent ย่อมาจากแนวคิดใด?
ข้อที่ 2: วงจรการทำงาน 3 จังหวะของ ReAct Loop เรียงลำดับอย่างไร?
ข้อที่ 3: อะไรคือความแตกต่างระหว่าง "การเรียก Tool ใน Day 4-5" กับ "Autonomous ReAct Engine ใน Day 6"?
ข้อที่ 4: การกำหนดพารามิเตอร์ `max_iterations: int = 6` ใน `ReActAgentEngine` มีจุดประสงค์หลักเพื่ออะไร?
ข้อที่ 5: สัญญาณใดที่บอกให้ ReAct Loop ทราบว่างานเสร็จสิ้นแล้วและสามารถหลุดออกจากลูป (Terminate) ได้?
ข้อที่ 6: ในแต่ละรอบของลูป ผลลัพธ์จากการรัน Tool (Observation) ถูกส่งกลับเข้าไปในระบบอย่างไร?
ข้อที่ 7: หากโมเดลเผลอตอบ JSON ผิดไวยากรณ์ (Malformed JSON) ใน Step ใด Step หนึ่ง โค้ด `ReActAgentEngine` จัดการอย่างไร?
ข้อที่ 8: ฟิลด์ `"thought"` ใน JSON Response ของโมเดลมีประโยชน์ทางวิศวกรรมอย่างไร?
ข้อที่ 9: หาก Agent ทำงานจนครบ `max_iterations` แล้วยังไม่ได้ข้อสรุป ระบบจะตอบสนองอย่างไร?
ข้อที่ 10: ค่า `temperature=0.1` ที่กำหนดไว้ใน ReAct Engine ส่งผลดีอย่างไรต่อการวนลูปของ Agent?
ข้อที่ 11: การที่ ReAct Engine สามารถสลับเรียก `run_maven_test` แล้วต่อด้วย `read_file` ได้เอง แสดงถึงคุณสมบัติใด?
ข้อที่ 12: การแยกคลาส `ReActAgentEngine` ออกมาเป็นโมดูลอิสระใน `core/engine.py` มีประโยชน์อย่างไร?
ข้อที่ 13: เมื่อเรามี ReAct Engine สำเร็จใน Day 6 ใน **Day 7 (Refactor to Modular Engine & Personal CLI)** เราจะทำสิ่งใดต่อ?
ข้อที่ 14: ทำไมการรัน Autonomous ReAct Loop บน Mac M3 16GB แบบ Local ถึงคุ้มค่ากว่าการยิงต่อ Cloud API?
ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ เมื่อเห็น Agent วนลูปคิด รันเทส อ่านโค้ด และสรุปผลได้เอง 100% รู้สึกอย่างไร?
🏁 สรุปหมุดเชื่อมโยง Day 6 (Chapter Wrap-up)
ใน Day 6 — Building the Autonomous ReAct Loop Engine คุณได้สร้างโมดูล core/engine.py ซึ่งเป็นหัวใจสำคัญที่เปลี่ยนสมองกลธรรมดาให้กลายเป็น Autonomous Software Engineer มันสามารถคิด (Reason) ➔ ทำ (Act) ➔ สังเกตผล (Observe) วนลูปได้เองอย่างมีเป้าหมาย พร้อมระบบ Self-Correction และ Max Iterations Guardrail ตอนนี้ Agent ของคุณเป็นอิสระแล้ว พร้อมก้าวสู่หมุดหมายสุดท้ายของสัปดาห์แรก: Day 7 — Refactor to Modular Engine & Personal CLI เพื่อแพ็กทุกชิ้นส่วนเป็น CLI ประจำเครื่อง Mac ของคุณแล้วครับ!