n4nAI

Splitting code files in LangChain for RAG pipelines

Learn to split code files effectively in LangChain for RAG pipelines using language-aware splitters, with runnable examples and verification steps.

n4n Team4 min read916 words

Audio narration

Coming soon — every post will get a voice note here.

When building RAG systems over codebases, naive text splitting destroys the semantic structure that makes code retrievable. LangChain’s code splitters preserve function boundaries, class definitions, and import statements by parsing syntax trees instead of counting characters. This guide walks through selecting, configuring, and validating a langchain code splitter rag pipeline that actually works in production.

Step 1: understand why character splitters fail on code

Character-based splitters like RecursiveCharacterTextSplitter treat code as plain text. They break mid-function, separate decorators from their targets, and scatter related logic across chunks. The result: retrieval returns fragments that don’t compile, lack context, and hallucinate when fed to an LLM.

Language-aware splitters use tree-sitter grammars to parse source files into abstract syntax trees (ASTs), then split along syntactic boundaries — functions, classes, methods, imports. This preserves the semantic units developers actually search for.

Step 2: install the required dependencies

LangChain’s code splitters live in langchain-text-splitters and require tree-sitter language packages. Install the base package plus the grammars for your target languages:

pip install langchain-text-splitters tree-sitter tree-sitter-python tree-sitter-javascript tree-sitter-typescript tree-sitter-go tree-sitter-rust tree-sitter-java

Each tree-sitter-* package provides a compiled grammar. If your language isn’t listed, check PyPI for tree-sitter-<language> or build from the tree-sitter grammar repository.

Step 3: choose the right splitter for your language

LangChain provides Language enum constants and a factory function. The most common splitters:

from langchain_text_splitters import Language, RecursiveCharacterTextSplitter
from langchain_text_splitters.code import CodeSplitter

# Option 1: Language-specific splitter (recommended)
python_splitter = CodeSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=2000,      # max characters per chunk
    chunk_overlap=200,    # overlap to preserve context across boundaries
)

# Option 2: Generic recursive splitter with code-aware separators
# Use when language isn't supported or you need fallback behavior
generic_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=2000,
    chunk_overlap=200,
)

Supported Language values include: PYTHON, JAVASCRIPT, TYPESCRIPT, GO, RUST, JAVA, CPP, CSHARP, RUBY, PHP, SWIFT, KOTLIN, SCALA, SQL, HTML, CSS, MARKDOWN, LATEX, SOL, PROTO, DOCKERFILE.

The CodeSplitter (Option 1) uses tree-sitter directly and produces cleaner boundaries. The RecursiveCharacterTextSplitter.from_language (Option 2) uses language-aware separator lists as a fallback — faster but less precise.

Step 4: load code files with appropriate document loaders

Pair the splitter with a loader that preserves file paths and metadata. DirectoryLoader with TextLoader works for simple cases; UnstructuredFileLoader handles more formats but adds overhead.

from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_core.documents import Document
from pathlib import Path

def load_codebase(root: str, glob: str = "**/*.py") -> list[Document]:
    loader = DirectoryLoader(
        root,
        glob=glob,
        loader_cls=TextLoader,
        loader_kwargs={"encoding": "utf-8"},
        show_progress=True,
        use_multithreading=True,
    )
    docs = loader.load()
    
    # Enrich metadata with relative path and language hint
    for doc in docs:
        path = Path(doc.metadata["source"])
        doc.metadata["relative_path"] = str(path.relative_to(root))
        doc.metadata["language"] = path.suffix.lstrip(".")
    
    return docs

# Usage
documents = load_codebase("/path/to/repo", "**/*.py")
print(f"Loaded {len(documents)} files")

For polyglot repos, call load_codebase per language with the appropriate glob and language suffix, then combine the document lists.

Step 5: split documents while preserving metadata

Pass loaded documents through the splitter. Each output chunk inherits the source document’s metadata — critical for citation and filtering at query time.

from langchain_text_splitters.code import CodeSplitter
from langchain_text_splitters import Language

