Para construir un verificador de datos en tiempo real que ancle un LLM en busqueda en vivo, divide el texto entrante en afirmaciones individuales, conserva solo las que vale la pena comprobar (numeros, entidades con nombre, afirmaciones politicas o estadisticas), busca cada una en resultados de Google en vivo y luego pide al modelo que dictamine si esta respaldada, contradicha o no verificable, citando los enlaces reales de los resultados. El paso de anclaje es lo que detiene las citas inventadas de las que tanta gente se queja. Como dijo un desarrollador tras conectar una API de busqueda de Google a su pipeline: la uso precisamente porque el modelo tiende a inventar fuentes que no existen. En cuanto cada veredicto tiene que apuntar a una URL real que el modelo acaba de recuperar, las citas falsas casi desaparecen. Este tutorial te da un pipeline ejecutable: extraccion de afirmaciones, un filtro de relevancia, un POST al endpoint /api/v1/google de Scavio por afirmacion y un prompt de juicio estricto que devuelve veredicto y citas. Cuesta 1 credito (0,005 USD) por afirmacion comprobada, asi que una transcripcion de 40 frases con 12 afirmaciones relevantes sale por unos 0,06 USD. Se honesto sobre lo que esto hace y lo que no: el anclaje en SERP elimina las citas inventadas y mantiene el modelo actualizado, pero no dictamina la verdad. Sigues necesitando filtrar la calidad de las fuentes y aplicar criterio humano, y una afirmacion que simplemente no aparece en la busqueda es no verificable, no falsa.
Requisitos previos
- Una clave de API de Scavio (el registro gratuito da 50 creditos para probar)
- Una clave de API de un LLM (cualquier modelo de chat con respuesta en JSON sirve)
- Python 3.9+ o Node 18+
- Familiaridad basica con peticiones HTTP y JSON
Guia paso a paso
Paso 1: Divide el texto en afirmaciones atomicas
Separa la transcripcion en frases y luego en afirmaciones atomicas. Una frase puede contener varias afirmaciones; mantenlas separadas para que cada una tenga su propia busqueda. Un corte por frases basta para empezar; mas adelante puedes usar un divisor basado en modelo si necesitas mas precision.
import re
def split_claims(text: str) -> list[str]:
sentences = re.split(r'(?<=[.!?])\s+', text.strip())
return [s.strip() for s in sentences if len(s.strip()) > 0]Paso 2: Conserva solo las afirmaciones que vale la pena comprobar
La mayoria de las frases no merecen comprobacion. Quedate con las que hacen una afirmacion falsable: numeros, porcentajes, fechas, entidades con nombre o afirmaciones politicas y estadisticas. Este filtro es lo que mantiene bajo tu gasto en creditos, porque solo buscas afirmaciones que de verdad pueden verificarse.
import re
NUM = re.compile(r'\d')
ENTITY = re.compile(r'\b[A-Z][a-z]+\b')
KEYWORDS = ('percent', '%', 'billion', 'million', 'increased', 'decreased', 'banned', 'passed', 'voted', 'rate', 'tax', 'budget')
def is_check_worthy(claim: str) -> bool:
has_number = bool(NUM.search(claim))
has_entity = bool(ENTITY.search(claim))
has_keyword = any(k in claim.lower() for k in KEYWORDS)
return (has_number or has_keyword) and has_entityPaso 3: Busca cada afirmacion en Google en vivo
Envia el texto de la afirmacion con POST a /api/v1/google. Toma los primeros resultados organicos y las entradas de people_also_ask; esos fragmentos son la evidencia sobre la que razonara el modelo. Cada llamada cuesta 1 credito cuando light_request es true, y 2 creditos con el conjunto completo de funciones del SERP.
import os, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}", "Content-Type": "application/json"}
def retrieve(claim: str) -> list[dict]:
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": claim, "light_request": True})
data = r.json()
evidence = []
for row in data.get("organic_results", [])[:5]:
evidence.append({"title": row["title"], "snippet": row.get("snippet", ""), "link": row["link"]})
for paa in data.get("people_also_ask", [])[:3]:
evidence.append({"title": paa.get("question", ""), "snippet": paa.get("snippet", ""), "link": paa.get("link", "")})
return evidencePaso 4: Dictamina la afirmacion frente a la evidencia
Pasa la afirmacion junto con los fragmentos recuperados a un LLM con un prompt estricto: que devuelva respaldada, contradicha o no verificable, una razon de una linea y citas tomadas solo de los enlaces que le pasaste. La regla nunca inventes una fuente es el nucleo de todo. Si la evidencia no cubre la afirmacion, la respuesta es no verificable.
PROMPT = '''You are a fact-checking judge. Given a CLAIM and EVIDENCE (search snippets with links), return strict JSON:
{"verdict": "supported|contradicted|unverifiable", "reason": "one sentence", "citations": ["<link from evidence>"]}
Rules: cite ONLY links present in EVIDENCE. Never invent a source. If evidence does not address the claim, verdict is "unverifiable".
CLAIM: {claim}
EVIDENCE: {evidence}'''
def judge(claim, evidence, llm):
msg = PROMPT.format(claim=claim, evidence=evidence)
return llm.complete(msg, response_format="json")Ejemplo en Python
import os, re, json, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}", "Content-Type": "application/json"}
NUM = re.compile(r'\d')
ENTITY = re.compile(r'\b[A-Z][a-z]+\b')
KEYWORDS = ('percent', '%', 'billion', 'million', 'increased', 'decreased', 'banned', 'passed', 'voted', 'rate', 'tax', 'budget')
def split_claims(text):
return [s.strip() for s in re.split(r'(?<=[.!?])\s+', text.strip()) if s.strip()]
def is_check_worthy(claim):
return (bool(NUM.search(claim)) or any(k in claim.lower() for k in KEYWORDS)) and bool(ENTITY.search(claim))
def retrieve(claim):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": claim, "light_request": True})
data = r.json()
ev = [{"title": x["title"], "snippet": x.get("snippet", ""), "link": x["link"]}
for x in data.get("organic_results", [])[:5]]
ev += [{"title": p.get("question", ""), "snippet": p.get("snippet", ""), "link": p.get("link", "")}
for p in data.get("people_also_ask", [])[:3]]
return ev
PROMPT = '''You are a fact-checking judge. Given a CLAIM and EVIDENCE, return strict JSON:
{{"verdict":"supported|contradicted|unverifiable","reason":"one sentence","citations":["<link>"]}}
Cite ONLY links present in EVIDENCE. Never invent a source. No coverage => "unverifiable".
CLAIM: {claim}
EVIDENCE: {evidence}'''
def fact_check(text, llm):
results = []
for claim in split_claims(text):
if not is_check_worthy(claim):
continue
evidence = retrieve(claim)
verdict = llm.complete(PROMPT.format(claim=claim, evidence=json.dumps(evidence)), response_format="json")
results.append({"claim": claim, **json.loads(verdict)})
return results
# transcript = "..."
# for r in fact_check(transcript, my_llm):
# print(r["verdict"], r["claim"], r["citations"])Ejemplo en JavaScript
const H = {
Authorization: `Bearer ${process.env.SCAVIO_API_KEY}`,
"Content-Type": "application/json",
};
const KEYWORDS = ["percent", "%", "billion", "million", "increased", "decreased", "banned", "passed", "voted", "rate", "tax", "budget"];
const splitClaims = (text) =>
text.trim().split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
const isCheckWorthy = (c) =>
(/\d/.test(c) || KEYWORDS.some((k) => c.toLowerCase().includes(k))) && /\b[A-Z][a-z]+\b/.test(c);
async function retrieve(claim) {
const r = await fetch("https://api.scavio.dev/api/v1/google", {
method: "POST",
headers: H,
body: JSON.stringify({ query: claim, light_request: true }),
});
const data = await r.json();
const ev = (data.organic_results || []).slice(0, 5).map((x) => ({ title: x.title, snippet: x.snippet || "", link: x.link }));
(data.people_also_ask || []).slice(0, 3).forEach((p) => ev.push({ title: p.question || "", snippet: p.snippet || "", link: p.link || "" }));
return ev;
}
const PROMPT = (claim, evidence) => `You are a fact-checking judge. Given a CLAIM and EVIDENCE, return strict JSON:
{"verdict":"supported|contradicted|unverifiable","reason":"one sentence","citations":["<link>"]}
Cite ONLY links present in EVIDENCE. Never invent a source. No coverage => "unverifiable".
CLAIM: ${claim}
EVIDENCE: ${JSON.stringify(evidence)}`;
async function factCheck(text, llm) {
const out = [];
for (const claim of splitClaims(text)) {
if (!isCheckWorthy(claim)) continue;
const evidence = await retrieve(claim);
const verdict = await llm.complete(PROMPT(claim, evidence), { responseFormat: "json" });
out.push({ claim, ...JSON.parse(verdict) });
}
return out;
}Salida esperada
[
{
"claim": "The new budget increases infrastructure spending by 12 percent next year.",
"verdict": "supported",
"reason": "Two retrieved sources state a 12 percent infrastructure increase in the proposed budget.",
"citations": ["https://example-news.test/budget-2026", "https://example-gov.test/budget-summary"]
},
{
"claim": "Unemployment fell to its lowest level since 1969.",
"verdict": "contradicted",
"reason": "Retrieved sources report the lowest level since 2001, not 1969.",
"citations": ["https://example-stats.test/unemployment"]
},
{
"claim": "The committee met privately on Tuesday to discuss the proposal.",
"verdict": "unverifiable",
"reason": "No retrieved source addresses a private Tuesday meeting.",
"citations": []
}
]