← Phase 1 Phase 1 • Day 6 Reboot

Local SE Agent Masterclass

MacBook M3 16GB • Complete Implementation Standard
📍 ส่วนที่ 1: หมุดตั้งโจทย์ & วัตถุประสงค์หลัก (Title Connection)

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 1: Setup & Ollama API ✓ Day 2: JSON Mode & Structured Output ✓ Day 3: Tool Calling & Dispatcher ✓ Day 4: File System Reader ✓ Day 5: Maven & Test Runner Day 6: Autonomous ReAct Loop Day 7: Modular Refactor
📂 ส่วนที่ 2: ผังโครงสร้างโปรเจกต์จริง & ตำแหน่งไฟล์ (Project File Structure)

โค้ดประจำ Day 6 จะต่อยอดจาก se_agent/ โดยเพิ่มโมดูลแกนกลาง core/engine.py และสคริปต์รันจำลองการคิดและทำงานวนลูปอัตโนมัติ test_react_loop.py:

se_agent/
├── requirements.txt                       # ollama>=0.4.0
├── test_connection.py                    # Day 1
├── test_structured_output.py             # Day 2
├── test_tool_dispatch.py                 # Day 3
├── test_file_reader.py                   # Day 4
├── test_maven_runner.py                 # Day 5
├── test_react_loop.py                   # [ไฟล์ใหม่ Day 6] สคริปต์ส่งคำสั่ง 1 ครั้ง แล้วปล่อยให้ AI วนลูปคิดและทำเองจนจบ
├── sample_maven_project/                  # โปรเจกต์ Java ที่มีบั๊กแฝง (จาก Day 5)
│   ├── pom.xml
│   ├── src/main/java/Calculator.java
│   └── src/test/java/CalculatorTest.java
├── core/
│   ├── __init__.py
│   ├── client.py                          # Ollama Client Wrapper
│   ├── parser.py                          # Robust JSON Parser
│   ├── dispatcher.py                      # Function Registry & Invoker
│   ├── engine.py                          # [ไฟล์ใหม่ Day 6] หัวใจ ReAct Loop Engine ควบคุมลูป Thought-Action-Observation
│   └── tools/
│       ├── __init__.py
│       ├── file_reader.py                  # Safe File Reader Tool (Day 4)
│       └── test_runner.py                  # Subprocess Maven Runner Tool (Day 5)
└── main.py
📍 core/engine.py ทำหน้าที่อะไร?

ทำหน้าที่เป็น State Machine Controller ถือประวัติการสนทนา (Conversation Memory) วนลูปส่งให้ Ollama ตรวจจับว่าโมเดลขอใช้ Action หรือส่ง final_answer ถ้าขอใช้ Action จะสั่ง Dispatcher รัน Tool แล้วนำ Observation แปะต่อท้ายเพื่อวนลูปต่ออัตโนมัติ

📍 test_react_loop.py ทำหน้าที่อะไร?

สคริปต์รันบน Terminal เพื่อทดสอบส่งโจทย์ซับซ้อน: "ช่วยตรวจโปรเจกต์ sample_maven_project รันเทส ส่องดูโค้ดที่มีบั๊ก แล้วสรุปผลให้ฟัง" โดยดูการทำงานแบบ Real-time ทีละ Step จนจบงานอัตโนมัติ

🏛️ ส่วนที่ 3: สถาปัตยกรรม ReAct Pattern (Reason ➔ Act ➔ Observe)
💭
1. Thought (Reasoning)
"ฉันต้องเริ่มจากรันเทสก่อนเพื่อหาจุดพัง"
➔
🛠️
2. Action (Tool Call)
{"tool": "run_maven_test", ...}
➔
👁️
3. Observation
BUILD FAILURE: testApplyDiscount failed
ทำไม ReAct Loop ถึงเป็นรากฐานของ Autonomous Software Engineer?
❌ ทำไม Chatbot แบบถาม-ตอบ 1 ครั้งถึงแก้ปัญหาโค้ดไม่ได้:

โค้ดในชีวิตจริงไม่ได้แก้จบใน 1 ขั้นตอน การแก้บั๊กต้องเริ่มจากรันเทส ➔ ดูผลพัง ➔ ส่องหาบรรทัดโค้ด ➔ ทำความเข้าใจ ➔ แก้ไข ➔ รันซ้ำจนเขียว การถาม-ตอบรอบเดียวจึงทำได้แค่เดาและมักจะตอบผิด

✅ พลังของ ReAct Loop:

โมเดลจะนำผลลัพธ์ (Observation) จากการรันเทสกลับมาเป็นข้อมูลดิบเพื่อคิดต่อว่า "อ๋อ มันพังตรงฟังก์ชัน applyDiscount งั้นก้าวต่อไปฉันจะขอเปิดอ่าน Calculator.java" เกิดเป็นปัญญาประดิษฐ์ที่ปรับตัวตามผลลัพธ์จริงในเครื่องได้เอง!

💻 ส่วนที่ 4: ซอร์สโค้ดจริง & คำอธิบายทีละบรรทัด (Line-by-Line Implementation)
📄 1. core/engine.py (ไฟล์ใหม่ประจำ Day 6) ลูปควบคุม ReAct อัตโนมัติพร้อมระบบป้องกัน Infinite Loop
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 ก่อนหน้าได้แบบต่อเนื่อง

📄 2. test_react_loop.py (สคริปต์รันจำลองการคิดและทำงานวนลูปอัตโนมัติ) เชื่อมโยง Ollama + File Reader + Maven Runner ➔ ReAct Loop
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()
⚡ ส่วนที่ 5: ปฏิบัติการจริงบน Terminal Mac M3 (Step-by-Step Lab)

เปิด Terminal บน MacBook M3 แล้วทำตาม 4 ขั้นตอนนี้เพื่อทดสอบ Autonomous ReAct Loop Engine ทันที:

ขั้นตอนที่ 1: เข้าโฟลเดอร์โปรเจกต์เดิมและเปิด Virtual Environment
cd ~/se_agent
source .venv/bin/activate
ขั้นตอนที่ 2: สร้างไฟล์ใหม่สำหรับ Day 6
touch core/engine.py test_react_loop.py
ขั้นตอนที่ 3: คัดลอกโค้ดลงในไฟล์

- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/engine.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน test_react_loop.py

ขั้นตอนที่ 4: สั่งรันสคริปต์เพื่อชม AI ทำงานแบบอัตโนมัติ
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%!

📝 ส่วนที่ 6: ชุดควิซประเมินความรู้เชิงวิศวกรรม 15 ข้อ (ตอบปุ๊บรู้ผลทันที)

ข้อที่ 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 ของคุณแล้วครับ!