DEV Community

Cover image for Codebase Knowledge Base (09): codebase-memory-mcp in the Wild — Three-Path Retrieval on LightRAG
WonderLab
WonderLab

Posted on

Codebase Knowledge Base (09): codebase-memory-mcp in the Wild — Three-Path Retrieval on LightRAG

Eight Articles Built the Ship. Now Let's Sail It.

If you've followed the series so far, your working memory should hold a clear checklist:

  • Article 03: AST function-level chunking + vector retrieval, Recall@5 = 0.958 — the ceiling for text-only approaches
  • Article 05: Graph retrieval rescues Q8, but BFS noise breaks Q1 — it's a one-for-one trade
  • Articles 06 & 07: Structure-aware embedding and hybrid search can't actually bridge the gap
  • Article 08: Three orthogonal paths — vector for semantics, graph for structure, symbol for exact matches — route by query intent

All of that was measured on a 30-function toy codebase. The toy was honest. But it was still a toy.

Today we change the venue.

LightRAG is one of the highest-starred open-source knowledge graph RAG frameworks on GitHub. The codebase has been fully indexed by codebase-memory-mcp: 20,674 nodes, 94,517 edges, covering 409 Python files and 101 TypeScript files (Web UI). That's a medium-scale real project — large enough to make everything from the earlier experiments matter.

This article's job is straightforward: take three real questions, fire each one down the appropriate retrieval path, and spread the results flat so you can see exactly what each path delivers.


A Quick Map Before We Start

Before firing queries, one orientation pass.

LightRAG's name might imply a simple retrieval utility, but the actual code structure is considerably richer. Running get_architecture reveals the most important signal first: layer boundaries.

entry layer:   kg/, chunker/        ← storage adapters, chunkers
core layer:    base, api, parser    ← abstract base classes, REST API, parsers
internal:      examples/, tests/   ← examples, tests (not exported)
Enter fullscreen mode Exit fullscreen mode

This immediately tells you: if you want to understand 'how does LightRAG process a document,' the main arena is lightrag/, the entry point is lightrag.py, and storage implementations live in kg/ (10+ backends: Neo4j, MongoDB, Qdrant, Milvus, PostgreSQL, ...).

Now for the retrieval runs.


Path 1: Vector — Ask in Natural Language, Get a Concept Entry Point

Question: What retrieval modes does LightRAG support, and when should each one be used?

This is a classic capability query: the caller doesn't know what the relevant function or class is called — they just know what they want to understand. This is exactly where the vector path excels.

Tool: search_graph (BM25 + vector dual index)

query: "query search hybrid retrieval mode"
label: Method
limit: 5
Enter fullscreen mode Exit fullscreen mode

Top result (the rest are test files):

QueryParam  (lightrag/base.py, line 83)
  "local": Focuses on context-dependent information.
  "global": Utilizes global knowledge.
  "hybrid": Combines local and global retrieval methods.
  "naive": Performs a basic search without advanced techniques.
  "mix": Integrates knowledge graph and vector retrieval.
  "bypass": ...
Enter fullscreen mode Exit fullscreen mode

One query, straight to the source:

class QueryParam:
    """Configuration parameters for query execution in LightRAG."""

    mode: Literal["local", "global", "hybrid", "naive", "mix", "bypass"] = "mix"
Enter fullscreen mode Exit fullscreen mode

All six retrieval modes, right in the QueryParam dataclass. The docstring explains each one:

Mode Description
local Local context retrieval, focuses on the entity neighborhood around the query
global Global knowledge retrieval, aggregates relationships across documents
hybrid local + global combined
naive Plain vector search, no knowledge graph
mix Knowledge graph + vector fusion (the default)
bypass Pass directly to LLM, skip retrieval entirely

Notice what was returned: a class definition, not a function implementation. The vector path is highly effective at 'find the concept entry point' queries — you describe a feature, it returns the most relevant abstraction. From there you drill down.

A second search_graph call confirmed the pattern:

query: "insert document knowledge graph extraction"
→ hit: _find_related_text_unit_from_entities (operate.py:5260)
       _find_related_text_unit_from_relations (operate.py:5511)
       run_rebuild_entities_relations (tools/rebuild_vdb.py:900)
Enter fullscreen mode Exit fullscreen mode

The intent 'insert documents, extract knowledge graph' navigated directly to the extraction logic in operate.py — not a pile of results that happen to contain the word 'insert'.


