Publicado el 6 min de lectura

Comprueba citas textuales de IA con fuentes locales y Python

En esta página

Un asistente de IA cita tu política de devoluciones y presenta una frase entre comillas. Antes de reenviar la respuesta, comprueba si esa frase aparece realmente en la fuente citada. Un identificador de documento verosímil no basta: el modelo puede haber cambiado el texto o vinculado una referencia equivocada.

Crea un filtro local de citas con Python 3.9 o posterior, sin paquetes adicionales. Comprueba identificadores de fuente, presencia de citas normalizadas y coincidencias ambiguas. Todos los ejemplos usan texto sintético: prueban el verificador, no la precisión de un modelo.

Unas comillas turquesas conectan fragmentos resaltados coincidentes en dos documentos, junto a una cita ámbar sin correspondencia.

1. Conserva las fuentes y solicita registros de citas

Guarda el texto revisado que recibió el asistente, con identificadores versionados como returns-v1. Conserva el archivo original, la fecha de consulta y un hash SHA-256 de sus bytes en el registro de ejecución. Una página web que cambia no es una referencia reproducible. Limita este registro a las fuentes que el usuario actual tiene permiso para leer.

Pide al asistente un registro por cita: citation_id, source_id y quote. Exige un fragmento copiado, no una paráfrasis. Conserva también la frase de la respuesta para revisarla manualmente. Si recibes JSON, aplica primero el análisis estricto y la validación de campos; las tuplas siguientes son datos de prueba ya estructurados, no un lector de JSON.

Esta comprobación va después de la recuperación, no la sustituye. El tutorial de evaluación de recuperación RAG ayuda a determinar si las pruebas necesarias llegaron al asistente.

2. Localiza citas sin borrar diferencias importantes

Guarda este código como check_citations.py en un directorio nuevo:

import re
import unicodedata

sources = {
    "returns-v1": "Returns are accepted within 30 days.\nA receipt is required.",
    "help-v1": "Contact support. Read the guide. Contact support.",
    "cafe-v1": "Café orders close at noon.",
}
citations = [
    ("c1", "returns-v1", "Returns are accepted\nwithin 30 days."),
    ("c2", "returns-v1", "A receipt is not required."),
    ("c3", "missing-v1", "A receipt is required."),
    ("c4", "help-v1", "Contact support."),
    ("c5", "returns-v1", "   "),
]


def normalize(text):
    return " ".join(unicodedata.normalize("NFC", text).split())


def check(source_id, quote):
    if not isinstance(source_id, str) or not isinstance(quote, str):
        return "invalid fields"
    if source_id not in sources:
        return "unknown source"
    needle = normalize(quote)
    if not needle:
        return "empty quote"
    text = normalize(sources[source_id])
    pattern = re.escape(needle)
    if re.match(r"\w", needle[0]):
        pattern = r"(?<!\w)" + pattern
    if re.match(r"\w", needle[-1]):
        pattern += r"(?!\w)"
    hits = sum(1 for _ in re.finditer("(?=" + pattern + ")", text))
    if hits == 0:
        return "quote not found"
    return "located" if hits == 1 else "ambiguous quote"


def main():
    reasons = [check(source_id, quote) for _, source_id, quote in citations]
    for (citation_id, _, _), reason in zip(citations, reasons):
        print(f"{citation_id}: {reason}")
    print(f"located={reasons.count('located')}/{len(reasons)}")
    return int(not reasons or any(reason != "located" for reason in reasons))


if __name__ == "__main__":
    raise SystemExit(main())

La documentación de Unicode de Python explica la normalización NFC, que permite comparar representaciones canónicamente equivalentes de caracteres acentuados. Usar split() sin separador reduce las secuencias de espacios en blanco. Es una política explícita para prosa corriente, no una comparación byte a byte; evítala cuando la disposición tenga significado, como en código fuente o tablas.

Se conservan mayúsculas, puntuación, cifras y negaciones. Las comprobaciones condicionales de límites de palabra rechazan 7 days dentro de 17 days y paid dentro de unpaid. Son comprobaciones conservadoras de caracteres, no una segmentación lingüística universal. La anticipación también cuenta coincidencias superpuestas. Una aparición única recibe located, deliberadamente no true ni supported.

