Dayward AI
Week 2 · D12About 6 hours

Cost and Model Routing: Choosing a Model per Stage, Caching, Degradation, and a Budget Circuit Breaker

Get a clear tally of the whole line's costs, tier models per stage, bring cost down with caching and degradation, and fit every run with a budget circuit breaker that actually pulls the brake.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Break cost down per stage, and point out where most of the money goes and why
  2. Configure a model-routing policy per stage that makes an explainable trade-off between quality and cost
  3. Implement a budget circuit breaker that safely stops a run when it goes over budget instead of burning through it all

For eleven days you made this line steadier: resumable, concurrent, reviewable, quality-checked. Today changes dimension — making it affordable to run. When you finish, scroll back up and tick the three goals off.

Plain-Language Walkthrough

The line producer's first day: put the books on the table

Productions have a role called the line producer, responsible for one thing: where the money went. The classic rookie error is to start by squeezing the catering budget — catering is a few hundred a day, and that crane is a few thousand. Squeeze the wrong thing and you exhaust yourself saving nothing.

Your production line is the same. For eleven days run.json has been quietly keeping the books (D7 printed a bill, D8 persisted ledger entries), and nobody has read the table carefully. Today we open it, because the four kinds of call on this line differ in price by three orders of magnitude.

StageBilling unitPublished rateUsage per episodeSubtotal
Shot video (768P)per secondCNY 0.503 shots, 18 secondsCNY 9.00
Look tests and per-shot first framesper imageCNY 0.0255 imagesCNY 0.125
Line dubbingper billed characterabout CNY 0.00035 (derived)27 charactersCNY 0.0095
Script and review textper tokenper the official consolenot priced

Three caveats must come first, or everything after them is wrong.

First, the speech tier is not an official rate. The vendor publishes only package pricing (an HD package at CNY 630 for 2 million characters), and the per-unit rate is derived from it, a bit over three yuan per ten thousand characters. So every speech-related amount in this course is marked as derived — reporting a derived figure to your boss as an official price will end badly.

Second, the text stage is simply not priced. Its per-unit rate is whatever the official console says; if we cannot get it, we write no number, and the ledger still counts tokens with the amount column blank. An uncertain number written into a table is far more dangerous than a blank, because nobody will ever check it again.

Third — the most counterintuitive one — the video row's rate cannot be attached to any specific model id. The official pay-as-you-go page gives per-second tiers (CNY 0.50 per second at 768P, 0.80 at 2K, 0.33 at 480P), while the Hailuo line used in this course's code is deducted as credits from a package, with no published per-second rate; the per-second tiers belong to a separately billed set of models. The two billing regimes are simply not interchangeable. So the lab's table prices by "tier plus spec," and the video rows are derived constants for ledger demonstration, marked as such in the code comments and on the dashboard.

That is itself a good teaching point: the first task in building a cost ledger is not writing the table, it is establishing what each item is actually billed by. Different product lines from one vendor, pay-as-you-go versus subscription, credits versus cash — the regimes are frequently inconsistent. Either verify them or honestly mark them derived. The worst outcome is mixing regimes in one table and then budgeting from it.

Add the four rows: just over CNY 9.13 per episode, of which video is 98.5%. That is the origin of this chapter's — and this whole course's — central judgment:

The most expensive, slowest and most failure-prone thing on this line is video generation, so the entire engineering design orbits the question of how to call the video API one fewer time.

Looking back at the first eleven days feels different now: idempotency keys (D8) exist to avoid repeat video calls, reference-image reuse (D3) exists to need fewer video attempts, the concurrency gate (D9) exists to avoid blowing the video quota, and human review before assembly (D10) exists to avoid shooting a whole episode for nothing. They are not unrelated best practices; they are corollaries of one judgment.

How does the table become code? The key is that the ledger records at the moment of the call, not by guessing afterwards. Each record carries the stage, the type, the model, the usage, whether it hit the cache and whether it was really billed; aggregate by stage at the end and where the money went is obvious.

pricing.js
// Rate table: published rates only. Where we cannot get one, leave it blank rather than invent it.
const RATES = {
  'video-768p': { unit: 'second', cny: 0.5, approx: false },
  'video-2k': { unit: 'second', cny: 0.8, approx: false },
  'video-480p': { unit: 'second', cny: 0.33, approx: false },
  image: { unit: 'item', cny: 0.025, approx: false },
  // Only package pricing is published, so this tier is derived and must be flagged on the dashboard
  tts: { unit: 'character', cny: 0.00035, approx: true },
}
 
