0Pricing
AI Agents · Pelajaran

Agen Tanya Jawab Multidokumen

Indeks korpus dokumen dan jawab pertanyaan berdasarkan seluruh dokumen.

Agen Tanya Jawab Multidokumen adalah pelajaran AI Agents gratis di CoddyKit. Ini adalah pelajaran 3 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar AI Agents, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus AI Agents mencakup 4 pelajaran total.

Ikhtisar Tanya Jawab Multi-Dokumen

Agen tanya jawab multi-dokumen menjawab pertanyaan dengan mengambil konten yang relevan dari koleksi N dokumen, menyusun jawaban, dan mengaitkan setiap pernyataan dengan sumbernya.

Berbeda dari tanya jawab satu dokumen, agen multi-dokumen harus menangani informasi yang bertentangan antarsumber dan menalar dokumen mana yang paling relevan dengan pertanyaan.

Mengindeks Banyak Dokumen

Sebelum menjawab pertanyaan apa pun, semua dokumen harus diindeks: diurai, dibagi menjadi potongan, disematkan, dan disimpan dalam basis data vektor. Setiap potongan disimpan bersama metadata yang mengaitkannya kembali dengan dokumen sumber.

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}')

Mengambil Potongan yang Relevan

Saat menerima pertanyaan, kueri penyimpanan vektor untuk mendapatkan potongan yang paling mirip secara semantik dari semua dokumen yang telah diindeks. Parameter n_results menentukan jumlah potongan yang akan diambil.

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

Atribusi Sumber dalam Prompt

Saat meneruskan potongan yang diambil kepada LLM, beri label pada setiap potongan dengan sumber dokumennya. LLM kemudian dapat mengutip sumber menggunakan rujukan bernomor dalam jawaban.

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))

Penalaran Lintas Dokumen

Beberapa pertanyaan memerlukan penyusunan informasi dari beberapa dokumen — bukan sekadar menemukan satu potongan yang cocok. Contohnya: "Manakah dari ketiga kontrak tersebut yang memiliki klausul penalti paling rendah?"

Gunakan pendekatan dua langkah: ambil potongan yang relevan dari setiap dokumen, lalu minta LLM membandingkan dan menyusunnya.

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}')

Menangani Informasi yang Bertentangan

Dokumen yang berbeda mungkin menyatakan fakta yang bertentangan — satu kontrak menyatakan bahwa pembayaran jatuh tempo dalam 30 hari, sedangkan kontrak lain menyatakan 60 hari. Agen harus mendeteksi dan menampilkan konflik ini, bukan diam-diam memilih salah satunya.

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
    ))

Penyaringan Berdasarkan Ambang Batas Relevansi

Tidak semua potongan yang diambil benar-benar relevan — kemiripan vektor memiliki kompromi antara cakupan dan ketepatan. Tetapkan ambang batas relevansi minimum untuk mengecualikan potongan yang hanya memiliki kecocokan lemah dan dapat menyesatkan LLM.

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))

Membuat Sitasi Sumber

Setelah menghasilkan jawaban, ekstrak dokumen sumber yang dikutip dan kembalikan sebagai daftar terstruktur. Hal ini membantu pengguna menemukan dokumen asli untuk verifikasi.

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}

Pengurutan Ulang dengan Pengode Silang

Pengambilan vektor awal menggunakan pengode ganda (cepat, bersifat perkiraan). Pengode silang mengurutkan ulang hasil teratas dengan menilai setiap pasangan (kueri, potongan) secara bersamaan — lebih akurat, tetapi lebih lambat. Pendekatan dua tahap ini meningkatkan kualitas jawaban akhir.

# 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))

Penyaringan Metadata Tingkat Dokumen

Saat pengguna menentukan dokumen atau rentang tanggal tertentu, lakukan penyaringan pada tingkat metadata sebelum pencarian penyematan. Hal ini mencegah dokumen yang tidak relevan mengotori hasil.

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']}}
)

Memperbarui Indeks dengan Dokumen Baru

Koleksi dokumen berubah seiring waktu — berkas baru ditambahkan dan berkas lama diperbarui. Alur pemrosesan pengindeksan harus mendukung pembaruan bertahap: menambahkan dokumen baru, mengindeks ulang dokumen yang diperbarui, dan menghapus dokumen yang telah dihapus.

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')

Pemeriksaan Pengetahuan

Dalam sistem tanya jawab multi-dokumen yang menggunakan pembangkitan berbantuan pengambilan, apa yang dicegah oleh ambang batas relevansi?

Rangkuman: Agen Tanya Jawab Multi-Dokumen

Tanya jawab multi-dokumen: indeks semua dokumen (urai → potong → sematkan → simpan dengan metadata) → ambil potongan yang relevan dari semua dokumen → format dengan label sumber → susun jawaban dengan sitasi.

Teknik lanjutan: perbandingan lintas dokumen untuk pertanyaan komparatif, pemberian prompt untuk mendeteksi konflik, penyaringan berdasarkan ambang batas relevansi, pengurutan ulang dengan pengode silang untuk ketepatan, dan penyaringan metadata untuk membatasi kueri pada dokumen atau rentang tanggal tertentu.

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Agen Tanya Jawab Multidokumen” gratis?

Ya — teks lengkap “Agen Tanya Jawab Multidokumen” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus AI Agents, upgrade ke CoddyKit PRO. Kursus AI Agents mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Agen Tanya Jawab Multidokumen”?

Indeks korpus dokumen dan jawab pertanyaan berdasarkan seluruh dokumen. Kamu berlatih AI Agents dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai AI Agents?

Tidak diperlukan pengalaman sebelumnya. AI Agents di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 3 dari 4.

Berapa lama pelajaran “Agen Tanya Jawab Multidokumen” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran AI Agents ini?

Ya. Setiap pelajaran AI Agents menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Mengurai PDF dengan PyMuPDF dan pdfplumber
  2. OCR untuk Dokumen Hasil Pemindaian
  3. Agen Tanya Jawab Multidokumen
  4. Klasifikasi dan Perutean Dokumen
← Kembali ke AI Agents