- Published on
Acervo v0.5 — 21x más eficiente que un agente, y ahora recuerda lo que le decís
- Authors

- Name
- Sandy Veliz
- @sandy_veliz
v0.4 demostró que Acervo podía responder preguntas sobre proyectos indexados 12x más eficiente que un agente con tools. Pero si le contabas algo en una conversación, se olvidaba al turno siguiente. El pipeline conversacional estaba roto.
v0.5 arregla eso. Y de paso rehice la arquitectura completa.
El estado de la v0.4
Los benchmarks de v0.4 mostraban números sólidos para proyectos indexados: RESOLVE 100%, GROUND 92%, 12.1x de eficiencia vs un agente. Pero esos benchmarks corrían sobre grafos pre-construidos por el indexador. Cuando intentabas usar Acervo en una conversación pura — sin proyecto indexado, solo chateando — pasaba esto:
Turn 1: "Tenemos 4 proyectos: Butaco con Angular, Checkear con React..."
S1: extrae 9 entidades, 8 relaciones ✓
Grafo: 0 nodos ✗
Turn 2: "¿Qué proyectos usan Supabase?"
S2: 0 nodos activados
warm_tokens: 0
→ El LLM no recibe contexto del grafo. Responde desde la historia o inventa.
S1 extraía correctamente pero nada llegaba al grafo. Y aunque llegara, S2 no sabía traversar las relaciones para encontrar respuestas. Y aunque traversara, S3 no inyectaba el resultado al LLM.
Tres componentes rotos en cadena. La causa: un módulo de 1,848 líneas con dos code paths — uno para proyectos indexados (que funcionaba) y otro para conversaciones (que no).
Arquitectura hexagonal
La solución no era parchear. Era separar.
acervo/
├── ports/ Protocolos: LLM, Embedder, VectorStore, GraphStore, Telemetry
├── domain/
│ ├── pipeline.py Orquestador (~30 líneas)
│ ├── s1_extractor.py Extracción de entidades y topics
│ ├── s2_activator.py BFS sobre el grafo → HOT/WARM/COLD
│ ├── s3_assembler.py Compresión de contexto con budget por intent
│ └── s15_updater.py Persistencia post-respuesta
├── adapters/ OpenAI client, ChromaDB, JSON persistence
└── facade.py Wrapper backward-compatible
Cada stage es un módulo independiente con input/output definido. S2 tiene UN solo code path — funciona igual con un proyecto indexado de 231 nodos que con una conversación de 9 nodos. El pipeline orquestador son 30 líneas que llaman S1→S2→S3.
La API pública no cambió. from acervo import Acervo sigue funcionando igual.
BFS semántico — retrieval por distancia en el grafo
Este es el cambio técnico más importante de v0.5.
Cuando preguntás algo, lo que te importa está CERCA del topic en el knowledge graph. Si preguntás por Supabase, te importan los proyectos que usan Supabase (1 edge de distancia), y vagamente las otras tecnologías de esos proyectos (2 edges). No necesitás ver Angular, que está a 3+ edges de distancia.
S2 implementa esto como BFS (breadth-first search) con profundidad limitada:
Pregunta: "¿Qué proyectos usan Supabase?"
Seed: Supabase (match directo en el grafo)
BFS nivel 0 → HOT: Supabase
BFS nivel 1 → WARM: Checkear, Walletfy (1 edge: uses_technology)
BFS nivel 2 → COLD: React, Next.js (2 edges de distancia)
Contexto inyectado al LLM:
<ctx>
<hot>
Supabase [Tech] — used by: Checkear, Walletfy
</hot>
<warm>
Checkear [Project]: React + Supabase — gestión de tareas
Walletfy [Project]: Next.js + Supabase — finanzas personales
</warm>
</ctx>
81 tokens. Un agente necesitaría ~7,000 tokens y 3 tool calls.
Cada turno recalcula los layers desde cero. No hay cache, no hay estado de sesión. El grafo es el estado. Si la app crashea y reinicia, el próximo mensaje funciona igual — BFS desde el topic actual, mismo resultado.
Cuando el topic cambia ("Ahora hablemos de Angular"), el BFS arranca desde otro nodo y el contexto cambia instantáneamente. No hay "calentamiento" ni re-lectura de archivos. El grafo ya tiene todo.
Formato comprimido de contexto
v0.4 mandaba 616 tokens promedio de warm context. v0.5 manda 350. Misma información, 43% menos tokens.
El insight: los LLMs no necesitan prosa gramatical. Entienden formato compacto igual de bien:
ANTES (~25 tokens por nodo):
Checkear is a Project. It uses the technology React and the
technology Supabase. It is an app de gestión de tareas.
DESPUÉS (~12 tokens por nodo):
Checkear [Project]: React + Supabase — gestión de tareas
Tres cambios específicos:
XML tags cortos —
<ctx>,<hot>,<warm>en vez de<knowledge_context>,<primary_context>. 9 tokens menos solo en delimitadores.Relaciones legibles — "Used by: Checkear, Walletfy" en vez de "uses_technology: Checkear, uses_technology: Walletfy". El LLM entiende la relación y el humano puede debuggear.
Instrucción de grounding — "Answer using the knowledge context above. If the answer is not in the context, say so clearly." Una línea que reduce alucinaciones dramáticamente sin agregar tokens al contexto propiamente dicho.
Fine-tune v2 del extractor
El modelo de extracción (acervo-extractor-v2, publicado en HuggingFace) ahora produce dos campos nuevos en su output JSON:
- intent —
overview | specific | chat | followup— reemplaza el clasificador regex que teníamos antes. - retrieval —
summary_only | with_chunks— le dice a S2 si necesita traer chunks del vector store o si alcanza con los nodos del grafo.
El modelo se entrenó continuando desde el LoRA de v1, con learning rate reducido (5e-5 vs 2e-4 original) y ~390 ejemplos nuevos enfocados en intent classification y extracción de contenido indexado.
La precisión de intent está en 78% — el patrón de fallo dominante es clasificar preguntas specific como overview. Es training data para v3.
Grafo unificado
Una verificación importante: indexación y conversación alimentan el mismo grafo.
Cuando acervo index parsea un proyecto TypeScript, crea nodos de tipo file, section, symbol, entity. Cuando S1 extrae entidades de la conversación, crea nodos de tipo entity. Ambos usan el mismo _make_id() determinístico, el mismo upsert_entities() con dedup, y el mismo nodes.json.
Si curate creó un nodo "Express.js" durante indexación y después en la conversación el usuario menciona "Express.js", no se duplica — S1.5 appendea facts al nodo existente. S2 traversa ambas fuentes por igual en un solo BFS.
Verificado con un test combinado: P1 (Todo App, 232 nodos indexados) + 3 turnos conversacionales. El nodo "Sandy" (de conversación) coexiste con los 232 nodos indexados. S2 encontró 183 nodos (24 hot + 91 warm + 68 cold) mezclando ambas fuentes.
Graph Quality Specs
En v0.4, el curate a veces creaba entidades fantasma: "Ron Weasley" aparecía en un documento de project management, "MongoDB" en un proyecto que usa SQLite. Estas entidades contaminan el grafo y producen respuestas incorrectas.
v0.5 introduce specs de calidad automatizadas. Cada proyecto de test tiene un YAML que define qué DEBE y qué NO DEBE existir en el grafo:
expected_entities:
required:
- {label: "Sherlock Holmes", type: "person"}
- {label: "Express", type: "technology"}
forbidden:
- {label_contains: "Ron Weasley"}
- {label_contains: "MongoDB"}
- {label_contains: "Foundation Sprint"} # section title, not entity
Resultado: 85/85 checks pasan. Cero phantoms en los tres proyectos de test.
| Proyecto | Checks | Entidades | Nodos | Edges |
|---|---|---|---|---|
| P1 Code (TypeScript/React) | 31/31 ✓ | 7 | 231 | 1,109 |
| P2 Literature (Sherlock Holmes) | 22/22 ✓ | 5 | 40 | 307 |
| P3 PM Docs (sprints/issues) | 32/32 ✓ | 6 | 108 | 331 |
Tests de escenarios conversacionales
v0.5 agrega Layer 3 al sistema de benchmarks: escenarios conversacionales que testean cómo el grafo CRECE turno a turno.
C1: Portfolio de proyectos — 10 turnos. El usuario describe 4 proyectos con stacks tecnológicos, agrega personas al equipo, cambia de tema, vuelve, hace preguntas cruzadas. El grafo crece de 0 a 14 nodos.
| Turn | Fase | Nodos | Edges | Warm | Status |
|---|---|---|---|---|---|
| 1 | info_dump | 9 | 18 | 204 | ✗ (89% entity acc) |
| 2 | info_dump | 11 | 24 | 203 | ✓ (100%) |
| 3 | retrieval | 11 | 24 | 81 | ✗ (intent wrong) |
| 5 | chat | 11 | 24 | 0 | ✓ |
| 6 | update | 13 | 28 | 169 | ✓ |
| 8 | topic_change | 14 | 29 | 18 | ✓ |
| 9 | overview | 14 | 29 | 320 | ✓ |
| 10 | overview | 14 | 29 | 320 | ✓ |
7/10 turnos pasan. Los 3 fallos son gaps del modelo extractor (training data para v3), no bugs del pipeline.
C2: Conocimiento personal — libros leídos a un hijo. 3/6 pasan. Gap principal: el modelo no extrae "Mateo" como persona. Dominio personal = más training data necesario.
C3: Construcción progresiva — proyecto que evoluciona, incluyendo corrección ("cambié de SQLite a PostgreSQL"). 7/8 pasan. 100% entity accuracy. Único fallo: "¿Qué lenguajes sé?" no encuentra seed porque las tecnologías no están taggeadas como "lenguaje que el usuario sabe" vs "tecnología del proyecto."
Benchmarks: v0.4 vs v0.5
79 turns totales: 55 sobre proyectos indexados + 24 conversacionales.
| Métrica | v0.4.0 | v0.5.0 | Cambio |
|---|---|---|---|
| Eficiencia RESOLVE | 12.1x | 21.3x | ↑ 76% |
| Eficiencia GROUND | 9.2x | 20.8x | ↑ 126% |
| GROUND accuracy | 92% | 100% | ↑ zero hallucination |
| RESOLVE accuracy | 100% | 85% | ↓ (BFS más preciso, menos brute force) |
| RECALL | 67% | 67% | — |
| Pipeline conversacional | ✗ | ✓ (71%) | NUEVO |
| BFS semantic layers | ✗ | ✓ | NUEVO |
| Graph quality specs | ✗ | 85/85 | NUEVO |
| warm_tokens promedio | 616 | 350 | ↓ 43% |
RESOLVE bajó de 100% a 85% — tradeoff deliberado. El S2 de v0.4 activaba TODOS los nodos por label matching (brute force, siempre encontraba algo). El S2 de v0.5 es BFS desde seeds (preciso, pero si no encuentra el seed correcto, falla). Los 2 turnos que fallan son preguntas donde el keyword no matchea ningún label de nodo. Solucionable con fallback a keyword matching en v0.6.
Los ratios de eficiencia individuales llegan a 82.9x en P2 (Sherlock Holmes) — Acervo comprime un epub entero en ~181 tokens de contexto. Un agente tendría que leer el capítulo completo cada vez.
Lo que no funciona
RECALL: 67%. S1.5 no siempre extrae facts de la respuesta del asistente. Si el LLM dice "Alice es la lead developer" y S1.5 no lo persiste, el turno 12 no puede recordarlo. Más training data de S1.5 = más recall.
Intent: 78%. El modelo over-classifica como "overview" cuando debería ser "specific." 9 misclassificaciones en 55 turnos. Cada una es un ejemplo de training para el fine-tune v3.
Person extraction en dominio personal. "Mateo" (nombre del hijo en C2) no se extrae. El modelo fue entrenado mayormente con contexto técnico. Más diversidad en el training set.
Correcciones no borran el dato viejo. "Cambié de SQLite a PostgreSQL" agrega PostgreSQL al grafo pero no quita SQLite. El grafo acumula ambos. S1 no fue entrenado para detectar correcciones explícitas — v3.
Qué sigue
v0.6 tiene tres objetivos:
Fine-tune v3 — los tests de v0.5 generaron automáticamente los ejemplos de fallo. Intent classification, person extraction, correcciones, S1.5 recall.
Keyword fallback en S2 — cuando el BFS no encuentra seed, caer a label matching como v0.4 pero con el budget y formato nuevo de v0.5.
Topic-scoped vector search — cuando S2 identifica el cluster de nodos relevantes, el vector search se filtra a solo esos chunks en vez de buscar global.
Todo está público. El código, el training data, el modelo — todo open source bajo Apache 2.0.
→ Benchmark report interactivo
- Acervo core: https://github.com/sandyeveliz/acervo
- Extractor model v2: https://huggingface.co/SandyVeliz/acervo-extractor-v2
- Fine-tuning repo: https://github.com/sandyeveliz/acervo-models
- Acervo Studio: https://github.com/sandyeveliz/acervo-studio