tva
← บทความเชิงลึก

จับคู่งานด้วย LLM: สร้างไปป์ไลน์ให้คะแนนความเหมาะสม

การกรองด้วยคีย์เวิร์ดรวดเร็วและต้นทุนต่ำ แต่ผิดพลาดในจุดที่สำคัญ เมื่อผู้สรรหาบุคลากรกำหนดโปรไฟล์ผู้สมัครในอุดมคติ ทั้งความเชี่ยวชาญเฉพาะด้าน ความรู้เชิงลึกด้านเทคโนโลยี รูปแบบการสื่อสาร และความต้องการตามบริบทอีกนับสิบข้อ การนับคำไม่อาจประเมินว่าประกาศงานเหมาะกับโปรไฟล์นั้นได้อย่างแม่นยำ ความต่างเชิงความหมายระหว่าง “ใช้ Python มากกว่า 5 ปี” กับ “เขียน Python สำหรับเวิร์กโฟลว์วิศวกรรมข้อมูลในสภาพแวดล้อมใช้งานจริงที่ใช้ async I/O อย่างหนัก” มองไม่เห็นด้วยตัวกรองคีย์เวิร์ด แต่เป็นสิ่งที่โมเดลภาษาประเมินได้เมื่อมีพรอมป์ต์ที่เหมาะสม

เราสร้างไปป์ไลน์จับคู่ที่ให้ LLM ให้คะแนนคิวประกาศงานเทียบกับโปรไฟล์ผู้สมัครที่กำหนดไว้ ประมวลผลพร้อมกัน ติดตามค่าโทเคนแบบเรียลไทม์ และมีปุ่มพักและทำต่อในแอป macOS แบบเนทีฟที่สร้างด้วย SwiftUI บทความนี้อธิบายโครงสร้างระบบและกรณีที่ประโยชน์ของการจับคู่ด้วย LLM คุ้มค่ากว่าแนวทางที่ง่ายกว่า


โครงสร้างพรอมป์ต์

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

โมเดลส่งกลับออบเจ็กต์ JSON แบบมีโครงสร้างสองฟิลด์ ได้แก่ คะแนนตัวเลขจาก 0 ถึง 100 และเหตุผลสั้น ๆ ที่อธิบายปัจจัยหลักที่ทำให้เหมาะหรือไม่เหมาะ การบังคับโครงสร้างผลลัพธ์สำคัญ เพราะคำตอบที่ไม่มีโครงสร้างต้องใช้การแยกข้อความที่เปราะบาง และมีรูปแบบแสดงคะแนนไม่สม่ำเสมอ การใช้ tool calling หรือ function calling พร้อมสคีมา JSON (ในโหมดเข้มงวดหรือโหมดผลลัพธ์แบบมีโครงสร้าง หากผู้ให้บริการมีให้) ช่วยขจัดปัญหาประเภทนี้ได้เป็นส่วนใหญ่และทำให้รวมผลในขั้นต่อไปได้ง่าย แต่ยังควรตรวจสอบความถูกต้องของออบเจ็กต์หลังแยกวิเคราะห์

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

ประมวลผลพร้อมกันด้วย ThreadPoolExecutor

เมื่อมีคิว 50 หรือ 100 ประกาศ การเรียก API ตามลำดับช้าเกินไปสำหรับการใช้งานแบบโต้ตอบ concurrent.futures.ThreadPoolExecutor ของ Python จัดการได้อย่างตรงไปตรงมา จำนวนตัวประมวลผลกำหนดตามขีดจำกัดอัตราการเรียกใช้ของผู้ให้บริการ API โดยทั่วไป 5 ถึง 10 คำขอพร้อมกันให้ปริมาณงานที่ดีโดยไม่เกิดข้อผิดพลาดจากโควตา

รูปแบบนี้ใช้ as_completed() เพื่อรับ futures ตามลำดับที่เสร็จ แทนลำดับที่ส่ง จึงแสดงผลจับคู่ที่เสร็จเร็วที่สุดก่อน และอัปเดตหน้าจอทีละรายการโดยไม่ต้องรอทั้งชุด:

from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=8) as executor:
    futures = {
        executor.submit(score_listing, listing, profile): listing
        for listing in listings
    }
    for future in as_completed(futures):
        listing = futures[future]
        score, rationale = future.result()
        update_results(listing, score, rationale)

