Ачыстка даных для прадакшн-RAG: кадоўка, OCR, дэдуплікацыя і PII
Як ператварыць распарсеныя карпаратыўныя дакументы ў тэкст, гатовы да пошуку: выпраўленне кадоўкі, выдаленне шаблоннага тэксту і дублікатаў, пазнакі рызыкоўнага OCR, апрацоўка моў і маскіраванне PII.
Крыніцы: Data Cleaning and Preprocessing for Production RAG: Building Retrieval-Ready Documents
Уявіце банк, які выкарыстоўвае асістэнта з ведамасцяў для аналізу 200 000 внутрашняях і рэгулятарных дакументаў. Працэс прыемкі дакументаў праходзіць гладка: PDF-файлы аналізуюцца, сканаваныя цыркуляры з праблемамі проты пераўодзбандытва праходзяць праз алгорытм OCR, файлы з SharePoint прыбываюць за планам, а табелі выдэляюцца окрэма. Здаецца, ў цыім корпусе няма нічога, чаго бы не хватало.
Аднак адпаведзі не ўсё такія однаковыя. Спеціяліст з адпаведнення правілам прасіць, якія дакументы неабходны для паўнейшага аналізу высокарызыковага корпатыўнага кліента. Асістэнт вяртае пяць фрагментаў. У трох з іх знаходзіцца неабходная політыка KYC, а ў двух — практычна ідэнтычны тэкст з старэйшай версіі дакумента. Кожны фрагмент пачынаецца тым самым заголовкам:
CONFIDENTIAL - INTERNAL USE ONLY
Group Financial Crime Compliance
Page 47 of 132
Адзін фрагмент таксама містыць рэчэнне "Усі корпаратывныя кліенты з высокім рызыкам павінны працэваць пад паглыбленай перапрацоўкай дакументаў." Паглядзіце атрымліваюча: All ператворыўся на AII, а diligence — на diIigence, таму што OCR пасплутаў малаясцю напісаны l і вялікасцю напісаны I.
У іншых частках таго ж файла засталася простора без розліку, апостроф у фармаце Windows-1252 быў неправяма дэкадаваны, а футэр праказваецца 132 разы, таму што ён быў на кожной сторонцы. Архіваваная копія правіл знаходзіцца ў індэксе празаўсёды з апраўнанай, таму што SharePoint захаваў обе.
З тэхнічной точкі зору працэс падключэння дакументаў працаваў. Аднак з практычной — корпус дакументаў забруднены, і інжынеры, якія ствараюць свою першую сістэму RAG, часта недаоцэніваюць значэнне гэтай разлікі.
Чаму ховаюцца забрудненыя даны
Збійы зазвычай настолькі незначныя, што нічога не зламваецца. Неправільная последовальнасць байтоў не выклікае падвыключэння базы дадзеных тыпу вектар. Павторэны параграф не спрычынае абяроны API для эмбедынгу. Павторэны футэр не ўвельчвае рывень аўтаматычных паведамленняў, а помылка OCR з 1 на l не зупіняе працэс індэксавання. Сістэма весела імбеддзіць усё, а рахунак прыходзіць пазней у вастоце непаводарэнняў пад час выкарыстоўвання дадзеных.
Даклоненыя дасведчыкі займаюць калькі месца ў рэзультатах тыпу “топ-к”. Павторэныя шаблонныя фрагменты ствараюць штучную сэроўоднечу межы некалякуюцымі часткамі тэксту. Адказы OCR выклікаюць змены ў важлівых элементах, прыцэнтных значэннях, номерах рахункоў і норматыўных лімітах. Артыфакты кодавання дадаюць токены, якія не маюць ніяго значэння для бізнесу. Калі з’яўляецца некарэктны адказ, кожны наступны компонент можа вядома выкананы сваю працу: прыстрой аднаходжэння дасведчыкаў вярнуў тое, што было схована, прыстрой ранжавання ранжаваў тое, што яму было дана, а большая мовная мадэль працавала на аднойчынны кантэкст, які яна отрымала. Працоўныя недагодзінкі выйшлі раней, чым усе тры компоненты.
Неабяжна політыка проты праваленняў, якая прыводзіць да таго, што аперацыі за суму падаючую вышэй чым 10 000 еўро абяжаюць дадатковыя перакантрацыі па канштоўкам джэрэла срэдзявы. Калі выйдзе бракавальны OCR і паспяшная канвертацыя кодавання, тады у індэксе будзе зафіксавана фраза „EUR l0,000“. Чалавек момантальна з’явіць брак. Аднак мадэль для вбудоўвання все рава спрыяе гэтаму тэксту як да панэйроў, так і да канштоўкаў джэрэла срэдзявы, таму пошук працюе аблікова, і гэта самае небяспечнае. Занізаны тэкст доходзіць да LLM, каліколькі сэмантычны пошук прымае тэкстовыя недосконаласці. Сістэма знаходзіць правы раздзял політыкі, але ў яму знаходзіцца некорэктная рэгуляторная значэнне.
Саме таму чыстка не можа быць сведжаная да text.strip().lower().
Дзе мае месца чыстка ў праграме RAG
Чысткі гэта працэ, якая выконваліцца между прыйманням дакумента і яго разбіваннем на часткі, і гэта парадоксальная последовасць. Не трэба меркаваць на аднаковасць дакументаў раней, чым усунуць явныя парадукты кодавання. Не трэба агрэсыўна нормалізаваць тэкст раней, чым выявіць яго мову. Не трэба адмахваць павтараючыяся правовыя клазузы толькі таму, што яны налягаюць на стандартныя формулы. А карэкты з OCR з нізкім рэвалюяцыем не павінны таямна перапісваць прававыя обавязкі.
Калі шумны тэкст ужэ разбіўся на часткі і быў уключаны, чыстка стае значна сложней. Павтараючыяся параграфы ужо створылі павтараючыяся вектары, зламаныя словы паўтарылі межы частак, павтараючыся заголовкі зайнялі прастор для токенавання, а памылкі OCR ўжо ўвайшлі ў семантычныя прэдставленні. Чыстка павінна выконваліцца раней, чым дакумент разбіваецца на часткі, а не як патч, прыняты пасля таго, як якосьць пачне падыходзіць да нуля.
Нормалізацыя кодавання: спачатку прайсці за чысткой тэкста
Колекцыі падпрыемства рэдкая час раздзеляюць адну і тую ж історыю кодавання. Сучасная палітра можа быць UTF-8, архіў кораспандалення з кліентамі — Windows-1252, а старэйшыя экспорты — Latin-1. Тэкст, копіюваны между SharePoint, электроннымі лістамі, Word і старымі сістэмамі дакументаў, могаў быць кодаваны і дэкадаваны калькі разоў прытым, як толькі прабывае да апрацоўкі. Рэзультат выглядае так:
Customer’s identity must be verified before account opening.
The customer must provide proof of address.
Enhanced Due Diligence â€" High Risk Customers
Эта паўстаноўчасць называецца mojibake, і яна выступае тады, калі байты, запісаныя ў аднам кодаванні, чытуюцца як у іншам. Чалавек адразу бачыць, што customer’s зіпсавана. У працэсе адзычэння зіпсаваны токен падае да токенаўца інакш, чым customer's, і ў масштабных сістэмах такія несувязанасці фрагментуюць слоўнік і дадаюць непатрэбныя варыяцыі ў прасторэ эмбедынга.
Будзьце астаткавы ў тым, насколькі далека трэба ідзьці з рэмантаваннем. Неразумная функцыя судзіць, што будзь-які сімвол за межамі ASCII ёсць непатрэбным, пры тым чым у еврапейскіх корпусах платэжаў законна ўжываюцца Société Générale, Deutsche Bundesbank, €10,000 і Überweisung. Калі адняць акцэнты і сімволы, рэальная інфармацыя знікае. Мета — не тэкст толькі на ASCII, а стабільны Unicode, які зберагае свой значэння.
Рэмантаванне Unicode за дапамою ftfy
Вам не трэба самім каталогаваць кожны прыклад моўібаке, таму што ftfy вялікай часткай вядомых бягаў у Unicode і кодаванні ўжо вылечвае. Там можа пачыцца стадыя чысткі пры выпуску:
import re
import unicodedata
from dataclasses import dataclass, field
import ftfy
@dataclass
class TextCleaningResult:
original_text: str
cleaned_text: str
transformations: list[str] = field(default_factory=list)
quality_flags: list[str] = field(default_factory=list)
def normalize_encoding(text: str) -> TextCleaningResult:
"""
Repair common encoding problems while preserving meaningful Unicode.
Suitable for policy, AML/KYC, payment, and regulatory documents where
accented names, currency symbols, and multilingual text must survive.
"""
transformations: list[str] = []
quality_flags: list[str] = []
if not text:
return TextCleaningResult(original_text=text, cleaned_text=text)
cleaned = text
# Repair mojibake and common Unicode encoding mistakes.
repaired = ftfy.fix_text(cleaned)
if repaired != cleaned:
transformations.append("ftfy_encoding_repair")
cleaned = repaired
# NFC preserves characters while producing a consistent Unicode
# representation. Safer than aggressively stripping accents.
normalized = unicodedata.normalize("NFC", cleaned)
if normalized != cleaned:
transformations.append("unicode_nfc_normalization")
cleaned = normalized
# Replace non-breaking spaces with normal spaces.
if "\u00a0" in cleaned:
cleaned = cleaned.replace("\u00a0", " ")
transformations.append("non_breaking_space_normalization")
# Remove zero-width characters that frequently leak from PDFs,
# web pages, and copied Office content.
zero_width_chars = {"\u200b", "\u200c", "\u200d", "\ufeff"}
if any(char in cleaned for char in zero_width_chars):
cleaned = "".join(char for char in cleaned if char not in zero_width_chars)
transformations.append("zero_width_character_removal")
# Remove control characters; preserve newline and tab because
# they may still carry document structure needed downstream.
cleaned_without_controls = "".join(
char for char in cleaned
if char in "\n\t" or unicodedata.category(char) != "Cc"
)
if cleaned_without_controls != cleaned:
transformations.append("control_character_removal")
cleaned = cleaned_without_controls
# Normalize horizontal whitespace without flattening paragraphs.
whitespace_normalized = re.sub(r"[ \t]+", " ", cleaned)
whitespace_normalized = re.sub(r"\n{3,}", "\n\n", whitespace_normalized)
if whitespace_normalized != cleaned:
transformations.append("whitespace_normalization")
cleaned = whitespace_normalized
cleaned = cleaned.strip()
# Keep suspicious replacement characters observable rather than silently deleting them.
if "\ufffd" in cleaned:
quality_flags.append("unicode_replacement_character_detected")
return TextCleaningResult(
original_text=text,
cleaned_text=cleaned,
transformations=transformations,
quality_flags=quality_flags,
)
Зверніце увагу на тое, чаго функцыя не чыніць. Яна не перакладае все на маленькія літэры, не адзьяўляе акцэнты з Société Générale, не адчыняе знак еўро і не ператварае дакумент у адну лінію. Гэтыя прытупленні ёсць намеральнымі.
Чаму агрэсыўнае чышчэнне шкодзіць
Большасць старэйшых пайплайнаў NLP былі разрабатаны для модэляў тыпу „сумка слоў“ або TF-IDF, і яны перакладалі на маленькія літэры, адзьяўлялі пунктуацыю, адчынялі словы-стоперы і выконвалі процедуру стэмінгу. Аплікацыя гэтых методаў слепа, без увагі да сучасных эмбеддынгаў, ведае корыстную інфармацыю. Паглядзіце, што гэта можа зробіць з гэтым рэчэннем:
Customer must NOT be classified as low risk.
Фінансавыя тэксты прыносяць ўсё бол чутлівыя прыклады. EUR 10,000 і EUR 100,000 выглядаюць аднакова па літэрах, але сутэчна разніцяюцца па своем значэнні, а Article 6(1)(a) і Article 6(1)(b) маюць аднаковую структуру, але разныя прававыя наследкі. Асновны прынцып — агрэсывна нормалізацыя фарматування і консерватыўна адрабоўка значэння. Змена customer’s на customer's — гэта змена формы, якая вярна відтворае месца, якое хацеў падаць аўтар. Адключэнне знака €, адчыненне акцэнаў у імені кліента чы аўтаматычная перакладзе l0% на 10% — гэта змены фактов, на якія можа раслучвацца падпрыемство.
Адключэнне стандартных фраз: структура, а не ключавыя словы
Загаловкі і ніжкі — гэта не той самы тып проблемы, як шум канкодавання. Павтарэнне Сторонка 47 з 132 явная справа шаблонаваго тэксту, але трэбованне паўнай адпаведнасці, якое з’являецца ў калькі секцый правілаў, можа быць павтаранам умышленна і мець правовую значыню. Тое, як часта з’являецца лінія, не дазволяе візначыць, з каго з гэтых двух варыянтаў мова.
Лепшы сігнал — гэта павтарэнне, якое соўпадае з полазамі. Загаловкі знаходзяцца ў верхней часты сторанак, а ніжкі — у ніжней; номеры сторанак лягка зменшваюцца, але застаюцься ў тым жа структурным патэрне:
KYC Policy | Version 7.2 | Page 41 of 132
KYC Policy | Version 7.2 | Page 42 of 132
Логіка точнага падання вважае два разныя стрыктуры, тады калі логіка, якае ведае структуру, вважае два экзампляры аднаго шаблона. Гэта ўсьмо тая прычына, чаму адказванне на выклікі стандартных фрагментаў работае найэфектывнейша, калі ўсё ўжо існуюць межы сторанак, таму трэба ухіліцца ад ператварэння распакаванага документа ў вялічэзны стрыктур пасля яго прыёму.
import re
from collections import Counter
from dataclasses import dataclass
@dataclass
class BoilerplatePattern:
normalized_text: str
occurrences: int
page_ratio: float
position: str
def normalize_boilerplate_candidate(line: str) -> str:
"""
Normalize variable fields so structurally identical headers and
footers can be compared across pages.
"""
normalized = line.strip()
normalized = re.sub(
r"\bpage\s+\d+\s+of\s+\d+\b", "page <n> of <n>",
normalized, flags=re.IGNORECASE,
)
normalized = re.sub(r"\bpage\s+\d+\b", "page <n>", normalized, flags=re.IGNORECASE)
normalized = re.sub(r"\s+", " ", normalized)
return normalized.casefold()
def detect_repeated_marginal_text(
pages: list[dict],
margin_lines: int = 3,
min_page_ratio: float = 0.6,
) -> list[BoilerplatePattern]:
"""
Detect repeated text in page-header or page-footer regions.
A candidate is considered boilerplate only when it appears in the same
marginal position across a substantial fraction of the document.
"""
if not pages:
return []
header_counts: Counter = Counter()
footer_counts: Counter = Counter()
for page in pages:
lines = [line.strip() for line in page["text"].splitlines() if line.strip()]
if not lines:
continue
header_counts.update(normalize_boilerplate_candidate(l) for l in lines[:margin_lines])
footer_counts.update(normalize_boilerplate_candidate(l) for l in lines[-margin_lines:])
total_pages = len(pages)
patterns: list[BoilerplatePattern] = []
for position, counts in (("header", header_counts), ("footer", footer_counts)):
for normalized_text, occurrences in counts.items():
page_ratio = occurrences / total_pages
if page_ratio >= min_page_ratio:
patterns.append(BoilerplatePattern(
normalized_text=normalized_text,
occurrences=occurrences,
page_ratio=page_ratio,
position=position,
))
return patterns
min_page_ratio прагаты ўскладніць прыняцця рашэння. Правілле не ўключае «выдаліць любую лінію, якая павтараецца два разы». Усё складаецца з таго, што «лінію, структурна сэроднюю іншым лініям, трэба спрацавляць як шаблонную частку, якая постаўляецца ў канцы сторніцы на большай часткі дакумента». У разе політыкі з 100 сторніц, дзе заголовак CONFIDENTIAL - INTERNAL USE ONLY з’являецца на 98 сторніцах, падставы для такога рашэння ўсё болей пераканлівыя.
Ішчо адна важлівая дэталь: выдаленне чаго-небудзь з тэксту, які вікорыстоўваецца для вбудоввання, не значыць яго выдаленне з сістэмы. Інодзе інфармацыя павінна пераходзіць з роўня сэмантычнага кантэнту на роўень метаданных. Поле такія, як версія, власнік, класыфікацыя і дата набрання чыннасці, лепшая захавваць як метаданныя, чым копіюваць ў кожны фрагмент тэксту.
Усуненне дуплікацый за межамі точных копіяў
Дакумент можа быць ідэальна адкодаваны, без пашкоджэнняяў ад OCR і без зайвага тэксту, пры чым яго адналёгчэнне выкарыстоўвання можа застацца проблематычным проста таму, што ён знаходзіцца калькі разоў у корпусе дакументаў. Рэпазітарыяў прэдпрыемстваў заполнены такімі дакументамі:
AML_Policy_v8_Final.docx
AML_Policy_v8_Final_Approved.docx
AML_Policy_v8_Final_Copy.docx
AML_Policy_v8_Approved_2026.docx
Указаць усе чатыры файлы як адзінаковыя дасобы, і ў такім случае іх часткі будуць конкуруваць падчас адзыскання. Калі запітаць пра паглыбленую разведку кліянтав, якія становяць высокы рызык, можна буда заполніць тры месца адной часткай політыкі, залішыўшы два для сапраўдна разных доказаў. Дуплікаты коштаюць больш, чым прастор на дыску: яны спакалююць значэнне пярэдніх k найболей значымых рэзультатаў.
Точныя дуплікаты з хэшаваннем контэнту
import hashlib
from dataclasses import dataclass
@dataclass
class DeduplicationRecord:
document_id: str
source: str
content_hash: str
duplicate_of: str | None = None
def canonicalize_for_hashing(text: str) -> str:
"""
Deterministic normalization before hashing.
Does not lowercase or remove punctuation because those transformations
could collapse documents that are not actually identical.
"""
lines = [" ".join(line.split()) for line in text.splitlines()]
return "\n".join(lines).strip()
def calculate_content_hash(text: str) -> str:
canonical_text = canonicalize_for_hashing(text)
return hashlib.sha256(canonical_text.encode("utf-8")).hexdigest()
def find_exact_duplicates(
documents: list[dict],
) -> tuple[list[dict], list[DeduplicationRecord]]:
"""
Keep one canonical copy of each identical document while preserving
duplicate lineage for auditability.
"""
seen_hashes: dict[str, str] = {}
unique_documents: list[dict] = []
records: list[DeduplicationRecord] = []
for document in documents:
content_hash = calculate_content_hash(document["clean_text"])
if content_hash in seen_hashes:
records.append(DeduplicationRecord(
document_id=document["document_id"],
source=document["source"],
content_hash=content_hash,
duplicate_of=seen_hashes[content_hash],
))
continue
seen_hashes[content_hash] = document["document_id"]
unique_documents.append(document)
records.append(DeduplicationRecord(
document_id=document["document_id"],
source=document["source"],
content_hash=content_hash,
))
return unique_documents, records
Хэш таксама ўжыткаваны для інкрэментальнага індэксавання. Калі той самы нормалізаваны документ з’явіцца пад час наступнага запуску працэсу апрацоўкі, яго хэш ужо вядомы, таму працэс можа праскочыць практыку чысткі, разбівання на часткі і ўведэння яго знову.
Прыбліжныя дуплікаты з MinHash LSH
Точны хэшавання падходзіць для простых случаў. Складнейшым ёсць кэсы, калі існуюць два варыянты правіл KYC, напрыклад версія 7 і версія 8, якія на 98% ідэнтычныя за тэкстам, але разлікаюцца ў однай рэгуляторной клазе. З точкі зору сарабатковання з правіламі яны абсалютна не ўважаюцца дуплікатамі, адтолькі тая последняя клаза зменяе вімогі до контролю.
У рэгулюючыйся сістэме RAG адказ на пытанне пра выяўленні майжэ дакладных копій павінен спачатку даць адказ на іншае пытанне – а самэ пра тое, якія дакументы ўстатку схожыя, каб іх было варта расследаваць, а выдаленне залішыцца окрэмай рашэннем. Для 200 000 дакументаў паверховая порэванаць трэбуе апошнё 20 мільярда порэванацей. MinHash з гашаваннем, чулым да локаліявання, ухіляецца ад гэтага, ствараючы компактныя падпісы і размешчаючы схожыя з іх у кандыдатскіх кантэйнерах.
import re
from datasketch import MinHash, MinHashLSH
def create_word_shingles(text: str, shingle_size: int = 5) -> set[str]:
"""
Five-word shingles work well for long policy and regulatory documents
because they capture local textual structure without being overly
sensitive to isolated formatting changes.
"""
tokens = re.findall(r"\b\w+\b", text.casefold())
if len(tokens) < shingle_size:
return {" ".join(tokens)} if tokens else set()
return {
" ".join(tokens[i:i + shingle_size])
for i in range(len(tokens) - shingle_size + 1)
}
def create_minhash(text: str, num_perm: int = 128) -> MinHash:
shingles = create_word_shingles(text)
minhash = MinHash(num_perm=num_perm)
for shingle in shingles:
minhash.update(shingle.encode("utf-8"))
return minhash
def build_duplicate_index(
documents: list[dict],
threshold: float = 0.85,
num_perm: int = 128,
) -> tuple[MinHashLSH, dict[str, MinHash]]:
"""
Build an LSH index for candidate near-duplicate discovery.
The threshold identifies candidates. It does not automatically
determine whether a document is deleted.
"""
lsh = MinHashLSH(threshold=threshold, num_perm=num_perm)
signatures: dict[str, MinHash] = {}
for document in documents:
document_id = document["document_id"]
signature = create_minhash(document["clean_text"], num_perm=num_perm)
signatures[document_id] = signature
lsh.insert(document_id, signature)
return lsh, signatures
Калі політыкі KYC версій v7 і v8 патрапляюць у адзін катэгорыя, сістэма вырашае, што іх трэба расследаваць адзынакова, і нічага больш. Яна не вырашыла, што адну з іх трэба адмахнуць. Рашэнне па прымене ў рэальных умовах должна łączyць сяродзіста тэксту з метаданнымі, такімі як ID політыкі, версія, дата набыць чыннасці, статус затверджэння і юрысдыкцыя, адколі сама сяродзіста не можа памочыць у выборэ.
Паклёсы OCR: правільна на выгляд тэкст, які ўсё-такі некарэктны
Рэмонт кодавання выправляе сімволы, якія былі збераганы некорэктна. Паклёсы OCR належаць да аднойчынай катэгорыі. У такім случае прыстрой мог упэўнена прачытаць абсалютна правільны сімвол Unicode і проста выбраць некарэктны. Тыповыя падобныя сімволы — 0 і O, 1, l і I, 5 і S, а таксама rn і m.
Загальныя практыкі бэдоў можа быць нізкімі, але сумарная точнасць не ўсё, што мае значэнне для рэгулююцькіх сістэм RAG. 40-сторонній документ, у яком 99,8% тэксту правільны, можа ўсё ж мець паспяшаныя найважлівейшыя даны. Ключовая архітэктурная паўза закладваецца ў тым, што бэды OCR разлічаюцца па рызыку. Ісправленне custorner на customer ўскладненняў не стварае, адтолькі кантэкст і слоўнік сільна падказваюць правільную форму слова. А пераклад EUR l0,000 на EUR 10,000 належыць да зусім іншай катэгорыі, адтолькі неправільны расчын можа пацягнуць за сабой наследкі для дапэўнення адпаведнасці правілам.
import re
from dataclasses import dataclass
@dataclass
class OCRIssue:
token: str
issue_type: str
risk_level: str
suggested_value: str | None
requires_review: bool
CURRENCY_PATTERN = re.compile(r"\b(EUR|USD|GBP|INR)\s+([A-Za-z0-9.,]+)\b")
PERCENTAGE_PATTERN = re.compile(r"\b([A-Za-z0-9.,]+)\s*%")
def detect_numeric_ocr_issues(text: str) -> list[OCRIssue]:
"""
Detect suspicious OCR substitutions inside financial values.
This function identifies candidates. It does not silently rewrite
high-risk values in the source text.
"""
issues: list[OCRIssue] = []
substitutions = {"O": "0", "o": "0", "I": "1", "l": "1"}
def inspect_numeric_token(token: str, issue_type: str) -> None:
if token.replace(",", "").replace(".", "").isdigit():
return
corrected = "".join(substitutions.get(char, char) for char in token)
numeric_candidate = corrected.replace(",", "").replace(".", "")
if numeric_candidate.isdigit() and corrected != token:
issues.append(OCRIssue(
token=token,
issue_type=issue_type,
risk_level="high",
suggested_value=corrected,
requires_review=True,
))
for match in CURRENCY_PATTERN.finditer(text):
inspect_numeric_token(match.group(2), issue_type="currency_value")
for match in PERCENTAGE_PATTERN.finditer(text):
inspect_numeric_token(match.group(1), issue_type="percentage")
return issues
Функцыя няма цілесапраўлена меняць дакумент. Звычайны выраз падпарадкавання можа паказаць, што l0,000 выглядае падазроўна ў кантэксце валюты, але не можа дапамогчы пераканацца, што ў выхідным тэксце сапраўды было напісана 10,000.
Звычны слоўнік лепш справляецца з звычайным слоўнікам, які знае адпаведную галоўню. Тэксты, звязаныя з банкаваўствам, патрабуюць загальнага словніку плюс такіх тэрмінаў, як beneficial ownership, counterparty, SWIFT, IBAN, SEPA і sanctioned. У працыншчы случае прадукты для перапісваму слоў будуць «правільна» перакладаць легітымныя фінансавыя тэрміны на гorsзыя.
У працэйнай суперблоке аналіз OCR-распазнавання спраўляе баланс межы паверыльнасці дваржыкі і значэння можлівай памылкі. Распазнаванне тэксту з нізкай значымасцю, калі ён паверліва, часта можа быць выправлена без участка чалавека. Контэнт з высокым рызыкам — тое ў чым сягаюць грошовыя значэння, пратэкты, номеры норматывных актов і даты — павінен праходзіць перагляд незалежна ад паверыльнасці OCR. Фінансавая сістэма должна ставіць мету контролюемую правільнасць, а не максымальную автаматызацыю.
Багатыязычная падготовка
Адкрыцья мовы здаецца незначным, калі корпус дакументаў перасягае межы. База дадзеных па операцыях з платамі можа мець дакументы на англійскай, лісты кліяўтав на нямецкай, норматывныя уведамленні на французкай і контракты на іспанскай, а адна картка расследавання па прынцыпам AML можа мець кілька мов у аднай запісі прыгодзе.
Язык вплывае на усё, што выклікаецца пасля яго. Корэкцыя апострыфаў залежыць ад яго, шаблоны памылак OCR разніцяцца залежна ад алфавіту, іменаваныя ентытэты маюць апострыфы, якія павінны застацца пасля нормалізаціі, а сама мадэль укладкі можа вядзецца інако залежна ад языка, які індэксуецца.
from dataclasses import dataclass
from langdetect import DetectorFactory, LangDetectException, detect_langs
DetectorFactory.seed = 0 # Makes pipeline output reproducible.
@dataclass
class LanguagePrediction:
language: str
confidence: float
alternatives: list[tuple[str, float]]
needs_review: bool
def detect_document_language(
text: str,
confidence_threshold: float = 0.85,
) -> LanguagePrediction:
sample = text.strip()
if not sample:
return LanguagePrediction(language="unknown", confidence=0.0, alternatives=[], needs_review=True)
try:
predictions = detect_langs(sample)
except LangDetectException:
return LanguagePrediction(language="unknown", confidence=0.0, alternatives=[], needs_review=True)
best = predictions[0]
alternatives = [(p.lang, round(p.prob, 4)) for p in predictions]
return LanguagePrediction(
language=best.lang,
confidence=best.prob,
alternatives=alternatives,
needs_review=best.prob < confidence_threshold,
)
Калі дакумент суплетаныя языкамі, выявленне на рэгламентнай гэтым левелі лепшая, чым адна дагаведка для всьего файла. Гэта таксама дае інструменту для раздзелення паведамленняў падказку пра структуру: калі раздзеленне адбываецца праз межу языка, якую інструмент некалі бачыў, адна частка можа мець два незв’язаныя лінгвістычныя кантэксты.
Дзеяныя каманды спачатку перакладаюць усё на англійскі язык. Гэта спрошчае індэкс, але ўводзіць яшчэ адну ператворэнне межа автантычным тэкстам і тым, што атрымліваецца, пры чым клазула пра адпаведнасць па німецкаму можа выкарыстоўваць формулаванні з точным правовым значэнням, якое пераклад незначна зменшвае. Болей моцны дызайн выкорыстоўвае тэкст у адной з асновных мов як істочнік правды і храніць любы пераклад як дадатковую версію, пры чым паводак фіксуецца ў метаданых. Метаданыя языка таксама павінны быць точнымі:
metadata = {
"primary_language": "en",
"languages": ["en", "de", "fr"],
"language_distribution": {"en": 0.72, "de": 0.21, "fr": 0.07},
"is_multilingual": True,
}
Адказ пра выявленне мовы таксама можа служыць пераканальнай перапрацоўкай. Падазроўцуймо, што палітыка проты валютных афераў у Вэльсе, якая як правіла ўтвараецца на англійскай мове, раптам паказвае primary_language = "unknown" з confidence = 0.31. Файл можа не быць багатомовным; OCR могла настолькі паспаліць тэкст, што прыстрой для выявлення больш не можа яго адзначыць.
Персональныя даны і чутлівыя даны: выявляйце іх прыяўленне
Прадзейнаванне — адна з апошніх можлівасцей кантролюваць тое, што прайходзіць у інфраструктуру пошуку. Файл KYC можа міць імены кліяў, номеры паспортаў, IBAN, номеры тэлефонаў і адрэсы электронной пошты. Калі гэтыя даны праходзяць праз службу прыяўлення і патрапляюць у базу вектараў, проблема стае набагато сложнейшай, чым захаванне аднаго орыгінальнага дакумента, адтолькі што копіі цяпер існуюць у запітах прыяўлення, вектарах, метаданых, кэшах, індэксах, логах і рэзервных копіях.
Пытанне не ў тым, чы можа мадэль абмесцавання разумець персональную інформацыю, адколькі ёна можа. Пытанне ў тым, чы вообшча трэба абмесцаваць гэтую інформацыю.
Выдаленне кожной ентытэту, які знаходзіць дэтэктар, адчыняе корпус для большай безпекі, але гэта паслабляе можлівасць пошуку за ентытэтамі. Калі кожна назва компаніі заменяецца на <ORGANIZATION>, то следчы, які расследуе справы пра валютную злочыннасць і запітвае пра Northbridge Trading GmbH, нічога не знайдзе. Таму стандартны падход — перш чым прыймаць якія-небудзь дзеянні, задаць правілы апрацоўкі для кожнага знайдзенага тыпу ентытэту. Публічныя і оператывныя назвы зазвычай залішаюцца, персональную інформацыю кліента маскуюць, токенізуюць або обмежваюць доступ да яе, внутрашнія організацыйныя апавяданні падконтрольваюцься правіламі доступу, а паролі ніколі не падаюць у індэкс.
from dataclasses import dataclass, field
from presidio_analyzer import AnalyzerEngine
@dataclass
class SensitiveEntity:
entity_type: str
start: int
end: int
score: float
original_value: str
@dataclass
class PIIScanResult:
entities: list[SensitiveEntity] = field(default_factory=list)
needs_review: bool = False
class BankingPIIDetector:
def __init__(self) -> None:
self.analyzer = AnalyzerEngine()
def detect(
self,
text: str,
language: str = "en",
score_threshold: float = 0.70,
) -> PIIScanResult:
"""
Detect sensitive entities without modifying the source text.
Detection is deliberately separated from anonymization because
different entity types require different handling policies.
"""
results = self.analyzer.analyze(
text=text, language=language, score_threshold=score_threshold
)
entities = [
SensitiveEntity(
entity_type=r.entity_type,
start=r.start,
end=r.end,
score=r.score,
original_value=text[r.start:r.end],
)
for r in results
]
high_risk_types = {"CREDIT_CARD", "IBAN_CODE", "US_SSN", "IP_ADDRESS"}
return PIIScanResult(
entities=entities,
needs_review=any(e.entity_type in high_risk_types for e in entities),
)
Замена значэнняў на заполнічныя элементы, увведзенные рукой, зазвычай даст лепыяся рэзультаты для RAG, чым ўсуненне іх:
Customer IBAN: <IBAN>
Customer Email: <EMAIL_ADDRESS>
Зьвярэнне застаецца ў своем значэнчыкаваным фармаце, таму запіт пра тое, дзе пераканальваецца IBAN кліента, все ўсё можа падходзіць да тэксту, які містыць <IBAN>. Калі неабходна адражэнне звязкаў межу выказаннямі, дэтэрміністычныя токены лепшы за загальныя заместнікі. CUSTOMER_7F29A1, які павтараецца ў 40 запісах, дапамагае застаць цэлысны след даследжэння, тады калі кожна імя будзе заменена на <PERSON>, гэты след будзе разрыхтаваны.
Метаданныя — гэта лёгка забываемы віклап. Тэкст можна адрэдагаваць аберагліва, пры чым ў яго полях все равно можа застацца customer_name: "Lena Hoffmann" або шлях да крыніцы, напрыклад /customers/Lena_Hoffmann/KYC/passport_review.pdf. Таму контроль чутлівых дадзэнняў павінен распаўсюджвацца на усё, што індэксуецца, а не толькі на тэкст.
Інструментарыя высокага стандарта
Калі ўжо тэстуеце, дастатнія будуць індывідуальныя функцыі чысткі. У працэўнай суперактуальнасці дыяждзіць строгей стандарт: дакумент не павінен пераходзіць да наступных крокаў толькі таму, што серыя крокаў выканалася без адхыленняў. Сістэма павінна фіксаваць, што было зменена, што ўсё яшчэ выглядае падазроўна, чы не з’явіліся чувственныя даны, чы файл ёсць дуплікатам іншага, а таксама чы канечны рэзультат паслугвае для індэксавання.
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class QualityStatus(str, Enum):
PASS = "pass"
REVIEW = "review"
REJECT = "reject"
@dataclass
class Transformation:
stage: str
operation: str
details: dict[str, Any] = field(default_factory=dict)
@dataclass
class QualityFlag:
code: str
severity: str
details: dict[str, Any] = field(default_factory=dict)
@dataclass
class RetrievalReadyDocument:
document_id: str
source: str
raw_text: str
clean_text: str
language: str | None = None
language_confidence: float | None = None
content_hash: str | None = None
duplicate_of: str | None = None
transformations: list[Transformation] = field(default_factory=list)
quality_flags: list[QualityFlag] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
status: QualityStatus = QualityStatus.PASS
Разлік межа raw_text і clean_text ёсць фундаментальным. Першы выказвае, што насправды было выкарана пад час запоўнення, а другі — якую форму прэдставлення мы гатовы фрагментаваць і вбудоўваць. Журнал трансформацый пояснюе, як адна форма ператварылася ў іншую, і гэтаі ланцуг мае вельмі важлівую значэнне, калі праз шасць месцаў пасля таго з’являецца адзін з бракоў у выкарыстоўванні дадзеных.
import logging
logger = logging.getLogger(__name__)
class ProductionDocumentCleaner:
def __init__(
self,
pii_detector: BankingPIIDetector,
pii_anonymizer,
redact_pii_for_embeddings: bool = True,
) -> None:
self.pii_detector = pii_detector
self.pii_anonymizer = pii_anonymizer
self.redact_pii_for_embeddings = redact_pii_for_embeddings
def clean(self, document: dict) -> RetrievalReadyDocument:
result = RetrievalReadyDocument(
document_id=document["document_id"],
source=document["source"],
raw_text=document["text"],
clean_text=document["text"],
metadata=document.get("metadata", {}).copy(),
)
try:
self._normalize_text(result)
self._detect_language(result)
self._validate_ocr(result)
self._handle_sensitive_data(result)
self._calculate_hash(result)
self._apply_quality_gate(result)
except Exception as exc:
logger.exception("Preprocessing failed for document %s", result.document_id)
result.quality_flags.append(QualityFlag(
code="PREPROCESSING_EXCEPTION",
severity="critical",
details={"error": str(exc)},
))
result.status = QualityStatus.REJECT
return result
def _apply_quality_gate(self, document: RetrievalReadyDocument) -> None:
severities = {flag.severity for flag in document.quality_flags}
if "critical" in severities:
document.status = QualityStatus.REJECT
elif "high" in severities:
document.status = QualityStatus.REVIEW
else:
document.status = QualityStatus.PASS
Выходны пункт выдае адзін з трох чыстаўленых статаусаў. PASS дазволяе дакументу продаваць працэс разбівання на часткі без нерашытых высакарызычных проблем. REVIEW означае, што быў атрыманы прыдатны для вжыцтва контэнт, але дзеякія трансфармацыі патрабуюць паўтарнай пераканання чалавека пры індэксаванні. REJECT означае, што рэзультат не ўстойчывы дастатнька, каб яго індэксаваць. Сканаваны документ з сумнівным прагам l0% пераходзіць у статаус REVIEW; PDF, у яком на 80 старонках ўсьмо толькі 17 спачатку неразбіраемых знакоў, верагодна атрымае статаус REJECT; а правілы, у якіх ўсія паспеленні былі толькі апострофамі і прабеламі, отрымаюць статаус PASS.
Паказнікі для стадзіі чысткі ўсё таксама варта зберагчы. Калі text_reduction_ratio зазвычай скарочвае 4-8% дакумента, а пасля выпуску расту да 63%, значы ў чымсь з’явілася проблема. Якшчы unknown_language_rate паднікае з 0,3% да 18% пасля апдэйта бібліятэкі OCR, значы дэтэктар выяўляе проблемы ў адной з паперадзей стадзій. Такія цыфры робяць працэс чысткі відным і керованым, а не прыводзяць да неконтрольванага працэсу обрабоцкі.
Компрэсіі ў працоўнай суперактывацыі
Жадная настройка чысткі не падходзіць для всіх, і выборы маюць рэальныя наследкі.
- Рэмонт кодавання.
ftfyўжоўчы і эфектываўна рашае тыповыя проблемы з моібакэ і панэламі Unicode. Спрыяйце яму як дапаможніку для прагрэсіўнай нормалізацыі Unicode, а не як разрэшэнню перапісваць тэкст на свае жаданне. - Адзьякраванне дублікаў. Алгорытм хэшавання SHA-256 практычна безкоштовны і можа выконваць адзьякраванне точных дублікаў раней, чым будуць выкорыстаны дорожэйшыя методы аналізу схожасці. Алгорытмы MinHash і LSH падыходзяць для пошуку майжэ дублікаў у велікых масштабах, але ўстановленыя імі порогі должны даваць толькі кандыдатавакі, а не рашэння пра выдаленне.
- Корэкцыя за дапамою OCR. Пошук у словніку коштуе мало і працюе стабільна з звычайнымі словамі. Мадэль LLM, якая разумее кантэкст, можа распазнаваць складнейшыя кейсы, але генератывныя мадэлі пад час прадзействавання прынесу дапаможныя вітраты і новы рызык — можлівасць стварэння правдападобных перапісваў. Для матэрыялаў, якія рэгулююцца, нехай мадэль прыначае правкі у структураванам фармаце, які можа затвердзіць рэвізор, а нехай яна рэдагуе дакументы без перакладзення ўвесь час.
- Мультыязычнае адкалекаванне. Адказванне цэлага дакумента ўжоць недорога і падходзіць для направлення. Адказванне на рэўлінгавом лэвеле лепшая падходзіць для файлаў з сумешанымі мовамі, але коштуе дорожа; калі ўжоць будзе лепшы варыянт, гэта залежыць ад рэальныя распадзелу мов у вашых дакументах і ад крэтарыяў для адказвання. Слова «мультыязычнае» на карточцы модэлю мало што гаворае пра можлівасць адказвання на пытанні пра правілы AML на англійскай мове, калі трэба знайсці тэрміны на нямецкай мове.
- Персональныя даны. Жорсткае выдаленне інформацыі зменшае рызык, але ускладнюе пошук па конкрэтныях элементах. Часта лепшы падход — аддзельнае векторнае пошукаванне на сэмантычнай аснове і безпечны пошук на абоўсцю точнай зусімнадзе, такім чынам адзін індэкс не павінен рашаць оба прыбліжэння.
Галоўная ідэя, якая спаявае ўсё гэта: нехай трэбованні да адказвання і рызык спакалення значэння керуюць рашэннямі па прадзеяванню, а не ступенем апрантасці чыставанага тэксту.
Праўець да пачатковай політыкі
Верніцеся да тую первічную політыку KYC. Працэс абсарбавання выкарыстоўваў яе з успехам, а сэрвіс чысткі цяпер выканае значна больш заходаў пры включэнні дадзеных у індэкс. Ён відновляе зламаны кодаванне, не выкарыстоўваючы пры гэму легітымныя багатыя мовныя сімвалы, адключае павтараючыяся заголовкі стороніч, захоўваючы метаданы дакумента, выявляе точныя копіі, якія зберагліся ў разных рэпазітарыях, і спрыяе таму, што старэйшыя версіі політык будуць спрацавляць як скарыгі, а не як элементы, якія трэба слепа адмаўляць. Ён пазначае EUR l0,000 як серьёзную анамалію OCR і не дозволяе самавольна ўжываць прыпускі, распазнае багатыя мовныя часткі, выявляе чутлівыя ідэнтыфікаторы кліянтаў пры ўхідзе іх у шар укладкі, фіксуе кожную трансформацыю і выявляе, чы рэзультат павінен працаваць, прайсці на перагляд чы быць адмаўленым.
Толькі тады дакумент практыкуецца на часткі. Парадак мае значэнне, таму што працэс часткавання не можа вярнуць інфармацыю, якая ўжо была знішчана падчас прадзеявання, а методы вбудовывання не можа розразіць автарытатныя знанні ад спалучэння, якое прайшло ў корпус без пераканальнага контролю.
Пасля чысткавання выступае іншая проблема: дзе разлучыць чыстую 80-стороннюя політыку AML? Стварыць простае вікно з 500 токенамі — гэта проста задача, але ёна можа разлучыць правіло з яго аўтаноміяй, падчыстыць табліцу ад ўсіх заголовкаў чы прыяўжніць некалякуючыя пункты разам, проста таму, што ў яных аднаковы бюджэт токенамі. Методы разлучэння варьююцца ад фіксаванага розмеру і рэкурсыўных да семантычных, усведамленых пра структуру, на рэвэлі-дзецячым і на рэвэлі-пропозыцыйным роўні, і кожны з іх адпавядае самастойнаму падходу.
Спіс пераконтраў пры часткаванні
Перш чым файл прайдзе да наступных этапаў — фрагментавання і вбудоввання, на стадыі прадзеявання трэба быць можлівым даць чыстае адказ на кожна з следуючых пытанняў:
- Чы не было працягвання кодавання і нормалізацыі Unicode без выключэння сімвалаў, якія несу значэнне?
- Чы былі адмаўлены кантрольныя сімвалы, сімвалы з нулявай шыроцюю, павтараючыяся загаловкі і наголаскі, а таксама іншыя факторы, якія паслабляюць кантроль над данымі?
- Чы былі выявлены точныя і майжэ тотальныя дуплікаты, пры чым асалоджаныя версіі дакумента засталіся недастрыжанымі?
- Чы сумнівныя распазнаўанні OCR дазволаў ў вялічынах, працэнтах, датах, ідэнтыфікаторах і правовых апавядомленнях былі заданы кому-небудзь, а не паспешна выправлены?
- Чы была выявлена мова з адпаведной дзеярожнасцю, уключаючы контэнт з сумешанымі мовамі?
- Чы чувствяглыя даны былі класіфікаваны па правілах безпекі прыстрымку перад ўбудовванням?
- Чы кожна значныя змяну можна было праследаваць да первіснага апрацоўванага тэксту?
- Чы гэта ўсунецца ясны вердыкт PASS, REVIEW або REJECT пры пачатку роздзелення на часткі?
Калі якая-небудзь адказ не ўсё зрозумела, дакумент не гатовы да выкарыстоўвання.
Калі пашкоджаная політыка ужо была роздзелена на 80 частак, увключана, скопіявана ў вектарны індэкс, зберэжана у кешы і выдаўана жывой аплікацыёй, тады выправленне вучальнага файла больш не дапаможа. Кожную падгэтаваную частку неабходна знайсці, ўдаліць яе вектары, анулюваць кешы, перапрацаваць дакумент і пераканацца, што старыя версіі зняты. Адхіленне ад некальканосных дадзеных — гэта самыя эконамічныя заходы, пры чым гэта не ператворыцца на вектарные прэдставленні.
Паказаны код ствароўваецца з метай яснасці і правільнасці. Для прыменнення ў практыцы неабходна дадаць структураваная логгірацыя, калібруванне і адарожненне секрэтных даных, стойкія летапы аўдыту для ператворэнняў, паметры прадзеянае перадобработкі, механізмы вярнення пасля аберанняў і пераглед чалавекам для корэкцый з высокім рызыком. У рэгулююцых сэрэгах кожна разрушальная ператворэнне павінна быць працоўна прыязначальная да первачнага выхаднага дасвіду.