0Pricing
AI Agents · Lekcja

Agenci pytań i odpowiedzi dla wielu dokumentów

Indeksowanie korpusu dokumentów i odpowiadanie na pytania na podstawie wszystkich dokumentów.

Agenci pytań i odpowiedzi dla wielu dokumentów to bezpłatna lekcja AI Agents na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej AI Agents, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs AI Agents zawiera 4 lekcji w sumie.

Przegląd systemów pytań i odpowiedzi dotyczących wielu dokumentów

Agent pytań i odpowiedzi dotyczących wielu dokumentów odpowiada na pytania, pobierając istotne treści z kolekcji N dokumentów, tworząc na ich podstawie odpowiedź i przypisując każde twierdzenie do jego źródła.

W przeciwieństwie do systemów pytań i odpowiedzi dotyczących pojedynczego dokumentu agenci obsługujący wiele dokumentów muszą radzić sobie ze sprzecznymi informacjami pochodzącymi z różnych źródeł oraz oceniać, które dokumenty są najbardziej istotne dla danego pytania.

Indeksowanie wielu dokumentów

Przed udzieleniem odpowiedzi na jakiekolwiek pytania wszystkie dokumenty trzeba zindeksować: przeanalizować, podzielić na fragmenty, przekształcić na wektory osadzeń i zapisać w wektorowej bazie danych. Każdy fragment jest zapisywany wraz z metadanymi zawierającymi odnośnik do dokumentu źródłowego.

import chromadb
from chromadb.utils import embedding_functions
import os

client = chromadb.PersistentClient(path='./doc_index')
ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.getenv('OPENAI_API_KEY'),
    model_name='text-embedding-3-small'
)
collection = client.get_or_create_collection('documents', embedding_function=ef)

def index_document(doc_id, doc_path, doc_title):
    # Parse and chunk
    chunks = pdf_to_chunks(doc_path, chunk_size=800, overlap=150)
    for i, chunk in enumerate(chunks):
        chunk_id = f'{doc_id}_chunk_{i}'
        collection.add(
            ids=[chunk_id],
            documents=[chunk['text']],
            metadatas=[{
                'doc_id': doc_id,
                'title': doc_title,
                'page': chunk['page'],
                'source_file': doc_path
            }]
        )
    print(f'Indexed {len(chunks)} chunks from: {doc_title}')

Pobieranie istotnych fragmentów

Po otrzymaniu pytania należy przeszukać magazyn wektorowy w celu znalezienia fragmentów o największym podobieństwie semantycznym spośród wszystkich zindeksowanych dokumentów. Parametr n_results określa liczbę pobieranych fragmentów.

def retrieve_relevant_chunks(question, n_results=8):
    results = collection.query(
        query_texts=[question],
        n_results=n_results,
        include=['documents', 'metadatas', 'distances']
    )

    chunks = []
    for i in range(len(results['documents'][0])):
        chunks.append({
            'text':      results['documents'][0][i],
            'metadata':  results['metadatas'][0][i],
            'distance':  results['distances'][0][i],
            'relevance': 1 - results['distances'][0][i]  # cosine similarity proxy
        })

    # Sort by relevance
    chunks.sort(key=lambda x: x['relevance'], reverse=True)
    return chunks

Wskazywanie źródeł w promptcie

Przekazując pobrane fragmenty do LLM, należy oznaczyć każdy z nich źródłem dokumentu. Dzięki temu LLM może cytować źródła za pomocą numerowanych odwołań w odpowiedzi.

def format_chunks_for_prompt(chunks, max_chars=4000):
    sections = []
    used_chars = 0

    for i, chunk in enumerate(chunks, 1):
        meta = chunk['metadata']
        header = f"[Source {i}: {meta['title']}, page {meta.get('page', '?')}]"
        content = chunk['text'][:600]
        entry = f'{header}\n{content}'

        if used_chars + len(entry) > max_chars:
            break

        sections.append(entry)
        used_chars += len(entry)

    return '\n\n'.join(sections)

QA_PROMPT = '''Answer the question based on the provided document excerpts.
Cite sources as [Source N]. If sources conflict, mention both views.

{context}

Question: {question}
Answer:'''

def answer_question(question):
    chunks = retrieve_relevant_chunks(question, n_results=6)
    context = format_chunks_for_prompt(chunks)
    return llm_call(QA_PROMPT.format(context=context, question=question))

Wnioskowanie między dokumentami

Niektóre pytania wymagają syntezy informacji z wielu dokumentów, a nie tylko znalezienia jednego pasującego fragmentu. Na przykład: „Która z trzech umów zawiera najniższą karę umowną?”

Należy zastosować podejście dwuetapowe: pobrać istotne fragmenty z każdego dokumentu, a następnie poprosić LLM o ich porównanie i utworzenie syntezy.

