ENDEAVOR LOCAL AGENT TH ทำงานอย่างไร — เจาะลึกสถาปัตยกรรม

ภาพประกอบ: ENDEAVOR LOCAL AGENT TH ทำงานอย่างไร — เจาะลึกสถาปัตยกรรม

บทความ เริ่มต้นใช้งาน บอกวิธีติดตั้งและใช้งาน บทความนี้เจาะลึกว่าข้างในทำงานอย่างไร — LOCAL AGENT TH ใช้โมเดลใหญ่กว่า AGENT LITE มาก (35B เทียบกับ 2B) จึงพึ่งพาความสามารถของโมเดลเองได้มากกว่า แต่จุดที่น่าสนใจคือทีมพัฒนายังคงไม่ยอมฝากการตัดสินใจสำคัญไว้กับโมเดลทั้งหมด โดยเฉพาะเรื่องการวางแผนงานและความปลอดภัย

วงจรหลัก: ยังเป็น ReAct node เดียว แต่มีชั้นก่อนหน้าเพิ่มขึ้น

โครงกราฟหลักยังเรียบง่าย — START → react → END ผ่าน LangGraph node เดียว เหมือน AGENT LITE — แต่ก่อนถึง node นั้น มีชั้น planner คั่นอยู่ ที่ทำหน้าที่ตัดสินใจว่าคำถามนี้ “ง่าย” พอจะให้โมเดลตอบตรงๆ หรือ “ซับซ้อน” พอที่ต้องบังคับให้วางแผนก่อน

   START ──▶ [ planner node ] ──▶ [ react node ] ──┐
                    │                    ▲          │
              จัดหมวดคำถาม               └── วน tool ─┘
              simple / complex                       │

                                                     END

(รายละเอียดกลไก react node แบบเต็ม — วิธี LangGraph วนลูป, state, tool_calls — อยู่ใน ReAct Agent ทำงานอย่างไร)

Planner: จัดหมวดคำถามก่อนคิดคำตอบ

planner.py เรียกโมเดลเบาๆ 1 ครั้งต่อคำถาม (ปิด thinking, temperature=0.0 เพื่อผลลัพธ์นิ่ง, cache client instance ไว้ไม่สร้างใหม่ทุกครั้ง) ให้จัดหมวดเป็น JSON ล้วนๆ 2 แบบ:

  • {"mode": "simple"} — ตอบตรงได้เลย ไม่ต้องวางแผน
  • {"mode": "complex", "plan": [...2-6 ขั้นตอนที่เป็นผลลัพธ์ที่ต้องการ...]} — ต้องแตกเป็นขั้นตอนก่อน

Contract ของ JSON เข้มงวด และมีretry แค่ 1 ครั้งถ้า parse ไม่ผ่าน (JSON เพี้ยน, mode ไม่รู้จัก, plan ไม่ใช่ list, complex แต่ step น้อยกว่า 2 — ทุกกรณีนี้ถือเป็น schema violation ที่ต้อง retry ไม่ใช่ปัดตกเป็น simple เงียบๆ) — ถ้ายัง parse ไม่ผ่านอีกรอบสอง ค่อย fallback ไปเป็น simple mode แทนที่จะค้าง ถ้าตอบมาเกิน 6 ขั้นตอน ระบบตัดเหลือ 5 ขั้นแรก + ขั้นสุดท้ายเสมอ (ไม่ใช่ตัดจาก step 7 ทิ้งตรงๆ) เพราะขั้นสุดท้ายมักเป็น synthesis ที่ห้ามหาย

ในซอร์สยังมีฟังก์ชัน is_trivial() (จับคำทักทาย/เรียกความจำด้วย regex) หลงเหลืออยู่ แต่ไม่ได้ถูกเรียกใช้จริงในเส้นทางการทำงาน — เหลือไว้เป็น diagnostic ให้ unit test เท่านั้น การจัดหมวด simple/complex จริงๆ ผ่าน LLM call ด้านบนเสมอ ไม่มี shortcut ข้ามโมเดลด้วย regex

กลไกที่น่าสนใจที่สุด: บังคับสร้างแผนแบบ deterministic

