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 chunksAtribusi 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
- Mengurai PDF dengan PyMuPDF dan pdfplumber
- OCR untuk Dokumen Hasil Pemindaian
- Agen Tanya Jawab Multidokumen
- Klasifikasi dan Perutean Dokumen