def split_code_documents(
    documents: list[Document],
    language: Language = Language.PYTHON,
    chunk_size: int = 2000,
    chunk_overlap: int = 200,
) -> list[Document]:
    splitter = CodeSplitter.from_language(
        language=language,
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
    )
    
    chunks = splitter.split_documents(documents)
    
    # Add chunk index for ordering and deduplication
    for i, chunk in enumerate(chunks):
        chunk.metadata["chunk_index"] = i
        chunk.metadata["chunk_size"] = len(chunk.page_content)
    
    return chunks

# Usage for Python files
python_chunks = split_code_documents(documents, Language.PYTHON)
print(f"Produced {len(python_chunks)} chunks")

Tuning chunk_size: Code chunks should fit within your embedding model’s context window and leave room for the query + retrieved chunks in the generation prompt. For text-embedding-3-large (8192 tokens) and a 4k generation context, 1500-2500 characters per chunk is a safe range. Overlap of 10-15% (200-300 chars) preserves cross-boundary context without excessive duplication.

Step 6: handle multi-language repositories

Real codebases mix languages. Build a dispatcher that routes each file to its language-specific splitter:

from langchain_text_splitters import Language
from typing import Dict, List

LANGUAGE_MAP: Dict[str, Language] = {
    ".py": Language.PYTHON,
    ".js": Language.JAVASCRIPT,
    ".ts": Language.TYPESCRIPT,
    ".tsx": Language.TYPESCRIPT,
    ".jsx": Language.JAVASCRIPT,
    ".go": Language.GO,
    ".rs": Language.RUST,
    ".java": Language.JAVA,
    ".cpp": Language.CPP,
    ".cs": Language.CSHARP,
    ".rb": Language.RUBY,
    ".php": Language.PHP,
    ".swift": Language.SWIFT,
    ".kt": Language.KOTLIN,
    ".scala": Language.SCALA,
    ".sql": Language.SQL,
}

def split_polyglot_documents(documents: list[Document]) -> list[Document]:
    all_chunks = []
    
    for doc in documents:
        ext = Path(doc.metadata["source"]).suffix
        language = LANGUAGE_MAP.get(ext)
        
        if language is None:
            # Fallback: generic text splitter for unknown extensions
            from langchain_text_splitters import RecursiveCharacterTextSplitter
            fallback = RecursiveCharacterTextSplitter(chunk_size=2000, chunk_overlap=200)
            chunks = fallback.split_documents([doc])
        else:
            splitter = CodeSplitter.from_language(language=language, chunk_size=2000, chunk_overlap=200)
            chunks = splitter.split_documents([doc])
        
        for i, chunk in enumerate(chunks):
            chunk.metadata["chunk_index"] = i
            chunk.metadata["splitter_language"] = language.value if language else "generic"
        
        all_chunks.extend(chunks)
    
    return all_chunks

This approach ensures each language gets its proper AST parser while gracefully degrading for unsupported file types.

Step 7: embed and index chunks in a vector store

With chunks produced, embed and store them. This example uses FAISS for local development; swap the vector store for production (Pinecone, Weaviate, Qdrant, etc.).

from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.vectorstores import VectorStore

def build_vector_store(chunks: list[Document], persist_path: str = "./code_index") -> VectorStore:
    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
    
    # Batch embedding for throughput
    vector_store = FAISS.from_documents(chunks, embeddings)
    vector_store.save_local(persist_path)
    
    return vector_store

# Usage
vector_store = build_vector_store(python_chunks)
print(f"Indexed {vector_store.index.ntotal} vectors")

Metadata filtering: Include relative_path, language, and chunk_index in the vector store metadata. At query time, filter by language or path prefix to scope retrieval (e.g., only search src/auth/).

Step 8: verify split quality with automated checks

Don’t eyeball chunks. Write assertions that catch regressions when you change splitter config or upgrade tree-sitter grammars.

