El repo tenía unos 11.000 archivos. El agente disponía de una herramienta bash y una instrucción vaga: "encuentra dónde validamos las firmas de los webhooks". Ejecutó grep -rn "signature" ., obtuvo 600 coincidencias repartidas entre JS de terceros, fixtures de pruebas, un CHANGELOG y tres helpers criptográficos sin relación, y luego apuntó con total seguridad al archivo equivocado. Lo vimos hacer esto cuatro veces seguidas con frases distintas antes de que alguien dijera lo obvio: grep busca cadenas de texto, y el agente estaba haciendo una pregunta sobre significado.
Ese es el problema entero en una frase. grep y rg son excelentes para "dónde aparece exactamente este token". Son inútiles para "dónde está la cosa que hace X", porque el código que hace X casi nunca contiene las palabras que usarías para describir X. La función se llama verify_hmac. Tú buscaste "webhook signature". Cero solapamiento, y la única herramienta que el agente toca primero no devuelve nada útil.
La solución es la búsqueda semántica: incrustar (embed) el código, incrustar la consulta, encontrar los fragmentos cuyo significado esté más cerca. El problema es que la mayoría de los consejos de "simplemente usa embeddings" dan por hecho que enviarás tu código fuente a una API en la nube y alquilarás una GPU. Para un monorepo privado, ninguna de las dos cosas es aceptable: el equipo legal no aprobará subir código propietario a un tercero, y no quieres una factura por token que crece con cada reindexado.
Así que esta es la configuración que de verdad ejecutamos: Octocode, indexando un repo grande para búsqueda semántica totalmente en local: sin GPU, sin embeddings en la nube, nada que salga de la máquina. Aquí está exactamente cómo funciona, la configuración que importa y las cuatro cosas que nos mordieron.
Por qué grep no basta, en concreto
Antes de recurrir a los embeddings, vale la pena ser preciso sobre por qué la búsqueda de texto falla para los agentes, porque eso te dice qué tiene que hacer el reemplazo.
- Desajuste de vocabulario. La consulta es la intención ("rate limiting"), el código es el mecanismo (
Semaphore,token_bucket,RetryAfter). No hay tokens en común. - Sin ranking.
rgte da todas las coincidencias con el mismo peso. 600 coincidencias no son una respuesta; son un segundo problema de búsqueda. - Sin estructura. Una coincidencia de texto en un comentario, en una prueba y en una función central son idénticas para
grep. No puede decirte que la tercera es la definición.
Lo que quieres en su lugar es: ordenar por cercanía semántica, pero sin perder aquello en lo que grep es genuinamente bueno: las coincidencias exactas de identificadores. La respuesta de Octocode es hacer ambas cosas y fusionarlas, a lo que llegaré. Primero, cómo se convierte el código en una forma que se pueda buscar.
Cómo Octocode fragmenta e indexa el código
El RAG estándar corta los archivos en ventanas de texto de tamaño fijo: cada N caracteres, con algo de solapamiento, e incrustar. Para prosa está bien. Para código es activamente malo: parte una función en dos fragmentos, incrusta la mitad de un brazo de match y pierde el hecho de que un método pertenece a un tipo.
Octocode analiza primero cada archivo con tree-sitter, recorre el AST y fragmenta siguiendo límites de símbolos reales (funciones, métodos, clases, módulos) en lugar de desplazamientos de bytes arbitrarios. La arquitectura es un parser por lenguaje que extrae funciones, clases, imports y exports, y luego procesamiento por fragmentos para todo lo que sea demasiado grande para incrustar de una pieza. Un método extraído de dentro de un bloque impl o de una clase lleva el nombre de su tipo padre en su lista de símbolos, así que buscar Suppression.mark_set o Foo.bar acierta directamente con el fragmento del método.
La cobertura de lenguajes son gramáticas tree-sitter reales, no heurísticas con regex. Según el repo, indexados como código con análisis AST completo:
| Lenguaje | Extensiones |
|---|---|
| Rust | .rs |
| Python | .py |
| TypeScript / JavaScript | .ts, .tsx, .js, .jsx |
| Go | .go |
| PHP | .php |
| C / C++ | .c, .h, .cpp, .hpp, .cc, .cxx, más las extensiones de módulo de C++20 .cppm, .ixx, .mxx, .ccm, .cxxm |
| Ruby | .rb |
| Java | .java |
| Swift | .swift |
| JSON, Bash, CSS, Lua, Svelte, Markdown | rutas tree-sitter dedicadas |
Todo lo demás que sea útil como contexto (yaml, toml, dockerfile, makefile, ini, conf, env, xml, html, sql, rst y compañía) se indexa como bloques de texto, así que sigue siendo buscable, solo que sin extracción semántica de símbolos.
Los parámetros de fragmentación viven bajo [index]:
[index]
chunk_size = 2000 # máximo de caracteres por fragmento de código
chunk_overlap = 100 # solapamiento entre fragmentos adyacentes
quantization = true # compresión vectorial RaBitQ, ~32x, pérdida de calidad mínima
require_git = true # por defecto, solo indexa dentro de un repo git
chunk_size es un techo, no un objetivo: una función de 40 líneas más pequeña que el límite se queda entera. El límite solo entra en juego con cuerpos genuinamente grandes, y Octocode ya indexa clases grandes en Python, TypeScript, C++ y Ruby método por método, en lugar de volcar la clase entera en un solo fragmento. El sentido de todo esto: cuando más tarde busques "la función que gestiona la configuración del pull remoto", la unidad que se incrusta es la función, con su nombre y su tipo padre adjuntos, no una ventana que la atraviesa por casualidad.
Embeddings locales: sin GPU, sin nube, sin clave de API
La parte que la gente da por hecho que requiere la nube: convertir código en vectores. No la requiere.
Octocode incluye un stack local-first construido sobre fastembed (con ONNX Runtime por debajo). Apuntas la configuración a un modelo local con el prefijo fastembed: y los embeddings se ejecutan en la CPU, en tu máquina:
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code" # 768-d, diseñado para código
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5" # texto y documentos
Una cadena de modelo siempre es provider:model. El prefijo del proveedor es toda la decisión de enrutamiento: fastembed: y huggingface: son locales (sin clave, sin red); voyage:, jina:, google:, openai:, together:, octohub: son de nube y necesitan la clave de API correspondiente en el entorno. Mézclalos si quieres (embeddings de código locales, embeddings de texto en la nube), pero para un repo privado el camino totalmente local es el objetivo.
Aquí no hay requisito de GPU ni ruta de código para GPU: esto es inferencia ONNX en CPU. En una máquina moderna de varios núcleos con AVX2, es lo bastante rápido como para que el embedder rara vez sea el cuello de botella; normalmente lo son la E/S de disco y el análisis con tree-sitter. Las propias notas de rendimiento del repo sitúan la indexación local con FastEmbed alrededor de 45 segundos para 1.000 archivos como orden de magnitud aproximado; tu resultado variará según el tamaño de los archivos y el número de núcleos, así que tómalo como ilustrativo, no como una promesa.
Dos advertencias honestas:
- La primera ejecución descarga el modelo. El primer
octocode indextras apuntar a un modelofastembed:descarga los pesos ONNX (unos cientos de MB) a un directorio de caché (~/.local/share/octocode/fastembed/en Linux/macOS). Coste único, y después funciona offline para siempre. - Los proveedores locales dependen de la plataforma y van detrás de feature flags. FastEmbed y HuggingFace requieren las características de compilación
fastembed/huggingface. Las builds por defecto las incluyen donde están bien soportadas; en plataformas donde no, recurres a un proveedor de nube. Compruebaoctocode models list fastembedpara ver qué expone realmente tu binario. El reciente trabajo de proveedores de embeddings locales en 0.17.0 es lo que hizo que ese listado fuera honesto sobre qué está disponible y dónde.
Si nunca has configurado búsqueda semántica sobre código, la introducción a la búsqueda semántica de código de Octocode cubre los conceptos básicos; este post asume que ya los conoces y quieres la configuración de producción.
Indexar el repo
Con la configuración lista, indexar es un solo comando desde la raíz del repo:
cd /path/to/big-monorepo
octocode index
# → Indexed 12,847 blocks across 342 files
Lo que ocurre en realidad, en orden: descubrir archivos (respetando las reglas de ignorado), analizar cada uno con tree-sitter, extraer símbolos, fragmentar siguiendo los límites del AST, incrustar cada fragmento localmente y escribir todo en un almacén columnar LanceDB. La base de datos no vive en tu repo: se indexa por proyecto bajo el directorio de datos del sistema (~/.local/share/octocode/<project-id>/storage en Linux/macOS). Eso mantiene tu árbol de trabajo limpio y significa que un git clean -fdx no borrará tu índice.
Flags útiles:
octocode index --force # ignora la caché incremental, reconstruye desde cero
octocode index --verbose # ver el progreso por archivo
octocode stats # cuántos bloques de código/texto/docs, nodos del grafo, obsolescencia del índice
octocode stats es el comando a ejecutar cuando algo te da mala espina: te dice si tu HEAD coincide con el último commit indexado, así que ves de un vistazo si el índice está obsoleto.
Incremental vs. reindexado completo
Las ejecuciones posteriores de octocode index son incrementales: solo los archivos cambiados se vuelven a analizar y a incrustar. Cada ejecución de indexado termina con una pasada de optimización de tablas para que la ruta de consulta siga siendo rápida a medida que la base de datos crece (la indexación incremental repetida solía dejar una cola sin indexar que las búsquedas tenían que escanear por fuerza bruta; eso ahora se gestiona automáticamente).
Cuando cambias cómo se indexan las cosas (una nueva regla de fragmentación, un modelo de embedding distinto), lo incremental no volverá a fragmentar retroactivamente los archivos que ya tiene. Cambiar el modelo de embedding es el caso importante: los vectores de un modelo no son comparables con los de otro. Tras cambiar code_model o text_model, haz una reconstrucción limpia:
octocode clear # elimina las tablas indexadas
octocode index # reconstruye con el nuevo modelo
Modo watch: mantén el índice vivo
Un índice de una sola pasada queda obsoleto en el momento en que alguien hace commit. Para un agente que se supone que responde preguntas sobre el estado actual del repo, ejecuta el watcher:
octocode watch # reindexa automáticamente al cambiar archivos
octocode watch --debounce 5 # espera 5s tras el último cambio antes de reindexar
octocode watch --quiet
El watcher aplica debounce (configurable, 1–30 segundos), así que un git checkout que toca 400 archivos dispara un reindexado, no 400. Respeta las mismas reglas de ignorado que un índice completo, así que editar un archivo de node_modules no lo despierta.
Si ejecutas el servidor MCP (siguiente sección), hay una opción aún más limpia: pon mcp_index = true bajo [index] y el servidor MCP indexa y vigila en el mismo proceso: una sola cosa en ejecución en lugar de dos. El valor por defecto es false, que sirve un índice existente en modo solo lectura y asume que indexas por separado; actívalo cuando quieras que el servidor sea dueño de todo el ciclo de vida.
Búsqueda híbrida: semántica y exacta, fusionadas
Esta es la parte que arregla el fallo original de grep sin tirar a la basura la única fortaleza de grep.
La búsqueda puramente vectorial pierde con consultas densas en identificadores. Busca parse_remote y los embeddings densos te devolverán encantados cosas que están semánticamente cerca de parse_remote mientras se saltan la función literalmente llamada parse_remote. La búsqueda por palabras clave BM25 tiene el fallo opuesto: clava los identificadores exactos y falla con intenciones parafraseadas como "función que gestiona la configuración del pull remoto".
Octocode ejecuta ambas en cada consulta y las fusiona con Weighted Reciprocal Rank Fusion, dentro de LanceDB. Los pesos están expuestos:
[search.hybrid]
enabled = true
default_vector_weight = 0.7
default_keyword_weight = 0.3
Para un repo denso en código donde los identificadores exactos cargan mucha señal, inclínate hacia las palabras clave. El propio benchmark de recuperación del repo (127 consultas de búsqueda de código con verdad de referencia por rango de líneas, ejecutadas totalmente en local con jina-embeddings-v2-base-code y sin reranker) muestra el efecto con claridad: pasar del reparto por defecto 0.7/0.3 a un 0.3/0.7 ajustado a palabras clave subió Hit@5 de 0,598 a 0,732 y Recall@10 de 0,671 a 0,807, sin coste añadido. Eso es un +22% en Hit@5 a partir de una línea de configuración. Para documentos extensos, inclínate al otro lado (0.8/0.2), ya que ahí domina la intención.
Encima de la fusión, un reranker opcional reordena los mejores candidatos:
[search.reranker]
enabled = true
model = "fastembed:jina-reranker-v2-base-multilingual" # reranker local, sin clave
top_k_candidates = 50 # trae esta cantidad de la búsqueda vectorial
final_top_k = 10 # devuelve esta cantidad tras el reranking
El reranker también se ejecuta en local si lo apuntas a un modelo fastembed:. Es una pasada con cross-encoder sobre los 50 mejores candidatos, que captura casos donde el ranking de primera etapa equivocó el orden. El coste es real pero acotado: solo ve top_k_candidates, no el índice entero.
Desde la CLI, la superficie de búsqueda es directa:
octocode search "webhook signature verification"
octocode search "auth" "middleware" "session" # multi-consulta, hasta 10
octocode search "database connection pool" --mode code # restringe a bloques de código
octocode search "auth" --detail-level signatures # solo firmas, compacto
octocode search "authentication refactor" --mode commits # busca en el historial de git
La ruta --mode commits merece conocerse: el historial de commits se indexa de forma perezosa en la primera búsqueda de commits (no durante octocode index), así que puedes preguntar "cuándo cambiamos el flujo de auth" y obtener commits ordenados semánticamente, no solo un git log --grep.
Búsqueda estructural: cuando necesitas patrones de AST, no significado
A veces no quieres semántica en absoluto: quieres "cada .unwrap() en Rust" o "cada new Foo() en JS". Eso es una consulta estructural, y la búsqueda de texto se equivoca constantemente (un comentario que menciona .unwrap() no es una llamada). Octocode envuelve ast-grep:
octocode grep '$FUNC.unwrap()' --lang rust
octocode grep 'new $CLASS($$$ARGS)' --lang javascript
octocode grep 'console.log($ARG)' --lang javascript --rewrite 'logger.info($ARG)' --update-all
$VAR coincide con un nodo del AST, $$$ARGS coincide con una secuencia de argumentos, y --rewrite hace refactorización consciente del AST in situ. Lo genuinamente útil para los agentes: los LLM se equivocan de forma fiable con el kind del nodo; escribirán function_declaration para Python, que en realidad es function_definition. Cuando un patrón no coincide con nada, Octocode prueba interpretaciones progresivamente más laxas y conoce el kind correcto por lenguaje, así que el casi-acierto del agente sigue devolviendo resultados en lugar de un cero silencioso.
GraphRAG: relaciones, no solo similitud
La búsqueda semántica encuentra código similar. No puede decirte que auth_middleware.rs importa jwt.rs, llama a user_store.rs y está conectado a router.rs. Eso es una pregunta de grafo, y Octocode construye el grafo durante la indexación.
[graphrag]
enabled = true
use_llm = true
Extrae aristas imports, calls, extends e implements a lo largo de nueve lenguajes desde el AST, y con use_llm = true añade relaciones arquitectónicas de más alto nivel (configures, uses, patrones factory/observer/strategy) descubiertas por un LLM. Desde la CLI:
octocode graphrag get-relationships --node-id "src/auth/middleware.rs"
octocode graphrag find-path --source-id "src/auth/mod.rs" --target-id "src/database/mod.rs"
octocode graphrag overview
Esta es la diferencia entre "muéstrame código que parezca auth" y "muéstrame todo lo que depende de el módulo de auth". Para un agente que hace una refactorización, la segunda pregunta es la que evita roturas.
Hay un giro más nuevo y opcional: expansión de recuperación consciente del grafo. Con [search] graph_expansion = true (y GraphRAG habilitado), la búsqueda incorpora bloques de código de archivos estructuralmente relacionados con tus mejores aciertos antes del reranking, de modo que una consulta que aterriza en el middleware de auth puede sacar a la luz el helper de JWT que llama, incluso si ese helper no coincidía con el texto de la consulta. Está desactivado por defecto y los comentarios del código son contundentes al respecto: pruébalo con A/B en tu propia evaluación antes de confiar en él, porque la expansión puede añadir ruido con la misma facilidad que señal.
Conectarlo a un agente vía MCP
Todo lo anterior se expone a los asistentes de IA a través de un servidor MCP integrado. Esta es la forma principal de usar Octocode: el agente obtiene herramientas, no un shell.
octocode mcp --path /path/to/your/project
O en la configuración de un cliente (Claude Desktop, Cursor, Windsurf, Claude Code):
{
"mcpServers": {
"octocode": {
"command": "octocode",
"args": ["mcp", "--path", "/path/to/your/project"]
}
}
}
Las herramientas que ve el agente, verificadas contra el servidor:
| Herramienta MCP | Qué hace |
|---|---|
semantic_search |
Búsqueda híbrida semántica + palabras clave, multi-consulta, todos los modos incluido commits |
view_signatures |
Estructura de archivos (firmas, clases, imports) por glob, sin leer archivos enteros |
structural_search |
Coincidencia de patrones de AST vía ast-grep, con la recuperación por kind |
graphrag |
Consultas de relaciones: search, get-node, get-relationships, find-path, overview |
view_signatures es la heroína anónima. En lugar de que un agente queme contexto leyendo tres archivos de 800 líneas para encontrar la firma de una función, pide las firmas por glob y obtiene el esqueleto. (En versiones recientes puedes pasar una sola cadena de glob, no solo un array, una cosa pequeña que detuvo muchas llamadas a herramientas malformadas.)
Para un monorepo con muchos subproyectos, hay un modo multi-repo: octocode mcp --multi --path /workspace escanea los subdirectorios inmediatos buscando repos git y los sirve todos desde un único endpoint, con cada herramienta ganando un argumento project para elegir el destino. Un solo servidor MCP, todos los repos del workspace.
Las cuatro cosas que nos mordieron
Reales, no hipotéticas: esto es lo que nos costó tiempo.
1. El índice vive fuera del repo, y eso confundió a todo el mundo. La primera vez que alguien ejecutó el agente sobre un clon recién hecho, no tenía índice: la base de datos es por proyecto bajo ~/.local/share/octocode/, no en el árbol de trabajo. Clonar ≠ indexar. La solución es una línea en el onboarding: tras clonar, ejecuta octocode index una vez. Obvio en retrospectiva; nada obvio a las 2 de la madrugada.
2. Cambiar de modelo de embedding degradó silenciosamente los resultados. Alguien cambió code_model para probar un modelo local distinto y no limpió el índice. Las consultas nuevas se incrustaron con el nuevo modelo, los fragmentos viejos con el antiguo, los vectores no eran comparables, rankings basura. Los vectores de modelos distintos no viven en el mismo espacio. Siempre octocode clear && octocode index tras cambiar un modelo de embedding. No hay ninguna advertencia que te salve aquí: simplemente empeora en silencio.
3. El reparto vector/palabra clave por defecto era incorrecto para nuestro repo. Estuvimos una semana con 0.7/0.3 (orientado a vectores) y seguíamos fallando consultas de identificadores exactos. El código es denso en identificadores; el benchmark de arriba muestra que los pesos ajustados a palabras clave son drásticamente mejores para la búsqueda de código. Cambiar a 0.3/0.7 fue el ajuste de configuración de mayor impacto que hicimos.
4. Los archivos binarios y generados no se indexan, y eso es correcto, pero compruébalo. Octocode se salta los archivos binarios (comprueba si hay bytes nulos y una proporción de caracteres imprimibles antes de tratar el contenido como texto) y respeta .gitignore, .git/info/exclude y los archivos .noindex. Eso es exactamente lo que quieres: no quieres embeddings de bundles de terceros minificados. Pero si un directorio que te importa está en gitignore (algunos equipos ponen en gitignore los clientes de API generados), no se indexará y te preguntarás por qué la búsqueda no lo encuentra. Coloca un .noindex para excluir rutas adicionales; revisa tu .gitignore si falta algo que esperabas. octocode stats te mostrará los recuentos de bloques para que puedas comprobar la cobertura.
Una configuración mínima de extremo a extremo
Juntándolo todo, esto es lo completo para un monorepo privado, todo en local:
# ~/.local/share/octocode/config.toml (o una sobrescritura local del proyecto)
[embedding]
code_model = "fastembed:jinaai/jina-embeddings-v2-base-code"
text_model = "fastembed:nomic-ai/nomic-embed-text-v1.5"
[search.hybrid]
enabled = true
default_vector_weight = 0.3 # código denso en identificadores → favorece palabras clave
default_keyword_weight = 0.7
[search.reranker]
enabled = true
model = "fastembed:jina-reranker-v2-base-multilingual"
[graphrag]
enabled = true
use_llm = false # relaciones solo por AST, sin llamadas a LLM, totalmente offline
cd /path/to/monorepo
octocode index # la primera ejecución descarga modelos, luego indexa
octocode search "webhook signature verification" # comprobación de sanidad
octocode watch --quiet & # mantenlo vivo
claude mcp add octocode -- octocode mcp --path . # conéctalo al agente
Nada en ese flujo toca la red tras la descarga única de modelos. Sin GPU. Sin factura por token. El agente que estaba haciendo grep de "signature" y apuntando al archivo equivocado ahora llama a semantic_search, obtiene verify_hmac rankeado primero, y view_signatures para confirmar antes de tocar nada.
Preguntas frecuentes
¿Necesito una GPU? No. Los embeddings locales se ejecutan en CPU vía ONNX Runtime. Una CPU moderna de varios núcleos es suficiente; el embedder rara vez es el cuello de botella.
¿Sale algo de código de mi máquina? Con modelos fastembed:/huggingface: y graphrag.use_llm = false, no: indexación, búsqueda y reranking son todos locales. Los proveedores de embeddings en la nube y las funciones de GraphRAG/mensajes de commit con LLM son opcionales y obviamente necesitan claves.
¿Dónde vive el índice, y está en mi repo? Bajo el directorio de datos del sistema, indexado por proyecto (~/.local/share/octocode/<project-id>/ en Linux/macOS), no en tu árbol de trabajo. Un git clean no lo tocará; un clon recién hecho no lo tendrá.
¿Cómo de grande se vuelve la base de datos? Aproximadamente del orden de ~10KB por archivo, con la cuantización RaBitQ dando ~32x de compresión vectorial. Un repo de 10k archivos son decenas a pocos cientos de MB, no gigabytes.
¿Incremental o reindexado completo? Incremental por defecto: solo se vuelven a incrustar los archivos cambiados. Fuerza una reconstrucción completa con octocode index --force, o octocode clear && octocode index tras cambiar el modelo de embedding.
¿Puede buscar en el historial de git? Sí, octocode search "..." --mode commits. Los commits se indexan de forma perezosa en la primera búsqueda de commits, así que el octocode index inicial se mantiene rápido.
— Don
Octocode es de código abierto bajo Apache-2.0. El stack local-first llegó en 0.15.0 y el listado de proveedores de embeddings locales en 0.17.x. Si tu configuración tropieza con un problema que este post no cubrió, abre un issue: así es como se anotaron la mayoría de estos consejos en primer lugar.



