Files
writing-agent/docs/briefs/creation-round-cost.md
chenmoranzhiandCursor 1cc4ae557a 创作一轮改为先选题再填写,减少工具空转和重复覆盖。
上下文按规则、历史、产物分段拼装,便于吃提示缓存并核对费用。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 23:48:20 +08:00

85 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 创作轮次与前缀花费
> 状态:结论已记录。缓存粘滞已接入:本局 `sessionId` 在 OpenRouter 请求体写成 `session_id`。不手选节点导致的「先选再写」尚无做法。
> 记录:2026-10-01。用量取自本地 `~/.writing-agent/stats`,2026-09-20 起。
> 相关:`src/dictate/prompt.ts`、`src/dictate/turn.ts`、`src/dictate/read-module.ts`、`src/llm/client.ts`。
## 已记录的花费
9 月 20 日以来,OpenRouter 上的 token 主要在创作落盘的输入,不在游玩输出。
| 路径 | 请求 | 会话约 | prompt | 其中缓存 | 未缓存 | 输出 |
|---|---:|---:|---:|---:|---:|---:|
| 创作落盘 `dictate_agent` | 416 | 5 | 1198 万 | 782 万 | 417 万 | 38 万 |
| 游玩 `worker:world-simulator` | 28 | 2 | 77 万 | 0 | 77 万 | 7 万 |
输出大约是 prompt 的 3%–9%。输出单价更高,仍然要算,但排在输入后面。缓存命中仍按折扣价计,缩体积会同时降低全价和折扣价。
每轮花费可以看成:
```text
请求次数 ×(未缓存输入 × 全价 + 缓存输入 × 折扣价 + 输出 × 输出价)
```
创作这 416 次是最大的一块。大约五个会话,一句用户话后面跟着一串读和写。
## 缓存粘滞:必做
OpenRouter 请求体现由统计层带上本局 `sessionId`,字段名 `session_id`(最长 256 字)。直连其它渠道不写这个字段。有 `session_id` 后,粘滞从该会话第一次成功请求就开始,不再等缓存命中,也不再因为第一条 user 被重写而换上游。`provider.order` 仍不发送,它会关掉这层粘滞。
同一条前缀,落点稳定时能命中,换上游就是 0:
- 创作工具循环间隔大约 7–15 秒。Gemini 上同一轮里缓存从约 3.8k 爬到 7.6k、11.5k、15k,整段 prompt 大约 50%–68% 命中。burst 的第一下经常是 0。中间偶尔整段掉回 0,是换了上游。
- 游玩 world-simulator 在 OpenRouter 上 28 次全是 0。gemini-3.7 间隔中位数约 5.5 分钟,gemini-3.8 约 22 分钟,prompt 1.8 万–3.4 万。
- 直连 glm-5.3 的游玩,间隔 8–30 分钟仍常见 80%–91%。某一轮从约 11k 掉到 6656 并停住,说明前缀中段改过,只保住开头约 6.6k。
因此缓存粘滞是必做项。不传稳定的 `session_id`,OpenRouter 每一轮按新开头重新选上游,游玩前缀缓存等于没有。`provider.order` 会关掉这层粘滞,不能拿来代替 `session_id`。
上下文排序对游玩前缀是有效的:人设和静态产物在前,历史居中,变量和本轮输入在后。它只在同一上游上表现得出来。
## 未决:不手选节点,选和写至少两轮
用户不手动指定本轮写哪个能力。系统提示里只有能力目录和短说明(何时用、何时不用、档位)。`prompt.md` 里的任务、原则、产物规格、评分不在这条提示里。
模型选定能力后调用 `read_module`。程序把这些段落放进工具结果,并在 `运行.对话落盘.已读能力` 记下该能力当前版本。版本是这些段落的哈希。随后的 `insert` 要核对这个账:没读过,或磁盘上的提示已经改过,就拒绝写入。`self_score` 也按这次读到的评分维度检查。`narrative`、`variable-context`、`context-order`、`worker-spec`、`refine` 以及 `auto` 能力不走这道门,其余内容能力都走。库条目同理:目录只是摘要,正文要等 `read_library_entry` 回来。
读的那一轮,完整写法还不在上下文里,模型给不出合格正文。同一项能力至少是:
```text
第 1 次请求:凭短说明选定,并 read_module
第 2 次请求:看见任务 / 原则 / 规格 / 评分之后 insert
```
前缀发送两遍。第二遍还多带上那份写法。手选节点时,程序可以把该节点的完整写法放进唯一一次请求,模型当场写完。不手选时,第一次请求不能把所有节点的完整提示都放进去,这第二次省不掉。
一次只返回一个工具时,倍数再乘上去。提示要求一轮通常写 2~3 个已经说清的能力,模型常做成「读甲、写甲、读乙、写乙」,最后再单独回用户一句。每一拍都把已经变长的前缀再送一遍。「选和写拆成两次」是翻倍的原因;「每次只动一个节点」把两倍变成好几倍。
驱动程序已经会在同一步执行模型这一次返回的全部工具。接口没有禁止一轮多个 `insert`。日志里偶尔同轮两次写入。多数时候模型一次只吐一个工具。
结构化输出可以把一次生成拆成多份产物:一份 JSON 里多条 `{ position, content }`,程序再落盘,用户可见回复也可以放在同一份里。这省的是写入侧重复发送前缀。规格说明仍须在生成这批产物之前到达模型;否则模型是在没看见写法的情况下填内容。读和写如何合成更少的请求,本文不规定做法。
## 一本书上的拆账
本机作品 `4c13b2b4` 的当前会话(25 份产物、22 轮用户话)按现在的 `buildDictateMessages` 拼出来,不发请求。字数大约按 2 字 1 token 对照:同书末次 OpenRouter 请求 prompt 为 46897 token。
| 块 | 字数 | 占当前 messages |
|---|---:|---:|
| 已有产物合计 | 60518 | 69% |
| 全量对话 | 19975 | 23% |
| 系统提示(含方案进度 4392) | 6695 | 8% |
| 工具定义 JSON | 4917 | 另计,约等于系统提示 |
| 当前选用预设里启用的正文 | 362 | 可忽略 |
产物没有一份独自占满。最大的是 `设计.正文组成` 8879 字,其次变量规则 6005、实现机制 4569。其余多为两三千字,加在一起才是大头。会话中段的检查点(第 119 条消息之前)产物已经是 56102 字,和结尾同一量级。
按前面两段收窄再量同一份材料:
| 方案 | 字数 |
|---|---:|
| 选择:方案进度 + 用户最后一句(130 字) | 4522 |
| 填写若仍带全部产物 + 最近 4 条对话(1770 字) | 约 6.2 万 |
| 再注入 1 份节点提示(正文约 5 千字,叙事指南 5557) | 约 6.7 万 |
| 现在这一次请求的 messages | 88218 |
选择请求很小,目录也稳定,缓存几乎打在整段上。填写请求的大头仍是已有产物,不是对话,也不是刚插入的节点提示。砍掉全量对话大约省两成,改不了产物那七成。节点提示每份约 5 千字,一次写两三份会在产物之上再加一两成,但只发送一次。