What is Contextual Chunking?
Contextual Chunking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Split documents along their structure and attach to each chunk the context it came from.
Eduardo maintains the search over a health insurer's internal manual. Somebody cut the documents every five hundred tokens, ignoring the structure. And one of the chunks says only this.
The deadline is twenty one business days from the request, and it may be extended once, with justification. In the cases covered by item three point two.
Deadline for what? For whom? From which version of the manual? Search finds this passage for the authorization deadline for surgery. But also for the reimbursement deadline, because the passage never says which topic it covers. And the model answers with confidence, and sometimes gets the topic wrong.
In this lesson, you'll learn to cut documents so that each chunk can be understood, and found, on its own.
Think of a file card. A good one doesn't just say what it contains. It says which folder, which drawer, and which year it came from. The pattern makes two changes, both at indexing time.
First, split by structure. Headings, sections, clauses, list items and tables are the boundaries. A chunk never starts mid sentence, and never separates a table from its header. There's still a maximum size, but it's the limit, not the rule.
Second, attach context. Each chunk gets a header with its path, document, chapter and section, its version, and optionally one sentence, written by a model, that situates the passage.
That header goes into the text you index, for the search. And into the text you send to the model, for the answer and the citation.
The document arrives, as a PDF, a web page or plain text.
A parser finds the headings, sections, lists and tables.
The split follows that structure, within the maximum size.
A small model reads the document and the passage, and writes one sentence that says where it belongs.
Each chunk becomes a header, with path, version and that sentence, plus the original text.
And the full chunk is indexed, for keyword search and for vector search.
Now the same passage, after the pattern. Three things changed, and each one teaches something.
The path. Authorizations, then elective surgery, now goes into the embedding and into keyword search. A reimbursement question no longer looks just as similar.
The context sentence. It translates the vague passage into the vocabulary of the question. Authorization, elective surgery, individual plan.
And the neighboring clause. If the section fits the limit, item three point two travels with it. If it doesn't, the header still points at section four point three, and the citation doesn't pretend the passage is complete.
What does it cost? Say eight hundred tokens of body and eighty of header. Five chunks per question go from four thousand tokens to four thousand four hundred. The real cost sits in ingestion, one small call per chunk, with the whole document cached. Not in the question.
And putting the path at the start helps twice. The search finds the right topic. And the model, which reads the start of a context better than the middle, sees first where the passage came from.
Here's one for you. You turned on the context sentence, but you generated it by reading only the chunk, not the document. In the eval, the reimbursement question still retrieves the authorization passage. Why?
Because the sentence is as vague as the chunk. The model writing it never saw the chapter either. So it writes something like, this passage discusses a deadline.
The sentence exists to inject the context the chunk lost. Without the document in that call, there's no context to inject. That's why the call carries the whole document, cached, and reuses it across every chunk.
Short documents that fit in a single chunk. A two page remote work policy doesn't need to be cut. A title and a version at the start are enough. Splitting it only invents boundaries, and can cut the rule in half.
Unstructured content, like conversation transcripts. There are no headings to follow. Split by topic, or by overlapping windows.
And material that fits whole in the context. That's the smell from two lessons ago. Don't chunk what the model could read.
Now the common traps. Cutting by characters and calling it structural. If the boundary ignores the table title, you only changed the size.
Which is exactly what happened at the insurer. The rows with the deadline numbers landed in one chunk. The header, with the columns, authorization, reimbursement, appeal, stayed in the previous one. So the bot started citing twenty one, without saying which deadline. A table becomes its own chunk, with its title and its columns.
And leaving repealed versions in the index. A header that says version six doesn't stop the search from returning version six. Filter by effective date at query time, and remove the old document when you reindex. That's solved in the index, not in the prompt.
What to measure. Search recall and precision, before and after, on the same eval.
Build twin questions on purpose. The reimbursement deadline and the authorization deadline. If the reimbursement passage stays out of the top five for the authorization question, the header is doing its job.
Check the citations. The section cited in the answer has to be the right one.
And alert on the empty-path rate at ingestion. If thirty percent of the chunks come out with no path, the parser broke on a new kind of PDF. Don't paper over it with the generated sentence. Fix the conversion, and reindex.
One security point. A chunk can carry personal data, a member's name, a card number. Under data protection law, the index is one more place where that data lives. If your policy forbids sending those documents to the model that writes the context sentence, stay on the structural header, built only from title and section.
On the page, there are two tabs. The TypeScript one does only the structural part. It splits the document by its headings, keeps the path as it goes down, and puts title, path and version at the top of each chunk. No model call at all.
The Python one adds the context sentence. It sends the whole document as a cached block, and asks a small model, for each chunk, which topic and case that passage covers.
And that's the order to adopt it. Start with the structural header. It's cheap, and it already solves much of the problem.
Turn on the generated sentence only if the topic confusion eval still fails.
So here's the idea to take with you. A chunk cut blindly is a sentence without an address. Cut along the document's own structure, and give each piece its path, its version and, when it helps, a sentence that says what it's about. Then both the search and the model know where they are.
That path in the header is made of exact words, and exact words are what keyword search loves. In the next lesson, we'll combine keyword search with embeddings: hybrid search. See you there.
Chunking com Contexto
Contextual Chunking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Split documents along their structure and attach to each chunk the context it came from.
Split documents along their structure (sections, clauses, tables) and attach to each chunk the context it came from: document, section, date, and a sentence that situates the passage. It is for RAG systems where chunks lose their meaning on their own.
A file card that says not only what it contains, but which folder, drawer, and year it came from.
Eduardo maintains search over a health insurer’s internal manual (a Brazilian private plan, overseen by ANS, the national regulator). The documents were cut every 500 tokens, ignoring structure. One of the chunks says only:
...the deadline is 21 business days from the request, and it may be
extended once with justification. In the cases covered by item 3.2...
Deadline for what? For whom? From which version of the manual? Search finds this passage for “what is the authorization deadline for elective surgery?”, but also for “what is the reimbursement deadline?”, because the passage does not say which topic it covers. The model answers confidently and sometimes gets the topic wrong.
The blind cut also separated a table from its title. The rows with deadline numbers landed in one chunk; the header with the columns (authorization, reimbursement, appeal) stayed in the previous one. The bot started citing “21” without saying which deadline.
Then there is item 3.2. It lives in another chunk. Anyone who reads only the passage above cannot tell what the item allows. In this insurer, that mixes ANS rules with the plan’s own rules.
The question this pattern answers: how do you split documents so that each chunk is understandable and findable on its own?
Two changes at indexing time:
The header goes into the indexed text (for search) and into the text sent to the model (for the answer and the citation).
Anthropic described this idea in Contextual Retrieval (September 2024). Before indexing, a model reads the document and the chunk and writes a sentence that situates the passage. That sentence goes into the text used for the embedding and for keyword search.
Take the deadline passage and the document it came from. After the blind cut, the index stores only the middle. After this pattern, the same middle gets a header:
[Member Handbook | Authorizations | 4.1 Elective surgery | v7, effective 2026-03]
This passage covers the authorization deadline for elective surgery on individual plans.
The deadline is 21 business days from the request, and it may be
extended once with justification. In the cases covered by item 3.2
(urgency and emergency, see section 4.3), the deadline does not apply.
Three things changed, and each one teaches a lesson.
A token account, with illustrative numbers. Suppose 800 tokens of body and 80 of header. Across 2,000 chunks, the index grows by about 160,000 tokens. At query time, five chunks go from 4,000 to 4,400 tokens. The extra cost sits in ingestion (one small call per chunk, with the document cached), not in the question.
The paper “Lost in the Middle” (Liu et al., 2023) shows that models use the start and end of a context better than the middle. Putting the path at the start of the chunk helps twice: search finds the right topic, and the model reads first where the passage came from.
flowchart LR
D(["1. Document"]):::io --> P["2. Parse the structure"]:::tool
P --> S["3. Split by section and clause"]:::tool
S --> C(["4. Generate a context sentence"]):::llm
C --> H["5. Build header and passage"]:::tool
H --> I[("6. Index")]:::store
schema ChunkContext { summary: string /* one sentence */ }
function index_document(doc):
tree = parse_structure(doc) # 2. headings, sections, tables
for section in tree.leaves(max_tokens=800): # 3. by structure
path = " > ".join(section.ancestors_titles())
ctx = llm(CONTEXT_PROMPT, doc.summary, section.text, model="small") -> ChunkContext # 4
header = f"[{doc.title} | {path} | version {doc.version}] {ctx.summary}"
index.add(id=section.id, text=header + "\n" + section.text,
metadata={doc: doc.id, path: path, version: doc.version}) # 5 and 6
Start with the structural header. Turn on the generated sentence only if the topic-confusion eval still fails.
| Aspect | What happens | How it shows up |
|---|---|---|
| Cost | At indexing: one small call per chunk, with the document cached | ingestion cost per document |
| Latency | None at query time; the work happens at indexing | ingestion time |
| Size | Headers make each chunk slightly bigger | tokens per chunk |
| Failure: wrong parser | Badly read structure produces meaningless chunks | chunks with an empty or swapped path |
| Failure: wrong context sentence | It places the passage under the wrong topic | off-topic search results |
| Failure: old version | Repealed documents stay indexed | answers citing a previous version |
{ "question": "What is the authorization deadline for elective surgery?", "relevant": ["manual-v7#authorizations-4.1"], "not_relevant": ["manual-v7#reimbursement-2.3"] }
Build twin questions on purpose. If the relevant reimbursement passage is not in the top 5 for the authorization question, the header is doing its job.
An indexed chunk can carry personal data (a member’s name, a card number). Under Brazil’s LGPD, the index is one more place that data lives. Do not send documents with personal data to the context-sentence model if company policy forbids it. In those cases, stay on the structural header, built only from title and section.
| Pros | Cons |
|---|---|
| Each chunk is understandable on its own | A parser per document type |
| Search finds the right topic | Indexing cost for the generated sentence |
| Citations with the exact section | Slightly larger chunks |
| Versions distinguishable in the index | Reindexing needed when structure changes |
Both tabs split a document by headings and build the header with the path; the Python tab also generates the context sentence with the whole document cached.
Python. Use this when documents are in Markdown or were converted to a format with headings.
import re
import anthropic
client = anthropic.Anthropic()
def split_by_headings(markdown: str, max_chars: int = 3000) -> list[dict]: # 2 and 3
chunks, path = [], []
for block in re.split(r"\n(?=#{1,4} )", markdown):
m = re.match(r"(#{1,4}) (.+)", block)
if m:
level = len(m.group(1))
path = path[: level - 1] + [m.group(2).strip()]
for i in range(0, len(block), max_chars): # large section: split, keeping the path
chunks.append({"path": " > ".join(path), "text": block[i : i + max_chars]})
return chunks
def situate(doc_text: str, chunk_text: str) -> str: # 4
reply = client.messages.create(
model="claude-haiku-4-5", max_tokens=100,
system=[{"type": "text", "text": f"<document>\n{doc_text}\n</document>",
"cache_control": {"type": "ephemeral"}}], # document cached across chunks
messages=[{"role": "user", "content": "In one sentence, say which topic and case this passage covers, "
f"to help find it in a search.\n<passage>{chunk_text}</passage>"}],
)
return reply.content[0].text.strip()
def index_document(doc: dict) -> list[dict]:
out = []
for c in split_by_headings(doc["markdown"]):
header = f"[{doc['title']} | {c['path']} | version {doc['version']}] {situate(doc['markdown'], c['text'])}"
out.append({"id": f"{doc['id']}#{c['path']}", "text": f"{header}\n{c['text']}", "path": c["path"]}) # 5
return outWhat you gained: chunks with path, version, and a context sentence, with the document read from cache for each chunk. What you lost: heading-based parsing depends on the document having headings; unmarked PDFs need better conversion first.
TypeScript. Use this when you want to start with only the structural header, with no generated sentence.
type Chunk = { id: string; path: string; text: string };
export function chunkByHeadings(doc: { id: string; title: string; version: string; markdown: string }, maxChars = 3000): Chunk[] {
const chunks: Chunk[] = [];
let path: string[] = [];
for (const block of doc.markdown.split(/\n(?=#{1,4} )/)) {
const m = block.match(/^(#{1,4}) (.+)/);
if (m) path = [...path.slice(0, m[1].length - 1), m[2].trim()];
const p = path.join(" > ");
const header = `[${doc.title} | ${p} | version ${doc.version}]`; // 5. structural header
for (let i = 0; i < block.length; i += maxChars) {
chunks.push({ id: `${doc.id}#${p}#${i}`, path: p, text: `${header}\n${block.slice(i, i + maxChars)}` });
}
}
return chunks;
}What you gained: an indexing step with no model cost that already solves most of the lost context. What you lost: very vague passages stay hard to find; add the generated sentence if the eval shows failures.
Answer the 6 questions to complete this lesson. Watching alone does not complete it: the check is what counts toward the trail and the certificate.
Contextual Chunking is an AI design pattern for LLM apps and agents, in the Context and knowledge group. Split documents along their structure and attach to each chunk the context it came from.
In the real-world scenarios Internal knowledge base chat and Legal assistant.