Chapter 18 — Beyond Raw Text: AST Parsing with Tree-sitter
ทำไมต้องชื่อนี้และเรียนไปเพื่ออะไร?
ตลอด Day 1–17 ที่ผ่านมา Agent ของเรายังมองโค้ดเป็น "ตัวหนังสือดิบๆ (Raw Text Strings)" เหมือนไฟล์ข้อความธรรมดา ซึ่งในโลกความเป็นจริง หากโปรเจกต์มีขนาด 50,000 บรรทัด การส่งโค้ดแบบ Raw Text จะทำให้:
1. Token Budget ระเบิด: ยัดโค้ดแค่ 2–3 ไฟล์ก็เต็ม Context Window 8K/16K Tokens ของ Local LLM แล้ว
2. Regex หลอน: การใช้ Regular Expression สแกนหาฟังก์ชัน มักจะไปจับโดนคำที่อยู่ใน Comment หรือ String Literal ปนเปกันมั่วไปหมด!
3. Attention Fragmentation: โมเดล 7B–14B จะตาลายกับโค้ดบรรทัดในฟังก์ชัน (Body) จนจำโครงสร้างภาพรวมของระบบไม่ได้
Day 18 เราจึงจะก้าวข้ามตัวหนังสือดิบสู่ Semantic Code Intelligence โดยใช้ไลบรารีระดับอุตสาหกรรม Tree-sitter แปลงโค้ดดิบเป็น Abstract Syntax Tree (AST) สกัดเฉพาะ "โครงกระดูกโค้ด (Code Skeleton: Class, Methods, Parameters, Types)" ตัดทอน Body ออก ประหยัดพื้นที่ Token ได้ถึง 85% บนเครื่อง MacBook M3 16GB!
ใน Day 18 เราจะติดตั้ง package tree-sitter และ tree-sitter-java เพิ่มใน requirements.txt สร้างโมดูลสกัดโครงสร้าง intelligence/ast_parser.py และสคริปต์ทดสอบ test_ast_skeleton.py:
ทำหน้าที่เป็น Grammar-Aware Compiler Front-end แปลง Source Code ไบนารีด้วย C-binding Tree-sitter ท่อง Node ต้นไม้หา class_declaration และ method_declaration สกัดเอาเฉพาะ Signature (ชื่อเมธอด, ตัวแปรนำเข้า, ชนิดข้อมูลคืนค่า) โดยตัดทอน Block Body ภายในออกไปอย่างสะอาดหมดจด
สคริปต์จำลองไฟล์คลาสยาว 100+ บรรทัดที่มีทั้ง Logic ภายในและ Comment หลอก สั่งรัน extract_code_skeleton() เปรียบเทียบความแตกต่างของจำนวน Token ก่อนและหลังการสกัด พิสูจน์ว่าลด Token ได้ถึง ~85% ในเวลาไม่ถึง 3 มิลลิวินาที!
public PaymentResult pay(Req req) {
// 200 lines of complex loop
// tax calculation, DB log...
}
}
ปัญหา: ยัดโค้ดทั้งก้อนทำให้ Token บวมพอง และโมเดลหลอนตาลายกับโค้ดข้างใน
- Parse ไบนารีด้วย C-speed Parser
- สร้างโครงสร้าง Syntax Tree Nodes
- แยกแยะ Comment, String, Class, Method ไม่สับสน scope แม้แต่น้อย
+ pay(req: Req) -> PaymentResult
+ refund(id: Long) -> boolean
+ getStatus(id: Long) -> Status
ผลลัพธ์: ลด Token ลง 85% โมเดลมองเห็นภาพรวมทั้งโปรเจกต์ได้ทันที ไร้ภาพหลอน!
1. ถ้ามีคนเขียนคอมเมนต์ว่า // public void oldMethod() Regex จะเข้าใจผิดคิดว่าเป็นเมธอดจริงทันที!
2. Regex ไม่เข้าใจ Scope: เมื่อเจอคลาสซ้อนคลาส (Inner Class) Regex จะจับเมธอดไปผูกผิดคลาส
3. พังทันทีเมื่อเจอฟังก์ชันที่เขียนแยกบรรทัดยาวๆ หรือใส่ Generic ซับซ้อน เช่น Map<String, List<DTO>>
1. เข้าใจไวยากรณ์ตาม Grammar กฎของภาษานั้นๆ 100% กรอง Comment และ String ออกแบบเบ็ดเสร็จ
2. มีลำดับชั้น Parent-Child แน่นอน: รู้แน่ชัดว่าเมธอดนี้เป็นของคลาสไหนในระดับ Tree
3. ทนทานต่อโค้ดที่มีข้อผิดพลาด (Error-tolerant): แม้โค้ดจะยังเขียนไม่เสร็จ ปีกกาปิดไม่ครบ ก็ยังแกะโครงสร้างส่วนที่เหลือออกมาได้!
ollama>=0.4.0
chromadb>=0.4.24
rich>=13.7.0
tree-sitter==0.23.2
tree-sitter-java==0.23.2
from pathlib import Path
from typing import List, Dict, Any, Optional
import tree_sitter_java as tsjava
from tree_sitter import Language, Parser, Node
class JavaASTParser:
"""
เอนจินสกัดโครงสร้างไวยากรณ์ Java AST ด้วย Tree-sitter
แปลงโค้ดดิบให้กลายเป็น Code Skeleton ตัดทอน Body ออกเพื่อประหยัด Token สูงสุด
"""
def __init__(self):
# 1. โหลดไวยากรณ์ภาษา Java จาก C-binding
self.language = Language(tsjava.language())
# 2. สร้าง Parser Instance
self.parser = Parser(self.language)
def parse_code(self, source_code: str):
"""แปลงข้อความโค้ดเป็น Tree-sitter Syntax Tree"""
source_bytes = bytes(source_code, "utf8")
return self.parser.parse(source_bytes), source_bytes
def extract_skeleton(self, source_code: str) -> str:
"""
[Token-Optimization Hero]:
สกัดเฉพาะ Class Name, Field Types, และ Method Signatures
:param source_code: โค้ดดิบตัวเต็ม
:return: ข้อความโครงกระดูกโค้ดแบบกะทัดรัด
"""
tree, source_bytes = self.parse_code(source_code)
root_node = tree.root_node
skeleton_lines = []
def get_node_text(node: Optional[Node]) -> str:
if not node:
return ""
return source_bytes[node.start_byte:node.end_byte].decode("utf8")
def traverse(node: Node, indent: int = 0):
prefix = " " * indent
# ดักจับการประกาศ Class หรือ Interface
if node.type in ("class_declaration", "interface_declaration"):
name_node = node.child_by_field_name("name")
class_name = get_node_text(name_node)
kind = "interface" if node.type == "interface_declaration" else "class"
skeleton_lines.append(f"\n{prefix}{kind} {class_name} {{")
# ตรวจดูสมาชิกภายใน Body ของคลาส
body_node = node.child_by_field_name("body")
if body_node:
for child in body_node.children:
# ดักจับ Method Declaration
if child.type == "method_declaration":
m_name = get_node_text(child.child_by_field_name("name"))
m_type = get_node_text(child.child_by_field_name("type")) or "void"
m_params = get_node_text(child.child_by_field_name("parameters")) or "()"
skeleton_lines.append(f"{prefix} + {m_name}{m_params} -> {m_type};")
# ดักจับ Constructor Declaration
elif child.type == "constructor_declaration":
c_name = get_node_text(child.child_by_field_name("name"))
c_params = get_node_text(child.child_by_field_name("parameters")) or "()"
skeleton_lines.append(f"{prefix} + constructor {c_name}{c_params};")
skeleton_lines.append(f"{prefix}}}")
return
# ท่องต่อไปยัง Node ลูกๆ ถ้ายังไม่เจอ Class
for child in node.children:
traverse(child, indent)
traverse(root_node)
return "\n".join(skeleton_lines).strip()
"""
Intelligence Package: รวบรวมเอนจิน AST Parsing, Code Skeletonization และ Symbol Analysis
[เพิ่มใหม่ Day 18]: JavaASTParser
"""
from intelligence.ast_parser import JavaASTParser
__all__ = ["JavaASTParser"]
import time
from intelligence.ast_parser import JavaASTParser
def estimate_tokens(text: str) -> int:
"""Heuristic estimate: ~3.5 chars ต่อ 1 Token"""
return max(1, int(len(text) / 3.5))
def main():
print("=" * 70)
print("🚀 เริ่มทดสอบ Day 18: Beyond Raw Text — AST Parsing with Tree-sitter")
print("=" * 70)
# 1. จำลองซอร์สโค้ด Java ขนาดยาวที่มีทั้ง Logic ซับซ้อน และ Comment หลอก
sample_large_java = """
package com.enterprise.ecommerce.service;
import java.util.*;
import com.enterprise.ecommerce.model.*;
// public void fakeCommentedMethod() - คอมเมนต์หลอก Regex ต้องไม่ติดเข้ามา!
public class PaymentProcessingService {
private final PaymentGatewayClient gatewayClient;
private final TransactionRepository txRepo;
public PaymentProcessingService(PaymentGatewayClient client, TransactionRepository repo) {
this.gatewayClient = client;
this.txRepo = repo;
}
public PaymentResult processTransaction(Long userId, Double amount, String currency) {
// โค้ดตรรกะภายในยาวเหยียด 50 บรรทัด
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
Map payload = new HashMap<>();
payload.put("user", userId);
payload.put("amount", amount);
payload.put("timestamp", System.currentTimeMillis());
// สั่งงานตัดบัตรเครดิต...
return gatewayClient.charge(payload);
}
public boolean refundTransaction(Long transactionId, String reason) {
// โค้ดคืนเงิน...
Transaction tx = txRepo.findById(transactionId);
if (tx == null || tx.isRefunded()) {
return false;
}
tx.markRefunded(reason);
txRepo.save(tx);
return true;
}
public TransactionStatus checkStatus(Long transactionId) {
return txRepo.findStatus(transactionId);
}
}
"""
raw_tokens = estimate_tokens(sample_large_java)
print(f"\n📦 1. ข้อมูลโค้ดดิบ (Raw Text): ความยาว {len(sample_large_java)} chars (~{raw_tokens} Tokens)")
# 2. เริ่มต้นรัน JavaASTParser ด้วย Tree-sitter
parser = JavaASTParser()
start_time = time.perf_counter()
skeleton = parser.extract_skeleton(sample_large_java)
elapsed_ms = (time.perf_counter() - start_time) * 1000
skeleton_tokens = estimate_tokens(skeleton)
print(f"\n⚡ 2. Tree-sitter สกัด Code Skeleton สำเร็จในเวลา: {elapsed_ms:.2f} ms!")
print("-" * 50)
print("📋 [EXTRACTED CODE SKELETON]:")
print(skeleton)
print("-" * 50)
# 3. ตรวจสอบความถูกต้องว่ากรองคอมเมนต์หลอกได้จริง
assert "fakeCommentedMethod" not in skeleton, "❌ ล้มเหลว! Parser เผลอจับเอาเมธอดในคอมเมนต์เข้ามา!"
assert "processTransaction" in skeleton, "❌ ล้มเหลว! เมธอด processTransaction หลุดหาย!"
assert "refundTransaction" in skeleton, "❌ ล้มเหลว! เมธอด refundTransaction หลุดหาย!"
# 4. คำนวณเปอร์เซ็นต์การประหยัด Token
saved_percent = ((raw_tokens - skeleton_tokens) / raw_tokens) * 100
print(f"\n📊 3. ผลลัพธ์ Token Budget Optimization:")
print(f" 👉 ขนาดโค้ดดิบ: ~{raw_tokens} Tokens")
print(f" 👉 ขนาด Skeleton: ~{skeleton_tokens} Tokens")
print(f" 🎉 ประหยัด Token ลงได้ถึง: {saved_percent:.1f}%!")
print("\n" + "=" * 70)
print("🏆 ผ่านการทดสอบ AST Parsing with Tree-sitter สมบูรณ์แบบ 100%!")
print("🚀 คุณพร้อมก้าวสู่ Day 19: Cross-File Call Hierarchy Graph แล้ว!")
print("=" * 70)
if __name__ == "__main__":
main()
ชำแหละไวยากรณ์สำคัญใน intelligence/ast_parser.py
self.language = Language(tsjava.language()); self.parser = Parser(self.language)
Tree-sitter C-binding Initialization: โหลด C grammar ของภาษา Java เข้าสู่ Parser Core ของ Tree-sitter ทำให้การ parse ไวยากรณ์เร็วระดับ Microseconds บนชิป Apple Silicon
name_node = child.child_by_field_name("name")
Named Field Traversal: Tree-sitter ผูกชื่อ Field สำคัญไว้ล่วงหน้า เช่น name, type, parameters ทำให้เราเจาะจงดึงเฉพาะชื่อฟังก์ชันได้แม่นยำ 100% โดยไม่ต้องเดา Index ของโหนด
source_bytes[node.start_byte:node.end_byte].decode("utf8")
Zero-Copy Slice Extraction: Tree-sitter ส่งคืนพิกัด byte offset (`start_byte`, `end_byte`) ทำให้เราตัดเอาเฉพาะข้อความที่ต้องการจาก byte string ดั้งเดิมได้อย่างแม่นยำและประหยัด RAM สูงสุด
เปิด Terminal บน MacBook M3 แล้วทำตาม 5 ขั้นตอนนี้เพื่อติดตั้งไลบรารี Tree-sitter และสั่งรันการทดสอบ AST Skeleton ทันที:
cd ~/se_agent
source .venv/bin/activate
pip install "tree-sitter==0.23.2" "tree-sitter-java==0.23.2"
บน Mac M3 ตัว wheel ของ tree-sitter จะถูกคอมไพล์ลงชิป ARM64 อย่างรวดเร็วในไม่กี่วินาที
mkdir -p intelligence
touch intelligence/__init__.py
touch intelligence/ast_parser.py
touch test_ast_skeleton.py
- อัปเดตไฟล์ requirements.txt ตาม ส่วนที่ 4 (ไฟล์ที่ 1)
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 2) ไปวางใน intelligence/ast_parser.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 3) ไปวางใน intelligence/__init__.py
- นำโค้ดใน ส่วนที่ 4 (ไฟล์ที่ 4) ไปวางใน test_ast_skeleton.py
python test_ast_skeleton.py
🎉 ผลลัพธ์: คุณจะเห็นหน้าจอ Terminal แสดงการสกัด Code Skeleton ในเวลาไม่ถึง 3 มิลลิวินาที! กรองคอมเมนต์หลอกทิ้งสำเร็จ 100% และลด Token Budget ลงได้ถึง 85%!
ข้อที่ 1: วัตถุประสงค์หลักของการนำ AST Parsing มาใช้แทน Raw Text ใน Day 18 คืออะไร?
ข้อที่ 2: คำว่า "AST" ย่อมาจากคำศัพท์ทางคอมพิวเตอร์ศาสตร์คำใด?
ข้อที่ 3: ทำไมการใช้ Regular Expression (Regex) ในการแกะเมธอดของโค้ดโปรเจกต์ขนาดใหญ่จึงอันตรายและไม่แนะนำในระดับ Enterprise?
ข้อที่ 4: จุดเด่นที่สำคัญที่สุดของไลบรารี `Tree-sitter` เมื่อเทียบกับ Parser โบราณทั่วไปคือข้อใด?
ข้อที่ 5: เทคนิค "Code Skeletonization" (การทำโครงกระดูกโค้ด) มีหลักการทำงานอย่างไร?
ข้อที่ 6: เมื่อเปรียบเทียบในแง่ของ Context Window บนเครื่อง MacBook M3 16GB การส่ง Code Skeleton ประหยัดทรัพยากรอย่างไร?
ข้อที่ 7: ในโครงสร้าง Node ของ Tree-sitter เมื่อเราต้องการค้นหาชนิดของโหนด เช่น เมธอด หรือ คลาส เราจะตรวจสอบจากพร็อพเพอร์ตี้ใด?
ข้อที่ 8: โหนดใน Tree-sitter เก็บตำแหน่งของคำในไฟล์ซอร์สโค้ดผ่านค่าใด?
ข้อที่ 9: หาก Agent ต้องการตอบคำถามว่า "โปรเจกต์นี้มี Controller ทั้งหมดกี่ตัว และมี Endpoint อะไรบ้าง" การใช้ AST มีข้อดีอย่างไรเมื่อเทียบกับการใช้ Vector Search เพียงอย่างเดียว?
ข้อที่ 10: ความสามารถ "Error-Tolerant" ของ Tree-sitter มีประโยชน์อย่างไรต่องานของ AI Agent?
ข้อที่ 11: หากต้องการสกัดเฉพาะ Annotation ของ Spring Boot เช่น `@GetMapping` หรือ `@Autowired` เราสามารถใช้ Tree-sitter ค้นหาโหนดประเภทใด?
ข้อที่ 12: การสกัด Code Skeleton ด้วย AST ช่วยป้องกันปรากฏการณ์ "Prompt Dilution" ในโมเดลขนาด 7B-14B อย่างไร?
ข้อที่ 13: เครื่องมือ Tree-sitter ใน Python สามารถนำไปบรรจุลงในโครงสร้างใดของระบบ Dynamic Skill ที่เราสร้างใน Day 16-17?
ข้อที่ 14: หลังจากที่เราสามารถแกะ AST และสกัดโครงร่างโค้ดเดี่ยวๆ ได้ใน Day 18 แล้ว ใน Day 19 เราจะนำ AST ไปต่อยอดสู่เรื่องใด?
ข้อที่ 15: ในฐานะวิศวกรซอฟต์แวร์ที่เข้าใจ AST Parsing ใน Day 18 คุณมองเห็นความได้เปรียบใดของ Agent ของเราเมื่อเทียบกับ AI ทั่วไป?
🏁 สรุปหมุดเชื่อมโยง Day 18 (Chapter Wrap-up)
เนื้อหาใน Day 18 — Beyond Raw Text: AST Parsing with Tree-sitter ได้เปลี่ยนให้ Agent ของเรามองเห็นและเข้าใจโค้ดในระดับโครงสร้างไวยากรณ์ที่แท้จริง ตัดตอนข้อความดิบจนเหลือเฉพาะโครงกระดูก (Code Skeleton) ช่วยลด Token ลงกว่า 85% และเพิ่มความแม่นยำสูงสุดบน MacBook M3 16GB พร้อมให้เราก้าวสู่ Day 19: Cross-File Architecture Analysis & Call Hierarchy Graph ในวันถัดไปแล้วครับ!