Rechazo con fundamento
El valor real de RAG está en rechazar lo que los documentos no sostienen. El
Showcase — Rechazo con fundamento
El valor real de RAG está en rechazar lo que los documentos no sostienen. El corpus es una política de RH estrecha. Pregunta sobre PTO, incapacidad, trabajo remoto o gastos y responde con una cita; pregunta por el precio de la acción o el clima y responde "That isn't covered in the HR policies I have" en lugar de improvisar. El rechazo es el guardrail contra la alucinación confiada.
Córrelo
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Abre http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.
Qué hay aquí
backend/store.py— elVectorStorecompartido.backend/ai_openai.py/backend/ai_gemini.py— la KB estrecha de políticas y un system prompt con una regla dura de rechazo.frontend/app/page.tsx— caja de pregunta; prueba preguntas dentro y fuera de alcance.
Detenlo
docker compose down
Ejecútalo en tu máquina
Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.
unzip grounded-refuse.zip
cd grounded-refuse
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.
Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.
backend/ai_openai.py
"""Showcase 2 (OpenAI): grounded answers that refuse when unsupported.
RAG's real value isn't answering — it's REFUSING to answer what the documents
don't support. The corpus here is a narrow HR policy. Ask about leave or
expenses and it answers with a citation; ask about anything else — the stock
price, the weather, last night's game — and it declines instead of improvising.
That refusal is the guardrail against confident hallucination.
"""
from openai import OpenAI
from store import VectorStore
_client = OpenAI()
_EMBED_MODEL = "text-embedding-3-small"
_CHAT_MODEL = "gpt-5.4-nano"
_KB = [
{"id": "pto", "text": "Full-time employees accrue 15 days of paid time off per year, rolling over up to 5 days."},
{"id": "sick", "text": "Sick leave is separate from PTO: 8 paid sick days per year, no rollover."},
{"id": "remote", "text": "Employees may work remotely up to 3 days per week with manager approval."},
{"id": "expenses", "text": "Reimbursable expenses must be submitted within 30 days with a receipt via the Expenses portal."},
{"id": "parental", "text": "Parental leave is 12 weeks paid, available to all parents within a year of the birth or adoption."},
]
_SYSTEM = (
"You are an HR policy assistant. Answer strictly from the policy context "
"below and cite the [id] you used. If the answer is not in the context, "
"reply exactly: 'That isn't covered in the HR policies I have.' Do not guess "
"and do not use outside knowledge.\n\nContext:\n{context}"
)
_store: VectorStore | None = None
def _embed(texts):
return [item.embedding for item in _client.embeddings.create(model=_EMBED_MODEL, input=texts).data]
def _get_store() -> VectorStore:
global _store
if _store is None:
_store = VectorStore(_embed)
_store.add(_KB)
return _store
def run(question: str) -> str:
hits = _get_store().search(question.strip(), k=3)
context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
response = _client.responses.create(
model=_CHAT_MODEL,
instructions=_SYSTEM.format(context=context),
input=[{"role": "user", "content": question.strip()}],
)
return response.output_text
backend/ai_gemini.py
"""Showcase 2 (Gemini): grounded answers that refuse when unsupported.
Same narrow policy corpus and the same hard refusal rule, on Gemini.
"""
import os
from google import genai
from google.genai import types
from store import VectorStore
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_EMBED_MODEL = "gemini-embedding-001"
_CHAT_MODEL = "gemini-3.1-flash-lite"
_KB = [
{"id": "pto", "text": "Full-time employees accrue 15 days of paid time off per year, rolling over up to 5 days."},
{"id": "sick", "text": "Sick leave is separate from PTO: 8 paid sick days per year, no rollover."},
{"id": "remote", "text": "Employees may work remotely up to 3 days per week with manager approval."},
{"id": "expenses", "text": "Reimbursable expenses must be submitted within 30 days with a receipt via the Expenses portal."},
{"id": "parental", "text": "Parental leave is 12 weeks paid, available to all parents within a year of the birth or adoption."},
]
_SYSTEM = (
"You are an HR policy assistant. Answer strictly from the policy context "
"below and cite the [id] you used. If the answer is not in the context, "
"reply exactly: 'That isn't covered in the HR policies I have.' Do not guess "
"and do not use outside knowledge.\n\nContext:\n{context}"
)
_store: VectorStore | None = None
def _embed(texts):
return [e.values for e in _client.models.embed_content(model=_EMBED_MODEL, contents=texts).embeddings]
def _get_store() -> VectorStore:
global _store
if _store is None:
_store = VectorStore(_embed)
_store.add(_KB)
return _store
def run(question: str) -> str:
hits = _get_store().search(question.strip(), k=3)
context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
response = _client.models.generate_content(
model=_CHAT_MODEL,
contents=[types.Content(role="user", parts=[types.Part(text=question.strip())])],
config=types.GenerateContentConfig(system_instruction=_SYSTEM.format(context=context)),
)
return response.text or ""
Archivos del proyecto
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbackend/store.pybootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json