def cross_document_compare(question, doc_ids):
    # Retrieve best chunks per document
    per_doc_chunks = {}
    for doc_id in doc_ids:
        results = collection.query(
            query_texts=[question],
            n_results=3,
            where={'doc_id': {'$eq': doc_id}}  # filter by document
        )
        if results['documents'][0]:
            per_doc_chunks[doc_id] = results['documents'][0]

    # Format with document labels
    context_parts = []
    for doc_id, texts in per_doc_chunks.items():
        doc_label = f'Document {doc_id}'
        combined = ' '.join(texts[:2])[:600]
        context_parts.append(f'== {doc_label} ==\n{combined}')

    comparison_context = '\n\n'.join(context_parts)
    return llm_call(f'Compare these documents to answer: {question}\n\n{comparison_context}')

Obsługa sprzecznych informacji

Różne dokumenty mogą zawierać sprzeczne fakty — jedna umowa może stanowić, że płatność jest wymagalna w ciągu 30 dni, a inna, że w ciągu 60 dni. Agent musi wykrywać takie konflikty i przedstawiać je użytkownikowi, zamiast po cichu wybierać jedną z wersji.

CONFLICT_PROMPT = '''You are analyzing multiple document sources.
Some may contain conflicting information.

For each factual claim you make:
1. Cite the source document
2. If another source contradicts it, explicitly note the conflict
3. Indicate which source you believe is more authoritative, if possible

Document excerpts:
{context}

Question: {question}

Answer (with conflict notes where applicable):'''

def answer_with_conflict_detection(question):
    chunks = retrieve_relevant_chunks(question, n_results=8)
    context = format_chunks_for_prompt(chunks)
    return llm_call(CONFLICT_PROMPT.format(
        context=context, question=question
    ))

Filtrowanie według progu trafności

Nie wszystkie pobrane fragmenty są rzeczywiście istotne — podobieństwo wektorowe wiąże się z kompromisem między pełnością a precyzją. Należy ustawić minimalny próg trafności, aby wykluczyć słabo pasujące fragmenty, które mogłyby wprowadzić LLM w błąd.

MIN_RELEVANCE = 0.72  # cosine similarity threshold

def retrieve_above_threshold(question, n_results=10, threshold=MIN_RELEVANCE):
    chunks = retrieve_relevant_chunks(question, n_results=n_results)
    relevant = [c for c in chunks if c['relevance'] >= threshold]

    print(f'Retrieved: {len(chunks)}, Above threshold: {len(relevant)}')

    if not relevant:
        # Fallback: use top 3 even if below threshold
        return chunks[:3]

    return relevant

def answer_with_threshold(question):
    chunks = retrieve_above_threshold(question)
    if not chunks:
        return 'I could not find relevant information in the indexed documents.'
    context = format_chunks_for_prompt(chunks)
    return llm_call(QA_PROMPT.format(context=context, question=question))

Generowanie cytowań źródeł

Po wygenerowaniu odpowiedzi należy określić, które dokumenty źródłowe zostały zacytowane, i zwrócić je jako uporządkowaną listę. Ułatwia to użytkownikom znalezienie oryginalnych dokumentów w celu ich weryfikacji.

import re

def extract_citations(answer_text, chunks):
    # Find all [Source N] references in the answer
    cited_nums = set(int(m) for m in re.findall(r'\[Source (\d+)\]', answer_text))

    citations = []
    for num in sorted(cited_nums):
        idx = num - 1
        if idx < len(chunks):
            meta = chunks[idx]['metadata']
            citations.append({
                'source_num': num,
                'title':     meta.get('title', 'Unknown'),
                'page':      meta.get('page', 'N/A'),
                'file':      meta.get('source_file', '')
            })

    return citations

def answer_with_citations(question):
    chunks = retrieve_above_threshold(question)
    context = format_chunks_for_prompt(chunks)
    answer = llm_call(QA_PROMPT.format(context=context, question=question))
    citations = extract_citations(answer, chunks)
    return {'answer': answer, 'citations': citations}

Re-ranking za pomocą cross-encodera

Początkowe pobieranie wektorowe korzysta z bi-enkoderów (szybkich, przybliżonych). Cross-encoder ponownie szereguje najlepsze wyniki, oceniając wspólnie każdą parę (zapytanie, fragment) — jest dokładniejszy, ale wolniejszy. Takie dwuetapowe podejście poprawia jakość końcowej odpowiedzi.

# pip install sentence-transformers
from sentence_transformers import CrossEncoder

reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

def rerank_chunks(question, chunks, top_k=4):
    # Score each chunk against the question
    pairs = [(question, c['text']) for c in chunks]
    scores = reranker.predict(pairs)

    # Attach scores and re-sort
    scored_chunks = list(zip(scores, chunks))
    scored_chunks.sort(key=lambda x: x[0], reverse=True)

    top_chunks = [chunk for _, chunk in scored_chunks[:top_k]]
    print(f'Re-ranked {len(chunks)} chunks -> kept top {top_k}')
    return top_chunks

def answer_with_reranking(question):
    # Retrieve more initially
    initial_chunks = retrieve_relevant_chunks(question, n_results=12)
    # Re-rank for precision
    top_chunks = rerank_chunks(question, initial_chunks, top_k=4)
    context = format_chunks_for_prompt(top_chunks)
    return llm_call(QA_PROMPT.format(context=context, question=question))

