What is Sectioning?
Sectioning is an AI design pattern for LLM apps and agents, in the Composition group. Break the task into independent parts and run them all in parallel.
Seccionamento
Sectioning is an AI design pattern for LLM apps and agents, in the Composition group. Break the task into independent parts and run them all in parallel.
Break the task into independent parts, defined in code, and run them all in parallel, each with its own focused prompt. It is for tasks that look at the same input from several angles, or that split into pieces with no dependency between them.
A judging panel where each judge scores only one criterion, all at the same time, and the scores are added at the end.
Felipe maintains the team’s automated PR reviewer. The prompt asks it to review security, performance, tests, style, and API compatibility, all in one call. Reviews are slow, and quality is uneven: style comments always show up, security problems only sometimes.
When he looks at the traces, he sees why. The model has to hold five lists of criteria at once and tends to cover best what is easiest to notice. A SQL injection hides among twelve comments about variable names.
Felipe tried splitting it into five calls, one per aspect. Quality improved, but reviews took five times as long, because the calls ran one after another.
The question this pattern answers: how do you give each part of a task its own focused call without paying the sum of the latencies?
Split the task into fixed sections, decided in code: one aspect per section, or one piece of the input per section. Each section has its own short, specialized prompt. They all run at the same time. A final step, in code or with a model, combines the results.
The difference from Orchestrator-Workers is that the split does not depend on the input: code knows in advance which sections exist. The difference from Voting is that each section does something different, instead of all doing the same thing.
Anthropic’s “Building effective agents” post (December 2024) calls this parallelization, in the sectioning variant: break the work into independent subtasks and run them at the same time. The other variant is voting, where several calls do the same job.
flowchart LR
IN(["1. PR diff"]):::io --> S1(["2. Security"]):::llm
IN --> S2(["2. Performance"]):::llm
IN --> S3(["2. Tests"]):::llm
IN --> S4(["2. Compatibility"]):::llm
S1 --> J["3. Merge and dedupe"]:::tool
S2 --> J
S3 --> J
S4 --> J
J --> F{"4. Filter by severity"}
F --> OUT(["5. PR comments"]):::io
Felipe’s PR changes api/users.py. Four sections run at the same time over the same diff. Outputs (simplified):
{ "section": "security", "file": "api/users.py", "line": 48, "severity": "high",
"message": "query built with an f-string from the user's email" }
{ "section": "tests", "file": "api/users.py", "line": 48, "severity": "medium",
"message": "no test for a malformed email on this endpoint" }
The merge sees the same line twice. A dedupe key of file plus line would keep the security high and drop the tests finding, if sorting is by severity. That is why the dedupe key needs to include the message, or you accept that a tests comment vanishes when security already spoke about the line.
With a single call, the model covered style and barely saw the injection. “Lost in the Middle” (Liu et al., 2023) helps explain: five lists of criteria in the same window starve the middle one (security, in Felipe’s prompt). A security-only section does not compete with style.
schema Finding { file: string, line: int, severity: "high" | "medium" | "low", message: string }
schema Findings { items: list[Finding] }
SECTIONS = {
"security": SECURITY_PROMPT,
"performance": PERFORMANCE_PROMPT,
"tests": TESTS_PROMPT,
"compatibility": API_COMPAT_PROMPT,
}
function review(diff):
# 2. every section in parallel
parallel {
for name, prompt in SECTIONS:
results[name] = llm(prompt, diff) -> Findings
trace.span("section." + name, results[name])
}
# 3. merge in code
all = flatten(results)
unique = dedupe(all, key=(f.file, f.line, similar(f.message)))
# 4. filter
return [f for f in unique if f.severity in ["high", "medium"]] # 5
“Security first, then tests using the security findings” is not sectioning. It is a chain. Parallelism only pays off if each section can run without the others’ results.
If the split changes with the diff (an infra PR needs sections a CSS PR does not), a model decides the list, and the pattern becomes Orchestrator-Workers.
Cap concurrency. Four sections on one PR is fine. Forty PRs at once, four sections each, is 160 calls in a burst. A semaphore of, say, 8 in-flight calls keeps you inside the provider’s rate limit. The slowest section still sets latency; the cap only spreads the burst.
Pick the model per section. Style and “missing tests” often run on a small model. Security deserves the larger one. That is a cascade inside sectioning: expensive attention only on the aspect that pays for it.
Four sections read the same diff four times. Suppose a 3,000-token diff and 200 tokens of prompt per section. With no cache, that is about 12,800 input tokens. With prompt caching on the shared prefix (the diff first, the instruction after), the provider charges the full diff once and later reads come cheap.
Latency does not add up: you wait for the slowest section. If security takes 8 seconds and style takes 3, the total is close to 8, plus the merge. That is why a section that got slow deserves its own alert.
Sections by chunk look like Map-Reduce, and the border is the merge. If each chunk returns findings of the same type and code only concatenates them, it is still sectioning. If each chunk returns a summary and a model stitches one text, you have moved to Map-Reduce.
The guard section in parallel is the case where you accept throwing work away. The answer is already being generated while the guard reads the input. If the guard rejects, you discard tokens already spent. It pays off when answer latency matters more than the discarded cost, and when the guard is cheaper than generating again afterwards.
| Aspect | What happens | How it shows up in the trace |
|---|---|---|
| Calls | One per section, plus the merge when it uses a model | sibling section.* spans |
| Latency | That of the slowest section, plus the merge | spans overlapping in time |
| Cost | Each section reads the whole input; prompt caching reduces it | input tokens multiplied by the number of sections |
| Failure: duplicates | Two sections flag the same problem | near-identical findings in the final result |
| Failure: silent section | A section fails, or never finds anything | a persistent zero count for one section |
| Failure: rate limit | Too many simultaneous sections | 429 errors on section spans |
A PR reviewer dataset is not “random PRs”. It is diffs with planted problems: an injection on line 48, an N+1, an endpoint with no test. Without planting, security recall is a guess. With planting, the security section has a golden case per failure type.
When the single call already finds the N+1 and the performance section does not, that section’s prompt is worse than the old block. Copy the slice that worked. Do not invent new criteria in the dark.
{ "diff": "fixtures/pr-311.diff", "expected": [{ "section": "security", "file": "api/users.py", "line": 48, "kind": "sql_injection" }] }
The merge is where sectioning usually degrades in silence. Deduping by file and line is too simple, as the code on this page admits. Different findings on the same line vanish. Prefer a key that includes a short hash of the message, or keep both and let the PR UI group them.
If you put a model in the merge to “write the final comment”, you bring back a serial bottleneck and a chance the model softens a security high. Merge in code, comment with the text the section already wrote. The reviewer should see who found what: a security finding stays tagged as security, even after dedupe.
The diff came from the user. A security section that reads the PR can be the target of prompt injection in the commented code itself. Isolate the section’s instruction from the diff text, and do not let the security section execute anything: it only returns findings.
Merging in code also avoids a final model that “rewrites” security findings downward. The minimum severity to comment sits in a rule, not in a prompt.
| Pros | Cons |
|---|---|
| Each aspect gets full attention | Input cost multiplied |
| Latency of the slowest section, not the sum | The merge has to handle duplicates |
| Short prompts, easy to test and tune | More simultaneous calls against the rate limit |
| New sections plug in without touching the others | A fixed split does not adapt to unusual inputs |
Both tabs run four sections in parallel over the same diff and merge the findings in code.
Python. Use this when the pipeline is async Python and sections return a shared schema.
import asyncio
from typing import Literal
import anthropic
from pydantic import BaseModel
client = anthropic.AsyncAnthropic()
class Finding(BaseModel):
file: str
line: int
severity: Literal["high", "medium", "low"]
message: str
class Findings(BaseModel):
items: list[Finding]
SECTIONS = {
"security": "You review security only: injection, authentication, secrets, permissions.",
"performance": "You review performance only: queries in loops, allocation, complexity.",
"tests": "You review tests only: missing cases for the changed code.",
"compatibility": "You review public API compatibility only: signatures and contracts.",
}
async def section(name: str, prompt: str, diff: str) -> list[dict]:
reply = await client.messages.parse( # 2
model="claude-sonnet-5-5", max_tokens=4000, system=prompt,
messages=[{"role": "user", "content": f"<diff>\n{diff}\n</diff>"}],
output_format=Findings,
)
return [{"section": name, **f.model_dump()} for f in reply.parsed_output.items]
async def review(diff: str) -> list[dict]:
results = await asyncio.gather(
*(section(n, p, diff) for n, p in SECTIONS.items()), return_exceptions=True
)
findings = [f for r in results if not isinstance(r, Exception) for f in r] # a failed section: continue
seen, unique = set(), []
for f in sorted(findings, key=lambda f: ["high", "medium", "low"].index(f["severity"])):
key = (f["file"], f["line"]) # 3. dedupe
if key not in seen:
seen.add(key)
unique.append(f)
return [f for f in unique if f["severity"] != "low"] # 4What you gained: four focused reviews in the time of one. What you lost: deduplication by file and line is simple; different findings on the same line get dropped.
TypeScript. Use this when the reviewer runs in a Node action or service.
import Anthropic from "@anthropic-ai/sdk";
import { zodOutputFormat } from "@anthropic-ai/sdk/helpers/zod";
import { z } from "zod";
const client = new Anthropic();
const Findings = z.object({
items: z.array(z.object({
file: z.string(), line: z.number().int(),
severity: z.enum(["high", "medium", "low"]), message: z.string(),
})),
});
const SECTIONS: Record<string, string> = {
security: "You review security only: injection, authentication, secrets, permissions.",
performance: "You review performance only: queries in loops, allocation, complexity.",
tests: "You review tests only: missing cases for the changed code.",
compatibility: "You review public API compatibility only: signatures and contracts.",
};
async function section(name: string, system: string, diff: string) {
const { parsed_output } = await client.messages.parse({
model: "claude-sonnet-5-5", max_tokens: 4000, system,
messages: [{ role: "user", content: `<diff>\n${diff}\n</diff>` }],
output_config: { format: zodOutputFormat(Findings) },
});
return (parsed_output?.items ?? []).map((f) => ({ section: name, ...f }));
}
export async function review(diff: string) {
const settled = await Promise.allSettled(
Object.entries(SECTIONS).map(([n, p]) => section(n, p, diff)), // 2. in parallel
);
const all = settled.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
const order = { high: 0, medium: 1, low: 2 };
const seen = new Set<string>();
return all
.sort((a, b) => order[a.severity] - order[b.severity])
.filter((f) => {
const key = `${f.file}:${f.line}`; // 3
if (seen.has(key)) return false;
seen.add(key);
return f.severity !== "low"; // 4
});
}What you gained: allSettled lets one section fail without bringing down the review. What you lost: the same limit of simple deduplication.
Sectioning is an AI design pattern for LLM apps and agents, in the Composition group. Break the task into independent parts and run them all in parallel.
In the real-world scenarios Automated PR reviewer, Content moderation, and Deep research with citations.