ทีมที่ใช้หลายช่องทางสื่อสารพร้อมกันจะเจอปัญหาเดียวกันเมื่อเริ่มเก็บความรู้จากแชท คนคนเดียวมีหลายบัญชี ใน LINE ใช้ชื่อเล่นตามด้วยอีโมจิ ใน Discord ใช้ username ที่ตั้งไว้ตั้งแต่สมัยเรียน ส่วนในระบบจัดการงานใช้ชื่อจริงกับอีเมลบริษัท ถ้าระบบไม่รู้ว่าทั้งสามบัญชีเป็นคนเดียวกัน คำถามง่าย ๆ อย่าง "เรื่องนี้ใครเป็นคนตอบ" หรือ "งานนี้คุยกันที่ห้องไหน" ก็ตอบไม่ได้
บทความนี้เล่าวิธีที่เราใช้รวมตัวตนข้ามแพลตฟอร์มในระบบเก็บความรู้จากแชทภายใน แนวคิดหลักคือ ให้เครื่องจับคู่เฉพาะเคสที่หลักฐานชัด บันทึกหลักฐานไว้ทุกครั้ง และส่งเคสกำกวมให้คนตัดสิน ตัวอย่างชื่อ อีเมล และ id ในบทความเป็นค่าสมมติทั้งหมด
ทำไมจับคู่ด้วยชื่ออย่างเดียวไม่พอ
ความคิดแรกของหลายคนคือ เอาชื่อแสดงมาเทียบกัน ถ้าตรงกันก็ถือว่าเป็นคนเดียวกัน วิธีนี้พังเร็วกว่าที่คิด ด้วยเหตุผลเหล่านี้
- ชื่อแสดงเปลี่ยนได้ตลอด คนเปลี่ยนชื่อ LINE ตามเทศกาล ใส่อีโมจิ หรือเปลี่ยนชื่อเล่นใน Discord เฉพาะบางเซิร์ฟเวอร์ ชื่อที่เห็นวันนี้อาจไม่ใช่ชื่อเดียวกับเมื่อเดือนก่อน
- ชื่อเล่นภาษาไทยซ้ำกันบ่อยมาก ทีมขนาดหลักร้อยคนมีคนชื่อเล่นเดียวกันสองสามคนเป็นเรื่องปกติ ชื่อเล่นจึงเป็นหลักฐานที่อ่อนที่สุด
- คนเดียวกันสะกดต่างกัน ชื่อไทยกับชื่อโรมัน การถอดเสียงหลายแบบ หรือใช้ชื่อเล่นภาษาอังกฤษ ทำให้การเทียบตรงตัวพลาดคนที่ควรจับคู่ได้
- จับคู่ผิดแพงกว่าจับคู่ไม่ได้ ถ้าระบบเอาข้อความของคนหนึ่งไปผูกกับอีกคน คำตอบที่ได้จะผิดแบบมั่นใจ และคนที่ใช้งานจะเลิกเชื่อระบบ ส่วนบัญชีที่ยังจับคู่ไม่ได้แค่รอคนตรวจ
ข้อสุดท้ายเป็นหลักที่ใช้ตัดสินใจทุกจุดในระบบนี้ ถ้าไม่แน่ใจ ให้ส่งคนตรวจ อย่าเดา
ออกแบบตารางให้ผูกด้วย id ไม่ใช่ชื่อ
เราแยกข้อมูลเป็นสามส่วน คือคนจริง บัญชีบนแต่ละแพลตฟอร์ม และประวัติการตัดสินของคน
CREATE TABLE people (
id bigserial PRIMARY KEY,
real_name text NOT NULL,
nickname text,
email text UNIQUE -- เช่น somchai@example.com
);
CREATE TABLE person_identities (
id bigserial PRIMARY KEY,
person_id bigint REFERENCES people(id),
source text NOT NULL, -- 'line' | 'discord' | 'tasks'
source_user_id text NOT NULL, -- id ที่แพลตฟอร์มออกให้ ไม่เปลี่ยน
display_name text,
username text,
match_source text, -- ชนิดหลักฐานที่ใช้จับคู่
confidence numeric(5,4),
decision_status text NOT NULL, -- auto_accepted | needs_review | approved | rejected
evidence jsonb,
decided_by text,
decided_at timestamptz,
UNIQUE (source, source_user_id)
);
จุดสำคัญมีสามข้อ ข้อแรก ทุกบัญชีผูกด้วย source_user_id ที่แพลตฟอร์มออกให้ เช่น userId ของ LINE หรือ user id ตัวเลขของ Discord ชื่อแสดงเก็บไว้แค่ประกอบการอ่าน ข้อสอง UNIQUE (source, source_user_id) ทำให้บัญชีหนึ่งผูกกับคนได้แค่คนเดียว แม้งานจะรันซ้ำ ข้อสาม ตาราง people ใช้รายชื่อจากระบบงานและอีเมลบริษัทเป็นจุดยึด (anchor) เพราะเป็นแหล่งที่มีชื่อจริงและแก้โดยผู้ดูแล ไม่ใช่ชื่อที่ผู้ใช้ตั้งเอง
ให้คะแนนตามชนิดหลักฐาน
เราไม่ได้ใช้โมเดลซับซ้อน กฎไม่กี่ข้อที่เรียงตามความแข็งของหลักฐานครอบคลุมเคสส่วนใหญ่ได้ และอธิบายเหตุผลกับคนตรวจได้ทุกครั้ง ตัวอย่างลำดับที่เราใช้มีดังนี้
- ชื่อเต็มตรงกับชื่อจริงในระบบงาน เช่นชื่อเล่นใน Discord คือ "Somchai Bank" และในระบบงานมีคนชื่อ Somchai ชื่อเล่น Bank อยู่คนเดียว คะแนนสูงมาก ราว 0.98
- handle ตรงกับส่วนหน้าของอีเมลบริษัท เช่นชื่อแสดง "somchai.k" กับอีเมล
somchai.k@example.comคะแนนสูง ราว 0.96 - handle เดียวกันกับบัญชีที่ยืนยันแล้วบนอีกแพลตฟอร์ม เช่น Discord ยืนยันแล้วว่าเป็นคนที่ 42 และบัญชี LINE ใหม่ใช้ชื่อเดียวกับ handle นั้นพร้อมอีโมจิ คะแนนราว 0.94
- ชื่อเล่นตรงกันอย่างเดียว คะแนนต่ำ และไม่ยอมรับอัตโนมัติเลย ส่งคนตรวจเสมอ
ตัวเลขเหล่านี้ไม่ใช่ความน่าจะเป็นที่คำนวณมา เป็นลำดับความเชื่อถือที่ตกลงกันในทีม สิ่งที่สำคัญกว่าตัวเลขคือ match_source ที่บอกว่าใช้กฎข้อไหน เพราะเมื่อพบว่ากฎใดพลาดบ่อย เราค้นแล้วแก้ทั้งกลุ่มได้
เราไม่ใช้ LLM ตัดสินว่าใครเป็นใคร ถ้าจะใช้ ให้ช่วยสรุปหลักฐานหรือเสนอผู้สมัครให้คนดู แต่การยอมรับอัตโนมัติควรมาจากกฎที่ตรวจสอบได้
ตรวจความกำกวมก่อนยอมรับอัตโนมัติ
คะแนนสูงอย่างเดียวยังไม่พอ ก่อนยอมรับอัตโนมัติ เราตรวจเงื่อนไขอีกสามข้อ
- มีผู้สมัครที่ผ่านเกณฑ์เพียงคนเดียว ถ้าในระบบงานมีสองคนที่ชื่อเล่น Bank และชื่อเต็มของบัญชีนี้ไม่ได้ชี้ชัดว่าเป็นคนไหน ให้ส่งคนตรวจ
- คนนั้นยังไม่มีบัญชีอื่นบนแพลตฟอร์มเดียวกัน ถ้ามีอยู่แล้ว อาจเป็นบัญชีที่สอง หรืออาจเป็นคนละคน ให้คนดู
- บัญชีนี้ยังไม่เคยถูกคนปฏิเสธ (rejected) มาก่อน การตัดสินของคนต้องไม่ถูกงานอัตโนมัติเขียนทับ
เมื่อผ่านทั้งหมดจึงบันทึกเป็น auto_accepted ถ้าไม่ผ่านข้อใดข้อหนึ่งจะบันทึกเป็น needs_review พร้อมผู้สมัครทุกคนที่เป็นไปได้ ให้คนเลือกในหน้า review ภายใน
เก็บหลักฐานทุกครั้ง ให้คนตรวจย้อนได้
ทุกแถวมี evidence เป็น JSON ที่อธิบายว่าทำไมจับคู่ เช่น ใช้กฎข้อไหน ชื่อที่เทียบคืออะไร ตรวจแล้วว่าไม่มีคนอื่นชื่อซ้ำ พบในห้องใด กี่ข้อความ และมาจากรอบงานไหน หลักฐานแบบนี้ช่วยได้มากในสองกรณี กรณีแรก คนตรวจเปิดดูแล้วตัดสินได้ในไม่กี่วินาที โดยไม่ต้องไปค้นแชทเอง กรณีที่สอง ถ้าพบภายหลังว่าจับคู่ผิด เรารู้ทันทีว่ากฎไหนทำให้พลาด
{
"rule": "exact_full_name",
"compared": ["Somchai Bank", "Somchai (Bank)"],
"other_candidates": [],
"already_has_identity_on_source": false,
"rooms": ["project-alpha"],
"n_msgs": 2,
"run": "hourly-2026-11-15T10"
}
เก็บหลักฐานเท่าที่จำเป็นต่อการตัดสินใจ ไม่ต้องคัดลอกเนื้อหาข้อความลงไป ชื่อห้องและจำนวนข้อความก็พอให้คนตรวจไปเปิดดูต่อได้
งานรายชั่วโมง: ทำเฉพาะสิ่งที่ใหม่
เราไม่ได้จับคู่ใหม่ทั้งหมดทุกรอบ งานรายชั่วโมงดูเฉพาะข้อความหลังเวลาของรอบก่อน แล้วถามสามคำถาม มีห้องใหม่ไหม มีผู้ส่งใหม่ไหม และมีผู้ส่งเดิมที่เปลี่ยนชื่อแสดงไหม
-- ผู้ส่งที่ไม่เคยเห็นก่อนรอบนี้
SELECT platform, sender_id, max(sender_name) AS name
FROM chat_messages m
WHERE created_at > :since
AND NOT EXISTS (
SELECT 1 FROM chat_messages o
WHERE o.platform = m.platform
AND o.sender_id = m.sender_id
AND o.created_at <= :since)
GROUP BY 1, 2;
รอบส่วนใหญ่จะไม่มีอะไรใหม่ และนั่นคือผลลัพธ์ที่ดี งานจบเร็ว ไม่แจ้งเตือนใคร ไม่ต้องให้คนเปิดดู แจ้งเตือนเฉพาะเมื่อมีการยอมรับอัตโนมัติใหม่ หรือมีเคสเข้าคิวรอคนตรวจ การไม่ส่งข้อความว่า "ไม่มีอะไรใหม่" ทุกชั่วโมงทำให้คนยังสนใจตอนที่ระบบแจ้งจริง
การชี้ว่าห้องแชทไหนเป็นของโปรเจกต์ไหนในระบบงานก็ใช้รูปแบบเดียวกัน เสนอคู่พร้อมหลักฐานและคะแนน แล้วให้คนยืนยันคู่ที่ไม่ชัด
เขียนข้อมูลอย่างปลอดภัย
งานที่เขียนลงฐานข้อมูลทุกชั่วโมงต้องพลาดแล้วแก้ได้ เราใช้กติกาสั้น ๆ ดังนี้
- ทุกการเขียนอยู่ใน transaction และตั้ง
ON_ERROR_STOPเพื่อให้ล้มทั้งชุดเมื่อมีคำสั่งใดผิด - ก่อนแก้หลายแถว สร้างสำเนาตารางที่มีวันที่และรอบงานในชื่อ เพื่อย้อนกลับได้โดยไม่ต้องกู้ backup ทั้งฐาน และตั้งเวลาลบสำเนาเก่าออก
- insert แบบไม่ซ้ำ ด้วย
ON CONFLICT DO NOTHINGหรือWHERE NOT EXISTSถ้างานรันซ้ำเพราะ cron ทับกันหรือรันมือ ข้อมูลก็ไม่ซ้ำ - แหล่งข้อมูลถูกอ่านอย่างเดียว งานนี้ไม่เขียนกลับไปที่แชทหรือระบบงาน
\set ON_ERROR_STOP on
BEGIN;
CREATE TABLE _bak_person_identities_20261115_10 AS
SELECT * FROM person_identities;
INSERT INTO person_identities
(person_id, source, source_user_id, display_name,
match_source, confidence, decision_status, evidence,
decided_by, decided_at)
VALUES
(42, 'line', 'U0000000000000000000000000example', 'Bank 🎉',
'cross_source_handle', 0.9400, 'auto_accepted',
'{"rule":"cross_source_handle","other_candidates":[]}',
'hourly-job', now())
ON CONFLICT (source, source_user_id) DO NOTHING;
COMMIT;
ข้อมูลตัวตนเป็นข้อมูลส่วนบุคคล
ตารางที่บอกว่าบัญชีแชทไหนเป็นของใคร คือข้อมูลส่วนบุคคลตาม พ.ร.บ. คุ้มครองข้อมูลส่วนบุคคล ต้องดูแลเหมือนข้อมูลบุคคลอื่น
- ใช้เพื่อวัตถุประสงค์ที่แจ้งทีมไว้ เช่น ค้นความรู้และเชื่อมงาน ไม่ควรนำไปวัดผลงานรายบุคคลจากจำนวนข้อความ
- เก็บในฐานข้อมูลภายในที่ไม่เปิดออกอินเทอร์เน็ต หน้า review เปิดเฉพาะเครือข่ายภายในและเฉพาะคนที่มีหน้าที่
- ไม่ commit ข้อมูลตัวตน ไฟล์ dump แชท หรือผลการจับคู่ลง Git ใน repo เก็บแค่ schema กับโค้ด และสแกนความลับก่อนทุก commit
- ถ้ามีคนขอให้ลบหรือออกจากทีม ต้องรู้ว่าจะลบหรือปิดตัวตนเขาได้จากตารางไหนบ้าง
ถ้าจะส่งข้อความให้ LLM ช่วยสรุป ให้ใช้โมเดลที่ host เองภายในองค์กร หลักคิดเดียวกับที่เราเล่าไว้ใน ให้ LLM ที่ host เองทำงานกะดึกแบบ shadow mode และ รัน OCR ภาษาไทยบนเครื่อง Apple Silicon แบบ local คือข้อมูลภายในไม่ควรต้องออกไปข้างนอกเพื่อให้ระบบทำงานได้
กับดักที่เจอ
- ผูกด้วยชื่อแสดง แล้วข้อมูลหลุดเมื่อคนเปลี่ยนชื่อช่วงวันเกิดหรือเทศกาล
- ยอมรับอัตโนมัติจากชื่อเล่นอย่างเดียว ทั้งที่ทีมมีชื่อเล่นซ้ำหลายคน
- ให้งานอัตโนมัติเขียนทับการตัดสินของคน แล้วคนต้องปฏิเสธเคสเดิมซ้ำทุกวัน
- ไม่เก็บหลักฐาน คนตรวจต้องไปไล่แชทเองทุกเคส คิวจึงค้าง
- ไม่มี unique constraint งานที่รันซ้ำจึงสร้างบัญชีซ้ำ
- แจ้งเตือนทุกรอบแม้ไม่มีอะไรใหม่ จนคนเลิกอ่าน
- เก็บผลการจับคู่ในไฟล์ CSV บนเครื่องส่วนตัว หรือ commit ลง Git
สรุป
การรวมตัวตนข้าม LINE, Discord และระบบงาน ไม่ต้องใช้ระบบซับซ้อน ผูกทุกบัญชีด้วย id ที่แพลตฟอร์มออกให้ ยึดรายชื่อคนจริงจากระบบงานกับอีเมลบริษัท ให้คะแนนตามชนิดหลักฐาน ตรวจความกำกวมก่อนยอมรับอัตโนมัติ เก็บหลักฐานทุกครั้ง และให้คนตัดสินเคสที่ไม่ชัด โดยงานอัตโนมัติไม่เขียนทับการตัดสินนั้น
เมื่อตัวตนถูกต้อง ระบบค้นความรู้จากแชทจะตอบได้ว่าใครพูดอะไร ในห้องของโปรเจกต์ไหน และคนในทีมจะเชื่อคำตอบนั้น ถ้ายังไม่แน่ใจว่าบัญชีไหนเป็นของใคร ปล่อยให้รอคนตรวจดีกว่าเดา
