Ollama RAG: Preguntas y respuestas con base de conocimiento privada local
En este capítulo, combinamos las capacidades de embeddings, recuperación y generación aprendidas anteriormente en una aplicación realmente utilizable: alimenta tus documentos al modelo local para que responda preguntas únicamente según tus materiales e indique la fuente.
El código completo tiene menos de 100 líneas, cero dependencias de bibliotecas vectoriales externas: cópialo y ejecútalo.
Desglose de requisitos y selección de tecnología
Primero aclaremos el problema: el modelo grande no conoce los materiales de tu disco duro; preguntarle directamente solo produce respuestas inventadas. La idea de RAG (generación aumentada por recuperación) es un "examen a libro abierto": primero se recuperan fragmentos relevantes de los materiales, se insertan en el prompt y el modelo responde basándose en ellos.
La selección de tecnología sigue el principio de "dependencia mínima":
| Etapa | Selección | Justificación |
|---|---|---|
| Modelo de embeddings | embeddinggemma | Tamaño pequeño, rápido en local; alternativa: qwen3-embedding |
| Modelo de generación | qwen3.5:4b | Bueno en chino, se ejecuta con 16 GB de RAM |
| Almacenamiento de vectores | Lista de Python + similitud coseno | Cero dependencias, claro para la enseñanza; para volúmenes grandes, cambia a bibliotecas profesionales como Chroma |
| Formato de documentos | txt / md | El texto plano basta para explicar el principio; para PDF, consulta la ampliación al final del artículo |
Necesitas descargar (pull) dos modelos:
ollama pull embeddinggemma ollama pull qwen3.5:4b
EmbeddingGemma es un modelo de embeddings de texto ligero, multilingüe y de código abierto de Google, diseñado específicamente para convertir texto en vectores. Es muy adecuado para la implementación offline en dispositivos locales. Ollama ya ha adaptado y encapsulado este modelo, por lo que puedes descargarlo y usarlo directamente.
Paso 1: Análisis y segmentación de documentos
El contexto del modelo es limitado y el documento completo no cabe, por lo que hay que dividirlo en fragmentos pequeños. La estrategia de segmentación usa el enfoque más simple de "longitud fija + solapamiento": cada fragmento tiene 300 caracteres y los fragmentos adyacentes se solapan en 50 caracteres para evitar cortar frases clave.
Ejemplo
import os
def load_chunks(folder, size=300, overlap=50):
"""Leer todos los archivos txt/md en la carpeta, dividirlos en fragmentos con marcadores de origen."""
chunks = []
for name in sorted(os.listdir(folder)):
if not name.endswith(('.txt', '.md')):
continue
path = os.path.join(folder, name)
text = open(path, encoding='utf-8').read().strip()
# Corte de ventana deslizante: paso = size - overlap
step = size - overlap
for i in range(0, len(text), step):
piece = text[i:i + size].strip()
if len(piece) > 20: # Filtrar fragmentos demasiado cortos
chunks.append({'file': name, 'text': piece})
return chunks
Paso 2: Vectorización y recuperación
Convierte todos los fragmentos y la pregunta del usuario en vectores y usa la similitud coseno para encontrar los top-k fragmentos más relevantes para la pregunta.
Ejemplo
EMBED_MODEL = 'embeddinggemma'
def embed(texts):
"""Genera vectores en lote (lista de entrada, lista de salida de vectores)"""
result = ollama.embed(model=EMBED_MODEL, input=texts)
return result['embeddings']
def cosine(a, b):
"""Similitud coseno: cuanto más cerca de 1, más similar"""
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a) ** 0.5
nb = sum(x * x for x in b) ** 0.5
return dot / (na * nb)
def search(question, chunks, vectors, top_k=3):
"""Recuperación: devuelve los k fragmentos más relevantes y sus similitudes"""
q_vec = embed([question])[0]
scored = [
(cosine(q_vec, v), c)
for v, c in zip(vectors, chunks)
]
scored.sort(key=lambda x: -x[0])
return scored[:top_k]
EmbeddingGemma genera vectores con normalización L2; aquí se mantiene la fórmula completa de la similitud coseno para que el principio quede claro; si se sustituye por el producto punto, el resultado es el mismo.
Paso 3: Ensamblar el prompt y generar respuestas con citas
Escribe los fragmentos recuperados junto con sus números de fuente en el prompt y pide explícitamente al modelo que "responda solo según los materiales" e indique las citas.
Ejemplo
GEN_MODEL = 'qwen3.5:4b'
def answer(question, chunks, vectors):
hits = search(question, chunks, vectors, top_k=3)
# Concatenar los fragmentos con los números de origen en el contexto
context = '\n\n'.join(
f'[Datos{idx}] (de {c["file"]})\n{c["text"]}'
for idx, (score, c) in enumerate(hits, 1)
)
prompt = fResponde a la pregunta basándote únicamente en la siguiente información y marca el número de la información utilizada.
Si la información es insuficiente para responder, indique directamente que no lo sabe.
Datos:
{context}
Pregunta: {question}'''
response = chat(
model=GEN_MODEL,
messages=[{'role': 'user', 'content': prompt}],
options={'temperature': 0.2}, # Baja temperatura, reduce la volatilización
)
print(response.message.content)
print('\nFuente de referencia: ')
for idx, (score, c) in enumerate(hits, 1):
print(f[Documento {idx}] {c["file"]} (similitud {score:.3f}))
Ejecución completa: punto de entrada del programa principal
Ejemplo
if __name__ == '__main__':
folder = 'docs' # Pon tus documentos txt/md en la carpeta docs
chunks = load_chunks(folder)
# Indexación fuera de línea: vectorizar todos los segmentos de una sola vez
print(f'Indexando {len(chunks)} fragmentos...')
vectors = embed([c['text'] for c in chunks])
print('Indexación completada.\n')
# Bucle de preguntas y respuestas en línea
while True:
q = input(Pregunta (exit para salir):).strip()
if q.lower() == 'exit':
break
if q:
answer(q, chunks, vectors)
print()
Prepara los materiales y ejecuta:
mkdir docs cp EJEMPLO-python-notes.md docs/ python rag.py
Salida:
$ python rag.py 正在索引 42 个片段... 索引完成。 问题(exit 退出):Python 的切片是什么? Python 切片是用 [start:stop:step] 从序列截取子序列的语法 [资料1]。 例如 s[1:4] 取索引 1 到 3 的字符 [资料2]。 参考来源: [资料1] EJEMPLO-python-notes.md(相似度 0.712) [资料2] EJEMPLO-python-notes.md(相似度 0.668)
Observa la indicación de fuente al final de la respuesta: cada frase se puede rastrear hasta un archivo concreto de tu directorio docs. Este es precisamente el valor central de RAG frente a "preguntar directamente al modelo".
Optimización de resultados y preguntas frecuentes
Si el resultado de RAG no es bueno, en el 80% de los casos el problema está en la recuperación, no en la generación. Las palancas de optimización, ordenadas por impacto:
| Palanca | Valor predeterminado | Dirección de ajuste |
|---|---|---|
| Tamaño de fragmento (size) | 300 caracteres | Si las respuestas quedan demasiado fragmentadas, auméntalo; si es demasiado grande, la recuperación pierde precisión. Rango recomendado: 200~500 |
| Solapamiento (overlap) | 50 caracteres | Si las frases se cortan con frecuencia, auméntalo. |
| Número de resultados a recuperar (top_k) | 3 | Auméntalo cuando los materiales sean densos, pero ocupará más contexto. |
| Umbral de similitud | No activado | Filtra los fragmentos con similitud demasiado baja para reducir interferencias irrelevantes. |
| Temperatura de generación | 0.2 | Mantén una temperatura baja; la base de conocimiento no necesita creatividad. |
Consulta rápida de problemas frecuentes:
| Síntoma | Causa | Solución |
|---|---|---|
| La respuesta no está relacionada con los materiales | La recuperación no encontró coincidencias | Comprueba si los fragmentos son demasiado pequeños o demasiado grandes y confirma que el idioma de la pregunta coincide con el de los materiales. |
| El modelo sigue inventando respuestas | Restricciones insuficientes en el prompt | Refuerza la expresión "solo según los materiales"; si no se recupera nada, responde directamente "no lo sé". |
| La indexación es muy lenta | Demasiados fragmentos, embeddings uno por uno | Usa embed por lotes; cuando el volumen de documentos sea grande, cambia a una biblioteca vectorial profesional con indexación incremental. |
| Las respuestas empeoran con documentos largos | La información clave se fragmenta | Segmenta por límites semánticos, como títulos/párrafos, en lugar de usar longitud fija. |
Otras ampliacionesHoja de ruta hacia producción: cuando el volumen de documentos supere las decenas de miles o se necesite análisis de PDF, reemplaza la "biblioteca vectorial basada en listas" por una biblioteca vectorial profesional como Chroma (que ofrece persistencia y búsqueda eficiente de vecinos cercanos), usa pypdf para extraer el texto de los PDF y el resto del esqueleto del código no cambia: este es precisamente el valor de desacoplar las etapas en este artículo.