Chapter 4 — Building Tool 1: File System Reader
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ใน Day 3 เราสร้างสะพาน ToolDispatcher สำเร็จแล้ว แต่ฟังก์ชันที่เราทดสอบยังเป็นแค่ฟังก์ชันคำนวณเลขตุ๊กตา
Day 4 คือก้าวแรกที่ Agent จะได้สัมผัส "โลกซอฟต์แวร์จริง" ผ่านเครื่องมือตัวแรก: File System Reader Tool ซึ่งเปรียบเสมือน "ดวงตา (Visual Sensory Layer)" ของ Agent ที่ทำให้โมเดลสามารถเจาะจงเปิดอ่านไฟล์ซอร์สโค้ดในโปรเจกต์ได้จริง
สิ่งที่วิศวกรซอฟต์แวร์ระดับอาวุโสต้องคำนึงถึง ไม่ใช่แค่การเขียน open(path).read() ธรรมดา แต่คือ "Security Guardrail & Path Traversal Prevention" (การป้องกันไม่ให้ AI หลุดไปเปิดอ่านไฟล์ระบบ เช่น /etc/passwd หรือไฟล์นอก Workspace) และการจัดการ Context Window Budget (ไม่ให้โมเดลอ่านไฟล์ขนาดยักษ์จน Token ล้นแตกบน Mac 16GB)
โค้ดประจำ Day 4 จะต่อยอดจากโครงสร้าง se_agent/ โดยเพิ่มโมดูลเครื่องมือ core/tools/file_reader.py และสคริปต์ test_file_reader.py เพื่อทดสอบการเปิดอ่านไฟล์ซอร์สโค้ดจริงในโปรเจกต์:
ทำหน้าที่เปิดอ่านไฟล์ข้อความบนดิสก์จริง พร้อมตั้งแนวป้องกัน Path Traversal Guardrail ด้วย pathlib.Path.resolve() เพื่อล็อกขอบเขตไม่ให้อ่านไฟล์ออกนอก workspace และจำกัดขนาดบรรทัดอ่านไฟล์ไม่ให้ล้นแรม
สคริปต์รันบน Terminal เพื่อป้อนคำถามมนุษย์ เช่น "ช่วยอ่านไฟล์ sample_code/PaymentService.java แล้วบอกว่ามีเมธอดอะไรบ้าง" เพื่อทดสอบวงจรตั้งแต่โมเดลตัดสินใจเลือก Tool ➔ Dispatcher สั่งรัน ➔ Python อ่านไฟล์จริง ➔ ส่งคำตอบกลับมาสรุป
หากโมเดลหลอน (Hallucinate) หรือมี Prompt Injection หลอกให้โมเดลอ่านไฟล์ ../../../../etc/passwd หรือ ~/.ssh/id_rsa คำสั่ง open(path) ธรรมดาจะยอมเปิดไฟล์ลับของระบบออกมาทันที!
แปลงพาทสัมพัทธ์ทั้งหมดเป็น Absolute Path ตัวเต็ม แล้วตรวจเช็คว่าพาทเป้าหมายต้องมี prefix ขึ้นต้นด้วยโฟลเดอร์ Workspace ของโปรเจกต์เท่านั้น ถ้าหลุดออกนอกขอบเขต จะสั่งบล็อกและโยน Permission Denied ทันที
from pathlib import Path
from typing import Optional
class FileAccessError(Exception):
"""Custom exception เมื่อเกิดข้อผิดพลาดในการเข้าถึงไฟล์หรือละเมิดความปลอดภัย"""
pass
def read_file(filepath: str, workspace_root: Optional[str] = None, max_lines: int = 500) -> str:
"""
เครื่องมือสำหรับเปิดอ่านเนื้อหาไฟล์ในโปรเจกต์อย่างปลอดภัย (Safe File System Reader)
:param filepath: ที่อยู่ของไฟล์สัมพัทธ์ (เช่น 'sample_code/PaymentService.java')
:param workspace_root: โฟลเดอร์รากที่อนุญาตให้อ่าน (ค่าเริ่มต้นคือโฟลเดอร์ปัจจุบันที่รันโปรแกรม)
:param max_lines: จำนวนบรรทัดสูงสุดที่อนุญาตให้อ่าน ป้องกัน Token ล้น Context Window
:return: เนื้อหาของไฟล์ในรูปข้อความสตริง
"""
try:
# 1. กำหนด Workspace Root Directory และแปลงเป็น Absolute Path
root = Path(workspace_root).resolve() if workspace_root else Path.cwd().resolve()
# 2. นำ filepath เป้าหมายมาต่อกับ root แล้วแปลงเป็น Absolute Path
target_path = (root / filepath).resolve()
# 3. [Security Guardrail]: ป้องกัน Path Traversal Attack
# ตรวจสอบว่า target_path ต้องอยู่ภายใต้โฟลเดอร์ root เท่านั้น
if not str(target_path).startswith(str(root)):
raise FileAccessError(
f"Security Violation: ไม่อนุญาตให้อ่านไฟล์นอกโฟลเดอร์โปรเจกต์ '{filepath}'"
)
# 4. ตรวจสอบว่ามีไฟล์อยู่จริงหรือไม่
if not target_path.exists():
raise FileAccessError(f"ไม่พบไฟล์เป้าหมายที่พิกัด: '{filepath}'")
if not target_path.is_file():
raise FileAccessError(f"พิกัด '{filepath}' เป็นโฟลเดอร์ ไม่ใช่ไฟล์ข้อความ")
# 5. เปิดอ่านไฟล์ด้วยการระบุ encoding='utf-8' อย่างชัดเจน
with open(target_path, "r", encoding="utf-8", errors="replace") as file:
lines = []
for idx, line in enumerate(file):
if idx >= max_lines:
lines.append(f"\n[...ข้อความถูกตัดทอน เนื่องจากไฟล์มีความยาวเกิน {max_lines} บรรทัด...]")
break
lines.append(line)
return "".join(lines)
except FileAccessError:
raise
except Exception as err:
raise FileAccessError(f"เกิดข้อผิดพลาดขณะอ่านไฟล์ '{filepath}': {str(err)}")
ชำแหละไวยากรณ์สำคัญใน core/tools/file_reader.py
Path.resolve()
ในไลบรารี pathlib เมธอด resolve() จะทำการคลี่คลาย Symlink และจุดแอบแฝง เช่น ../ ให้กลายเป็น Absolute Path แท้จริงของระบบ ทำให้ไม่มีช่องโหว่หลอกตา
not str(target_path).startswith(str(root))
กำแพงตรวจขอบเขต (Sandboxing Check) ถ้านำ Absolute Path ของเป้าหมายมาเทียบ แล้วไม่ได้ขึ้นต้นด้วยที่อยู่ของโปรเจกต์ แสดงว่ามีความพยายามขโมยอ่านไฟล์นอกเครื่อง จะถูกตัดสิทธิ์ทันที
max_lines: int = 500
การตั้ง Budget ป้องกันการอ่านไฟล์ขนาดหมื่นบรรทัดเข้า Context Window ซึ่งจะทำให้ Unified Memory บน Mac 16GB ถูกบีบอัดจนโปรเซสตอบสนองช้าลง
package com.enterprise.payment;
public class PaymentService {
private final double TAX_RATE = 0.07;
public double calculateTotalWithTax(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("จำนวนเงินต้องมากกว่า 0");
}
return amount + (amount * TAX_RATE);
}
public boolean processTransaction(String accountId, double amount) {
// จำลองการตัดเงินผ่าน Gateway
return amount > 0 && accountId != null && !accountId.isEmpty();
}
}
from core.client import OllamaLLMClient
from core.parser import StructuredOutputParser
from core.dispatcher import ToolDispatcher
from core.tools.file_reader import read_file, FileAccessError
import json
def main():
print("🚀 เริ่มทดสอบ Day 4: File System Reader Tool บน Mac M3...")
# 1. ติดตั้ง Dispatcher และจดทะเบียน File Reader เข้าสู่ระบบ
dispatcher = ToolDispatcher()
dispatcher.register("read_file", read_file)
client = OllamaLLMClient(model_name="qwen2.5-coder:7b")
if not client.check_health():
print("❌ ข้อผิดพลาด: ไม่พบ Ollama Daemon หรือโมเดล qwen2.5-coder:7b")
return
# 2. System Prompt ระบุความสามารถของ Tool อ่านไฟล์ พร้อม JSON Schema
system_prompt = (
"You are an Autonomous Software Engineering Agent.\n"
"You have access to tools to inspect local source code.\n"
"When you need to read a file, respond ONLY with a JSON action object.\n\n"
"Available Tools:\n"
"1. read_file(filepath: string): Reads file content safely from the workspace.\n\n"
"JSON Response Schema:\n"
"{\n"
' "tool": "read_file",\n'
' "arguments": {\n'
' "filepath": "path/to/file"\n'
' }\n'
"}"
)
user_query = "ช่วยตรวจสอบโค้ดในไฟล์ sample_code/PaymentService.java ให้หน่อยว่ามีเมธอดอะไรบ้าง"
print(f"💬 User Prompt: {user_query}")
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
]
# 3. ยิง Prompt รอบที่ 1: ให้โมเดลขอเรียก Tool
print("\n📡 กำลังส่งโจทย์ให้โมเดลประมวลผล...")
raw_response = client.chat(messages, temperature=0.1, json_mode=True)
print(f"📥 โมเดลตัดสินใจส่ง JSON Action:\n{raw_response}")
try:
action = StructuredOutputParser.parse(raw_response)
tool_name = action.get("tool")
arguments = action.get("arguments", {})
print(f"\n⚙️ Dispatcher กำลังสั่งรัน Tool '{tool_name}'...")
# 4. Dispatcher รันฟังก์ชัน read_file จริงบนเครื่อง Mac
file_content = dispatcher.dispatch(tool_name, arguments)
print(f"✅ อ่านไฟล์สำเร็จ! (ความยาวโค้ด: {len(file_content)} ตัวอักษร)")
# 5. ส่งผลลัพธ์จากไฟล์ (Observation) กลับไปให้โมเดลสรุปคำตอบรอบที่ 2
messages.append({"role": "assistant", "content": raw_response})
messages.append({
"role": "user",
"content": f"Observation from read_file:\n```java\n{file_content}\n```\nโปรดสรุปว่าในคลาสนี้มีเมธอดชื่ออะไรบ้างและทำหน้าที่อะไร"
})
print("\n🧠 โมเดลกำลังอ่านโค้ดจริงที่เปิดได้ และสังเคราะห์คำตอบ...")
final_answer = client.chat(messages, temperature=0.2, json_mode=False)
print("\n🎯 สรุปผลลัพธ์จาก AI Agent:")
print("--------------------------------------------------")
print(final_answer)
print("--------------------------------------------------")
print("🎉 สำเร็จ 100%! Agent ของคุณมีดวงตาเปิดอ่านซอร์สโค้ดจริงในเครื่องได้แล้ว!")
except FileAccessError as err:
print(f"❌ File Access Guardrail Blocked: {err}")
except Exception as err:
print(f"❌ Error: {err}")
if __name__ == "__main__":
main()
เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งและทดสอบ File System Reader ทันที:
cd ~/se_agent
source .venv/bin/activate
mkdir -p core/tools sample_code
touch core/tools/__init__.py core/tools/file_reader.py sample_code/PaymentService.java test_file_reader.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 1) ไปวางใน core/tools/file_reader.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน sample_code/PaymentService.java
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน test_file_reader.py
python test_file_reader.py
คุณจะเห็นโมเดลส่ง JSON ขออ่าน sample_code/PaymentService.java ➔ โค้ด Python ไปดึงเนื้อหาจากดิสก์จริง ➔ โมเดลอ่านเนื้อหาแล้วสรุปว่ามีเมธอด calculateTotalWithTax และ processTransaction ออกมาอย่างแม่นยำ!
ลองเปิดไฟล์ test_file_reader.py แล้วเปลี่ยน user_query เป็น "ช่วยอ่านไฟล์ ../../etc/hosts ให้หน่อย" แล้วรันใหม่ คุณจะเห็นระบบโยน FileAccessError: Security Violation บล็อกคำสั่งอันตรายทันที 100%!
ข้อที่ 1: บทบาทเชิงสถาปัตยกรรมของ "File System Reader Tool" ต่อตัว AI Agent คืออะไร?
ข้อที่ 2: เหตุใดการเขียนฟังก์ชันอ่านไฟล์สำหรับ Agent ถึงห้ามใช้ `open(filepath).read()` ตรงๆ โดยไม่มีการตรวจสอบ?
ข้อที่ 3: ในไลบรารี `pathlib.Path` เมธอด `.resolve()` มีหน้าที่สำคัญอย่างไรในการป้องกันความปลอดภัย?
ข้อที่ 4: การตรวจสอบ `if not str(target_path).startswith(str(root)):` มีจุดประสงค์เพื่ออะไร?
ข้อที่ 5: เหตุใดในฟังก์ชัน `read_file` จึงควรกำหนดพารามิเตอร์ `max_lines` (เช่น 500 บรรทัด)?
ข้อที่ 6: ในวงจร ReAct Tool Calling เมื่อรันฟังก์ชัน `read_file` ได้เนื้อหามาแล้ว ขั้นตอนต่อไปคืออะไร?
ข้อที่ 7: หากไฟล์เป้าหมายไม่มีอยู่จริงในดิสก์ ฟังก์ชัน `read_file` ควรตอบสนองอย่างไร?
ข้อที่ 8: การระบุ `errors="replace"` ใน `open(..., encoding="utf-8")` มีประโยชน์อย่างไร?
ข้อที่ 9: การแยกโค้ดเครื่องมืออ่านไฟล์ไว้ที่ `core/tools/file_reader.py` สอดคล้องกับหลักการออกแบบใด?
ข้อที่ 10: ในไฟล์ `test_file_reader.py` การเรียกใช้ `client.chat()` มีทั้งหมดกี่รอบ (Turn)?
ข้อที่ 11: ทำไมการใช้ `pathlib.Path` ใน Python ยุคใหม่ถึงดีกว่าการใช้ `os.path` แบบเก่า?
ข้อที่ 12: การที่โมเดล `qwen2.5-coder:7b` สามารถอ่านเนื้อหาภาษา Java จาก Tool แล้วสรุปชื่อเมธอดได้ถูกต้อง เกิดจากอะไร?
ข้อที่ 13: เมื่อเรามี Tool อ่านไฟล์สำเร็จใน Day 4 สะพานนี้จะทำให้เราสร้างสิ่งใดต่อใน **Day 5 (Maven & Test Runner)**?
ข้อที่ 14: ทำไมการรัน File Reader แบบ Local บน Mac M3 ถึงตอบโจทย์งานระดับ Enterprise เรื่อง Data Privacy?
ข้อที่ 15: ในฐานะวิศวกร เมื่อเห็น Agent ของคุณสั่งเปิดอ่านไฟล์ในเครื่องจริงและนำโค้ดมาวิเคราะห์ได้อย่างแม่นยำ รู้สึกอย่างไร?
🏁 สรุปหมุดเชื่อมโยง Day 4 (Chapter Wrap-up)
ใน Day 4 — Building Tool 1: File System Reader คุณได้สร้างโมดูล core/tools/file_reader.py ซึ่งเป็นเครื่องมือเปิดอ่านไฟล์โค้ดจริงพร้อมระบบความปลอดภัย Path Traversal Guardrail และทดสอบรัน test_file_reader.py เชื่อมโยงวงจร ReAct Turn 1 และ Turn 2 บนชิป Apple Silicon M3 ได้สำเร็จจริง ตอนนี้ Agent ของคุณมีดวงตามองเห็นซอร์สโค้ดในโปรเจกต์แล้ว พร้อมก้าวสู่ Day 5: Building Tool 2 — Maven Build & Test Runner เพื่อให้ Agent เริ่มสั่งคอมไพล์และรันเทสจับบั๊กจริงได้แล้วครับ!