💡 ตัวอย่างการทำงานเต็มที่พร้อมใช้งานบน GitHub:
document-to-markdown-rag-chunks-java

ปัญหาโต๊ะรับเคลม: ทำไมการรับข้อมูล RAG ถึงล้มเหลวกับไฟล์จริง

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

งานรับข้อมูลส่วนใหญ่ทำผิดอย่างใดอย่างหนึ่งในสามอย่างนี้ ตัวสกัดข้อความแบบ plain‑text จะคืนเนื้อหาอีเมลเท่านั้นและละเลยไฟล์แนบ ตัวแบ่งโทเคนขนาดคงที่ 512 โทเคนจะตัดรายงานกลางหัวข้อ “Findings” และตัวแยกวิเคราะห์แบบโฮสต์จะแก้ปัญหาทั้งสองได้ แต่ต้องอัปโหลดไฟล์เคลมที่มีข้อมูลส่วนบุคคลและข้อมูลทางการแพทย์ไปยังเซิร์ฟเวอร์ของคนอื่นและคิดค่าบริการต่อหน้า

สิ่งที่ทีมต้องการคือสิ่งแคบและชัดเจน: อ่านไฟล์ Word, PDF, EML และ ZIP ภายในบริการ Java ที่เป็นเจ้าของข้อมูลอยู่แล้ว เก็บโครงสร้างเป็น Markdown แบ่งตามหัวข้อของผู้เขียน และแนบแหล่งที่มาและพาเรนต์ให้กับแต่ละชิ้นข้อมูล

ภาพรวมตลาด: คู่มือ 2026 เรื่อง “การแยกเอกสารสำหรับ RAG” ที่เรารีวิวเปรียบเทียบตัวแยก Python แบบโอเพ่นซอร์สกับ API คลาวด์ที่คิดค่าบริการต่อหน้า ไลบรารี Java ที่ทำงานบนฮาร์ดแวร์ของคุณเองยังค่อนข้างหายากในบทสนทนานั้น

ทำไม Pipeline ของ RAG ต้องการ Markdown แทน Plain Text?

Markdown รักษาส่วนของเอกสารที่บอกให้ตัวดึงข้อมูลรู้ว่าความคิดหนึ่งจบที่ไหนและความคิดต่อไปเริ่มที่ไหน หัวข้อจะกลายเป็นบรรทัดที่เริ่มด้วย # ตารางยังคงเป็นแถวและคอลัมน์ และรายการยังคงรายการของมัน ตัวแบ่งชิ้นข้อมูล (chunker) สามารถแบ่งตามหัวข้อเหล่านั้นได้ และ LLM ที่อ่านชิ้นข้อมูลจะเห็นโครงร่างเดียวกับที่คนอ่านเห็น Plain text จะทิ้งโครงร่างนั้นไป ทำให้ขนาดเป็นสัญญาณเดียวที่ใช้ในการแบ่ง

ทำไมโซลูชันทั่วไปถึงใช้ไม่ได้ที่นี่

ทีมมักลองทำอย่างใดอย่างหนึ่งในสามก่อนจะสร้างชั้นการรับข้อมูลที่เหมาะสม:

  • การสกัดข้อความแบบ plain‑text พร้อมตัวแบ่งโทเคน: ตั้งค่าเร็ว แต่ชิ้นข้อมูลจะผสมส่วนและสูญเสียหัวข้อที่คุณต้องการอ้างอิง
  • API การแยกข้อมูลบนคลาวด์: โครงสร้างดี แต่ไฟล์ต้องออกจากเครือข่ายและค่าใช้จ่ายจะเพิ่มขึ้นตามจำนวนหน้าที่ทำดัชนีใหม่ทุกครั้ง
  • ตัวแยกที่พัฒนาขึ้นเองตามฟอร์แมต: ไลบรารีหนึ่งสำหรับ DOCX อีกอันสำหรับ PDF ตัวแยก MIME สำหรับอีเมลและ java.util.zip สำหรับไฟล์บีบอัด แต่ละอันมีกรณีขอบที่แตกต่างกัน