const estimate = (rateId, units) =>
  rateId ? Number((RATES[rateId].cny * units).toFixed(4)) : 0
 
function summarize(rows) {
  const byStage = new Map()
  for (const r of rows) {
    // Cache hits, and failed calls that were not billed, are recorded but cost nothing
    const cny = r.cached || !r.billed ? 0 : estimate(r.rateId, r.units)
    byStage.set(r.stage, (byStage.get(r.stage) ?? 0) + cny)
  }
  const total = [...byStage.values()].reduce((a, b) => a + b, 0)
  // Tie-break by stage name so two runs produce line-for-line identical reports
  return [...byStage].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
    .map(([stage, cny]) => ({ stage, cny, share: total ? cny / total : 0 }))
}

Two details deserve mention. One is the cached and billed booleans: cache hits are recorded with zero cost, and a failed or safety-blocked video is not billed, likewise recorded with zero cost. Without those two flags the report either overstates the bill or hides how much the cache saved. The other is the sort tie-breaker — order by name when amounts match, or line order drifts between runs and you will think something is broken.

With the books open the question gets concrete: video is over ninety percent, can that ninety percent be reduced? It can, along four non-overlapping routes — tiering, caching, degradation, circuit breaking — each covered below.

Tiered routing: a draft pass and a final pass are not the same job

Do you deploy to production on every line of code you change? Of course not — run locally, run CI, then ship. Generated content is the same, and yet many people run every experiment at the most expensive tier.

Tiered routing means the same business code selects different models and specs according to what this run is for. This line has at least two tiers. The draft tier is for revising the script, adjusting the storyboard, trying camera moves — you are judging whether the narrative flows, and softer picture does not affect that. The final tier is the version that ships, where picture quality, duration and look tests can none of them be skipped.

The key is that the switching criterion lives in the code, not in someone's head: script review passed (D2), human review passed (D10), and final output explicitly requested — all three before the final tier is used.

What differs between tiers? The lab's two configurations:

DimensionDraft tierFinal tier
Video resolution512P768P
Max seconds per shot36
Character look testsnot generated, text description onlygenerated
Text model tier hintMiniMax-M2.7-highspeedMiniMax-M3
Speech model tier hintspeech-2.8-turbospeech-2.8-hd

The measured comparison: draft CNY 4.58, final CNY 9.13, 99% more expensive. So every extra attempt at the draft stage saves half the money; if an episode takes five attempts to settle, tiering alone saves twenty-odd yuan, and a five-episode season saves over a hundred.

There is a trap here that must be admitted, and it happens to be the best teaching material of the section. The official tiers are 768P at CNY 0.50 per second, 2K at 0.80, 480P at 0.33 — and our draft tier requests 512P, a tier with no corresponding rate. What now?

Estimate on the expensive side. The lab bills 512P at the 768P rate, flags that ledger row with rateApprox, and prints the flag on the dashboard verbatim. The rule sounds conservative, but the worst outcome of over-estimating is a few fewer attempts, while the worst outcome of under-estimating is a doubled bill at month end with no idea why.

Estimating high has a consequence: 512P and 768P cost the same on paper, so lowering the resolution saves literally nothing. What actually saves money in the draft tier is the other two items — three-second shots and no look tests. Which is the reminder: degrade against the books, not against intuition.

Caching: the most profitable few lines on this line

The second route is caching. Re-run after changing one shot's visual description and the bill drops from CNY 9.13 to CNY 3.03 — 67% saved for a few dozen lines of code.

Why does this line suit caching so well? Because the artifacts are expensive and deterministic: the same prompt, reference image, duration and resolution regenerate into something indistinguishable, at full price again. For a chatbot, a different answer each time is a feature; here, a different regeneration each time is mostly a nuisance.

Designing the cache key was D8's subject (the workflow engine's idempotency key, one key governing both "skip finished nodes" and "hit the cache"). Today covers two things D8 did not.

First: the saving is layered. Re-running the same episode prints "cache hits 11 / 11, new spend CNY 0.0000"; changing one line or one shot's visual description prints "cache hits 9 / 11" — only the edited shot's first frame and video are recomputed and everything else is reused. That is why cache granularity must be per shot and not per episode: one notch coarser and one changed character re-runs a whole episode, making the cache pointless.

