0Pricing
AI Agents · Урок

Классификация и маршрутизация документов

Распределение документов по категориям и передача их специализированным обработчикам агентов.

«Классификация и маршрутизация документов» — бесплатный урок AI Agents на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения AI Agents, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс AI Agents содержит 4 уроков всего.

Зачем нужна классификация документов

Агент интеллектуальной обработки документов может получать документы самых разных типов: счета, договоры, отчёты, электронные письма и чеки. Для каждого типа требуются своя логика извлечения и свои бизнес-правила.

Классификация документов направляет каждый документ к подходящему обработчику до начала дальнейшей обработки, выступая в роли логики первичного приёма агента.

Классификация на основе LLM

Самый простой и гибкий классификатор использует LLM. Передайте модели образец текста документа и попросите определить его тип. Этот подход хорошо работает, когда типы документов чётко различаются.

import openai
import os

client = openai.OpenAI(api_key=os.getenv('OPENAI_API_KEY'))

DOC_TYPES = ['invoice', 'contract', 'report', 'email', 'receipt', 'form', 'letter', 'other']

CLASSIFY_PROMPT = '''Classify this document into one of these types: {types}

Document excerpt (first 1000 characters):
{text}

Respond with ONLY the document type as a single word from the list above.'''

def classify_with_llm(text):
    response = client.chat.completions.create(
        model='gpt-4o-mini',  # cheap and fast for classification
        messages=[{
            'role': 'user',
            'content': CLASSIFY_PROMPT.format(
                types=', '.join(DOC_TYPES),
                text=text[:1000]
            )
        }],
        max_tokens=10,
        temperature=0
    )
    predicted = response.choices[0].message.content.strip().lower()
    return predicted if predicted in DOC_TYPES else 'other'

Резервный вариант классификации на основе правил

Классификация с помощью LLM точна, но требует затрат и увеличивает задержку. Для распространённых, чётко определённых типов документов классификатор на основе ключевых слов и правил работает быстро, бесплатно и понятно.

Используйте правила как быстрый путь, а для неоднозначных случаев переходите к LLM.

CLASSIFICATION_RULES = {
    'invoice': [
        'invoice', 'invoice number', 'bill to', 'amount due',
        'total amount', 'tax invoice', 'payment terms'
    ],
    'contract': [
        'agreement', 'terms and conditions', 'hereby agrees',
        'party a', 'party b', 'whereas', 'obligations'
    ],
    'report': [
        'executive summary', 'quarterly report', 'annual report',
        'findings', 'recommendations', 'methodology'
    ],
    'email': ['from:', 'to:', 'subject:', 'date:', 'dear ', 'regards,'],
    'receipt': ['receipt', 'thank you for your purchase', 'transaction id', 'cashier']
}

def classify_with_rules(text):
    text_lower = text.lower()
    scores = {}
    for doc_type, keywords in CLASSIFICATION_RULES.items():
        score = sum(1 for kw in keywords if kw in text_lower)
        if score > 0:
            scores[doc_type] = score
    if not scores:
        return None  # no match — fall through to LLM
    return max(scores, key=scores.get)

if __name__ == '__main__':
    demo_text = 'INVOICE\nBill To: Acme Corp\nAmount Due: $500\nPayment Terms: Net 30'
    print('Classified as:', classify_with_rules(demo_text))

Многоуровневая стратегия классификации

Объедините классификацию на основе правил и LLM в многоуровневый подход: сначала применяйте быстрые правила, а LLM используйте только тогда, когда правила не дают однозначного результата. Это позволяет снизить затраты, сохраняя точность в нестандартных случаях.

def classify_document(text):
    # Tier 1: rule-based (free, fast)
    result = classify_with_rules(text)
    if result:
        print(f'Rule-based classification: {result}')
        return {'type': result, 'method': 'rules', 'confidence': None}

    # Tier 2: LLM (accurate, slower)
    result = classify_with_llm(text)
    print(f'LLM classification: {result}')
    return {'type': result, 'method': 'llm', 'confidence': None}

Пороги уверенности

Не все результаты классификации имеют одинаковую степень уверенности. Для классификаторов на основе LLM запрашивайте вместе с классификацией оценку уверенности. Если уверенность низкая, помечайте документ для проверки человеком.

import json

CLASSIFY_WITH_CONFIDENCE_PROMPT = '''Classify this document. Return JSON:
{{"type": "invoice", "confidence": 0.95, "reason": "Contains invoice number and payment terms"}}

Valid types: {types}
Document excerpt: {text}

JSON:'''

def classify_with_confidence(text):
    response = client.chat.completions.create(
        model='gpt-4o-mini',
        messages=[{'role': 'user', 'content': CLASSIFY_WITH_CONFIDENCE_PROMPT.format(
            types=', '.join(DOC_TYPES),
            text=text[:1000]
        )}],
        temperature=0
    )
    try:
        result = json.loads(response.choices[0].message.content)
        return result
    except json.JSONDecodeError:
        return {'type': 'other', 'confidence': 0.0, 'reason': 'Parse error'}

