Files
writing-agent/docs/preset-workbench.md
T
chenmoranzhi f3303038fe 新增预设工作台:编辑、版本树、结构检查、用例试跑放在同一页。
改预设要能一边写一边看装出来的 messages,还要多模型对照,所以把编辑与试跑拆成两个视图塞进一个页面;版本存完整快照、可分叉,用例全局共用一份、运行记录按预设存。顺带让读预设文件先剥 BOM,静态资源改成不缓存,免得改了脚本还要强刷。
2026-10-26 01:09:40 +08:00

11 KiB
Raw Blame History

预设工作台

本仓库自用的预设编辑 / 试跑页面:/presets.html。它只编辑本仓库的原生 PresetPackage, 不做 SillyTavern 兼容导入(导入仍在设置页);目标是把「改一版 → 看结构 → 看实际 messages → 多模型试跑 → 标记好坏」压在同一页里。

1. 页面分工

页面是「一条身份条 + 两个视图 + 两个抽屉」。编辑与试跑是两件不同任务:编辑要连续写,试跑要宽度做对照, 因此不挤在同一个视口里。

身份条    预设选择 / 新建 / 改名 / 脏状态 / 重新载入 / 试跑入口 / 版本树入口 / 保存两档 / 应用到对话页
          唯一脏状态、唯一 primary(应用到对话页)都在这里,任何视图都能看到。
          保存两档:「保存至当前版本」覆盖当前节点,「保存为新版本」追加子节点。
          版本 id 不上条(只做 title),要看序列去版本树。

编辑视图  左轨(240)发送顺序大纲:序号 1..N、启用开关、位置标记、错误红点;
          行可直接拖动改顺序(落点画一条强调色横线),标记洞也能拖——左轨顺序就是装配顺序;
          未加入顺序的模块折叠在底部;最后两栏是「生成参数」与「假历史」。
          中区  当前模块正文(一次只编辑一个;标记洞只显示说明,不能改名/删除,但能移顺序)。
          「假历史」中区是逐条编辑:每条一个角色下拉 + 一整句正文 + 加一句 / 删除 / 上下移。
          右栏(380)实际发出的 messages:顶部是结构检查状态带 + 用例选择 + 随时重算。
          改正文、切模块、切用例都会本地重算预览(不调模型)。

试跑视图  顶部运行栏一行:当前用例名 · 模型(下拉多选:勾选不关菜单,可全选可用 / 清空)
          · 已选条(只占一行,超出可左右拖,点一项取消) · 「运行此用例」。
          左侧用例列表(一条 = 一个用例,点开进大浮窗编辑),右侧全宽结果网格(同一用例 × 多模型对照)。
          低频入口:身份条上的「试跑 · 上次结果摘要」按钮,进入后按钮变成「← 编辑」。

抽屉      版本是右侧抽屉(版本树 / 说明 / 保存 / 载入 / 分叉);
          用例表单是**大浮窗**(左列 5 个字段,右列历史条目编辑器),字段多、历史要宽度,
          不再挤在窄侧栏里。两者都是低频或临时的,不常驻占内容宽度。

编辑视图一屏不滚:大纲、正文、预览各自滚动。预览在 ≤1180px 退成右侧浮层,≤860px 大纲也退成浮层。

2. 草稿、版本与应用

草稿         页面内存里的 state.preset;每次编辑只改这里。
重新载入     丢掉草稿,从 presets/<presetId>.json 重读;不动版本树。
保存至当前版本 覆盖当前节点(amend),不新增节点。
保存为新版本  把草稿作为子节点追加到版本树,当前节点移到新节点。
载入选中     把选中节点的完整快照载入草稿,并把当前节点切过去。
从选中分叉   用当前草稿在选中节点下建子节点,原分支不动。
应用到对话页  先保存为新版本,再写 presets/<presetId>.json;对话页从这里读。

版本节点保存的是完整 PresetPackage 快照,不是 diff;因此任意节点都可独立载入。 版本树文件:<data>/presets/versions/<presetId>.json。新建预设会同时落预设文件和根版本。

版本抽屉里是 git 式的树:竖线是分支,圆点是节点(实心 = 当前编辑位置),分叉另开一条线。 点任一节点弹菜单:「读取」把这个快照换进编辑器,「从它分叉」用当前草稿在它下面开新分支, 再往右是「复制 id」。底部四个按钮(保存为新版本 / 保存至当前版本 / 载入选中 / 从选中分叉)保留给键盘流。

两种保存别搞混:

保存为新版本(Ctrl+S)        在当前位置下追加子节点;历史留着,能回滚。
保存至当前版本(Ctrl+Shift+S)  直接覆盖当前节点,不产生新节点。适合“这条还没定,先改它”。
应用到对话页                  先保存为新版本,再覆盖 presets/<presetId>.json。对话页读的是这个文件。

装配只看 prompt_order.enabled;prompts[].enabled 是酒馆遗留字段。工作台的模块开关 写的是顺序开关,因此和真实发送一致。

2.1 左轨定死顺序

左轨顺序 = 装配顺序 = 真实发送顺序。

装配(src/preset/markers.ts: applyExtendedMarkers)只补缺不重排:
缺哪个洞就按锚点插进来(历史前 → 对话历史 → 历史后 → 本轮对话 → … → 本轮之后),
已经在 prompt_order 里的条目一个不动,作者拖到哪儿就发在哪儿。

