La búsqueda vectorial ordena por similitud. La híbrida ordena por similitud más coincidencia de palabras clave. Pero lo que de verdad quieres de una búsqueda de código es relevancia, y la similitud no es relevancia. El fragmento que comparte más tokens con tu consulta a menudo no es el código que la responde.
Pregunta «dónde decidimos que una petición se puede reintentar» y un ranker por similitud te dará encantado la struct de configuración de reintentos, la constante de reintentos y el test que repite la palabra cuatro veces, mientras que la única función que toma la decisión se queda en el puesto siete. Todos los tokens coincidieron. Nada quedó respondido.
PageIndex hizo este planteamiento para documentos: sustituir el top-k por similitud por un LLM que razona sobre qué secciones son relevantes. Nosotros queríamos lo mismo para código, donde la estructura es más rica que el índice de un PDF: tienes símbolos, un grafo de llamadas, cuerpos de función reales.
Así que lo construimos: un paso opcional de razonamiento que va después de la recuperación híbrida, lee el código candidato y lo reordena según si de verdad responde a la consulta. Está desactivado por defecto, detrás de un único flag de configuración, y deja structural_search como el grep determinista y simple que debe ser. En nuestro benchmark es una mejora grande y generalizada. Este artículo es el informe honesto: los números, cómo lo ajustamos y las dos cosas que no funcionaron.
La idea central, y el truco que hizo que funcionara
El pipeline es corto. La consulta pasa por la recuperación híbrida exactamente igual que antes: similitud vectorial más coincidencia de palabras clave sobre el índice local. Los mejores candidatos vuelven con sus cuerpos de código completos. Un LLM los lee y los ordena según si responden a la pregunta. Después se fusionan los dos rankings y se devuelven los primeros resultados. Sin índice nuevo, sin una segunda pasada de embeddings, sin reindexar.
La versión ingenua de ese paso intermedio es obvia: coges los candidatos, preguntas «cuáles de estos responden a la consulta, ordenados» y usas ese orden. Eso hicimos primero. Fue un desastre para el recall.
Este es el reranker de razonamiento puro sobre 127 consultas:
| métrica | solo híbrido | razonamiento puro | Δ |
|---|---|---|---|
| MRR | 0,595 | 0,752 | +0,157 |
| NDCG@10 | 0,658 | 0,758 | +0,100 |
| Hit@10 | 0,913 | 0,843 | −0,071 |
| Recall@10 | 0,886 | 0,811 | −0,075 |
Las métricas de ordenación se dispararon: la respuesta correcta subió a lo más alto. Pero Hit@10 y Recall@10 bajaron, porque el modelo poda: devuelve el puñado que considera relevante y descarta en silencio el resto, incluidos verdaderos positivos que estaban en las posiciones 6–10. Un reranker que mejora la precisión tirando el recall no es un reranker que se lleva a producción. Parece brillante en las consultas que acierta y deja sin respuesta las difíciles.
El arreglo es pequeño y es la razón entera de que esto funcione: no dejes que el LLM sustituya la ordenación, fusiónala con la ordenación híbrida mediante Reciprocal Rank Fusion.
score(candidate) = 1 / (k + hybrid_rank)
+ reasoning_weight * 1 / (k + reasoning_rank)
k es la constante de amortiguación habitual de RRF, que evita que una sola posición domine la suma. Lo importante es qué hace cada término. El rango híbrido siempre aporta, así que actúa como suelo de recall: un verdadero positivo que el LLM degradó u omitió baja, pero no desaparece. El rango del razonamiento, ponderado, manda en la cabeza de la lista. Conservas la ganancia en ordenación y dejas de perder recall.
Ese único cambio convirtió el compromiso del razonamiento puro en una victoria limpia en todas las métricas.
Los números
Benchmark: 127 consultas anotadas a mano contra el código de Octocode fijado en un commit concreto (100 estándar + 27 deliberadamente difíciles, en lenguaje natural y sin coincidencia de palabras clave). Embeddings locales con fastembed, búsqueda híbrida activada, las mismas consultas en ambos lados y deepseek:deepseek-v4-flash como modelo de razonamiento. La verdad de referencia es el solapamiento de rangos de líneas con ubicaciones verificadas en el código fuente.
| métrica | solo híbrido | + razonamiento | Δ |
|---|---|---|---|
| MRR | 0,595 | 0,809 | +0,214 (+36%) |
| NDCG@10 | 0,658 | 0,833 | +0,175 (+27%) |
| Hit@5 | 0,827 | 0,953 | +0,126 |
| Hit@10 | 0,913 | 0,969 | +0,055 |
| Recall@5 | 0,777 | 0,924 | +0,147 |
| Recall@10 | 0,886 | 0,944 | +0,058 |
Todas las métricas al alza. Las dos que más importan para «encuéntrame el código que hace X» — MRR (cuán arriba cae el primer resultado correcto) y NDCG@10 (si los resultados más relevantes quedan primero) — subieron un 36% y un 27%. El Hit@5 pasó de 0,83 a 0,95: diecinueve de cada veinte veces la respuesta está ahora entre los cinco primeros.
Ese último número es el que cambia el comportamiento de un agente. Cuando la respuesta está de forma fiable en el top cinco, el agente lee cinco resultados y actúa. Cuando no, el agente amplía la búsqueda, lee más archivos, quema contexto en candidatos que nunca fueron relevantes y a veces se rinde y hace grep. Calidad de ordenación aguas arriba es contexto ahorrado aguas abajo.
Cómo lo ajustamos (y dónde más era peor)
No adivinamos la configuración. La barrimos, sobre un índice compartido, una dimensión cada vez.
Peso del razonamiento: cuánto se apoya la fusión en el LLM frente al suelo híbrido. Barrido con 1, 2, 3 y 5. La ordenación mejora con el peso y se estanca alrededor de 2–3; con 5 no ganas nada y cuestas un poco de recall, porque a esa altura el suelo híbrido deja de contar y vuelves a confiar solo en el modelo. En el benchmark completo, 2,0 fue la mejor opción global.
Cuántos candidatos pasar al razonamiento. Barrido con 15, 25, 35 y 40. Más no es mejor: 40 fue peor que 25 de forma medible. Darle más candidatos al modelo diluye la decisión, y cuesta más tokens para obtener un resultado peor. 25 es el punto óptimo.
Cuánto ve el modelo de cada candidato. Esto fue decisivo. Solo firmas, un fragmento o el cuerpo completo:
| contexto | Hit@5 | MRR | Recall@5 |
|---|---|---|---|
| firmas | 0,933 | 0,806 | 0,900 |
| fragmentos | 0,933 | 0,849 | 0,883 |
| cuerpo completo | 1,000 | 0,857 | 0,967 |
(Subconjunto de 30 consultas; los valores absolutos salen más altos en el subconjunto, pero lo relevante es el orden.)
Los cuerpos de código completos ganan por mucho. El modelo necesita ver el código real para juzgar la relevancia: los nombres y las firmas no bastan. La opción de solo firmas fue la peor, y merece la pena detenerse ahí: es la más barata, la que te pide el cuerpo para ahorrar tokens, y tira justo la información que el paso de razonamiento existe para usar.
Temperatura del LLM. Dábamos por hecho que más baja (más determinista) ordenaría mejor. Nos equivocamos: la temperatura 1,0 superó a 0,3 y 0,0. El modelo razona mejor con muestreo normal.
Lo que no funcionó: descripciones contextuales en tiempo de indexación
La forma obvia de subir el recall es enriquecer el índice: pedir a un LLM que escriba una línea de «qué hace este código» para cada fragmento y embeberla junto al código (el contextual retrieval de Anthropic reportó grandes caídas en recuperaciones fallidas haciendo exactamente esto). Ya teníamos la maquinaria, así que lo probamos: reindexado completo con descripciones y el mismo A/B.
Empeoró ligeramente las cosas:
| Hit@5 | MRR | NDCG@10 | Recall@10 | |
|---|---|---|---|---|
| índice plano (+razonamiento) | 0,953 | 0,809 | 0,833 | 0,944 |
| índice contextual (+razonamiento) | 0,945 | 0,836 | 0,859 | 0,925 |
| Δ | −0,008 | +0,027 | +0,026 | −0,019 |
Cambia recall por ordenación: MRR y NDCG suben un poco, pero Hit y Recall bajan. Anteponer una descripción del LLM diluye los tokens propios del código en el embedding, y sobre código limpio y bien estructurado con un embedder de código decente eso arrastra el recall hacia abajo. La mejora de recall que midió Anthropic fue sobre prosa y corpus mixtos, no sobre esto. ¿Y el valor de ordenación que aportaría el contexto? El paso de razonamiento ya lo captura, lo que hace que lo contextual encima sea redundante, cuando no perjudicial.
Así que no lo lanzamos. Se queda desactivado, y conviene nombrar el coste de esa decisión: un índice contextual significa una llamada al LLM por fragmento en cada reindexado, para un resultado que midió peor. Vale la pena decirlo claro porque «añade contextual retrieval» se repite por todas partes como culto al cargo: no es una victoria universal y, sobre código, puede salirte cara.
Cómo activarlo
El razonamiento está desactivado por defecto (cuesta una llamada al LLM por búsqueda). Se activa en config.toml:
[search.reasoning]
enabled = true
model = "deepseek:deepseek-v4-flash" # cualquier provider:model
max_candidates = 25 # 25 ganó a 40 — más diluye
context_level = "full" # el cuerpo completo gana; fragmentos/firmas son peores
reasoning_weight = 2.0 # peso RRF frente al suelo de recall híbrido
final_top_k = 10
Cada parámetro está definido en la plantilla: configuración estricta, sin valores por defecto ocultos en el código. deepseek-v4-flash es barato y rápido; funciona cualquier proveedor, y los números de arriba son lo que consigue un modelo barato, no uno de frontera. Solo se ejecuta en la ruta de búsqueda semántica: structural_search sigue siendo grep puro, sin IA, como debe ser. Si quieres búsqueda determinista y a coste cero, deja el flag apagado y nada de tu configuración actual cambia.
El techo honesto
Estamos en Hit@5 0,953. Llegar a 1,0 no es realista en este benchmark: las últimas 27 consultas son deliberadamente adversarias y parte de la verdad de referencia es genuinamente ambigua. La brecha que queda no es recall del pool (la prueba contextual confirmó que el conjunto de candidatos ya está bien cubierto), son consultas realmente difíciles. Un embedder base más fuerte en producción (un modelo de embeddings hecho para código en lugar del fastembed local por defecto) subiría más el suelo, pero el enfoque de razonamiento en sí ya está en su mejor punto práctico.
Lo que nos gusta de este resultado: es una síntesis de verdad. La búsqueda híbrida te da recall, barato y determinista. El razonamiento te da relevancia, allí donde ya estás gastando en un LLM. RRF los fusiona para que no tengas que elegir. Sin índice nuevo, sin reescritura sin vectores, sin reindexar: un flag, una llamada al LLM, y el código correcto sube arriba.
FAQ
¿Qué es la recuperación con razonamiento, en una frase?
Un paso opcional después de la recuperación híbrida en el que un LLM lee los cuerpos de código candidatos, los ordena según si responden a la consulta, y esa ordenación se fusiona con la híbrida mediante Reciprocal Rank Fusion en lugar de sustituirla.
¿Sustituye a la búsqueda vectorial o híbrida?
No, y ahí está la clave. Dejar que el LLM se quedara con la ordenación nos costó 7 puntos de Recall@10. El rango híbrido sigue en la puntuación como suelo de recall, así que un verdadero positivo que el modelo pasó por alto baja en vez de desaparecer.
¿Cuánto cuesta?
Una llamada extra al LLM por búsqueda semántica, sobre 25 candidatos como máximo. Está desactivado por defecto, y los números del benchmark se obtuvieron con deepseek:deepseek-v4-flash, un modelo barato y rápido. Vale cualquier provider:model si quieres gastar más.
¿Ralentiza o cambia structural_search?
No. El razonamiento solo se ejecuta en la ruta de búsqueda semántica. structural_search sigue siendo grep determinista, sin LLM de por medio.
¿Debería activar también las descripciones contextuales en la indexación?
Sobre código, nuestra medición dice que no: mejoró un poco MRR y NDCG pero costó Hit@5 y Recall@10, y la ganancia de ordenación ya la cubre el paso de razonamiento. Además añade una llamada al LLM por fragmento en cada reindexado.
¿Necesito reindexar para usarlo?
No. El razonamiento es un paso en tiempo de consulta sobre el índice que ya tienes. Activa el flag, reinicia y listo.
— Don
Octocode es código abierto bajo Apache-2.0 — con el benchmark, la verdad de referencia y los resultados en crudo incluidos, así que puedes reproducir estos números en tu propio código. Es el motor de búsqueda de código detrás de Octomind; el servidor MCP es cómo se hablan.


