Guia practica para construir un agente RAG de soporte interno, de cero a produccion, con la teoria para entender cada decision de diseno.
Version 1 · Laboratorio en c:\prueba-agente ·
Convenciones: TU = lo ejecutas vos
TEORIA = concepto para entender el diseno
Un agente de soporte interno recibe una consulta de un empleado ("no me anda la VPN", "como pido vacaciones", "cual es la politica de gastos"), busca en documentacion interna, responde, y si no puede resolver, escala a un humano o ejecuta una accion (abrir ticket).
Un agente de soporte solido cubre cuatro cosas:
| Capacidad | Como se logra |
|---|---|
| RAG (que responda con datos reales, no invente) | Cargas docs internos, los vectorizas, el agente cita esa fuente |
| Tools / acciones (que el agente HAGA cosas) | Le das herramientas: buscar en KB, crear ticket, consultar estado |
| Orquestacion (n8n atando todo) | Trigger de entrada -> agente -> respuesta / escalado |
| Razonamiento con estado (LangGraph) | Un grafo que decide: responder vs. escalar, con memoria y control humano |
Estos son los conceptos base del stack. Conviene tenerlos claros antes de construir.
El modelo de lenguaje (GPT-4o, Claude, Llama...). Solo predice texto. No sabe nada de tu empresa ni puede "hacer" cosas por si mismo. Todo lo demas existe para darle contexto (RAG) y darle manos (tools).
El patron estrella del soporte interno. En vez de esperar que el modelo "sepa" tu politica de vacaciones, hacemos:
Por que importa: reduce alucinaciones, permite citar la fuente, y se actualiza cambiando documentos (sin reentrenar el modelo).
Un embedding transforma texto en un vector. Textos con significado parecido quedan cerca en el espacio vectorial. "Como pido dias libres" y "solicitud de vacaciones" caen cerca aunque no compartan palabras. Eso es busqueda semantica, superior al buscar-por-palabra-clave.
Una tool es una funcion que el LLM puede pedir ejecutar (buscar_en_kb, crear_ticket, consultar_estado_pedido). El modelo no la ejecuta: decide llamarla y con que argumentos; tu codigo (o n8n) la ejecuta y le devuelve el resultado. Asi el agente pasa de "charlar" a "actuar".
Un LLM en un bucle: piensa -> elige una tool -> ve el resultado -> vuelve a pensar, hasta resolver. El patron clasico se llama ReAct (Reasoning + Acting).
| LangChain | LangGraph | |
|---|---|---|
| Que es | Libreria de piezas: prompts, modelos, retrievers, tools, cadenas | Framework para agentes como grafo con estado |
| Forma | Cadena lineal (A -> B -> C) | Grafo con ciclos, ramas y estado compartido |
| Cuando | Flujos simples y deterministas | Agentes que razonan en bucle, ramifican, reintentan, esperan a un humano |
| En una frase | "LangChain me da las piezas; LangGraph me da el control de flujo con estado para un agente que decide entre responder y escalar." | |
El agente pausa y pide aprobacion o intervencion humana antes de una accion sensible (ej: crear un ticket urgente, dar una respuesta que no esta seguro). LangGraph lo hace nativo con checkpoints e interrupts. Mencionarlo suma muchos puntos.
Orquestador visual de automatizaciones (nodos conectados). Self-hostable y gratis. Trae nodos de IA (categoria "Advanced AI") que son LangChain por dentro: AI Agent, Chat Model, Vector Store, Memory, Tools. Sirve como la capa que recibe el evento (chat, webhook, email) y ata las integraciones (Slack, tickets, base de datos).
Vas a construir el mismo agente dos veces para dominar las dos formas que te pueden pedir:
La estructura de carpetas final sera:
c:\prueba-agente\
docs\ (esta guia)
knowledge\ (documentos internos falsos para el RAG)
langgraph-agent\ (el servicio Python del Lab B)
agent.py
server.py
ingest.py
requirements.txt
.env
n8n-data\ (datos de n8n, si usamos volumen local)
Todo lo de esta guia se ejecuta en PowerShell. Abrilo como usuario normal (no hace falta admin salvo donde se indique).
mkdir c:\prueba-agente
cd c:\prueba-agente
mkdir knowledge, langgraph-agent
(La carpeta docs ya existe: es donde esta esta guia.)
Corre estos comandos para ver que ya tenes. Si alguno dice "no se reconoce", lo instalamos.
docker --version
node --version
python --version
git --version
code --version
Instalaciones (usa winget, el gestor de paquetes de Windows):
| Herramienta | Para que | Comando |
|---|---|---|
| Docker Desktop | correr n8n | winget install Docker.DockerDesktop |
| Node.js LTS | base de n8n / utilidades | winget install OpenJS.NodeJS.LTS |
| Python 3.11+ | el agente LangGraph | winget install Python.Python.3.12 |
| Git | control de version | winget install Git.Git |
| VS Code | tu IDE | winget install Microsoft.VisualStudioCode |
docker run hello-world.
platform.openai.com y crea una cuenta.sk-.....env de Python..env (que ignoramos con .gitignore) y en las
credenciales cifradas de n8n.smith.langchain.com y crea cuenta (plan gratuito).lsv2_....n8n no necesita cuenta externa: es self-host. El "owner account" se crea local la primera vez que abris la interfaz (Fase 2).
Con Docker Desktop abierto y corriendo:
docker volume create n8n_data
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8n
Explicacion: -d lo corre en segundo plano; -p 5678:5678 publica el
puerto; -v n8n_data:... guarda tus flujos aunque reinicies el contenedor.
Ahora: abri el navegador en http://localhost:5678. La primera vez te pide crear el owner account (email + password locales, inventados, no salen de tu maquina). Ya tenes n8n corriendo.
Comandos utiles de manejo:
docker ps # ver que n8n esta corriendo
docker stop n8n # apagarlo
docker start n8n # prenderlo
docker logs -f n8n # ver sus logs en vivo
Objetivo: en 20-30 min tener un agente que responde en un chat, usando OpenAI. Primero sin RAG (para ver el patron), luego le sumamos la base de conocimiento en la Fase 4.
En n8n: menu de la izquierda -> Credentials -> Add credential ->
busca "OpenAI" -> pega tu sk-... -> guardar.
gpt-4o-mini (barato y bueno).Sos el asistente de soporte interno de la empresa ACME.
Respondes en espanol, claro y breve. Si no tenes la
informacion, decilo honestamente y ofreces escalar a un
humano. No inventes politicas.
Abajo, boton Chat. Escribi "hola, quien sos?". Deberia responder como asistente de ACME. Ya tenes un agente basico funcionando. Todavia no sabe de tu empresa; eso es la Fase 4.
En c:\prueba-agente\knowledge crea 3-4 archivos .txt o
.md con politicas inventadas. Ejemplo de contenido para
vacaciones.md:
Politica de vacaciones ACME
- Cada empleado tiene 21 dias habiles por ano.
- Se solicitan con 15 dias de anticipacion en el portal RRHH.
- Aprobacion a cargo del jefe directo en 48 horas.
- Los dias no usados se pierden el 31 de marzo del ano siguiente.
Crea tambien vpn.md, gastos.md, onboarding.md. Cuanto
mas concretos, mejor demo.
n8n trae un Simple Vector Store (en memoria) perfecto para la demo:
Esta es la version "de empresa" del RAG. Cuando el stack corporativo usa
PostgreSQL, la base vectorial suele ser pgvector: la extension que agrega a Postgres
un tipo de dato vector y operadores de distancia, convirtiendolo en base
vectorial. Ventaja: una sola base para datos y embeddings, sin sumar un servicio aparte.
La imagen oficial ya trae la extension compilada:
docker run -d --name pgvector -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=soporte -p 5432:5432 pgvector/pgvector:pg16
Habilitar la extension dentro de la base soporte:
docker exec -it pgvector psql -U postgres -d soporte -c "CREATE EXTENSION IF NOT EXISTS vector;"
host.docker.internal (n8n esta en su contenedor; Postgres en otro
publicado en tu host)5432 · Database: soporte ·
User: postgres · Password: la que pusistesoporte_kb,
dimension 1536 (la de text-embedding-3-small).localhost. Para llegar al Postgres publicado en tu host usa
host.docker.internal:5432, no localhost:5432. (Alternativa pro: poner
ambos contenedores en la misma red Docker y usar el nombre del contenedor pgvector
como host.)Reemplaza el InMemoryVectorStore del ingest.py de la Fase 5.
pip install langchain-postgres "psycopg[binary]"
Nuevo ingest.py (variante Postgres):
import os, glob
from langchain_postgres import PGVector
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from dotenv import load_dotenv
load_dotenv()
# Desde Python (en tu PC, fuera de Docker) el host es localhost.
# Desde n8n (en Docker) seria host.docker.internal. Mismo Postgres,
# distinta vista de red.
CONN = "postgresql+psycopg://postgres:postgres@localhost:5432/soporte"
def build_retriever(kb_dir=r"..\knowledge"):
docs = []
for path in glob.glob(os.path.join(kb_dir, "*.md")):
with open(path, encoding="utf-8") as f:
docs.append(Document(page_content=f.read(),
metadata={"source": os.path.basename(path)}))
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
store = PGVector.from_documents( # <-- ingesta: embebe + inserta
documents=chunks,
embedding=OpenAIEmbeddings(),
collection_name="soporte_kb",
connection=CONN,
use_jsonb=True,
)
return store.as_retriever(search_kwargs={"k": 3})
El resto del agente (agent.py, server.py) no cambia:
sigue usando build_retriever(). Solo cambiaste donde viven los vectores.
Esto crea automaticamente dos tablas: langchain_pg_collection
(las colecciones) y langchain_pg_embedding (los chunks + su vector).
Cargar los vectores no es lo mismo que indexarlos para busqueda veloz. Crea el indice
aproximado sobre la columna embedding:
-- HNSW: mejor recall y velocidad de consulta (recomendado por defecto)
docker exec -it pgvector psql -U postgres -d soporte -c \
"CREATE INDEX IF NOT EXISTS idx_kb_hnsw ON langchain_pg_embedding USING hnsw (embedding vector_cosine_ops);"
-- IVFFlat: build mas barato, PERO requiere datos ya cargados y elegir 'lists'
-- CREATE INDEX ON langchain_pg_embedding USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
vector_cosine_ops = distancia coseno, la tipica para embeddings
de OpenAI. Tambien existen vector_l2_ops (euclidea) y vector_ip_ops
(producto interno).lists.Verificar que quedo todo:
docker exec -it pgvector psql -U postgres -d soporte -c "\d langchain_pg_embedding"
docker exec -it pgvector psql -U postgres -d soporte -c "SELECT count(*) FROM langchain_pg_embedding;"
vector y
operadores de distancia. Lo elijo cuando la empresa ya tiene Postgres: una sola base para
datos y embeddings, menos infra que mantener."Ahora el mismo agente, pero como codigo. Esto demuestra que dominas LangGraph, no solo arrastrar nodos.
cd c:\prueba-agente\langgraph-agent
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned y acepta.Crea el archivo requirements.txt con:
langgraph
langchain
langchain-openai
langchain-community
langchain-core
fastapi
uvicorn
python-dotenv
pip install -r requirements.txt
Crea .env (mismo folder) con tus keys:
OPENAI_API_KEY=sk-tu-key-aca
# Para la Fase 7 (dejalas listas):
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2-tu-key-aca
LANGSMITH_PROJECT=soporte-interno
Y un .gitignore con .env y .venv/ dentro.
ingest.pyEste script lee los .md de knowledge\, los parte en chunks, los
vectoriza y arma un retriever en memoria que reutilizamos.
import os, glob
from langchain_openai import OpenAIEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
from dotenv import load_dotenv
load_dotenv()
def build_retriever(kb_dir=r"..\knowledge"):
docs = []
for path in glob.glob(os.path.join(kb_dir, "*.md")):
with open(path, encoding="utf-8") as f:
docs.append(Document(page_content=f.read(),
metadata={"source": os.path.basename(path)}))
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_documents(docs)
store = InMemoryVectorStore.from_documents(chunks, OpenAIEmbeddings())
return store.as_retriever(search_kwargs={"k": 3})
Nota: RecursiveCharacterTextSplitter vive en el paquete
langchain-text-splitters, que se instala junto con langchain. Si
faltara: pip install langchain-text-splitters.
agent.pyUn grafo LangGraph con estado y una rama de decision: recupera contexto, genera respuesta, y si el modelo no esta seguro, marca escalar.
from typing import TypedDict, List
from langgraph.graph import StateGraph, START, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from ingest import build_retriever
retriever = build_retriever()
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
class State(TypedDict):
question: str
context: str
answer: str
escalate: bool
def retrieve(state: State):
docs = retriever.invoke(state["question"])
ctx = "\n\n".join(f"[{d.metadata['source']}] {d.page_content}" for d in docs)
return {"context": ctx}
def generate(state: State):
system = SystemMessage(content=(
"Sos el soporte interno de ACME. Responde SOLO con el contexto dado. "
"Si el contexto no alcanza, responde exactamente 'ESCALAR' y nada mas."))
user = HumanMessage(content=(
f"Contexto:\n{state['context']}\n\nPregunta: {state['question']}"))
resp = llm.invoke([system, user]).content
return {"answer": resp, "escalate": resp.strip().upper().startswith("ESCALAR")}
def escalate(state: State):
return {"answer": ("No tengo esa informacion en la base. "
"Abri un ticket para un agente humano: [TICKET-CREADO]")}
def route(state: State):
return "escalate" if state["escalate"] else END
graph = StateGraph(State)
graph.add_node("retrieve", retrieve)
graph.add_node("generate", generate)
graph.add_node("escalate", escalate)
graph.add_edge(START, "retrieve")
graph.add_edge("retrieve", "generate")
graph.add_conditional_edges("generate", route, {"escalate": "escalate", END: END})
graph.add_edge("escalate", END)
app = graph.compile()
if __name__ == "__main__":
out = app.invoke({"question": "cuantos dias de vacaciones tengo?"})
print(out["answer"])
Probalo directo:
python agent.py
Deberia responder con el dato de tu vacaciones.md. Preguntas fuera de la KB
disparan la rama escalate. Eso es un agente con estado y control de flujo:
justo lo que evalua LangGraph.
server.pyfrom fastapi import FastAPI
from pydantic import BaseModel
from agent import app as agent_app
api = FastAPI()
class Query(BaseModel):
question: str
@api.post("/chat")
def chat(q: Query):
result = agent_app.invoke({"question": q.question})
return {"answer": result["answer"], "escalated": result.get("escalate", False)}
uvicorn server:api --reload --port 8000
Probalo desde otra terminal PowerShell:
curl.exe -X POST http://localhost:8000/chat -H "Content-Type: application/json" -d "{\"question\":\"como pido vacaciones?\"}"
Ahora n8n es la puerta y LangGraph el cerebro. En un workflow nuevo ("Soporte Lab B"):
POSThttp://host.docker.internal:8000/chat{ "question": "{{ $json.chatInput }}" }{{ $json.answer }} como respuesta del chat.localhost es el contenedor, no tu PC. Para llegar a tu
servicio Python que corre en el host, se usa http://host.docker.internal:8000, no
http://localhost:8000. Recordalo.Ya pusiste las variables LANGSMITH_* en el .env. Con
LANGSMITH_TRACING=true, cada corrida del agente aparece en
smith.langchain.com: veras cada paso del grafo, que documentos recupero, que
prompt mando al LLM, cuantos tokens gasto y cuanto tardo.
Corre python agent.py otra vez y abri LangSmith: vas a ver la traza. Saber
depurar un agente mirando la traza es una habilidad clave para operarlo en produccion.
El demo responde a un usuario. Una plataforma corporativa responde a cientos en paralelo, todo el dia, sin caerse ni fundir el presupuesto. El principio rector:
| Componente | Cuello tipico bajo carga | Palanca principal |
|---|---|---|
| n8n | Proceso unico, ejecuciones en cola, SQLite bloqueando | Queue mode + workers + Postgres + Redis |
| LLM (OpenAI) | Rate limit (429), latencia, costo | Retry/backoff, right-sizing, cache, streaming, fallback |
| Embeddings | 1 request por chunk, recomputo | Batch + cache + no re-embeber lo que no cambio |
| Postgres/pgvector | Scan secuencial, conexiones agotadas | Indice HNSW ajustado + pooling (PgBouncer) |
| Servicio LangGraph | Endpoint sincrono serializa todo; estado en memoria | Async + varios workers + checkpointer externo |
| Transversal | Repetir trabajo identico, sin visibilidad | Cache semantica + colas + metricas p95/costo |
Por defecto n8n corre en regular mode (un solo proceso, base SQLite): no escala. Para volumen se pasa a queue mode: un proceso main reparte las ejecuciones por Redis a varios procesos worker que escalan horizontalmente.
# Variables clave (en el/los contenedores n8n)
EXECUTIONS_MODE=queue
QUEUE_BULL_REDIS_HOST=redis
DB_TYPE=postgresdb # NUNCA SQLite en produccion (no soporta concurrencia)
DB_POSTGRESDB_HOST=postgres
N8N_CONCURRENCY_PRODUCTION_LIMIT=10 # ejecuciones simultaneas POR worker
N8N_DEFAULT_BINARY_DATA_MODE=filesystem # binarios fuera de memoria/DB
EXECUTIONS_DATA_PRUNE=true # que la tabla de ejecuciones no crezca infinito
EXECUTIONS_DATA_MAX_AGE=336 # horas (14 dias)
n8n worker) y se
escalan segun la cola. El main solo orquesta.gpt-4o-mini para el 90%); patron router que escala a un modelo
grande solo cuando hace falta.from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0,
max_retries=5, timeout=30, streaming=True)
backup = ChatOpenAI(model="gpt-4o", max_retries=3)
llm = llm.with_fallbacks([backup]) # si el primario falla, usa el backup
CacheBackedEmbeddings): no re-embeber
chunks identicos.text-embedding-3-small
1536 dims < -large 3072): busqueda mas rapida y menos almacenamiento.from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import LocalFileStore
from langchain_openai import OpenAIEmbeddings
store = LocalFileStore("./emb_cache")
embeddings = CacheBackedEmbeddings.from_bytes_store(
OpenAIEmbeddings(), store, namespace="soporte")
m y ef_construction en el
build; hnsw.ef_search en consulta (sube recall a costa de latencia).maintenance_work_mem alto para construir el indice,
work_mem/shared_buffers para consultas.halfvec / binaria en pgvector reciente) para
cortar almacenamiento y acelerar. Read replicas si el retrieval domina.-- Build ajustado y parametro de consulta
CREATE INDEX ON langchain_pg_embedding
USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64);
SET hnsw.ef_search = 40; -- por sesion: mas alto = mas recall, mas latencia
async def + await
graph.ainvoke(...) + cliente LLM/DB async. Un endpoint sincrono serializa TODAS las
peticiones — mata la concurrencia.# estado externo -> cualquier replica sirve cualquier hilo
from langgraph.checkpoint.postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string(CONN)
app = graph.compile(checkpointer=checkpointer)
# arranque multi-worker
# gunicorn server:api -k uvicorn.workers.UvicornWorker -w 4 --timeout 60
En soporte interno muchas preguntas se repiten ("como reseteo la clave", "como pido vacaciones"). Una cache semantica responde esas desde Redis sin tocar retrieval ni LLM: recorta costo y latencia de forma dramatica.
from langchain_core.globals import set_llm_cache
from langchain_community.cache import RedisSemanticCache
set_llm_cache(RedisSemanticCache(redis_url="redis://redis:6379",
embedding=embeddings, score_threshold=0.1))
Capas de cache utiles: respuesta del LLM (semantica), resultado del retrieval, y los embeddings (11B.3).
No se adivina, se calcula. Necesitas tres numeros: (1) el techo de RPM/TPM de tu cuenta LLM, (2) tu objetivo de usuarios concurrentes, y (3) tu presupuesto de latencia p95. Con eso: workers ~= concurrencia objetivo / throughput por worker, y el pool de Postgres y el semaforo del LLM se dimensionan para no pasar el techo de cada recurso. Despues se valida con una prueba de carga (k6/Locust) y se mueve el cuello.
Todo lo anterior, junto. Esta es la forma "de produccion" del sistema: cada mejora de las secciones 11B.1–11B.8 ubicada en su lugar. Es el diagrama que conviene poder dibujar de memoria en la pizarra.
| Elemento del diagrama | Mejora que representa | Seccion |
|---|---|---|
| Load balancer / nginx | TLS, rate-limit por tenant, varios procesos detras | 11B.5, 11B.8 |
| n8n queue mode + N workers | Escala horizontal, Postgres, pruning, webhooks dedicados | 11B.1 |
| Bull (Redis) | Cola interna de ejecucion de n8n | 11B.1 |
| Servicio Agente async, sin estado, N replicas | Async de punta a punta, multi-worker, autoescala | 11B.5 |
| Checkpointer (Postgres) | Estado fuera del proceso → escala horizontal | 11B.5 |
| LLM providers | Retry/backoff, semaforo, fallback, prompt-cache, streaming | 11B.2 |
| Retrieval: PgBouncer + pgvector (HNSW) + replicas | Indice ANN, pooling, read replicas | 11B.4 |
| Cache (Redis) | Cache semantica / de retrieval / de embeddings | 11B.3, 11B.6 |
| Observabilidad | Trazas muestreadas + metricas p95/costo/hit-rate | 11B.7 |
| RabbitMQ + workers de ingesta | Desacoplar ingesta, batch de embeddings, reintentos | 11B.3, 11B.7 |
Un orden de construccion sugerido para tener un primer agente funcionando rapido. Objetivo: ~90 minutos.
| Min | Tarea |
|---|---|
| 0-10 | Levantar n8n, cargar credencial OpenAI, crear workflow vacio |
| 10-30 | Lab A: AI Agent + Chat Model + Memory respondiendo |
| 30-55 | RAG: cargar KB al vector store, conectarla como tool, probar preguntas reales |
| 55-80 | Lab B: levantar el servicio LangGraph y conectarlo por HTTP desde n8n |
| 80-90 | Escalado + prueba final punta a punta del flujo completo |
Si preferis practicar sin gastar en OpenAI:
winget install Ollama.Ollama
ollama pull llama3.1
ollama pull nomic-embed-text # para embeddings
En Python cambias dos lineas:
pip install langchain-ollama
# en vez de ChatOpenAI:
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3.1", temperature=0)
# en vez de OpenAIEmbeddings:
from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(model="nomic-embed-text")
En n8n hay nodo "Ollama Chat Model". Como n8n esta en Docker, la URL de Ollama es
http://host.docker.internal:11434.
| Sintoma | Causa / solucion |
|---|---|
docker: command not found |
Docker Desktop no instalado o no abierto. Abrilo y espera a que diga "running". |
| n8n no abre en localhost:5678 | docker ps para ver si corre; docker logs n8n para el error. |
| n8n no llega al servicio Python | Usa host.docker.internal, no localhost (ver Fase 6). |
Activate.ps1 no se puede cargar |
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned |
OpenAI: 401 / insufficient_quota |
Key mal pegada, o sin credito. Revisa Billing en platform.openai.com. |
ModuleNotFoundError |
Venv no activado (.\.venv\Scripts\Activate.ps1) o falto
pip install -r requirements.txt. |
| El agente inventa datos | Reforza el system prompt ("responde SOLO con el contexto") y baja
temperature a 0. |
Postgres: type "vector" does not exist |
Falto CREATE EXTENSION vector; en esa base (ver Fase 4B.1). |
| n8n no conecta a Postgres | Host host.docker.internal (no localhost) y verifica que el
contenedor pgvector este corriendo (docker ps). |
Se puede armar el RAG completo usando Redis como base vectorial, sin Postgres. Aca Redis actua como fuente de la verdad (no como cache). La pieza que lo habilita es el motor de busqueda vectorial: en Redis 8 viene integrado en el core (antes era el modulo RediSearch dentro de Redis Stack).
El analisis (parsing) y el chunking son iguales a cualquier RAG (chunks de ~500/50). Los embeddings los genera el LLM/API; Redis solo los guarda e indexa. Cada chunk es un key de Redis (Hash), con el vector como bytes float32:
import redis, numpy as np
r = redis.Redis()
# un key por chunk: doc:{docId}:{chunkId}
r.hset("doc:vac:001", mapping={
"content": chunk_text, # el texto, para devolverlo al LLM
"source": "vacaciones.md", # metadata: cita / gestion
"tenant": "acme", # metadata: filtro
"embedding": np.array(vec, dtype=np.float32).tobytes(), # el vector
})
# indice inverso para poder actualizar/borrar el documento entero:
r.sadd("chunks:vac", "doc:vac:001")
Una sola vez se crea el indice vectorial sobre el prefijo doc::
FT.CREATE idx:kb ON HASH PREFIX 1 doc:
SCHEMA
content TEXT
source TAG
tenant TAG
embedding VECTOR HNSW 6 TYPE FLOAT32 DIM 1536 DISTANCE_METRIC COSINE
Usa HNSW (ANN, para volumen) o FLAT (fuerza bruta exacta, para
pocos datos). COSINE es la metrica de los embeddings de OpenAI, DIM 1536
la de text-embedding-3-small.
Embebes la pregunta y pasas el vector como parametro; Redis devuelve los top-k chunks con su texto, metadata y score:
FT.SEARCH idx:kb "(@tenant:{acme})=>[KNN 3 @embedding $BLOB AS score]"
PARAMS 2 BLOB $query_bytes
SORTBY score RETURN 2 content source DIALECT 2
El (@tenant:{acme})=>[KNN ...] es busqueda hibrida: primero
filtra por metadata, despues hace el KNN. Ese resultado va al LLM como contexto.
pip install langchain-redis
from langchain_redis import RedisVectorStore, RedisConfig
from langchain_openai import OpenAIEmbeddings
config = RedisConfig(index_name="kb", redis_url="redis://localhost:6379")
vs = RedisVectorStore.from_documents(chunks, OpenAIEmbeddings(), config=config)
retriever = vs.as_retriever(search_kwargs={"k": 3})
El resto del agente (agent.py, server.py) no cambia: solo cambia
de donde build_retriever() saca los vectores.
appendonly yes (AOF) + snapshots RDB. Al reiniciar, Redis recarga los keys y
reconstruye el indice desde los hashes.chunks:{docId} lista los
keys de sus chunks. Actualizar = borrar los viejos (los del SET) e insertar los nuevos. Para
ingesta incremental, un docmeta:{docId} con el hash del contenido.replicaof) para HA;
Redis Cluster para repartir el keyspace.FLOAT16/BFLOAT16
o la compresion SVS-VAMANA de Redis 8.| Dimension | pgvector (Postgres) | Redis |
|---|---|---|
| Donde viven los vectores | En disco | En RAM |
| Velocidad | Muy buena | Maxima (in-memory) |
| Costo a escala | Bajo (disco barato) | Alto (RAM cara) |
| Corpus grande (millones) | Ideal | Limitado por RAM |
| SQL / joins / transaccional | Si | No |
| Suele estar ya para... | datos relacionales | cache + cola de n8n |
| Como sistema de registro | Muy probado | Bueno (AOF+RDB), menos "de registro" |
| Cuando elegirlo | Default; corpus grande; system of record | Ya usas Redis; latencia minima; corpus chico-mediano; unificar infra |
Nota: los nombres exactos (Redis 8 core vs Redis Stack, el paquete
langchain-redis, DIALECT 2) son estables a mi corte, pero la API
vectorial de Redis evoluciono rapido — confirmalos contra la doc oficial al montarlo.
Fin de la guia. Cualquier error o paso que no corra, anotalo y se resuelve: la depuracion tambien es parte de dominar el stack.