3. Prueba el rechazo y la corrección

Ejecuta:

python3 check_citations.py

Salida estándar esperada:

c1: located
c2: quote not found
c3: unknown source
c4: ambiguous quote
c5: empty quote
located=1/5

El código de salida es 1, con stderr vacío: cuatro registros necesitan revisión. Es intencional, no un fallo del programa. Una lista de citas vacía también falla, para que la ausencia de datos no parezca un éxito.

Después, ejecuta las pruebas de casos límite y sustituye los registros en memoria por citas correctas:

python3 - <<'PYTEST'
import check_citations as demo

assert demo.check("cafe-v1", "Cafe\u0301 orders close at noon.") == "located"
assert demo.check("returns-v1", "within\t30 days.") == "located"
assert demo.check("returns-v1", "0 days") == "quote not found"
assert demo.check("returns-v1", "a receipt is required.") == "quote not found"
assert demo.check("returns-v1", "A receipt is required!") == "quote not found"
assert demo.check("returns-v1", None) == "invalid fields"
assert demo.check([], "anything") == "invalid fields"
assert demo.check("returns-v1", "\t\n") == "empty quote"
assert demo.check("help-v1", "Contact support.") == "ambiguous quote"
demo.sources["edge-v1"] = "17 days unpaid"
assert demo.check("edge-v1", "7 days") == "quote not found"
assert demo.check("edge-v1", "paid") == "quote not found"
demo.sources["overlap-v1"] = "!!!"
assert demo.check("overlap-v1", "!!") == "ambiguous quote"
print("edge checks passed")
demo.citations = [
    ("c1", "returns-v1", "Returns are accepted within 30 days."),
    ("c2", "cafe-v1", "Café orders close at noon."),
]
raise SystemExit(demo.main())
PYTEST

Salida estándar esperada:

edge checks passed
c1: located
c2: located
located=2/2

Este comando termina con código 0, con stderr vacío. Ambos comandos se comprobaron localmente con estos datos de prueba. Las aserciones cubren acentos, espacios en blanco, límites de palabras, coincidencias superpuestas, mayúsculas, puntuación y tipos de campo inválidos. La ejecución correcta no reescribe el script guardado ni las fuentes originales.

4. Resuelve el problema en el límite adecuado

  • Fuente desconocida: usa el identificador versionado exacto del registro; nunca lo sustituyas silenciosamente por un documento más reciente.
  • Cita no encontrada: compara el fragmento original. La extracción de PDF, los errores de OCR o las comillas tipográficas pueden explicar una diferencia. Corrige la extracción o revisa manualmente, en lugar de eliminar toda la puntuación.
  • Cita ambigua: solicita un fragmento más largo. El Text Quote Selector del W3C ilustra el uso del texto anterior y posterior para distinguir pasajes repetidos. Esta demostración rechaza la ambigüedad en vez de implementar ese modelo.
  • Todos los registros se localizan, pero la respuesta es incorrecta: examina el párrafo completo y la afirmación que supuestamente respalda. Una subcadena puede omitir una excepción o una negación cercana; su presencia no demuestra que respalde la afirmación, siga vigente o sea cierta.

No descargues automáticamente las URL que proporcione el modelo en este filtro. Resuelve los identificadores aprobados localmente, conserva el texto original para revisarlo y no interpretes las posiciones de caracteres normalizados como posiciones del archivo original. Añade comprobaciones independientes para afirmaciones sin cita y cobertura de fuentes obligatorias. Para cifras copiadas, el verificador de números en resúmenes comprueba unidades y períodos que la presencia de una cita no puede evaluar por sí sola.

Conclusión práctica

Exige registros de citas rastreables, conserva las pruebas y rechaza fragmentos desconocidos, ausentes o ambiguos antes de reenviar una respuesta. Un resultado correcto reduce el trabajo de revisión; no elimina la necesidad de comprobar qué significa la cita en su contexto.

Artículos relacionados

Últimos artículos