จุดที่ทำให้ LOCAL AGENT TH ต่างจาก agent ทั่วไปคือฟังก์ชัน _force_plan_or_directive() ใน graph.py — แทนที่จะหวังว่าโมเดลจะเรียก tool สร้างแผนเองตามที่ planner บอก ระบบเขียนโค้ดบังคับให้เกิดขึ้นจริง:

  • ถ้าคำถามมีคำกริยาชี้ชัดว่าเป็นงานค้นคว้า/วิจัย (search-verb) และ planner บอกว่า “complex” — โค้ดจะฉีดคู่ข้อความปลอมเข้าไปในประวัติสนทนาโดยตรง: AIMessage ที่มี tool_calls=[create_plan] ตามด้วย ToolMessage ที่บรรจุแผนงานจาก planner ไว้แล้ว การเรียก tool สร้างแผนจึงถูกการันตีว่าจะเกิดขึ้นโดยไม่ต้องพึ่งว่าโมเดลจะ “นึกออก” ที่จะเรียกเองไหม
  • ถ้า planner บอกว่า “simple” — ฉีดข้อความสั่งแบบ soft directive แทน (เป็นคำแนะนำ ไม่ใช่ tool call ปลอม) ให้โมเดลตอบตรงได้เลยโดยไม่ต้องผ่านขั้นตอนวางแผน

หลักคิดเดียวกับ AGENT LITE — โค้ด ไม่ใช่ prompt — แต่ประยุกต์กับปัญหาคนละแบบ: AGENT LITE ใช้โค้ดชดเชยความผิดพลาดของโมเดลเล็ก ส่วน LOCAL AGENT TH ใช้โค้ดการันตีขั้นตอนสำคัญไม่ให้พลาดแม้โมเดลจะฉลาดกว่ามากก็ตาม

การจัดการ context ยาว: บีบอัดแบบมีเกณฑ์ชัดเจน

Session สนทนายาวๆ ทำให้ context ใกล้เพดานของโมเดลได้ ระบบตั้งค่าคงที่ 4 ตัวควบคุมการบีบอัด:

ค่าคงที่ ความหมาย
_COMPACT_TRIGGER = 1.00 context เต็ม 100% ของเพดานที่ตั้งไว้ → เริ่มบีบอัด
_COMPACT_RESET = 0.70 บีบอัดจนเหลือ 70% แล้วหยุด ไม่บีบจนสั้นเกินจำเป็น
_COMPACT_MIN_MSGS = 4 ต้องมีอย่างน้อย 4 ข้อความถึงจะเริ่มพิจารณาบีบอัด (กันบีบ session สั้นๆ โดยไม่จำเป็น)
_COMPACT_STRIP_MAX_CHARS = 5000 ตัดผลลัพธ์ tool ที่ยาวเกิน 5,000 ตัวอักษรต่อรายการก่อน ไม่ใช่ตัดข้อความสนทนา

การบีบอัดเลือกตัดผลลัพธ์ดิบจาก tool ที่ยาวเกินไปก่อนเป็นอันดับแรก เพราะเป็นส่วนที่กินพื้นที่มากสุดแต่มีค่าน้อยสุดเมื่อผ่านไปหลายรอบแล้ว

ระบบความปลอดภัย: สองชั้นที่ทำงานอิสระจากกัน

จุดที่ต่างจาก AGENT LITE ชัดเจนที่สุดคือ LOCAL AGENT TH มีเครื่องมือให้เข้าถึงไฟล์และรันโค้ดได้จริง (ไม่ใช่แค่ค้นเว็บ) จึงต้องมีระบบป้องกันที่แน่นหนากว่า — ออกแบบเป็น 2 ชั้นที่ทำงานอิสระจากกัน ไม่พึ่งพากันเอง:

คำสั่ง bash/python_exec เข้ามา


┌───────────────────────────────┐
│ ชั้น 1: Python path guard       │  ← ถ้าพลาด/มีบั๊ก...
│ เขียนได้แค่ workspace/          │
│ อ่านบล็อก .ssh/.aws/.gnupg      │
└───────────────────────────────┘


┌───────────────────────────────┐
│ ชั้น 2: macOS Seatbelt (OS)     │  ← ...ชั้นนี้ยังกันไว้ได้
│ sandbox-exec บังคับที่ระดับ OS  │     เพราะเป็นคนละกลไก ไม่พึ่งกัน
│ ไม่ผ่าน Python เลย              │
└───────────────────────────────┘


   คำสั่งรันจริง (ถ้าผ่านทั้ง 2 ชั้น)