LOW_CONFIDENCE_THRESHOLD = 0.6

def classify_and_check(text):
    result = classify_with_confidence(text)
    if result['confidence'] < LOW_CONFIDENCE_THRESHOLD:
        result['needs_review'] = True
        print(f'Low confidence ({result["confidence"]}) — flagging for review')
    return result

Маршрутизаторы документов

После классификации маршрутизатор направляет документ специализированному обработчику. Каждый обработчик знает, как извлекать конкретные поля, относящиеся к данному типу документа.

def handle_invoice(text):
    # Extract: vendor, invoice number, total, due date
    extract_prompt = f'''Extract from this invoice (JSON):
{{"vendor": "", "invoice_number": "", "total": 0, "due_date": "", "line_items": []}}

{text[:2000]}\n\nJSON:'''
    return llm_call(extract_prompt)

def handle_contract(text):
    # Extract: parties, effective date, term, key obligations
    extract_prompt = f'''Extract from this contract (JSON):
{{"parties": [], "effective_date": "", "term_months": 0, "key_obligations": []}}

{text[:2000]}\n\nJSON:'''
    return llm_call(extract_prompt)

ROUTERS = {
    'invoice':  handle_invoice,
    'contract': handle_contract,
    'report':   lambda t: llm_call(f'Summarize this report in 3 bullet points:\n{t[:2000]}'),
    'email':    lambda t: llm_call(f'Extract: sender, subject, action required from this email:\n{t[:1000]}')
}

def route_document(text):
    classification = classify_document(text)
    doc_type = classification['type']
    handler = ROUTERS.get(doc_type, lambda t: llm_call(f'Describe this document:\n{t[:1000]}'))
    return handler(text)

Классификация подтипов

Типы верхнего уровня, например «договор», могут иметь подтипы: трудовой договор, NDA, договор оказания услуг или договор аренды. Классификатор подтипов на втором этапе позволяет точнее извлекать поля.

CONTRACT_SUBTYPES = {
    'employment': ['employment', 'employee', 'employer', 'salary', 'compensation', 'job title'],
    'nda': ['non-disclosure', 'confidential', 'nda', 'proprietary information'],
    'service': ['service agreement', 'scope of work', 'deliverables', 'milestone'],
    'lease': ['lease', 'landlord', 'tenant', 'rent', 'premises', 'square feet']
}

def classify_contract_subtype(text):
    text_lower = text.lower()
    scores = {
        subtype: sum(1 for kw in keywords if kw in text_lower)
        for subtype, keywords in CONTRACT_SUBTYPES.items()
    }
    best = max(scores, key=scores.get)
    if scores[best] == 0:
        return 'general'
    return best

def handle_contract_routed(text):
    subtype = classify_contract_subtype(text)
    print(f'Contract subtype: {subtype}')
    # Route to specialized extractor
    if subtype == 'nda':
        return extract_nda_fields(text)
    elif subtype == 'employment':
        return extract_employment_fields(text)
    else:
        return handle_contract(text)

Конвейер пакетной классификации

В рабочей среде документы поступают пакетами. Обрабатывайте их эффективно: сначала классифицируйте все документы, сгруппируйте их по типам, а затем обрабатывайте каждую группу параллельно.

from concurrent.futures import ThreadPoolExecutor
import time

def classify_batch(documents):
    results = []
    for doc in documents:
        text = extract_text(doc['path'])  # PDF, OCR, or plain text
        classification = classify_document(text[:1500])
        results.append({
            'doc_id':   doc['id'],
            'path':     doc['path'],
            'type':     classification['type'],
            'method':   classification['method'],
            'text':     text
        })
    return results

def process_batch(documents, max_workers=4):
    # Step 1: classify all (fast)
    classified = classify_batch(documents)

    # Step 2: group by type
    from collections import defaultdict
    by_type = defaultdict(list)
    for doc in classified:
        by_type[doc['type']].append(doc)

    # Step 3: process each group
    all_results = {}
    for doc_type, docs in by_type.items():
        handler = ROUTERS.get(doc_type)
        if handler:
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                futures = {executor.submit(handler, d['text']): d for d in docs}
                for fut, doc in futures.items():
                    all_results[doc['doc_id']] = fut.result()
    return all_results

Обработка неизвестных типов и типа «другое»

Для документов, классифицированных как «другое», или документов с низкой уверенностью нужна резервная стратегия. Возможные варианты: направить документ на проверку человеком, попробовать универсальное извлечение или спросить пользователя, к какому типу относится документ.

HUMAN_REVIEW_QUEUE = []