GroupDocs.Parser for Java ครอบคลุมสี่ฟอร์แมตทั้งหมดภายใต้คลาส Parser เพียงคลาสเดียวและทำงานในกระบวนการเดียว

โซลูชัน: GroupDocs.Parser ใน Workflow การรับข้อมูลเคลม

GroupDocs.Parser for Java อ่านเอกสารและสามารถคืนข้อความเป็น Markdown ผ่าน getFormattedText อีเมลและไฟล์บีบอัดจะถูกเปิดเป็นคอนเทนเนอร์ที่รายการของมันเปิดเป็นอินสแตนซ์ Parser ใหม่ ดังนั้น PDF ที่แนบมากับอีเมลจะถูกประมวลผลเช่นเดียวกับ PDF ที่อยู่บนดิสก์ ตัวอย่างโปรเจกต์ห่อหุ้มฟังก์ชันสถิติเหล่านี้ไว้ในหกเมธอดแบบ static และเขียนผลลัพธ์เป็น JSON Lines ซึ่งงานฝังเวกเตอร์หรือ vector store ใดก็สามารถโหลดได้ ไม่ได้ใช้เฟรมเวิร์ก RAG ใด ๆ; รูปแบบผลลัพธ์คือจุดเชื่อมต่อ

เพื่อทำตามคุณต้องมี:

  • JDK 11 หรือใหม่กว่า สำหรับ pom.xml ที่ให้มา (โค้ดเองใช้คุณสมบัติของ Java 8)
  • GroupDocs.Parser for Java 26.10 (รับไลเซนส์ชั่วคราว)
  • ไฟล์ DOCX, PDF, EML หรือ ZIP จำนวนไม่กี่ไฟล์

pom.xml ของโปรเจกต์ประกาศรีโพสิตอรี https://releases.groupdocs.com/java/repo/ และการพึ่งพาเดียว ดังนั้นการสร้างจึงทำได้ด้วยคำสั่งเดียว:

mvn compile exec:java

การทำชั้นรับข้อมูลขั้นตอนต่อขั้นตอน

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

ขั้นตอน 1 - เปิดไฟล์แต่ละไฟล์และกำหนดเส้นทาง

ไฟล์ที่อัปโหลดแต่ละไฟล์จะได้รับ Parser ของตนเอง ชื่อไฟล์จะกลายเป็น source id และไฟล์ระดับบนสุดไม่มีพาเรนต์

// One call per uploaded claim document
String sourceId = new File(path).getName();
try (Parser parser = new Parser(path)) {
    return chunkParser(parser, sourceId, null, 0);  // null parent: top-level file
} catch (UnsupportedDocumentFormatException ex) {
    // An unknown format is logged and skipped, the batch keeps going
    System.out.println("skipped (unsupported format): " + sourceId);
    return new ArrayList<>();
}

chunkParser จะสกัดเนื้อหา, แบ่งชิ้นข้อมูล, และลงลึกไปยังไฟล์แนบขณะที่ระดับความลึกต่ำกว่า 2

ขั้นตอน 2 - ขอ Markdown, ยอมรับ Plain Text

ขั้นตอนสกัดข้อมูลจะตรวจสอบว่าฟอร์แมตรองรับอะไรบ้างแทนการกำหนดรายการส่วนขยายแบบคงที่

if (parser.getFeatures().isFormattedText()) {
    // Headings, lists and tables survive as Markdown
    FormattedTextOptions options =
            new FormattedTextOptions(FormattedTextMode.Markdown);
    try (TextReader reader = parser.getFormattedText(options)) {
        if (reader != null) {
            return new ExtractedText("markdown", reader.readToEnd());
        }
    } catch (IOException ex) {
        throw new RuntimeException(ex);
    }
}
try (TextReader reader = parser.getText()) {
    if (reader != null) {  // fallback for formats without formatted text
        return new ExtractedText("plain", reader.readToEnd());
    }
} catch (IOException ex) {
    throw new RuntimeException(ex);
}
return new ExtractedText("none", "");  // e.g. a photo with no OCR