def verify_chunks(chunks: list[Document], sample_size: int = 50) -> dict:
    import random
    import ast
    
    results = {
        "total_chunks": len(chunks),
        "empty_chunks": 0,
        "oversized_chunks": 0,
        "syntax_errors": 0,
        "missing_metadata": 0,
        "boundary_issues": 0,
    }
    
    sample = random.sample(chunks, min(sample_size, len(chunks)))
    
    for chunk in sample:
        content = chunk.page_content
        
        # Check 1: no empty chunks
        if not content.strip():
            results["empty_chunks"] += 1
            continue
        
        # Check 2: size bounds
        if len(content) > 3000:  # adjust for your chunk_size + overlap
            results["oversized_chunks"] += 1
        
        # Check 3: required metadata
        required = ["source", "relative_path", "language", "chunk_index"]
        if not all(k in chunk.metadata for k in required):
            results["missing_metadata"] += 1
        
        # Check 4: valid Python syntax (for Python chunks)
        if chunk.metadata.get("language") == "py":
            try:
                ast.parse(content)
            except SyntaxError:
                results["syntax_errors"] += 1
        
        # Check 5: chunk doesn't start/end mid-token (heuristic)
        lines = content.strip().splitlines()
        if lines:
            first = lines[0].strip()
            last = lines[-1].strip()
            # Heuristic: chunks shouldn't start with 'else:', 'elif', 'except:', 'finally:'
            if first.startswith(("else:", "elif ", "except:", "finally:", "catch ", "}")):
                results["boundary_issues"] += 1
    
    return results

# Run verification
verification = verify_chunks(python_chunks)
print(verification)
# Target: all zeros except total_chunks

Add this to your CI pipeline. Fail the build if syntax_errors > 0 or boundary_issues > threshold.

A minimal RAG chain that filters by language and path:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_core.output_parsers import StrOutputParser
from operator import itemgetter

def build_rag_chain(vector_store: VectorStore, k: int = 6):
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
    
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are a code assistant. Answer questions using only the provided context.
Cite sources using the relative_path and chunk_index from metadata.
If the context doesn't contain the answer, say you don't know."""),
        ("human", """Context:
{context}

Question: {question}