Filtrowanie metadanych na poziomie dokumentu

Gdy użytkownik wskaże konkretny dokument lub zakres dat, należy zastosować filtrowanie na poziomie metadanych przed wyszukiwaniem osadzonych wektorów. Zapobiega to zanieczyszczaniu wyników przez nieistotne dokumenty.

def retrieve_filtered(question, filters=None, n_results=8):
    query_kwargs = {
        'query_texts': [question],
        'n_results': n_results,
        'include': ['documents', 'metadatas', 'distances']
    }

    # ChromaDB metadata filters
    # Example: {'doc_id': 'contract_2024', 'year': {'$gte': 2023}}
    if filters:
        query_kwargs['where'] = filters

    results = collection.query(**query_kwargs)
    return [
        {'text': t, 'metadata': m, 'relevance': 1 - d}
        for t, m, d in zip(
            results['documents'][0],
            results['metadatas'][0],
            results['distances'][0]
        )
    ]

# Example usage
chunks = retrieve_filtered(
    'What are the payment terms?',
    filters={'doc_id': {'$in': ['contract_a', 'contract_b']}}
)

Aktualizowanie indeksu o nowe dokumenty

Kolekcje dokumentów zmieniają się z czasem — dodawane są nowe pliki, a stare są aktualizowane. Potok indeksowania musi obsługiwać aktualizacje przyrostowe: dodawać nowe dokumenty, ponownie indeksować zaktualizowane i usuwać usunięte.

import os
import hashlib

# Track indexed documents by file hash
index_registry = {}  # {filepath: {hash, doc_id, indexed_at}}

def file_hash(filepath):
    with open(filepath, 'rb') as f:
        return hashlib.md5(f.read()).hexdigest()

def index_if_new_or_changed(filepath, title):
    fhash = file_hash(filepath)
    existing = index_registry.get(filepath)

    if existing and existing['hash'] == fhash:
        print(f'Skipping unchanged: {title}')
        return existing['doc_id']

    if existing:
        # Remove old chunks from vector store
        collection.delete(where={'doc_id': {'': existing['doc_id']}})
        print(f'Re-indexing updated: {title}')
    else:
        print(f'Indexing new: {title}')

    doc_id = hashlib.md5(filepath.encode()).hexdigest()[:8]
    index_document(doc_id, filepath, title)
    index_registry[filepath] = {'hash': fhash, 'doc_id': doc_id}
    return doc_id

def sync_document_directory(directory):
    pdf_files = [f for f in os.listdir(directory) if f.endswith('.pdf')]
    for fname in pdf_files:
        fpath = os.path.join(directory, fname)
        title = fname.replace('.pdf', '').replace('_', ' ').title()
        index_if_new_or_changed(fpath, title)
    print(f'Sync complete: {len(pdf_files)} files processed')

Sprawdzenie wiedzy

In a multi-document Q&A system using retrieval-augmented generation, what does the relevance threshold serve to prevent?

Podsumowanie: agenci pytań i odpowiedzi dotyczących wielu dokumentów

Pytania i odpowiedzi dotyczące wielu dokumentów: zindeksowanie wszystkich dokumentów (analiza → dzielenie na fragmenty → osadzanie → zapis z metadanymi) → pobranie istotnych fragmentów ze wszystkich dokumentów → sformatowanie z oznaczeniami źródeł → utworzenie odpowiedzi z cytowaniami.

Zaawansowane techniki obejmują porównywanie dokumentów przy pytaniach porównawczych, wykrywanie konfliktów za pomocą odpowiednio skonstruowanych promptów, filtrowanie według progu trafności, re-ranking za pomocą cross-encodera w celu zwiększenia precyzji oraz filtrowanie metadanych ograniczające zapytania do konkretnych dokumentów lub zakresów dat.

Często zadawane pytania

Czy lekcja „Agenci pytań i odpowiedzi dla wielu dokumentów” jest bezpłatna?

Tak — pełny tekst „Agenci pytań i odpowiedzi dla wielu dokumentów” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu AI Agents, przejdź na CoddyKit PRO. Kurs AI Agents zawiera 4 lekcji w sumie.

Co nauczysz się w „Agenci pytań i odpowiedzi dla wielu dokumentów”?

Indeksowanie korpusu dokumentów i odpowiadanie na pytania na podstawie wszystkich dokumentów. Ćwiczysz AI Agents z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć AI Agents?

Nie wymagamy żadnego doświadczenia. AI Agents w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Agenci pytań i odpowiedzi dla wielu dokumentów”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji AI Agents?

Tak. Każda lekcja AI Agents zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Parsowanie PDF za pomocą PyMuPDF i pdfplumber
  2. OCR dla skanowanych dokumentów
  3. Agenci pytań i odpowiedzi dla wielu dokumentów
  4. Klasyfikacja i routing dokumentów
← Powrót do AI Agents