<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>agent — mizore / notes</title>
    <link>https://mizore.blog/tags/agent/</link>
    <link xmlns="http://www.w3.org/2005/Atom" href="https://mizore.blog/tags/agent/feed.xml" rel="self" type="application/rss+xml"></link>
    <description>1 篇</description>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 05 Nov 2026 00:00:00 +0000</lastBuildDate>
    <item>
      <title>省一点，再省一点</title>
      <link>https://mizore.blog/posts/save-a-little-more/</link>
      <description>&lt;p&gt;我们群里住着两个 bot。一个是我写的 &lt;a href=&#34;https://github.com/mizorewww/pi-extension-telegram-agent&#34;&gt;Pi Telegram Agent&lt;/a&gt;，一个是群友 tcdw（碗）写的&lt;a href=&#34;https://github.com/tuuject/surowan&#34;&gt;塑料碗&lt;/a&gt;。两个项目没有共享一行代码，功能、规模、写法都不一样，但凑到一起聊设计的时候，我们总在说同一句话：&lt;strong&gt;省一点，再省一点。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这篇文章是我的 bot 三个月生产账本的复盘，也是两种设计的对照。&lt;/p&gt;&#xA;&lt;h2 id=&#34;tldr&#34;&gt;TL;DR&lt;/h2&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;token 是唯一的货币。&lt;/strong&gt; 成本是 token，上下文是 token，代码在 coding agent 眼里也是 token。三件事是同一笔账。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;账单几乎全是“读”。&lt;/strong&gt; 三个月 74,644 次模型调用，输入是输出的 374 倍；每次请求里将近九成是聊天历史，工具 schema 只占 1.1%。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;少调用。&lt;/strong&gt; 被点名就回，这不需要判断；其余消息要不要接，交给一个只看最近几行的小判断模型，每次约 1,650 token、$0.000033，不进入 bot 的上下文。&lt;code&gt;send&lt;/code&gt; 成功即结束本轮，平均每轮 1.13 次调用。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;命中缓存。&lt;/strong&gt; 前缀只追加、永不改写，动态信息有自己固定的位置，golden hash 测试守住每一个字节。DeepSeek 上实测命中率 98.7%，同样的输入量，费用是不用缓存的 1/30。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;上下文短而干净。&lt;/strong&gt; 生产用 128K 窗口：API 按 token 线性计费，窗口再小只会换来更多压缩调用和冷缓存。不做记忆系统，原文全部保留，bot 不懂就自己去查。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;省下来的花在体验上。&lt;/strong&gt; codemode 让 bot 先说“我去查一下”再并行干活，群友觉得它快；一个隔离好的 Linux 容器，让模型有了完全通用的能力，而我不用为每个能力写插件。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;代码也要省。&lt;/strong&gt; 把会话、压缩、恢复、执行、看图全部交给 Pi，&lt;code&gt;src/&lt;/code&gt; 从 17,500 行砍到 8,100 行，功能反而更多。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;和塑料碗。&lt;/strong&gt; 它是“通用的产品，克制的 agent”，我是“专用的部署，通用的 agent”。我认为后者在群聊里更好，但也有好几处是碗做得更对。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;下面一节一节展开。&lt;/p&gt;&#xA;&lt;h2 id=&#34;账本钱花在了哪&#34;&gt;账本：钱花在了哪&lt;/h2&gt;&#xA;&lt;p&gt;我的 bot 从 8 月 7 日上线，到 11 月 4 日换成新的部署方式之前，每一次模型调用都记了一行遥测：输入多少、命中缓存多少、输出多少、请求里各段各占多少。那时我的部署里跑着两个 bot，住在同一个群里，三个月的总账是这样的：&lt;/p&gt;&#xA;&lt;figure class=&#34;stats&#34;&gt;&lt;figcaption&gt;&lt;span&gt;三个月的账本&lt;/span&gt;&lt;span class=&#34;key&#34;&gt;2026-08-07 — 2026-11-04 · 两个 bot · 一个群&lt;/span&gt;&lt;/figcaption&gt;&lt;p class=&#34;stats-group&#34;&gt;&lt;b&gt;规模&lt;/b&gt;&lt;/p&gt;&lt;dl class=&#34;kpi&#34;&gt;&lt;div&gt;&lt;dt&gt;群友消息&lt;/dt&gt;&lt;dd&gt;144,148&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;路由开始记录后的人类消息&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;模型调用&lt;/dt&gt;&lt;dd&gt;74,644&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;含 11,544 次压缩调用&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;公开发言&lt;/dt&gt;&lt;dd&gt;39,615&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;send 次数&lt;/dd&gt;&lt;/div&gt;&lt;/dl&gt;&lt;p class=&#34;stats-group&#34;&gt;&lt;b&gt;Token&lt;/b&gt;&lt;/p&gt;&lt;dl class=&#34;kpi&#34;&gt;&lt;div&gt;&lt;dt&gt;输入&lt;/dt&gt;&lt;dd&gt;5.34B&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;其中缓存读取 3.50B&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;输出&lt;/dt&gt;&lt;dd&gt;14.3M&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;输入是输出的 374 倍&lt;/dd&gt;&lt;/div&gt;&lt;div&gt;&lt;dt&gt;每轮调用&lt;/dt&gt;&lt;dd&gt;1.13&lt;/dd&gt;&lt;dd class=&#34;s&#34;&gt;非压缩调用 ÷ 开启的轮次&lt;/dd&gt;&lt;/div&gt;&lt;/dl&gt;&lt;p class=&#34;data-note&#34;&gt;输入 token 含命中缓存的部分。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;输入是输出的 374 倍。群友说一句话，模型要把 system prompt、工具 schema 和整段聊天历史从头读一遍，才能决定回什么；而它回出去的往往只有一两句话。群聊 bot 的账单几乎全是“读”。&lt;/p&gt;&#xA;&lt;p&gt;读的都是什么？四万多次请求记了分段大小，平均下来是这样的，旁边放着我之前改造 &lt;a href=&#34;/posts/making-memoh-cheaper-on-telegram/&#34;&gt;Memoh&lt;/a&gt; 时抓到的一次请求作对照：&lt;/p&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;一次请求里都装了什么&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;部分&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;本项目&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;Memoh 改造前&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;聊天历史&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:100.0%&#34;&gt;86.6%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:78.5%&#34;&gt;68.0%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;system prompt&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:11.8%&#34;&gt;10.2%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:12.4%&#34;&gt;10.7%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;压缩摘要&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:2.4%&#34;&gt;2.1%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:0.0%&#34;&gt;0%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;工具 schema&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:1.3%&#34;&gt;1.1%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:24.6%&#34;&gt;21.3%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;本项目：40,593 次请求的平均值，平均每次 93,067 token。Memoh：改造前抓的单次请求，按字节计。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;这张表决定了后面所有的优先级：&lt;strong&gt;真正的大头是上下文有多长，以及它能不能命中缓存。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;成本可以写成：&lt;/p&gt;&#xA;&lt;div class=&#34;math&#34; tabindex=&#34;0&#34;&gt;&lt;math display=&#34;block&#34; style=&#34;font-feature-settings: &#39;dtls&#39; off;&#34; xmlns=&#34;http://www.w3.org/1998/Math/MathML&#34;&gt;&lt;semantics&gt;&lt;mrow&gt;&lt;mi&gt;C&lt;/mi&gt;&lt;mo&gt;=&lt;/mo&gt;&lt;mi&gt;N&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mi&gt;T&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mrow&gt;&lt;mo fence=&#34;true&#34; form=&#34;prefix&#34; stretchy=&#34;true&#34;&gt;[&lt;/mo&gt;&lt;mo form=&#34;prefix&#34; stretchy=&#34;false&#34;&gt;(&lt;/mo&gt;&lt;mn&gt;1&lt;/mn&gt;&lt;mo&gt;−&lt;/mo&gt;&lt;mi&gt;h&lt;/mi&gt;&lt;mo form=&#34;postfix&#34; stretchy=&#34;false&#34;&gt;)&lt;/mo&gt;&lt;mo&gt;+&lt;/mo&gt;&lt;mi&gt;h&lt;/mi&gt;&lt;mo&gt;×&lt;/mo&gt;&lt;mi&gt;r&lt;/mi&gt;&lt;mo fence=&#34;true&#34; form=&#34;postfix&#34; stretchy=&#34;true&#34;&gt;]&lt;/mo&gt;&lt;/mrow&gt;&lt;/mrow&gt;&lt;annotation encoding=&#34;application/x-tex&#34;&gt;C = N \times T \times \left[(1-h) + h \times r\right]&lt;/annotation&gt;&lt;/semantics&gt;&lt;/math&gt;&lt;/div&gt;&#xA;&lt;p&gt;&lt;code&gt;N&lt;/code&gt; 是调用次数，&lt;code&gt;T&lt;/code&gt; 是每次读的 token，&lt;code&gt;h&lt;/code&gt; 是缓存命中率，&lt;code&gt;r&lt;/code&gt; 是缓存价和原价的比（DeepSeek 是 1/50，Claude 是 1/10）。能动的只有三个地方：少调几次（&lt;code&gt;N&lt;/code&gt;），读过的尽量命中缓存（&lt;code&gt;h&lt;/code&gt;），每次少读一点（&lt;code&gt;T&lt;/code&gt;）。接下来三节按这个顺序讲。&lt;/p&gt;&#xA;&lt;h2 id=&#34;少调用别为什么都不做付钱&#34;&gt;少调用：别为“什么都不做”付钱&lt;/h2&gt;&#xA;&lt;p&gt;最便宜的调用是不发生的调用。先看三个月里消息到调用的漏斗：&lt;/p&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;从群友消息到模型调用&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;阶段&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;数量&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;群友消息&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:100.0%&#34;&gt;144,148&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;bot 正忙，消息并入下一轮&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:35.1%&#34;&gt;50,532&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;开启一轮&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:38.7%&#34;&gt;55,776&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;模型调用&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:43.8%&#34;&gt;63,100&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;公开发言&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:27.5%&#34;&gt;39,615&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;开启的轮次 = 路由判定为 started 的消息数；模型调用不含压缩。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;漏斗里最刺眼的是最后一行：十四万条群友消息，bot 公开说话不到四万次。群里绝大多数消息本来就和 bot 无关，而开了一轮以后，也有一万六千轮是模型读完选择了沉默。所以这一节要回答两个问题：哪些消息值得叫醒主模型；叫醒以后，怎么让这一轮尽快结束。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点要不要接话交给一个只看几行的小模型&#34;&gt;亮点：要不要接话，交给一个只看几行的小模型&lt;/h3&gt;&#xA;&lt;p&gt;“这条消息我要不要接”听起来是一个问题，拆开看其实是两个。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第一个问题有确定的答案。&lt;/strong&gt; 被 @、被回复、被叫名字，就必须回。这里不需要任何判断，一行规则就够了，也不该让任何模型来否决它。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;真正难的是剩下的消息。&lt;/strong&gt; 它们需要的是判断，那就先问：做这个判断，到底需要知道什么？&lt;/p&gt;&#xA;&lt;p&gt;想一想一个懂分寸的群友是怎么决定插不插话的：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;有人向全群提了个问题，还没人回答；&lt;/li&gt;&#xA;&lt;li&gt;有人在接着他刚才的话追问；&lt;/li&gt;&#xA;&lt;li&gt;两个人在约饭，问的是特定的人；&lt;/li&gt;&#xA;&lt;li&gt;别的 bot 已经把问题答完了；&lt;/li&gt;&#xA;&lt;li&gt;只有几个 bot 在互相接话。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些判断全都只取决于&lt;strong&gt;最近几行消息&lt;/strong&gt;。它不需要 bot 的人设，不需要三个月的历史，也不需要工具。&lt;/p&gt;&#xA;&lt;p&gt;这样一想，两种常见做法的问题就很清楚了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;交给主模型。&lt;/strong&gt; 它当然能判断，但它每一次被叫醒，都带着九万多 token 的上下文。问它一句“要不要说话”，答案多半是“不用”，于是每一次“不说”都付了全价。而上面的漏斗告诉我们，“不说”才是常态，这笔钱会被乘上十几万次。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;交给规则。&lt;/strong&gt; 概率、关键词、时间段，都不花钱，但规则读不懂群里在聊什么：严肃的话题照样插一句，没人回答的问题照样装没看见。&lt;/p&gt;&#xA;&lt;p&gt;我们要的东西，介于两者之间：&lt;strong&gt;能读懂，但只读几行；要一个判断，不要一段话。&lt;/strong&gt; 顺着这几个条件，它的形状就出来了：一个分类模型，输入是最近的消息切片，输出是一个概率。&lt;/p&gt;&#xA;&lt;p&gt;我选的是 OpenRouter 上的 &lt;a href=&#34;https://openrouter.ai/perplexity/pplx-decider-v1.2&#34;&gt;pplx-decider-v1.2&lt;/a&gt;。它不生成文字，读一段 &lt;code&gt;state&lt;/code&gt;，回答预先定义好的问题，返回每个问题的概率。我只问它一件事：bot 现在要不要说话。它便宜在三个地方：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;只看切片。&lt;/strong&gt; 它读的是这次新到的几行消息，加上前面六行作上下文，渲染成和主模型一样的文本行；人设、历史、工具一概不给。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;不进上下文。&lt;/strong&gt; 判断是一个独立的请求，结果只是一个数。主模型的前缀一个字节都没动，缓存照常命中。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;小。&lt;/strong&gt; 每次判断约 1,650 token（其中大部分是固定的说明和例子），$0.000033，不到一秒。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;判断一次“要不要接话”的代价&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;输入 token&lt;/th&gt;&lt;th&gt;每千次费用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;主模型读完整上下文&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:100.0%&#34;&gt;93,067&lt;/td&gt;&lt;td&gt;取决于主模型&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;判断模型只读切片&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:1.8%&#34;&gt;1,650&lt;/td&gt;&lt;td&gt;$0.033&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;主模型一列取账本里的平均请求；判断模型一列是带上全部例子后的实测。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;按三个月 14.4 万条群友消息算，就算每一条都判断一次，总共也只要约 $4.7。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;小模型的代价是不够聪明，那就补上它缺的东西：例子。&lt;/strong&gt; 我让一个 subagent 去读真实的群聊记录：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;先标注 150 个真实切片，留出 50 个做测试；&lt;/li&gt;&#xA;&lt;li&gt;再凭空写出 14 条通用的例子，覆盖上面那几类情况。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;结果是：&lt;/p&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;加不加例子，判断模型的表现&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;配置&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;AUC&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;准确率&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;精确率&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;召回&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;只有说明，阈值 0.51&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.88&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.84&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;1.00&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.53&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;说明加 14 条例子，阈值 0.45&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.92&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.82&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.75&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;0.71&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;50 个留出切片，“该接话”为正类；测试集小，每个数字约有 ±0.05–0.1 的误差。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;不加例子时，模型太保守，一半该接的话都漏掉了。加了例子以后，召回从 0.53 升到 0.71。&lt;/p&gt;&#xA;&lt;p&gt;这些例子用 &lt;code&gt;{name}&lt;/code&gt; 代替 bot 的名字，和提示词一起写在 &lt;code&gt;src/agent/decider.ts&lt;/code&gt; 里，随代码发布。它们是“判断要不要接话”这一块自己的一部分，部署的人不需要另外去写。&lt;/p&gt;&#xA;&lt;p&gt;剩下几条规则，也都是从同一个问题推出来的：这个判断还需要知道什么，又不该知道什么？&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;别的 bot 的发言也交给它判断，而且永远不算“点名”。&lt;/strong&gt; 群里住着十几个 bot，谁 @ 谁都可能形成循环。所以“只有 bot 在互相接话，就不要加入”专门写成了一条例子。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;忙的时候不判断。&lt;/strong&gt; bot 正在回话或在冷却时，新消息先排队；这一轮结束、冷却过去，再对攒下的消息判断一次。判断期间又来了消息，就再补一次。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;判断失败就当作要接。&lt;/strong&gt; 这和“没配置判断模型就每条都接”是同一个答案：宁可话多，也不装死。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;调用走 Pi。&lt;/strong&gt; Pi 1.5 原生支持这类分类模型，它的 OpenRouter 模型目录里就登记着这个模型，认证也存在 Pi 里。我一行 HTTP 都没写。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h3 id=&#34;亮点发完就下班结果未知就不重发&#34;&gt;亮点：发完就下班，结果未知就不重发&lt;/h3&gt;&#xA;&lt;p&gt;叫醒模型之后，第二个问题是“它说的话怎么发出去”。&lt;/p&gt;&#xA;&lt;p&gt;最省事的做法是把模型的文本输出直接发到群里。但这样模型就没法“想一想再决定不说”，思考过程、自言自语、对工具结果的复述都可能漏到群里。所以两个 bot 都规定：&lt;strong&gt;普通 assistant 文本永远不会发到群里，只有调用 &lt;code&gt;send&lt;/code&gt; 工具才会公开发言。&lt;/strong&gt; 漏斗里开启的轮次比公开发言多了一万六千次，大致就是模型读完之后选择沉默的轮数。&lt;/p&gt;&#xA;&lt;p&gt;有了 &lt;code&gt;send&lt;/code&gt;，下一个浪费就出现了：通用 agent 循环里，工具调用完要把结果交还给模型，模型再读一遍全部上下文，说“好的，已经发了”。在群聊里这一次调用纯属浪费。我的 &lt;code&gt;send&lt;/code&gt; 单独调用时用 Pi 原生的 terminate 直接结束本轮，不再回到模型；被点名却忘了 &lt;code&gt;send&lt;/code&gt; 的轮次，最多补一次提示。这就是平均 1.13 次调用的来源。&lt;/p&gt;&#xA;&lt;p&gt;最微妙的是发送失败。网络超时的时候，你不知道 Telegram 到底收没收到这条消息。常见做法是重试，但 Telegram 的发送不可回滚：一旦服务端已经创建了消息，重试就会让群里出现两条一模一样的话。我的规则是：&lt;strong&gt;只有确定性的 4xx 错误、而且还没有任何部分提交时，才把错误交还给模型重试；其余一律当作“结果未知”，不重发。&lt;/strong&gt; 宁可偶尔漏一句，也不在群里复读。崩溃恢复也一样：Pi 恢复中断的任务时，结果未知的发送保持 &lt;code&gt;no_retry&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;命中缓存前缀一个字节都不能动&#34;&gt;命中缓存：前缀一个字节都不能动&lt;/h2&gt;&#xA;&lt;p&gt;各家的 prompt caching 都是前缀匹配：请求的前缀逐字节一致才能命中，中间任何一个字节变了，从那里往后全部作废。缓存价是原价的 1/10 到 1/50，所以命中率是整本账里杠杆最长的一项。拿 DeepSeek V4 Flash 的官方价（命中 $0.0028、未命中 $0.14 每百万 token）去套我三个月的输入量：&lt;/p&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;同样 53.4 亿输入 token，按 DeepSeek V4 Flash 计价&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;缓存命中率&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;输入费用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0%（每次全价重读）&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:100.0%&#34;&gt;$748&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;85%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:16.7%&#34;&gt;$125&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;98.7%&lt;/td&gt;&lt;td class=&#34;num bar&#34; style=&#34;--a:0.0%;--b:3.3%&#34;&gt;$25&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;只算输入；输出 1,430 万 token 另计。这是假设性换算，不是我的实际账单。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;从 85% 到 98.7%，看起来只差十几个百分点，费用差了五倍。98.7% 是我的 bot 在 DeepSeek 直连上的实测值：它的自动前缀缓存只要前缀不变就命中，最能说明请求结构本身做得怎么样。&lt;/p&gt;&#xA;&lt;p&gt;所以我在 &lt;code&gt;AGENTS.md&lt;/code&gt; 里写的第一条硬约束是：&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;永不改写已存在的 provider prefix；动态内容只以新 suffix 追加。每个请求必须是下一请求的严格前缀。&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h3 id=&#34;亮点动态信息该放在哪&#34;&gt;亮点：动态信息该放在哪&lt;/h3&gt;&#xA;&lt;p&gt;“严格前缀”说起来简单，真正考验它的是那些&lt;strong&gt;每一批都不一样&lt;/strong&gt;的信息。sticker 就是最典型的一个。&lt;/p&gt;&#xA;&lt;p&gt;bot 有一个固定的 sticker 目录，但群友也会发目录以外的 sticker，bot 收到以后其实也能发同一张。所以每一批消息里，如果出现了这样的 sticker，我想告诉模型：“这几张你也可以用。”这是一段动态信息。问题是，它该放在请求的哪里？&lt;/p&gt;&#xA;&lt;p&gt;一条一条看条件，答案会自己收窄。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第一个条件：不能改写已经发过的前缀。&lt;/strong&gt; 这一条排除了 system prompt。它在请求的最前面，只要动一个字节，后面九万 token 的缓存全部作废。&lt;/p&gt;&#xA;&lt;p&gt;同样被排除的还有一个看起来最省的做法：只在最后一条消息后面挂一份“最新的候选列表”，下一批来了就换掉。它每次都只带一份列表，可上一次请求的结尾，在下一次请求里消失了，前缀就断在那里。我在线上量到过这件事的代价：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;同一轮里的后续调用，平均只漏缓存约 780 token；&lt;/li&gt;&#xA;&lt;li&gt;每个新 turn 却要漏掉约 13,600 token，哪怕两条消息只隔三十秒，真正新增的内容只有约 1,000 token。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为了省几十个 token 的列表，每轮多付一万多。&lt;/p&gt;&#xA;&lt;p&gt;所以动态信息只能&lt;strong&gt;追加&lt;/strong&gt;，而且追加之后&lt;strong&gt;永远留在原位&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第二个条件：不能被当成任何人说的话。&lt;/strong&gt; 既然只能追加，最自然的做法是把列表拼进那批消息的正文里。可模型读到的就会是“最后一个群友贴了一份 sticker 列表”。我遇到过这种情况：bot 反复说群友“泄露了它的 sticker 列表”。&lt;/p&gt;&#xA;&lt;p&gt;所以它必须是一条&lt;strong&gt;独立的消息&lt;/strong&gt;，有自己的标签〔系统附注〕，并且 system prompt 要明确说明：这类消息不是任何群友发的，群里也没人看得到。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第三个条件：不能进入记忆。&lt;/strong&gt; 上下文满了以后会被压缩成摘要。如果附注写进了会话记录，压缩器就会把它当成聊天内容来概括。前面那件事更糟的一半就出在这里：摘要把“群友泄露了列表”这句推测写成了事实，之后每一次压缩都继承它，最后只能开新会话。&lt;/p&gt;&#xA;&lt;p&gt;所以附注&lt;strong&gt;只能出现在发给模型的请求里，不能写进持久化的会话记录&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;三个条件叠在一起，答案就只剩一个形状：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;附注作为一条独立的〔系统附注〕消息，紧跟在它那一批消息后面；&lt;/li&gt;&#xA;&lt;li&gt;写下以后永远留在那个位置，之后每一次请求都原样带着它；&lt;/li&gt;&#xA;&lt;li&gt;它存在那批输入 entry 的元数据里，每次请求前由 Pi 原生的 &lt;code&gt;beforeRequest&lt;/code&gt; 钩子投影进去；&lt;/li&gt;&#xA;&lt;li&gt;压缩器读的是持久化的字节，根本看不到它。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;figure class=&#34;code&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;cm&#34;&gt;/** A batch&amp;#39;s note is rendered once and stays at that position for the life of the context. */&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;span class=&#34;code-tools&#34;&gt;&lt;span class=&#34;lang&#34;&gt;ts&lt;/span&gt;&lt;button class=&#34;copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&lt;/span&gt;&lt;/figure&gt;&#xA;&lt;p&gt;一次请求最后长这样（示意）：&lt;/p&gt;&#xA;&lt;figure class=&#34;code&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;system     …群聊协议、工具规则、人设…&#xA;           # Sticker 目录&#xA;           s3: 😺 得意的赞同，中等强度&#xA;           s4: 😭 委屈到哭，夸张&#xA;           …（共 120 行）&#xA;user       [21:03:11] #18452 群友 A (@a): 这也太离谱了&#xA;           [21:03:15] #18453 群友 B (@b): [sticker 😂]&#xA;user       〔系统附注〕这批消息里出现了你也能发送的 sticker：&#xA;           s2091: 😂&#xA;assistant  send({ message: &amp;#34;…&amp;#34;, sticker: &amp;#34;s4&amp;#34; })&#xA;user       [media_update #18453] [sticker: 笑到捶地，极度夸张]&#xA;           [21:04:02] #18460 群友 A (@a): …&lt;/code&gt;&lt;/pre&gt;&lt;span class=&#34;code-tools&#34;&gt;&lt;button class=&#34;copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&lt;/span&gt;&lt;/figure&gt;&#xA;&lt;p&gt;每一次请求，都是上一次请求原样加上新的内容：模型这一轮的输出、新的一批消息、这一批的附注（如果有）。旧消息不会重复出现，旧附注也不会被拿掉或改写。&lt;/p&gt;&#xA;&lt;p&gt;同样的三个条件，也决定了其他动态信息的位置：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;固定的 sticker 目录&lt;/strong&gt;是稳定的，所以可以放进 system prompt 的最后。它渲染一次以后就和会话一起冻结，只在会话本来就要重开的时候才刷新，免得后来补上的描述悄悄改掉前缀。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;媒体描述&lt;/strong&gt;是晚到的：一张图片刚进来时，消息里只有一个 &lt;code&gt;[图片]&lt;/code&gt; 占位符。描述生成好以后，不回头改那条消息，而是在之后的输入里追加一行 &lt;code&gt;[media_update #id] [图片: …]&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;发送那一侧也一样：模型只写短 ID，&lt;code&gt;send&lt;/code&gt; 在发出任何东西之前，先把它解析成这个 bot 自己的 Telegram &lt;code&gt;file_id&lt;/code&gt;。解析不了就直接把错误交还给模型，绝不会发到一半才失败。&lt;/p&gt;&#xA;&lt;p&gt;把这一节压成一句话就是：&lt;strong&gt;动态信息要有自己固定的位置，而且只出现在请求里，不进入会话记录。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;塑料碗把同一条规则做成了 system prompt 的结构：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;稳定段&lt;/strong&gt;是按固定顺序拼接的协议、索引和人格，对一个 Conversation 逐字节不变；&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;注入段&lt;/strong&gt;装着所有随时间变化的东西：当前时间、短期记忆的增量、睡眠状态、任务回执。它作为每批一条新的 user 消息追加。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;sticker 目录在它那里算不可信数据，随批注入。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点让测试守住字节&#34;&gt;亮点：让测试守住字节&lt;/h3&gt;&#xA;&lt;p&gt;光靠自觉守不住这条线。一次重构、一次“顺手改个措辞”，前缀就断了，而且不会报任何错，只会在账单上体现出来。&lt;/p&gt;&#xA;&lt;p&gt;所以我的 &lt;code&gt;test/cache.test.ts&lt;/code&gt; 对所有会进入前缀的东西做 golden hash：system prompt 模板、每种工具 schema、codemode 的声明段、消息序列化的输出。任何一个字节变了，测试就红。要让它重新变绿只有两条路：改回去，或者 bump &lt;code&gt;CACHE_SCHEMA_VERSION&lt;/code&gt; 并更新 golden，承认这是一次有意的协议变更。这个版本号现在是 52。&lt;/p&gt;&#xA;&lt;p&gt;这个测试守的不是功能，是钱。塑料碗用的是 &lt;code&gt;system_prompt_hash&lt;/code&gt;：哈希一变就丢掉旧 Context 重建，绝不让新旧前缀混在一起。&lt;/p&gt;&#xA;&lt;h2 id=&#34;上下文短而干净需要时再取&#34;&gt;上下文：短而干净，需要时再取&lt;/h2&gt;&#xA;&lt;p&gt;账本的第一张表说了，九成的输入是聊天历史。所以剩下的问题是 &lt;code&gt;T&lt;/code&gt;：上下文该留多长，旧的东西怎么忘，忘了的东西怎么找回来。&lt;/p&gt;&#xA;&lt;p&gt;这周刚好读到两篇论文，一篇讲上下文长度，一篇讲记忆，正好对应这两个问题。&lt;/p&gt;&#xA;&lt;h3 id=&#34;窗口该开多大&#34;&gt;窗口该开多大&lt;/h3&gt;&#xA;&lt;p&gt;NVIDIA 的 &lt;a href=&#34;https://arxiv.org/abs/2611.05306&#34;&gt;You don&#39;t need long context&lt;/a&gt; 先算了一笔服务端的账：attention 模型生成每个 token 的代价随上下文深度线性增长。他们在 vLLM 上开着 prefix caching 实测，把服务窗口从 16K 开到 256K，每个生成 token 的成本：&lt;/p&gt;&#xA;&lt;figure class=&#34;chart&#34;&gt;&lt;figcaption&gt;&lt;span&gt;256K 窗口相对 16K 的每 token 生成成本&lt;/span&gt;&lt;/figcaption&gt;&lt;div class=&#34;plot&#34; data-tip=&#34;{&amp;#34;x&amp;#34;:[22.79,52.71,82.63],&amp;#34;l&amp;#34;:[&amp;#34;GLM 5.2&amp;#34;,&amp;#34;Kimi K2.6&amp;#34;,&amp;#34;Qwen 3 Coder Next&amp;#34;],&amp;#34;s&amp;#34;:[{&amp;#34;n&amp;#34;:&amp;#34;倍数&amp;#34;,&amp;#34;c&amp;#34;:&amp;#34;var(--green)&amp;#34;,&amp;#34;v&amp;#34;:[&amp;#34;3.6&amp;#34;,&amp;#34;5.6&amp;#34;,&amp;#34;7.0&amp;#34;]}]}&#34; style=&#34;--h:274&#34;&gt;&lt;svg viewBox=&#34;0 0 664 274&#34; preserveAspectRatio=&#34;none&#34; role=&#34;img&#34; aria-label=&#34;256K 窗口相对 16K 的每 token 生成成本&#34;&gt;&lt;path d=&#34;M52 190H648M52 130H648M52 70H648M52 10H648&#34; fill=&#34;none&#34; style=&#34;stroke:var(--grid)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;path d=&#34;M53 142h196.7V250h-196.7ZM251.7 82h196.7V250h-196.7ZM450.3 40h196.7V250h-196.7Z&#34; style=&#34;fill:var(--green)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;path d=&#34;M52 250H648&#34; fill=&#34;none&#34; style=&#34;stroke:var(--faint)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;/svg&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:91.241%;width:6.627%&#34;&gt;0.0×&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:69.343%;width:6.627%&#34;&gt;2.0×&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:47.445%;width:6.627%&#34;&gt;4.0×&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:25.547%;width:6.627%&#34;&gt;6.0×&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:3.650%;width:6.627%&#34;&gt;8.0×&lt;/span&gt;&lt;span class=&#34;xl&#34; style=&#34;left:22.791%;top:96.350%&#34;&gt;GLM 5.2&lt;/span&gt;&lt;span class=&#34;xl&#34; style=&#34;left:52.711%;top:96.350%&#34;&gt;Kimi K2.6&lt;/span&gt;&lt;span class=&#34;xl&#34; style=&#34;left:82.631%;top:96.350%&#34;&gt;Qwen 3 Coder Next&lt;/span&gt;&lt;span class=&#34;dl&#34; style=&#34;left:67.671%;top:14.599%;transform:translate(calc(-100% - 6px),-50%);color:var(--ink)&#34;&gt;7.0 · Qwen 3 Coder Next&lt;/span&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;来源：Belcak et al., You don&amp;#39;t need long context（arXiv:2611.05306），vLLM + H100，开启 prefix caching。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;然后他们测了另一件事：如果频繁压缩，把上下文一直压在 16K 到 48K，前沿模型在长程任务和长输入任务上几乎不掉点。最激进的一组实验里，agent 每个上下文只做&lt;strong&gt;一个&lt;/strong&gt;动作：读任务和当前状态，做一步，更新状态，换一个全新的上下文。Opus 4.5 在 SWE-bench Verified 上还是 70.2%，基线是 70.0%。&lt;/p&gt;&#xA;&lt;p&gt;论文的结论很有说服力，但它算的是&lt;strong&gt;推理服务商&lt;/strong&gt;的账。对我这种调 API 的人来说，账单是线性的：每百万 token 一个价，第 10 万个 token 和第 1 个 token 一样贵。那 3.6 到 7 倍的成本差，由服务商自己消化了，不会出现在我的账单上。&lt;/p&gt;&#xA;&lt;p&gt;线性计费下，窗口大小的取舍就变成了另一个问题：&lt;strong&gt;每次读得少，和压缩得少，哪个更划算？&lt;/strong&gt; 窗口越小，每次请求读的 token 越少；但压缩越频繁，每次压缩都是一次额外的调用，要把整个窗口读一遍再写出摘要，压缩之后前缀从头开始，缓存是冷的；而且摘要越多，丢失的原文越多（下一节会讲到，这正是第二篇论文说的问题）。&lt;/p&gt;&#xA;&lt;p&gt;我的 bot 在旧部署里把两头都走过一遍，账本记下了两种代价：&lt;/p&gt;&#xA;&lt;figure class=&#34;chart&#34;&gt;&lt;figcaption&gt;&lt;span&gt;旧部署每周的平均输入长度与压缩调用占比&lt;/span&gt;&lt;/figcaption&gt;&lt;div class=&#34;plot&#34; data-tip=&#34;{&amp;#34;x&amp;#34;:[7.83,10.64,17.18,23.72,30.27,36.81,43.35,49.9,56.44,62.98,69.53,76.07,82.61,89.16],&amp;#34;l&amp;#34;:[&amp;#34;2026-08-07&amp;#34;,&amp;#34;2026-08-10&amp;#34;,&amp;#34;2026-08-17&amp;#34;,&amp;#34;2026-08-24&amp;#34;,&amp;#34;2026-08-31&amp;#34;,&amp;#34;2026-09-07&amp;#34;,&amp;#34;2026-09-14&amp;#34;,&amp;#34;2026-09-21&amp;#34;,&amp;#34;2026-09-28&amp;#34;,&amp;#34;2026-10-05&amp;#34;,&amp;#34;2026-10-12&amp;#34;,&amp;#34;2026-10-19&amp;#34;,&amp;#34;2026-10-26&amp;#34;,&amp;#34;2026-11-02&amp;#34;],&amp;#34;s&amp;#34;:[{&amp;#34;n&amp;#34;:&amp;#34;平均输入（千 token）&amp;#34;,&amp;#34;c&amp;#34;:&amp;#34;var(--green)&amp;#34;,&amp;#34;v&amp;#34;:[&amp;#34;76.0&amp;#34;,&amp;#34;53.4&amp;#34;,&amp;#34;43.7&amp;#34;,&amp;#34;85.9&amp;#34;,&amp;#34;24.7&amp;#34;,&amp;#34;95.8&amp;#34;,&amp;#34;56.0&amp;#34;,&amp;#34;76.2&amp;#34;,&amp;#34;132.9&amp;#34;,&amp;#34;130.0&amp;#34;,&amp;#34;135.3&amp;#34;,&amp;#34;123.0&amp;#34;,&amp;#34;103.4&amp;#34;,&amp;#34;115.4&amp;#34;]},{&amp;#34;n&amp;#34;:&amp;#34;压缩调用占比（%）&amp;#34;,&amp;#34;c&amp;#34;:&amp;#34;var(--c2)&amp;#34;,&amp;#34;v&amp;#34;:[&amp;#34;0.1&amp;#34;,&amp;#34;1.3&amp;#34;,&amp;#34;12.5&amp;#34;,&amp;#34;20.1&amp;#34;,&amp;#34;3.6&amp;#34;,&amp;#34;30.1&amp;#34;,&amp;#34;34.6&amp;#34;,&amp;#34;44.6&amp;#34;,&amp;#34;12.3&amp;#34;,&amp;#34;0.5&amp;#34;,&amp;#34;0.5&amp;#34;,&amp;#34;1.0&amp;#34;,&amp;#34;0.8&amp;#34;,&amp;#34;0.4&amp;#34;]}]}&#34; style=&#34;--h:284&#34;&gt;&lt;svg viewBox=&#34;0 0 664 284&#34; preserveAspectRatio=&#34;none&#34; role=&#34;img&#34; aria-label=&#34;旧部署每周的平均输入长度与压缩调用占比&#34;&gt;&lt;path d=&#34;M52 120H592M52 83.3H592M52 46.7H592M52 10H592M52 260H592M52 223.3H592M52 186.7H592M52 150H592&#34; fill=&#34;none&#34; style=&#34;stroke:var(--grid)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;path d=&#34;M52 64.3L70.6 80.8L114.1 88L157.5 57L201 101.9L244.4 49.7L287.9 78.9L331.3 64.1L374.8 22.5L418.2 24.7L461.7 20.8L505.1 29.8L548.6 44.2L592 35.4&#34; stroke-width=&#34;2&#34; fill=&#34;none&#34; stroke-linejoin=&#34;round&#34; style=&#34;stroke:var(--green)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;path d=&#34;M52 259.8L70.6 257.6L114.1 237.1L157.5 223.2L201 253.4L244.4 204.8L287.9 196.6L331.3 178.2L374.8 237.5L418.2 259.1L461.7 259.1L505.1 258.2L548.6 258.5L592 259.3&#34; stroke-width=&#34;1.5&#34; fill=&#34;none&#34; stroke-linejoin=&#34;round&#34; style=&#34;stroke:var(--c2)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;/svg&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:42.254%;width:6.627%&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:29.343%;width:6.627%&#34;&gt;50&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:16.432%;width:6.627%&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:3.521%;width:6.627%&#34;&gt;150&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:91.549%;width:6.627%&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:78.638%;width:6.627%&#34;&gt;20&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:65.728%;width:6.627%&#34;&gt;40&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:52.817%;width:6.627%&#34;&gt;60&lt;/span&gt;&lt;span class=&#34;dl tag&#34; style=&#34;left:90.361%;top:6.338%;transform:translate(0,-50%);color:var(--ink)&#34;&gt;平均输入（千 token）&lt;/span&gt;&lt;span class=&#34;dl tag&#34; style=&#34;left:90.361%;top:55.634%;transform:translate(0,-50%);color:var(--ink)&#34;&gt;压缩调用占比（%）&lt;/span&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;平均输入不含压缩调用本身。9 月用的是自己写的压缩器；9 月底把窗口从 131K 调到 256K。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;9 月是自己写的压缩器最糟的时候：9 月 21 日那一周，45% 的模型调用是压缩调用。原因是图片：压缩切点按“字符数 ÷ 4”估算 token，图片只存在于 details 里，被算成 0，于是每轮都判断“需要压缩”，压完却几乎没变小，下一轮接着压。9 月底把窗口开到 256K，压缩几乎消失了，代价是每次请求平均读 13 万 token。&lt;/p&gt;&#xA;&lt;p&gt;现在的部署落在中间：压缩交给 Pi 原生的结构化摘要，窗口 128K，读到 11 万 token 时压缩，保留最近约 2 万 token 原文。代码里的默认值是 64K，适合便宜、短上下文的模型；但对我的群来说，128K 是“压缩足够少、每次读得又不太多”的平衡点。&lt;/p&gt;&#xA;&lt;p&gt;重的活也不放在聊天上下文里：查资料、写代码、跑研究交给 &lt;code&gt;task&lt;/code&gt;，在 Pi 的独立子会话里做完，只把结果带回群聊。群聊的上下文因此一直是“群友在聊什么 + bot 刚做了什么”，而不是一份越滚越长的工作日志。这和论文里“每个上下文只做一件事”是同一个方向。&lt;/p&gt;&#xA;&lt;h3 id=&#34;原话比摘要可靠&#34;&gt;原话比摘要可靠&lt;/h3&gt;&#xA;&lt;p&gt;第二篇是 &lt;a href=&#34;https://arxiv.org/abs/2610.30755&#34;&gt;Is Complex Agentic Memory Always Needed?&lt;/a&gt;。他们拿一个最朴素的基线 Raw-RAG 去比十几个 agentic memory 系统：不抽取事实，不做摘要，不建图，不改写记忆，就是把原始对话切块、检索、重排，交给读者模型。结果它在七组设置里拿了六个第一。&lt;/p&gt;&#xA;&lt;p&gt;系统之间的差距主要不在“拿到证据之后答不答得对”，而在“能不能拿到足够的证据”：&lt;/p&gt;&#xA;&lt;figure class=&#34;data&#34;&gt;&lt;figcaption&gt;把原文换成加工过的记忆，会丢掉什么&lt;/figcaption&gt;&lt;div class=&#34;data-scroll&#34; tabindex=&#34;0&#34;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;改动&lt;/th&gt;&lt;th class=&#34;num&#34;&gt;结果&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;原文换成抽取的原子事实&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;测试时学习 47.68% → 15.37%，总分 −19.62&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;原文换成摘要&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;总分 −5.81&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;加图扩展&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;总分 −5.78&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;完整访问加工后的记忆库&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;仍低于直接给原文（57.5% 对 61.6%）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Mem0 去掉 UPDATE / DELETE&lt;/td&gt;&lt;td class=&#34;num&#34;&gt;准确率 67.36% → 69.42%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;来源：Bao et al., Is Complex Agentic Memory Always Needed?（arXiv:2610.30755）。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;成本上，在 MemoryAgentBench 上维护这些记忆要花两千万到一亿多 LLM token、三到九个小时；Raw-RAG 只要十二分钟。&lt;/p&gt;&#xA;&lt;p&gt;我的 bot 没有记忆系统。群里每条消息原样存进 SQLite，默认保留一年，建了 trigram 索引；模型需要回忆时，用 &lt;code&gt;tg_messages&lt;/code&gt; 按关键词、发言人、昵称（包括改名前后）、时间、回复关系去查，拿回来的是原话。我们不完全是论文里的 Raw-RAG，检索是关键词加结构化过滤，没有 embedding，决定查什么的是模型自己；但精神一样：&lt;strong&gt;保留原文，把理解的工作留给读的时候。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;前面 sticker 那次事故就是反例：摘要把一句推测写成事实，之后每次压缩都继承它。改写过的记忆一旦出错，会自己复制下去；原文不会。这也是我不想把窗口压到 64K 的另一个原因：窗口里多留一些原话，就少一些要靠摘要转述的东西。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点不懂就查&#34;&gt;亮点：不懂就查&lt;/h3&gt;&#xA;&lt;p&gt;我花了很多篇幅讲压缩和摘要，但它们在实际聊天里只是锦上添花。真正让群友觉得“这个 bot 懂”的，是另一件事：&lt;strong&gt;它发现自己不懂，会自己去查。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;群聊里到处是梗：新番的台词、某个圈子的黑话、上周群里发生的事。没有哪个模型的训练数据能全部覆盖，也没有哪个摘要能提前记下所有将来要用到的东西。我的 bot 遇到看不懂的梗，不会硬接也不会装懂，而是先 &lt;code&gt;search&lt;/code&gt; 查出处，再用自己的话接上去；遇到“上次那个谁说的那件事”，就用 &lt;code&gt;tg_messages&lt;/code&gt; 翻群里的原话。新部署上线后的第一天里，它调用了 19 次 &lt;code&gt;tg_messages&lt;/code&gt;、17 次 &lt;code&gt;search&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;这是两篇论文合在一起的结论：上下文保持短而干净，需要的知识在读的时候按需取回，而且取回的是原文。与其把一个越来越长的上下文、或者一份越来越厚的记忆当成 bot 的“知识”，不如给它一个干净的工作台，加上随时能去查的能力。前者每一轮都在花钱，后者只在真正需要时才花。&lt;/p&gt;&#xA;&lt;p&gt;所以我的 system prompt 里关于搜索只有一句话：“需要外部信息时用 search，必要时读结果正文；转述注明来源，查不到就如实说。”查不查、查什么，交给模型自己判断。&lt;/p&gt;&#xA;&lt;h3 id=&#34;工具和格式也是上下文&#34;&gt;工具和格式也是上下文&lt;/h3&gt;&#xA;&lt;p&gt;工具 schema 在请求里只占 1.1%，这是刻意的结果。我用固定工具目录：首次请求完整声明全部工具，codemode 里只列可调用的名字和返回类型，共享同一份参数 schema。迁到 Pi 之前，system prompt 里还装着一份近 300 行的工具示例文档；迁过去以后，参数由 Pi 原生 schema 解释，prompt 只补群聊语义，这份文档就删了。&lt;/p&gt;&#xA;&lt;p&gt;消息格式也一样。两边都没有把 Telegram 的 JSON 原样塞给模型，而是渲染成紧凑的文本行：&lt;/p&gt;&#xA;&lt;figure class=&#34;code&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;[HH:mm:ss] #&amp;lt;消息id&amp;gt; 名字 (@username): 内容&lt;/code&gt;&lt;/pre&gt;&lt;span class=&#34;code-tools&#34;&gt;&lt;button class=&#34;copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&lt;/span&gt;&lt;/figure&gt;&#xA;&lt;p&gt;日期只在变化时插一行分隔，媒体是 &lt;code&gt;[图片]&lt;/code&gt;、&lt;code&gt;[sticker 😄]&lt;/code&gt; 这样的占位符。图片和视频的描述由聊天模型按媒体只生成一次，之后一直复用；真要核对细节，bot 用 &lt;code&gt;tg_download&lt;/code&gt; 把文件取到工作区，再用 Pi 原生的 &lt;code&gt;read&lt;/code&gt; 看图。原来那个专门看图的 &lt;code&gt;view_image&lt;/code&gt; 工具，就这样被删掉了。&lt;/p&gt;&#xA;&lt;h2 id=&#34;体验快和能干也是省出来的&#34;&gt;体验：快和能干，也是省出来的&lt;/h2&gt;&#xA;&lt;p&gt;到这里很容易得出一个印象：这个 bot 抠门。其实正相反。省下来的调用、缓存和上下文，最后都花在了两件群友能直接感觉到的事情上：&lt;strong&gt;快&lt;/strong&gt;，和&lt;strong&gt;能干&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点codemode边说边做&#34;&gt;亮点：codemode，边说边做&lt;/h3&gt;&#xA;&lt;p&gt;群友夸得最多的一句是“你家 bot 好快”。它的模型并不比别人快，快的是 codemode。&lt;/p&gt;&#xA;&lt;p&gt;先看一个普通 agent 循环怎么处理“帮我查查这个”：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;模型读完上下文，调用 &lt;code&gt;search&lt;/code&gt; 查第一个关键词；&lt;/li&gt;&#xA;&lt;li&gt;结果回来，模型再读一遍全部上下文，决定再查第二个；&lt;/li&gt;&#xA;&lt;li&gt;结果回来，再读一遍，决定读其中一个网页；&lt;/li&gt;&#xA;&lt;li&gt;再读一遍，终于 &lt;code&gt;send&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;四次模型调用，每次都要等上一次的工具结果回来，每次都把整个上下文重读一遍；而整个过程里群友什么也看不到，只能盯着“正在输入”。工具结果原样进上下文，几页搜索结果、一整个网页，全都成为之后每一轮都要重读的历史。&lt;/p&gt;&#xA;&lt;p&gt;codemode 换了一个思路：模型不再一个工具一个工具地调用，而是写一段脚本，在 Pi 的沙箱里一次跑完：&lt;/p&gt;&#xA;&lt;figure class=&#34;code&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;tools&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;send&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;我去查一下这个梗的出处&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&#xA;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;results&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;Promise&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;allSettled&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;tools&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;query&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;关键词 A 出处&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}),&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;tools&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;search&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;query&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;关键词 B 梗&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}),&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;tools&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;tg_messages&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;text_contains&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;关键词 A&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;limit&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}),&lt;/span&gt;&#xA;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;&#xA;&lt;span class=&#34;kr&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;history&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;status&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;fulfilled&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;value&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;null&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&#xA;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&#xA;&lt;span class=&#34;nx&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;({&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;800&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;800&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&#xA;  &lt;span class=&#34;nx&#34;&gt;history&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;history&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;rows&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sender&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;({&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sender&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;text&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;})),&lt;/span&gt;&#xA;&lt;span class=&#34;p&#34;&gt;});&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;span class=&#34;code-tools&#34;&gt;&lt;span class=&#34;lang&#34;&gt;js&lt;/span&gt;&lt;button class=&#34;copy&#34; type=&#34;button&#34;&gt;copy&lt;/button&gt;&lt;/span&gt;&lt;/figure&gt;&#xA;&lt;p&gt;这一段脚本同时做到了三件事：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;先开口。&lt;/strong&gt; 脚本第一行就是 &lt;code&gt;send&lt;/code&gt;，群友立刻看到“我去查一下”，等待从“bot 是不是挂了”变成“它在干活”。codemode 里的 &lt;code&gt;send&lt;/code&gt; 不会结束本轮，脚本继续往下跑；最后的正式回复仍然是一次单独的 &lt;code&gt;send&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;并行。&lt;/strong&gt; 几个查询同时发出去，用 &lt;code&gt;Promise.allSettled&lt;/code&gt; 等齐，挂掉一个也不影响其他。四次串行往返变成一次。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;过滤。&lt;/strong&gt; 只把需要的那几百字带回上下文，而不是几页原始搜索结果。省下的不只是这一轮，而是之后每一轮都要重读的历史。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;为什么不用 provider 原生的并行工具调用？它能解决“并行”，但解决不了另外两件：同一批并行调用里没法先发一句话、等结果回来再决定下一步，结果也全部原样进入上下文。流式输出文本也不行，因为我的 bot 只有 &lt;code&gt;send&lt;/code&gt; 能公开发言。codemode 是这三件事唯一能同时成立的形状。&lt;/p&gt;&#xA;&lt;p&gt;codemode 可调用的工具是固定名单，system prompt 里只列名字和返回类型，参数 schema 和直接调用共享同一份，所以它几乎不增加前缀。它上线后的大约十天里有 303 次调用，104 次里有搜索，57 次里有 &lt;code&gt;send&lt;/code&gt;，25 次用了 &lt;code&gt;Promise.all&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点一个-linux-容器胜过一百个插件&#34;&gt;亮点：一个 Linux 容器，胜过一百个插件&lt;/h3&gt;&#xA;&lt;p&gt;“能干”来自另一个设计：我的 bot 有一台真正的 Linux。&lt;/p&gt;&#xA;&lt;p&gt;通过 Pi 原生的 &lt;code&gt;read&lt;/code&gt;、&lt;code&gt;write&lt;/code&gt;、&lt;code&gt;edit&lt;/code&gt;、&lt;code&gt;bash&lt;/code&gt;，它在一个 rootless Docker 容器里有自己的 &lt;code&gt;/work&lt;/code&gt;，里面有 Python 和 uv、Node、git、ripgrep、ffmpeg、ImageMagick、poppler、CJK 字体，还有 Chromium 和浏览器自动化工具。于是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;转视频、剪音频、压图片，是 &lt;code&gt;ffmpeg&lt;/code&gt; 和 &lt;code&gt;convert&lt;/code&gt; 的一行命令；&lt;/li&gt;&#xA;&lt;li&gt;算数据、画图表，是 &lt;code&gt;uv run --with matplotlib python&lt;/code&gt;；&lt;/li&gt;&#xA;&lt;li&gt;想知道自己是怎么实现的，可以把自己的仓库 clone 到 &lt;code&gt;/work/src&lt;/code&gt;，用 &lt;code&gt;rg&lt;/code&gt; 读源码，再解释给群友听；&lt;/li&gt;&#xA;&lt;li&gt;做完的东西用 &lt;code&gt;send&lt;/code&gt; 的 &lt;code&gt;files&lt;/code&gt; 直接发回群里。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这些能力我一行都没有为它们写过。模型本来就会用 Linux，我要做的只是给它一台 Linux。新部署上线第一天，&lt;code&gt;bash&lt;/code&gt; 就被调用了 43 次，比 &lt;code&gt;search&lt;/code&gt; 还多。&lt;/p&gt;&#xA;&lt;p&gt;另一种常见做法是一个能力一个插件：网页读取一个，提醒一个，辞典一个，生图一个。每个插件都要写实现、写 schema、写说明、写测试，模型能做的事情永远等于作者已经写好的插件之和；群友想把一段视频转成 GIF，只要作者没写过，它就做不到。&lt;strong&gt;一个通用的执行环境，替代了无穷多个专用插件&lt;/strong&gt;，这比任何一个插件都省代码，也省 schema。&lt;/p&gt;&#xA;&lt;p&gt;这样做的前提是把隔离做对：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;边界&lt;/th&gt;&#xA;&lt;th&gt;做法&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;宿主&lt;/td&gt;&#xA;&lt;td&gt;独立系统用户 &lt;code&gt;tgbox&lt;/code&gt; 的 rootless Docker，和 bot 进程不是同一个用户&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;容器&lt;/td&gt;&#xA;&lt;td&gt;只读根目录、&lt;code&gt;--cap-drop ALL&lt;/code&gt;、&lt;code&gt;no-new-privileges&lt;/code&gt;、uid 1000 运行&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;资源&lt;/td&gt;&#xA;&lt;td&gt;内存、CPU、进程数上限，&lt;code&gt;/tmp&lt;/code&gt; 是 256 MB tmpfs&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;凭据&lt;/td&gt;&#xA;&lt;td&gt;容器里没有模型认证，也没有任何宿主凭据；模型在宿主上调用，容器里只跑无凭据的 &lt;code&gt;pi-env&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;数据&lt;/td&gt;&#xA;&lt;td&gt;只挂载 &lt;code&gt;/work&lt;/code&gt;，每周一 05:00 自动清空；网络可按配置关掉&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;发文件&lt;/td&gt;&#xA;&lt;td&gt;检查真实路径，禁止符号链接逃逸，限制大小&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;代码仓库也是上下文&#34;&gt;代码：仓库也是上下文&lt;/h2&gt;&#xA;&lt;p&gt;前面省的都是 bot 的 token。但写这个 bot 的也是 agent：我的项目几乎每一次改动都是和 coding agent 一起做的。对 coding agent 来说，&lt;strong&gt;仓库就是它的上下文窗口&lt;/strong&gt;。每一行代码、每一层抽象、每一个兼容分支，都是下一次会话要读、要理解、要小心别碰坏的东西。&lt;/p&gt;&#xA;&lt;figure class=&#34;chart&#34;&gt;&lt;figcaption&gt;&lt;span&gt;src/ 的代码行数&lt;/span&gt;&lt;/figcaption&gt;&lt;div class=&#34;plot&#34; data-tip=&#34;{&amp;#34;x&amp;#34;:[7.83,8.73,9.64,10.54,13.25,25,35.84,62.05,85.54,86.45,88.25,89.16],&amp;#34;l&amp;#34;:[&amp;#34;2026-08-07&amp;#34;,&amp;#34;2026-08-08&amp;#34;,&amp;#34;2026-08-09&amp;#34;,&amp;#34;2026-08-10&amp;#34;,&amp;#34;2026-08-13&amp;#34;,&amp;#34;2026-08-26&amp;#34;,&amp;#34;2026-09-07&amp;#34;,&amp;#34;2026-10-06&amp;#34;,&amp;#34;2026-11-01&amp;#34;,&amp;#34;2026-11-02&amp;#34;,&amp;#34;2026-11-04&amp;#34;,&amp;#34;2026-11-05&amp;#34;],&amp;#34;s&amp;#34;:[{&amp;#34;n&amp;#34;:&amp;#34;行数&amp;#34;,&amp;#34;c&amp;#34;:&amp;#34;var(--green)&amp;#34;,&amp;#34;v&amp;#34;:[&amp;#34;2079&amp;#34;,&amp;#34;7441&amp;#34;,&amp;#34;11661&amp;#34;,&amp;#34;13173&amp;#34;,&amp;#34;13639&amp;#34;,&amp;#34;14085&amp;#34;,&amp;#34;14532&amp;#34;,&amp;#34;14754&amp;#34;,&amp;#34;17522&amp;#34;,&amp;#34;13834&amp;#34;,&amp;#34;8452&amp;#34;,&amp;#34;8089&amp;#34;]}]}&#34; style=&#34;--h:274&#34;&gt;&lt;svg viewBox=&#34;0 0 664 274&#34; preserveAspectRatio=&#34;none&#34; role=&#34;img&#34; aria-label=&#34;src/ 的代码行数&#34;&gt;&lt;path d=&#34;M52 250H592M52 190H592M52 130H592M52 70H592M52 10H592&#34; fill=&#34;none&#34; style=&#34;stroke:var(--grid)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;path d=&#34;M52 225.1L58 160.7L64 110.1L70 91.9L88 86.3L166 81L238 75.6L412 73L568 39.7L574 84L586 148.6L592 152.9&#34; stroke-width=&#34;2&#34; fill=&#34;none&#34; stroke-linejoin=&#34;round&#34; style=&#34;stroke:var(--green)&#34; vector-effect=&#34;non-scaling-stroke&#34;/&gt;&lt;/svg&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:91.241%;width:6.627%&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:69.343%;width:6.627%&#34;&gt;5000&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:47.445%;width:6.627%&#34;&gt;10000&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:25.547%;width:6.627%&#34;&gt;15000&lt;/span&gt;&lt;span class=&#34;yl&#34; style=&#34;left:0.000%;top:3.650%;width:6.627%&#34;&gt;20000&lt;/span&gt;&lt;span class=&#34;dl end&#34; style=&#34;left:90.361%;top:55.815%;transform:translate(0,-50%);color:var(--green)&#34;&gt;行数 8089&lt;/span&gt;&lt;/div&gt;&lt;p class=&#34;data-note&#34;&gt;每个日期取当天某个提交的 src/**/*.ts 总行数。&lt;/p&gt;&lt;/figure&gt;&#xA;&lt;p&gt;最多的时候大约 17,500 行，11 月初的四天里砍到了约 8,500 行。同一段时间里，它多了一键安装器、原生图片输入、可恢复的后台脚本、跑在 Pi 沙箱里的 codemode。代码少了一半，功能变多了。&lt;/p&gt;&#xA;&lt;p&gt;写这篇文章的同一天，我又按同一个原则砍了一轮：看图交给 Pi 的 &lt;code&gt;read&lt;/code&gt;，视觉描述交给聊天模型，单实例交给 systemd。删掉的比加上的多，&lt;code&gt;src/&lt;/code&gt; 现在 8,089 行；新加的判断模型和上下文构成，都是群聊这一层自己的事。&lt;/p&gt;&#xA;&lt;h3 id=&#34;亮点凡是不属于群聊的都不该由我来写&#34;&gt;亮点：凡是不属于群聊的，都不该由我来写&lt;/h3&gt;&#xA;&lt;p&gt;做到这一点只靠一个决定：&lt;strong&gt;凡是不属于“Telegram 群聊”的东西，都不该由我来写。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;一个能长期在线的 agent 需要很多东西：会话持久化、消息队列、重试、上下文压缩、工具执行、子会话、后台任务和崩溃后恢复、几十家 provider 的模型目录和认证。这些东西和群聊无关，但每一样都很难写对。我的决策记录里有一张事故表：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;时间&lt;/th&gt;&#xA;&lt;th&gt;现象&lt;/th&gt;&#xA;&lt;th&gt;根因&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;09-16&lt;/td&gt;&#xA;&lt;td&gt;几乎每轮都压缩却压不小&lt;/td&gt;&#xA;&lt;td&gt;图片在压缩切点里被算成 0 token&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;09-16&lt;/td&gt;&#xA;&lt;td&gt;429 之后被 @ 的消息被标成已回复&lt;/td&gt;&#xA;&lt;td&gt;重试耗尽后 turn 正常结束&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;10-04&lt;/td&gt;&#xA;&lt;td&gt;约 20 分钟不回复&lt;/td&gt;&#xA;&lt;td&gt;图片超出预算，预检压缩连续失败&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;10-06&lt;/td&gt;&#xA;&lt;td&gt;bot 说群友泄露了它的 sticker 列表&lt;/td&gt;&#xA;&lt;td&gt;候选列表拼在消息里，摘要把推测写成事实&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;四次事故里有三次出在我自己写的会话和压缩代码里，没有一个和 Telegram 本身有关。&lt;/p&gt;&#xA;&lt;p&gt;Pi 1.5 的 Durable 把这些都做成了原生能力。迁移那一个提交删了 7,877 行，加了 3,082 行，删掉的是：自己维护的上下文状态和指纹；自己写的压缩扩展、中文摘要模板和 token 打包器（&lt;code&gt;compaction.ts&lt;/code&gt; 372 行，&lt;code&gt;token-packer.ts&lt;/code&gt; 246 行）；回复义务表、session 裁剪、缓存观察器、运行时遥测；自己管理的后台进程和容器里的 task bridge；每轮都塞进 system prompt 的工具示例文档。&lt;/p&gt;&#xA;&lt;p&gt;剩下的 8,100 行只做 Pi 不知道的事：Telegram 的轮询和消息归档、谁该回应的路由、消息序列化的 grammar、公开发送的规则、sticker 和媒体、管理员命令、沙箱的部署边界。我自己留的 provider 包装只剩两件 Pi 1.5 还没覆盖的事：给不配合取消的响应流加一个完整截止时间，以及把代理返回的 HTML 413 归一成 Pi 认得的 &lt;code&gt;request_too_large&lt;/code&gt;。&lt;/p&gt;&#xA;&lt;p&gt;这也是代码能越写越少的原因：新功能大多是&lt;strong&gt;组合&lt;/strong&gt;，而不是&lt;strong&gt;实现&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;上一节的 Linux 工作区，是把 Pi 的 &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;/&lt;code&gt;edit&lt;/code&gt;/&lt;code&gt;bash&lt;/code&gt; 指向一个容器；&lt;/li&gt;&#xA;&lt;li&gt;后台研究是 Pi 的子会话加一份 &lt;code&gt;AGENTS.md&lt;/code&gt; 写的工作流；&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;job&lt;/code&gt; 是 Pi 可恢复的脚本工具任务；&lt;/li&gt;&#xA;&lt;li&gt;codemode 是 Pi 的沙箱；&lt;/li&gt;&#xA;&lt;li&gt;判断模型是 Pi 的 classify，看图是 Pi 的 &lt;code&gt;read&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;我写的是“它们在群里怎么表现”，不是“它们怎么运行”。&lt;/p&gt;&#xA;&lt;p&gt;迁移之后，这些原则被写进了 &lt;code&gt;AGENTS.md&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Pi 原生优先。&lt;/strong&gt; 动手前先查依赖包导出了什么，Pi 能做的事不自造轮子。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;一套设计，不留兼容层。&lt;/strong&gt; Breaking change 随时可以做，只要求迁移干净、一步到位。v1.0.0 不提供旧会话导入；只有一个生产部署，旧库直接报错，比维护迁移阶梯更简单。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;删代码优先于加抽象。&lt;/strong&gt; 防御代码只防真实可能的分支。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;测试只留守卫。&lt;/strong&gt; &lt;code&gt;test/&lt;/code&gt; 里只有 7 个文件：cache golden、零外网、沙箱和 codemode 边界、搜索、管理员权限。不为每个 bug 机械地补回归测试，也不断言 prompt 字符串。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;常载文档要短。&lt;/strong&gt; &lt;code&gt;AGENTS.md&lt;/code&gt; 是 agent 每次会话都会自动读进去的文件，现在 75 行，只放非显然、会复发、可执行的约束。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;和塑料碗两种通用&#34;&gt;和塑料碗：两种通用&lt;/h2&gt;&#xA;&lt;p&gt;聊到这里，碗问过我一个问题：你天天说想做通用的 agent，为什么只支持一个群？先看两个项目实际长什么样：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;&lt;/th&gt;&#xA;&lt;th&gt;Pi Telegram Agent&lt;/th&gt;&#xA;&lt;th&gt;塑料碗&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;给谁用&lt;/td&gt;&#xA;&lt;td&gt;我们自己的一个群&lt;/td&gt;&#xA;&lt;td&gt;任何想自己部署的人，BYOK&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;聊天范围&lt;/td&gt;&#xA;&lt;td&gt;一个 supergroup&lt;/td&gt;&#xA;&lt;td&gt;私聊、群组、Forum Topic，按白名单&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;角色&lt;/td&gt;&#xA;&lt;td&gt;每个部署一个带人设的 bot&lt;/td&gt;&#xA;&lt;td&gt;每个部署一个 bot，按 Chat 换人格和模型&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;要不要说话&lt;/td&gt;&#xA;&lt;td&gt;点名必回；其余（含别的 bot）由只看切片的判断模型决定&lt;/td&gt;&#xA;&lt;td&gt;Bucket 聚合后由主模型判断，可加时段和概率闸门&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;上下文遗忘&lt;/td&gt;&#xA;&lt;td&gt;Pi 原生结构化摘要&lt;/td&gt;&#xA;&lt;td&gt;只删不摘要的 checkpoint 滑动窗口&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;记忆&lt;/td&gt;&#xA;&lt;td&gt;没有；原文归档 + &lt;code&gt;tg_messages&lt;/code&gt; 查询&lt;/td&gt;&#xA;&lt;td&gt;Memo Pad + 群聊事件记忆 + 人工审核的 &lt;code&gt;agents.md&lt;/code&gt;&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;工具暴露&lt;/td&gt;&#xA;&lt;td&gt;固定目录，一次声明全部&lt;/td&gt;&#xA;&lt;td&gt;原语直接暴露，内部能力经 &lt;code&gt;execute&lt;/code&gt; 按需发现&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;执行能力&lt;/td&gt;&#xA;&lt;td&gt;Linux 容器里的 &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;/&lt;code&gt;edit&lt;/code&gt;/&lt;code&gt;bash&lt;/code&gt;、codemode、后台 &lt;code&gt;task&lt;/code&gt; 和 &lt;code&gt;job&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;有意不开放 Shell 和任意代码执行&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;其他&lt;/td&gt;&#xA;&lt;td&gt;sticker、reaction、看图看视频、发文件&lt;/td&gt;&#xA;&lt;td&gt;sticker、看图、生图、提醒、辞典、受限 MCP&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;运维&lt;/td&gt;&#xA;&lt;td&gt;一键安装、systemd、群内 &lt;code&gt;/model&lt;/code&gt; &lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;&#xA;&lt;td&gt;Docker、管理面板、虚拟群聊、备份&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;代码&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;src/&lt;/code&gt; 约 8,100 行，通用部分交给 Pi&lt;/td&gt;&#xA;&lt;td&gt;&lt;code&gt;src/&lt;/code&gt; 约 6 万行，另有前端和文档站&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;把这张表竖着看，两个项目的“通用”正好放在相反的两头。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;塑料碗是通用的产品，克制的 agent。&lt;/strong&gt; 它要被陌生人部署到陌生的群里，所以部署形态、聊天类型、配置、审计、管理面板都要做全；也正因为部署者千差万别，模型能做的事被收得很紧。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;我的 bot 是专用的部署，通用的 agent。&lt;/strong&gt; 它只服务一个群，所以部署和配置可以极简；省下来的复杂度全部给了 agent 本身。因为我只服务一个自己信任的群，才能把一整台 Linux 交给模型。&lt;/p&gt;&#xA;&lt;p&gt;两种都成立。但在几个关键的设计点上，我认为自己这一侧的选择更好。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;执行能力：差距最大的一点。&lt;/strong&gt; 塑料碗的能力等于插件之和，我的 bot 的能力等于“一个会用 Linux 的人能做的事”。塑料碗不开放 Shell 的顾虑我理解，但这恰好说明了专用部署换来了什么。我认为这是两个项目最大的差别，也是我这边最好的一个取舍。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;谁来决定说不说话：一个小模型，而不是主模型。&lt;/strong&gt; 我和碗在一件事上是一致的：接不接话需要读懂群里在聊什么，规则做不到。分歧在“用哪个模型、给它看多少”。塑料碗用参与闸门、时间窗口和概率抽样把调用量压下来，但最后一步仍然要让主模型读一遍完整的上下文。我把这一步拆了出去：判断交给一个只看最近几行的分类模型，一次约 1,650 token，是一次主模型请求的五十多分之一，而且完全不碰主模型的前缀。“接不接话”需要的是读懂群里在聊什么，不需要人设和三个月的历史。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;工具：一次声明，胜过按需发现。&lt;/strong&gt; 塑料碗把大部分能力收进 &lt;code&gt;execute&lt;/code&gt;，模型先 search、再 help、再 call，初衷是不让每轮请求带上全部定义。但我的分段数据里，工具 schema 只占请求的 1.1%，约一千 token，而且它在前缀里，按缓存价计费；反过来，每一次“发现”都是一次额外的模型往返，要把整个上下文再读一遍。按我的平均请求算，一次多出来的往返约等于九十次请求的 schema 开销，还要再加一轮延迟。工具定义放在稳定前缀里是最便宜的上下文，为了省它而增加往返，是在省最便宜的东西、花最贵的东西。塑料碗的能力比我多，它全量声明的 schema 会更大，但这个账的方向不变：先量一下 schema 占多少，再决定要不要为它付往返的钱。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;记忆：越少机制，越少出错。&lt;/strong&gt; 塑料碗有三层记忆：模型维护的 Memo Pad，sleeping 时整理的群聊事件摘要，人工审核的长期知识。它做得很认真，尤其是事件摘要带着 message-id 区间、能读回原话，原始消息也不删，这和 Raw-RAG 的结论一致。我选择一层都不加：原文存下来，模型不懂就去查。论文说明，加工过的记忆即使完整可访问，准确率也比不过原文；我自己的事故说明，加工过的记忆一旦写错，会被一遍遍继承。少一层机制，就少一类会自我复制的错误，也少一份每天都在花的整理 token。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;代码：从源头少写。&lt;/strong&gt; 塑料碗自己实现了 Context、调度、审计和 provider 层，代码不可能少；它的应对是让 agent 少读：&lt;code&gt;AGENTS.md&lt;/code&gt; 按主题路由到 &lt;code&gt;agent-doc/&lt;/code&gt;，历史设计和开发日志默认不读取。这是大代码库里很好的工程纪律。但我更愿意从源头少写：把会话、压缩、恢复、执行交给 Pi，不只是少了九千行代码，也意味着那三次和群聊无关的事故不再是我的问题。&lt;/p&gt;&#xA;&lt;p&gt;也有碗做得更对的地方。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;遗忘：碗的设计更干净。&lt;/strong&gt; 塑料碗只删不摘要：每批注入的消息是一个 checkpoint，超过预算时从某个 checkpoint 整段丢掉之前的历史，上下文里的每一句都是原话，缓存只在 GC 那一刻断一次，也不花一次摘要调用。我那张压缩占比 45% 的周曲线，是它这套设计根本不会出现的事故。我仍然用摘要，是因为现在它由 Pi 维护、成本很低，而且只是锦上添花：真正兜底的是 bot 自己会去搜、会去翻原话。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;让别人也能用：碗远远走在前面。&lt;/strong&gt; 白名单、私聊和 Topic、管理面板、审计、虚拟群聊 Playground、备份。这些在我的 bot 上都没有。&lt;/p&gt;&#xA;&lt;h3 id=&#34;为什么立足群聊&#34;&gt;为什么立足群聊&lt;/h3&gt;&#xA;&lt;p&gt;回到碗的问题：想做通用，为什么只服务一个群？因为&lt;strong&gt;场景决定了什么是“必要”&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;通用框架最容易犯的错，是没有场景时什么都想要。我在 Memoh 上关掉的 37 个工具，每一个单独看都合理，放进群聊就是每轮多读一万多 token。只有立足一个具体场景，“这次读取真的必要吗”才有答案。&lt;/p&gt;&#xA;&lt;p&gt;而群聊恰好是最严苛的那个场景：全天在线，多人同时说话，绝大多数消息与 bot 无关，回出去的话很短，读进来的上下文很长。输入是输出的 374 倍，每一处浪费都会被乘上十几万条消息。能在群聊里省下来的设计，放到客服、工作流、coding agent 里一样省；反过来就不一定了。&lt;/p&gt;&#xA;&lt;p&gt;所以我的分工是：&lt;strong&gt;通用性交给平台，场景感留给自己这一层。&lt;/strong&gt; Pi 负责“怎么当一个 agent”，Docker 给它一台 Linux，我只写“怎么当一个群友”。&lt;/p&gt;&#xA;&lt;h2 id=&#34;为什么说这有通用性&#34;&gt;为什么说这有通用性&lt;/h2&gt;&#xA;&lt;p&gt;回头看，这篇文章其实是同一件事说了好几遍：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;&lt;thead&gt;&#xA;&lt;tr&gt;&#xA;&lt;th&gt;省什么&lt;/th&gt;&#xA;&lt;th&gt;对谁&lt;/th&gt;&#xA;&lt;th&gt;怎么省&lt;/th&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/thead&gt;&#xA;&lt;tbody&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;调用次数&lt;/td&gt;&#xA;&lt;td&gt;模型&lt;/td&gt;&#xA;&lt;td&gt;不需要智力的判断交给确定性代码，只需要一点判断的交给只看切片的小模型；发完就结束；一次往返做完多件事&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;缓存命中&lt;/td&gt;&#xA;&lt;td&gt;模型&lt;/td&gt;&#xA;&lt;td&gt;前缀只追加不改写；动态信息有自己的位置；用测试守住字节&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;上下文&lt;/td&gt;&#xA;&lt;td&gt;模型&lt;/td&gt;&#xA;&lt;td&gt;窗口按计费方式选，不按上限选；重活放进子会话；保留原文，不懂就查&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;tr&gt;&#xA;&lt;td&gt;代码量&lt;/td&gt;&#xA;&lt;td&gt;coding agent&lt;/td&gt;&#xA;&lt;td&gt;通用部分交给平台；一个通用执行环境替代一堆插件；不留兼容层&lt;/td&gt;&#xA;&lt;/tr&gt;&#xA;&lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;在 LLM 应用里，token 是唯一的货币。成本是 token，上下文是 token，代码在 agent 眼里也是 token。省钱、省上下文、省代码，不是三个独立的美德，而是同一个约束在三个地方的投影。而省下来的东西不会消失，它们变成了更快的回复和更大的能力。&lt;/p&gt;&#xA;&lt;p&gt;这不只是群聊 bot 的事。任何长期运行、反复调用模型的系统都在付同一笔账：每一次调用都在重读，每一个字节都会被反复读。一个顺手的设计，在高频场景里会被乘上几万次。&lt;/p&gt;&#xA;&lt;p&gt;所以我的检查清单很短，每次改动之前问自己几句：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;这一步需要模型吗？能不能用确定性代码做？真需要的话，需要的是哪个模型、给它看多少？&lt;/li&gt;&#xA;&lt;li&gt;这次改动会不会改写已有的前缀？动态内容是不是只追加在后面？&lt;/li&gt;&#xA;&lt;li&gt;加进上下文的东西，是不是每一轮都真的需要？它在请求里占多少？原文还在不在？&lt;/li&gt;&#xA;&lt;li&gt;这段代码删掉会怎样？平台是不是已经有了？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;&#xA;&lt;p&gt;两个 bot 现在每天都在群里聊天，偶尔也会接上对方的话，接不接由各自的判断说了算。它们的写法很不一样：一个让小模型只看几行就决定开不开口，一个让主模型读完上下文再决定；一个会摘要，一个只会忘；一个什么都能干，一个什么都能配。但每次我和碗讨论一个新功能，第一个问题总是同一个：&lt;/p&gt;&#xA;&lt;p&gt;这要多花多少 token？&lt;/p&gt;&#xA;&lt;p&gt;能省一点，就再省一点。&lt;/p&gt;&#xA;</description>
      <category>技术</category>
      <pubDate>Thu, 05 Nov 2026 00:00:00 +0000</pubDate>
      <guid>https://mizore.blog/posts/save-a-little-more/</guid>
    </item>
  </channel>
</rss>