TL;DR — มอบ skill (ทักษะ) ที่สามารถนำกลับมาใช้ใหม่ได้ให้กับ AI coding agent ของคุณ (เช่น GitHub Copilot,Claude Code,OpenAI Codex,Gemini CLI) เพื่อให้มันสามารถเริ่ม, เชื่อมต่อ และตรวจสอบ (introspect)BEAM node ที่กำลังรันอยู่ได้ แทนที่จะต้องเดาพฤติกรรมตอน runtime หรือเขียน script ทดสอบแล้วทิ้งไป สถาปัตยกรรมนี้ช่วยให้ agent สามารถคิวรีสถานะของ GenServer, ตรวจสอบ supervision trees, ดูข้อมูลใน ETS tables และทำ hot-reload code ได้ ทั้งหมดนี้ผ่าน script shell เพียงตัวเดียว

บทความนี้สมบูรณ์ในตัวเอง: คุณสามารถส่งลิงก์นี้ให้ coding agent ของคุณแล้วบอกว่า "นำรูปแบบนี้ไปใช้กับโปรเจกต์ของฉัน"


𝐓𝐇𝐄 𝐏𝐑𝐎𝐁𝐋𝐄𝐌 (ปัญหา)

เมื่อ AI coding agent ทำงานในโปรเจกต์ Elixir โดยทั่วไปมันจะมีสองทางเลือกในการยืนยันการเปลี่ยนแปลง: รันการทดสอบ (mix test) หรือใช้การวิเคราะห์ code แบบคงที่ (static reasoning) ทั้งสองวิธีนี้ไม่ยอมให้มัน สังเกตการณ์ระบบที่กำลังทำงานอยู่จริงๆ ได้เลย — เช่น การเช็คว่า GenServer มีสถานะที่ถูกต้องไหม,supervision tree กู้คืนจากการแครชได้จริงหรือเปล่า หรือข้อความถูกส่งไปถึงปลายทางจริงๆ หรือไม่

BEAM VM มีระบบการตรวจสอบภายใน (introspection) ระดับโลกติดตั้งมาให้ในตัว ทุกๆ Erlang/Elixir node สามารถเชื่อมต่อจาก node อื่นได้โดยใช้ distributed Erlang เคล็ดลับคือการสอนให้ coding agent รู้วิธีใช้งานสิ่งนี้

ทำไมสิ่งนี้ถึงสำคัญ: "The Soul of Erlang and Elixir"

ในทอล์กเรื่อง "The Soul of Erlang and Elixir", Saša Jurić ได้แสดงความความสามารถนี้กับระบบที่กำลังรันอยู่จริง เขา SSH เข้าไปใน server ที่กำลังทำงาน เปิด remote console และเจาะลึกลงไปในปัญหาโดยไม่ต้องรีสตาร์ทอะไรเลย:

"BEAM เป็น runtime ที่สามารถ debug, ตรวจสอบสถานะ และสังเกตการณ์ได้อย่างยอดเยี่ยม BEAM ยอมให้เราเข้าไปเชื่อมต่อกับระบบที่กำลังทำงานอยู่เพื่อแอบดูและปรับแต่งข้อมูลภายใน และได้รับข้อมูลที่มีประโยชน์มากมาย — โดยที่ผมไม่ต้องตั้งค่า flag พิเศษอะไรเลย ไม่ต้องรีสตาร์ทระบบหรือทำอะไรทั้งสิ้น ผมสามารถทำสิ่งนี้ได้เป็นปกติอยู่แล้ว" — Saša Jurić

จาก remote shell เขาสามารถลิสต์ process ทั้งหมด, ระบุ process ที่กิน CPU สูงจากจำนวน reduction, ดู stack trace, ติดตามการเรียก function(trace), สั่งปิด process ด้วย Process.exit(pid, :kill)— และส่วนที่เหลือของระบบยังคงทำงานต่อที่ 10,000requests/second ได้โดยไม่ถูกรบกวน จากนั้นเขาก็ทำการ hot-deploy ตัวแก้ไขเข้าไปในโหนดโปรดักชันที่กำลังรันอยู่โดยไม่ต้องรีสตาร์ท

นี่คือสิ่งที่เรากำลังมอบให้กับ AI coding agent: ความสามารถแบบเดียวกับที่ Saša สาธิตด้วยตัวเอง แต่ห่อหุ้มไว้ใน interface แบบ script(dev_node.sh rpc) ที่ agent สามารถเรียกใช้ได้โดยไม่ต้องมี interactive TTY ทำให้ agent กลายเป็นโอเปอเรเตอร์ที่ SSH เข้าไปใน BEAM ได้


