💡 مثال کامل قابل اجرا در گیتهاب موجود است:
document-to-markdown-rag-chunks-java
مشکل میز ادعاها: چرا پردازش RAG در فایلهای واقعی شکست میخورد
یک خط لولهٔ پردازش RAG در محل، لایهای برای تجزیه اسناد است که فایلهای موجود تیم را به متن جستجوپذیر و قابل استناد تبدیل میکند بدون اینکه به شخص ثالثی ارسال شوند. عملیاتهای بیمه نشان میدهند چرا این مهم است. یک متصدی ادعا از دستیار داخلی میپرسد «متصدی دربارهٔ آسیب آب در این بیمهنامه چه گفته است؟»، و پاسخ در ایمیلی از مارس گذشته، داخل یک گزارش PDF پیوستشده، زیر سرعنوانی به نام «یافتهها» قرار دارد. دستیار تنها در صورتی میتواند آن را پیدا کند که پردازش پیوست را خوانده، سرعنوان را حفظ کرده و به یاد داشته باشد این گزارش با کدام ایمیل آمده است.
اکثر کارهای پردازش یکی از این سه مورد را اشتباه میکنند. یک استخراجکنندهٔ متن ساده فقط بدنهٔ ایمیل را برمیگرداند و پیوست را نادیده میگیرد. یک تقسیمکنندهٔ ثابت ۵۱۲ توکن گزارش را از وسط «یافتهها» قطع میکند. و یک تجزیهکنندهٔ میزبانیشده هر دو را حل میکند، اما فقط پس از اینکه فایل ادعا، با جزئیات شخصی و پزشکیاش، به سرورهای شخص دیگری بارگذاری شده و به ازای هر صفحه هزینهبرداری میشود.
آنچه تیم نیاز دارد دقیق و مشخص است: خواندن فایلهای Word، PDF، EML و ZIP داخل سرویس Java که قبلاً دادهها را در اختیار دارد، حفظ ساختار آنها به صورت Markdown، تقسیم بر اساس سرعنوانهای نویسنده، و افزودن منبع و والد به هر بخش.
نمای کلی بازار: راهنمای ۲۰۲۶ دربارهٔ «تجزیه اسناد برای RAG» که بررسی کردیم، پارسرهای متنباز Python را با APIهای ابری که به ازای هر صفحه هزینه میگیرند مقایسه میکند. یک کتابخانهٔ Java که روی سختافزار خودتان اجرا میشود، در آن گفتگو تقریباً غایب است.
چرا خطوط لولهٔ RAG به Markdown نیاز دارند نه به متن ساده؟
Markdown بخشهای سندی را که به بازیابیکننده میگویند یک ایده کجا تمام میشود و بعدی کجا شروع، حفظ میکند. سرعنوانها به خطوط # تبدیل میشوند، جدولها به همان ردیفها و ستونها میمانند و فهرستها آیتمهای خود را نگه میدارند. یک بخشبند میتواند بر پایهٔ این سرعنوانها تقسیم شود و یک LLM که بخش را میخواند همان طرح کلی را میبیند که یک انسان میبیند. متن ساده این طرح کلی را از بین میبرد و تنها اندازه را به عنوان سیگنال تقسیم باقی میگذارد.
چرا راهحلهای عمومی در اینجا کار نمیکنند
تیمها معمولاً پیش از ساخت یک لایهٔ پردازش مناسب، یکی از سه کار زیر را امتحان میکنند:
- استخراج متن ساده بههمراه یک تقسیمکنندهٔ توکن: راهاندازی سریع، اما بخشها بخشهای مختلف را مخلوط میکنند و سرعنوانی که میخواهید استناد کنید را از دست میدهند.
- یک API تجزیه ابری: ساختار خوب، اما فایلها شبکه را ترک میکنند و هزینه با هر صفحهای که دوباره فهرست میشود، رشد میکند.
- یک تجزیهکنندهٔ داخلی برای هر فرمت: یک کتابخانه برای DOCX، دیگری برای PDF، یک تجزیهکنندهٔ MIME برای ایمیل و
java.util.zipبرای آرشیوها، هر کدام با موارد خاص خود.
GroupDocs.Parser برای Java تمام چهار فرمت را پشت یک کلاس Parser پوشش میدهد و درونفرآیند اجرا میشود.
راهحل: GroupDocs.Parser در یک جریان کاری پردازش ادعاها
GroupDocs.Parser برای Java یک سند را میخواند و میتواند متن آن را به صورت Markdown از طریق getFormattedText برگرداند. ایمیلها و آرشیوها بهعنوان مخازن نمایان میشوند که آیتمهایشان بهعنوان نمونههای جدید Parser باز میشوند، بنابراین یک PDF پیوستشده به ایمیل دقیقاً مانند یک PDF روی دیسک پردازش میشود. پروژهٔ نمونه این کار را در شش متد ایستا میپیچد و نتیجه را به صورت JSON Lines مینویسد، که هر کار جاسازی یا فروشگاه برداری میتواند آن را بارگذاری کند. هیچ چارچوب RAG درگیر نیست؛ قالب خروجی نقطهٔ یکپارچهسازی است.
برای دنبال کردن مثال نیاز دارید به:
- JDK 11 یا بالاتر برای
pom.xmlارائهشده (کد خود به ویژگیهای Java 8 محدود میشود) - GroupDocs.Parser برای Java 26.10 (دریافت مجوز موقت)
- چندین فایل DOCX، PDF، EML یا ZIP
pom.xml پروژه مخزن https://releases.groupdocs.com/java/repo/ و وابستگی تک را اعلام میکند، بنابراین ساخت تنها با یک فرمان است:
mvn compile exec:java
پیادهسازی لایهٔ پردازش گام به گام
مراحل زیر یک فایل ادعا را از صندوق ورودی تا فهرست پیگیری میکند. هر گام به یک یا دو متد در RagChunksDemo.java مرتبط است. ما آنها را عمداً بهصورت متدهای ایستای ساده نگه داشتهایم تا بتوانید هر یک را بهراحتی در یک کارگر موجود بدون پذیرش بقیه استفاده کنید.
گام 1 - باز کردن هر فایل و مسیردهی آن
هر فایل بارگذاریشده یک Parser مخصوص به خود دریافت میکند. نام فایل تبدیل به شناسهٔ منبع میشود و یک فایل سطح‑بالا والد ندارد.
// 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 - درخواست Markdown، پذیرش متن ساده
مرحلهٔ استخراج بررسی میکند که فرمت چه قابلیتهایی دارد بهجای اینکه فهرستی ثابت از پسوندها را کدگذاری کند.
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به بخشبند میرسد، بنابراین میتواند نقطهٔ تقسیم و استنادی باشد.isFormattedText(): در اجرای نمونه DOCX و ایمیل به صورت Markdown برمیگردند در حالی که PDF به متن ساده باز میگردد و فیلدformatثبت میکند چه اتفاقی افتاده است.- نتیجهٔ
none: عکسهای ادعا داخل یک ZIP بدون OCR هیچ بخشی تولید نمیکند، که صادقانه و آسان برای نظارت است.
گام 3 - تقسیم بر اساس سرعنوانهای متصدی
بخشبند یک پشته از سرعنوان جاری در هر یک از شش سطح 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');
}
بخشهای بیش از ۱۵۰۰ کاراکتر در نقاط شکست پاراگراف برش میخورند، بنابراین یک بخش طولانی یافتهها به چند بخش تبدیل میشود که مسیر سرعنوان مشترکی دارند. در فایل Word بستهبندیشده این مسیرها بهصورت Heading 1 > Heading 1.2 > Heading 1.2.1 تولید میشوند.
گام 4 - پیگیری پیوستها و نوشتن JSON Lines
این گام مهمترین موردی است که میز ادعاها به آن اهمیت میدهد. getContainer() پیوستهای ایمیل و ورودیهای ZIP را فهرست میکند؛ هر آیتم بهعنوان سند فرزند باز میشود و شناسهٔ والد را به ارث میبرد.
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 مینویسد. با چهار ورودی نمونه اجرا ۲۲ خط تولید میکند. بخش از پیوست ایمیل نمونه email-with-attachment.eml/attachment.txt#0 است و parent آن email-with-attachment.eml است. این جفت به دستیار اجازه میدهد بگوید «از گزارشی که به ایمیل ۱۴ مارس پیوست شده بود» بهجای «از جایی در آرشیو».
جریان کاری واقعی: باز‑فهرستگذاری هفتگی ادعاها
تصور کنید تیم عملیات ادعاها هر جمعه صندوقهای ایمیل ادعاهای بستهشدهٔ هفته را بهصورت فایلهای EML و بستههای ZIP اسناد پشتیبان صادر میکند. یک کارگر Java این صادرات را به مخزن اسناد منتقل میکند. افزودن پردازش به معنای فراخوانی chunkDocument برای هر فایل و افزودن خروجی JSON Lines به صف جاسازی است.
گام 4 همان چیزی است که یک گزارش ذخیرهشده را به ایمیلی که آن را تحویل داده، پیوست نگه میدارد. گام 3 به این معناست که وقتی دستیار «یافتهها» را نقل میکند، استناد نام فایل و سرعنوان را میدهد نه فقط شمارهٔ بخش. قاعدهٔ کلی من برای این نوع فهرست این است: اگر بخشی نتواند به یک فایل و یک سرعنوان استناد شود، نباید در فهرست باشد.
تأثیر تجاری: چه تغییری ایجاد میکند
| جنبه | تنظیمات معمول قبل | با خط لولهٔ GroupDocs.Parser |
|---|---|---|
| محل تجزیهٔ فایلها | نقطهٔ انتهایی شخص ثالث | داخل JVM خودتان |
| مدل هزینهٔ تجزیه | به ازای هر صفحه، در هر باز‑فهرستگذاری | بدون هزینهٔ ابری به ازای صفحه |
| مرزهای بخش | پنجرههای ثابت توکنی | سرعنوانهای نویسنده، حداکثر ۱۵۰۰ کاراکتر |
| پیوستهای ایمیل | اغلب نادیده یا صافشده | بهعنوان اسناد فرزند تجزیه میشوند |
| دادههای استناد | در بهترین حالت نام فایل | source، parent و heading_path |
| وابستگیهای زمان اجرا | SDK بههمراه دسترسی به شبکه | یک بستهٔ Maven |
اثر ترکیبی یک فهرست است که تیم انطباق میتواند آن را تأیید کند و دستیار پاسخهایش به سند و بخش واقعی اشاره میکند.
سایر سناریوهای بیمهای که GroupDocs.Parser مناسب آنهاست
پس از وجود لایهٔ پردازش، همان کد برای کارهای دیگر با حجم اسناد بالا بهکار میرود:
- جستجوی متن سیاستنامه: اسناد Word سیاستنامه بر اساس سرعنوانهای بند تقسیم میشوند، بنابراین پاسخها دقیقاً بخش موردنظر را نقل میکنند.
- ارسالهای کارگزار: بارگذاریهای ZIP با PDFها و صفحات گستردهٔ ترکیبی بهصورت عضو بهعضو فهرست میشوند، هر کدام به ارسال مرتبط هستند.
- مدیریت شکایات: ایمیلهای مشتری و پیوستهایشان بهصورت یکجا فهرست میشوند، بهطوری که ایمیل والد هر بخش پیوست است.
شروع کار در محیط ادعاهای شما
نمونه را کلون کنید، آن را روی چند ایمیل صادرشدهٔ خود اجرا کنید و output/chunks.jsonl را باز کنید تا مسیرهای سرعنوان و شناسههای والد را پیش از اتصال به کار جاسازی بررسی کنید. نکات معماری مرحله‑به‑مرحله در مورد استفادهٔ مستندات برای این خط لوله آمده است و مرجع خروجی Markdown فهرست میکند که چه قالببندیهایی در این حالت حفظ میشوند.