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 ได้
𝐓𝐇𝐄 𝐏𝐀𝐓𝐓𝐄𝐑𝐍 (รูปแบบสถาปัตยกรรม)
มีส่วนประกอบสามส่วนที่ทำงานร่วมกัน:
- โปรเจกต์รันด้วยชื่อโหนด (
node name) และcookie(cookie) ที่กำหนดไว้ — เพื่อให้scriptช่วยของagentสามารถเชื่อมต่อได้ scriptdev_node.sh— อยู่ในโปรเจกต์เพื่อทำหน้าที่start,stop,status,rpcและeval_file- Skill definition — บอกให้
coding agentรู้ว่า เมื่อไหร่ และ อย่างไร ที่ควรจะใช้live introspection
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เป็นธรรมเนียมที่scriptdev_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 ระบบจะทำสิ่งนี้:
- เริ่ม โหนด BEAM ชั่วคราวที่ซ่อนอยู่ พร้อมชื่อสุ่ม (
rpc_<pid>) และใช้cookieเดียวกัน - เรียก
Node.connect/1เพื่อเชื่อมต่อกับแอปเป้าหมายผ่านdistributed Erlang - ใช้
:rpc.call/4เพื่อรันCode.eval_string/1บนโหนดเป้าหมาย — ทำให้codeรันในบริบทของแอปจริงๆ และเข้าถึงmoduleและสถานะทั้งหมดได้ - แสดงผลลัพธ์ผ่าน
IO.inspect/2และปิดตัวเองลง
นี่คือกลไกเดียวกับที่ iex --remsh ใช้ แต่ถูกห่อหุ้มไว้ใน interface ที่ AI agent สามารถเรียกใช้ได้โดยไม่ต้องใช้ interactive TTY
𝐒𝐄𝐂𝐔𝐑𝐈𝐓𝐘 𝐂𝐎𝐍𝐒𝐈𝐃𝐄𝐑𝐀𝐓𝐈𝐎𝐍𝐒 (ข้อควรระวังด้านความปลอดภัย)
-cookie devcookie เป็นที่รู้กันทั่วไป ใครก็ตามในเครื่องเดียวกันสามารถเชื่อมต่อได้ ใช้สำหรับการพัฒนาในเครื่องเท่านั้น
-Code.eval_string/1 สามารถรัน code อะไรก็ได้ (Arbitrary code execution) ซึ่งเป็นสิ่งที่ตั้งใจเพื่อให้ agent ทำงานได้เต็มที่ แต่ต้องระวังหากใช้ในสภาพแวดล้อมที่แชร์กับผู้อื่น
---sname จำกัดการเชื่อมต่อเฉพาะภายในเครื่องเดียวกัน การใช้ --name จะอนุญาตให้เชื่อมต่อข้ามเครื่องได้ (ไม่แนะนำหากไม่มี TLS)