侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 218 篇文章
  • 累计收到 7 条评论

工具结果占了请求体 99.7%,四轮之后 36 万字符

2026-10-20 / 0 评论 / 9 阅读

工具结果占了请求体 99.7%,四轮之后 36 万字符

先造一条工具结果。两千条构建步骤记录,八个字段,JSON.stringify 之后 304135 字符。

单条平均 152.1 字符,没有嵌套,也没有长文本字段。真实场景里带上日志片段或者 SQL 结果的工具,单条就能到这个数的几倍。

接着把这一个结果包进 messages。system 提示词 632 字符,user 那句请求 53 字符,assistant 里带 tool_call 的 245 字符,工具结果自己 360194 字符。

== 1 一条工具结果的字符数 ==
  记录条数                2000
  JSON.stringify 之后     304135 字符
  每条平均                152.1 字符
  末尾再挂一行报错之后    304220 字符
  JSON.parse 回读         成功
  单条记录样例            {"step":"build:package-0124","status":"ok","ms":31448,"exit":0,"owner":"ci-5","image":"node:20-alpine","note":"命中远端缓存,跳过安装","at":"2026-10-20T03:02:04Z"}
== 2 包进 messages 之后的请求体 ==
  system                       632 字符
  user                          53 字符
  assistant(带 tool_call)      245 字符
  tool(工具结果)           360194 字符
  messages 数组整体          361129 字符
  工具结果占请求体        99.7%
  再算上 tools 定义          361608 字符

有个差额值得停一下:工具结果本身 304135 字符,进了 messages 变成 360194,多出 56059 字符,18.4%。工具返回的文本里每个双引号,在请求体里都要写成反斜杠加引号,两个字符;这份 JSON 里正好有 56006 个双引号,多出来的绝大部分就是它们,剩下五十几个是消息外壳。工具结果越像结构化文本,这笔税越重,回灌一份 JSON 比回灌同样长度的无引号文本贵将近两成。

四轮之后,请求体是 36 万字符

把循环跑满四轮,每轮查 500 条,四个工具结果依次追加进 messages,请求体是这么涨的:

== 3 每多一轮工具调用,请求体怎么增长 ==
  每轮工具返回 500 条记录,单轮新增约 76053 字符,共 4 轮

  轮次    消息条数  本轮新增    请求体      相对第 1 轮   累计发送
  第 1 轮 4         90315 字符  91003 字符  1.00 倍       91003 字符
  第 2 轮 6         90331 字符  181334 字符 1.99 倍       272337 字符
  第 3 轮 8         90311 字符  271645 字符 2.99 倍       543982 字符
  第 4 轮 10        90332 字符  361977 字符 3.98 倍       905959 字符

  第 4 轮请求体是第 1 轮的 3.98 倍
  4 轮实际发出去的字符合计 905959 字符,是单轮最大请求体的 2.50 倍
  同一份 system 提示词被重发了 4 次,共 2528 字符

每轮新增 90315、90331、90311、90332 字符,几乎一模一样,因为每轮查回来的记录条数和字段都一样。请求体从 91003 涨到 361977,第四轮是第 1 轮的 3.98 倍。

单看某一轮,36 万字符还算能接受。看累计就变了:四轮真正发出去的字符是 905959,等于最大那一轮的 2.50 倍。这条曲线是平方的,请求体按轮数线性涨,累计按轮数的平方涨。

工具结果先截到 8000 字符再进 messages,四轮的形状一样,每一格小一个数量级:第 4 轮 39606 字符,累计 100049 字符。同样四轮,差 805910 字符。

截到 8000 字符,两千条记录剩 52 条

三种处理放在一起,比的是字符数和留下来的东西:

== 4 三种处理的字符数与保留信息 ==
  原始工具结果 304220 字符,其中记录 2000 条,末尾一行是报错

  策略                      字符数      占原样    完整记录    末条记录    末尾报错行
  原样回灌                  304220 字符 100.0%    2000/2000   在          在
  截到前 8000 字符          8000 字符   2.6%      52/2000     没了        没了
  头 4000 加尾 4000         8020 字符   2.6%      51/2000     在          在

按字符截到 8000,字符数是原样的 2.6%,能完整读出来的记录 52 条。头尾各留 4000,字符数 8020,完整记录 51 条,末条记录和末尾那行报错都还在。

两种的字符数只差 20 个,留下的东西完全不同。那 20 个字符是「中间省略 296220 字符」这行标记。它不改变请求体的大小,但它让工具结果自己说了一句少了一截。

截到前 8000 的那份没有这句话。文本停在第 8000 个字符,落在第 53 条记录的字段名中间,后面就是结尾。

报错行在最后一行

上面那个工具结果末尾还挂着一行报错。真正难看的是日志:201930 字符,2401 行,10 条 ERROR,最后一行是 exit=137 的 OOM。

== 5 报错行还在不在 ==
  构建日志 201930 字符,2401 行
  其中 ERROR 行 10 条,WARN 行 24 条,最后一行是致命报错

  策略                      字符数      行数    保住 ERROR  保住 WARN   第 60 行那条  致命行
  原样回灌                  201930 字符 2401    10/10       24/24       在            在
  截到前 8000 字符          8000 字符   95      1/10        0/24        在            没了
  头 4000 加尾 4000         8020 字符   96      1/10        0/24        没了          在

  头 4000 加尾 4000 保留 96 行,丢掉 2305 行,96.0% 的字符
  截到前 8000 字符保住的是最早那条 ERROR,致命那条在第 2401 行
  同为 8000 字符预算,两种截法保住的 ERROR 行重合 0 条

原样回灌 10 条 ERROR 全在。截到前 8000 字符只剩 1 条,是第 60 行那次超时重试,也就是整份日志里最早的一次失败。头尾各 4000 也只剩 1 条,是最后那行 OOM。两种截法的预算一样,保住的 ERROR 行重合 0 条。

这一条是我这轮改主意的地方。原本觉得头尾截法是给 JSON 补结构用的,测下来它在日志上更值钱:一份流水账的结论通常写在最后,而前面的几千行 INFO 只是过程。

前 8000 字符保住的是「最早出过什么事」,头尾保住的是「最后为什么停下」。这两句话在排障里不等价。第 60 行那次超时重试离末行两千三百多行,它说明不了进程为什么被杀;原因在最后一行,而那一行不在前 8000 个字符里。

代价也摆在表里:头尾版本丢掉中间 2305 行,96.0% 的字符,只在切口处留一行字。截到前 8000 的那份连切口都没有。

本机这轮能确定的只有两件事:请求体多大,以及文本里还剩哪些行。模型看到只有 52 条的 JSON 会怎么回答,看到没有尾巴的日志会不会自己意识到少了东西,这一层没有模型接口可以调,没实测。所以这些取舍只按「信息在不在」来定,不按「模型能不能读懂」来定。

错误栈、构建日志这类结论落在末尾的结果,留头留尾;一条一条同构、前面的记录代表性够用的,截前面就行。两种都该在返回值里写一句少了多少,字符成本是二十个字,换回来的是下游知道自己在看残本。

两千条记录和两千多行日志都是按固定规则生成的,字符数用 length 直接数,没有估算。

    🤞 分享