เหตุผลที่ตั้งค่านี้สำคัญสำหรับข้อมูลเคลม:

  • FormattedTextMode.Markdown: หัวข้อ “Findings” ของผู้ประเมินจะปรากฏต่อ chunker เป็น ## Findings ทำให้เป็นจุดแบ่งและอ้างอิงได้
  • isFormattedText(): ในการรันตัวอย่าง DOCX และอีเมลจะคืนเป็น Markdown ส่วน PDF จะถอยกลับเป็น plain text และฟิลด์ format จะบันทึกว่ามีอะไรเกิดขึ้น
  • ผลลัพธ์ none: รูปภาพเคลมใน ZIP จะไม่สร้างชิ้นข้อมูลหากไม่มี OCR ซึ่งเป็นการบ่งชี้ที่ตรงไปตรงมาและง่ายต่อการตรวจสอบ

ขั้นตอน 3 - แบ่งตามหัวข้อของผู้ประเมิน

chunker จะเก็บสแตกของหัวข้อปัจจุบันที่แต่ละระดับของ Markdown ทั้งหกระดับ หัวข้อใหม่จะปิดส่วนที่กำลังทำและรีเซ็ตทุกอย่างที่อยู่ต่ำกว่าระดับนั้น

for (String line : markdown.split("\\r?\\n", -1)) {
    Matcher heading = HEADING.matcher(line);   // "#" to "######" + title
    if (heading.matches()) {
        // Close the previous section under the old path
        addSection(chunks, section.toString(), sourceId, parentId, path, format);
        section.setLength(0);
        int level = heading.group(1).length();
        stack[level - 1] = heading.group(2).trim();
        for (int i = level; i < stack.length; i++) {
            stack[i] = null;                     // forget deeper headings
        }
        path = joinPath(stack);                  // "Heading 1 > Heading 1.2"
    }
    section.append(line).append('\n');
}

ส่วนที่ยาวเกิน 1 500 ตัวอักษรจะถูกตัดที่จุดพักย่อหน้า ดังนั้นส่วน “Findings” ที่ยาวจะกลายเป็นหลายชิ้นข้อมูลที่แชร์เส้นทางหัวข้อเดียวกัน ในไฟล์ Word ที่รวมมาด้วยจะได้เส้นทางเช่น Heading 1 > Heading 1.2 > Heading 1.2.1

ขั้นตอน 4 - ติดตามไฟล์แนบและเขียน JSON Lines

นี่คือขั้นตอนที่ทีมเคลมให้ความสำคัญที่สุด getContainer() จะรายการไฟล์แนบของอีเมลและรายการใน ZIP; แต่ละรายการเปิดเป็นเอกสารลูกและสืบทอด id ของพาเรนต์

Iterable<ContainerItem> items = parser.getContainer();
if (items == null) {
    return chunks;                    // not an email or archive
}
for (ContainerItem item : items) {
    // "claim-0412.eml/report.pdf" style ids keep the lineage readable
    String childId = sourceId + "/" + item.getFilePath();
    try (Parser child = item.openParser()) {
        chunks.addAll(chunkParser(child, childId, sourceId, depth + 1));
    } catch (UnsupportedDocumentFormatException ex) {
        System.out.println("skipped (unsupported format): " + childId);
    }
}

writeJsonl จะเขียนบรรทัด UTF‑8 หนึ่งบรรทัดต่อชิ้นข้อมูลพร้อม id, source, parent, heading_path, format และ text ด้วยอินพุตตัวอย่างสี่ไฟล์ การรันจะสร้าง 22 บรรทัด ชิ้นข้อมูลจากไฟล์แนบของอีเมลตัวอย่างคือ email-with-attachment.eml/attachment.txt#0 และ parent ของมันคือ email-with-attachment.eml คู่นี้ทำให้ผู้ช่วยสามารถพูดว่า “จากรายงานที่แนบมากับอีเมลวันที่ 14 มีนาคม” แทน “จากที่ไหนสักแห่งในไฟล์บีบอัด”

