The Script Agent: Turning a Single Sentence Into Structured Data — Character Cards, Scenes, and Shots
Get the model to produce script data a program can consume directly instead of a piece of prose, add a reviewer role that polishes it against judgeable criteria, and distill settings shared across episodes into a bible file.
Today's Goals
- Constrain model output with a schema so the script lands directly as program-readable character cards, scenes, and shot data
- Add a reviewer role to the script agent that improves the draft in a loop against judgeable criteria
- Extract character and world-bible settings into a separate store, so every later episode stays on track
On yesterday's task graph, every node other than the script feeds on fields of the shot data. Today we enter the writers' room and genuinely produce that data — come back and tick off the three goals.
Plain-Language Walkthrough
The line between prose and data: why a script must become structured data first
Ask a model to write a short drama script and you get a rather presentable stretch of prose: scene description, dialogue, emotional cues, all in one flow. Comfortable for a person to read and entirely unusable by a program.
Think about how a crew works. The writer delivers a script, and no department works from the script directly. The script first becomes a shot list: which shot number, what shot size, how long, who is present, what is in frame, how the camera moves, which line is spoken. The art department reads what is in frame, the camera department how the camera moves, sound which line is spoken, and editing how long. One table split across six departments, each reading only its own columns.
Our pipeline is the same thing with departments replaced by six nodes. The image node reads visual, the video node reads camera and durationSec, the voiceover node reads dialogue and the character's voiceId, and the timeline node reads durationSec. All that information exists in a stretch of prose, and mixed together it may as well not — you cannot reliably extract a slow push-in, six seconds, and half a line from "he walks slowly to the window, his voice dropping."
Somebody will think a regex, or another model call parsing the prose, would do. It would, at two costs: one generation and one parse, with the parse's failure rate fluctuating with script style so you never know whether the next episode parses into an empty array. Better to have the model produce the data structure from the outset, merging writing and formatting into one step.
So today's dividing line is drawn like this:
| Given to the model | Given to the program |
|---|---|
| Inventing plot, characters, lines, imagery | Specifying what the output looks like and checking whether it qualifies |
| Judging whether the hook is strong and the motivation holds | Judging whether durations, character counts, shot numbers, and references are valid |
That line recurs in every later section. Anything a program can judge should never be given to the model.
Which fields a shot card should have
More fields is not better, and there is one criterion: does a downstream node genuinely read it? This course fixes seven fields on a shot card, with nothing added or removed across fourteen days:
type Shot = {
id: string // the shot number, shaped s01, unique in the episode — also part of the artifact directory name and the cache key
sceneId: string // a scene id pointing at one entry in this episode's scene table
shotSize: 'wide' | 'medium' | 'close' // three sizes only; do not introduce a fourth
durationSec: number // the planned duration, possibly rewritten by the real voiceover duration on day 5
characters: string[] // character ids present, used on day 3 to decide which reference sheet to pass
visual: string // the visual description, entering the image and video prompts
camera: string // the camera movement, entering the video prompt
dialogue: { characterId: string; text: string }[] // the lines, entering voiceover
}from typing import Literal, TypedDict
class Line(TypedDict):
characterId: str
text: str
class Shot(TypedDict):
id: str # the shot number, shaped s01, unique in the episode
sceneId: str # a scene id pointing at one entry in this episode's scene table
shotSize: Literal["wide", "medium", "close"] # three sizes only; do not introduce a fourth
durationSec: int # the planned duration, possibly rewritten by the real voiceover duration on day 5
characters: list[str] # character ids present, deciding which reference sheet to pass on day 3
visual: str # the visual description, entering the image and video prompts
camera: str # the camera movement, entering the video prompt
dialogue: list[Line] # the lines, entering voiceoverThree conventions deserve their own mention.
Three shot sizes only. Film distinguishes seven or eight, and our downstream only splices it into a prompt, where a model cannot tell finer distinctions apart, while extra branches burden validation and later statistics. An enum's size should be decided by downstream discriminating power, not by industry habit.
Duration fields always end in Sec or Ms. Day 5's voiceover duration is milliseconds and day 6's timeline is milliseconds while a shot's planned duration is seconds. A bare duration mixed among them will sooner or later be multiplied wrongly by a thousand — a very quiet bug making the finished cut a thousandth of its correct length.
A shot number is s01 rather than 1. It becomes a directory name, a filename prefix, and part of day 8's cache key. Zero-padded, sorting matches visual order, which you will thank yourself for past ten shots.
The engineering cost is that once fixed, fields are hard to change. Every key written into Shot today has code reading it across the next twelve days; and by day 10's review console there will also be a front-end page rendering it. So take this step slowly and ask, key by key, which downstream reads it.
Using a schema to make the model be quiet: three roads and how each crashes
Getting a model to output valid JSON has broadly three roads in the industry.
One: prompt constraint plus local validation. Write "output JSON only, no explanation" into the system prompt, extract the JSON from the reply, validate it yourself, and feed the problems back for another attempt when it fails. It crashes when the model does not obey — adding pleasantries, adding explanation, or wrapping the JSON in fences.
Two: a vendor's JSON mode or structured output parameter. Some vendors offer a parameter forcing JSON or constraining decoding to a schema, with the server guaranteeing parseability. It crashes because support and field names differ between vendors; whether it exists and what it is called is per the official documentation, and using it binds this code to that vendor.
Three: borrowing tool calling's parameter schema. Define "submit the script" as a tool whose parameter schema is your data structure and have the model call it. It crashes because some implementations treat the schema as a strong suggestion and may still produce invalid arguments.
This course takes the first, for a reason continuous with yesterday's provider abstraction: it depends on no vendor's extension field, and switching vendors needs not one line changed. The cost is writing two things yourself — extracting the JSON and validating it.
The extraction must handle the two most common cases, pleasantries around it and fences wrapping it; and when validation fails, feeding the pathed problems back verbatim and asking it to fix only those helps far more than switching to a larger model:
// Find the first brace or bracket, then the last matching closer, and parse only what lies between.
// JSON.parse over the whole reply blows up three times in ten against a real model.
function extractJson(text: string): unknown {
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(text)
const body = fenced ? fenced[1] : text
const start = body.search(/[[{]/)
const close = body[start] === '{' ? '}' : ']'
return JSON.parse(body.slice(start, body.lastIndexOf(close) + 1))
}
// On a validation failure, feed back "path plus reason" and retry once; failing twice is a real failure.
async function ask(text, system, user, parse) {
for (let attempt = 1; attempt <= 2; attempt++) {
const raw = await text.complete({ system, user })
const issues = []
const value = parse(extractJson(raw.text), issues)
if (issues.length === 0) return value
if (attempt === 2) throw new Error('schema validation failed twice')
user += `\n\nThe last version had these field problems. Fix only these and change nothing else:\n${issues.map((i) => `- ${i.path}: ${i.message}`).join('\n')}`
}
}import json
import re
# Find the first brace or bracket, then the last matching closer, and parse only what lies between.
# json.loads over the whole reply blows up three times in ten against a real model.
def extract_json(text: str):
fenced = re.search(r"```(?:json)?\s*(.*?)```", text, re.S)
body = fenced.group(1) if fenced else text
start = min((i for i in (body.find("["), body.find("{")) if i >= 0), default=-1)
close = "}" if body[start] == "{" else "]"
return json.loads(body[start : body.rfind(close) + 1])
# On a validation failure, feed back "path plus reason" and retry once; failing twice is a real failure.
async def ask(text, system, user, parse):
for attempt in (1, 2):
raw = await text.complete(system=system, user=user)
issues: list[dict] = []
value = parse(extract_json(raw["text"]), issues)
if not issues:
return value
if attempt == 2:
raise ValueError("schema validation failed twice")
lines = "\n".join(f"- {i['path']}: {i['message']}" for i in issues)
user += f"\n\nThe last version had these field problems. Fix only these:\n{lines}"A writer and a reviewer: give the second role a scorecard
Generating one version at a time and starting over when dissatisfied is the most expensive way to revise, because each pass pays again for the ninety percent already correct. A crew does not work that way: the writer delivers, the producer and director go through a list item by item, and the writer changes only what was flagged.
Bringing that in gives a writer and a reviewer. But there is a key engineering judgment here, and it is today's most portable sentence:
The reviewer is not a model but two layers: code judging hard faults and a model judging soft faults.
Hard faults are program-decidable: does each shot have a camera, is the duration between 3 and 8 seconds, does any single line exceed 25 characters, does everyone in the character cards appear, is the episode's total between 45 and 90 seconds. Those five take a few lines to check, cost nothing, miss nothing, and give stable verdicts. Soft faults are what a program cannot judge: is there a hook in the first three seconds, does the motivation hold, does the ending leave people wanting — those belong to a model.
// Five hard faults, each matching a real constraint on a later day rather than padding
function checkHard(ep, characters) {
const issues = []
for (const s of ep.shots) {
if (!s.camera.trim()) issues.push(`[camera] ${s.id} has no camera movement; day 4 has nothing for the prompt`)
if (s.durationSec < 3 || s.durationSec > 8) issues.push(`[duration] ${s.id} duration out of range`)
for (const d of s.dialogue) {
if (d.text.length > 25) issues.push(`[dialogue] ${s.id} line is ${d.text.length} characters, over 25`)
}
}
return issues
}
// Seventy percent hard, thirty percent soft: what a machine can judge must not be outweighed by a model's praise
const hardScore = Math.max(0, 100 - hard.length * 8)
const score = Math.round(hardScore * 0.7 + softScore * 0.3)# Five hard faults, each matching a real constraint on a later day rather than padding
def check_hard(ep, characters):
issues = []
for s in ep["shots"]:
if not s["camera"].strip():
issues.append(f"[camera] {s['id']} has no camera movement; day 4 has nothing for the prompt")
if not 3 <= s["durationSec"] <= 8:
issues.append(f"[duration] {s['id']} duration out of range")
for d in s["dialogue"]:
if len(d["text"]) > 25:
issues.append(f"[dialogue] {s['id']} line is {len(d['text'])} characters, over 25")
return issues
# Seventy percent hard, thirty percent soft: what a machine can judge must not be outweighed by praise
hard_score = max(0, len(hard) * -8 + 100)
score = round(hard_score * 0.7 + soft_score * 0.3)Note the bracketed category code at the start of each hard fault. It is not for a human but for the writer: given [camera], the writer knows what class to fix, and that class disappears next round. Categorized feedback makes revision converge; a bare "not good enough" only makes the model rewrite everything and break what it already fixed.
The lab's offline scores rise 69, 86, 91 across three rounds, with hard fault counts genuinely falling each round — because those five rules are computed locally rather than invented. Run it with a different sentence and the curve's shape holds while the content changes entirely, which is what the business logic genuinely running looks like.
The world bible: cross-episode consistency appears for the first time
So far we made one episode. But this course ends at a five-episode season, and the problem arrives immediately: in episode two, is the lead still the same person?
Short drama audiences are extremely sensitive to that. A changed name, a changed occupation, a personality that does not match, and the comments start. And a model has no memory — on your second call it knows nothing of episode one.
The remedy is plain: extract what does not change across episodes into its own store, read before generating each episode. That is the world bible, landed at script/world.json, with character cards at script/characters.json:
world.json: the title, a one-line premise, the tone, the setting, and a few hard rules (only the lead knows this, or every episode must end on an unanswered question).characters.json: each character's id, name, appearance, personality, and voice id.
Why are appearance and voice here too? Because they are not merely settings but downstream input parameters: the appearance description goes verbatim into day 3's image prompt, and the voice id goes verbatim into day 5's speech endpoint. Putting them in the same file as the name solves consistency in one file rather than in three places written three times.
Day 14 genuinely puts that bible to work: five episodes generated in sequence, each carrying the same world.json and characters.json, so characters and style do not drift across episodes. Today you only need it stored separately with field names matching exactly what downstream will use.
When to stop: three exits, none omissible
A generate-and-review loop carries a fatal temptation: if every round improves it, run a few more. That is the fastest way to burn money.
A production-ready loop needs three exits:
Exit one, stop on passing. Wrap the moment the score reaches the threshold. Do not chase a perfect score — in this lab the loop stops at 91 with one [dialogue] hard fault still present. That is deliberate: a threshold means good enough, not perfect, and chasing those last few points costs far more than it returns.
Exit two, stop at the round limit. Write the scoring criteria strictly enough and the model may never reach the threshold, and a loop without a limit runs forever. On hitting the limit do two things: deliver the highest-scoring draft (not the last, since review fluctuates) and print explicitly that it did not pass and hit the limit, so the user knows a human is needed.
Exit three, a human decides. Today leaves only the interface, with day 10's review console implementing it. The criterion: when the remaining problems are all soft faults, hand it to a person. A loop can fix hard faults, and soft fault scores come from the model itself, so a model evaluating and revising itself only circles in place.
The engineering cost is that all three exits need parameters, and parameters need tuning. Set the threshold high and rounds run out; set it low and the draft is unusable; set the round limit high and money burns; set it low and it is always one push short. The lab makes them the environment variables PASS_SCORE and MAX_ROUNDS, and you should genuinely change them and watch how the score curve moves — worth more than reading this section ten times.
Source Reading
Hands-On Lab
Today needs no ffmpeg and calls no image, video, or speech endpoint, using only the text category of the four providers; MOCK=1 throughout is enough. starter/ runs as-is with the score unchanged across four rounds, the limit exhausted, and a 12-second shot and a 40-character line surviving into the final draft — the four numbered exercises match those symptoms.
- Run the solution once, read the three rounds' scores and each round's hard fault list, then open
drafts/and diff adjacent rounds to see exactly what changed each round. - Back in the starter, do exercise 1: add the other four hard faults to the scorecard, and round one's hard fault count should rise from 3 to 5.
- Do exercise 2: change the final score to seventy percent hard and thirty percent soft, at which point the score rises as hard faults fall.
- Do exercise 3: add the stop-on-passing and deliver-the-best-draft-at-the-limit exits to the loop, and confirm it stops at round 3 rather than round 4.
- Do exercise 4: add the referential integrity check, verify with
INJECT=badrefthat a bad draft is caught, then run three premises of your own and confirm no field is missing and the content changes.
Interview Questions
Today's 3 questions are in the question bank below, weighted toward structured output's implementation routes and failure modes, a generate-and-review loop's convergence conditions, and where cross-episode consistency is stored. 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
- Constrain model output with a schema so the script lands directly as program-readable character cards, scenes, and shot data
- Add a reviewer role to the script agent that improves the draft in a loop against judgeable criteria
- Extract character and world-bible settings into a separate store, so every later episode stays on track
- Name structured output's three implementation routes, and why this course took the first
- Explain why validation must have both a type layer and a referential integrity layer
- All 5 acceptance criteria of the lab pass
- Answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D3) we visit the art department: wiring up the image generation endpoint and producing character reference sheets and scene images in batch from today's shot data. Why that order? Because the image node feeds on exactly two fields settled today — the character cards' appearance and the shots' visual. And the moment images appear, a problem that is only latent today explodes on the spot: a generation model has no memory, and the lead in the second image quite likely has a different face. D3 explains that cause and what each of three locking techniques — reference images, seeds, and prompt templates — can and cannot hold in place.
Interview questions
How do you get a model to emit valid structured data reliably, and what do you do when schema validation fails?怎么让模型稳定输出合法的结构化数据?schema 校验失败时你会怎么处理?
Common in ChinaCommon overseasBasic#structured-output#schema-validationHow to reason about it · think before answering
- The real question is the second half. Answering only use JSON mode signals you have never run this in production, because all the work happens after validation fails.
- Lay out three paths: prompt constraints plus local validation; a vendor's JSON mode or structured-output parameter; or defining the data structure as a tool's parameter schema. Vendor support and field names differ, so the latter two bind that code to one vendor.
- State the selection rule: cross-vendor or offline-capable means path one, paying with your own JSON extraction and validator; single-vendor and success-rate-driven means use their structured output. Extraction must handle code fences and surrounding chatter — parsing the whole reply directly breaks often.
- Handle failure as a ladder, not just a retry: feed the path-annotated issues back and ask it to fix only those (more effective than upgrading the model); then degrade to a minimal required-fields-only structure; then fail the round and persist the artifact for a human — never swallow the error and return an empty array.
- High-signal point: validate in two layers. Type and range checks catch malformed data but not wrong references — a nonexistent scene id or a duplicate shot number passes typing and explodes downstream. Referential integrity needs its own pass.
- Likely follow-up: how many retries? Two. The first covers a disobedient model; if it still fails with concrete issues in hand, the prompt or the schema itself is wrong and more retries just buy the same error.
分析过程 · 先想清楚再作答
- 这题的题眼在后半句。前半句答「用 JSON 模式」就结束的人,等于说自己没在生产里跑过——真正的活儿全在校验失败之后。
- 先把三条路摆开:提示词约束加本地校验;厂商提供的 JSON 模式或结构化输出参数;把数据结构定义成工具的参数 schema 让模型去调。各家对后两条的支持程度和字段名都不一样,选它就等于把这段代码绑在某一家上。
- 给出选择依据:要跨厂商、要能离线跑,就选第一条,代价是自己写抠 JSON 与校验;只服务一家且追求成功率,就用那一家的结构化输出。抠 JSON 这一步必须处理围栏与前后寒暄,直接解析整段回复在真实模型上很容易炸。
- 校验失败的处理是一条阶梯,别只答重试:把带路径的问题原样喂回去让它只修这些(比换更大的模型有效);仍不过就降级到只要必填字段的最小结构;再不过就整轮失败并留档,让人来看,而不是吞掉异常返回一个空数组。
- 还有一条区分度很高:校验要分两层。判类型与范围只能挡住格式错,挡不住写错对象——引用了不存在的场景 id、镜号重复,这类稿子能通过类型检查,然后在下游某一步才爆。引用完整性必须单独查一遍。
- 可预期的追问:重试几次合适?两次。第一次是模型没听话,第二次带着具体问题还改不对,说明是提示词或 schema 本身有问题,再重试只是花钱买同一个错误。
Key points
- Three paths: prompt plus local validation, vendor structured output, or tool parameter schema — the latter two bind you to a vendor
- JSON extraction must handle code fences and surrounding prose; never parse the whole reply directly
- Failure handling is a ladder: feed back path-annotated issues, degrade to a minimal structure, then fail the round and persist for a human
- Validate in two layers — types and ranges, then referential integrity and id uniqueness
- Cap retries at two; beyond that the prompt or schema is wrong, not luck
答题要点
- 三条路:提示词加本地校验、厂商结构化输出参数、工具参数 schema,后两条会绑定厂商
- 抠 JSON 要处理围栏与前后寒暄,不能直接解析整段回复
- 失败处理是阶梯:带路径的问题喂回去只修这些、降级到最小结构、整轮失败留档给人
- 校验分两层,类型与范围之外必须单独查引用完整性与 id 唯一性
- 重试上限两次,再不过说明是提示词或 schema 的问题,不是运气问题
In a generator-plus-reviewer loop, how do you define convergence so it does not burn budget indefinitely?生成加评审这种双角色循环,收敛条件该怎么定才不会一直烧钱?
Common in ChinaCommon overseasDeep dive#agent-loop#cost-controlHow to reason about it · think before answering
- This screens whether you have ever made such a loop actually terminate. Answering only set a max round count scores nothing — that prevents an infinite loop, it is not convergence design. The signal is naming three exits plus how the reviewer itself is built.
- Start with the reviewer: it should not be one model but two layers. Machine-checkable defects (missing fields, out-of-range numbers, length limits, invalid references) go to code; only the judgment calls go to the model. This decides score stability — a pure-model reviewer can swing by ten-plus points on the same draft, and then convergence is meaningless.
- Then the three exits: stop on threshold (the threshold means good enough, not perfect — chasing the last few points costs far more than it returns); stop at the round cap, handing back the highest-scoring draft rather than the last one, because review scores fluctuate; and escalate to a human once only judgment-call issues remain, since a model reviewing and revising itself just circles.
- Also cover the scoring weights: hard defects should dominate, say seventy percent, with the model's soft score at thirty. Otherwise one flattering model review outweighs five real field errors and the loop declares success on round one.
- Conclusion and cost: all three exits need parameters, and parameters need empirical tuning. Too high a threshold burns every round; too low ships an unusable draft. Plot the score curve before shipping and confirm it rises monotonically.
- Likely follow-up: how do you know it is improving rather than oscillating? Track the hard-defect count — it is deterministic, while the score jitters. If hard defects do not fall, the writer is not acting on feedback, and the fix is feedback granularity: tag each issue with a category so the model knows which class to repair.
分析过程 · 先想清楚再作答
- 这题在考「你有没有让这种循环真的停下来过」。只答「设一个最大轮数」拿不到分,那只是防死循环,不是收敛设计。区分度在于你能不能说出三个出口以及评审本身该怎么构造。
- 先拆评审:评审不该是一个模型,而是两层——能被程序判定的硬伤用代码查(字段缺失、数值越界、长度超限、引用不合法),程序判不了的软伤才交给模型。这一步决定了分数稳不稳定:全交给模型,同一份稿子两次评分能差十几分,循环就没有收敛可言。
- 再说三个出口:达标就停(阈值是「够用」不是「完美」,追最后几分成本远高于收益);到轮数上限就停,而且要交出历史最高分那一稿而不是最后一稿,因为评审有波动;剩下的问题全是软伤时转人工,因为让模型自己评自己改只会原地打转。
- 还要说计分方式:硬伤应该占大头(比如七成),软分占小头。否则模型一句好评就能盖过五条实打实的字段问题,循环会在第一轮就假装达标。
- 结论加代价:三个出口都需要参数,而参数必须实测调。阈值高了轮数用满,低了稿子不能看;上限大了烧钱,小了永远差一口气。上线前要把分数曲线画出来看它是不是单调上升。
- 可预期的追问:怎么知道循环真的在变好而不是在抖动?看硬伤条数,它是确定性的;分数会抖,硬伤条数不会。硬伤降不下去就说明写手根本没在按意见改,问题出在意见的粒度上——意见要带分类标签,模型才知道该改哪一类。
Key points
- Split the reviewer: code judges hard defects, the model judges only judgment calls — otherwise scores are unstable and nothing converges
- Three exits: stop on threshold, stop at the round cap returning the best draft, escalate to a human when only soft issues remain
- Weight hard defects heavily so a flattering model review cannot mask real field errors
- Tag each review issue with a category so the writer repairs one class at a time
- Measure convergence by hard-defect count, not score — the score jitters, the count does not
答题要点
- 评审分两层:硬伤用代码判,软伤才交给模型,否则分数不稳定、循环无从收敛
- 三个出口:达标就停、到轮数上限交历史最高分那一稿、只剩软伤时转人工
- 计分让硬伤占大头,避免模型一句好评盖过实打实的字段问题
- 评语必须带分类标签,写手才能只改那一类,改稿才是收敛的
- 观测收敛看硬伤条数而不是分数,分数会抖、硬伤条数是确定的
To keep character definitions consistent across many episodes, where do you store that state and how do you use it?多集内容要保持人物设定一致,你会把这份设定放在哪、怎么用?
Common in ChinaCommon overseasIntermediate#state-management#consistencyHow to reason about it · think before answering
- The crux is that models have no memory. Answering just concatenate previous episodes into the context invites a fatal follow-up: context grows linearly with episode count, so by episode five you pay repeatedly for four full episodes, and the model may still miss details.
- Break it down by separating what is invariant across episodes from what is recomputed each time. Invariant: the world, each character's appearance, personality, voice id, and a few hard rules. Recomputed: scenes and shots. Extract the invariant part into its own file and load it verbatim before generating each episode.
- Add the commonly missed point: the fields in that file are not only lore, they are downstream input parameters. Appearance text goes straight into image prompts, the voice id goes straight into the speech API. Keeping them beside the name means consistency is solved in one file rather than restated in three places.
- Choose the storage boundary by write frequency: the profile is written once and read many times, while the shot list is rewritten on every run. Mixing lifetimes in one file makes it impossible to rerun one episode without disturbing the others.
- Conclusion and cost: the profile itself can drift. Change a character's appearance mid-season and previously generated assets no longer match, so version the profile and include that version in the asset cache key — editing the profile then invalidates exactly the affected assets. That is only possible because it lives on its own.
- Likely follow-up: should you use a vector store? Usually not. Cross-episode canon is small, structured, and must be injected in full; retrieval risks dropping the one line that matters. Retrieval fits large corpora where only a few relevant items are needed.
分析过程 · 先想清楚再作答
- 这题的题眼是「模型没有记忆」。答成「把前一集的输出拼进上下文」的人会被追问到崩——上下文会随集数线性膨胀,第五集时你在为前四集的全文反复付费,而且模型仍然可能漏读。
- 怎么拆:先分辨哪些是「跨集不变」的,哪些是「每集重算」的。不变的是世界观、人物外貌、性格、音色与几条硬规则;每集重算的是场景与分镜。把不变的那部分抽成单独的档案文件,每一集生成前原样读进去。
- 接着说一个容易被忽略的点:档案里的字段不只是设定,还是**下游的输入参数**。外貌描述要原样进图像提示词,音色 id 要原样进语音接口。所以它们必须和名字放在同一份档案里,一致性问题才是在一个文件里解决的,而不是散在三处各写一遍。
- 存放位置的判据是写入频率:档案一次生成、多次读取,分镜每跑一次就重写。生命周期不同的数据放同一个文件,你就没法只重跑一集而不动其他集。按写入频率切分文件,是这类流水线最省事的一条习惯。
- 结论与代价:档案本身也会漂——中途改了人物外貌,之前生成的资产就对不上了。所以档案要有版本,且资产的缓存键要包含档案版本,改档案等于让相关资产失效。这条也是把它单独存放才做得到的。
- 可预期的追问:那要不要上向量库做检索?多数情况下不需要。跨集共享的设定是**有限的、结构化的、必须全量注入的**,检索反而可能漏掉关键一条。检索适合的是「素材库很大且只需要相关几条」的场景。
Key points
- Models are stateless; cross-episode consistency comes from an external profile, not from stuffing prior episodes into context
- Split by invariant versus recomputed: world and character profiles persist, scenes and shots are regenerated per episode
- Appearance text and voice id are downstream input parameters, so they belong beside the character's name
- Split files by write frequency — a read-mostly profile versus a rewritten shot list — or you cannot rerun one episode alone
- Version the profile and fold that version into the asset cache key so edits invalidate exactly the affected assets
答题要点
- 模型没有记忆,跨集一致性靠外部档案而不是把前几集拼进上下文
- 按「跨集不变」与「每集重算」切分:世界观与人物卡是档案,场景与分镜每集重来
- 档案里的外貌与音色 id 同时是下游的输入参数,所以必须和名字放在一起
- 按写入频率切分文件,档案读多写少,分镜每次重写,混在一起就没法只重跑一集
- 档案要有版本并进资产缓存键,改设定才能精确地让相关资产失效