𝐓𝐇𝐄 𝐏𝐀𝐓𝐓𝐄𝐑𝐍 (รูปแบบสถาปัตยกรรม)

มีส่วนประกอบสามส่วนที่ทำงานร่วมกัน:

  1. โปรเจกต์รันด้วยชื่อโหนด (node name) และ cookie(cookie) ที่กำหนดไว้ — เพื่อให้ script ช่วยของ agent สามารถเชื่อมต่อได้
  2. script dev_node.sh — อยู่ในโปรเจกต์เพื่อทำหน้าที่ start,stop,status,rpc และ eval_file
  3. Skill definition — บอกให้ coding agent รู้ว่า เมื่อไหร่ และ อย่างไร ที่ควรจะใช้ live introspection

BEAM Live Introspection Pattern

RPC node จะถูกรันในฐานะ hidden node (ใช้ flag--hidden) ในระบบ distributed Erlang โหนดที่ซ่อนอยู่จะไม่เข้าร่วมใน cluster mesh ส่วนกลาง — มันจะไม่กระตุ้นให้เกิดการเชื่อมต่อแบบ transitive, ไม่ปรากฏใน nodes() และมองไม่เห็นสำหรับการลงทะเบียน process แบบ :global นี่คือสิ่งที่เราต้องการ: โหนดตรวจสอบควรสังเกตการณ์ระบบโดยไม่เข้าไปเป็นส่วนหนึ่งของ cluster หรือทำให้ตัวจัดตารางงาน (scheduler) พยายามส่งงานมาให้มันทำ


𝐒𝐭𝐞𝐩 𝟏: เตรียมโปรเจกต์ให้พร้อมสำหรับการตรวจสอบ

application ของคุณต้องเริ่มต้นด้วย short name (--sname) และ cookie (--cookie) วิธีที่ง่ายที่สุดคือสร้าง script run ไว้ที่ root ของโปรเจกต์:

ไฟล์ run(ตัวอย่างสำหรับ Phoenix)

#!/bin/bash
cd "$(dirname "$0")" || exit 1
SNAME="$(basename "$(pwd)")"
exec elixir --sname "$SNAME" --cookie devcookie -S mix phx.server > run.log 2>&1

อย่าลืมสั่ง chmod +x run

หลักการทำงานของ --sname: คำสั่ง --sname my_app จะลงทะเบียนโหนดในชื่อ my_app@<hostname> โดย cookie(ที่เป็นความลับ) ต้องตรงกันทั้งสองฝั่งเพื่อให้ distributed Erlang เชื่อมต่อกันได้ การใช้ชื่อโฟลเดอร์โปรเจกต์เป็น sname เป็นธรรมเนียมที่ script dev_node.sh ใช้เพื่อให้ทุกอย่างทำงานได้โดยไม่ต้องตั้งค่าเพิ่มเติม

สำหรับโปรเจกต์ที่ไม่ใช่ Phoenix เปลี่ยน mix phx.server เป็น mix run --no-halt:

exec elixir --sname "$SNAME" --cookie devcookie -S mix run --no-halt > run.log 2>&1

สำหรับ Production Releases เมื่อรัน Mix release ให้ใช้ flag--sname และ --cookie ในไฟล์ config ของ release หรือ rel/env.sh.eex:

export RELEASE_NODE="my_app"
export RELEASE_COOKIE="devcookie"
export RELEASE_DISTRIBUTION="sname"

⚠️ ในโปรดักชันควรใช้ cookie ที่แข็งแกร่งกว่านี้ devcookie มีไว้สำหรับการพัฒนาในเครื่องเท่านั้น cookie ของ Erlang เป็นเรื่องสำคัญด้านความปลอดภัยมาก มันคือสิ่งเดียวที่กั้นระหว่างคุณกับผู้บุกรุกที่จะเข้ามาควบคุม cluster ของคุณได้อย่างสมบูรณ์


𝐒𝐭𝐞𝐩 𝟐: เพิ่ม script dev_node.sh

สร้าง scripts/dev_node.sh ในโปรเจกต์ของคุณ นี่จะเป็นจุดเข้าใช้งานจุดเดียวสำหรับการตรวจสอบ BEAM ทั้งหมด:

#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
APP_NAME="${DEV_NODE_NAME:-$(basename "$PROJECT_DIR")}"
COOKIE="${DEV_NODE_COOKIE:-devcookie}"
HOSTNAME="$(hostname -s)"
FQDN="${APP_NAME}@${HOSTNAME}"
PIDFILE=".dev_node.pid"

case "${1:-help}" in
 start)
 if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
 echo "Node already running (pid $(cat "$PIDFILE"))"
 exit 0
 fi
 echo "Starting node ${FQDN} ..."
 elixir --sname "$APP_NAME" --cookie "$COOKIE" -S mix run --no-halt > .dev_node.log 2>&1 &
 echo $! > "$PIDFILE"
 for i in $(seq 1 30); do
 if elixir --sname "probe_$$" --cookie "$COOKIE" --hidden -e "
 Node.connect(:\"${FQDN}\") |> IO.inspect()
 " 2>/dev/null | grep -q "true"; then
 echo "Node ${FQDN} is up (pid $(cat "$PIDFILE"))"
 exit 0
 fi
 sleep 1
 done
 echo "ERROR: Node did not become reachable within 30s. Check .dev_node.log"
 exit 1
 ;;

 stop)
 if [ -f "$PIDFILE" ]; then
 kill "$(cat "$PIDFILE")" 2>/dev/null && echo "Node stopped" || echo "Node was not running"
 rm -f "$PIDFILE"
 else
 echo "No pidfile found"
 fi
 ;;

 status)
 if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
 echo "Node running (pid $(cat "$PIDFILE"))"
 else
 echo "Node not running"
 rm -f "$PIDFILE" 2>/dev/null
 fi
 ;;

 rpc)
 shift
 EXPR="$*"
 elixir --sname "rpc_$$" --cookie "$COOKIE" --hidden --no-halt -e "
 target = :\"${FQDN}\"
 true = Node.connect(target)
 {result, _binding} = :rpc.call(target, Code, :eval_string, [\"\"\"
 ${EXPR}
 \"\"\"])
 IO.inspect(result, pretty: true, limit: 200, printable_limit: 4096)
 System.halt(0)
 "
 ;;

 eval_file)
 shift
 FILE="$1"
 elixir --sname "rpc_$$" --cookie "$COOKIE" --hidden --no-halt -e "
 target = :\"${FQDN}\"
 true = Node.connect(target)
 code = File.read!(\"${FILE}\")
 {result, _binding} = :rpc.call(target, Code, :eval_string, [code])
 IO.inspect(result, pretty: true, limit: 200, printable_limit: 4096)
 System.halt(0)
 "
 ;;

 help|*)
 echo "Usage: scripts/dev_node.sh {start|stop|status|rpc <expr>|eval_file <path>}"
 echo ""
 echo "Environment variables:"
 echo " DEV_NODE_NAME - sname for the node (default: project directory name)"
 echo " DEV_NODE_COOKIE - cluster cookie (default: devcookie)"
 ;;
esac

อย่าลืมตั้งสิทธิ์ให้รันได้:

mkdir -p scripts
chmod +x scripts/dev_node.sh

การทำงานของ dev_node.sh

คำสั่ง หน้าที่
start รัน mix run --no-halt เป็นเบื้องหลัง และรอจนกว่าจะเชื่อมต่อได้
stop ปิดโหนดเบื้องหลังผ่านไฟล์ PID
status ตรวจสอบว่าโหนดยังทำงานอยู่ไหม
rpc <expr> สร้างโหนดชั่วคราว เชื่อมต่อกับแอป รัน code ผ่าน :rpc.call แสดงผล แล้วปิดโหนด
eval_file <path> เหมือน rpc แต่อ่าน code มาจากไฟล์ .exs— เหมาะสำหรับงานตรวจสอบที่ซับซ้อน

การตัดสินใจด้านสถาปัตยกรรม: การเรียก rpc แต่ละครั้งจะเป็นแบบไม่มีสถานะ (stateless) โหนดที่ซ่อนอยู่จะถูกสร้างใหม่ รัน code และปิดตัวลง วิธีนี้ช่วยหลีกเลี่ยงปัญหาการเชื่อมต่อค้าง แต่ตัวแปรที่ประกาศไว้จะไม่ถูกส่งต่อไปยังการเรียกครั้งถัดไป Flag--hidden ช่วยให้โหนด RPC ไม่เข้าไปยุ่งกับการจัดตารางงานของ cluster

เพิ่ม shutdown.sh เพื่อการปิดที่นุ่มนวล (Graceful Stop)

#!/usr/bin/env bash
"$(cd "$(dirname "$0")" && pwd)/dev_node.sh" rpc "System.halt()"

คำสั่งนี้จะสั่งให้โหนดที่รันอยู่ปิดตัวลงผ่าน System.halt() ของ BEAM ซึ่งจะช่วยรัน callback ของการปิด application อย่างถูกต้อง


𝐒𝐭𝐞ป 𝟑: สร้าง Skill Definition

Skill คือไฟล์ markdown (SKILL.md) ที่มี YAML front-matter และคำแนะนำสำหรับ coding agent โดยจะเก็บไว้ในโฟลเดอร์เดียวกับ script ช่วย

สร้างโครงสร้างโฟลเดอร์:

beam-introspection/
├── SKILL.md
└── scripts/
 └── dev_node.sh (คัดลอกหรือสร้าง symlink)

เนื้อหาใน SKILL.md

---
name: beam-introspection
description: >
 เริ่ม, เชื่อมต่อ และตรวจสอบพฤติกรรม `runtime` ของ BEAM/Elixir node
 ใช้เมื่อต้องการทดสอบ,`debug`, ตรวจสอบพฤติกรรม หรือสังเกตสถานะของ `application`
 เช่น GenServer state, supervision trees, ETS tables หรือทำ hot-reload`code`
 ใช้แทนการเขียน `script` ทดสอบแล้วทิ้ง
---

# BEAM Live Introspection Skill

## จุดประสงค์
ทักษะนี้ช่วยให้คุณสามารถเริ่ม, เชื่อมต่อ, ตรวจสอบ และควบคุมโหนด BEAM/Elixir ที่กำลังรันอยู่ได้ แทนการเขียน `script` แยกต่างหาก

## เมื่อไหร่ที่ควรใช้
- เมื่อต้องการดูสถานะของ GenServer หรือพฤติกรรมของ `process`
- เมื่อต้องการทดสอบลำดับการทำงานกับระบบที่รันอยู่จริง
- เมื่อต้องการ `debug` โดยการไล่ดู process tree แบบสดๆ
- เมื่อต้องการยืนยันว่า `code` ที่แก้ทำงานได้ผ่านการ hot-reload

## วิธีใช้งาน
1. ตรวจสอบว่ามี `scripts/dev_node.sh` ในโปรเจกต์
2. เริ่มโหนดด้วย `scripts/dev_node.sh start`
3. ใช้ `rpc` เพื่อตรวจสอบ เช่น:
`scripts/dev_node.sh rpc "Supervisor.which_children(MyApp.Supervisor)"`

𝐒𝐭𝐞𝐩 𝟒: ลงทะเบียน Skill กับ Agent ของคุณ

แต่ละ coding agent มีตำแหน่งเก็บ skill ต่างกัน แต่โครงสร้างภายในจะเหมือนกัน:

GitHub Copilot

  • User-level:~/.github/skills/beam-introspection/
  • Project-level: เพิ่มคำแนะนำใน .github/copilot-instructions.md หรือ AGENTS.md

Claude Code

  • User-level:~/.claude-<profile>/skills/beam-introspection/
  • Project-level: สร้างไฟล์ CLAUDE.md ที่ root ของโปรเจกต์

Gemini CLI

  • User-level:~/.gemini/skills/beam-introspection/
  • Project-level: เพิ่มใน GEMINI.md หรือ AGENTS.md

script สำหรับติดตั้งด่วนสำหรับทุก Agent รัน script นี้จาก root ของโปรเจกต์เพื่อติดตั้ง skill ให้กับทุก agent ที่รองรับ:

#!/usr/bin/env bash
set -euo pipefail

SKILL_NAME="beam-introspection"
SKILL_SOURCE="$(cd "$(dirname "$0")" && pwd)/scripts/dev_node.sh"

AGENT_DIRS=(
 "$HOME/.github/skills"
 "$HOME/.agents/skills"
 "$HOME/.codex/skills"
 "$HOME/.gemini/skills"
)

for d in "$HOME"/.claude-*/; do
 [ -d "$d" ] && AGENT_DIRS+=("${d}skills")
done

for dir in "${AGENT_DIRS[@]}"; do
 target="$dir/$SKILL_NAME"
 mkdir -p "$target/scripts"
 if [ -f "$SKILL_SOURCE" ]; then
 cp "$SKILL_SOURCE" "$target/scripts/dev_node.sh"
 chmod +x "$target/scripts/dev_node.sh"
 echo "Installed to $target"
 fi
done

𝐔𝐒𝐀𝐆𝐄 𝐄𝐗𝐀𝐌𝐏𝐋𝐄𝐒 (ตัวอย่างการใช้งาน)

เมื่อติดตั้งเสร็จแล้ว นี่คือตัวอย่างสิ่งที่คุณสามารถโต้ตอบกับ agent ได้:

"GenServer ของฉันทำงานอยู่ไหม?" คุณถาม: "เช็คหน่อยว่า OrderProcessor GenServer ยังรันอยู่ไหม และ state ของมันเป็นยังไง" Agent จะรัน:

scripts/dev_node.sh rpc "
 case GenServer.whereis(MyApp.OrderProcessor) do
 nil -> :not_running
 pid -> {:running, pid, :sys.get_state(pid)}
 end
"

"ทำไมคิวถึงค้าง?" คุณถาม: "กระบวนการจัดการข้อความมีปัญหา ช่วย debug ให้หน่อย" Agent จะรัน:

scripts/dev_node.sh rpc "
 Process.list()
 |> Enum.map(fn pid -> {pid, Process.info(pid, [:registered_name, :message_queue_len])} end)
 |> Enum.reject(fn {_, info} -> Keyword.get(info, :message_queue_len) == 0 end)
 |> Enum.sort_by(fn {_, info} -> Keyword.get(info, :message_queue_len) end, :desc)
 |> Enum.take(5)
"

"Hot-reloadcode ที่แก้แล้วทดสอบเลย" คุณถาม: "ฉันแก้ตรรกะการลองใหม่แล้ว ช่วยโหลดเข้าโหนดที่รันอยู่แล้วทดสอบให้หน่อย" Agent จะรัน:

mix compile
scripts/dev_node.sh rpc "IEx.Helpers.recompile()"
scripts/dev_node.sh rpc "MyApp.OrderProcessor.retry_pending()"

𝐇𝐨𝐰 𝐈𝐭 𝐖𝐨𝐫𝐤𝐬 𝐔𝐧𝐝𝐞𝐫 𝐭𝐡𝐞 𝐇𝐨𝐨𝐝 (เบื้องหลังการทำงาน)

เมื่อรัน dev_node.sh rpc ระบบจะทำสิ่งนี้:

  1. เริ่ม โหนด BEAM ชั่วคราวที่ซ่อนอยู่ พร้อมชื่อสุ่ม (rpc_<pid>) และใช้ cookie เดียวกัน
  2. เรียก Node.connect/1 เพื่อเชื่อมต่อกับแอปเป้าหมายผ่าน distributed Erlang
  3. ใช้ :rpc.call/4 เพื่อรัน Code.eval_string/1 บนโหนดเป้าหมาย — ทำให้ code รันในบริบทของแอปจริงๆ และเข้าถึง module และสถานะทั้งหมดได้
  4. แสดงผลลัพธ์ผ่าน IO.inspect/2 และปิดตัวเองลง

นี่คือกลไกเดียวกับที่ iex --remsh ใช้ แต่ถูกห่อหุ้มไว้ใน interface ที่ AI agent สามารถเรียกใช้ได้โดยไม่ต้องใช้ interactive TTY


𝐒𝐄𝐂𝐔𝐑𝐈𝐓𝐘 𝐂𝐎𝐍𝐒𝐈𝐃𝐄𝐑𝐀𝐓𝐈𝐎𝐍𝐒 (ข้อควรระวังด้านความปลอดภัย)

-cookie devcookie เป็นที่รู้กันทั่วไป ใครก็ตามในเครื่องเดียวกันสามารถเชื่อมต่อได้ ใช้สำหรับการพัฒนาในเครื่องเท่านั้น -Code.eval_string/1 สามารถรัน code อะไรก็ได้ (Arbitrary code execution) ซึ่งเป็นสิ่งที่ตั้งใจเพื่อให้ agent ทำงานได้เต็มที่ แต่ต้องระวังหากใช้ในสภาพแวดล้อมที่แชร์กับผู้อื่น ---sname จำกัดการเชื่อมต่อเฉพาะภายในเครื่องเดียวกัน การใช้ --name จะอนุญาตให้เชื่อมต่อข้ามเครื่องได้ (ไม่แนะนำหากไม่มี TLS)

References