การจัดการข้อผิดพลาดต้องใส่ใจเป็นพิเศษ คำขอ API แต่ละรายการอาจล้มเหลวจากขีดจำกัดอัตราการเรียกใช้ เครือข่ายขัดข้องชั่วคราว หรือการปฏิเสธตามนโยบายเนื้อหา ตัวประมวลผลแต่ละตัวควรจับข้อยกเว้นและส่งค่าบ่งชี้กลับ (ตัวอย่างโค้ดด้านบนละส่วนนี้ไว้เพื่อความกระชับ) มิฉะนั้น future.result() จะโยนข้อยกเว้นของตัวประมวลผลซ้ำในลูปผลลัพธ์และหยุดประมวลผลผลลัพธ์ที่เหลือ ประกาศที่ให้คะแนนไม่ได้เข้าคิวลองใหม่ แทนถูกทิ้งโดยไม่แจ้ง การเห็นความล้มเหลวอย่างชัดเจนเข้าใจได้ง่ายกว่ารายการคะแนนที่ขาดบางรายการโดยไม่มีคำอธิบาย

ติดตามโทเคนระหว่างทำงาน

ต้นทุนโทเคนเมื่อทำงานจำนวนมากมักถูกประเมินต่ำเกินไป พรอมป์ต์ 400 โทเคนที่ส่งให้ 100 ประกาศพร้อมคำตอบ 150 โทเคนสะสมต้นทุนได้รวดเร็วตามระดับโมเดล ไปป์ไลน์ติดตามยอดสะสมด้วยตัวนับที่ปลอดภัยสำหรับหลายเธรด เป็นจำนวนเต็มธรรมดาที่ป้องกันด้วย threading.Lock และเพิ่มค่าหลังได้รับคำตอบ API แต่ละครั้งจากจำนวนโทเคนในข้อมูลเมตาของคำตอบ

frontend ของ SwiftUI อ่านตัวนับนี้ตามเวลา แสดงโทเคนขาเข้าและขาออกสะสม พร้อมค่าใช้จ่ายประมาณตามราคาต่อโทเคนของโมเดลปัจจุบัน ผู้สรรหาจึงเห็นต้นทุนรอบการทำงานแบบสด และสังเกตได้เมื่อโปรไฟล์ทำให้เหตุผลยาวผิดปกติ ซึ่งมักบ่งชี้ว่าต้องปรับพรอมป์ต์ให้กระชับ หรือข้อความประกาศมีข้อมูลรบกวนมากกว่าที่คาด

การปรับปรุงที่ใช้ได้จริงคือประมาณจำนวนโทเคนก่อนส่ง ผู้ให้บริการรายใหญ่มีไลบรารีแบ่งโทเคนหรือจุดเชื่อมต่อสำหรับนับโทเคน เช่น ไลบรารี tiktoken ของ OpenAI หรือจุดเชื่อมต่อสำหรับนับโทเคนของ Anthropic การนับล่วงหน้าช่วยให้ไปป์ไลน์เตือนเมื่อข้อความประกาศยาวผิดปกติจนใช้บริบทมากเกินสัดส่วน หรือเมื่อพรอมป์ต์รวมใกล้ขีดจำกัดบริบทของโมเดล การตรวจพบก่อนเรียก API ช่วยเลี่ยงค่าใช้จ่ายสูญเปล่ากับคำขอที่จะถูกตัดหรือปฏิเสธ

พักและทำต่อ

ขณะตรวจผลระหว่างรอบการทำงาน ผู้สรรหาอาจต้องการพักคำขอให้คะแนนใหม่โดยไม่สูญเสียงานที่กำลังทำ กลไกพักและทำต่อใช้ threading.Event เป็นสัญญาณร่วม ก่อนเรียก API แต่ละครั้ง เธรดตัวประมวลผลตรวจสถานะสัญญาณ หากพักจะรอที่ event.wait() เมื่อเปิดสัญญาณอีกครั้งก็ทำงานต่อ

วิธีนี้เรียบร้อยกว่าการปิดและเริ่มเธรดใหม่ เพราะคำขอ API ที่ส่งไปแล้วทำจนเสร็จตามปกติ การพักหมายถึง “หยุดเริ่มคำขอใหม่” แทน “ยกเลิกทุกอย่างตอนนี้” ความต่างนี้สำคัญเมื่อต้องการให้ชุดปัจจุบันเสร็จก่อนผู้ใช้ตรวจผล เพราะชุดที่ยังไม่ครบทำความเข้าใจได้ยากกว่า