ชั้นที่ 1: Python-level path guard

  • เขียนไฟล์: จำกัดเฉพาะในโฟลเดอร์ workspace/ เท่านั้น เขียนออกนอกนี้ไม่ได้เลย
  • อ่านไฟล์: เปิดกว้างกว่า (อ่าน path นอก workspace ได้บ้างเพื่อความยืดหยุ่น) แต่บล็อกกลุ่ม path ที่เข้าข่ายเก็บ credential เช่น .ssh, .aws, .gnupg และ path ระบบสำคัญ
  • กัน path traversal: ทุก path ผ่าน os.path.realpath() ก่อนตรวจสอบ กัน symlink หลอกและ ../ ไต่ออกนอกขอบเขตที่อนุญาต

ชั้นที่ 2: macOS sandbox-exec (Seatbelt) — ชั้น OS

Layer แรกเป็นแค่โค้ด Python ซึ่งถ้ามี bug หรือถูกเลี่ยงได้ก็ยังเสี่ยง — ทีมพัฒนาจึงเพิ่มชั้นที่ 2 ที่ทำงานที่ระดับระบบปฏิบัติการไม่ใช่ระดับแอป: ทุกครั้งที่รันคำสั่ง bash, bash_bg หรือ python_exec process ย่อยนั้นถูกห่อด้วย macOS Seatbelt sandbox profile ที่กำหนด rule แบบ (deny file-write*) / (deny file-read*) / (allow file-write* <path>) ชัดเจน และมี timeout ต่อ process แยกกัน

บทเรียนจริงที่เจอระหว่างพัฒนา: สมมติฐานเดิมของทีมคือ sandbox-exec ใช้กติกา “last-match-wins” (rule หลังสุดที่ match ชนะ) — แต่การทดสอบจริงพบว่าไม่ใช่: เมื่อ path ใดซ้อนทับกัน (เช่น deny ทั้งโฟลเดอร์ แล้ว allow path ย่อยข้างใน) deny ชนะ allow เสมอไม่ว่าจะเขียนก่อนหรือหลังในไฟล์ profile เรื่องนี้ถูกค้นพบตอนแก้บั๊กจริง: bash('cat /etc/passwd') เคยอ่านไฟล์ผ่านได้ทั้งที่ตั้งใจ block เพราะ deny เดิมครอบแค่ ~/.ssh/~/.aws/~/.gnupg แต่ไม่ครอบ /etc เมื่อลองแก้ด้วยการ deny ทั้ง /etc แล้ว allow เฉพาะ /etc/ssl กลับมา ก็พังคนละแบบทันที (curl/git ผ่าน HTTPS ใช้ไม่ได้ เพราะ deny ทั้งโฟลเดอร์ชนะ allow ย่อยเสมอ ไม่มีทาง exception กลับมาได้) จนต้อง fix ด้วยการ deny เป็นไฟล์เจาะจง (literal) แทนการ deny ทั้งโฟลเดอร์ (subpath) — เช่น deny เฉพาะ /etc/passwd, /etc/shadow, /etc/sudoers ตรงๆ โดยไม่แตะ /etc/ssl, /etc/hosts ที่ curl/git ต้องใช้

ความสำคัญของชั้นนี้คือ แม้ path guard ชั้น Python จะพลาดหรือถูกข้าม (เช่นบั๊กในโค้ด หรือช่องโหว่ที่ยังไม่รู้) sandbox-exec ยังบล็อกการเข้าถึงไฟล์นอกขอบเขตที่ระดับ OS อยู่ดี — นี่คือหลัก defense-in-depth: สองชั้นป้องกันคนละแบบ ไม่ใช่ป้องกันซ้ำแบบเดียวกัน และแม้แต่ชั้นนี้เองก็ยังพลาดสมมติฐานผิดได้ ต้องทดสอบจริงถึงจะมั่นใจ ไม่ใช่แค่เชื่อ documentation ของเครื่องมือ

เครื่องมือ 26 ตัว จัดเป็น 9 หมวด

หมวด จำนวน ตัวอย่าง
Web & Research 7 ค้นเว็บ, ดึงเนื้อหาหน้าเว็บ, ค้นข่าว
File & Code 5 อ่าน/เขียนไฟล์, แก้โค้ด, list โฟลเดอร์
Code Execution 4 รัน bash (sync), bash แบบ background (bash_bg), รัน Python, วาดกราฟ (ทั้งหมดผ่าน sandbox-exec)
Vision 1 อ่านข้อความจากรูปด้วย OCR
Memory 1 จำข้อเท็จจริงเกี่ยวกับผู้ใช้ข้าม session แบบถาวร
Audio 1 อ่านข้อความออกเสียงผ่านลำโพงเครื่องที่รัน agent
MCP 4 ต่อ/เรียก MCP server ภายนอกที่ผู้ใช้ระบุ URL ให้
Automation 1 ตั้ง standing trigger (awake) ให้ agent ทำงานเองตามเวลา/เมื่อไฟล์เปลี่ยน — ดูหัวข้อถัดไป
Planning & Loops 2 สร้างแผน (create_plan), วนทำตามแผน
Research Skill 1 โหมดค้นคว้าเชิงลึกเฉพาะทาง (เปิดด้วย /research)