Path 2: Graph — Follow the Call Chain, Understand What a Function Triggers

Question: What does ainsert() call? What is the full execution path for document ingestion?

This is a structural query: the caller knows the entry point name and wants to understand the complete execution path downstream. The graph path owns this category.

Tool: trace_path (call graph BFS)

function_name: ainsert
mode: calls
direction: outbound
depth: 2
Enter fullscreen mode Exit fullscreen mode

Returns (hop=1 direct callees, hop=2 indirect):

hop=1 direct:
  apipeline_enqueue_documents   (pipeline.py)
  apipeline_process_enqueue_documents   (pipeline.py)
  generate_track_id   (utils.py)
  resolve_chunk_options   (parser/routing.py)

hop=2 indirect (inside pipeline):
  _run_pipeline_batch
  _validate_and_fix_document_consistency
  _atomic_release_busy_or_consume_pending
  compute_mdhash_id
  sanitize_text_for_encoding
  normalize_document_file_path
  filter_keys        (BaseKVStorage)
  upsert             (BaseVectorStorage)
  get_by_id          (BaseVectorStorage)
  get_docs_by_statuses  (DocStatusStorage)
  get_namespace_data
  get_namespace_lock
  ...  (36 nodes total)
Enter fullscreen mode Exit fullscreen mode

These 36 nodes form the complete document ingestion path. But the node list alone isn't the full story — the source code is:

async def ainsert(
    self,
    input: str | list[str],
    split_by_character: str | None = None,
    ...
) -> str:
    """Async insert documents with checkpoint support (fixed-token chunking only).

    SDK convenience entry point. It **always** chunks with the fixed-token
    (F) strategy: ``process_options`` is intentionally not passed, so the
    document runs the F chunker. ...

    The LightRAG **server / REST API does not call this method** — it
    ingests via :meth:`apipeline_enqueue_documents` +
    :meth:`apipeline_process_enqueue_documents` with a per-document
    ``process_options`` selector, which is how F/R/V/P are chosen there.
    """
    chunk_opts = resolve_chunk_options(
        self.addon_params,
        split_by_character=split_by_character,
        split_by_character_only=split_by_character_only,
    )
    await self.apipeline_enqueue_documents(input, ids, file_paths, track_id, chunk_options=chunk_opts)
    await self.apipeline_process_enqueue_documents()
    return track_id
Enter fullscreen mode Exit fullscreen mode

There is a critical design decision buried in this source that you would never see from the function name alone:

ainsert only supports the fixed-token chunking strategy (F strategy). If you want recursive-character (R), semantic-vector (V), or paragraph-semantic (P) chunking, you cannot call ainsert — you must call apipeline_enqueue_documents + apipeline_process_enqueue_documents directly with an explicit process_options.

Equally: LightRAG's REST API server doesn't call ainsert either — it goes directly to the pipeline layer. So SDK users and REST API users are actually running different code paths.

This is what the graph path uniquely delivers: not just 'here is a function,' but 'here is what role this function plays in the system.' A vector search would probably return ainsert for 'how to insert documents.' But it wouldn't tell you that this method is intentionally designed as a simplified F-only entry point, and what that means for how you should actually use it.


Path 3: Symbol — Precise Hit, Zero Ambiguity

Question: Which parts of the codebase call BaseVectorStorage.upsert?

This is an impact analysis query: the caller wants to do a refactor or security audit and needs to know every caller of a given interface. No semantic understanding is required — only precise matching.

Tool: search_code (graph-augmented grep)

pattern: "BaseVectorStorage"
mode: compact
limit: 5
Enter fullscreen mode Exit fullscreen mode

Results show upsert (the core method of BaseVectorStorage) has fan_in = 268 — one of the most heavily called methods in the entire project.

A more targeted query:

pattern: "QueryParam"
Enter fullscreen mode Exit fullscreen mode

Immediately locates:

QueryParam   lightrag/base.py:83    (Class, in_degree=19)
  ↑ called by 19 locations:
  - lightrag/lightrag.py:2056  (method: query)
  - lightrag/lightrag.py:2091  (method: aquery)
  - lightrag/operate.py:4323   (_perform_kg_search)
  - lightrag/lightrag.py:2344  (aquery_llm)
  ...
Enter fullscreen mode Exit fullscreen mode

