Verhindern Sie, dass Ihr RAG veraltete Dokumente bereitstellt: Überwachung von Aktualität und Veraltung
Verhindern Sie, dass RAG veraltete Dokumente bereitstellt: Ändern Sie die Erfassung, die Verfallsflaggen sowie die nach Aktualität gewichtete Neubewertung in Databricks Vector Search, damit die Agenten aktuelle Dokumente verwenden.
Quellen: Your RAG Treats a 3-Year-Old Doc the Same as Yesterday’s — Here’s How to Fix It
Nehmen wir an, ein RAG-System analysiert bereits PDFs, teilt sie in Blöcke auf, integriert sie, holt Kontext ab und behält sogar das Gesprächsgedächtnis über verschiedene Interaktionen hinweg. Dennoch kann sich ein weiteres Problem darin verbergen. Der Vektorindex behandelt alle Blöcke gleich – ein Firmware-Leitfaden aus dem Jahr 2022 und ein Hotfix-Bulletin vom vorigen Dienstag haben denselben Suchgewicht, solange beide semantisch zur Abfrage passen. Das System hat keine Ahnung davon, dass ein Dokument ersetzt wurde oder dass eine neuere Version Vorrang haben sollte.
In der Praxis führt dies zu erheblichen Schäden: veraltete Konformitätsverfahren, nicht mehr gültige API-Referenzen sowie alte Fehlerbehebungsschritte, die nicht mehr anwendbar sind. Das System liefert Informationen, die veraltet sind, mit voller Zuversicht – weil ihm nichts anderes mitgeteilt wurde.
Um dieses Problem zu beheben, sind drei Komponenten erforderlich, die alle in einen Databricks-Pipeline mit Vector Search und Delta Sync passen.
Drei Probleme auf drei Ebenen
Es scheint sich um ein einziges Problem zu handeln, doch es handelt sich um getrennte Herausforderungen an verschiedenen Stellen im Pipeline-Prozess:
- Einbezug von Änderungen. Neue und bearbeitete Dokumente müssen zum Index gelangen, ohne dass eine vollständige Neuberechnung erforderlich ist.
- Veraltetheit. Jeder Datenblock benötigt einen dokumentierten Zustand – aktuell, veraltet oder ersetzt – damit das System sie voneinander unterscheiden kann.
- Rangfolge. Wenn zwei Datenblöcke etwa gleich relevant sind, sollte der neuere Vorrang haben.
Problem 1: Einfügung neuer Inhalte
Mit Databricks Vector Search auf Delta Sync existiert bereits die Infrastruktur für inkrementelle Aktualisierungen. Delta Sync folgt dem Change Data Feed der Quelltabelle und überträgt Einfüge, Aktualisierungen und Löschungen in den Index.
Die wichtigste Entscheidung betrifft den Typ des Pipelines. Im auslösbaren Modus entscheiden Sie selbst, wann synchronisiert werden soll:
# Manual control — you decide when to sync
vsc.create_delta_sync_index(
...
pipeline_type="TRIGGERED",
)
# After appending new chunks:
index = vsc.get_index(index_name="<YOUR_INDEX>")
index.sync() # Only processes CDF deltas, not full re-index
Im kontinuierlichen Modus aktualisiert sich der Index innerhalb weniger Minuten nach Änderungen in der Tabelle:
# Automatic — syncs within minutes of table changes
vsc.create_delta_sync_index(
...
pipeline_type="CONTINUOUS",
)
# No manual sync() calls needed
Bei den meisten Produktionsumgebungen bietet der auslösende Modus mehr Kontrolle. Sie können eine Gruppe von Dokumenten aufnehmen und einmal synchronisieren, anstatt für jede Zeile einen Index-Update auszulösen.
Der Ablauf bei der Aufnahme neuer oder geänderter Dokumente sieht wie folgt aus:
from pyspark.sql import functions as F
from pyspark.sql.functions import expr
def ingest_new_documents(volume_path, chunked_table, parsed_table, doc_version="2.0"):
"""Parse new PDFs, chunk them, append to the chunks table.
Existing chunks from updated docs are soft-deleted first."""
# Read new files - binaryFile gives us modificationTime
new_docs_df = spark.read.format("binaryFile").load(volume_path)
# Check which files already exist in the table
existing_paths = (
spark.read.table(chunked_table)
.filter("is_active = true")
.select("path").distinct().collect()
)
existing_set = {row["path"] for row in existing_paths}
all_paths = [r["path"] for r in new_docs_df.select("path").distinct().collect()]
updated_paths = [p for p in all_paths if p in existing_set]
# Soft-delete chunks from updated documents
if updated_paths:
path_list = ", ".join([f"'{p}'" for p in updated_paths])
spark.sql(f"""
UPDATE {chunked_table}
SET is_active = false, superseded_by = '{doc_version}'
WHERE path IN ({path_list}) AND is_active = true
""")
# Parse with ai_parse_document v2.0
parsed_df = new_docs_df.withColumn(
"parsed_content",
expr(f"""ai_parse_document(content, map(
"version", "2.0",
"imageOutputPath", "{volume_path}/parsed_images/"
))""")
).drop("content")
# ... your chunking logic here (RecursiveCharacterTextSplitter) ...
# produces df_chunks with [path, chunk]
# Enrich with ACTUAL file modification time (not current_timestamp!)
mod_times_df = parsed_df.select("path", "modificationTime").distinct()
df_chunks_enriched = (
df_chunks
.join(mod_times_df, on="path", how="left")
.withColumn("id", F.monotonically_increasing_id()
+ spark.read.table(chunked_table).count())
.withColumn("ingested_at", F.current_timestamp())
.withColumn("source_modified_at", F.col("modificationTime"))
.withColumn("doc_version", F.lit(doc_version))
.withColumn("is_active", F.lit(True))
.withColumn("superseded_by", F.lit(None).cast("string"))
.drop("modificationTime")
)
# Append - CDF picks up the changes automatically
df_chunks_enriched.write.mode("append").saveAsTable(chunked_table)
return df_chunks_enriched
Betrachten Sie genau die Spalte source_modified_at. Es handelt sich dabei nicht um current_timestamp(), sondern um die tatsächliche Dateiänderungszeit, die vom binaryFile-Leser bereitgestellt wird. Dieser Unterschied ist für die Bewertung der Aktualität von großer Bedeutung, denn ein Dokument, das vor drei Monaten bearbeitet wurde, sollte anders bewertet werden als eines, das gestern bearbeitet wurde – unabhängig davon, wann der Eingabeprozess ausgeführt wurde.
Problem 2: Überwachung der Veraltetheit
Bei einer reinen Ähnlichkeitssuche wird nicht erkannt, ob ein Datensatz veraltet ist. Die Tabelle mit den Datensätzen benötigt Metadatumsäulen, die den Lebenszyklus jedes Inhaltsstücks erfassen.
Das Schema
Die Tabelle wächst von drei auf acht Spalten an:
id | path | chunk | ingested_at | source_modified_at | doc_version | is_active | superseded_by
ingested_atdokumentiert, wann der Datensatz in den Pipeline eingefügt wurde, als Prüfungsverlauf.source_modified_atdokumentiert, wann die Quelldatei tatsächlich geändert wurde, und dient zur Bewertung der Aktualität.doc_versionist eine Versionsbezeichnung für das Dokument.is_activeist ein Flag für eine sanfte Löschung, wobei „false“ bedeutet, dass der Datensatz von der Abfrage ausgeschlossen wird.superseded_bygibt den neueren Datensatz an, der diesen ersetzt hat.
Hinzufügen der Spalten zu einer vorhandenen Tabelle
Eine bereits vorhandene Tabelle, wie z. B. docs_chunked, kann diese Spalten ohne Neubau aufnehmen, und source_modified_at kann aus den tatsächlichen Zeitstempeln der Dateien in der Tabelle „parsed-documents“ nachgefüllt werden:
parsed_table = f"{catalog}.{schema}.docs_parsed"
chunked_table = f"{catalog}.{schema}.docs_chunked"
# Add columns
spark.sql(f"""
ALTER TABLE {chunked_table}
ADD COLUMNS (
ingested_at TIMESTAMP,
source_modified_at TIMESTAMP,
doc_version STRING,
is_active BOOLEAN,
superseded_by STRING
)
""")
# Backfill source_modified_at from actual file timestamps
# docs_parsed has modificationTime from the binaryFile reader
spark.sql(f"""
MERGE INTO {chunked_table} AS chunks
USING (
SELECT DISTINCT path, modificationTime
FROM {parsed_table}
) AS parsed
ON chunks.path = parsed.path
WHEN MATCHED AND chunks.ingested_at IS NULL THEN
UPDATE SET
chunks.ingested_at = current_timestamp(),
chunks.source_modified_at = parsed.modificationTime,
chunks.doc_version = '1.0',
chunks.is_active = true,
chunks.superseded_by = null
""")
Lebenszyklusverwaltung
Sobald diese Spalten vorhanden sind, ist die Verwaltung der Frische ein einfacher SQL-Befehl:
# Soft-delete an outdated document
spark.sql(f"""
UPDATE {chunked_table}
SET is_active = false
WHERE path LIKE '%Firmware_Guide_v5%'
""")
# Supersede v6 with v7
spark.sql(f"""
UPDATE {chunked_table}
SET is_active = false, superseded_by = 'Firmware_Guide_v7.pdf'
WHERE path LIKE '%Firmware_Guide_v6%' AND is_active = true
""")
# Staleness report
display(spark.sql(f"""
SELECT path, doc_version, source_modified_at,
datediff(current_date(), source_modified_at) as age_days,
CASE
WHEN datediff(current_date(), source_modified_at) < 30
THEN '🟢 Fresh'
WHEN datediff(current_date(), source_modified_at) < 90
THEN '🟡 Aging'
ELSE '🔴 Stale'
END as freshness
FROM {chunked_table}
WHERE is_active = true
ORDER BY source_modified_at DESC
"""))
Index neu erstellen
Nach dem Hinzufügen von Spalten muss der Index neu erstellt werden, damit die neuen Metadaten synchronisiert werden:
vsc.create_delta_sync_index_and_wait(
endpoint_name="<YOUR_VS_ENDPOINT>",
index_name="<YOUR_INDEX>",
source_table_name=chunked_table,
primary_key="id",
embedding_source_column="chunk",
embedding_model_endpoint_name="databricks-gte-large-en",
pipeline_type="TRIGGERED",
columns_to_sync=[
"id", "chunk", "path",
"ingested_at", "source_modified_at",
"doc_version", "is_active", "superseded_by"
],
)
Von nun an enthält die Vektorabfrage die Aktualitätsfelder und kann inaktivere Zeilen bei einem Eingabefrage ausschließen.
Problem 3: Bevorzugung neuerer Inhalte
Hier wird es interessant. Allein die Flags lassen die Rangfolge der aktiven Blöcke rein semantisch bestimmen. Ein Konformitätsdokument aus dem Jahr 2023 und ein aktualisiertes Dokument vom letzten Monat, beide als aktiv markiert, erhalten dieselbe Behandlung.
Die Lösung besteht darin, mehr Daten zu holen, mithilfe der Aktualität erneut zu rangieren und nur die besten Ergebnisse zurückzugeben.
Die Bewertung
Ein exponentieller Abfall mit einer einstellbaren Halbwertszeit erledigt die Aufgabe:
import math
from datetime import datetime
def recency_score(timestamp_str, half_life_days=90):
"""
Exponential decay scoring based on content age.
- Age 0 days → score 1.0
- Age = half_life → score 0.5
- Age = 2× half_life → score 0.25
"""
try:
if isinstance(timestamp_str, str):
parsed_ts = datetime.fromisoformat(
timestamp_str.replace("Z", "+00:00")
)
else:
parsed_ts = timestamp_str
age_days = (datetime.now(parsed_ts.tzinfo) - parsed_ts).days
return math.exp(-0.693 * age_days / half_life_days)
except Exception:
return 0.5
Der Wert half_life_days ist die wichtigste Einstellmöglichkeit und sollte dem jeweiligen Bereich entsprechen. Verwenden Sie etwa 30 Tage für schnell ändernden Inhalt wie Release-Notes und Incidents-Berichte, 90 Tage für mäßig häufig geänderten Material wie technische Dokumentationen sowie 365 Tage für langsam ändernden Inhalt wie Architekturspezifikationen und Konformitätsstandards.
Eine benutzerdefinierte Abrufwerkzeug
Ersetzen Sie das standardmäßige VectorSearchRetrieverTool durch ein benutzerdefiniertes LangChain-Tool, das diese Aktualitätslogik umsetzt:
from langchain.tools import tool
from databricks.vector_search.client import VectorSearchClient
@tool
def knowledge_search(query: str) -> str:
"""Search knowledge base with recency-weighted ranking.
Newer documents are prioritized over older ones."""
vsc = VectorSearchClient(disable_notice=True)
index = vsc.get_index(index_name="<YOUR_INDEX>")
# Over-fetch: get 15 results for re-ranking
results = index.similarity_search(
query_text=query,
columns=["chunk", "path", "source_modified_at", "doc_version"],
filters={"is_active": True}, # Only active chunks
num_results=15,
)
data = results.get("result", {}).get("data_array", [])
if not data:
return "No relevant documents found."
# Re-rank: combined = 70% similarity + 30% recency
scored = []
for row in data:
chunk, path, modified_at, version, similarity = (
row[0], row[1], row[2], row[3], row[4]
)
recency = recency_score(modified_at, half_life_days=90)
combined = similarity * 0.7 + recency * 0.3
scored.append({
"chunk": chunk, "path": path,
"doc_version": version, "combined_score": combined,
})
scored.sort(key=lambda x: x["combined_score"], reverse=True)
# Return top 5 with source attribution
parts = []
for r in scored[:5]:
parts.append(
f"[Source: {r['path']} | Version: {r['doc_version']}]\n{r['chunk']}"
)
return "\n\n---\n\n".join(parts)
Dann übergeben Sie es dem Agenten:
agent = create_agent(
model=model,
tools=[knowledge_search], # Custom tool instead of VectorSearchRetrieverTool
system_prompt=system_prompt,
checkpointer=checkpointer,
)
Konfigurieren über YAML
Anstatt die Gewichte fest einzuschreiben, speichern Sie sie im Konfigurationsfile des Agents:
llm_endpoint_name: databricks-gpt-oss-120b
vector_search:
index_name: <catalog>.<schema>.docs_chunked_index
lakebase:
host: <your-lakebase-host>
endpoint: projects/<project>/branches/production/endpoints/primary
user: <your-email>
retrieval:
overfetch_count: 15
final_count: 5
similarity_weight: 0.7
recency_weight: 0.3
half_life_days: 90
Einige Vorlagen, die für verschiedene Bereiche geeignet sind:
# Release notes / changelogs (fast-changing)
retrieval:
similarity_weight: 0.5
recency_weight: 0.5
half_life_days: 30
# Architecture / compliance docs (slow-changing)
retrieval:
similarity_weight: 0.85
recency_weight: 0.15
half_life_days: 365
# Mixed knowledge base (balanced)
retrieval:
similarity_weight: 0.7
recency_weight: 0.3
half_life_days: 90
Wie die Schichten zusammenpassen
Wenn ein neuer PDF in einem Unity Catalog-Volumen ankommt, liest ingest_new_documents() ihn mit binaryFile ein, wodurch die Änderungszeit erfasst wird. ai_parse_document() extrahiert Text, Tabellen und Abbildungen. Ein rekursiver Splitter teilt den Text in Abschnitte auf. Ältere Abschnitte aus demselben Pfad werden durch Setzen von is_active = false sanft gelöscht. Neue Abschnitte werden mit der tatsächlichen source_modified_at-Zeit hinzugefügt, und index.sync() überträgt die Änderung über den Change Data Feed.
Wenn ein Benutzer eine Anfrage stellt, fragt das benutzerdefinierte Tool Vector Search mit einem Filter auf is_active ab, holt 15 Kandidaten, bewertet jeden als Ähnlichkeitswert mal 0,7 plus Aktualitätswert mal 0,3 und übermittelt die fünf besten an das Modell zusammen mit Quell- und Versionsbezeichnungen. Das Modell antwortet anschließend und gibt neueren Inhalten Vorrang.
Administratoren verwalten den Lebenszyklus mit SQL: UPDATE ... SET is_active = false zur Außerbetriebnahme veralteter Dokumente, superseded_by zur Erfassung von Versionsketten sowie einem Aktualitätsbericht, um die Altersverteilung des Inhalts zu sehen.
Was ändert sich in der Praxis?
Vor der Gewichtung nach Aktualität könnte eine Abfrage wie „Wie verläuft das Verfahren zur Firmware-Update?“ Teile aus drei Versionen der Firmware-Anleitung zurückgeben. Alle sind semantisch relevant, doch nur die neueste enthält die aktuellen Schritte, wodurch der Agent v5, v6 und v7 zu einer einzigen Antwort kombinieren könnte.
Nach Hinzufügen des is_active-Filters und der Bewertung nach source_modified_at:
- v5-Teile sind inaktiv und werden vor der Abrufung ausgefiltert.
- v6-Teile sind inaktiv, als durch v7 ersetzt markiert und ausgefiltert.
Der Agent antwortet nun mit der aktuellen Version und zeigt genau an, welches Dokument er verwendet hat.
Lektionen, die man beibehalten sollte
- Verwenden Sie die tatsächliche Änderungszeit der Datei, nicht die Zeit des Einreichens. Ein Dokument, das vor drei Monaten geändert wurde, sollte wie drei Monate alt aussehen und nicht als „heute erst eingereicht“ gelten, nur weil der Pipeline-Prozess heute Morgen ausgeführt wurde.
- Verwenden Sie eine sanfte Löschung statt einer vollständigen Löschung. Durch das Setzen von
is_active = falsekönnen Sie nachverfolgen, was und wann entfernt wurde, während eine vollständige Löschung die Historie verliert. - Passen Sie die Halbwertszeit an den jeweiligen Anwendungsbereich an. Dreißig Tage eignen sich für Incidentsberichte, 90 Tage für technische Dokumente und 365 Tage für Architekturspezifikationen. Es gibt keinen universellen Standard, da dies von der Geschwindigkeit, mit der der Inhalt sich ändert, abhängt.
- Die Auswahl der Gewichte ist einfach. Herausfordernd ist es,
source_modified_atkorrekt einzustellen und einen konsistenten Lebenszyklus zu etablieren: Wer entscheidet, wann ein Dokument ersetzt wird, wie Versionen verfolgt werden und wann der Veraltungsbericht ausgeführt wird. Der Code selbst ist einfach, doch der um ihn herumliegende Prozess sorgt dafür, dass er in der Produktion funktioniert.
Die Databricks-Dokumentation zu Vector Search Delta Sync, ai_parse_document, Change Data Feed sowie der Suchqualität behandelt die zugrundeliegenden Funktionen.