Second: a cache hit is not free. Cached files occupy disk, video especially; and the first run after clearing the cache spends everything at once. So the dashboard prints hit rows too, at zero cost, plus a separate line reading "saved by cache (estimated) CNY 9.13" — you must see both what this run spent and what it would have spent.

Degradation is not failure: three degradable dimensions

The third route is degradation. Its difference from failure is clear: failure means getting nothing, degradation means getting something lesser that is still usable. A video call dropping resolution on a weak signal rather than hanging up is the same idea.

This line has three degradable dimensions, ordered from least to most noticeable to the audience:

  1. Resolution: 768P down to 512P. Watching vertical short drama on a phone, most viewers will not notice.
  2. Duration: six seconds per shot down to four. The pace quickens, and the story survives.
  3. Shot count: five shots down to three. This one already alters the narrative and is the last resort.

The correct shape of degradation is not "run out of money halfway and cut the remaining shots" but compute before starting, degrade if it does not fit, then run: the former leaves half a wasted episode, the latter produces one complete episode that is merely lesser.

degrade.js
// Three degradable dimensions, ordered from least to most noticeable to the audience
const STEPS = [
  { label: 'resolution 768P -> 512P', apply: (p) => ({ ...p, resolution: '512P' }) },
  { label: 'shot length 6s -> 4s', apply: (p) => ({ ...p, maxShotSeconds: 4 }) },
  { label: 'shot count: keep the first 2', apply: (p) => ({ ...p, maxShots: 2 }) },
]
 
// project is a pure function: given a plan it computes projected spend, sending no requests
function degradeToFit(plan, project, limitCny) {
  let cur = plan
  const applied = []
  const skipped = []
  let projected = project(cur)
  for (const step of STEPS) {
    if (projected <= limitCny) break
    const next = step.apply(cur)
    if (project(next) >= projected) {
      // This step saves nothing on the books, so degrading only loses quality
      skipped.push(step.label)
      continue
    }
    cur = next
    applied.push(step.label)
    projected = project(cur)
  }
  return { plan: cur, applied, skipped, projected, fits: projected <= limitCny }
}

Note the middle "skip if it saves nothing" logic. That is not defensive code, it is a genuine criterion: 512P costs the same as 768P in our rate table, so the resolution step is skipped automatically — every degradation step must be validated against the books, and skipped if it fails validation. The log prints adopted and skipped steps separately with reasons.

The budget circuit breaker: a soft limit warns, a hard limit stops

The first three routes save money; this last one is the backstop — no amount of saving stops one runaway retry storm from sending the bill to the moon.

A circuit breaker's core semantics fit in one sentence: decide before spending, not tally afterwards. Written as an after-the-fact tally, the best you get is "this run overspent by 40%" — the money is gone, and that log line does nothing but make you unhappy. The correct shape is reservation: before calling a paid API, deduct the projected amount from the budget; if the deduction succeeds, send the request, and if not, raise.

budget.js
class BudgetExceededError extends Error {}
 
class Budget {
  #spent = 0
  #softFired = false
  constructor({ softCny, hardCny, onSoft }) {
    Object.assign(this, { softCny, hardCny, onSoft })
  }
 
  // Call before spending. If the deduction fails, raise; the caller stops safely.
  reserve(stage, amountCny) {
    if (this.#spent + amountCny > this.hardCny) {
      throw new BudgetExceededError(
        `hard limit tripped: spent ${this.#spent.toFixed(4)}, ${stage} needs ${amountCny.toFixed(4)}`
      )
    }
    this.#spent += amountCny
    if (!this.#softFired && this.#spent > this.softCny) {
      this.#softFired = true
      this.onSoft?.(this.#spent) // warn once, do not flood
    }
  }
 
  // A failed or safety-blocked video is not billed, so the reservation is refunded
  refund(amountCny) {
    this.#spent = Math.max(0, this.#spent - amountCny)
  }
}

The two limits have different jobs. The soft limit warns and changes nothing: one alert on crossing is enough, so that you can decide while you still have room — keep going, or drop a tier by hand. The hard limit stops, and must really stop.

And inside "stops" hides this chapter's most important judgment: what counts as stopping safely?

Not process.exit(1). That discards the ledger and the progress file, so the next run starts from scratch and spends the already-spent money again — turning the breaker into an amplifier of waste. Safe stopping has three parts:

