ทีมที่ใช้หลายช่องทางสื่อสารพร้อมกันจะเจอปัญหาเดียวกันเมื่อเริ่มเก็บความรู้จากแชท คนคนเดียวมีหลายบัญชี ใน LINE ใช้ชื่อเล่นตามด้วยอีโมจิ ใน Discord ใช้ username ที่ตั้งไว้ตั้งแต่สมัยเรียน ส่วนในระบบจัดการงานใช้ชื่อจริงกับอีเมลบริษัท ถ้าระบบไม่รู้ว่าทั้งสามบัญชีเป็นคนเดียวกัน คำถามง่าย ๆ อย่าง "เรื่องนี้ใครเป็นคนตอบ" หรือ "งานนี้คุยกันที่ห้องไหน" ก็ตอบไม่ได้

บทความนี้เล่าวิธีที่เราใช้รวมตัวตนข้ามแพลตฟอร์มในระบบเก็บความรู้จากแชทภายใน แนวคิดหลักคือ ให้เครื่องจับคู่เฉพาะเคสที่หลักฐานชัด บันทึกหลักฐานไว้ทุกครั้ง และส่งเคสกำกวมให้คนตัดสิน ตัวอย่างชื่อ อีเมล และ id ในบทความเป็นค่าสมมติทั้งหมด

แผนภาพการจับคู่ตัวตนข้ามแพลตฟอร์ม: งานรายชั่วโมงอ่านข้อความใหม่จาก LINE และ Discord แบบอ่านอย่างเดียว หาผู้ส่งใหม่ สร้างผู้สมัครจับคู่กับรายชื่อคนจากระบบงานและอีเมลบริษัท ให้คะแนนตามหลักฐาน ตรวจความกำกวม แล้วแยกเป็นยอมรับอัตโนมัติหรือคิวให้คนตรวจ โดยการตัดสินของคนไม่ถูกเขียนทับ
ภาพรวม: แหล่งข้อมูลถูกอ่านอย่างเดียว งานรายชั่วโมงจับคู่ด้วยหลักฐานและคะแนน แล้วแยกเคสชัดออกจากเคสกำกวม (คลิกที่ภาพเพื่อดูขนาดเต็ม)

ทำไมจับคู่ด้วยชื่ออย่างเดียวไม่พอ

ความคิดแรกของหลายคนคือ เอาชื่อแสดงมาเทียบกัน ถ้าตรงกันก็ถือว่าเป็นคนเดียวกัน วิธีนี้พังเร็วกว่าที่คิด ด้วยเหตุผลเหล่านี้

  • ชื่อแสดงเปลี่ยนได้ตลอด คนเปลี่ยนชื่อ 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 ตัดสินว่าใครเป็นใคร ถ้าจะใช้ ให้ช่วยสรุปหลักฐานหรือเสนอผู้สมัครให้คนดู แต่การยอมรับอัตโนมัติควรมาจากกฎที่ตรวจสอบได้

ตรวจความกำกวมก่อนยอมรับอัตโนมัติ

คะแนนสูงอย่างเดียวยังไม่พอ ก่อนยอมรับอัตโนมัติ เราตรวจเงื่อนไขอีกสามข้อ

  1. มีผู้สมัครที่ผ่านเกณฑ์เพียงคนเดียว ถ้าในระบบงานมีสองคนที่ชื่อเล่น Bank และชื่อเต็มของบัญชีนี้ไม่ได้ชี้ชัดว่าเป็นคนไหน ให้ส่งคนตรวจ
  2. คนนั้นยังไม่มีบัญชีอื่นบนแพลตฟอร์มเดียวกัน ถ้ามีอยู่แล้ว อาจเป็นบัญชีที่สอง หรืออาจเป็นคนละคน ให้คนดู
  3. บัญชีนี้ยังไม่เคยถูกคนปฏิเสธ (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 ที่แพลตฟอร์มออกให้ ยึดรายชื่อคนจริงจากระบบงานกับอีเมลบริษัท ให้คะแนนตามชนิดหลักฐาน ตรวจความกำกวมก่อนยอมรับอัตโนมัติ เก็บหลักฐานทุกครั้ง และให้คนตัดสินเคสที่ไม่ชัด โดยงานอัตโนมัติไม่เขียนทับการตัดสินนั้น

เมื่อตัวตนถูกต้อง ระบบค้นความรู้จากแชทจะตอบได้ว่าใครพูดอะไร ในห้องของโปรเจกต์ไหน และคนในทีมจะเชื่อคำตอบนั้น ถ้ายังไม่แน่ใจว่าบัญชีไหนเป็นของใคร ปล่อยให้รอคนตรวจดีกว่าเดา