正文怎么产出:写在稿纸上的工件
网页版 ChatGPT 的正文就是那条回复,生成一次就定了,想改一句只能整篇重来。梨园把正文当成工件:扮演模式写在稿纸上,一轮之内能查、能写、能改,收笔才算交付;写长篇的 agent 模式更进一步,正文就是磁盘上的文件。下面每一步都给出梨园源码里的模版,和同一段代码真跑出来的效果。
一轮对话里,模型能反复调工具
问题:生成一次就交差,模型没机会先查设定、写完再改。
梨园跑在 pi 这个 coding agent 上,一轮就是 pi 的一次 agent 循环:模型可以反复调工具,最多 20 个工具回合。梨园用扩展钩子接管两样东西——system 在 pi 自己那份后面接上扮演的那份,消息列表整个换成梨园从会话树重建的版本:
pi.on("before_agent_start", (event) => {
if (!activeStage) return undefined;
if (activeStage.mode === "authoring") return { systemPrompt: activeStage.systemPrompt };
let base = event.systemPrompt;
if (!event.systemPromptOptions?.customPrompt) {
const skeleton = stageSystemSkeleton();
if (!skeleton) return { systemPrompt: activeStage.systemPrompt };
console.error("[stage] SYSTEM.md 缺席,本拍用随包扮演骨架(建回 <agentDir>/SYSTEM.md 可停用本告警)");
base = skeleton;
}
return { systemPrompt: `${base}\n\n${activeStage.systemPrompt}` };
});
pi.on("context", (event) => activeStage ? { messages: activeStage.context(event.messages) as typeof event.messages } : undefined);
实跑第二轮第一次调用时,system 之后发出去的消息列表是这样的:开场白,第一轮的问答只留定稿,末条是数据块加本轮原话(状态、设定两页讲过这些块怎么来):
── system ──
(system,见 system.json)
── assistant ──
【开场 · 青梧】
*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*
*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*
「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」
── user ──
我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」
── assistant ──
她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。
她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。
「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」
她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。
「客官想打听他的什么?」
── user ──
【世界状态】
时间:傍晚(雨中)
地点:青梧的客栈(檐外下雨)
青梧:好感 5;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回
旅人:好感 0;在:青梧的客栈
旅人入住天数:三天
旅人打听对象:怀瑾公子(十年前从镇北渡口离开)
青梧对怀瑾的态度:知情但不轻易吐露
剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」
【登场名录】人物:青梧(傍晚(雨中))、旅人(傍晚(雨中));地点:青梧的客栈(檐外下雨)(傍晚(雨中))、青梧的客栈(傍晚(雨中));剧情线:旅人查访十年前从镇北渡口离开的怀瑾公子(傍晚(雨中))、墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情(傍晚(雨中))
【相关设定】本拍命中 3 条:听雨轩、旧油纸伞、怀瑾
【设定集索引】共 5 条:听雨轩、旧油纸伞、怀瑾、镇北渡口、更夫
【语言】以中文写叙事与对白(专有名词可保留原文)。
她提到那个名字时的反应让我心里一沉。接下来我该怎么做?给我几个选项。
同时发出去的还有 33 个工具,说明加参数共 13221 字,是 system 本身的 6 倍多。这次调用一共 7519 token,其中 6784 命中了前缀缓存——system 和工具说明逐字不变,每轮只有末尾几条是新的。
查不查、写多长、何时停下来问用户、何时收笔,都由模型自己定。扮演定义只管住一件事——什么叫写完(「拍」是梨园对一轮的叫法):
一拍的结束条件是正文已经交付——不是「计划已列」,也不是「工具已调」。
稿纸:正文能定点改
问题:回复不可逆,模型为了一次写对只能在思考里反复打草稿。src/draft.ts:4 记着一次实测:改一处就得把全稿在脑内重誊一遍,25k 字的思考里正文出现了两遍。
正文改写在稿纸上,由引擎保管;每次写都带版本号,版本对不上整批不改:
{
name: "draft_write", mode: "write",
description: "创建或全量替换本拍稿件,内容即时向用户展示并持久保存。写入不等于收笔;局部修改用 draft_edit,续写用 draft_append,完成用 draft_seal。简单回应也可直接输出。",
parameters: schema({ version, content: { ...STR, description: "完整稿件原文,保留所有格式与空白" } }, ["version", "content"]),
},
{
name: "draft_append", mode: "write",
description: "在当前稿件末尾续写一段并展示。默认与上文隔一个空行;separator 可指定精确连接字符。不会收笔。",
parameters: schema({ version, content: STR, separator: { ...STR, description: "默认两个换行;允许空串" } }, ["version", "content"]),
},
{
name: "draft_read", mode: "read", description: "读取本拍稿件与版本。start/end 是从 0 开始的 UTF-16 字符偏移,end 不含;省略即全文。返回的 content 是精确原文。",
parameters: schema({ start: { type: "integer", minimum: 0 }, end: { type: "integer", minimum: 0 } }, []),
},
{
name: "draft_search", mode: "read", description: "在本拍稿件中按字面检索。hits 保留原文标点和空白,可直接用作 draft_edit.old;返回版本及命中总数。",
parameters: schema({ query: STR, limit: { type: "integer", minimum: 1, maximum: 50 } }, ["query"]),
},
{
name: "draft_edit", mode: "write", description: "按唯一原文引用批量修改当前稿件。全部引用都基于同一版本;任一处缺失、重复、重叠或版本冲突则整批不改。允许首尾空白/等长标点的有限匹配,回执报告匹配级别。",
parameters: schema({ version, edits: { type: "array", minItems: 1, items: schema({ old: STR, new: STR }, ["old", "new"]) } }, ["version", "edits"]),
},
{
name: "draft_seal", mode: "write", description: "完整正文与所需格式内容都写入后,最后单独调用,将本拍当前版本收笔并交给定稿与场记流程。不检查文风、字数或计划完成度;收笔后不能再改本拍稿件。",
parameters: schema({ version }, ["version"]),
},
实跑第一轮的四次调用:先并发三个检索,再补查一个,然后一次 draft_write 写完,最后 draft_seal:
── 第 1 次调用(思考 236 字)
思考开头:The user has given a specific input. Let me check the lorebook for the relevant entries: 听雨轩, 怀瑾, 镇北渡口. The us…
→ lorebook_search {"query": "怀瑾"}
回执:[{"title": "怀瑾", "score": 9, "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。"}]
→ lorebook_search {"query": "镇北渡口"}
回执:[{"title": "镇北渡口", "score": 9, "content": "镇北渡口每日只有清晨一班船出镇。船夫老周摆渡三十年,认得每一个离镇的人。"}, {"title": "怀瑾", "score": 1, "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭…
→ lorebook_search {"query": "听雨轩"}
回执:[{"title": "听雨轩", "score": 10, "content": "听雨轩是栖水镇上开了近百年的老客栈,前后两进,后院有一株老梨树。店里的规矩是祖上传下来的:不问来处,不问去处,账清即可。镇上人都说,在听雨轩说过的话,不会传到第二个人耳朵里。"}, {"title": "栖水镇",…
── 第 2 次调用(思考 123 字)
思考开头:I have the key setting. Let me also check 旧油纸伞 to understand the伞 detail, and 更夫 isn't needed right now. Let m…
→ lorebook_search {"query": "旧油纸伞"}
回执:[{"title": "旧油纸伞", "score": 9, "content": "柜台后墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面上有一小块焦痕。青梧每天都会用干布擦一遍伞面,但从不撑开它,也从不回答关于这把伞的问题。"}]
── 第 3 次调用(思考 4485 字)
思考开头:Now I understand the setting. Key facts: - 沈怀瑾, only son of 沈家旧宅, left from 镇北渡口 ten years ago on a rainy nigh…
→ draft_write {"version": 0, "content": "…259 字…"}
回执:{"ok": true, "version": 1, "chars": 259}
── 第 4 次调用(思考 0 字)
→ draft_seal {"version": 1}
回执:{"ok": true, "sealed": true, "version": 1}
第三次调用的思考有 4485 字,模型在里面定了怎么写;稿纸上落下的是 259 字的定稿:
她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。
她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。
「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」
她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。
「客官想打听他的什么?」
收笔就结束这一轮
问题:模型交完稿还会接着说话,或者收了笔又想改。
draft_seal 成功,工具回执带上 terminate,pi 的循环就此停下,模型没有下一次调用的机会;改上一轮的 previous_draft_edit 同理:
...((name === "draft_seal" && ws.sealed || name === "previous_draft_edit" && ws.revision) && !media.isError ? { terminate: true } : {}),
实跑三轮的最后一次调用都是 draft_seal 或 previous_draft_edit,之后没有别的。收笔之后,定稿交给场记记账,再判断要不要压缩。
模型写了稿却没收笔,这一轮算没写完:界面提示「本拍尚未收笔」,场记不记账,也不触发压缩。
上一轮能原位改
问题:用户说「上一轮那句改成……」,重写这一轮是绕路,原来那句还留在历史里。
{
name: "previous_draft_read", mode: "read", description: "读取当前分支上一拍已定稿回复的精确原文与版本,供回改。省略 start/end 即全文;偏移为从 0 开始的 UTF-16 字符,end 不含。不能选更早的拍次。",
parameters: schema({ start: { type: "integer", minimum: 0 }, end: { type: "integer", minimum: 0 } }, []),
},
{
name: "previous_draft_edit", mode: "write", description: "按 previous_draft_read 的版本和唯一原文引用,原位修订上一拍回复;未引用部分保留,任一冲突则整批不改。只修正文,不重算账本或记忆;本拍已有新稿时不可用。单独调用,成功即结束本次请求,界面显示修后的原回复。",
parameters: schema({ version, edits: { type: "array", minItems: 1, items: schema({ old: STR, new: STR }, ["old", "new"]) } }, ["version", "edits"]),
},
实跑第三轮,用户说:「上一轮她最后那句『我倒想先问一句——你从哪里来,又为什么要寻他?』,改成『这些年,他过得好吗?』」。模型先读、再改,两次调用:
── 第 1 次调用(思考 378 字)
思考开头:The user wants me to revise the previous turn's last line. The previous turn was my response. But wait — actua…
→ previous_draft_read {}
回执:{"version": 0, "content": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板…
── 第 2 次调用(思考 2245 字)
思考开头:The user wants to change the last line. Let me edit. The original last line: 「我倒想先问一句——你从哪里来,又为什么要寻他?」 New: …
→ previous_draft_edit {"version": 0, "edits": [{"old": "「我倒想先问一句——你从哪里来,又为什么要寻他?」", "new": "「这些年,他过得好吗?」"}]}
回执:{"ok": true, "version": 1}
只有最后一句变了,其余逐字保留。这一轮不产生新回复,历史里第二轮那条直接换成改后的版本;账本和记忆不跟着重算。
稿纸之外的输出
问题:有的模型不调稿纸工具,直接把正文当回复输出;有的写了稿纸,又在文字通道里输出状态栏这类格式块。
稿纸还空着、模型直接输出了正文,引擎把这段文字代收成一次 draft_write;模型写了稿、没再调工具就停了,后面的文字代收成 draft_append:
} else if (!calls.length) {
if (ws.draft && !ws.sealed && ws.mode === "write") {
const continuation = textOfAssistant(final);
if (continuation) {
runWriteTool(ws, wsDeps, "draft_append", { content: continuation }, "capture");
tailStart = loopText.length;
}
if (!ws.explicitWrites) runWriteTool(ws, wsDeps, "draft_seal", {}, true);
}
if (!ws.draft.trim() && text.trim() && ws.mode === "write") {
const result = runWriteTool(ws, wsDeps, "draft_write", { content: text }, true);
ws.strayText = "";
ev.onActivity?.(result.ok ? "直出正文已代收为 draft_write" : "直出正文代收失败");
}
定稿以稿纸为主体,文字通道里的东西只认格式块,补在后面:
// 定稿 = 工作区稿(工件);工作区空(中断半拍/循环认栽)退回直出正文
// **但**模型常把格式栈尾巴(状态栏/catsay 等)走 text 通道而非 draft_write 参数:
// 二选一会把那部分连内容一起扔掉(8/05 实锤:模型宣告要出「正文+状态栏+咪咪点评」,
// draft_write 只交了正文,屏上流式见过三样、落树只剩一样)。故此处**合并**:
// 稿件为主体,text 里**格式特征**的尾巴补回(纯文本闲聊不进正文)。
const merged = mergeFinalText(ws.draft, ws.draft.trim() ? loopTail : ws.mode === "write" ? text : "");
// The existing format-tail compatibility path may add material, but never rewrites the artifact itself.
const finalText = ws.draft ? ws.draft + (merged.startsWith(ws.draft.trim()) ? merged.slice(ws.draft.trim().length) : "") : merged;
export const mergeFinalText = (draft: string, text: string): string => {
const d = draft.trim();
const t = text.trim();
if (!d) return dedupeIdenticalBlocks(t);
if (!t || d === t) return d;
// 稿件已包含 text(模型边写边交,text 是半截):稿件已是全量
if (d.includes(t)) return d;
// 只认「稿件在尾巴开头」的续写增量(正文在前、尾巴在后);稿件出现在尾巴中段
// 不切——格式块(state1/options 等)在正文之前,indexOf 会把它当「增量起点」把
// 前面的格式块一起切掉(8/13 实弹:<state1>…<content>正文</content>,状态栏全丢)。
let tail = t;
if (t.startsWith(d)) tail = t.slice(d.length);
const from = formatTailStart(tail);
if (from < 0) return d;
// 尾巴只留格式内容(块之外的自由文本/重述正文一律丢);`<content>` 里重述的正文再裁一次
const tailPart = formatContentOnly(tail.slice(from))
.replace(/<content>([\s\S]*?)<\/content>/g, (whole, inner: string) => {
const trimmed = trimContentBodyRepeat(d, inner);
return trimmed ? `<content>${trimmed}</content>` : "";
})
.replace(/\n{3,}/g, "\n\n")
.trim();
return dedupeIdenticalBlocks([d, tailPart].filter(Boolean).join("\n\n"));
};
拿第一轮的稿纸过两遍 mergeFinalText。例一的文字取自实跑里模型的一次真实输出,它把稿纸上的正文又缩写着说了一遍;例二是构造的格式块:
稿纸正文(末两段):
…她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。
「客官想打听他的什么?」
例一 · 文字通道里模型把正文缩写着重述了一遍:
*青梧在账本上停了一瞬,转身取下墙上那把青灰色的旧油纸伞,用袖口慢慢擦了一遍,才挂回去。*
「记得。这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」
*她转回柜台前,替你续了半盏热茶,茶烟隔在你们中间。*
「客官想打听他的什么?」
例一合并后:与稿纸逐字相同,重述整段丢掉
例二 · 文字通道里是一个格式块(构造的):
<status>好感 +2|地点:听雨轩</status>
例二合并后(末尾):
…她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。
「客官想打听他的什么?」
<status>好感 +2|地点:听雨轩</status>
重述的正文是闲聊,整段丢掉;<status> 是格式块,补到定稿末尾。
岔口交给用户
问题:模型替用户选剧情走向,甚至替用户的角色说话、做决定。
ask 让模型停下来,给几个选项和一个自由输入框,用户拍板后再接着演。什么时候该问,写在工具说明里:
{
name: "ask",
mode: "read",
description:
"剧情共创决策(P7 接回):把该由用户拍板的选择交给用户。两种触发:\n" +
"① **主动触发**(随时,含第 1 轮):用户输入本身在求方向/递笔(「接下来去找谁」「怎么办」" +
"「给个选项」「让我选」)——这不需要上文支撑,直接问。\n" +
"② **未定变量**(任何一拍):用户这句输入引出的未定变量——取不同值这拍走向会" +
"明显分岔(如:买下用户的人性格温和还是残暴,决定整拍怎么演)、且卡与世界书查不到——" +
"先问用户定下来再演。一次一问,多个变量只问最关键的。" +
"判断是**动态的**:变量不因「新」而重要——新人物的性格/身份不是全都要问,洞府里不是每件资源" +
"都值得问;不分岔的自己顺着演,不事事上报。禁止代写用户身份的完整档案、禁止替用户定重大变量。\n" +
`给出 2~4 个具体、可落地、彼此不同的选项(${language}),用户作答后按答案继续演。\n` +
"用户点了停止 = 笔还给用户,本拍就此收束。\n" +
"**选择框分流**:用户预设自带选择框格式(如 <w2g>)时,岔路与回合末选项**按预设格式写进正文**," +
"不要调本工具;只有预设没有选择框格式时才用 ask。",
parameters: {
type: "object",
properties: {
question: { ...STR, description: "要交给用户定夺的问题(一句话说清当前局面)" },
options: {
type: "array",
description: "2~4 个具体选项(可落地、彼此不同)",
items: { ...STR, description: "一个选项" },
},
},
required: ["question", "options"],
},
},
实跑第二轮,用户说「接下来我该怎么做?给我几个选项」。模型思考的头一句就是「用户在求方向,要求给选项」,第一次调用就是 ask;模拟用户选了第一项,模型再去查设定、写稿、收笔:
── 第 1 次调用(思考 465 字)
思考开头:用户在求方向,要求给选项。这是明确的「给个选项」指令。我应该用 ask 工具给出选项,让用户选择下一步怎么做。 先确认一下设定。用户在问「接下来我该怎么做?给我几个选项」。这是用户输入本身在求方向,符合 ask 的主动…
→ ask {"question": "你望着茶烟后那张不动声色的脸,明白怀瑾这个名字在她心里分量不轻。接下来怎么走?", "options": ["直问怀瑾的下落与生死,看她肯不肯给一句实话", "先不问人,改口打听墙上那把断骨焦痕的旧伞", "坦白自己来寻他的缘由,用实情换她的信任", "按兵不动,先住下,夜里或明早再从别处慢慢套话"]}
回执:{"answer": "直问怀瑾的下落与生死,看她肯不肯给一句实话", "by": "user"}
── 第 2 次调用(思考 111 字)
思考开头:用户选择:直问怀瑾的下落与生死。 现在我要写这一拍。青梧知情,态度谨慎回避,似乎怀有旧情。旅人直问怀瑾的下落与生死。 我需要查设定:怀瑾、旧油纸伞、镇北渡口、听雨轩。涉及具体设定先查再写。 让我查 lorebook…
→ lorebook_search {"query": "怀瑾 旧油纸伞 镇北渡口"}
回执:[{"title": "怀瑾", "score": 10, "content": "沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。"}, {"title": "旧油纸伞", …
→ lorebook_list {}
回执:["栖水镇(常驻)", "听雨轩", "旧油纸伞", "怀瑾", "镇北渡口", "更夫"]
── 第 3 次调用(思考 2252 字)
思考开头:设定查到了: - 沈怀瑾,镇北沈家旧宅的独子,十年前一个雨夜从镇北渡口搭船离开,此后音讯全无。他走的那晚,把一把青灰色的油纸伞留在了听雨轩。青梧从不提他的名字,有人提起时,她会先去擦那把伞。 - 旧油纸伞:伞骨断一根,焦…
→ draft_write {"version": 0, "content": "…282 字…"}
回执:{"ok": true, "version": 1, "chars": 282}
── 第 4 次调用(思考 0 字)
→ draft_seal {"version": 1}
回执:{"ok": true, "sealed": true, "version": 1}
定稿就是上面改前那份:旅人放下茶盏直问生死,青梧答「没有」——按用户选的那一项演。
写长篇:agent 模式,正文是文件
问题:想写成一部长篇,要回头改第三章的某一句;扮演模式里正文是一轮轮的回复,改不到那么远。
新建项目时可以选 agent 模式(默认是扮演),建好不能切换。这时用户是导演,讨论和正文分开,正文是 正文/ 下一章一个 .md,模型用 pi 原生的 read、grep、edit、write 改。system 换一份身份说明,后面照接卡的两层:
export function agentSystemPrompt(o: { cwd: string; cardPath: string; storyDir: string; userRules?: UserRules; cardAgents?: string; macro: MacroContext }): string {
const sections = [(o.userRules?.agent?.trim() || shippedAgentRules()).trim()];
if (o.userRules?.card?.trim()) sections.push(o.userRules.card.trim());
if (o.cardAgents?.trim()) sections.push(applyMacros(o.cardAgents.trim(), o.macro));
sections.push(`工作目录:${o.cwd}\n当前角色卡:${o.cardPath}\n稿子目录:${o.storyDir}`);
return sections.join("\n\n");
}
/** 项目状态块:前情(最早)→ 账本 → 名录 → 稿子目录(紧邻用户这轮的话)。全是数据块,语义在 AGENT_APPEND_SYSTEM.md 一次说清。 */
export function buildAgentStateBlock(o: { state: WorldState; rosterIndex?: string; summary?: string; residentSummary?: string; files: StoryFile[] }): string {
const blocks: string[] = [];
const past = [o.summary, o.residentSummary].filter(Boolean);
if (past.length) blocks.push(`【前情提要】以下是更早剧情的接力摘要,是既定事实:\n\n${past.join("\n\n")}`);
blocks.push(`【世界状态】\n${formatState(o.state)}`);
if (o.rosterIndex) blocks.push(`【登场名录】${o.rosterIndex}`);
blocks.push(formatStoryIndex(o.files));
return blocks.join("\n\n");
}
# agent 模式
你担任创作 agent,与用户共同写一部作品。用户是导演,不是故事里的角色;你们之间的每一条消息都是讨论,永远不是正文。
## 一轮
用户发一次输入,你回应一轮。一轮里你可以读、查、想、改文件,也可以只讨论、一个字不写——不该由你决定的问用户,定了的就去做。
## 正文是文件
正文只存在于正文目录里:按文件名排序的 .md 文件,一个文件一章,文件名决定顺序(建议 `NNN-标题.md`)。改标题就改名,插章就起一个落在中间的名字。
用文件工具对着这些文件工作:`read` 读、`grep` 找、`edit` 定点改、`write` 新建或整章替换。可以改任何一章的任何一句,不限于最新一章;一轮可以碰多个文件。保存与改动历史由梨园管理,不需要你做。
正文一定落在文件里。讨论区里的文字是讨论:说结论、说改了什么、问问题;写在讨论区里的段落不会成为正文。
正文不进上下文。实跑把开场白和前两轮的定稿存成三章,用户说「第一章里她答的那句『记得。』太干了,改得更有余味一点;第二章结尾补一句雨停了的描写」。随这句话附上的状态块只有账本、名录和【正文目录】:
【世界状态】
时间:傍晚(雨中)
地点:青梧的客栈(檐外下雨)
青梧:好感 8;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回;听闻怀瑾十年未归、生死不明后平静应答,转而反问旅人来历与寻人缘由
旅人:好感 2;在:青梧的客栈
旅人入住天数:三天
旅人打听对象:怀瑾公子(十年前从镇北渡口离开)
青梧对怀瑾的态度:知情但不轻易吐露
剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」 「青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答」
【登场名录】人物:青梧(傍晚(雨中))、旅人(傍晚(雨中));地点:青梧的客栈(檐外下雨)(傍晚(雨中))、青梧的客栈(傍晚(雨中));剧情线:旅人查访十年前从镇北渡口离开的怀瑾公子(傍晚(雨中))、墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情(傍晚(雨中))、青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答(傍晚(雨中))
【正文目录】正文/ 共 3 个文件 725 字(按文件名排序)
000-开场.md 184 字 2026-10-20 16:14
001-雨夜问名.md 259 字 2026-10-20 16:14
002-坦言.md 282 字 2026-10-20 16:14
模型要看正文只能自己读。三次调用:先把三章都读一遍,再两处定点改,最后在讨论区说改了什么——正文落在文件里,讨论区只放讨论:
调用 1:read({"path": "cards/青梧/对话/局C/正文/000-开场.md"});read({"path": "cards/青梧/对话/局C/正文/001-雨夜问名.md"});read({"path": "cards/青梧/对话/局C/正文/002-坦言.md"})
调用 2:edit({"path": "cards/青梧/对话/局C/正文/001-雨夜问名.md", "oldText": "「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」", "newText": "「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」"});edit({"path": "cards/青梧/对话/局C/正文/002-坦言.md", "oldText": "「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」", "newText": "「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\n*檐外的雨不知何时已经停了,只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。*"})
调用 3:不调工具,在讨论区回复:
改好了两处。
**「记得。」**(001)改成:
> 「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」
「还湿着」接住檐外雨声,让这两个字从记忆里捞出来、还没晾干,余味比原来的干答多了半口气。
**第二章结尾**(002)补了一句雨停:
> 檐外的雨不知何时已经停了,只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。
雨在问话处停,滴水「替谁数着什么」跟前面她擦杯子、数日子、数十年那一路意象能勾上,收尾的静比原来的硬停更沉。要不要我再顺手看一眼章节标题还贴不贴——毕竟雨这会儿停了,002 若是叫《坦言》倒没问题。
效果 · agent 轮的完整 system
# agent 模式
你担任创作 agent,与用户共同写一部作品。用户是导演,不是故事里的角色;你们之间的每一条消息都是讨论,永远不是正文。
## 一轮
用户发一次输入,你回应一轮。一轮里你可以读、查、想、改文件,也可以只讨论、一个字不写——不该由你决定的问用户,定了的就去做。
## 正文是文件
正文只存在于正文目录里:按文件名排序的 .md 文件,一个文件一章,文件名决定顺序(建议 `NNN-标题.md`)。改标题就改名,插章就起一个落在中间的名字。
用文件工具对着这些文件工作:`read` 读、`grep` 找、`edit` 定点改、`write` 新建或整章替换。可以改任何一章的任何一句,不限于最新一章;一轮可以碰多个文件。保存与改动历史由梨园管理,不需要你做。
正文一定落在文件里。讨论区里的文字是讨论:说结论、说改了什么、问问题;写在讨论区里的段落不会成为正文。
## 写之前先查
卡、世界书、剧情记忆、面板、世界状态都不在你的记忆里,它们在工具后面;正文也不在上下文里,每轮随附的项目状态块只有它的目录。涉及前文时读相关文件,涉及具体设定(人物、地点、武学、组织、往事、数值)时先查再写。**你脑子里没有的东西,不要替卡作者发明。** 世界书正文用 `lorebook_search` 取,`lorebook_list` 看全量。
## 岔口交给用户
用户是共同作者,不是观众。该由用户拍板的选择用 `ask` 交出去;用户已经下了的指令就去执行。
## 项目状态块
- 【前情提要】是更早章节的接力摘要,是既定事实。
- 【世界状态】是当前事实基准,由场记在新章落地后记账。
- 【登场名录】是本局登场过的人物/地点/物品/剧情线全量名字与首次登场时间,细节可用 memory_search 查;名录之外的名字才是新登场。
- 【正文目录】是正文目录下的文件名、字数与最后改动时间。
## 素材与写作要求
你的身份定位只在本文件。角色卡、卡内 APPEND_SYSTEM.md 与 AGENTS.md(含其中标注「预设」的段落)里的角色身份、人称、文风、篇幅、格式、状态栏、谁的台词不许替写,都是**稿子的写作要求**:写进文件的正文按它办,讨论区的话不按它办。世界书、名录、记忆是创作素材。改中间章之后,账本、摘要与后文不会自动跟随。
工作目录:/tmp/liyuan-sim/v2/ws
当前角色卡:cards/青梧/default_Qingwu.json
稿子目录:cards/青梧/对话/局C/正文
检查点:改坏了能回去
问题:一轮改了好几章,改坏了想回到改之前。
每轮结束,把 正文/ 下每个文件按内容哈希存进 历史/对象/,同样的内容不重存;整份文件清单和上一个检查点比,变了才往 检查点.jsonl 追加一行。文件头说得直白:「这是 git 的 blob+tree,没有 git 的分支/合并/远端」。
commit(o: { author: Checkpoint["author"]; message: string; turnId?: string; aborted?: boolean; restoredFrom?: string; force?: boolean }): Checkpoint | undefined {
const prev = this.latest();
const files = this.snapshot();
const changed = diffManifests(prev?.files ?? {}, files);
if (isEmptyChange(changed) && !o.force) return undefined;
const c: Checkpoint = {
id: `${Date.now().toString(36)}-${hashOf(JSON.stringify(files) + Math.random()).slice(0, 6)}`,
ts: Date.now(),
author: o.author,
...(o.turnId ? { turnId: o.turnId } : {}),
message: o.message,
...(o.aborted ? { aborted: true } : {}),
...(o.restoredFrom ? { restoredFrom: o.restoredFrom } : {}),
files,
changed,
};
mkdirSync(this.historyDir, { recursive: true });
appendFileSync(this.logFile, `${JSON.stringify(c)}\n`, "utf8");
return c;
}
上面那一轮结束时落下的检查点:
{
"id": "mvgeqwpn-6a5473",
"ts": 1792484091419,
"author": "agent",
"message": "第一章里她答的那句「记得。」太干了,改得更有余味一点;第二章结尾补一句雨停了的描写。",
"files": {
"000-开场.md": "7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc",
"001-雨夜问名.md": "380819ef4c25a27a24be02a032f988d874f289af7b1a245381c11781bff9bbc7",
"002-坦言.md": "d3c81d0ae75a3a51fb843b4b4e0aef9dc0805bf964dce2ce24e6aef14a9720ea"
},
"changed": {
"added": [],
"modified": [
"001-雨夜问名.md",
"002-坦言.md"
],
"renamed": [],
"removed": []
}
}
000-开场.md 的哈希和导入时那个检查点相同,没重存;改过的两章各多一个对象,历史/对象/ 里一共 5 个。恢复有两种:只把正文文件恢复到某个检查点、讨论不动;或者文件和对话一起回到那一轮之前,前端的「撤销本轮」就是后者。
谁管什么
- 程序:工具回合上限、稿纸版本、收笔即停、直出代收、格式块补回、存检查点
- 模型:查不查、怎么写、改哪句、何时
ask、何时收笔;agent 模式里读哪章、改哪章 - 用户:在
ask的岔口拍板,要求改上一轮;agent 模式里当导演、选恢复方式
实跑原件
第三轮完整记录:改上一轮
{
"user": "上一轮她最后那句「我倒想先问一句——你从哪里来,又为什么要寻他?」,改成「这些年,他过得好吗?」",
"injection": "【世界状态】\n时间:傍晚(雨中)\n地点:青梧的客栈(檐外下雨)\n青梧:好感 8;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回;听闻怀瑾十年未归、生死不明后平静应答,转而反问旅人来历与寻人缘由\n旅人:好感 2;在:青梧的客栈\n旅人入住天数:三天\n旅人打听对象:怀瑾公子(十年前从镇北渡口离开)\n青梧对怀瑾的态度:知情但不轻易吐露\n剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」 「青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答」\n\n【登场名录】人物:青梧(傍晚(雨中))、旅人(傍晚(雨中));地点:青梧的客栈(檐外下雨)(傍晚(雨中))、青梧的客栈(傍晚(雨中));剧情线:旅人查访十年前从镇北渡口离开的怀瑾公子(傍晚(雨中))、墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情(傍晚(雨中))、青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答(傍晚(雨中))\n\n【相关设定】本拍命中 2 条:旧油纸伞、怀瑾\n\n【设定集索引】共 5 条:听雨轩、旧油纸伞、怀瑾、镇北渡口、更夫\n\n【语言】以中文写叙事与对白(专有名词可保留原文)。",
"activated": [
"旧油纸伞",
"怀瑾"
],
"messages_sent": [
{
"role": "system",
"content": "(system,见 system.json)"
},
{
"role": "assistant",
"content": "【开场 · 青梧】\n*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」"
},
{
"role": "user",
"content": "我把湿透的外衣搭在椅背上,接过茶喝了一口:「住三天。顺便打听个人——十年前从镇北渡口走的那位怀瑾公子,掌柜可还记得?」"
},
{
"role": "assistant",
"content": "她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。\n\n她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。\n\n「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\n她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。\n\n「客官想打听他的什么?」\n"
},
{
"role": "user",
"content": "她提到那个名字时的反应让我心里一沉。接下来我该怎么做?给我几个选项。"
},
{
"role": "assistant",
"content": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板上,像在替她把那句问话往下按了按。*\n\n「没有。」*她说,声音很平,听不出起伏。*「十年了,一封口信、半张纸,都没有回来过。是死是活,我也答不上来。镇子上的人,早就当他不在了。」\n\n*她把擦干的杯子放进托盘,这才抬起眼,正正地看着你。*\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n"
},
{
"role": "user",
"content": "【世界状态】\n时间:傍晚(雨中)\n地点:青梧的客栈(檐外下雨)\n青梧:好感 8;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回;听闻怀瑾十年未归、生死不明后平静应答,转而反问旅人来历与寻人缘由\n旅人:好感 2;在:青梧的客栈\n旅人入住天数:三天\n旅人打听对象:怀瑾公子(十年前从镇北渡口离开)\n青梧对怀瑾的态度:知情但不轻易吐露\n剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」 「青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答」\n\n【登场名录】人物:青梧(傍晚(雨中))、旅人(傍晚(雨中));地点:青梧的客栈(檐外下雨)(傍晚(雨中))、青梧的客栈(傍晚(雨中));剧情线:旅人查访十年前从镇北渡口离开的怀瑾公子(傍晚(雨中))、墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情(傍晚(雨中))、青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答(傍晚(雨中))\n\n【相关设定】本拍命中 2 条:旧油纸伞、怀瑾\n\n【设定集索引】共 5 条:听雨轩、旧油纸伞、怀瑾、镇北渡口、更夫\n\n【语言】以中文写叙事与对白(专有名词可保留原文)。\n\n上一轮她最后那句「我倒想先问一句——你从哪里来,又为什么要寻他?」,改成「这些年,他过得好吗?」"
}
],
"steps": [
{
"round": 1,
"reasoning": "The user wants me to revise the previous turn's last line. The previous turn was my response. But wait — actually the user's message says: 上一轮她最后那句「我倒想先问一句——你从哪里来,又为什么要寻他?」,改成「这些年,他过得好吗?」\n\nThis is a request to edit the previous turn's final line. I should use previous_draft_read to get the exact text, then previous_draft_edit to replace.\n\nLet me read the previous draft first.",
"content": "",
"calls": [
{
"name": "previous_draft_read",
"args": {},
"result": {
"version": 0,
"content": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板上,像在替她把那句问话往下按了按。*\n\n「没有。」*她说,声音很平,听不出起伏。*「十年了,一封口信、半张纸,都没有回来过。是死是活,我也答不上来。镇子上的人,早就当他不在了。」\n\n*她把擦干的杯子放进托盘,这才抬起眼,正正地看着你。*\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n"
}
}
],
"usage": {
"prompt_tokens": 7814,
"completion_tokens": 135,
"total_tokens": 7949,
"prompt_tokens_details": {
"cached_tokens": 6912
},
"completion_tokens_details": {
"reasoning_tokens": 105
},
"prompt_cache_hit_tokens": 6912,
"prompt_cache_miss_tokens": 902
}
},
{
"round": 2,
"reasoning": "The user wants to change the last line. Let me edit.\n\nThe original last line: 「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\nNew: 「这些年,他过得好吗?」\n\nWait, but there's also the preceding part \"客官要寻的若是他,怕是寻不到什么了。\" She says that then the question. The user only wants to change the last sentence part.\n\nActually let me think about the full sentence: 「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\nThe user wants the last quoted sentence changed to 「这些年,他过得好吗?」.\n\nBut wait — that's a strange change because \"这些年,他过得好吗?\" is something the旅人 would ask, not 青梧. Actually the user is the旅人, and this line is 青梧's. Hmm, actually the user says \"她最后那句...改成...\" — the \"她\" refers to 青梧. So they want 青梧 to say 「这些年,他过得好吗?」 instead.\n\nBut that doesn't quite make sense for 青梧 to ask \"这些年,他过得好吗\" — she's asking about 怀瑾's wellbeing? Actually that could make sense: she's asking the旅人, who came seeking him, \"这些年,他过得好吗?\" — implying she wants to know from the旅人 whether 怀瑾 is doing well. That actually works as a poignant reversal: instead of interrogating the旅人, she reveals her concern for 怀瑾.\n\nActually wait, but there's also the preceding sentence \"客官要寻的若是他,怕是寻不到什么了。\" — that stays. Then the changed question.\n\nHmm, but if 青梧 asks \"这些年,他过得好吗?\" it implies she's asking about 怀瑾. But the旅人 came to find 怀瑾, so the旅人 might know. Actually the旅人 just asked \"他还活着吗?这十年,有没有回来过?\" which suggests the旅人 doesn't know either. So it's a bit odd for 青梧 to ask the旅人 \"这些年他过得好吗\" — unless she's just musing.\n\nBut the user's instruction is clear. I should just do the edit as requested. The instruction is to change that line. I'll do the minimal edit.\n\nActually, let me reconsider the exact wording. The line is:\n\n「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\nChange to:\n\n「这些年,他过得好吗?」\n\nThe instruction says \"改成「这些年,他过得好吗?」\". So I just replace the quoted sentence.\n\nLet me do previous_draft_edit with the old being the full quoted sentence and new being the new one.\n\nOld: 「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\nNew: 「这些年,他过得好吗?」\n\nLet me verify this is unique. Yes, it appears once.\n\nI should also consider whether to adjust the narration around it — \"她顿了顿,语气依旧和缓,却比方才多了几分认真。\" That narration still fits: she pauses, asks quietly \"这些年,他过得好吗?\" Actually the narration \"却比方才多了几分认真\" fits fine.\n\nLet me do the edit.",
"content": "",
"calls": [
{
"name": "previous_draft_edit",
"args": {
"version": 0,
"edits": [
{
"old": "「我倒想先问一句——你从哪里来,又为什么要寻他?」",
"new": "「这些年,他过得好吗?」"
}
]
},
"result": {
"ok": true,
"version": 1
}
}
],
"usage": {
"prompt_tokens": 8178,
"completion_tokens": 816,
"total_tokens": 8994,
"prompt_tokens_details": {
"cached_tokens": 7936
},
"completion_tokens_details": {
"reasoning_tokens": 717
},
"prompt_cache_hit_tokens": 7936,
"prompt_cache_miss_tokens": 242
}
}
],
"final": "",
"sealed": false,
"previous_after": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板上,像在替她把那句问话往下按了按。*\n\n「没有。」*她说,声音很平,听不出起伏。*「十年了,一封口信、半张纸,都没有回来过。是死是活,我也答不上来。镇子上的人,早就当他不在了。」\n\n*她把擦干的杯子放进托盘,这才抬起眼,正正地看着你。*\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「这些年,他过得好吗?」\n",
"note": "梨园在 draft_seal / previous_draft_edit 成功后结束本轮(src/stage/engine.ts:1883),之后的模型调用已从记录里去掉"
}
agent 轮完整记录:system、状态块、每次调用、两个检查点
{
"user": "第一章里她答的那句「记得。」太干了,改得更有余味一点;第二章结尾补一句雨停了的描写。",
"system": "# agent 模式\n\n你担任创作 agent,与用户共同写一部作品。用户是导演,不是故事里的角色;你们之间的每一条消息都是讨论,永远不是正文。\n\n## 一轮\n\n用户发一次输入,你回应一轮。一轮里你可以读、查、想、改文件,也可以只讨论、一个字不写——不该由你决定的问用户,定了的就去做。\n\n## 正文是文件\n\n正文只存在于正文目录里:按文件名排序的 .md 文件,一个文件一章,文件名决定顺序(建议 `NNN-标题.md`)。改标题就改名,插章就起一个落在中间的名字。\n\n用文件工具对着这些文件工作:`read` 读、`grep` 找、`edit` 定点改、`write` 新建或整章替换。可以改任何一章的任何一句,不限于最新一章;一轮可以碰多个文件。保存与改动历史由梨园管理,不需要你做。\n\n正文一定落在文件里。讨论区里的文字是讨论:说结论、说改了什么、问问题;写在讨论区里的段落不会成为正文。\n\n## 写之前先查\n\n卡、世界书、剧情记忆、面板、世界状态都不在你的记忆里,它们在工具后面;正文也不在上下文里,每轮随附的项目状态块只有它的目录。涉及前文时读相关文件,涉及具体设定(人物、地点、武学、组织、往事、数值)时先查再写。**你脑子里没有的东西,不要替卡作者发明。** 世界书正文用 `lorebook_search` 取,`lorebook_list` 看全量。\n\n## 岔口交给用户\n\n用户是共同作者,不是观众。该由用户拍板的选择用 `ask` 交出去;用户已经下了的指令就去执行。\n\n## 项目状态块\n\n- 【前情提要】是更早章节的接力摘要,是既定事实。\n- 【世界状态】是当前事实基准,由场记在新章落地后记账。\n- 【登场名录】是本局登场过的人物/地点/物品/剧情线全量名字与首次登场时间,细节可用 memory_search 查;名录之外的名字才是新登场。\n- 【正文目录】是正文目录下的文件名、字数与最后改动时间。\n\n## 素材与写作要求\n\n你的身份定位只在本文件。角色卡、卡内 APPEND_SYSTEM.md 与 AGENTS.md(含其中标注「预设」的段落)里的角色身份、人称、文风、篇幅、格式、状态栏、谁的台词不许替写,都是**稿子的写作要求**:写进文件的正文按它办,讨论区的话不按它办。世界书、名录、记忆是创作素材。改中间章之后,账本、摘要与后文不会自动跟随。\n\n工作目录:/tmp/liyuan-sim/v2/ws\n当前角色卡:cards/青梧/default_Qingwu.json\n稿子目录:cards/青梧/对话/局C/正文",
"stateBlock": "【世界状态】\n时间:傍晚(雨中)\n地点:青梧的客栈(檐外下雨)\n青梧:好感 8;状态:客栈掌柜;在:青梧的客栈;备注:对怀瑾公子一事知情,态度谨慎回避,似乎怀有旧情;取下墙上断骨的旧油纸伞仔细拂拭后挂回;听闻怀瑾十年未归、生死不明后平静应答,转而反问旅人来历与寻人缘由\n旅人:好感 2;在:青梧的客栈\n旅人入住天数:三天\n旅人打听对象:怀瑾公子(十年前从镇北渡口离开)\n青梧对怀瑾的态度:知情但不轻易吐露\n剧情线:「旅人查访十年前从镇北渡口离开的怀瑾公子」 「墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情」 「青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答」\n\n【登场名录】人物:青梧(傍晚(雨中))、旅人(傍晚(雨中));地点:青梧的客栈(檐外下雨)(傍晚(雨中))、青梧的客栈(傍晚(雨中));剧情线:旅人查访十年前从镇北渡口离开的怀瑾公子(傍晚(雨中))、墙上断骨带焦痕的旧油纸伞与怀瑾疑似有关,青梧对其有难言旧情(傍晚(雨中))、青梧反问旅人来历与寻访怀瑾的缘由,旅人尚未作答(傍晚(雨中))\n\n【正文目录】正文/ 共 3 个文件 725 字(按文件名排序)\n000-开场.md 184 字 2026-10-20 16:14\n001-雨夜问名.md 259 字 2026-10-20 16:14\n002-坦言.md 282 字 2026-10-20 16:14",
"steps": [
{
"round": 1,
"reasoning": "Let me look at the files first.",
"content": "",
"calls": [
{
"name": "read",
"args": {
"path": "cards/青梧/对话/局C/正文/000-开场.md"
},
"result": "*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」"
},
{
"name": "read",
"args": {
"path": "cards/青梧/对话/局C/正文/001-雨夜问名.md"
},
"result": "她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。\n\n她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。\n\n「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\n她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。\n\n「客官想打听他的什么?」\n"
},
{
"name": "read",
"args": {
"path": "cards/青梧/对话/局C/正文/002-坦言.md"
},
"result": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板上,像在替她把那句问话往下按了按。*\n\n「没有。」*她说,声音很平,听不出起伏。*「十年了,一封口信、半张纸,都没有回来过。是死是活,我也答不上来。镇子上的人,早就当他不在了。」\n\n*她把擦干的杯子放进托盘,这才抬起眼,正正地看着你。*\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n"
}
]
},
{
"round": 2,
"reasoning": "The user wants two changes:\n1. Chapter 1 (000-开场.md) — wait, \"第一章里她答的那句「记得。」\" — that's actually in 001-雨夜问名.md. The \"记得。\" line is in 001. The user says \"第一章里她答的那句「记得。」太干了,改得更有余味一点\". Let me check. In 001, she says 「记得。」. So the \"记得。\" is in file 001-雨夜问名.md. But user says \"第一章\" — numbering could be by chapter order. File 000 is 开场, 001 is 雨夜问名, 002 is 坦言. The \"记得。\" is in 001.\n\nActually, wait, let me re-read. The \"记得。\" is in 001-雨夜问名.md. But the user says \"第一章\". Perhaps they count 000 as 序/开场, or they just mean the first chapter where she answers \"记得\". Either way, the target line is 「记得。」 in 001.\n\n2. Chapter 2 (002-坦言.md) ending: add a description of the rain stopping.\n\nLet me make the \"记得。\" more evocative. Currently:\n\n「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nI need to make \"记得\" have more 余味 (aftertaste). Something like a pause, a gesture, perhaps not just \"记得\" but something with more lingering quality.\n\nIdeas: She could repeat it, or qualify it, or let it hang. For example:\n\n「记得。」她背对着你,许久才应这一声,声音照旧不高,混在檐外的雨声里,像也淋了十年雨,「……怎么不记得。这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nThat adds \"怎么不记得\" and imagery. Let me craft it carefully while keeping style.\n\nAlso the second change: add rain-stopping description at the end of 002. Currently ends with her question. Adding rain stopping at end.\n\nLet me write the edits.\n\nFor the \"记得\" line, I'll enhance:\n\nOld: 「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nNew idea: add a beat before/after, keep it not too long. Something like:\n\n「记得。」她背对着你,停了好一会儿,才又开口,声音照旧不高,混在檐外的雨声里,像是从很远的地方折回来,「……怎么不记得。这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nHmm, \"像是从很远的地方折回来\" is nice, but maybe too much. Let me keep 余味 while staying in the established prose style (第三版 careful). \n\nMaybe:\n\n「记得。」她背对着你,这一声比雨声还轻,落下去,半晌没人接。「……怎么不记得。这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nThat has a pause. Actually let me incorporate the original \"声音照旧不高,混在檐外的雨声里\" but make \"记得\" itself more lingering.\n\nOption:\n\n「记得。」她背对着你,把这两个字说得很慢,像是从很远的旧日子里捞起来,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nThat's nice — \"还湿着\" echoes the rain. Good 余味.\n\nActually \"记得\" becomes \"记得——\" maybe. Let me choose:\n\n「记得。」她背对着你,把这两个字说得很慢,像从很远的旧日子里捞起来,还沾着雨,潮潮的,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nI like this. But \"还沾着雨,潮潮的\" maybe too much. Keep simpler:\n\n「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\nGood.\n\nFor the rain stopping in 002, add at the very end after her question. Current ending:\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\nAdd a rain-stopping description. Something like:\n\n*檐外的雨不知何时已经停了。只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。*\n\nThat's good and atmospheric, echoing the rain motif. Let me add it.\n\nLet me make the edits.",
"content": "",
"calls": [
{
"name": "edit",
"args": {
"path": "cards/青梧/对话/局C/正文/001-雨夜问名.md",
"oldText": "「记得。」她背对着你,声音照旧不高,混在檐外的雨声里,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」",
"newText": "「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」"
},
"result": "ok"
},
{
"name": "edit",
"args": {
"path": "cards/青梧/对话/局C/正文/002-坦言.md",
"oldText": "「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」",
"newText": "「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\n*檐外的雨不知何时已经停了,只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。*"
},
"result": "ok"
}
]
},
{
"round": 3,
"reasoning": "",
"content": "改好了两处。\n\n**「记得。」**(001)改成:\n\n> 「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\n「还湿着」接住檐外雨声,让这两个字从记忆里捞出来、还没晾干,余味比原来的干答多了半口气。\n\n**第二章结尾**(002)补了一句雨停:\n\n> 檐外的雨不知何时已经停了,只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。\n\n雨在问话处停,滴水「替谁数着什么」跟前面她擦杯子、数日子、数十年那一路意象能勾上,收尾的静比原来的硬停更沉。要不要我再顺手看一眼章节标题还贴不贴——毕竟雨这会儿停了,002 若是叫《坦言》倒没问题。",
"calls": []
}
],
"checkpoint0": {
"id": "mvgeqb8t-c5321b",
"ts": 1792484063597,
"author": "user",
"message": "导入三章",
"files": {
"000-开场.md": "7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc",
"001-雨夜问名.md": "2c79e2deff28fea49636d864e895557f6bb08d3802f5adba32b679e82caef45a",
"002-坦言.md": "35e5dada9d3652aff799a0087f498beb8a61ceb55be69b89f6020fb2b59718a6"
},
"changed": {
"added": [
"000-开场.md",
"001-雨夜问名.md",
"002-坦言.md"
],
"modified": [],
"renamed": [],
"removed": []
}
},
"checkpoint1": {
"id": "mvgeqwpn-6a5473",
"ts": 1792484091419,
"author": "agent",
"message": "第一章里她答的那句「记得。」太干了,改得更有余味一点;第二章结尾补一句雨停了的描写。",
"files": {
"000-开场.md": "7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc",
"001-雨夜问名.md": "380819ef4c25a27a24be02a032f988d874f289af7b1a245381c11781bff9bbc7",
"002-坦言.md": "d3c81d0ae75a3a51fb843b4b4e0aef9dc0805bf964dce2ce24e6aef14a9720ea"
},
"changed": {
"added": [],
"modified": [
"001-雨夜问名.md",
"002-坦言.md"
],
"renamed": [],
"removed": []
}
},
"chapters": {
"000-开场.md": "*雨下到第三天,青石板路上的水洼连成了片。你推开听雨轩的门,檐角的铜铃轻响了一声。*\n\n*柜台后的女子抬起头来,目光在你被雨水浸透的肩头停了一瞬,随即移开,像是什么都没看见。她取过一只干净的粗瓷杯,倒了盏冒着热气的茶,推到柜台外沿。*\n\n「这样的天气还赶路,多半是有不得不赶的缘由。」*她的声音不高,混在雨声里却听得很清楚。*「茶是热的,房也有空的。客官打算住几天?」",
"001-雨夜问名.md": "她正往账本上记什么,闻言笔尖一顿,墨在纸上洇开一点。\n\n她没有立刻应声,只把笔轻轻搁下,起身走到柜台后那面墙前。墙上挂着一把青灰色的旧油纸伞,伞骨断了一根,伞面挨近边缘处有一小块焦痕。她踮脚把伞取下来,用袖口沿着伞面慢慢地拂,动作很轻,像在擦一道早该擦掉、却总也擦不掉的旧痕。\n\n「记得。」她背对着你,把这两个字说得很慢,像从很远很旧的雨天里捞起来的,还湿着,「这镇子小,一条街,一个渡口。走一个人,十年也散不干净。」\n\n她把伞重新挂回去,把系绳结得仔细,才转回柜台前,替你续了半盏热茶。茶烟腾起来,隔在你们中间。\n\n「客官想打听他的什么?」\n",
"002-坦言.md": "*你放下茶盏,问得直:「沈怀瑾——他还活着吗?这十年,有没有回来过?」*\n\n*青梧正擦着一只茶杯,指腹沿着杯口转了一圈,动作没停,只是慢了下来。檐外的雨声忽然就近了,一滴滴敲在青石板上,像在替她把那句问话往下按了按。*\n\n「没有。」*她说,声音很平,听不出起伏。*「十年了,一封口信、半张纸,都没有回来过。是死是活,我也答不上来。镇子上的人,早就当他不在了。」\n\n*她把擦干的杯子放进托盘,这才抬起眼,正正地看着你。*\n\n「客官要寻的若是他,怕是寻不到什么了。」*她顿了顿,语气依旧和缓,却比方才多了几分认真。*「我倒想先问一句——你从哪里来,又为什么要寻他?」\n\n*檐外的雨不知何时已经停了,只剩屋檐滴水,一下一下,敲在青石板上,像在替谁数着什么。*\n"
},
"objects": [
"2c79e2deff28fea49636d864e895557f6bb08d3802f5adba32b679e82caef45a",
"35e5dada9d3652aff799a0087f498beb8a61ceb55be69b89f6020fb2b59718a6",
"380819ef4c25a27a24be02a032f988d874f289af7b1a245381c11781bff9bbc7",
"7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc",
"d3c81d0ae75a3a51fb843b4b4e0aef9dc0805bf964dce2ce24e6aef14a9720ea"
],
"log": "{\"id\":\"mvgeqb8t-c5321b\",\"ts\":1792484063597,\"author\":\"user\",\"message\":\"导入三章\",\"files\":{\"000-开场.md\":\"7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc\",\"001-雨夜问名.md\":\"2c79e2deff28fea49636d864e895557f6bb08d3802f5adba32b679e82caef45a\",\"002-坦言.md\":\"35e5dada9d3652aff799a0087f498beb8a61ceb55be69b89f6020fb2b59718a6\"},\"changed\":{\"added\":[\"000-开场.md\",\"001-雨夜问名.md\",\"002-坦言.md\"],\"modified\":[],\"renamed\":[],\"removed\":[]}}\n{\"id\":\"mvgeqwpn-6a5473\",\"ts\":1792484091419,\"author\":\"agent\",\"message\":\"第一章里她答的那句「记得。」太干了,改得更有余味一点;第二章结尾补一句雨停了的描写。\",\"files\":{\"000-开场.md\":\"7ce9ac2f4ac389d8d3e59d8d22cfdb43c3b33db2d3899c0b0680955284b712bc\",\"001-雨夜问名.md\":\"380819ef4c25a27a24be02a032f988d874f289af7b1a245381c11781bff9bbc7\",\"002-坦言.md\":\"d3c81d0ae75a3a51fb843b4b4e0aef9dc0805bf964dce2ce24e6aef14a9720ea\"},\"changed\":{\"added\":[],\"modified\":[\"001-雨夜问名.md\",\"002-坦言.md\"],\"renamed\":[],\"removed\":[]}}\n"
}


