Ask a model for a summary and the first draft is usually too sparse — it names a couple of things and pads the rest with filler: "remarkable detail", "an important moment". The obvious fix is to ask for "more detail", but then the summary gets longer and you have lost the whole point of a summary. Chain-of-Density (CoD) is the technique that fixes the content while holding the length fixed. I built an interactive demo of it, and the mechanism is simpler — and sharper — than I expected.
Fix the length; iterate on density
The one rule that makes CoD work: the length never changes. You set a target word count once and hold it every pass. Then each pass identifies 1–3 salient entities that are in the source but missing from the current summary, and rewrites the summary to include them at the same length — by fusing new facts into existing sentences and compressing the filler that was taking up space. Entities are the concrete nouns: people, places, dates, numbers, named things. Density (entities per 100 words) climbs steeply; length stays flat.
const TARGET = 50; // words — held constant every rewrite
const STEPS = 5; // densification passes
const PER_STEP = "1-3"; // new entities per pass
// the invariant enforced in every prompt: "Keep the length the same."
The two prompts per pass
Each pass is two small model calls. First, find what is missing:
const missing = await llm(`
List the 1-3 MOST salient entities in the ARTICLE that are
MISSING from the SUMMARY. Entities = people, places, dates,
numbers, named things. Return a JSON array of short strings.
ARTICLE: ${article}
SUMMARY: ${summary}
`); // e.g. ["James Webb Space Telescope", "NASA", "July 12, 2022"]
Then rewrite to fold them in without growing:
summary = await llm(`
Rewrite the SUMMARY so it now covers these NEW entities,
keeping EXACTLY the same length (~${TARGET} words).
Make room by FUSING facts into existing sentences and
COMPRESSING filler ("remarkable detail", "an important moment").
Do NOT drop any entity already present.
NEW: ${missing}
SUMMARY: ${summary}
`);
Watch this run on a real article in the demo and you can see the machine at work. A JWST summary starts as "A powerful new space telescope has released its very first color pictures…" (two entities, all filler). By pass 3 it is "On July 12, 2022, NASA unveiled the James Webb Space Telescope's first full-color images, led by galaxy cluster SMACS 0723 — the deepest infrared view of the universe yet…" — same length, five times the specifics, because the vague clauses were compressed to make room.
Keep every rung — the answer is in the middle
Here is the part people miss. Unlike a pass/fail loop, CoD does not converge on one final answer — it produces a ladder from sparse to dense, and you keep every rung. The last rung is the densest, but it is usually the worst: a comma-spliced pile-up that reads like a list. Informativeness rises while readability falls, and they cross somewhere in the middle.
const chain = [{ summary, entities: entitiesIn(summary) }];
for (let s = 1; s <= STEPS; s++) {
const missing = await findMissing(article, summary);
summary = await densify(summary, missing, TARGET); // same length
chain.push({ summary,
entities: chain[s-1].entities.concat(missing),
density: (chain[s-1].entities.length + missing.length) / words(summary) });
}
My demo plots two curves side by side: a density bar that rises steeply while the length bar barely moves, and a quality curve that rises then dips. The green bar — the sweet spot — is where richness and readability balance, and in the original CoD paper humans preferred exactly those middle passes, not the maximally-dense final one.
The cheap version: one prompt, whole chain
You don't actually need N round-trips. One prompt can emit the entire chain as JSON, and you pick the rung you want:
const chain = JSON.parse(await llm(`
Produce ${STEPS} increasingly dense summaries of the ARTICLE.
Each is ~${TARGET} words (SAME length). Each adds ${PER_STEP} missing
salient entities vs the previous, fusing + compressing to fit.
Return JSON: [{ step, summary, addedEntities }].
ARTICLE: ${article}
`));
const pick = chain[Math.round(chain.length * 0.6)]; // middle-ish sweet spot
return pick.summary;
The whole idea in one line: fix the length, then raise the information density pass after pass, and ship the readable sweet spot in the middle. It costs one prompt and it turns a flabby first draft into something genuinely dense — without ever letting it grow.
Step the chain and watch density climb at flat length:
https://dev48v.infy.uk/prompt/day40-chain-of-density.html
Top comments (0)