  1. Finished artifacts stay on disk, none deleted;
  2. The ledger is written: how much was spent, where it stopped, what is still missing, all persisted;
  3. The cache is written too, so a later run with a higher budget need not redo the finished parts.

When the lab reaches this point the log looks like this:

TextText
(5) Squeeze the hard limit to CNY 3, clear the cache, run the final tier (INJECT=overbudget)
  STOP hard limit tripped: spent CNY 0.1250, clips needs CNY 3.0000, over the CNY 3.0000 hard limit; this run ends here
  5 finished artifacts remain on disk and the cache is written: a later run with a higher budget need not redo them.

Both the breaker and the safety-block scenario are fault injections, off by default and enabled through the INJECT environment variable — the one switch this course uses everywhere it needs a deliberate failure, taking comma-separated scenario names. D12 supports two: overbudget trips the hard limit, and blocked makes one shot's first video generation hit content moderation.

One more easily missed rule: a failed or safety-blocked video is not billed. So the breaker's reservation must be refunded, or one moderation block wastes three yuan of headroom that never appears as spend on the bill. In the lab that is the refund method plus the ledger's billed: false record, with a dedicated dashboard line for "failed or blocked, not billed."

The cost dashboard: see at a glance what made this run expensive

A dashboard is not a report. Reports are for month end; a dashboard is for this run, and answers one question: where did the money go this time, and is it more or less than last time?

It presents two dimensions: by stage (assets / frames / clips / voice) and by provider and type. The first tells you which part of the flow to optimize; the second tells you which vendor to negotiate with, or which class of call to move elsewhere.

A finished dashboard looks like this:

TextText
-- Cost dashboard: final tier (first run) --
  by stage
    clips    CNY  9.0000   98.5%  ########################  4 calls (0 cache hits)
    frames   CNY  0.0750    0.8%  ........................  3 calls (0 cache hits)
    assets   CNY  0.0500    0.5%  ........................  2 calls (0 cache hits)
    voice    CNY  0.0096    0.1%  ........................  3 calls (0 cache hits)
  total (estimated) CNY 9.1346
  failed or blocked, not billed (estimated) CNY 3.0000: failed or safety-blocked video is not billed

Three design points: every row carries call count and cache hits, because "how much was spent" and "how many calls" are different things — a failed retry raises the count without raising the amount; percentages and a bar together, because people read bars far faster than numbers; and two fixed disclaimer lines at the bottom saying the amounts are estimates and cannot be reconciled against an invoice, naming which tiers are derived and which are officially published.

One last line: a dashboard exists to make a decision, not to be archived. If reading one leaves you unsure what to change next, it was wasted. On this line the dashboard always points at the same conclusion — ninety percent of the money is in video, and the next step is always how to call the video API one fewer time.

Source Reading

Hands-On Lab

🧪 D12 lab: per-stage tiered model routing and a cost dashboard with a budget breaker

Code location: labs/ai-drama-pipeline/day-12-cost-router

Acceptance criteria:

  1. Six stages complete, the draft tier and the final tier each print a cost dashboard, and the clips row exceeds 95% of the total on both.
  2. Re-running unchanged prints "cache hits 11 / 11" with new spend CNY 0.0000.
  3. Re-running after changing one shot's visual description prints "cache hits 9 / 11," with new spend covering only that shot's first frame and video.
  4. Running again with INJECT=overbudget produces a "STOP hard limit tripped" line followed by "5 finished artifacts remain on disk."
  5. With a CNY 5 budget, the adopted and skipped degradation steps print first, the degraded plan then runs to completion, and the total lands under CNY 5.

Acceptance takes two commands: MOCK=1 pnpm start for the happy path, and INJECT=overbudget,blocked MOCK=1 pnpm start to inject both the breaker and the safety block. Confirm ffmpeg is installed first (a missing one gives a readable error at startup), and remember every amount on the dashboard is an estimate — offline, the provider's returned cost is always 0. If you get stuck, look at the "cache hits x / y" line first; it locates about ninety percent of this lab's problems.

