Vector search จัดอันดับด้วยความคล้าย hybrid search จัดอันดับด้วยความคล้ายบวกการซ้อนทับของคีย์เวิร์ด แต่สิ่งที่คุณต้องการจริง ๆ จากการค้นหาโค้ดคือ ความเกี่ยวข้อง — และความคล้ายไม่ใช่ความเกี่ยวข้อง สนิปเป็ตที่มีโทเคนตรงกับคำค้นของคุณมากที่สุด บ่อยครั้งไม่ใช่โค้ดที่ตอบคำถามนั้น
ลองถามว่า "เราตัดสินใจตรงไหนว่าคำขอนี้ retry ได้" แล้วตัวจัดอันดับตามความคล้ายจะยินดียื่น struct คอนฟิกของ retry, ค่าคงที่ของ retry และเทสต์ที่มีคำนั้นอยู่สี่ครั้งให้คุณ ขณะที่ฟังก์ชันเดียวที่ตัดสินใจจริงนั่งอยู่อันดับเจ็ด โทเคนตรงกันหมด แต่ไม่มีอะไรตอบคำถามเลย
PageIndex เสนอประเด็นนี้ไว้สำหรับเอกสาร: แทนที่ top-k แบบความคล้ายด้วย LLM ที่ ให้เหตุผล ว่าส่วนไหนเกี่ยวข้อง เราอยากได้แบบเดียวกันสำหรับโค้ด ซึ่งโครงสร้างรวยกว่าสารบัญของ PDF มาก — มีสัญลักษณ์ มีกราฟการเรียก มีตัวโค้ดจริง ๆ
เราจึงสร้างมันขึ้นมา: ขั้นตอนการให้เหตุผลแบบเลือกเปิด ที่วางไว้หลัง hybrid retrieval อ่านโค้ดของผู้เข้าชิง แล้วจัดอันดับใหม่ตามว่ามันตอบคำค้นจริงหรือไม่ มันปิดอยู่โดยค่าเริ่มต้น เปิดด้วยแฟล็กในคอนฟิกอันเดียว และปล่อยให้ structural_search เป็น grep แบบกำหนดผลได้แน่นอนอย่างที่ควรเป็น บนเบนช์มาร์กของเรา นี่คือชัยชนะก้อนใหญ่ที่ดีขึ้นทุกด้าน โพสต์นี้คือบันทึกแบบตรงไปตรงมา — ตัวเลข วิธีที่เราจูน และสองอย่างที่ ไม่ ได้ผล
แนวคิดหลัก และทริกเดียวที่ทำให้มันใช้ได้
ไปป์ไลน์สั้นมาก คำค้นวิ่งผ่าน hybrid retrieval เหมือนเดิมทุกอย่าง — ความคล้ายเชิงเวกเตอร์บวกการจับคู่คีย์เวิร์ดบนดัชนีในเครื่อง ผู้เข้าชิงอันดับต้น ๆ กลับมาพร้อมตัวโค้ดเต็ม LLM อ่านมันแล้วจัดอันดับตามว่าตอบคำถามหรือไม่ จากนั้นอันดับสองชุดถูกหลอมรวมกันและคืนผลลัพธ์อันดับต้นออกไป ไม่มีดัชนีใหม่ ไม่มีการทำ embedding รอบสอง ไม่ต้อง reindex
เวอร์ชันตรงไปตรงมาของขั้นตอนตรงกลางนั้นชัดเจนอยู่แล้ว: เอาผู้เข้าชิงมา ถามว่า "อันไหนตอบคำค้นบ้าง เรียงมา" แล้วใช้ลำดับนั้น เราทำแบบนั้นก่อน และมันคือหายนะสำหรับ recall
นี่คือตัวจัดอันดับใหม่แบบใช้เหตุผลล้วนบนคำค้น 127 รายการ:
| เมตริก | hybrid อย่างเดียว | เหตุผลล้วน | Δ |
|---|---|---|---|
| MRR | 0.595 | 0.752 | +0.157 |
| NDCG@10 | 0.658 | 0.758 | +0.100 |
| Hit@10 | 0.913 | 0.843 | −0.071 |
| Recall@10 | 0.886 | 0.811 | −0.075 |
เมตริกด้านการจัดอันดับพุ่งขึ้น — คำตอบที่ถูกต้องถูกดึงขึ้นมาบนสุด แต่ Hit@10 กับ Recall@10 ตก เพราะโมเดลตัดทิ้ง มันคืนเฉพาะไม่กี่รายการที่มันตัดสินว่าเกี่ยวข้อง แล้วโยนที่เหลือทิ้งอย่างเงียบ ๆ รวมถึงผลที่ถูกต้องซึ่งนั่งอยู่อันดับ 6–10 ตัวจัดอันดับใหม่ที่เพิ่ม precision ด้วยการทิ้ง recall ไม่ใช่ตัวที่คุณจะปล่อยขึ้นโปรดักชัน มันดูเก่งกาจกับคำค้นที่มันทายถูก และทำให้คำค้นยาก ๆ กลายเป็นไม่มีคำตอบ
วิธีแก้เล็กมาก และมันคือเหตุผลทั้งหมดที่ทำให้เรื่องนี้เวิร์ก: อย่าให้ LLM มาแทนที่การจัดอันดับ — หลอมรวมมันกับอันดับจาก hybrid ด้วย Reciprocal Rank Fusion
score(candidate) = 1 / (k + hybrid_rank)
+ reasoning_weight * 1 / (k + reasoning_rank)
k คือค่าคงที่หน่วงมาตรฐานของ RRF ที่กันไม่ให้อันดับตำแหน่งใดตำแหน่งหนึ่งครอบงำผลรวม ประเด็นสำคัญคือแต่ละพจน์ทำอะไร อันดับจาก hybrid มีส่วนร่วมเสมอ มันจึงทำหน้าที่เป็นพื้นของ recall — ผลที่ถูกต้องซึ่ง LLM ลดอันดับหรือมองข้ามจะถูกดันลง ไม่ใช่ถูกลบ ส่วนอันดับจากการให้เหตุผลพร้อมน้ำหนักเป็นตัวกำหนดหัวของลิสต์ คุณได้ผลดีด้านการจัดอันดับไว้ และหยุดเลือด recall ไปพร้อมกัน
การเปลี่ยนแค่จุดเดียวนั้นเปลี่ยนการแลกได้แลกเสียของเหตุผลล้วนให้กลายเป็นชัยชนะสะอาด ๆ ในทุกเมตริก
ตัวเลข
เบนช์มาร์ก: คำค้น 127 รายการที่กำกับด้วยมือ ทดสอบกับโค้ดเบส Octocode ที่ตรึงไว้ที่คอมมิตหนึ่ง (มาตรฐาน 100 + คำค้นที่ตั้งใจทำให้ยาก 27 ซึ่งใช้ภาษาธรรมชาติโดยไม่มีคีย์เวิร์ดซ้อนทับเลย) ใช้ embedding ในเครื่องด้วย fastembed, เปิด hybrid search, คำค้นชุดเดียวกันทั้งสองฝั่ง และใช้ deepseek:deepseek-v4-flash เป็นโมเดลให้เหตุผล เกณฑ์ความถูกต้องคือการซ้อนทับของช่วงบรรทัดกับตำแหน่งในซอร์สที่ตรวจสอบแล้ว
| เมตริก | hybrid อย่างเดียว | + การให้เหตุผล | Δ |
|---|---|---|---|
| MRR | 0.595 | 0.809 | +0.214 (+36%) |
| NDCG@10 | 0.658 | 0.833 | +0.175 (+27%) |
| Hit@5 | 0.827 | 0.953 | +0.126 |
| Hit@10 | 0.913 | 0.969 | +0.055 |
| Recall@5 | 0.777 | 0.924 | +0.147 |
| Recall@10 | 0.886 | 0.944 | +0.058 |
ทุกเมตริกดีขึ้น สองตัวที่สำคัญที่สุดสำหรับโจทย์ "หาโค้ดที่ทำ X ให้หน่อย" — MRR (ผลลัพธ์ถูกตัวแรกอยู่สูงแค่ไหน) และ NDCG@10 (ผลที่เกี่ยวข้อง มากที่สุด ได้อยู่บนสุดหรือไม่) — ขยับขึ้น 36% และ 27% ส่วน Hit@5 ขึ้นจาก 0.83 เป็น 0.95: สิบเก้าในยี่สิบครั้ง คำตอบอยู่ในห้าอันดับแรกแล้ว
ตัวเลขสุดท้ายนี่แหละที่เปลี่ยนพฤติกรรมของเอเจนต์ เมื่อคำตอบอยู่ในห้าอันดับแรกอย่างเชื่อถือได้ เอเจนต์อ่านผลห้ารายการแล้วลงมือทำ เมื่อไม่เป็นแบบนั้น เอเจนต์จะขยายการค้นหา อ่านไฟล์เพิ่ม เผาคอนเท็กซ์ไปกับผู้เข้าชิงที่ไม่เคยเกี่ยวข้อง และบางครั้งก็ยอมแพ้แล้วหันไป grep คุณภาพการจัดอันดับที่ต้นทางคือคอนเท็กซ์ที่ประหยัดได้ที่ปลายทาง
เราจูนมันอย่างไร (และตรงไหนที่ "มากกว่า" คือแย่กว่า)
เราไม่ได้เดาค่าคอนฟิก เรากวาดค่าทีละมิติบนดัชนีชุดเดียวกัน
น้ำหนักการให้เหตุผล — การหลอมรวมจะเอนไปทาง LLM มากแค่ไหนเทียบกับพื้นของ hybrid กวาดที่ 1, 2, 3, 5 การจัดอันดับดีขึ้นตามน้ำหนักและนิ่งราว ๆ 2–3 ส่วน 5 ไม่ได้อะไรเพิ่มและเสีย recall ไปเล็กน้อย เพราะถึงจุดนั้นพื้นจาก hybrid แทบไม่มีความหมาย และคุณก็กลับไปเชื่อโมเดลตัวเดียวอีก บนเบนช์มาร์กเต็ม 2.0 คือค่าที่ดีที่สุดโดยรวม
จะให้เหตุผลกับผู้เข้าชิงกี่รายการ กวาดที่ 15, 25, 35, 40 มากกว่า ไม่ได้ ดีกว่า — 40 แย่กว่า 25 อย่างวัดได้ การป้อนผู้เข้าชิงเยอะขึ้นทำให้การตัดสินใจเจือจาง แถมเปลืองโทเคนกว่าเพื่อผลที่แย่กว่า 25 คือจุดที่พอดี
โมเดลเห็นเนื้อหาของผู้เข้าชิงมากแค่ไหน ข้อนี้ชี้ขาด เห็นแค่ลายเซ็น เห็นสนิปเป็ต หรือเห็นตัวโค้ดเต็ม:
| คอนเท็กซ์ | Hit@5 | MRR | Recall@5 |
|---|---|---|---|
| ลายเซ็น | 0.933 | 0.806 | 0.900 |
| สนิปเป็ต | 0.933 | 0.849 | 0.883 |
| ตัวโค้ดเต็ม | 1.000 | 0.857 | 0.967 |
(ชุดย่อย 30 คำค้น ตัวเลขสัมบูรณ์บนชุดย่อยจะสูงกว่า แต่ประเด็นอยู่ที่ลำดับ)
ตัวโค้ดเต็มชนะขาด โมเดลต้องเห็นโค้ดจริงถึงจะตัดสินความเกี่ยวข้องได้ — ชื่อกับลายเซ็นไม่พอ ตัวเลือก "ลายเซ็นอย่างเดียว" แย่ที่สุด และควรหยุดคิดตรงนี้สักหน่อย: มันเป็นตัวเลือกที่ถูกที่สุด เป็นตัวที่มือคุณจะคว้าเพื่อประหยัดโทเคน และมันโยนทิ้งข้อมูลที่ขั้นตอนการให้เหตุผลมีไว้เพื่อใช้พอดี
อุณหภูมิของ LLM เราเดาว่าค่าต่ำ (กำหนดผลได้แน่นอนกว่า) น่าจะจัดอันดับดีกว่า ผิด — อุณหภูมิ 1.0 ชนะทั้ง 0.3 และ 0.0 โมเดลให้เหตุผลได้ดีกว่าเมื่อสุ่มแบบปกติ
สิ่งที่ไม่ได้ผล: คำอธิบายเชิงบริบทตอนสร้างดัชนี
วิธีที่ชัดเจนในการดัน recall ขึ้นคือทำให้ดัชนีรวยขึ้น: ให้ LLM เขียนบรรทัดเดียวว่า "โค้ดนี้ทำอะไร" สำหรับทุกชิ้นส่วน แล้ว embed มันไปพร้อมกับโค้ด (contextual retrieval ของ Anthropic รายงานว่าการดึงข้อมูลที่ล้มเหลวลดลงมากด้วยวิธีนี้เป๊ะ ๆ) เรามีกลไกอยู่แล้ว จึงลองทดสอบ — reindex ใหม่ทั้งหมดพร้อมคำอธิบาย แล้ว A/B แบบเดิม
ผลออกมาแย่ลงเล็กน้อย:
| Hit@5 | MRR | NDCG@10 | Recall@10 | |
|---|---|---|---|---|
| ดัชนีธรรมดา (+การให้เหตุผล) | 0.953 | 0.809 | 0.833 | 0.944 |
| ดัชนีเชิงบริบท (+การให้เหตุผล) | 0.945 | 0.836 | 0.859 | 0.925 |
| Δ | −0.008 | +0.027 | +0.026 | −0.019 |
มันแลก recall ไปกับการจัดอันดับ: MRR กับ NDCG ขยับขึ้น แต่ Hit กับ Recall ขยับลง การเติมคำอธิบายจาก LLM ไว้ข้างหน้าทำให้โทเคนของโค้ดเองเจือจางลงใน embedding และบนโค้ดที่สะอาดและมีโครงสร้างดีพร้อมตัว embed โค้ดที่ใช้ได้ นั่นทำให้ recall ไหลลง ส่วน recall ที่ Anthropic วัดได้นั้นมาจากงานร้อยแก้วและคลังข้อมูลผสม — ไม่ใช่แบบนี้ แล้วคุณค่าด้านการจัดอันดับที่ดัชนีเชิงบริบทจะเพิ่มให้ล่ะ? ขั้นตอนการให้เหตุผลเก็บมันไปหมดแล้ว การซ้อนดัชนีเชิงบริบทลงไปอีกจึงมีค่าตั้งแต่ "ซ้ำซ้อน" ไปจนถึง "เป็นโทษ"
เราจึงไม่ปล่อยมันออกไป มันปิดไว้แบบนั้น และควรพูดต้นทุนของการตัดสินใจนี้ให้ชัด: ดัชนีเชิงบริบทหมายถึงการเรียก LLM หนึ่งครั้งต่อชิ้นส่วน ทุกครั้งที่ reindex เพื่อผลลัพธ์ที่วัดแล้วแย่กว่า ต้องพูดตรง ๆ เพราะ "เติม contextual retrieval สิ" ถูกทำตาม ๆ กันไปทั่วแบบลัทธิคาร์โก้ — มันไม่ใช่ชัยชนะสากล และบนโค้ดมันอาจทำให้คุณเสียของ
วิธีเปิดใช้งาน
การให้เหตุผลปิดอยู่โดยค่าเริ่มต้น (มันมีต้นทุนหนึ่งการเรียก LLM ต่อการค้นหา) เปิดได้ใน config.toml:
[search.reasoning]
enabled = true
model = "deepseek:deepseek-v4-flash" # provider:model ใดก็ได้
max_candidates = 25 # 25 ชนะ 40 — มากกว่านั้นทำให้เจือจาง
context_level = "full" # ตัวโค้ดเต็มชนะ; สนิปเป็ต/ลายเซ็นแย่กว่า
reasoning_weight = 2.0 # น้ำหนัก RRF เทียบกับพื้น recall ของ hybrid
final_top_k = 10
ทุกพารามิเตอร์ถูกนิยามไว้ในเทมเพลต — คอนฟิกแบบเข้มงวด ไม่มีค่าเริ่มต้นซ่อนอยู่ในโค้ด deepseek-v4-flash ถูกและเร็ว ผู้ให้บริการเจ้าไหนก็ใช้ได้ และตัวเลขข้างบนคือสิ่งที่โมเดลราคาถูกทำได้ ไม่ใช่โมเดลระดับแนวหน้า มันทำงานเฉพาะในเส้นทางการค้นหาเชิงความหมาย — structural_search ยังเป็น grep ล้วน ไม่มี AI อย่างที่ควรเป็น ถ้าคุณอยากได้การค้นหาที่กำหนดผลได้แน่นอนและต้นทุนศูนย์ ก็ปิดแฟล็กไว้ แล้วทุกอย่างในระบบเดิมของคุณจะไม่เปลี่ยนเลย
เพดานตามจริง
ตอนนี้เราอยู่ที่ Hit@5 0.953 การไปให้ถึง 1.0 บนเบนช์มาร์กนี้ไม่สมจริง — คำค้น 27 ข้อสุดท้ายตั้งใจทำมาให้โหด และเกณฑ์ความถูกต้องบางส่วนก็กำกวมจริง ๆ ช่องว่างที่เหลือไม่ใช่ recall ของกองผู้เข้าชิง (การทดสอบดัชนีเชิงบริบทยืนยันแล้วว่ากองผู้เข้าชิงครอบคลุมดีอยู่แล้ว) แต่เป็นคำค้นที่ยากจริง ๆ ตัว embedder พื้นฐานที่แรงกว่าในโปรดักชัน (โมเดล embedding ที่ทำมาเพื่อโค้ดโดยเฉพาะ แทน fastembed ในเครื่องที่เป็นค่าเริ่มต้น) จะยกพื้นขึ้นได้อีก แต่ตัววิธีการให้เหตุผลเองมาถึงจุดที่ดีที่สุดในทางปฏิบัติแล้ว
สิ่งที่เราชอบในผลลัพธ์นี้: มันคือการสังเคราะห์ของจริง hybrid search ให้ recall แบบถูกและกำหนดผลได้แน่นอน การให้เหตุผลให้ความเกี่ยวข้อง ในจุดที่คุณจ่ายค่า LLM อยู่แล้ว RRF หลอมทั้งสองเข้าด้วยกันเพื่อให้คุณไม่ต้องเลือก ไม่มีดัชนีใหม่ ไม่ต้องเขียนใหม่แบบไร้เวกเตอร์ ไม่ต้อง reindex — แฟล็กเดียว เรียก LLM หนึ่งครั้ง แล้วโค้ดที่ถูกต้องก็ขึ้นมาอยู่บนสุด
คำถามที่พบบ่อย
Reasoning retrieval คืออะไรในหนึ่งประโยค?
ขั้นตอนแบบเลือกเปิดหลัง hybrid retrieval ที่ LLM อ่านตัวโค้ดของผู้เข้าชิง จัดอันดับตามว่าตอบคำค้นหรือไม่ แล้วอันดับนั้นถูกหลอมรวมกับอันดับของ hybrid ด้วย Reciprocal Rank Fusion แทนที่จะเข้าไปแทนที่มัน
มันมาแทน vector หรือ hybrid search ไหม?
ไม่ และนั่นคือประเด็น การปล่อยให้ LLM ถือการจัดอันดับทำให้เราเสีย Recall@10 ไป 7 จุด อันดับจาก hybrid ยังอยู่ในคะแนนในฐานะพื้นของ recall ผลที่ถูกต้องซึ่งโมเดลมองข้ามจึงถูกลดอันดับ ไม่ใช่ถูกลบ
ต้นทุนเท่าไร?
เรียก LLM เพิ่มหนึ่งครั้งต่อการค้นหาเชิงความหมาย บนผู้เข้าชิงไม่เกิน 25 รายการ มันปิดอยู่โดยค่าเริ่มต้น และตัวเลขเบนช์มาร์กได้มาจาก deepseek:deepseek-v4-flash ซึ่งเป็นโมเดลถูกและเร็ว ใช้ provider:model ใดก็ได้ถ้าอยากจ่ายมากกว่านั้น
มันทำให้ structural_search ช้าลงหรือเปลี่ยนไปไหม?
ไม่ การให้เหตุผลทำงานเฉพาะในเส้นทางการค้นหาเชิงความหมาย structural_search ยังเป็น grep ที่กำหนดผลได้แน่นอน โดยไม่มี LLM อยู่ในวงจร
ควรเปิดคำอธิบายเชิงบริบทตอนสร้างดัชนีด้วยไหม?
สำหรับโค้ด ผลวัดของเราบอกว่าไม่: มันช่วย MRR กับ NDCG นิดหน่อย แต่เสีย Hit@5 กับ Recall@10 และผลด้านการจัดอันดับก็ถูกขั้นตอนการให้เหตุผลครอบไว้อยู่แล้ว แถมยังเพิ่มการเรียก LLM ต่อชิ้นส่วนในทุกครั้งที่ reindex
ต้อง reindex ก่อนใช้ไหม?
ไม่ต้อง การให้เหตุผลเป็นขั้นตอนตอนคิวรีที่วางทับดัชนีที่คุณมีอยู่แล้ว เปิดแฟล็ก รีสตาร์ต จบ
— Don
Octocode เป็นโอเพนซอร์สภายใต้ Apache-2.0 — มีทั้งเบนช์มาร์ก เกณฑ์ความถูกต้อง และผลดิบรวมอยู่ด้วย คุณจึงรันตัวเลขเหล่านี้ซ้ำบนโค้ดเบสของคุณเองได้ มันคือเอนจินค้นหาโค้ดที่อยู่เบื้องหลัง Octomind และทั้งสองคุยกันผ่านเซิร์ฟเวอร์ MCP


