Para darle busqueda web en vivo a un LLM local, recupera resultados reales antes de que el modelo responda: envia la pregunta del usuario al endpoint de Google de Scavio, toma los titulos, fragmentos y enlaces de los primeros resultados organicos, pegalos en un bloque de contexto y dile a Ollama que responda solo con esas fuentes y cite los enlaces. Ese es todo el truco. Un modelo local no tiene internet y su entrenamiento esta congelado en una fecha, asi que por su cuenta adivina. Peor aun, en 2026 la mayoria de los modelos locales todavia creen que es 2024, aunque cada resultado de busqueda que les pasas vuelva fechado en 2026, asi que tambien tienes que meterle la fecha de hoy en el prompt o discutiran con la evidencia. Esto es generacion aumentada por recuperacion, solo que apuntada a un SERP en lugar de a una base vectorial. El bucle de abajo corre de principio a fin en Python en tu portatil: Ollama en http://localhost:11434, un POST a https://api.scavio.dev/api/v1/google y un prompt de anclaje que obliga a citar. Scavio cobra $0.005 por credito y una peticion ligera de Google es 1 credito, asi que mil preguntas ancladas cuestan unos cinco dolares sin infraestructura que mantener. Tambien cubrimos la alternativa honesta: autoalojar SearxNG mas un crawler es gratis y privado, pero te toca a ti los contenedores, los proxies y el uptime. Los aproximadamente 1.000 busquedas gratis al mes de Brave son un buen punto de partida sin coste para uso aficionado.
Requisitos previos
- Ollama instalado y corriendo en local con un modelo descargado (por ejemplo, llama3.1 o qwen2.5) en http://localhost:11434
- Python 3.9+ con la libreria requests (pip install requests)
- Una clave de API de Scavio desde scavio.dev, exportada como SCAVIO_API_KEY
- Familiaridad basica con la API de chat de Ollama y las variables de entorno de la shell
Guia paso a paso
Paso 1: Recupera resultados en vivo del endpoint de Google de Scavio
Envia la pregunta del usuario a POST /api/v1/google con autenticacion Bearer. Usa light_request: false solo cuando necesites el SERP completo (people_also_ask, knowledge_graph); una peticion ligera es mas barata, 1 credito. Recibes organic_results estructurados con position, title, link y snippet, asi que no hay nada que scrapear ni parsear de HTML.
import os, requests
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}",
"Content-Type": "application/json"}
def search(query, k=5):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": query, "light_request": True})
r.raise_for_status()
return r.json().get("organic_results", [])[:k]Paso 2: Construye el bloque de contexto con la fecha de hoy
Convierte los primeros resultados en una cadena de contexto compacta y numerada. Inyecta la fecha actual para que el modelo deje de insistir en que es 2024. Manten cada fragmento corto; recortar aqui es lo que mantiene el prompt pequeno y el coste en tokens bajo.
from datetime import date
def build_context(results):
today = date.today().isoformat()
lines = [f"Today's date is {today}.", "", "Sources:"]
for i, row in enumerate(results, 1):
title = row.get("title", "")
snippet = (row.get("snippet", "") or "")[:280]
link = row.get("link", "")
lines.append(f"[{i}] {title}\n{snippet}\n{link}")
return "\n".join(lines)Paso 3: Manda el prompt anclado a Ollama y obliga a citar
Envia el contexto mas la pregunta a la API de chat local de Ollama. El mensaje de sistema es la barrera: responde solo con las fuentes, di cuando no cubren la pregunta y cita los marcadores [n]. stream va en false para recibir una unica respuesta JSON completa.
def ask_local(question, context, model="llama3.1"):
system = ("Answer ONLY from the provided sources. "
"If they don't contain the answer, say you don't know. "
"Cite sources inline as [1], [2]. Trust the dates in the sources.")
payload = {"model": model, "stream": False, "messages": [
{"role": "system", "content": system},
{"role": "user", "content": f"{context}\n\nQuestion: {question}"}]}
r = requests.post("http://localhost:11434/api/chat", json=payload)
r.raise_for_status()
return r.json()["message"]["content"]Paso 4: Corre el bucle completo y maneja los fallos
Encadena los tres pasos. Maneja los casos que de verdad pasan: cero resultados (diselo al modelo, no inventes respuesta), HTTP 429 por el limite de 1 peticion por segundo en free/payg (espera y reintenta) y cache obsoleta (reformula o anade un termino de actualidad). Nunca dejes que el modelo responda de la nada.
import time
def grounded_answer(question, model="llama3.1"):
try:
results = search(question)
except requests.HTTPError as e:
if e.response.status_code == 429:
time.sleep(1.1) # free/payg: 1 req/sec
results = search(question)
else:
raise
if not results:
return "No live results found. I won't guess."
ctx = build_context(results)
return ask_local(question, ctx, model)
if __name__ == "__main__":
print(grounded_answer("What changed in Python 3.13 free-threading?"))Ejemplo en Python
import os, time, requests
from datetime import date
H = {"Authorization": f"Bearer {os.environ['SCAVIO_API_KEY']}",
"Content-Type": "application/json"}
OLLAMA = "http://localhost:11434/api/chat"
def search(query, k=5):
r = requests.post("https://api.scavio.dev/api/v1/google", headers=H,
json={"query": query, "light_request": True})
r.raise_for_status()
return r.json().get("organic_results", [])[:k]
def build_context(results):
today = date.today().isoformat()
lines = [f"Today's date is {today}.", "", "Sources:"]
for i, row in enumerate(results, 1):
snippet = (row.get("snippet", "") or "")[:280]
lines.append(f"[{i}] {row.get('title','')}\n{snippet}\n{row.get('link','')}")
return "\n".join(lines)
def ask_local(question, context, model="llama3.1"):
system = ("Answer ONLY from the provided sources. "
"If they don't contain the answer, say you don't know. "
"Cite sources inline as [1], [2]. Trust the dates in the sources.")
payload = {"model": model, "stream": False, "messages": [
{"role": "system", "content": system},
{"role": "user", "content": f"{context}\n\nQuestion: {question}"}]}
r = requests.post(OLLAMA, json=payload)
r.raise_for_status()
return r.json()["message"]["content"]
def grounded_answer(question, model="llama3.1"):
try:
results = search(question)
except requests.HTTPError as e:
if e.response.status_code == 429:
time.sleep(1.1) # free/payg: 1 req/sec
results = search(question)
else:
raise
if not results:
return "No live results found. I won't guess."
return ask_local(question, build_context(results), model)
if __name__ == "__main__":
print(grounded_answer("Latest stable Node.js LTS version and its release date?"))Ejemplo en JavaScript
// Node 18+; Ollama running on localhost:11434
const H = {
Authorization: `Bearer ${process.env.SCAVIO_API_KEY}`,
"Content-Type": "application/json",
};
async function search(query, k = 5) {
const r = await fetch("https://api.scavio.dev/api/v1/google", {
method: "POST",
headers: H,
body: JSON.stringify({ query, light_request: true }),
});
if (!r.ok) throw new Error(`Scavio ${r.status}`);
const data = await r.json();
return (data.organic_results || []).slice(0, k);
}
function buildContext(results) {
const today = new Date().toISOString().slice(0, 10);
const lines = [`Today's date is ${today}.`, "", "Sources:"];
results.forEach((row, i) => {
const snippet = (row.snippet || "").slice(0, 280);
lines.push(`[${i + 1}] ${row.title || ""}\n${snippet}\n${row.link || ""}`);
});
return lines.join("\n");
}
async function askLocal(question, context, model = "llama3.1") {
const system =
"Answer ONLY from the provided sources. If they don't contain the answer, " +
"say you don't know. Cite sources inline as [1], [2]. Trust the dates in the sources.";
const r = await fetch("http://localhost:11434/api/chat", {
method: "POST",
body: JSON.stringify({
model,
stream: false,
messages: [
{ role: "system", content: system },
{ role: "user", content: `${context}\n\nQuestion: ${question}` },
],
}),
});
if (!r.ok) throw new Error(`Ollama ${r.status}`);
return (await r.json()).message.content;
}
async function groundedAnswer(question) {
const results = await search(question);
if (!results.length) return "No live results found. I won't guess.";
return askLocal(question, buildContext(results));
}
groundedAnswer("What is the latest stable Rust release?").then(console.log);Salida esperada
El modelo devuelve una respuesta corta anclada en el SERP recuperado, con citas [1]/[2] en linea y enlaces a los que de verdad puede apuntar, por ejemplo: "El ultimo Node.js LTS es 22.x, lanzado en 2026 [1]. La descarga y el changelog confirman la fecha [2]." seguido de las URLs de las fuentes. Cuando Scavio no devuelve organic_results, el bucle imprime "No live results found. I won't guess." en vez de una respuesta inventada.