ควรจัดการการคงสถานะระหว่างพักและทำต่ออย่างชัดเจน ไปป์ไลน์เก็บไฟล์จุดตรวจเป็นรายการ JSON ของ ID ประกาศที่ให้คะแนนแล้ว เมื่อทำต่อจะข้ามประกาศในจุดตรวจ หากบันทึกคะแนนและเหตุผลไว้อย่างถาวรควบคู่กับจุดตรวจด้วย เมื่อแอปขัดข้องระหว่างทำงาน ก็ไม่ต้องให้คะแนนประกาศเดิมซ้ำเมื่อเปิดใหม่ การทำซ้ำโดยไม่เกิดผลซ้ำซ้อนในระดับรอบการทำงานใช้แรงพัฒนาเพียงเล็กน้อยและลดความยุ่งยากได้มาก

frontend ของแอป macOS ด้วย SwiftUI

เราเลือก SwiftUI เพราะเครื่องเป้าหมายเป็น macOS และต้องการประสบการณ์แบบเนทีฟที่ตอบสนองเร็ว มีภาระระบบต่ำ และจัดการหน้าต่าง macOS ได้เหมาะสม การใช้ Electron ครอบแอปก็ทำได้ แต่เพิ่มความซับซ้อนที่ไม่จำเป็นสำหรับเครื่องมือเพิ่มประสิทธิภาพผู้ใช้คนเดียว backend ของ Python รันเป็นกระบวนการในเครื่องและเปิด HTTP API ขนาดเล็กด้วย Flask แอป SwiftUI ติดต่อผ่าน localhost

รูปแบบ ObservableObject และ @Published ของ SwiftUI จัดการวงจรอัปเดตตามข้อมูลได้ตรงไปตรงมา แบบจำลองมุมมองอ่าน backend ตามเวลาและอัปเดตอาร์เรย์ที่เผยแพร่ให้รายการผลและตัวนับโทเคนใช้แสดงผล หน้าต่างหลักมีสามส่วน: รายการผลเรียงคะแนนจากมากไปน้อย ตัวนับโทเคนและค่าใช้จ่ายประมาณแบบสด และแถบควบคุมพร้อมปุ่มพัก/ทำต่อและหยุด

แต่ละแถวแสดงชื่องาน บริษัท ป้ายคะแนนแยกสี และเหตุผลหนึ่งบรรทัดจาก LLM เมื่อเลือกแถวจะเปิดหน้ารายละเอียดพร้อมเหตุผลทั้งหมด ปัจจัยสำคัญที่โมเดลระบุ และลิงก์ตรงไปยังประกาศต้นฉบับ ผู้สรรหาใช้เวลาตรวจส่วนนี้มากที่สุด คะแนนเป็นสัญญาณชี้ว่าควรตรวจอะไร ไม่ใช่คำตัดสินสุดท้าย

เมื่อใดการจับคู่ด้วย LLM ดีกว่าการกรองคีย์เวิร์ด

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

แต่ในความเป็นจริง เงื่อนไขที่น่าสนใจส่วนใหญ่ขึ้นกับบริบท “ต้องการคนที่เขียนโค้ดสะอาดและทดสอบดี” เป็นความต้องการที่ปรากฏในวิธีเขียนคำอธิบายงาน ไม่ใช่คีย์เวิร์ดเฉพาะ ส่วน “ไม่ต้องการงานที่ใช้แต่ระบบเก่าโดยไม่มีแผนปรับปรุง” ต้องอ่านและตีความเนื้อหา ตัวกรองคีย์เวิร์ดค้นสัญญาณนี้ไม่ได้ แต่ LLM ที่ได้รับโปรไฟล์ซึ่งระบุเงื่อนไขชัดเจนประเมินได้โดยตรง

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

เมื่อใดซับซ้อนเกินความจำเป็น

การให้คะแนนด้วย LLM เพิ่มต้นทุนและเวลารอจริง สำหรับการคัดกรองปริมาณมากที่ไม่ต้องตีความละเอียด เช่น หลายพันประกาศที่มีเงื่อนไขผ่านหรือไม่ผ่านตรงไปตรงมา ชุดตัวกรองแบบมีโครงสร้างเร็วและถูกกว่าหลายลำดับขนาด สถาปัตยกรรมที่เหมาะมักรวมสองแนวทาง ใช้ตัวกรองเร็วตัดรายการที่ไม่เหมาะอย่างชัดเจนก่อน ลดคิวให้เหลือจำนวนที่จัดการได้ แล้วใช้ LLM ให้คะแนนเฉพาะส่วนที่เหลือ หากตัวกรองเบื้องต้นตัดประกาศออก 80% ต้นทุนการให้คะแนนด้วย LLM จะลดลงในสัดส่วนใกล้เคียงกัน และคุณภาพการจับคู่จะยังคงเดิมตราบใดที่ตัวกรองตัดเฉพาะรายการที่ไม่เหมาะอย่างชัดเจน

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

บทความที่เกี่ยวข้อง

บทความที่เกี่ยวข้อง