Workflow จริง: การทำดัชนีเคลมประจำสัปดาห์

ลองนึกภาพทีมปฏิบัติการเคลมที่ส่งออกกล่องเมลเคลมที่ปิดแล้วของสัปดาห์ทุกวันศุกร์เป็นไฟล์ EML และบันเดิล ZIP ของเอกสารสนับสนุน Java worker มีอยู่แล้วที่ย้ายไฟล์ส่งออกเหล่านั้นไปยัง document store การเพิ่มขั้นตอนรับข้อมูลหมายถึงการเรียก chunkDocument สำหรับแต่ละไฟล์และต่อผลลัพธ์ JSON Lines ไปยังคิวการฝังเวกเตอร์

ขั้นตอน 4 ทำให้รายงานสำรองที่แนบมากับอีเมลยังคงเชื่อมโยงกับอีเมลที่ส่งมา ขั้นตอน 3 ทำให้เมื่อผู้ช่วยอ้างอิง “Findings” การอ้างอิงจะระบุไฟล์และหัวข้อแทนหมายเลขชิ้นข้อมูลเท่านั้น หลักการของฉันสำหรับดัชนีประเภทนี้คือ หากชิ้นข้อมูลไม่สามารถอ้างอิงกลับไปยังไฟล์และหัวข้อได้ ควรลบออกจากดัชนีเลย

ผลกระทบต่อธุรกิจ: สิ่งที่เปลี่ยนแปลงไป

ด้าน การตั้งค่าปกติก่อนหน้า หลังใช้ pipeline ของ GroupDocs.Parser
ที่ที่ไฟล์ถูกแยก จุดสิ้นสุดของบุคคลที่สาม ภายใน JVM ของคุณเอง
โมเดลค่าใช้จ่ายการแยก ต่อหน้า, ทุกครั้งที่ทำดัชนีใหม่ ไม่มีค่าใช้จ่ายต่อหน้าในคลาวด์
ขอบเขตชิ้นข้อมูล หน้าต่างโทเคนคงที่ หัวข้อของผู้เขียน, จำกัด 1 500 ตัวอักษร
ไฟล์แนบของอีเมล มักถูกข้ามหรือแบน แยกเป็นเอกสารลูก
ข้อมูลการอ้างอิง ชื่อไฟล์เท่านั้น source, parent และ heading_path
การพึ่งพารันไทม์ SDK + การเข้าถึงเครือข่าย เพียง Maven artifact หนึ่ง

ผลรวมคือ ดัชนีที่ทีมคอมพลายอาจเซ็นรับรองได้และผู้ช่วยที่ตอบกลับพร้อมชี้ไปยังเอกสารและส่วนที่เป็นจริง

สถานการณ์ประกันอื่น ๆ ที่ GroupDocs.Parser เหมาะสม

เมื่อชั้นรับข้อมูลพร้อมใช้งาน โค้ดเดียวกันสามารถให้บริการงานที่ต้องจัดการเอกสารจำนวนมากอื่น ๆ ได้:

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

เริ่มต้นในสภาพแวดล้อมเคลมของคุณ

คัดลอกตัวอย่าง, รันกับอีเมลที่คุณส่งออกเองหลายไฟล์, แล้วเปิด output/chunks.jsonl เพื่อตรวจสอบเส้นทางหัวข้อและ parent id ก่อนนำเข้าไปยังงานฝังเวกเตอร์ สถาปัตยกรรมแบบขั้นตอนต่อขั้นตอนมีรายละเอียดใน documentation use case for this pipeline และอ้างอิงการแสดงผล Markdown มีใน Markdown output reference

แหล่งข้อมูล