Zpět na domů

RAG-systém pro 1C: architektura MCP-serverů

Článek popisuje architekturu MCP-serverů documents1c a metadata1c pro RAG-vyhledávání v 1C-dokumentaci. Rozebrány vrstvy: chunků s heading_path, cache Redis, vektorový vyhledávač pgvector, RRF, BM25 s vlastní tokenizací. Uveden zdrojový kód klíčových komponent.

Architektura RAG pro vyhledávání v 1C-dokumentaci
Advertisement 728x90

Víceúrovňová RAG architektura pro vyhledávání v dokumentaci 1C na MCP serverech

MCP servery documents1c a metadata1c implementují víceúrovňový RAG systém pro přesné vyhledávání v dokumentaci 1C a konfiguracích. Každá úroveň hodnocení řeší konkrétní problémy: od kontextu chunků až po kombinaci vektorového a fulltextového vyhledávání. Systém využívá PostgreSQL s pgvector, Redis pro ukládání do mezipaměti a vlastní tokenizaci pro termíny 1C.

Proč se vzdát vektorových databází s hierarchií

Standardní vektorová úložiště jako Qdrant nebo Weaviate si neporadí s dynamikou dokumentace 1C. Dokumenty nejsou samostatné bez kontextu: článek "Nastavení" vyžaduje uvedení oddílu, objekt Složka.Zaměstnanci — popis atributů a modulů.

Dokumentace se často aktualizuje — nové verze BSP, metody platformy. Přestavba stromu ve vektorové databázi je nákladná. Řešení: PostgreSQL s pgvector + heading_path vložený do každého chunku. Embeddingy se generují pro heading_path + text, ukládá se pouze text chunku.

Google AdInline article slot

Úspora: sloupec embedding halfvec(1024) v tabulce article_chunks místo samostatné vektorové databáze.

Úroveň 1: Chunky s kontextem nadpisů

Rozdělení markdownu na chunky bere v úvahu strukturu: YAML se odstraní, dělení podle nadpisů, bloky kódu se nepřerušují, překrytí 220 znaků.

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)

Příklad chunku:

Google AdInline article slot
Mzda a řízení personálu > Výpočet mezd > Nastavení daně z příjmu

Pro správný výpočet daně z příjmu je nutné uvést sazbu daně...

V databázi meta ukládá heading_path zvlášť pro filtry a odkazy. Embedding: heading_path + chunk_text.

Úroveň 2: Mezipaměť embeddingů v Redis

Generování embeddingů (text-embedding-qwen3-embedding-4b) trvá 50–200 ms. Pro opakující se dotazy — Redis s TTL, klíč SHA256 od 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

Mezipaměť se automaticky zneplatní při změně modelu.

Google AdInline article slot

Úroveň 3: Vektorové vyhledávání s halfvec

Kosinová vzdálenost přes <=> v pgvector, typ halfvec(1024) šetří paměť.

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;

Score = 1 - distance.

Úroveň 4: Sloučení hodnocení RRF

Vektorové vyhledávání je slabé pro přesné termíny jako &NaKlientoviNaServeru. RRF slučuje vektorový a FTS seznamy:

@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)
    # Normalizace a vrácení top_k

HBK-boost: pro dotazy bez filtru se přidáván chunky vestavěného jazyka 1C (load_mode='hbk') na začátek seznamu.

  • Top HBK-chunky podle query_emb
  • Vyloučení duplikátů
  • Umístění před hlavními výsledky

Úroveň 5: BM25 přehodnocení s 1C tokenizací

Pro 50–100 kandidátů z RRF se aplikuje BM25 s váhami (vector 0.6, bm25 0.4). Efektivní pro:

  • Specifické termíny: ObecnýModul, RegistrInformací, &NaServeru
  • Názvy objektů: Složka.Nomenklatura
  • Krátké technické dotazy

Vlastní tokenizace:

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

Rozděluje CamelCase, snake_case, vyčleňuje direktivy a GUID.

Co je důležité

  • heading_path v chuncích zajišťuje kontext bez stromu dokumentů
  • Redis-mezipaměť snižuje latenci embeddingů ze 100+ ms na 0 ms
  • RRF kombinuje sémantiku a přesné vyhledávání bez normalizace
  • BM25 s 1C tokenizací zvyšuje relevantní technické termíny
  • halfvec(1024) šetří paměť v pgvector při přesnosti cosine

— Editorial Team

Advertisement 728x90

Číst dál