Answer:"""),
    ])
    
    def format_docs(docs: list[Document]) -> str:
        formatted = []
        for doc in docs:
            meta = doc.metadata
            header = f"[File: {meta.get('relative_path', 'unknown')}, Chunk: {meta.get('chunk_index', '?')}]"
            formatted.append(f"{header}\n```&#123;meta.get('language', '')&#125;\n&#123;doc.page_content&#125;\n```")
        return "\n\n---\n\n".join(formatted)
    
    retriever = vector_store.as_retriever(
        search_kwargs={"k": k}
        # Add filter: {"language": "py"} or {"relative_path": {"$regex": "^src/auth/"}}
    )
    
    chain = (
        {"context": retriever | format_docs, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )
    
    return chain

# Usage
rag_chain = build_rag_chain(vector_store)
answer = rag_chain.invoke("How does the authentication middleware validate JWT tokens?")
print(answer)

Step 10: evaluate retrieval quality with a golden set

Splitting quality ultimately shows up in retrieval metrics. Build a small golden set of (question, relevant_file_paths) pairs and measure recall@k.

from typing import List, Set

GOLDEN_SET = [
    {
        "question": "How does the rate limiter handle burst traffic?",
        "relevant_paths": {"src/rate_limiter/token_bucket.py", "src/rate_limiter/__init__.py"},
    },
    {
        "question": "Where are database migrations defined?",
        "relevant_paths": {"migrations/", "alembic/"},
    },
    {
        "question": "What configuration options does the auth module accept?",
        "relevant_paths": {"src/auth/config.py", "src/auth/__init__.py"},
    },
]

def evaluate_recall(vector_store: VectorStore, k: int = 6) -> dict:
    retriever = vector_store.as_retriever(search_kwargs={"k": k})
    
    total_recall = 0.0
    results = []
    
    for item in GOLDEN_SET:
        docs = retriever.invoke(item["question"])
        retrieved_paths = {doc.metadata.get("relative_path", "") for doc in docs}
        relevant = item["relevant_paths"]
        
        # Check if any relevant path is a prefix of retrieved path
        found = any(
            any(ret.startswith(rel) for ret in retrieved_paths)
            for rel in relevant
        )
        
        recall = 1.0 if found else 0.0
        total_recall += recall
        results.append({
            "question": item["question"],
            "recall": recall,
            "retrieved": list(retrieved_paths)[:3],
        })
    
    return {
        "mean_recall_at_k": total_recall / len(GOLDEN_SET),
        "details": results,
    }

# Run evaluation
eval_results = evaluate_recall(vector_store, k=6)
print(f"Recall@6: {eval_results['mean_recall_at_k']:.2f}")
for r in eval_results["details"]:
    print(f"  {r['recall']:.0f} - {r['question'][:60]}...")

Target recall@6 > 0.8 for a well-tuned splitter. If lower, adjust chunk_size, chunk_overlap, or add more golden set k.

Step 11: handle edge cases in production

Large generated files

Auto-generated code (protobufs, GraphQL schemas, bundled assets) can exceed chunk limits. Filter them at load time:

def should_skip_file(path: Path) -> bool:
    skip_patterns = [
        "**/node_modules/**",
        "**/dist/**",
        "**/build/**",
        "**/*.min.js",
        "**/*.pb.go",
        "**/*_pb2.py",
        "**/generated/**",
    ]
    # Use pathlib match or fnmatch
    return any(path.match(p) for p in skip_patterns)

Mixed-language files

Files like .vue, .svelte, or Jupyter notebooks contain multiple languages. The tree-sitter grammar for the primary language (HTML for Vue) will parse the whole file but may not split embedded script blocks cleanly. Options:

  • Pre-process with a custom splitter that extracts <script> blocks
  • Accept coarser chunks for these files
  • Use UnstructuredFileLoader with mode="elements" for better segmentation

Encoding issues

TextLoader defaults to UTF-8. Legacy codebases may use Latin-1 or CP1252. Detect and handle:

import chardet

def load_with_detection(path: Path) -> Document:
    raw = path.read_bytes()
    detected = chardet.detect(raw)
    encoding = detected["encoding"] or "utf-8"
    content = raw.decode(encoding, errors="replace")
    return Document(page_content=content, metadata={"source": str(path)})

Step 12: monitor chunk distribution in production

Log chunk size distribution at index time. Skewed distributions indicate splitter misconfiguration.

import statistics

def log_chunk_stats(chunks: list[Document]) -> None:
    sizes = [len(c.page_content) for c in chunks]
    print(f"""
Chunk statistics:
  Count: {len(sizes)}
  Mean: {statistics.mean(sizes):.0f}
  Median: {statistics.median(sizes):.0f}
  Stdev: {statistics.stdev(sizes):.0f}
  Min: {min(sizes)}
  Max: {max(sizes)}
  P95: {statistics.quantiles(sizes, n=20)[18]:.0f}
  P99: {statistics.quantiles(sizes, n=100)[98]:.0f}
""")

log_chunk_stats(python_chunks)

Watch for bimodal distributions (two distinct chunk sizes) — usually means some files use a fallback splitter.

Common pitfalls and fixes

Symptom Likely cause Fix
Chunks end mid-function chunk_size too small Increase to 2000-3000
Too many tiny chunks (< 100 chars) Fallback splitter on unsupported language Add language to LANGUAGE_MAP or increase chunk_size
Syntax errors in verification Tree-sitter grammar version mismatch Pin tree-sitter-python==0.21.0 (or matching version)
Retrieval misses related functions Overlap too small Increase chunk_overlap to 300-400
Embedding latency high Too many chunks Increase chunk_size, reduce k at query time

Final checklist before deploying

  • All target languages have tree-sitter-* packages installed
  • verify_chunks passes with zero syntax errors
  • Golden set recall@6 > 0.8
  • Chunk size distribution shows single mode around target size
  • Metadata includes relative_path, language, chunk_index on every chunk
  • Retriever supports metadata filtering for scoped queries
  • CI runs verification on every index rebuild

The langchain code splitter rag pattern works because it respects the semantic boundaries that developers actually reason about. Treat the splitter as a tuned component — version your tree-sitter grammars, monitor chunk quality metrics, and evaluate retrieval like any other ML pipeline stage.

Tagslangchaincode-splitterchunkingrag

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All langchain document loaders & chunking posts →