def process_document(doc_path):
    text = extract_text(doc_path)
    classification = classify_with_confidence(text[:1500])

    # High confidence path
    if classification['confidence'] >= 0.8 and classification['type'] != 'other':
        handler = ROUTERS.get(classification['type'])
        return {
            'result': handler(text),
            'type': classification['type'],
            'auto_processed': True
        }

    # Low confidence or unknown type
    HUMAN_REVIEW_QUEUE.append({
        'path': doc_path,
        'predicted_type': classification['type'],
        'confidence': classification['confidence'],
        'reason': classification.get('reason', '')
    })

    print(f'Added to review queue: {doc_path} ({classification["confidence"]:.0%} confident)')
    return {'auto_processed': False, 'queued_for_review': True}

Цикл обратной связи для классификации

Когда человек исправляет ошибочную классификацию, журналируйте это исправление. Используйте такие записи, чтобы со временем улучшать классификатор на основе правил и дообучать или настраивать на нескольких примерах классификатор LLM.

correction_log = []

def log_correction(doc_path, predicted_type, correct_type, text_sample):
    correction_log.append({
        'doc_path': doc_path,
        'predicted': predicted_type,
        'correct': correct_type,
        'text_sample': text_sample[:200]
    })
    print(f'Logged correction: {predicted_type} -> {correct_type}')

def build_few_shot_examples(n=5):
    recent = correction_log[-n:]  # use most recent corrections
    examples = []
    for entry in recent:
        examples.append(
            f'Text: {entry["text_sample"]}\nCorrect type: {entry["correct"]}'
        )
    return '\n\n'.join(examples)

def classify_with_few_shot(text):
    few_shot = build_few_shot_examples()
    prompt = f'''Examples of correct classifications:\n{few_shot}\n\nNow classify:\n{text[:800]}\n\nType:'''
    return llm_call(prompt).strip().lower()

Извлечение структурированных данных после классификации

После определения типа документа извлеките конкретные поля, важные для этого типа. Используйте структурированное извлечение с помощью LLM и схемы JSON, чтобы получать единообразные данные, пригодные для разбора.

import json

class FakeMsg:
    def __init__(self, content): self.content = content

class FakeChoice:
    def __init__(self, content): self.message = FakeMsg(content)

class FakeResponse:
    def __init__(self, content): self.choices = [FakeChoice(content)]

class _Completions:
    @staticmethod
    def create(model, messages, temperature):
        return FakeResponse('{"vendor_name": "Acme", "invoice_number": "123", "total": 100}')

class _Chat:
    completions = _Completions()

class FakeClient:
    chat = _Chat()

client = FakeClient()

EXTRACTION_SCHEMAS = {
    'invoice': '{vendor_name: , invoice_number: , invoice_date: , due_date: , subtotal: 0, tax: 0, total: 0, line_items: []}',
}

def extract_structured_fields(text, doc_type):
    schema = EXTRACTION_SCHEMAS.get(doc_type)
    if not schema:
        return {'error': f'No extraction schema for type: {doc_type}'}
    prompt = (f'Extract fields from this {doc_type}. Return JSON matching this schema:\n'
              f'{schema}\n\nDocument:\n{text[:2000]}\n\nJSON:')
    response = client.chat.completions.create(model='gpt-4o-mini', messages=[{'role': 'user', 'content': prompt}], temperature=0)
    try:
        return json.loads(response.choices[0].message.content)
    except json.JSONDecodeError:
        return {'error': 'Could not parse extraction result'}

print(extract_structured_fields('Invoice #123 from Acme for $100', 'invoice'))

Проверка знаний

В чём преимущество многоуровневой стратегии классификации (сначала правила, затем LLM как резервный вариант)?

Итоги: классификация и маршрутизация документов

Классификация документов направляет входящие документы специализированным обработчикам. Используйте многоуровневый подход: быстрые правила на основе ключевых слов для распространённых типов, классификацию с помощью LLM и оценкой уверенности для нестандартных случаев, а также очереди проверки человеком для документов с низкой уверенностью.

Для каждого типа документа используется собственный обработчик извлечения. Классификация подтипов (например, NDA и трудового договора) позволяет точнее извлекать поля. Журналируйте исправления, чтобы со временем улучшать классификаторы с помощью цикла обратной связи.

Часто задаваемые вопросы

Урок «Классификация и маршрутизация документов» бесплатный?

Да — полный текст урока «Классификация и маршрутизация документов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс AI Agents, подпишись на CoddyKit PRO. Курс AI Agents содержит 4 уроков всего.

Чему я научусь в уроке «Классификация и маршрутизация документов»?

Распределение документов по категориям и передача их специализированным обработчикам агентов. Ты практикуешь AI Agents с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать AI Agents?

Предыдущий опыт не требуется. AI Agents на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.

Сколько времени занимает урок «Классификация и маршрутизация документов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке AI Agents?

Да. Каждый урок AI Agents включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Разбор PDF с помощью PyMuPDF и pdfplumber
  2. OCR для отсканированных документов
  3. Агенты для вопросов и ответов по нескольким документам
  4. Классификация и маршрутизация документов
← Назад к AI Agents