RAG Chunking skill for Codex
Chunks that follow structure and stand alone. Codex picks a chunking strategy per document type, splits on headings, clauses, rows or functions instead of fixed character windows, and keeps tables and code blocks whole. Each chunk carries its heading path and source location, sizes are measured in tokens, and two or three configurations are compared on a retrieval eval before re-embedding.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rag-chunking Installs to ~/.agents/skills/rag-chunking/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rag-chunking The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rag-chunking Installs to .agents/skills/rag-chunking/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rag-chunking.zipunzip -o rag-chunking.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rag-chunking PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $rag-chunking, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Chooses and implements chunking for retrieval by document type — structure-aware splitting for Markdown, HTML and docs sites, page and section handling for PDFs, row and table handling for spreadsheets, function-level splits for code, token-based sizes and overlap, contextual headers, parent-child chunks and metadata. Use when building or re-tuning a RAG index, when retrieval returns fragments without context, when tables or code get split mid-structure, or when choosing chunk size and overlap.
Use it when
- Building or re-tuning a retrieval index
- Retrieved snippets lack context or start mid-sentence
- Tables, lists or code are cut across chunks
- Mixed document types need different splitting
Use something else when
- Text extraction from scans or complex layouts is the problem: use document-ai-ocr
- You need the whole pipeline from ingest to cited answers: use rag-pipelines
How to check the result
Chunk statistics sit within target token ranges, twenty random chunks read cleanly on their own, and the chosen configuration matches or beats alternatives on recall@k.
The mistake it prevents
Using fixed character windows with large overlap to paper over bad boundaries, which bloats the index and still cuts tables in half.
Done well means: chunks follow the document’s own structure, each one makes sense on its own (with a contextual header), sizes are measured in tokens, tables and code blocks are never cut in half, metadata points back to the exact source location, and the chosen settings beat alternatives on a retrieval eval — not on intuition.
Defaults to start from
| Document type | Split on | Target size | Overlap | Extra |
|---|---|---|---|---|
| Markdown / docs sites | Headings (H1–H3), then paragraphs | 300–600 tokens | 0–10% | Heading path as header |
| HTML pages | Semantic tags (h*, section, article) after boilerplate removal | 300–600 | 0–10% | Drop nav, footer, cookie banners |
| PDFs (reports, manuals) | Detected headings; fall back to page + paragraph | 400–800 | 10–15% | Page number metadata; strip running headers/footers |
| Contracts / policies | Clause numbering (1.2, 1.2.3) | One clause, merge tiny ones | 0 | Clause id as citation anchor |
| FAQs / tickets | One Q&A or one ticket per chunk | as-is up to ~800 | 0 | Question text in header |
| Spreadsheets / CSV | Row groups with header row repeated | 20–50 rows | 0 | Or convert rows to sentences; or query with SQL instead |
| Code | Function/class (AST or tree-sitter) | one symbol, cap ~800 | 0 | File path + signature in header |
| Chat / transcripts | Speaker turns grouped by topic or time window | 300–600 | 1–2 turns | Speaker and timestamp metadata |
These are starting points. The right size depends on query type: short factual lookups favor smaller chunks; “explain how X works” favors larger ones or parent-child.
Workflow
- 1. Sample 20 documents per type and look at the extracted text (not the original)
- 2. Pick a strategy per type from the table
- 3. Implement token-aware splitting with structure boundaries
- 4. Add contextual headers and metadata
- 5. Inspect chunk statistics and 20 random chunks
- 6. Compare 2–3 configurations on the retrieval eval
3. Structure-aware, token-sized splitting
import re
def count_tokens(text: str) -> int:
try:
import tiktoken # optional dependency
return len(tiktoken.get_encoding("cl100k_base").encode(text))
except ImportError:
return max(1, len(text) // 4) # rough fallback
HEADING = re.compile(r"^(#{1,3})\s+(.*)$", re.M)
def split_markdown(doc_id: str, text: str, max_tokens=500, min_tokens=80):
sections, path = [], []
parts = HEADING.split(text) # [pre, hashes, title, body, hashes, title, body...]
if parts[0].strip():
sections.append(([], parts[0]))
for i in range(1, len(parts), 3):
level, title, body = len(parts[i]), parts[i + 1].strip(), parts[i + 2]
path = path[: level - 1] + [title]
sections.append((list(path), body))
chunks = []
for heading_path, body in sections:
buf = ""
for block in split_blocks(body): # paragraphs, but fenced code and tables whole
if buf and count_tokens(buf + block) > max_tokens:
chunks.append((heading_path, buf.strip()))
buf = ""
buf += block + "\n\n"
if buf.strip():
chunks.append((heading_path, buf.strip()))
return merge_small(chunks, min_tokens) # fold tiny chunks into their neighbor
def split_blocks(body: str):
blocks, cur, in_fence = [], [], False
for line in body.splitlines():
if line.strip().startswith("```"):
in_fence = not in_fence
if not line.strip() and not in_fence and cur:
blocks.append("\n".join(cur)); cur = []
else:
cur.append(line)
if cur:
blocks.append("\n".join(cur))
return blocks
A single block larger than max_tokens (huge table, long code) gets its own chunk or is
split by rows/lines with the table header or function signature repeated — never
mid-row or mid-statement.
Libraries that already do this (LangChain MarkdownHeaderTextSplitter /
RecursiveCharacterTextSplitter.from_tiktoken_encoder, LlamaIndex node parsers,
unstructured partitioning) are fine if the repo already depends on them; installing
new ones needs network and possibly approval.
4. Contextual headers and metadata
Prepend a short header to the text that gets embedded, so a chunk saying “this applies only to annual plans” carries what “this” is:
Document: Refund policy (v12) > Eligibility > Annual plans
<chunk text>
Optionally generate a one-sentence LLM summary of the chunk’s role in the document and prepend it (“contextual retrieval”). It improves recall on ambiguous chunks but costs one model call per chunk at ingest — measure before adopting, and run it through Batch.
Metadata per chunk: doc_id, chunk_index, heading_path, page (PDF),
char_start/char_end or anchor for citation, doc_type, lang, updated_at, ACL.
Store the raw text separately from the embedded text so citations show clean content.
Parent-child (small-to-big)
Embed small child chunks (100–200 tokens) for precise matching; at answer time return their parent section (800–1,500 tokens) for context. Deduplicate when several children share a parent. Use when users ask detailed questions whose answers need surrounding context.
5. Inspect
import statistics
sizes = [count_tokens(c.text) for c in chunks]
print(len(sizes), statistics.median(sizes), max(sizes), sum(s < 50 for s in sizes))
Red flags: many chunks under 50 tokens (headers alone, nav fragments), max far above the target (an unsplit table), identical chunks across documents (boilerplate), chunks starting mid-sentence.
6. Compare on retrieval eval
Run the same question set (see rag-evaluation) for 2–3 configurations, e.g. 300/0,
500/50 and parent-child. Choose by recall@k at the k you actually prompt with, then by
index size and cost. Re-chunking requires re-embedding — budget it.
Gotchas
- Splitting by characters, not tokens: sizes vary 2–3× across languages and code.
- Fixed-size windows that cut tables, lists and code blocks in half.
- Overlap as a fix for bad boundaries: it inflates the index and duplicates hits; fix boundaries instead.
- PDF running headers/footers (“Confidential — Page 7”) repeated in every chunk dominate embeddings; strip them.
- Losing the heading path: chunk text alone often omits the subject.
- Changing chunking without bumping a
prep_versionleaves a mixed index.
Verification
- Chunk stats within target ranges; zero chunks over the embedding model’s input limit.
- 20 random chunks read by eye: each understandable alone, no broken tables or code.
- Retrieval eval of the chosen config ≥ the alternatives tested.
Output contract
| Doc type | Strategy | Size / overlap | Chunks | Median tokens |
|---|---|---|---|---|
| Docs site | headings → paragraphs, header path prefix | 500 / 0 | 18,240 | 312 |
| PDF manuals | headings, page metadata, footers stripped | 700 / 80 | 9,880 | 540 |
| Pricing CSV | 25-row groups, header repeated | n/a | 140 | 410 |
**Eval:** recall@8 300/0 = 0.79, 500/0 = 0.84, parent-child = 0.86 → chose parent-child
**prep_version:** 3 (re-embed scheduled via Batch) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "RAG Chunking"
short_description: "Chunks that follow structure and stand alone"
default_prompt: "$rag-chunking Our RAG index splits tables in half — rework chunking for the PDF manuals" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.