Standing triggers: agent ทำงานเองได้โดยไม่ต้องมีคนพิมพ์

จุดที่ต่างจากโมเดล chat ทั่วไปชัดเจน — เครื่องมือ awake ให้โมเดลตั้งเงื่อนไขล่วงหน้าว่าเมื่อไรควรกลับมาทำงานเองอีกครั้งโดยไม่ต้องรอผู้ใช้พิมพ์ถาม มี daemon thread (awake_engine.py) คอยเช็คทุก 30 วินาทีว่าเงื่อนไขไหน “ตื่น” แล้วบ้าง แล้วส่ง query เข้า agent loop เดิมทันที:

ประเภท trigger ทำงานเมื่อ
file ไฟล์ที่ระบุถูกแก้ไข
every ครบทุก N นาทีตามที่ตั้ง
times / run_at ถึงเวลาที่กำหนดไว้ล่วงหน้า
once ครั้งเดียวหลังจากดีเลย์ที่ตั้ง
screen จอภาพเปลี่ยนแปลง (อ่านผ่าน OCR)

screen เห็นการเปลี่ยนแปลงบนจอและแจ้งได้ แต่ agent เวอร์ชันนี้คลิก/พิมพ์บนจอเองไม่ได้ (ไม่มี computer-use tool ในชุดนี้) — เหมาะกับ “เฝ้าดูแล้วบอก” มากกว่า “เฝ้าดูแล้วกดให้”

trigger ที่ fired แล้วจะแย่ง lock เดียวกับคำถามจริงของผู้ใช้ (_busy) เสมอ — ป้องกันไม่ให้งานเบื้องหลังกับคำถามสดของผู้ใช้รันพร้อมกันจนชนกัน ใครถือ lock อยู่ก่อนก็ทำงานให้เสร็จก่อน ส่วนกรณีรัน engine พร้อมกันหลายจุด (เช่นเครื่องหลักกับ Telegram bot) มีกลไกแยกต่างหากตัดสินว่า process ไหน “เป็นเจ้าของ” การยิง trigger จริง กันไม่ให้ trigger เดียวกันทำงานซ้ำสองรอบ

Skill modes: สลับ prompt และ toolset ทั้งชุด

สั่ง /research หรือ /pdf_to_text ในแชทจะเปลี่ยนทั้ง system prompt และชุดเครื่องมือที่โมเดลมองเห็น ไม่ใช่แค่เพิ่ม instruction เข้าไปในบทสนทนาเดิม — วิธีนี้ทำให้โมเดลโฟกัสกับ tool ที่เกี่ยวข้องกับงานนั้นจริงๆ ไม่สับสนกับเครื่องมือทั่วไปที่ไม่จำเป็นสำหรับงานเฉพาะทาง

ทำไมเรื่องนี้ถึงสำคัญ

LOCAL AGENT TH พิสูจน์ว่าการมีโมเดลใหญ่กว่าไม่ได้แปลว่าเลิกใช้ guardrail แบบ deterministic ได้ทั้งหมด — แค่ เปลี่ยนจุดที่ใช้: AGENT LITE ใช้โค้ดชดเชยจุดอ่อนของโมเดลเล็กแทบทุกขั้นตอน ส่วน LOCAL AGENT TH ปล่อยให้โมเดลตัดสินใจเนื้อหาส่วนใหญ่เอง แต่ยังคงบังคับขั้นตอนวางแผนด้วยโค้ดและป้องกันความปลอดภัยด้วย 2 ชั้นอิสระ เพราะงานที่เข้าถึงไฟล์และรันโค้ดได้จริงมีความเสี่ยงสูงกว่าการค้นเว็บอย่างเดียวมาก — ยิ่งงานเสี่ยงสูง ยิ่งไม่ควรฝากไว้กับดุลพินิจของโมเดลล้วนๆ

อ่านเพิ่มเติม

ความคิดเห็น

กำลังโหลดความคิดเห็น...