  1. Run the starter unmodified first, read all six stages' output end to end, and note the obviously wrong symptoms (an all-zero dashboard, a cache that never hits, a breaker that never trips, degradation that never degrades).
  2. Add the projection function and the scoring rules: make project compute an episode's cost without sending any request, then check that stage 6 starts printing degradation steps.
  3. Implement cache hits, re-run stages 3 and 4, and confirm hits go from 0 / 11 to 11 / 11 and 9 / 11 with new spend dropping accordingly.
  4. Change the budget to reserve in advance, run stage 5 with INJECT=overbudget, and see both the "STOP hard limit tripped" and "5 finished artifacts remain on disk" lines appear.
  5. Add INJECT=blocked, implement the refund and the not-billed rule for the moderation-blocked call, and confirm the "failed or blocked, not billed" line appears at the bottom of the dashboard.

Interview Questions

Today's 3 questions are in the question bank below, focused on cost-structure analysis, the criteria for tiered routing, and the safe-stop semantics of breaking and degrading. Read the analysis before the key points — practicing the derivation beats memorizing them. The cn / global labels let you pick by target market.

Checklist and Tomorrow

  • I can break cost down per stage, and point out where most of the money goes and why
  • I can configure a model-routing policy per stage that makes an explainable trade-off between quality and cost
  • I can implement a budget circuit breaker that safely stops a run when it goes over budget instead of burning through it all
  • I can state the three criteria for a safe stop, and explain why exiting the process outright is wrong
  • I can name the three degradation dimensions and why each step must be validated against the books first
  • All 5 lab acceptance criteria pass
  • I can answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D13) we publish: export one episode in three platforms' specs, have the model batch-generate titles and cover copy then score and pick the top three, and feed playback data back to guide the next episode. Why settle the accounts before distributing? Because distribution gives you an incentive to produce more variants, and producing variants is exactly where things run away — install the brakes before touching the accelerator.