19 callers, all precisely located with file and line number. If you're modifying QueryParam's interface — deprecating a mode, adding a parameter — this list is your blast radius assessment.

The symbol path is graph-augmented grep. Plain grep returns where a string appears. The symbol path also tells you each hit's position in the call graph — whether it's an entry point, who calls it, how high its in-degree is. That's enough to answer 'how big a change is this?' in seconds.


All Three in Combination: One Real Engineering Task, Three Perspectives

The three examples above demonstrated each path independently. Now, a scenario closer to real engineering:

Task: Understand LightRAG's full document ingestion flow, in order to modify the chunking strategy

This is a question an engineer would ask before making a feature change. The answer isn't in any single function — it needs multiple perspectives assembled into a map.

Step 1: Vector path — locate the concept entry points

search_graph("document chunking strategy pipeline insert")
 ainsert (lightrag.py:1428)
 ainsert_custom_chunks (lightrag.py  deprecated)
 resolve_chunk_options (parser/routing.py)
Enter fullscreen mode Exit fullscreen mode

The vector path reveals 'which entry points in this system relate to chunking,' and along the way flags that ainsert_custom_chunks is deprecated.

Step 2: Graph path — trace the execution chain

trace_path("ainsert", mode=calls, depth=2)
 hop=1: apipeline_enqueue_documents  resolve_chunk_options
 hop=2: _run_pipeline_batch  [various Storage.upsert]
Enter fullscreen mode Exit fullscreen mode

The graph path shows: chunking strategy selection happens in resolve_chunk_options (before enqueue), actual chunking happens in _run_pipeline_batch (pipeline execution), and the result gets written to multiple storage backends. If you want to change chunking strategy, modify parser/routing.py — not ainsert in lightrag.py.

Step 3: Symbol path — pin the interface definition

search_code("resolve_chunk_options")
 lightrag/parser/routing.py:chunk_strategy_key
 lightrag/parser/routing.py:slim_chunk_options
 lightrag/parser/routing.py:default_chunker_config
Enter fullscreen mode Exit fullscreen mode

The symbol path pins three related functions in routing.py, with their exact reference locations in the ainsert call chain.

After three steps you know: change chunking strategy in parser/routing.py; ainsert via SDK only supports F strategy, all others require calling the pipeline layer directly; storage writes are abstracted through BaseVectorStorage.upsert, so chunking changes won't affect storage backends. That's a complete engineering change map — each path contributed exactly what it's best at.


The Counter-Intuitive Finding: Scale Makes the Graph Path More Valuable

On the toy codebase (30 functions), trace_path expanded to a handful of nodes — the graph path's value wasn't obvious. On LightRAG (7,761 functions + 3,569 methods), the same call trace expanded to 36 nodes covering the complete path from user API to storage abstraction.

This isn't linear growth — it's a scale amplification effect: the larger the codebase, the richer any function's neighborhood, the harder it becomes for pure vector retrieval to surface structural relationships, and the greater the relative advantage of the graph path.

This is the Q8 problem from Article 05 reflected in a real codebase. ainsert and _run_pipeline_batch are semantically distant — 'high-level document insertion API' vs 'low-level batch pipeline executor' — no embedding model would bring those two close. But the call graph connects them in one hop.

When the question is 'what would change if I modify this?' or 'who calls this function?' — those questions have the same answer structure in a 5,000-function codebase as in a 30-function one: the answer is only in the graph.


Summary

Nine articles in, this series has tracked a single thread from first principles to production tooling: measure the ceiling of each single-path approach, identify where each fails, then design around the failure modes.

A final statement of where each path belongs:

  • Vector path: You don't know what the function is called, just what it does — search_graph(query=...) is the first move
  • Graph path: You know the entry point, want to understand its execution chain and blast radius — trace_path is the workhorse
  • Symbol path: You know the exact function or class name, want its definition and all references — search_code returns results in under a second

These aren't competing approaches. They're orthogonal dimensions. Any task that requires 'understand an unfamiliar codebase' needs all three to build a complete cognitive map.

The central conclusion, one more time:

Codebase semantic understanding is multi-dimensional. Every single signal has a blind spot. Pick the right tool, keep the paths distinct — that's what engineering-quality retrieval actually looks like.


For more details on codebase-memory-mcp and its full feature set, visit the GitHub repository.


Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.

Find more useful knowledge and interesting products on my Homepage

Top comments (0)