Volver al inicio

Sistema RAG para 1C: arquitectura de servidor MCP

El artículo describe la arquitectura de servidores MCP documents1c y metadata1c para búsqueda RAG en documentación de 1C. Capas analizadas: chunks con heading_path, caché Redis, búsqueda vectorial pgvector, RRF, BM25 con tokenización personalizada. Código fuente de componentes clave proporcionado.

Arquitectura RAG para búsqueda en documentación de 1C
Advertisement 728x90

Arquitectura RAG Multinivel para Búsqueda en Documentación 1C en Servidores MCP

Los servidores MCP documents1c y metadata1c implementan un sistema RAG multinivel para búsqueda precisa en documentación y configuraciones de 1C. Cada capa de clasificación aborda desafíos específicos: desde el contexto de fragmentos hasta la combinación de búsqueda vectorial y de texto completo. El sistema utiliza PostgreSQL con pgvector, Redis para caché y tokenización personalizada para terminología 1C.

Por qué Abandonamos las Bases de Datos Vectoriales con Jerarquías

Los almacenes vectoriales estándar como Qdrant o Weaviate tienen dificultades con la naturaleza dinámica de la documentación de 1C. Los documentos no son autónomos sin contexto: un artículo titulado "Configuración" requiere especificar la sección, y un objeto como Reference.Employees necesita descripciones de sus atributos y módulos.

La documentación se actualiza con frecuencia: nuevos lanzamientos de BSP, métodos de plataforma. Reconstruir el árbol en una base de datos vectorial es costoso. La solución: PostgreSQL con pgvector + heading_path incrustado en cada fragmento. Los embeddings se generan para heading_path + text, solo se almacena el texto del fragmento.

Google AdInline article slot

Ahorro: Una columna embedding halfvec(1024) en la tabla article_chunks en lugar de una base de datos vectorial separada.

Capa 1: Fragmentos con Contexto de Encabezado

La división de markdown en fragmentos considera la estructura: se elimina YAML, división por encabezados, los bloques de código no se rompen, superposición de 220 caracteres.

def split_markdown_into_chunks(
    text: str,
    max_len: int = 800,
    overlap: int = 220,
    include_heading_path: str = "full_path",
) -> list[str]:
    body = _strip_frontmatter(text)
    sections = _split_by_headers(body)
    chunks: list[str] = []

    for section, heading_path, current_heading in sections:
        context_heading = " > ".join(heading_path)
        section_with_context = f"{context_heading}\n\n{section}"

        if len(section_with_context) <= max_len:
            chunks.append(section_with_context)
        else:
            sub_chunks = _split_section_by_paragraphs(section_with_context, max_len)
            chunks.extend(sub_chunks)

    return _add_overlap(chunks, overlap)

Ejemplo de fragmento:

Google AdInline article slot
Nómina y Gestión de RRHH > Cálculo de Nómina > Configuración del Impuesto sobre la Renta Personal

Para calcular correctamente el impuesto sobre la renta personal, debe especificar la tasa impositiva...

En la base de datos, meta almacena heading_path por separado para filtros y enlaces. Embedding: heading_path + chunk_text.

Capa 2: Caché de Embeddings en Redis

Generar embeddings (text-embedding-qwen3-embedding-4b) toma 50–200 ms. Para consultas repetidas: Redis con TTL, clave SHA256 de model:dimensions:text.

async def embed_text(text: str) -> list[float]:
    model = _get_embed_model_name()
    dims = _get_embed_dimensions()

    if _cache_enabled():
        cached = await cache_get(text, model, dims)
        if cached is not None:
            return cached

    response = await to_thread(client.embeddings.create, model=model, input=text)
    vector = response.data[0].embedding

    if _cache_enabled():
        await cache_set(text, model, dims, vector, _cache_ttl())

    return vector

La caché se invalida automáticamente cuando cambia el modelo.

Google AdInline article slot

Capa 3: Búsqueda Vectorial con halfvec

Distancia coseno mediante <=> en pgvector, tipo halfvec(1024) ahorra memoria.

SELECT
    ac.id, ac.article_id, ac.chunk_text, ac.meta,
    ac.embedding::halfvec(1024) <=> CAST(:query_embedding AS halfvec(1024)) AS distance
FROM article_chunks ac
INNER JOIN articles a ON ac.article_id = a.id
WHERE a.job_id IN (SELECT id FROM jobs WHERE load_mode IN ('hbk', 'docs'))
AND a.destination = :destination
ORDER BY distance
LIMIT :limit;

Puntuación = 1 - distancia.

Capa 4: Fusión de Clasificaciones con RRF

La búsqueda vectorial es débil para términos precisos como &OnClientOnServer. RRF fusiona listas vectoriales y FTS:

@staticmethod
def _rrf_merge(vector_results, fts_results, top_k: int, k: int = 60) -> list[dict]:
    scores: dict[int, float] = {}
    items: dict[int, dict] = {}

    for rank, item in enumerate(vector_results, start=1):
        chunk_id = item["id"]
        scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
        items[chunk_id] = item

    for rank, item in enumerate(fts_results, start=1):
        chunk_id = item["id"]
        scores[chunk_id] = scores.get(chunk_id, 0.0) + 1.0 / (k + rank)
        if chunk_id not in items:
            items[chunk_id] = item

    sorted_ids = sorted(scores, key=lambda cid: scores[cid], reverse=True)
    # Normalización y retorno de top_k

Impulso HBK: Para consultas sin filtro, los fragmentos del lenguaje incorporado de 1C (load_mode='hbk') se agregan al principio de la lista.

  • Fragmentos HBK principales por query_emb
  • Se excluyen duplicados
  • Colocados antes de los resultados principales

Capa 5: Reclasificación BM25 con Tokenización 1C

Para 50–100 candidatos de RRF, se aplica BM25 con pesos (vector 0.6, bm25 0.4). Efectivo para:

  • Términos específicos: CommonModule, InformationRegister, &OnServer
  • Nombres de objetos: Reference.Nomenclature
  • Consultas técnicas cortas

Tokenización personalizada:

GUID_RE = re.compile(r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b", re.I)
VERSION_RE = re.compile(r"\bv?8\.\d+(?:\.\d+){1,3}\b", re.I)
DIRECTIVE_RE = re.compile(r"[&#][And-IA-Z][And-YaYoA-Za-z0-9_]*", re.I)

def _tokenize(self, text: str, expand_query: bool = False) -> list[str]:
    text = _norm_text(text)
    out = []

    for g in GUID_RE.findall(text): out.append(g.lower())
    for v in VERSION_RE.findall(text): out.append(v.lower())
    for d in DIRECTIVE_RE.findall(text): out.append(d.lower())

    for t in TOKEN_RE.findall(text):
        if t in RU_STOP: continue
        for p in _split_identifier(t):
            out.append(p.lower())
    return out

Divide CamelCase, snake_case, extrae directivas y GUIDs.

Conclusiones Clave

  • heading_path en fragmentos proporciona contexto sin un árbol de documentos
  • Caché Redis reduce la latencia de embedding de 100+ ms a 0 ms
  • RRF combina semántica y búsqueda exacta sin normalización
  • BM25 con tokenización 1C impulsa términos técnicos relevantes
  • halfvec(1024) ahorra memoria en pgvector manteniendo precisión coseno

— Editorial Team

Advertisement 728x90

Leer después