Interview questions

  • How do you break down the cost of a content-generation pipeline, and which stage would you optimize first?一条内容生成流水线的成本要怎么拆?拆完你会先优化哪一环,为什么?
    Common in ChinaCommon overseasBasic#cost-analysis#observability

    How to reason about it · think before answering

    1. This question checks whether you have actually read a bill. Answering with generic advice like use more caching signals you never ran this in production; naming the breakdown dimensions and rough ratios signals you did.
    2. Establish the dimensions first: by stage (script, image, video, speech), by billing unit (per second, per item, per character, per token), and by billable status (succeeded, cache hit, failed and not charged). Drop any one of them and a whole class of spend becomes invisible.
    3. Then give orders of magnitude. Video is billed per second, so a dozen seconds already costs a few yuan, while images are cents per item, speech is fractions of a cent per character, and text is lower still. Video typically dominates at over ninety percent.
    4. So the priority is driven by what is expensive, not by what is easy to change. Attack video first, cheapest lever to most expensive: caching and idempotency, tiered routing with a cheap draft tier, degradation across resolution, duration and shot count, and only then vendor negotiation.
    5. Add a credibility note: never put an unverified unit price in the table. Mark derived prices as estimates and leave unpublished ones blank while still counting usage. Reporting an estimate as an official price is how these projects lose trust.
    6. Expect the follow-up: how do you prove the optimization worked? Run the same input twice and compare the per-stage panel, not the monthly invoice, which mixes in traffic you did not cause.

    分析过程 · 先想清楚再作答

    1. 这题在考你有没有真的看过账单。凭感觉答「多用缓存、少调模型」的人一听就没做过;能说出「按什么维度拆、拆出来大概什么比例」的才是。
    2. 拆的维度要先立住:按环节(脚本、图像、视频、语音)、按计价单位(按秒、按张、按字符、按 token)、按是否计费(成功、命中缓存、失败未扣费)。三个维度缺一个,报表就会有一类花费永远看不见。
    3. 然后给数量级。多媒体生成这类流水线里视频按秒计价,一集十几秒就是几块钱;图像按张几分钱、语音按字符几厘钱、文本更低。结论是视频通常占九成以上,其余全是零头。
    4. 所以优化顺序不是「哪一环最容易优化」,而是「哪一环最贵」。先优化视频,手段按代价从低到高排:缓存与幂等(不重复调)、分档路由(草稿档用便宜规格)、降级(清晰度、时长、镜头数)、最后才是换厂商谈价。
    5. 补一句可信度:不确定的单价不要写进表。官方只给资源包价的档位要标明是折算值,官方没公开的档位就留空只统计用量——把估算值当官方价报上去,是这类项目最常见的翻车点。
    6. 可预期的追问是「那怎么证明优化生效了」。答案是同一份输入跑两遍,对照面板上按环节的金额与调用次数,而不是看月账单——月账单里混着别人的流量,归因不到你这次改动。

    Key points

    • Break it down three ways: by stage, by billing unit, and by whether the call was actually charged
    • Lead with the ratio: video is billed per second and usually exceeds ninety percent of per-episode cost
    • Optimize expensive first: caching and idempotency, tiered routing, degradation, vendor negotiation last
    • Leave unknown unit prices blank while still counting usage, and label derived prices as estimates
    • Validate by running the same input twice and diffing the per-stage panel, not the monthly invoice

    答题要点

    • 按三个维度拆:环节、计价单位、是否真的计费(成功 / 缓存命中 / 失败未扣费)
    • 先给比例再给结论:视频按秒计价,通常占单集成本九成以上,其余是零头
    • 优化顺序由贵到便宜:缓存与幂等、分档路由、降级、最后才谈价换厂商
    • 拿不到的单价宁可留空只统计用量,折算出来的要标明是折算值
    • 验证靠同一份输入跑两遍对照面板,不看混杂的月账单
  • When should you degrade instead of retry, and which dimensions can you degrade first?什么情况下该降级而不是重试?如果决定降级,你有哪些维度可以降,怎么排先后?
    Common in ChinaCommon overseasIntermediate#degradation#retry-strategy

    How to reason about it · think before answering

    1. The pivot is the word instead. This tests whether you separate two failure classes: retry addresses bad luck this time, degradation addresses cannot finish under this configuration. Answering retry three times then degrade misses the point.
    2. Give a reusable rule: retry fixes transient, configuration-independent problems such as rate limits, timeouts and server errors. Degradation fixes persistent, constraint-driven ones such as running out of budget, quota or time. Retrying the second class just burns resources faster.
    3. Name the class most people get wrong: a content-safety block should be neither retried nor degraded, it needs a changed input. Conflating the three is the biggest scoring mistake here.
    4. Order degradation dimensions by how noticeable they are, least to most: resolution, duration, then count of items. Touch the one that changes the content itself only as a last resort.
    5. Add an engineering rule: validate every degradation step against the cost model. If a step saves nothing on your rate card, degrading quality buys you nothing and should be skipped.
    6. Expect the follow-up: when do you decide? Project the cost with a pure function before the run starts and degrade up front. Cutting mid-run leaves a half-finished artifact and wastes everything already spent.

    分析过程 · 先想清楚再作答

    1. 题眼在「而不是」三个字。它考的是你能不能区分两类失败:重试针对的是「这次不巧」,降级针对的是「按当前配置根本跑不完」。答成「先重试三次再降级」就落进了套路。
    2. 给一条可复用的判据:重试解决的是**瞬时**且**与配置无关**的问题(限流、超时、服务端 5xx),降级解决的是**持续**且**由约束导致**的问题(预算不够、配额见底、截止时间快到了)。前者重试有效,后者重试只会把资源烧得更快。
    3. 顺带点出最容易被答错的一类:内容安全拦截既不该重试也不该降级,它要改输入。把三类混在一起是这题最大的失分点。
    4. 降级的维度要按「用户察觉难度」排,从低到高:清晰度、时长、数量(镜头数 / 条数)。先降察觉不到的,最后才动会影响内容本身的那一档。
    5. 还有一条工程判据:每一步降级都要拿成本模型验证一遍。如果某一档在你的单价表上省不出钱(比如更低的清晰度和当前档同价),那这一步降了只有损失,应该直接跳过。
    6. 可预期的追问是「降级要在什么时候决定」。答案是开跑之前先用纯函数预估一遍,算不过就降完再跑——跑到一半再砍,会留下半成品,前面花的钱全打水漂。

    Key points

    • Retry transient configuration-independent failures; degrade when the constraint makes completion impossible
    • Content-safety blocks are a third class: change the input rather than retrying or degrading
    • Order degradation by noticeability: resolution, duration, item count, content last
    • Validate each degradation step against the rate card and skip steps that save nothing
    • Decide before the run starts; cutting mid-run leaves a half-finished artifact and wastes prior spend

    答题要点

    • 重试针对瞬时且与配置无关的失败,降级针对持续且由约束导致的不可完成
    • 内容安全拦截是第三类:既不重试也不降级,要改输入
    • 降级维度按察觉难度排:清晰度、时长、数量,最后才动内容本身
    • 每一步降级都要拿成本模型验证,省不出钱的那一步直接跳过
    • 降级要在开跑前决定,跑到一半再砍会留下半成品且前面的钱白花
  • How would you design a budget circuit breaker for a pipeline that calls paid APIs, and what makes the stop safe?给一条会调用付费接口的流水线加预算熔断,你会怎么设计?做到什么程度才算安全停机?
    Common in ChinaCommon overseasDeep dive#budget-control#circuit-breaker

    How to reason about it · think before answering

    1. The discriminator is the word safe. Most candidates can say stop when over budget; what the interviewer wants is the state the system is left in afterwards.
    2. Rule one: the check happens before you spend. Use reservation-style accounting, projecting each paid call with a pure function and deducting it from the budget before issuing the request. After-the-fact accounting only tells you that the money is already gone.
    3. Rule two: the two thresholds do different jobs. A soft limit warns once so a human can decide whether to continue or downgrade; a hard limit must actually stop. Setting both to the same value means you have no soft limit.
    4. Rule three defines a safe stop, and all three parts are required: keep every finished artifact, persist the ledger and the point of interruption, and write the cache. Miss any one and the next run with a higher budget pays again for work already paid for, turning the breaker into a waste amplifier. Calling exit is therefore wrong.
    5. Rule four is refunds: if the vendor does not charge for failed or safety-blocked calls, the reserved amount must be released, otherwise you overstate the bill and silently consume headroom. Mark those ledger rows separately and show them as their own line on the panel.
    6. Two follow-ups to expect. Under concurrency the reservation must be atomic, so a shared counter needs a single owner or an atomic operation or you will oversell. And the limits themselves should be derived from historical usage through the same projection function, not guessed.

    分析过程 · 先想清楚再作答

    1. 这题的区分度全在「安全」两个字。多数人能答出「超预算就停」,但停完之后系统处在什么状态,才是面试官真正想听的。
    2. 先立第一条:判断必须发生在花钱之前。做法是预留式记账——每次调用付费接口前用一个纯函数预估这笔花费,从预算里扣,扣得动才发请求。事后统计只能告诉你已经超了,那时钱已经出去了。
    3. 第二条是两级上限的分工:软上限只提醒且只提醒一次,作用是让人在还有余地时决定继续还是降档;硬上限必须真的停。把两者做成同一个阈值,等于没有软上限。
    4. 第三条才是「安全停机」的定义,三个都要满足:已完成的产物一个不删、账本与停在哪一步落盘、缓存写入。少了任何一条,下一次带更高预算重跑就要把已经花掉的钱再花一遍——熔断反而成了浪费的放大器。所以直接退出进程是错的。
    5. 第四条是退款口径:失败或被内容安全拦下的调用如果厂商不计费,预扣的额度必须退回来,否则你会一边高估账单一边白占预算。台账上这类记录要单独标出来,面板上单独一行。
    6. 可预期的追问有两个。一是「并发下怎么保证不超」——预留必须是原子的,多个 worker 共享一个计数器时要走单点或原子操作,否则会超卖。二是「上限设多少」——用同一个预估函数按历史用量反推,而不是拍脑袋。

    Key points

    • Reserve before you spend: project the cost, deduct it, and skip the call if it does not fit
    • The soft limit warns once for a human decision; the hard limit must actually stop, with different thresholds
    • A safe stop keeps artifacts, persists the ledger and resume point, and writes the cache; never just exit
    • Release reservations for calls the vendor does not charge for, and show them as a separate ledger line
    • Make reservations atomic under concurrency and derive limits from historical usage via the same projector

    答题要点

    • 预留式记账:调用付费接口前先预估并扣减,扣不动就不发请求
    • 软上限只提醒一次供人决策,硬上限必须真的停,两者阈值必须不同
    • 安全停机三条:产物保留、账本与断点落盘、缓存写入,绝不直接退出进程
    • 厂商不计费的失败调用要退回预扣额度,并在台账与面板上单独标出
    • 并发下预留必须原子;上限用同一个预估函数按历史用量反推

Comments