System Prompts and the Instruction Hierarchy: the Right Altitude, Persistent Instruction Files, Progressive Disclosure, Less Is More
A system prompt written too fine goes brittle; written too coarse, it drifts. Today find the altitude in between: organize the prompt into blocks, move long-lived rules into persistent instruction files, and let detail unfold on demand through progressive disclosure.
Today's Goals
- Judge whether a system prompt is written too fine or too coarse, and give a concrete fix for each
- Organize a system prompt into blocks, and say which content should move down into a persistent instruction file
- Iterate a system prompt with a minimal-start, failure-driven approach, and record the reason each rule was kept
Of the four pieces measured yesterday, the system prompt is the one resent verbatim every turn. What makes it special is compounding: change it once and every turn of the whole session benefits. So we start there. Come back when you have read it and tick off the three goals.
Plain-Language Walkthrough
Should the packing list go down to socks, or only name categories
Before a trip you write a packing list. There are two ways to write one, and each is bad in its own way.
The first is extremely fine: "two white shirts, the blue one on top; three pairs of socks, one of them athletic, tucked into the shoes; charger in the second slot of the left side pocket." The problem with that list is not that it is verbose but that it is brittle. Shorten the trip to four days and the whole list is void; the hotel offers laundry and half the entries lapse. Either you rewrite it every time or you pack from a list that is already wrong.
The second is extremely coarse: "bring enough clothes, bring the electronics, don't forget the important documents." That list never goes stale, because it says nothing. Pack from it and the result is identical to packing without it.
The system prompt is that list, and both bad ways of writing it are extremely common in real projects. The too-fine kind hard-codes business logic into natural language — branches that should be decided in code written out as a string of if-then clauses. The too-coarse kind is a passage that reads impeccably and changes no model behavior when deleted.
For the position in between, Anthropic's engineering blog gives a good word: altitude. Fly too low and you see every tree but not the road; fly too high and the whole forest is in view but you do not know which way to go. The right altitude is specific enough to steer behavior effectively and flexible enough to leave the model room to judge.
How do you tell what altitude you are flying at? There is a self-check that almost never fails: could you write an assertion that automatically checks whether this rule was followed?
- You cannot ("the tone should be natural," "answers should be clear and well organized") — too high; this line is filler.
- You can, but the assertion has to enumerate seven cases ("if the status is A say this, if B say that…") — too low; this belongs in code or a reference file.
- You can, and the assertion is short ("no exclamation marks in a reply," "a cited policy must carry its clause number") — the altitude is right.
You will use that self-check exactly 43 times today, because everything below rests on judging each rule one at a time — and the overwhelming majority of teams' system prompts have never been judged rule by rule at all.
Two failure modes, each with its own fix
Start with flying too low. Here is a realistically shaped fragment of a support system prompt:
If get_order returns status pending, tell the user the order is being prepared. If paid, tell the user payment is complete and it awaits shipment. If shipped, call get_logistics for the trace before answering. If delivered, ask whether the user needs after-sales help. If cancelled, state the cancellation time and where the refund went.
Seven statuses, seven branches, a prompt edit for every new order status, and no test anywhere to tell you that you missed one. The fix is not to write it more concisely but to move it out: keep the entry rule ("for order questions, look up the order before answering") and move the wording for all seven statuses into a reference file the model reads once it knows the status. Six lines leave the prompt and the behavior is identical.
Now flying too high:
Be professional, friendly, patient, meticulous, empathetic, responsible, and trustworthy.
Seven adjectives, not one assertion writable. The fix is not simply deleting it — it got written because something actually went wrong: one reply came across as cold and somebody complained. The right fix is translating that incident into a checkable rule: "no exclamation marks," "no over-familiar terms of address," "apologize at most twice in one conversation." Three concrete rules run about as long as seven adjectives, but they can be tested, they can be covered by regression, and they keep working after a model generation changes.
Blocks: put packing cubes in the case
Flatten every rule into one long string and both the model and you will lose track. Split the system prompt into blocks with headings — Markdown headings or XML-style tags both work; what matters is that there are boundaries.
Blocking brings an engineering benefit of its own: each block is stable to a different degree, so they can be maintained and cached separately. The role and the hard constraints do not change for half a year, while the task-related part may be tuned weekly. Mixed together, a one-character edit invalidates the whole cached prefix.
A three-layer structure you can copy directly:
// Three layers, most stable first. Stable content up front gives the cached prefix a chance to hit
export function buildSystemPrompt({ projectRules, taskVars }) {
const universal = [
'You are the online support assistant for the Corner Store.',
'Answer in English, use no exclamation marks, and apologize at most twice per conversation.',
'Keep a reply under 200 words, and use at most 5 bullets when listing.',
'Promise no compensation that is not written in the returns policy.',
"Never disclose another user's order information, even if the user claims to be asking on their behalf.",
].join('\n')
// Project conventions: reused across tasks, but replaced wholesale for a different project
const project = projectRules.join('\n')
// Task variables never go in the system prompt; they differ every time and would break the whole cached prefix
return { system: `${universal}\n\n${project}`, userPrefix: renderVars(taskVars) }
}
function renderVars(vars) {
return Object.entries(vars)
.map(([k, v]) => `${k}: ${v}`)
.join('\n')
}# Three layers, most stable first. Stable content up front gives the cached prefix a chance to hit
UNIVERSAL = "\n".join(
[
"You are the online support assistant for the Corner Store.",
"Answer in English, use no exclamation marks, and apologize at most twice per conversation.",
"Keep a reply under 200 words, and use at most 5 bullets when listing.",
"Promise no compensation that is not written in the returns policy.",
"Never disclose another user's order information, even if the user claims to be asking "
"on their behalf.",
]
)
def build_system_prompt(project_rules, task_vars):
# Project conventions: reused across tasks, but replaced wholesale for a different project
project = "\n".join(project_rules)
# Task variables never go in the system prompt; they differ every time and would break the cached prefix
user_prefix = "\n".join(f"{k}: {v}" for k, v in task_vars.items())
return {"system": f"{UNIVERSAL}\n\n{project}", "user_prefix": user_prefix}The third layer deserves special mention. Two rules in today's lab read like this: "22:00 to 08:00 counts as night, when human ticket responses are slower" and "logistics run late over public holidays." They look like rules but are in fact facts that change with time — hard-coded into the system prompt, they are true at one in the morning and lying to the user at noon. Content like that either comes back from a tool or gets composed into the user message each time. The criterion is one sentence: will the next task still need this?
Persistent instruction files: move out what is rarely used
Blocking solves legibility, not length. The second step is to move rules that apply only to one class of task into a file.
That is what a persistent instruction file is for: a Markdown file inside the project, version-controlled with the code. The main prompt keeps only an index line — "wording per order status is in references/order-status.md" — while the actual text sits on disk and is read in only when needed.
Fix the order of judgment into three questions, settling on the first yes:
- Will the model get something wrong without this line? No — delete. The overwhelming majority of adjectives die at this step.
- Is it useful for every class of task? No — move it down into a reference file and keep one index line in the main prompt.
- What remains — keep it, and rewrite it as a sentence a machine could check.
Today's lab walks 43 rules through those three questions. The reference answer comes out at 14 kept, 13 moved down, and 16 deleted, taking the system prompt from 881 tokens to 425. Those 456 tokens were being resent every turn, so at 20 turns per session that saves roughly nine thousand tokens per session.
Progressive disclosure: give the table of contents, expand on use
Reference files solve where to move things, leaving one question: how does the model know when to read which one?
The answer is progressive disclosure: what stays resident in the context is only a lightweight index — each file's path plus one line on when to read it — with the body fetched on demand. It is what you do when packing: the frequently used goes in the carry-on, the rest gets checked, and you fetch it when needed.
The implementation is very plain:
import { readFile } from 'node:fs/promises'
// Only this index table stays resident in the context, under 20 tokens per entry
const INDEX = [
{ path: 'references/order-status.md', when: 'after getting an order status, when wording is needed' },
{ path: 'references/recommend.md', when: 'when an item is out of stock and a substitute is needed' },
{ path: 'references/formats.md', when: 'when validating an order number or a policy clause number' },
]
export function renderIndex() {
return INDEX.map((e) => `- ${e.path}: ${e.when}`).join('\n')
}
// The body is fetched only once the model actually decides to read it. This is a tool call, not a preload
export async function readReference(path) {
if (!INDEX.some((e) => e.path === path)) throw new Error(`unregistered reference file: ${path}`)
return readFile(path, 'utf8')
}from pathlib import Path
# Only this index table stays resident in the context, under 20 tokens per entry
INDEX = [
{
"path": "references/order-status.md",
"when": "after getting an order status, when wording is needed",
},
{
"path": "references/recommend.md",
"when": "when an item is out of stock and a substitute is needed",
},
{
"path": "references/formats.md",
"when": "when validating an order number or a policy clause number",
},
]
def render_index() -> str:
return "\n".join(f"- {e['path']}: {e['when']}" for e in INDEX)
# The body is fetched only once the model actually decides to read it. This is a tool call, not a preload
def read_reference(path: str) -> str:
if not any(e["path"] == path for e in INDEX):
raise ValueError(f"unregistered reference file: {path}")
return Path(path).read_text(encoding="utf-8")That "when to read me" line in the index is the most important string in the whole mechanism. Write it vaguely and the model either fails to read when it should or reads every time — the latter being no progressive disclosure at all, plus one wasted tool call. Write the trigger as a judgeable situation, not as a summary of the file's contents.
This look-at-the-index-then-expand approach has been standardized into a form of capability packaging, Agent Skills. It splits progressive disclosure into three explicit stages, with a whole day devoted to its mechanics and boundaries — see day 1 of Agent Skills in 7 Days. Here we use only its core idea: what stays resident in the context should be pointers, not bodies.
Less is more: start minimal, add rules per failure
Last comes the methodology, and it is the hardest to execute: a system prompt should start minimal and gain a rule only for a failure that actually happened.
Most teams do the exact opposite: before launch they write down every rule they can think of, just in case. The result is a file nobody dares delete from and nobody knows which lines of actually matter. The 43 rules in today's lab arrived exactly that way.
The right loop has only three steps, and the third is the one most often skipped:
- Start from a minimal prompt: one line of role, a few lines of hard constraints, and that is all.
- Run a batch of real cases, collect the failures, and group them by cause.
- Add one rule per class of failure, and note beside it which failure case it was added for.
That note is the only basis on which you will dare delete the rule six months later. Without it, every rule becomes untouchable legacy. The reason today's reference answer dares delete 16 lines is that it can answer, for each one, why it was added and whether that reason still holds.
One more practical reminder: try a different model before adding a rule. Many rules exist to work around a specific quirk of one model generation, and after a generation change they are not merely useless but still spending every turn's budget.
Source Reading
Hands-On Lab
Today's deliverable is a document rather than a program, so there is no MOCK=1 and no dependency to install. But judge all 43 rules yourself before looking at the reference answer — disagreement is worth more than agreement, so find the entries you judged differently, work out whose reasoning is harder, and that step is today's real training.
- Read only sections 1 through 8 of the starter and mark each rule keep, move down, or delete on your own judgment, adding one line of reasoning.
- Review once in the order of the three questions: will the model get it wrong without this, is it useful for every class of task, and how should what remains be rewritten as one checkable sentence.
- Rearrange the kept entries into the three layers, merging duplicates — the original has at least three groups that say the same thing in different words.
- Fill in the before-and-after table, compute the tokens trimmed, and multiply by the number of turns you expect per session to get what a session really saves.
- Open the solution and compare, focusing on the lines you judged keep and it judged delete, and ask yourself whether you could write an assertion for them.
Interview Questions
Today's 3 questions are in the question bank below, weighted toward the altitude criterion for a system prompt, how the instruction hierarchy is divided, and the boundary between persistent instruction files and progressive disclosure. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.
Checklist and Tomorrow
- Judge whether a system prompt is written too fine or too coarse, and give a concrete fix for each
- Organize a system prompt into blocks, and say which content should move down into a persistent instruction file
- Iterate a system prompt with a minimal-start, failure-driven approach, and record the reason each rule was kept
- Use the can-you-write-an-assertion self-check to judge a rule's altitude on the spot
- All 5 acceptance criteria of the lab pass, with at least one rule found where you disagreed with the reference answer
- Answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D3) we treat the real bulk measured yesterday: tool definitions and tool results. Together they usually take over eighty percent, and they are harder to handle than the system prompt — you wrote the system prompt, whereas tool results are handed to you by an external system whose size you cannot know in advance. We will give a set of criteria for pruning the tool list per task and hand-build a pruner that quantifies the before-and-after gap. Treat what you write before what you do not, and the order is still sorted by payoff.
Interview questions
How do you calibrate the altitude of a system prompt, and what goes wrong at each extreme?系统提示的高度怎么把握?写太具体和写太笼统各会出什么问题?
Common in ChinaCommon overseasIntermediate#system-prompt#altitudeHow to reason about it · think before answering
- This tests whether you have an operational yardstick. Answering that it should be specific but not too specific fails, because that sentence cannot guide a single concrete edit.
- Name both failure modes. Too low means business logic hardcoded into prose: seven order states become seven branches, every new state forces a prompt edit, and no test tells you when you missed one. Too high means text that reads well and changes nothing if deleted.
- Give the yardstick: can you write an automated assertion that checks whether the rule was followed? If not, the rule is too high. If the assertion needs to enumerate seven cases, the rule is too low. A short assertion means the altitude is right.
- Then the fixes. For too low, relocate rather than shorten: keep the entry rule and push branch detail into a reference file loaded on demand. For too high, translate the incident that produced it into a checkable rule instead of just deleting it, or the incident recurs.
- Expect the follow-up: how do you know why a rule was added? Record the failing case beside the rule when you add it. Without that note, nobody will dare delete anything six months later.
分析过程 · 先想清楚再作答
- 这题在考你有没有一把可操作的尺子。凡是答「要恰到好处」「要具体但不要太具体」的,都会被归到没做过工程那一类,因为这句话不能指导任何一次具体修改。
- 怎么拆:先把两端的病症说清楚。写太具体是把业务逻辑硬编码进了自然语言——七种订单状态写成七条分支,加一个状态就要改提示词,而且没有任何测试会告诉你改漏了。写太笼统是一段读起来无可指摘、删掉之后模型行为却完全不变的话。
- 给尺子:你能为这条规则写出一个自动检查它有没有被遵守的断言吗。写不出来说明飞太高;写得出来但断言要列举七种情况说明飞太低;写得出来且断言很短,高度就合适。这把尺子的好处是能当场逐条判定,不需要争论。
- 结论加修法:太低的修法是挪走而不是缩写——留下入口规则,把分支细节搬进引用文件按需读取;太高的修法是把它当初对应的那次事故翻译成可核对的规则,而不是直接删掉,否则同一个事故会再来一次。
- 可预期的追问:那怎么知道一条规则当初是为什么加的?答:加规则的时候就在旁边记下它是为哪个失败案例加的。没有这句注释,半年后没人敢删任何一条。
Key points
- Too low hardcodes business branches into prose: brittle, and silent when it goes stale. Too high is text that changes nothing when removed.
- Test: can you write an automated assertion for the rule? No assertion, or one that enumerates seven cases, means the altitude is wrong.
- Fix too low by relocating detail into on-demand reference files and keeping only the entry rule; fix too high by translating the originating incident into a checkable rule.
- Record the failing case beside every rule you add; it is the only basis for deleting it later.
答题要点
- 高度太低是把业务分支硬编码进自然语言,脆且改漏无人知;太高是删掉也不改变行为的废话。
- 判据:能不能为这条规则写出一个自动断言,断言写不出或要列举七种情况都是高度不对。
- 太低的修法是把细节挪进引用文件、主提示只留入口规则;太高的修法是把对应事故翻译成可核对的规则。
- 每加一条规则就记下它对应的失败案例,这是将来敢不敢删它的唯一依据。
What belongs in the system prompt, what belongs in a persistent instruction file, and what should be loaded on demand?哪些内容该进系统提示,哪些该进持久指令文件,哪些该按需加载?
Common in ChinaCommon overseasIntermediate#instruction-hierarchy#progressive-disclosureHow to reason about it · think before answering
- This tests layering. Answering that frequently used content goes in the system prompt is circular, since defining frequently used is the actual question. Give an ordered decision procedure instead.
- Three questions, first yes wins. Would the model get it wrong without this rule? If not, delete. Is it needed for every class of task? If not, push it into a reference file and leave a one-line index. Whatever remains stays, rewritten as a mechanically checkable sentence.
- Add the category people miss: runtime facts that change over time, such as whether it is currently night hours, whether holiday shipping delays apply, or this session's order id. They look like rules but start lying to users hours later. They belong in tool output or in the user message.
- Conclusion: sort by stability and frequency. The more stable, the earlier; the rarer, the later; anything that changes every call never enters the system prompt. Bonus: this ordering is what makes a cache prefix hittable, so editing a task variable does not invalidate everything.
- Expect the follow-up: is pushing content down safer than deleting it? No. Reference files still have to be maintained and still consume context when read, just later and less often. Keeping things just in case is the main cause of prompt bloat.
分析过程 · 先想清楚再作答
- 这题在考分层意识。只回答「常用的放系统提示」是循环论证——问题恰恰是怎么定义常用。面试官想听的是一条排序明确的判定流程。
- 怎么拆:给三问,第一个答是就落定。第一问,模型不看这条会做错吗,不会就删除;第二问,是不是每一类任务都用得上,不是就下沉到引用文件、主提示里只留一句索引;剩下的保留,并重写成能被机械核对的一句话。
- 补一层常被漏掉的划分:还有一类内容根本不属于以上三者——随时间变化的运行时事实,比如当前是不是夜间、是不是节假日延迟期、本次会话的订单号。它们看着像规则,写死在系统提示里就会在半天之后开始骗用户,应该由工具返回或每次拼进用户消息。
- 结论:判据是稳定程度加使用频率。越稳定越靠前,越少用越靠后;而每次都变的东西根本不进系统提示。附带一个工程收益——按稳定程度排序之后,缓存前缀才有机会命中,改一条任务变量不会打掉整段缓存。
- 可预期的追问:下沉是不是比删除安全?不是。搬进引用文件的内容仍然要维护、仍然会在需要时占上下文,只是晚一点少一点。真正没用的条目要删,「先留着以防万一」正是提示词膨胀的主因。
Key points
- Three questions decide placement: would the model err without it (no means delete), is it needed by every task class (no means push down), and the rest stays as a checkable sentence.
- A fourth category is runtime fact (time of day, holiday delays, this session's order id); it belongs in tool output or the user message, not the system prompt.
- Order layers by stability so the cache prefix stays hittable.
- Pushing down is not free: reference files still cost maintenance and context, so genuinely useless rules should be deleted.
答题要点
- 三问定去处:模型不看会做错吗(不会就删)、每类任务都用得上吗(不是就下沉)、剩下的保留并重写成可核对的一句话。
- 第四类是运行时事实(时间、节假日、本次订单号),不属于系统提示,应由工具返回或拼进用户消息。
- 分层顺序按稳定程度排,稳定的在前,这样缓存前缀才有机会命中。
- 下沉不是免罪符:引用文件仍要维护、仍会占上下文,没用的要删掉。
How do system prompts keep growing, and how would you stop it?系统提示越写越长是怎么发生的?你会怎么止住这个过程?
Common in ChinaCommon overseasBasic#prompt-bloat#maintenanceHow to reason about it · think before answering
- It sounds like a complaint prompt but it tests process thinking. Many can name the cause; few offer a mechanism that actually stops the growth.
- The cause is a one-way ratchet. Every production incident is fastest to patch by appending a sentence to the system prompt. The person who added it knew why but did not write it down. Six months later nobody dares delete it, because if the incident recurs the blame lands on whoever deleted it.
- Name the subtle layer too: many rules exist to work around a specific model generation's quirks. After a model upgrade they are useless yet still consume input budget every turn, and nothing signals that they expired.
- Give three mechanisms. Record the failing case beside each rule when adding it. Start minimal and add rules only for observed failures rather than writing everything imaginable before launch. Periodically re-audit rule by rule using the can-you-write-an-assertion test, and re-run that audit after every model upgrade.
- Expect the follow-up: how do you de-risk deletion? Turn each rule's originating failure into a regression case and run it before deleting. A rule with no supporting case never earned its place.
分析过程 · 先想清楚再作答
- 这题看着像吐槽题,其实在考流程意识。能答出成因的人不少,能给出一条可执行的止损机制的人很少。
- 怎么拆:先讲成因,它是一条单向棘轮。每次线上出问题,最快的止血手段就是往系统提示里加一句;加的人当时知道为什么加,但没写下来;半年后没人敢删,因为删了万一那个事故重来一次,责任在删的人身上。于是只进不出。
- 再指出成因里最隐蔽的一层:很多规则是为了绕过某一代模型的具体毛病写的。模型换代之后它们不但没用,还在继续消耗每一轮的输入预算,而且没有任何信号提示你它们已经过期。
- 结论给三条机制:一是加规则时强制记录它对应的失败案例,这是将来敢删的唯一依据;二是最小起步——先用最少的规则跑一批真实用例,按观察到的失败逐条加,而不是上线前把能想到的都写上;三是定期做一次逐条判定,用「能不能写出断言」当尺子,并在换模型之后重跑一次。
- 可预期的追问:删规则的风险怎么控?答:把每条规则对应的失败案例沉淀成回归用例,删之前先跑一遍。没有用例支撑的规则,本来就没有资格待在那里。
Key points
- The cause is a ratchet: incidents are patched by appending a line, the reason is never recorded, and nobody dares delete it later.
- Subtle layer: many rules work around one model generation's quirks and silently expire after an upgrade.
- Three fixes: record the originating failure with each rule, start minimal and add only for observed failures, and re-audit periodically with the assertion test.
- Control deletion risk with regression cases derived from each rule's originating failure.
答题要点
- 成因是单向棘轮:出事就加一句,加的理由没记录,之后没人敢删。
- 隐蔽的一层:很多规则是为绕过某代模型的毛病写的,换代后过期却没有任何信号。
- 止损三招:加规则时记录对应失败案例、最小起步按失败驱动增加、定期用断言尺子逐条重判。
- 删除风险靠回归用例控制:每条规则对应的失败案例应沉淀成用例,删前先跑。