Monen asiakirjan kysymys–vastaus-agentit
Asiakirjakokoelman indeksointi ja kysymyksiin vastaaminen kaikista asiakirjoista
Monen asiakirjan kysymys–vastaus-agentit on ilmainen Tekoälyagentit-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu Tekoälyagentit-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Tekoälyagentit-kurssilla on yhteensä 4 oppituntia.
Monen dokumentin kysymys-vastausjärjestelmän yleiskatsaus
Monen dokumentin kysymys-vastausagentti vastaa kysymyksiin hakemalla asiaankuuluvaa sisältöä N dokumentin kokoelmasta, muodostamalla vastauksen ja liittämällä jokaisen väitteen sen lähteeseen.
Yhden dokumentin kysymys-vastausjärjestelmästä poiketen monen dokumentin agenttien on käsiteltävä lähteiden ristiriitaisia tietoja ja pääteltävä, mitkä dokumentit ovat kysymyksen kannalta olennaisimpia.
Useiden dokumenttien indeksointi
Ennen kysymyksiin vastaamista kaikki dokumentit on indeksoitava: jäsennettävä, lohkottava, upotettava ja tallennettava vektoritietokantaan. Jokainen lohko tallennetaan metatietojen kanssa siten, että se voidaan yhdistää lähdedokumenttiinsa.
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}')Asiaankuuluvien lohkojen haku
Kysymyksen yhteydessä vektorivarastosta haetaan semanttisesti samankaltaisimmat lohkot kaikista indeksoiduista dokumenteista. n_results-parametri määrittää haettavien lohkojen määrän.
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 chunksLähteen ilmoittaminen kehotteessa
Kun haetut lohkot välitetään LLM:lle, jokainen lohko merkitään dokumenttilähteellään. Tämän jälkeen LLM voi viitata lähteisiin vastauksessa numeroiduilla viitteillä.
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))Dokumenttien välinen päättely
Joihinkin kysymyksiin vastaaminen edellyttää useiden dokumenttien tietojen yhdistämistä, ei vain yhden täsmäävän lohkon löytämistä. Esimerkiksi: ”Missä kolmesta sopimuksesta on pienin sopimussakko?”
Käyttäkää kaksivaiheista lähestymistapaa: hakekaa asiaankuuluvat lohkot kustakin dokumentista ja pyytäkää sitten LLM:ää vertailemaan ja yhdistämään niiden tiedot.
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}')Ristiriitaisten tietojen käsittely
Eri dokumenteissa voidaan esittää ristiriitaisia tietoja — yhden sopimuksen mukaan maksu erääntyy 30 päivän kuluttua ja toisen mukaan 60 päivän kuluttua. Agentin on tunnistettava ja tuotava ristiriidat esiin sen sijaan, että se valitsisi yhden vaihtoehdon kaikessa hiljaisuudessa.
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
))Relevanssikynnyksen mukainen suodatus
Kaikki haetut lohkot eivät ole aidosti asiaankuuluvia — vektorien samankaltaisuuteen liittyy kattavuuden ja täsmällisyyden välinen kompromissi. Asettamalla vähimmäisrelevanssikynnyksen voidaan poistaa heikosti täsmäävät lohkot, jotka saattaisivat johtaa LLM:ää harhaan.
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))Lähdeviitteiden muodostaminen
Vastauksen muodostamisen jälkeen poimitaan siinä viitatut lähdedokumentit ja palautetaan ne rakenteisena luettelona. Näin käyttäjät löytävät alkuperäiset dokumentit tarkistamista varten.
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}Uudelleenjärjestäminen cross-encoderilla
Alustavassa vektorinhaussa käytetään bi-enkoodereita (nopea, likimääräinen haku). Cross-encoder järjestää parhaat tulokset uudelleen pisteyttämällä jokaisen (kysely, lohko) -parin yhdessä — näin tulos on tarkempi, mutta hitaampi. Tämä kaksivaiheinen lähestymistapa parantaa lopullisen vastauksen laatua.
# 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))Dokumenttitason metatietosuodatus
Kun käyttäjä määrittää tietyn dokumentin tai ajanjakson, suodatus tehdään metatietotasolla ennen upotushakua. Näin asiaankuulumattomat dokumentit eivät vääristä tuloksia.
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']}}
)Indeksin päivittäminen uusilla dokumenteilla
Dokumenttikokoelmat muuttuvat ajan myötä: uusia tiedostoja lisätään ja vanhoja päivitetään. Indeksointiputken on tuettava inkrementaalisia päivityksiä: uudet dokumentit lisätään, päivitetyt indeksoidaan uudelleen ja poistetut poistetaan.
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')Tietotesti
Mitä relevanssikynnyksellä pyritään estämään RAG-menetelmää käyttävässä monen dokumentin kysymys-vastausjärjestelmässä?
Kertaus: monen dokumentin kysymys-vastausagentit
Monen dokumentin kysymys-vastausjärjestelmä: indeksoi kaikki dokumentit (jäsennä → lohko → upota → tallenna metatietojen kanssa) → hae asiaankuuluvat lohkot kaikista dokumenteista → muotoile ne lähdemerkinnöillä → muodosta vastaus lähdeviitteineen.
Edistyneitä tekniikoita ovat dokumenttien välinen vertailu vertailukysymyksissä, ristiriitojen tunnistamista ohjaavat kehotteet, relevanssikynnyksen mukainen suodatus, cross-encoderilla tehtävä uudelleenjärjestäminen täsmällisyyden parantamiseksi sekä metatietosuodatus kyselyiden rajaamiseksi tiettyihin dokumentteihin tai ajanjaksoihin.
Opi Tekoälyagentit tekoälytuutorin avulla — ilmaiseksi
Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.
- Kurssit
- 60
- Oppitunnit
- 239
Usein kysytyt kysymykset
Onko oppitunti ”Monen asiakirjan kysymys–vastaus-agentit” ilmainen?
Kyllä – oppitunnin ”Monen asiakirjan kysymys–vastaus-agentit” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko Tekoälyagentit-kurssin, päivitä CoddyKit PROhon. Tekoälyagentit-kurssilla on yhteensä 4 oppituntia.
Mitä opin oppitunnilla ”Monen asiakirjan kysymys–vastaus-agentit”?
Asiakirjakokoelman indeksointi ja kysymyksiin vastaaminen kaikista asiakirjoista Harjoittelet Tekoälyagentit-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.
Tarvitsenko kokemusta aloittaakseni Tekoälyagentit-opiskelun?
Aiempi kokemus ei ole tarpeen. CoddyKitin Tekoälyagentit-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.
Kuinka kauan ”Monen asiakirjan kysymys–vastaus-agentit”-oppitunnin suorittaminen kestää?
Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.
Voinko kirjoittaa ja suorittaa koodia tällä Tekoälyagentit-oppitunnilla?
Kyllä. Jokainen Tekoälyagentit-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.
Kaikki tämän kurssin oppitunnit
- PDF-tiedostojen jäsentäminen PyMuPDFilla ja pdfplumberilla
- Skannattujen asiakirjojen OCR
- Monen asiakirjan kysymys–vastaus-agentit
- Asiakirjojen luokittelu ja reititys