右侧预览只是展示:同一条装配路径 + 预设的「消息整形」。整形会合并消息(`strict` 默认),
条数变少时预览元信息会写「整形 strict(装配 N 条 → 发出 M 条)」;
想逐条原样发,把「生成参数 → 消息整形」改成 `none`。

3. 结构检查

不调用模型,只查会在装配 / 发送时出问题的结构:

  • 顺序引用了不存在的模块、同一模块在顺序里重复、顺序号重复;
  • 未启用 chatHistory、模块为空、全角 {{;
  • 未闭合的 {{({{{...}} 的首个 { 是字面量,不算未闭合);
  • 未识别宏(会原样外发);
  • 标记位置重复启用。

error 只阻止「应用到对话页」,不阻止保存版本 / 预览;需要时可以强制写回。

4. 用例

一个用例就是一条记录:自己存、自己运行。页面上不出现「组」这个层级。

用例是全局的,不和预设绑定:一个用例只描述「要测的输入 + 上下文」,任何预设都能拿它去跑。 盘上是 <data>/presets/tests/global/*.json(沿用旧 TestCaseGroup 形状,每份用例一个只装它的组文件); 旧的按预设分目录存的(tests/<presetId>/*.json)会在第一次访问时并进 global,原文件不删。 删掉最后一份用例时组文件一并收掉。

「这个预设上次打开的是哪条用例」另外存:<data>/presets/lab-state/<presetId>.json, 记 selectedCaseId(运行视图当前用例)与 previewCaseId(右栏预览用例)。 写盘而不是塞浏览器缓存 —— 换浏览器、重开页面都还是同一条;切回别的预设又回到它自己那条。

用例列表的点击分工:点卡片 = 选中(设为当前用例),只有卡片右边的「编辑」才打开用例编辑浮窗。

一个用例只包含变动项:

用例名 / 角色名({{char}}) / 角色描述 / 世界书文本 / 用户输入 / 可选历史

历史是逐条编辑的:每条一个角色下拉(user / assistant)加一整句正文,正文里的换行就是真换行, 不再用「每行 user: 内容」的文本行和 \n 转义(旧写法会把不带头部的续行吞掉)。 落盘仍是 {role, content}[],装配时一条一个 message,接在预设假历史之后。 组装走线上同一条路径:assemblePlayWorkerMessages → shapeFramedMessages。

4.1 假历史与示例对话是两回事

假历史     预设级、真多轮(user / assistant),进「对话.历史」区,排在真实历史之前。
           模型会把它当成已经说过的话,会占轮次;编辑器里正文换行原样保留。
           存储:PresetPackage.pseudoHistory,装配见 src/preset/pseudo-history.ts。

示例对话   酒馆 mes_example(<START> 分块),发给模型时是 system + example_user /
           example_assistant,不占轮次,只做 few-shot;正文仍是整段文本,保持酒馆兼容。

两者互不覆盖:要「让模型以为前情已经发生」用假历史,要「示范写法但不冒充轮次」用示例对话。 三者(预设假历史 / 用例历史 / 直聊种子历史)用的是同一个控件:web/history-editor.js + web/history-editor.css。 每行左侧的 ⠿ 是拖动手柄:按住拖到另一行的上半 / 下半即插到它前面 / 后面,松手就到位; ↑↓ 按钮保留(精确微调 / 不用鼠标拖时)。

5. 预览与多模型运行

预览    不调用模型;显示当前草稿 + 当前用例实际发出的 messages。
运行    每个 用例 × 模型 发一次;默认最多 4 路并发、单次 180 秒超时。
        运行使用请求里的草稿,所以不必先保存版本。

运行记录:<data>/presets/runs/<presetId>/<runId>.json,保存 presetVersionId、每次运行的 messages 快照和每个模型的结果。结果不自动评分,人工标记成功 / 失败;标记会落盘。 历史运行可在结果卡上方切回,并展开「发送内容」核对实际请求。

6. HTTP 接口

前缀 /api/preset-lab/:

GET  presets                  预设列表
POST presets/create           新建原生预设 + 根版本
GET  preset?presetId=         读预设文件
POST preset/apply             应用到对话页({force:true} 跳过 error)
GET  versions?presetId=       版本树
POST versions/commit          追加节点(缺省父节点=当前;指定历史节点=分叉)
POST versions/amend           覆盖当前节点(保存至当前版本)
POST versions/checkout        切换当前节点
POST check                    结构检查
POST preview                  按用例组装 messages
POST run                      多模型运行(可带 preset 草稿;一次跑一个用例)
GET  cases                    使用例列表(全局,不看 presetId)
POST cases/save               存单份用例(带 groupId 就写回原文件)
POST cases/delete             删单份用例(空文件自动收掉)
GET  lab-state?presetId=      这个预设上次打开的用例
POST lab-state                写 { presetId, selectedCaseId?, previewCaseId? }
GET  groups                   旧接口:测试组列表
POST groups/save               旧接口:整组覆盖
POST groups/delete             旧接口:删组
GET  runs?presetId=           运行记录列表
GET  run?presetId=&runId=     单次运行
POST verdict                  修改人工标记
GET  profiles                 可选 API 配置

7. 已知边界

  • 版本树不做自动合并;分叉后各分支独立。
  • 运行是普通 HTTP 请求,页面会一直等到全部结果返回;没有逐条流式进度。
  • 「应用到对话页」只改预设文件;要让已有会话立刻使用,去设置页点「启用」。
  • 测试用例不会自动生成,也不会自动评分。