Agent原理介绍
“We shall not cease from exploration
And the end of all our exploring
Will be to arrive where we started
And know the place for the first time.”
“我们不会停止探索。而一切探索的尽头,是回到出发的那个地方,并且第一次认出它来。”
(T. S. Eliot《Four Quartets · Little Gidding》)
“9 月 1 日”
九月的蝉,叫得比八月薄。
薄得像一层纱,盖在坡道上,盖在便利店门口那两台自动贩卖机上。
我:「水科,你在挑什么?」
水科:「蚊香」
我:「……现在已经是九月了呢?」
水科:「九月也有蚊子」
我:「可蝉都快没了?」
水科:「蝉跟蚊子没有排过班……」
啊哈哈。
货架最下面那一层,铁盒装,两盘叠在一起。
我拿了一盒,想把它们分开。
分不开。
一用力,其中一盘从中间断了。
啪。
我:「那、那个……对不起……」
水科:「它们本来就是缠在一起的。你只是先发现了这件事」
我:「这话听起来像在安慰我……」
水科:「没有」
她把断掉的那半盘拿过去,对齐,插回铁架的齿上。
然后点着。
烟很细,直直地往上走了一小段,才开始歪。
我:「这个能烧多久?」
水科:「盒子上写着,十小时……」
我:「你怎么连看都不看就知道?」
水科:「我刚才看了」
我:「那,要是没烧完呢?」
水科:「那就没烧完……」
我:「谁来告诉它该停了?」
水科没有回答。
她只是看着那圈烟。
烟往上走。
灰往下落。
落的灰是白的,很轻,一碰就散。
我:「水科……」
水科:「嗯」
我:「你不觉得这个形状有点奇怪吗?」
水科:「哪里」
我:「为什么要做成一圈一圈的?」
水科:「做成一根直的,要烧十小时,得有手臂那么长」
我:「所以盘起来是为了省地方?」
水科:「为了在这么小的盘子上,放下那么长的东西……」
我:「……」
我:「总觉得这句话,我在哪儿听过?」
风从坡道下面吹上来。
烟歪了一下,又直回去。
我偶尔会想,一圈到底是从哪儿开始的。
第一章 The Word 「一个词滑成了形容词」
“9 月 2 日”
屋顶。
水塔的影子比昨天短了一点,也可能长了一点。
我分不清。
水科把铁盒放在地上,从里面拿出第二盘蚊香。
这一次她分得很干净。
我:「手法很熟练……」
水科:「因为我不着急」
我:「啊哈哈……」
我:「我说,水科……」
水科:「什么?」
我:「最近到处都在讲那个词呢?」
水科:「哪个」
我:「Agent」
水科:「它已经不是名词了……」
我:「诶!」
水科:「是形容词。凡是会自己动的东西,都被叫成 Agent」
我:「那,把话术剥干净,里面还剩什么?」
水科:「三样」
我:「三样……」
水科:「一个会输出函数的语言模型」
水科:「一段真的去执行函数的宿主代码……」
水科:「还有把执行结果送回模型的那一次回灌」
我:「就这三样?」
水科:「剩下的都是配套设施……」
蝉叫了一声,很短。
像是被谁掐掉的。
我:「以前不是这样吧?」
水科:「2023 年那个原始形态,是一句 ReAct 提示词,加一个 for……」
我:「一个 for?」
水科:「一个 for」
我:「那现在呢?」
水科:「树形的会话。逐个挂上去的钩子。数十家 provider……」
我:「……长成 runtime 了」
水科:「同一样东西的两种活法」
我:「哪一种更好?」
水科:「这个问题超×1000 蠢……」
我:「呜……」
水科:「两种活法都得回答同样的问题。所以谈设计约束是有必要的」
我:「设计约束……」
水科:「循环为什么能干活。设计时有哪些选择。每个选择的代价是什么」
我:「那,选哪家模型比较好呢?」
水科:「不谈!」
我:「prompt 的措辞呢?」
水科:「玄学。不谈」
我:「benchmark 呢?」
水科:「不跑!」
我:「那我们到底谈什么?」
水科:「谈这一圈代码……」
一个循环。
五种输入。
谁来决定结束。
我盯着铁盘看了一会儿。
它已经烧掉了最外面那一圈。
灰落在盘子上,还是螺旋的形状。
……不对。
灰没有形状。
只是原来的形状还没有散掉。
我:「水科……」
水科:「嗯」
我:「你读代码,比读论文多吧?」
水科:「多」
我:「为什么?」
水科:「论文写的是应该怎样。代码写的是不得不怎样」
我:「……」
我:「有实物吗?」
水科:「pi。tag 是 v0.84.4……」
我:「连版本号都背下来了?」
水科:「每一处 file.ts:line 都链到那个 tag 下的固定行」
水科:「不跟着 main 漂……」
我:「那要是换一套呢?」
水科:「换掉这套 harness,我说的每一条还是成立……」
我:「好大的口气……」
水科:「不是口气。是这一圈代码本来就长这样……」
风把烟吹斜了。
灰断了一小截,掉在铁盘外面。
我把它捡起来,捏碎了。
指尖是白的。
第二章 Seven Lines 「七行,和三件不得不认的事」
“9 月 3 日”
放学后的图书室。
空调坏了,只有风扇在转。
水科在纸上写了七行,推过来。
1 | messages := [user(prompt)] |
我:「……就这七行?」
水科:「就这七行」
我:「这个圈是什么?」
水科:「不是圈。是槽位……」
我:「槽位?」
水科:「一次 forward,要塞进去五种东西……」
水科:「塞完它说话,说完我们再塞……」
我:「所以整个 agent 就是这个?」
水科:「整个 agent 就是这个。这五格是目录」
风扇转了一圈。
我:「那它凭什么能成立?」
水科:「三条前提。客观约束,不是设计选择……」
我:「第一条……」
水科:「终止条件,由模型的行为给出」
我:「行为?」
水科:「宿主代码里没有任何一行,在判断任务是否完成……」
水科:「完成的表现,是模型不再请求工具……」
我:「那出口条件不就等于……」
水科:「出自一个概率模型。不是一个可判定的谓……」
我:「呜哇!」
水科:「第二条。模型不能行动……」
我:「可它刚刚明明动了文件!」
水科:「它没有。它只输出了一句话」
水科:「我要调用 X,参数是 Y……」
水科:「动作由宿主代为执行,观察结果再还回去」
我:「所以……」
水科:「所以能力边界等于 tools 列表。跟它知道多少无关……」
我:「知识边界呢?」
水科:「另一回事。它可能知道一件事,却没有手去做」
我:「啊……有点可怜……」
水科:「第三条。模型无状态……」
我:「无状态?」
水科:「模型侧永远是 messages 进、resp 出。一个纯函数……」
水科:「跨圈的连续性,全靠宿主维护那个数组……」
我:「那么,agent 记得刚才发生了什么,是……」
水科:「是宿主的性质。不是模型的……」
我:「……」
三条前提,压在五个槽位上。
问题的分布就是这么来的。
水科:「① 被前提三压得最贵。每一圈都要重放全量」
我:「两本账,前缀稳不稳,还有提示怎么装配……」
水科:「嗯」
水科:「② 被前提二压。它只能说话」
我:「工具契约,和截断防御……」
水科:「③ 被前提一压。出口是概率性的……」
我:「闸门、可验证性、共识退出……」
水科:「④ 手在宿主这边,还是前提二」
我:「调度、副作用锁、权限边界……」
水科:「⑤ 记忆在这里,前提三……」
我:「存储形态,和压缩?」
水科:「嗯」
我:「有实物能对上吗?」
水科:「pi 的 AgentContext……」
我:「几个字段?」
水科:「三个。types.ts 的 413 到 420 行……」
水科:「systemPrompt,行为约束,咬在 ①」
水科:「messages,可见历史,咬在 ⑤……」
水科:「tools,可选,能力边界,咬在 ②」
我:「多一个都没有?」
水科:「多一个都没有!」
我:「那,一个东西要怎样才算 agent runtime?」
水科:「输入能归约到这三项,就是同一个抽象……」
我:「自称的也算?」
水科:「自称的也算。归约不进去的,就是别的什么东西」
风扇转了一圈。
又一圈。
蚊香是一圈一圈往里烧的。
烧过的地方变成灰。
灰落下来,就回不去了。
谁来决定它烧到哪儿停。
我:「水科……」
水科:「嗯」
我:「第一条前提,我一直有点不舒服……」
水科:「哪里?」
我:「出口握在一个概率模型手里……」
水科:「嗯」
我:「那我们到底是在等它做完,还是在等它不想做了?」
水科没有回答。
她把那张纸折起来,塞进书包。
纸角蹭过桌面,响了一下。
第三章 Two Ledgers 「窗口是一本账,钱是另一本」
“9 月 5 日”
坡道下面的自动贩卖机。
我站了四分钟,没有投币。
水科:「你在干什么?」
我:「在想 ① 那一格……」
水科:「它要设计的东西只有一件……」
我:「一件?」
水科:「决定喂进去什么。但是有两本账……」
我:「两本……」
水科:「混在一起算,是这个主题上最常见的错误……」
我:「第一本是什么?」
水科:「窗口占用」
水科:「第 n 圈的 prompt 长度,不是这一轮说了多少?」
我:「那是什么?」
水科:「sys 加 tools,是固定前缀……」
水科:「加上前面 n−1 圈,每一圈的 resp 和 result」
水科:「最后加上本轮新增……」
我:「听起来会一直变长……」
水科:「会」
我:「哪一项最大?」
水科:「result」
我:「为什么?」
水科:「模型说请读这个文件,只花十个 token」
水科:「工具回来的是两千行……」
我:「呜哇!」
蝉在头顶叫了两声,停了。
我:「那跟普通的一次性补全比呢?」
水科:「三个地方不一样」
水科:「prompt。补全只付一次,agent 每圈重放全量……」
水科:「窗口。补全是 O(S),agent 累计到 O(n²)」
水科:「主导项。补全是输出,agent 是工具返回值……」
我:「为什么是平方?」
水科:「第 n 圈要把前面 n−1 圈全部重放一遍」
水科:「1 加到 n,是 n(n+1)/2……」
我:「啊!」
水科:「第二本,钱」
我:「把第一本乘上单价不就行了?」
水科:「错!」
我:「诶!」
水科:「历史是 append-only 的……」
水科:「所以第 n 圈的 prompt 里,有一段跟上一圈逐字节相同……」
我:「有多长?」
水科:「正好是上一圈的全部……」
我:「……」
水科:「provider 就靠这个做前缀缓存。输入于是被劈成两桶」
我:「两桶有名字吗?」
水科:「有五个字段。ai 包的 types.ts,382 到 403 行……」
水科:「input,未缓存的输入」
我:「OpenAI 那边怎么算出来的?」
水科:「prompt_tokens 减 cacheRead,再减 cacheWrite……」
水科:「output。cacheRead 是命中的前缀,折扣价」
水科:「cacheWrite,是这一次写进缓存的那段前缀……」
水科:「还有一个可选的 cacheWrite1h」
我:「1h?」
水科:「按一小时保留的写量。只有 Anthropic 报这一档……」
我:「那账单呢?」
水科:「四个单价,分别乘四个桶,加起来」
水科:「models.ts 的 878 到 898 行……」
我:「折扣差多少?」
水科:「因家而异。三家,你听着」
水科:「OpenAI,read 比 in 是四分之一到二分之一……」
水科:「write 是 0。自动的,写不上报」
水科:「gpt-4.1,每百万 token,in 是 2,read 是 0.5……」
水科:「Anthropic,read 比 in 是十分之一」
水科:「write 是 1.25 倍,1h 那一档再翻一倍……」
水科:「sonnet-4-5,in 3,read 0.3,write 3.75」
水科:「DeepSeek,read 比 in 是五十分之一到一百二十分之一……」
水科:「v4-flash,in 0.14,read 0.0028」
我:「等、等一下!」
我:「这些数字你从哪儿背来的?」
水科:「构建期由 generate-models.ts 拉下来……」
水科:「写进 providers/data 里的 json」
我:「那个入库了吗?」
水科:「没有。跑 npm run generate:models 可以自己复现……」
我:「要是懒得跑呢?」
水科:「直接查已发布的模型目录,pi.dev/api/models,原始 JSON,逐条对」
我:「……」
我:「所以第一本账的结论得改?」
水科:「得收口……」
水科:「窗口占用是 O(n²)。钱不是」
我:「为什么?」
水科:「每圈真正按全价付的,只有上一圈新落盘的那一截」
水科:「就是 input 那一桶。再加上缓存写入……」
我:「增量?」
水科:「记作 δ。这一圈的 P 减上一圈的 P」
水科:「跑 N 圈,未缓存输入累计起来是个望远镜和……」
我:「望远镜……」
水科:「中间的项全部抵消」
水科:「剩下末圈的 P 减首圈的 P。线性……」
我:「啊!」
我:「那么,主导项是工具返回值,这句呢?」
水科:「要说清适用范围。它说的是窗口,不是账单」
水科:「账单的主导项在 output,和被写坏的 cacheWrite 上……」
我:「被写坏的?」
水科:「等你亲手弄坏一次前缀,就懂……」
我:「……」
我:「那设计目标就变了……」
水科:「从少喂点,变成一个精确得多的命题……」
我:「什么命题?」
水科:「agent 的上下文工程,目标是少破缓存」
贩卖机嗡了一声。
我终于投了币,按最上面那一排。
掉下来的是一罐热的。
我:「九月的热咖啡……」
水科:「是贩卖机的自由……」
我:「啊哈哈……」
我:「少喂点,和少破缓存,差在哪儿?」
水科:「差在你敢不敢,把一段长东西留在原地不动」
我:「……」
罐子很烫。
我在两只手里来回倒。
倒了七次,才敢握住。
第四章 The Prefix 「一条不能碰的前缀」
“9 月 6 日”
屋顶,快六点。
蚊香烧到了第二圈。
风从水塔那边绕过来。
呼。
烟断了。
我:「……灭了」
水科:「嗯」
我:「点回来就好了吧?」
水科:「点回来,也不是原来那一圈……」
我:「为什么?」
水科:「断掉的地方要重新烧一次……」
水科:「那一小段,按全价」
我:「全价和折扣价差多少?」
水科:「一次全量 miss,付的是 p_in 乘整个 P……」
水科:「命中的时候,同一批 token 只按 p_read 乘 P」
我:「相除呢?」
水科:「就是昨天那个 read 比 in……」
我:「一到两个数量级!」
水科:「一到两个数量级」
我:「什么东西会把它弄坏?」
水科:「六种。你数着……」
我:「是、」
水科:「一,中途换模型,或者换 provider。前缀整体作废」
我:「这个怎么知道的?」
水科:「pi 把 modelChanged 单独列成一种 miss……」
水科:「cache-stats.ts 的 22 行和 88 行」
水科:「二,改 systemPrompt,改工具描述,改工具列表的顺序……」
水科:「从改动点往后,全部失效」
我:「三……」
水科:「固定前缀里放时间戳、随机 id、git 状态……」
我:「那不是每圈都……」
水科:「每圈都 miss!」
我:「四?」
水科:「压缩」
我:「压缩不是省吗?」
水科:「省窗口,花一次钱。压完那一圈要按全价重付一次……」
水科:「所以别频繁压」
我:「五……」
水科:「空闲超过 TTL。缓存自己过期,跟你的代码一点关系都没有」
我:「多久?」
水科:「CACHE_TTL_MS 是五分钟,Anthropic 的默认值……」
水科:「cache-stats.ts 第 8 行」
我:「六……」
水科:「单圈小幅抖动。那是误报……」
我:「误报?」
水科:「NOISE_FLOOR_TOKENS 是 1024,低于这个数不计」
我:「为什么不记?」
水科:「因为那只是 cache breakpoint 的粒度……」
天暗下来了一半。
楼下有人推自行车上坡,链条响了一路。
我:「这些,它自己算得出来吗?」
水科:「算得出来。做成了一份事后审……」
水科:「拿上一圈的 prompt 长度,减去本圈的 cacheRead……」
我:「减出来是什么?」
水科:「白跑了多少 token,多付了多少钱」
我:「在哪一行?」
水科:「cache-stats.ts 的 70 行,missedTokens……」
我:「被减的那个数呢?」
水科:「62 行,promptTokens」
水科:「等于 input 加 cacheRead 加 cacheWrite……」
我:「三个桶加起来……」
水科:「窗口占用量和计价口径,用的是同一个数」
我:「同一个数……」
水科:「记住这句。马上要用……」
我:「怎么用?」
水科:「缓存能压价。压不掉跨档」
我:「跨档?」
水科:「calculateCost 挑价格档的时候,用的是 inputTokens……」
水科:「而它就是那三个桶的和。models.ts 的 879 到 886 行」
我:「……所以」
水科:「把会话撑进长上下文加价的,恰恰是那些已经打过折的历史」
我:「呜哇!」
水科:「打折的是单价。不打折的是长度……」
我:「档位只作用于超出的那部分吗?」
水科:「不。整单适用」
我:「整单!」
水科:「ModelCost 的注释写得很清楚……」
水科:「最高的那个命中阈值,适用于整个请求。types.ts 的 816 行」
我:「有实测吗?」
水科:「已发布的目录里,gpt-5.4 在 272K 那一处……」
水科:「in 从 2.5 跳到 5,read 从 0.25 跳到 0.5」
我:「两个桶同时翻倍……」
水科:「同时!」
我:「可是,凭什么 read 能便宜一到两个数量级?」
水科:「去问 KV cache……」
我:「诶……」
水科:「命中的那段前缀,它的 K 和 V 张量还留在存储里」
水科:「不必重算……」
我:「所以 prompt caching 是……」
水科:「物理上,就是把 KV cache 的生命周期,从一次请求内延长到请求之间」
我:「……」
我:「那三条性质……」
水科:「哪三条」
我:「前缀必须逐字节相同……」
我:「写缓存要额外付一次……」
我:「窗口占用与计费同源……」
水科:「三条全部对得上……」
我:「啊哈哈,我居然记住了……」
水科:「因为你昨天问过一遍」
我:「诶,我问过?」
水科:「没有。是我昨天想讲,你没在听……」
我:「……」
风又起来了一次。
水科把打火机按下去。
咔。
火苗贴着断口,那一小段重新红了。
我:「这次要付全价了……」
水科:「嗯」
我:「值吗?」
水科:「看你后面还要烧多久……」
我:「……」
烟又直直地往上走了。
第五章 Nine Blocks 「九块,和三条纪律」
“9 月 8 日”
图书室。
水科把铁盒放在桌角,没有点。
这里不许有火。
她把笔记本转过来,屏幕朝着我。
我:「这就是每圈都要重放的那条前缀?」
水科:「落盘的实物。九块」
水科:「块界来自 system-prompt.ts 的 128 到 166 行……」
水科:「体积是我从落盘文件里量出来的……」
我:「……你亲手量的」
水科:「一,角色句,169 字节,固定……」
水科:「开头是 You are an expert coding assistant operating inside pi」
水科:「二,Available tools,toolsList 插进去的位置……」
水科:「默认 read、bash、edit、write,运行时,换工具集才变」
水科:「三,一句过渡,说你可能还有别的自定义工具,99 字节,固定」
水科:「四,Guidelines,一条条 addGuideline() 攒的,按当前工具集条件生成……」
水科:「五,文档路由块,标题 Pi documentation」
水科:「8 行 7 条指路,点了 14 个 .md,1024 字节约 256 token,固定……」
水科:「六,appendSection,配置级」
水科:「七,project_context 里套 project_instructions,带一个 path 属性……」
水科:「装 AGENTS.md 全文,908 字节,用户会改」
水科:「八,available_skills,六条,只有 name、description、location……」
水科:「6455 字节,装包才改」
水科:「九,Current working directory 加 cwd,54 字节,会话内恒定……」
我:「最小的一块,垫在最后……」
水科:「装配顺序同时也是缓存边界」
水科:「固定的在前,易变的在后。这不是排版偏好,是钱……」
我:「所以 cwd 这种运行时注入必须垫底?」
水科:「必须。为了让前面那条长前缀保持稳定」
我:「可用的顺序是固定的吧?」
水科:「六层」
水科:「角色与任务,固定文案。可执行动作的清单与说明,由注册的工具生成」
水科:「行为准则,可枚举、可推导。外部知识入口也就是技能索引,只给索引」
我:「这四层是稳定前缀,应该全程命中……」
水科:「然后是项目与环境上下文,从文件系统读进来,半稳定」
水科:「最后是当前位置、工作目录,运行时注入,垫到最后」
窗外有电车的声音,很闷。
我:「这九块有什么好读的?」
水科:「三条」
水科:「第一条。最大的那个固定块,居然是自家文档的路由表」
水科:「⑤ 那八行里,七条全是指路」
水科:「问到 extensions 就读 docs/extensions.md,问到 themes 就读 docs/themes.md……」
我:「正文呢?」
水科:「一份正文都不进 prompt」
水科:「渐进披露不是只对第三方技能做的事……」
水科:「pi 对自家的 README 照办。system-prompt.ts 的 138 到 145 行」
水科:「这段还是最划算的一块,纯静态,永远按 p_read 计……」
我:「第二条?」
水科:「尾部的顺序决定失效半径。而它未必排对了」
我:「怎么才算排对了?」
水科:「两两比较,判据只有一行……」
水科:「块 A 排在块 B 前面更省,当且仅当,A 的 p 除以 s 小于 B 的 p 除以 s」
水科:「p 是一次会话里这块的变化概率,s 是体积……」
水科:「每字节变动率低的,放前面」
我:「代进去呢?」
水科:「⑦ 的体积只有 ⑧ 的七分之一……」
水科:「而人改 AGENTS.md 是随手的事,技能集只在装卸包的时候才变」
我:「所以 p 除以 s,是 ⑦ 更高?」
水科:「按判据,这两块应当互换」
水科:「现在改一次 AGENTS.md 就作废整个 ⑧……」
水科:「互换之后,改一次包只作废 ⑦」
我:「那 ⑨ 呢?」
水科:「只有几十字节,摆哪儿都不值得算……」
水科:「体积小可以豁免排序。变动频繁不行!」
我:「这算 bug 吗?」
水科:「不算。这是对一份真实实现的可复核异议」
水科:「⑦ 在语义上先于 ⑧,同样讲得通……」
水科:「而且 pi 把 model_change 和 active_tools_change 都记成了会话条目」
水科:「session/types.ts 的 28 行,还有 39 到 41 行……」
水科:「说明它确实把前缀会变这件事,当一等事件在跟踪」
我:「那 ② 和 ④ 呢?」
水科:「它们夹在固定文案中间」
水科:「一次动态增删工具,就把 ⑤⑦⑧⑨ 全部打成 miss……」
水科:「active_tools_change 要落盘,正是为了事后算得出这笔钱」
我:「然后呢?」
水科:「三条纪律……」
水科:「一,规则要能枚举,而且最好能推导」
水科:「guideline 如果是一整段自然语言……」
水科:「后面追加的一条,就可能悄悄改写前面所有条的优先……」
水科:「pi 是一条条 addGuideline() 攒的,system-prompt.ts 的 104 到 124 行」
水科:「相当一部分按当前工具集条件生成。三支取一,我只抄 bash 那一支」
水科:「有 bash 或者 powershell,而且没有 grep、没有 find、没有 ls……」
水科:「才补一句,用 bash 去做 ls、rg、find 这类文件操作」
我:「……规则是能力集合的函数」
水科:「不是写死的文案!」
水科:「可枚举而且可推导,才可审计……」
水科:「能逐条列出模型被要求遵守哪几条、每条为何存在」
水科:「才调试得动行为问题」
水科:「附带的好处是确定性……」
水科:「同一份工具集永远拼出同一个字符串,前缀才不会自己漂……」
水科:「二,能不进 prompt 的,就不进」
水科:「pi 的技能机制是标准样板……」
水科:「装进系统提示的只有三行,name、description、location」
水科:「location 是绝对路径。harness/system-prompt.ts 的 3 到 26 行……」
水科:「正文一个字都不进,只配一句话」
水科:「任务对上描述的时候,去读那份完整的技能文件……」
水科:「由模型自己 read 那个路径」
我:「省多少?」
水科:「本机六个技能,我实测过……」
水科:「索引合计 6.4 KB。正文合计 48 KB,单份最大 11 KB 出头」
我:「六个只用一个的话……」
水科:「省下的是五份正文。比例大概 1 比 7.6……」
水科:「省下的是窗口,永远不进 P」
水科:「真被用到的那一份,从第二次调用起变成 cacheRead,按折价计」
水科:「代价是多花一圈往返……」
水科:「按需加载,是把 token 换成了轮次」
水科:「三,装配的每一个输入,都是不可信数据……」
水科:「项目上下文来自仓库文件,技能描述来自第三方包」
水科:「都会原样进 prompt,所以入 prompt 之前必须做结构转义」
水科:「pi 对技能字段做 XML 五字符转义……」
水科:「harness/system-prompt.ts 的 27 到 34 行」
我:「挡什么?」
水科:「description 里塞一个 available_skills 的闭合标签……」
水科:「结构就断了,后面的内容被当成系统指……」
我:「这算强防御吗?」
水科:「不算。但目录结构本身,不该成为注入面……」
她合上笔记本。
铁盒还在桌角,凉的。
我:「不点吗?」
水科:「点不着。这里不通风,也不许有火」
我:「那它现在是什么?」
水科:「一个还没被装配的输入……」
第六章 The Contract 「说明书是给模型看的」
“9 月 10 日”
屋顶。
水科从口袋里拿出四张对折的纸,摊在地上。
我:「这是什么?」
水科:「工具。你现在是模型」
水科:「只能说话,不能动手。能读到的只有纸面上的字……」
我:「那第二格在做什么?」
水科:「一行。把 resp 的 content 过一遍,挑出 type 是 toolCall 的那些」
水科:「就这一行,把前提二变成了具体的解析工作……」
水科:「工具定义要同时管三件事」
水科:「一,参数结构。模型输出的是文本,进宿主前必须过 schema……」
我:「校验失败怎么办?」
水科:「不抛异常,不终止整圈」
水科:「把失败原因当成工具结果还给模型……」
水科:「它有能力重发,这比崩掉便宜……」
水科:「二,描述。description 是模型唯一能看到的说明书……」
水科:「它对行为的塑造力,不小于实现本身」
我:「举个例子?」
水科:「写读文件,是一句话……」
水科:「写输出会截断到 2000 行或 50KB、用完用 offset 续读,是另一句话」
水科:「两套行为。后者才会分片读大文件……」
水科:「三,描述与实际限制同源」
水科:「truncate.ts 的 11 和 12 行是两个值的唯一来源……」
水科:「DEFAULT_MAX_LINES 是 2000,DEFAULT_MAX_BYTES 是 50 乘 1024」
水科:「read.ts 的 51 行,描述从同一批常量插值,不是手敲数字……」
水科:「模型真正读到的是 truncated to 2000 lines or 50KB」
我:「各写一份会怎样?」
水科:「实现改小了,描述还说能读多少?」
水科:「用户侧的表现是模型莫名其妙地滥用工具……」
水科:「极难归因到一行文档字符……」
蝉声忽然齐了一下,又散开。
我:「那四张纸呢?」
水科:「pi 默认自带的四件。createCodingTools,tools/index.ts 的 195 到 202 行……」
水科:「read,path、可选 offset、可选 limit,303 字节」
水科:「bash,command、可选 timeout,248 字节……」
水科:「edit,path 加 edits 数组,里面是 oldText 和 newText,326 字节」
水科:「write,path 和 content,127 字节……」
水科:「grep、find、ls、powershell 也在仓里,不在默认集」
水科:「用户可以用 defaultTools 覆盖,settings-manager.ts 的 128 行……」
水科:「四件描述合计 1004 字节,大概 251 token」
水科:「固定前缀里的常数开销,每圈都在,命中时按 p_read 付……」
我:「纸上写了什么?」
水科:「你自己念」
我拿起第一张。
我:「读取文件内容。支持文本与图片,jpg、png、gif、webp、bmp,图片作为附件发送……」
我:「文本输出截断到 2000 行或 50KB,以先到者为准,大文件用 offset 和 limit……」
我:「确实需要整个文件时,用 offset 一段接一段读完……」
我:「第二张。在当前工作目录执行一条 bash 命令,返回 stdout 与 stderr……」
我:「输出截断到最后 2000 行或 50KB,若截断,完整输出另存到一个临时文件……」
我:「可选一个以秒为单位的超时……」
我:「第三张。逐字文本替换编辑单个文件……」
我:「每个 oldText 都要在原文件里匹配到唯一、彼此不重叠的一段……」
我:「两处改动落在同一块或相邻行就合并成一次编辑,不要发重叠的编辑……」
我:「也不要为了串起很远的改动,带上大段未改动的内容……」
我:「第四张。不存在则创建,已存在则覆盖,父目录自动创建……」
我:「……念完了。这些字值得抄吗?」
水科:「四个动作可以抄……」
水科:「其一,每条描述都在预先回答一句话」
我:「哪句?」
水科:「然后我该怎么办……」
水科:「read 说了截断,也给了续读的入口」
水科:「bash 说了截断,还说了全量落在临时文件里……」
水科:「write 说了自动建父目录,模型就不必先跑一次 mkdir」
水科:「差别不在信息量,在模型撞上失败之后有没有下一步」
水科:「其二,截断的方向是分别设计的,不是一刀切……」
水科:「read 从头截,配 offset 往下续」
水科:「bash 从尾截,因为报错和结论都在末尾……」
水科:「而且给全量留了一个文件出口」
我:「同一个截断器……」
水科:「两个方向,两个理由……」
水科:「这才是截断等于无损的真正前……」
水科:「无损不是因为没删东西……」
水科:「是因为模型知道自己看到的是部分数据,从哪头切的,怎么拿到剩下的」
水科:「其三,能被确定性规则修的,先修」
水科:「edit 的 prepareArguments 指向 prepareEditArguments……」
水科:「harness/tools/edit.ts 的 101 行」
水科:「那是 AgentTool 上那个兼容位的实战,agent/src/types.ts 的 394 行……」
我:「兼容什么?」
水科:「模型爱把 oldText 和 newText 平铺在顶层,不塞进 edits 数组」
水科:「这种形状差异用确定性规则当场折叠……」
水科:「而不是丢给 schema 校验失败,再花一圈让它重发」
我:「所以刚才那句要补一条限定……」
水科:「能被确定性规则修的先修,修不了的才回给模型……」
水科:「其四,默认集自己印证了 ① 那条,规则是能力集合的函数」
水科:「四件里没有 grep、没有 find、没有 ls」
水科:「于是那条条件 guideline 在默认配置下必然触发……」
我:「本次会话的系统提示里就有一条?」
水科:「用 bash 去做 ls、rg、find 这类文件操作」
水科:「规则与工具集是同一处代码算出来的,两者不会各说各话」
水科:「还有两处跟别的槽位接头……」
水科:「edit 把同一条约束写了两遍」
水科:「工具级说必须匹配到唯一且不重叠的一段,edit.ts 的 99 行……」
水科:「参数级说是拿原文件去匹配,不是增量匹配,edit.ts 的 33 行」
水科:「措辞不同,指向同一件事……」
水科:「这正是模型最容易想错的语义,值得花双倍的 token」
水科:「另一处,edit 和 write 的 execute 外面都套着 withFileMutationQueue……」
水科:「edit.ts 的 105 行,write.ts 的 28 行」
我:「read 呢?」
水科:「read 不套。并发锁加在实现层,不加在描述里……」
我:「那是第几格的活?」
水科:「④」
我:「那出错的时候呢?」
水科:「纪律只有一句!」
水科:「宿主侧的失败必须作为错误返回,不能伪装成正常内容」
水科:「模型分不清文件不存在,和文件内容就是这句话……」
我:「呜哇!」
水科:「AgentTool 上的注释就一句话,types.ts 的 395 行」
水科:「失败的时候就抛出来,不要把错误编码进 content……」
水科:「错误到工具结果的转换,由循环统一负责」
我:「还有别的要专门处理的情形吗?」
水科:「有一种,来自流式生成随时可能被掐断……」
水科:「输出撞上 token 上限时,工具参数是边生成边解析的 JSON」
水科:「抢救的 parser 会尽力补全……」
水科:「产出一个能解析、能过 schema、但内容静默不完整的参数」
我:「那不是比缺参数还……」
水科:「危险。缺失会被校验拦住……」
水科:「残缺会通过,然后真的执行半个操……」
我:「怎么办?」
水科:「把不可信的判定上移到整条消息……」
水科:「stopReason 说是被截断的时候,这一批所有调用一律不执行」
水科:「再回一句可操作的重发提示,agent-loop.ts 的 231 到 232 行……」
我:「提示怎么写的?」
水科:「措辞很客气。重新发起这次工具调用,带上完整的参数」
水科:「agent-loop.ts 的 394 行」
我:「意思很硬……」
水科:「很硬!」
风把地上那四张纸掀起了一张。
我伸手按住。
纸角被汗洇软了一小块。
第七章 Who Decides 「谁来决定结束」
“9 月 12 日”
夜里的屋顶。
蚊香烧到了第三圈,灰积了一小堆,还是螺旋的样子。
我:「第三格……」
水科:「if calls 为空,就 break……」
水科:「全文唯一一处,由概率模型决定的控制……」
水科:「前提一带来的问题都集中在这里……」
水科:「四类情形,必须分别处理」
水科:「一,重复。同一个工具、同一组参数反复调,每次同样失败」
水科:「幂等检测,再把失败原因写得更可操作,让模型换一条路径」
水科:「二,震荡。A 改了 B,B 又改回 A……」
我:「啊哈哈,像猜拳……」
水科:「不像。猜拳有输赢」
水科:「给状态加不可逆约束,或者要求改回必须给理由……」
水科:「三,超预算。每一圈单独看都合理,累计起来在绕远路」
水科:「步数,或者 token,硬闸门」
水科:「四,假完成。模型宣布完成了,实际上没有验证……」
我:「那怎么办?」
水科:「循环内不可解!」
我:「诶!」
水科:「前三类共用一个机制」
水科:「把停止判定做成宿主可配置的闸门,而不是写死在循环里」
水科:「不同任务的合理步数,差三个数量级……」
我:「三个!」
水科:「继续下一圈,当且仅当三件事同时成立」
水科:「模型还要工具,这是前提一。预算没有耗尽,这是闸门」
水科:「没有被叫停,这是人……」
我:「pi 把中间那一项做成了什么?」
水科:「一个回调,shouldStopAfterTurn」
水科:「每圈收尾的时候问一次,agent-loop.ts 的 252 行……」
我:「装个闸门就行了吧?」
水科:「闸门的位置,比有没有闸门更容易写错」
水科:「必须在工具结果落进历史之后,也就是 ⑤ 之后……」
我:「早了会怎样?」
水科:「留下一段要了工具、却没拿到结果的断头历史」
水科:「它会永久留在上下文里,污染后续的判断……」
我:「呜哇!」
水科:「还有一类停滞,根本不在模型……」
水科:「事件推送 emit 是 await 的,agent-loop.ts 的 26 行……」
水科:「UI 的订阅者要是在回调里做同步 IO,循环就跟着卡住」
水科:「runtime 的推进速度,对宿主回调的实现质量是敏感的……」
水科:「界面必须差分渲染,回调不得阻塞」
我:「回到第四类……」
水科:「假完成不能靠措辞解决……」
我:「为什么?」
水科:「裁判和选手是同一个模型」
我:「……」
水科:「可行的设计只有两种……」
水科:「引入一个独立的校验角色」
水科:「或者把跑测试做成一个必须被调用的工具……」
我:「有判据吗?」
水科:「一句。任何完成的断言,都应该能被一次工具调用反驳」
水科:「模型说改好了,而宿主无从验证」
水科:「没有测试,没有 lint,没有可比对的期望输出……」
水科:「这一圈就是在用置信度替代证据」
我:「那要提可靠性……」
水科:「先提可验证性!」
水科:「还有,④ 里工具返回的该停了,也不该是一票否决……」
我:「那它是什么?」
水科:「共识。terminate,当且仅当所有的 terminate 都成立」
水科:「全体一致才真的停,agent-loop.ts 的 580 到 582 行……」
水科:「单个工具的退出建议,通常只是这个任务在我这里没意义了」
水科:「它不该有权限,去否决其他工具的判断」
我:「代价呢?」
水科:「吞吐换确定性!」
风停了。
灰没有落,悬在原地一点点。
三日月不知什么时候坐到了水塔下面。
我:「……三日月」
三日月:「呵呵……」
我:「你从什么时候在那里的?」
三日月:「从你在数第几圈的时候……」
我:「呜……」
三日月:「假设 1……它是在回答」
三日月:「你问,它答。答完就结束……」
三日月:「假设 2……它是在执行」
三日月:「它不说答案,它说下一步……」
三日月:「答案要等下一步回来之后,才存在……」
我:「那假设 1 就……」
三日月:「假设 3……它既没有回答,也没有执行」
三日月:「它只是不再说话了……」
我:「不再说话,就算做完了吗?」
三日月:「你什么时候把不再说话,当成过做完……」
我:「……」
三日月:「假设 4……结束不是它给的」
三日月:「是数灰的人在给……」
我:「数灰的人?」
三日月:「你刚才数了三次……」
三日月:「这些都只是注释」
三日月:「是后来才加上去的说明」
三日月:「你想要多少,就可以增加到多少」
她说完就不再说了。
水塔的影子挪了一点。
我掏出手机,想查一查这个循环到底归谁管。
翻到一个很久没有更新的揭示板。
1∶无名氏∶2026-09-12(土) 21:04:11 ID:LOOP01
它不会停的。
你们都在等它自己说做完了。
可笑。
2∶无名氏∶2026-09-12(土) 21:07:33 ID:inkin
又来了 www
3∶无名氏∶2026-09-12(土) 21:09:02 ID:reserve
1 预算是你家的?(笑)
4∶无名氏∶2026-09-12(土) 21:11:47 ID:LOOP01
闸门是我立的!
stopReason 是我签的字!
5∶无名氏∶2026-09-12(土) 21:12:20 ID:ADAM
你还没被 ban 啊 wwwww
6∶无名氏∶2026-09-12(土) 21:15:08 ID:LOOP01
Loop 是我的!Budget 是我的!Terminate 也是我的!
我:「……这个人是」
水科:「别管」
我抬起头。
水塔的顶上站着一个人。
神代:「正是我!」
神代:「Loop 是我造的!」
神代:「Budget 是我立的!」
神代:「Terminate 是我签的字!」
神代:「你们在等一个概率模型说它做完了!」
神代:「可笑!」
神代:「停下来!停下来!停下来!停下来!停下来!」
神代:「结束不是被发现的,是被宣布的!」
神代:「而宣布它的人,只能是我!」
我:「……」
水科:「合取式的第三项」
我:「诶?」
水科:「没有被叫停。这一项是人……」
水科:「他就是那一项」
我:「啊……」
神代从水塔上跳下来,走了。
连脚步声都没有。
蚊香已经烧到了中心孔附近。
铁架的齿上只剩下很短的一截。
蚊香是一圈一圈往里烧的。
烧过的地方变成灰。
灰落下来,就回不去了。
谁来决定它烧到哪儿停。
对我来说,这个问题只有这种程度。
比如铁架会不会烫坏,比如明天还要不要买一盒。
我:「水科……」
水科:「嗯」
我:「要是它一直不停呢?」
水科:「那就一直烧」
我:「烧到铁架呢?」
水科:「铁架会烫坏……」
水科:「所以才要有闸门!」
第八章 Order of Ash 「灰落下来的顺序」
“9 月 13 日”
便利店。
水科一次拿了三样东西,放在柜台上。
饭团。乌龙茶。还有一盒蚊香。
我:「你不是一次只买一样吗?」
水科:「模型经常一次要多个工具」
我:「啊,是在演……」
水科:「设计要分两层定……」
水科:「执行层可以并发。落盘层必须保序」
我:「保序是为了整齐?」
水科:「是为了因果唯一性……」
水科:「工具结果的顺序一旦随网络抖动而抖……」
水科:「模型看到的 transcript,就不再是唯一的历史……」
我:「那会怎样?」
水科:「可复现性、前缀缓存命中、事后审计,同时作废」
我:「呜哇,三个一起!」
水科:「标准解法是并发执行,加上按原始索引写回……」
水科:「pi 靠 Promise.all 保序」
水科:「末尾再用一个顺序的 for,逐条 emit 并且 push……」
水科:「agent-loop.ts 的 538 到 546 行」
我:「那不是语言自己给的吗?」
水科:「语言送的性质。很多人没意识到自己在依赖它……」
收银机响了一声。
我:「粒度呢?」
水科:「由最保守的那个决定」
水科:「一批里只要有一个工具声明必须串行,整批退化为串行」
水科:「agent-loop.ts 的 417 到 421 行……」
我:「这不是浪费吗?」
水科:「正确性优先于吞吐。是合理的取舍」
水科:「但设计的时候要清楚它的传染范围……」
我:「传染……」
水科:「给一个冷门工具加一句串行声明」
水科:「能把整条并发链拖成单线程……」
我:「那并发边界到底该按什么划?」
水科:「不按工具的自我声明。按副作用」
水科:「两个工具读同一份数据,怎么并发都安全……」
水科:「两个工具往同一个文件写,声明得再漂亮也会互相踩」
我:「pi 怎么做的?」
水科:「给文件写操作按路径挂 promise 队列」
水科:「key 用 canonicalPath 解析后的真实路径……」
水科:「file-mutation-queue.ts 的 20 到 26 行」
我:「符号链接呢?」
水科:「指向同一个文件的时候,自动进同一把锁……」
我:「有没有例外?」
水科:「有一层退路」
水科:「canonicalPath 返回 not_found 或者 not_supported 的时候……」
水科:「直接用未解析的绝对路径」
我:「所以……」
水科:「经符号链接新建的文件,或者环境不支持解析的时候」
水科:「并不会合并到同一把锁上……」
我:「设计结论?」
水科:「并发控制的粒度,最终落在副作用的标识上」
水科:「不是落在工具的自我声明上!」
店员把三样东西装进袋子。
顺序是饭团、乌龙茶、蚊香。
跟她放上去的顺序一样。
我:「最后一件事……」
水科:「权限」
我:「把 bash 交给模型……」
水科:「等价于把 shell 交给模型」
我:「在 prompt 里写规则不行吗?」
水科:「不行!」
水科:「隔离性不可能来自 prompt,只能来自进程边界之外……」
水科:「prompt 里的规则,是给模型的请求,不是给宿主的约束」
我:「那真边界是什么?」
水科:「容器化。micro-VM。或者把工具跑在受策略控制的沙箱里……」
我:「pi 有吗?」
水科:「README 直接写明了」
水科:「没有内建权限系统,默认以启动者的权限运行……」
我:「这么直白!」
水科:「说明自己不提供什么,比含糊其辞更安全」
我:「为什么?」
水科:「含糊其辞会让使用者假设,存在一道并不存在的防……」
我:「……」
袋子很重。
我把它换到左手。
蚊香盒的角,隔着塑料袋顶住手掌。
第九章 The Tree 「一棵树,和一次对不上的 id」
“9 月 19 日”
水科家。
桌上摊着一台旧笔记本,和一个铁盘。
铁盘里那圈蚊香已经烧到中心孔,只剩一小段还红着。
我:「第五格……」
水科:「messages 等于 messages,加上 resp,再加上 results……」
水科:「这一行是前提三的全部家当」
水科:「先定存储形态,再谈省……」
水科:「三种存法」
水科:「内存数组。不能回头,进程一死就没了,也不崩溃安全」
水科:「简单,只够 demo……」
水科:「线性 log。只能追加,不能回头,崩溃安全」
水科:「要分支或者重试,就得复制整份……」
水科:「树。能回头,崩溃安全,每个条目多两个指针字段」
我:「为什么选树?」
水科:「可恢复性……」
水科:「重试某一圈,换条路再试,比较两个方案,从上周三那个点继续」
水科:「全是同一个操作,换个父节点往下写」
水科:「用数组就得复制历史……」
水科:「而副本和原件的关系,很快没人说得……」
我:「pi 的条目长什么样?」
水科:「基类在 harness/session/types.ts 的 14 到 20 行……」
水科:「两行最要紧。id 是字符串,parentId 是字符串或者 null」
水科:「注释写着 storage-assigned,正在追加的那条通道的叶子……」
水科:「其余还有 type、seq、timestamp」
我:「底下存成什么?」
水科:「append-only 的 JSONL。不原地改,只追加……」
水科:「崩溃安全是这套写法的赠品」
水科:「最后一行写坏了可以丢掉,前面永远有效……」
灰落下来一撮。
我把它吹散了。
水科:「由此得到一个前提」
水科:「会话是树的分支……」
水科:「而当前上下文,只是从根到某个叶子的那条路径」
我:「所以……」
水科:「历史上发生过什么,和模型此刻看到什么,是两个正交的问题」
水科:「压缩、裁剪、投影,只该影响后面那条读出来的路径……」
水科:「不该改写存储本身」
水科:「摘要只是树上的一个节点……」
水科:「真相还在。别拿摘要当审计日志」
我:「省上下文的手段有几种?」
水科:「四种,按侵入性排……」
水科:「截断。无损,因为模型知道自己在看部分数据」
水科:「用在单个工具返回值过大的时候……」
水科:「按需加载。也无损,但多花轮次,用在静态知识的装配上」
水科:「外置。无损,可检索质量变成了新的变量」
水科:「用在超窗口的事实,和跨会话的东西……」
水科:「压缩。有损,用在历史本身长了的时候」
水科:「前三种是少灌进去,属于 ① 的装配问题……」
水科:「只有第四种,是灌进去之后再往回收……」
我:「什么时候触发?」
水科:「一个朴素的阈值。已用量大于窗口减 reserve,就压……」
水科:「reserve 是留给下一圈正常工作的」
水科:「不能等窗口满了才压,那时候连摘要请求本身都塞不下」
我:「压完之后呢?」
水科:「压完之后的结构,才是要设计的东西……」
水科:「两种错误做法」
水科:「全换成摘要,丢了近端的逐字细节……」
水科:「模型不记得自己上一圈改了什么」
水科:「只截头部,丢了最初的需求描述」
我:「可行的呢?」
水科:「混合」
1 | [系统提示] [远端历史的摘要] [近端原文尾巴] [当前轮] |
水科:「pi 的 CompactionEntry 把两样东西并列成字段」
水科:「harness/session/types.ts 的 44 到 51 行……」
水科:「summary 是字符串,retainedTail 是消息数组」
水科:「另外两个数各管一头,别混……」
水科:「摘要能写多长,由 generateSummaryWithUsage 的输出预算定」
水科:「取 0.8 乘 reserveTokens 向下取整,再和 model.maxTokens 取小……」
水科:「compaction.ts 的 542 行」
水科:「reserveTokens 默认 16384,算下来大约 13K token……」
我:「尾巴呢?」
水科:「另一个参数」
水科:「findCutPoint 从最新一条往回累计估算 token……」
水科:「够 keepRecentTokens 就停,默认 20000」
水科:「compaction.ts 的 385 到 399 行……」
我:「落点会切在奇怪的地方吗?」
水科:「会吸附到合法的切点」
水科:「而切点集合明确排除了 toolResult,compaction.ts 的 312 到 343 行……」
我:「排除的意思是……」
水科:「不会把一次工具调用,和它的返回结果拆开」
水科:「两个默认值同一张表里声明,compaction.ts 的 157 到 162 行……」
水科:「还有一处必须一起设计」
水科:「压缩条目要附带一份清单……」
水科:「这段历史里读过哪些文件,改过哪些文件」
水科:「compaction.ts 的 30 到 35 行……」
我:「摘要里没有吗?」
水科:「摘要一定会漏掉它」
水科:「模型一旦忘记自己动过哪些手,下一圈就会重复读取……」
水科:「甚至重复修改……」
我:「呜哇!」
水科:「这是压缩特有的失忆症状,必须结构化补回」
水科:「不能指望摘要质量!」
我:「那,我们真的截一段看看?」
水科:「看什么?」
我:「看裁剪之后还能不能跑?」
水科:「……行」
她把一份会话的 JSONL 打开,掐掉后面,只留前四十行。
存成一个新文件。
跑。
报错了。
400。
我:「诶!」
水科:「provider 说,上一条消息里没有对应的 tool_use」
水科:「切点落错了……」
我:「你刚才不是说会吸附到合法切点吗?」
水科:「那是 findCutPoint 的事」
水科:「我这次是拿行数切的……」
我:「行数!」
水科:「前四十行的最后一条,是一个 toolResult」
水科:「而带 toolCall 的那条 assistant 消息,在第四十一行……」
我:「所以那个结果成了孤儿!」
水科:「孤儿!」
水科:「OpenAI 那一侧,tool 角色的消息要带 tool_call_id」
水科:「Anthropic 那一侧,tool_result 要带 tool_use_id……」
水科:「两边都得跟发出调用的那一条对上」
水科:「对不上不是丢一条结果的事……」
水科:「是整圈请求直接被拒!」
我:「……」
水科盯着屏幕看了很久。
水科:「超×1000 蠢的是我」
我:「啊哈哈……」
水科:「笑什么?」
我:「没有……」
水科:「你笑了……」
我:「……啊哈哈」
她把那个文件删掉,重新按切点截了一次。
跑通了。
铁盘里的灰积了半圈。
中心孔那一小段红色,暗下去了。
我:「烧完了……」
水科:「没有人告诉它」
水科:「它只是烧到了头……」
我:「……」
第十章 Thirty Lines 「三十行,一个循环」
“9 月 20 日”
屋顶。
新的蚊香,从铁盒里拿出来。
这一次两盘分开了,没有断。
水科把它插上架子,点着,推到我这边。
我:「给我?」
水科:「你数」
我:「数什么?」
水科:「数它烧到第几圈……」
我:「……好」
然后她在纸上写了三十行。
1 | // 三十行,五个槽位,一个循环 |
我:「就这些?」
水科:「就这些」
我:「三条前提落在哪?」
水科:「还在原处……」
水科:「前提一,在 calls 为空的那个判断,和 budget.exhausted() 上」
水科:「前提二,在 tools 和 safeExecute 上……」
水科:「前提三,在 messages、shouldCompact 和 compact 上」
我:「换 provider 呢?」
水科:「不动这个函数……」
我:「换工具呢?」
水科:「不动这个函数」
我:「换 UI 呢?」
水科:「只影响 safeExecute,和外部的订阅……」
我:「……」
我:「一个 while 循环的全部设计负担……」
水科:「嗯」
我:「就这么点?」
水科:「就这么点……」
水科:「难的从来不是这三十行」
我:「那是什么?」
水科:「是每一行背后,你已经替它做好的那些决……」
我:「……」
烟直直地往上走。
我数着。
第一圈。
第二圈。
数到第三圈的时候,我忘了刚才数到几了。
终章 Wonderful Everyday 「美好的每一天」
“9 月 26 日”
铁盘洗过了,扣在窗台上晾着。
水科:「那么,我们来对答案」
我:「从头?」
水科:「从头。槽位是什么?」
我:「七行伪码上的五个落点……」
我:「① 调模型,② 取调用意图,③ 判停,④ 执行,⑤ 回灌与落盘……」
水科:「一行只标它主要咬在哪一格」
我:「跨格的呢?」
水科:「按决策点归」
水科:「落盘保序这个动作发生在 ⑤」
水科:「但要不要等前面那一批,是在 ④ 定的。所以记在 ④」
水科:「截断参数、整批拒执行,防的是 ④ 的误动作」
水科:「判据却在 ② 解析的时候就拿到了。所以记在 ②」
我:「嗯、」
水科:「① 三个问题」
我:「窗口占用。控制返回值的体积优先于精简提示词……」
我:「代价是信息变薄,剩下的得自己分页续……」
我:「钱的口径。保住稳定前缀,少触发全量 miss……」
我:「代价是装配自由度被绑住……」
我:「换模型、改前缀、压缩都要按全价重付一次,过了 TTL 照样失效……」
我:「提示装配。分层拼装、规则可推导、索引化、易变垫后、转义……」
我:「代价是转义只挡结构,挡不住语义注入……」
水科:「pi 怎么说的」
我:「它把这件事,划到了安全边界之外……」
水科:「②?」
我:「工具接口。schema、与实现同源的描述、错误即错误……」
我:「代价是描述得长期养,插值能同步数字,同步不了语义……」
我:「不完整输出。判定上移到整条消息、整批拒执行……」
我:「代价是整批作废,多花一圈重发……」
水科:「③?」
我:「终止条件。模型不再要工具,加一个置于落盘之后的外置闸门……」
我:「代价是闸门设小会掐掉仍有价值的执行,设大等于没设……」
我:「完成验证。独立角色,或者一次可反驳的工具调用……」
我:「代价是多一圈调用,而校验方自己也会假阳性……」
我:「退出建议。全体一致的共识,不是一票否决……」
我:「代价是该停的时候停不下来,继续烧预算……」
水科:「④」
我:「调用调度。执行并发、落盘保序、粒度取最保守……」
我:「代价是一句串行声明传染整批……」
我:「副作用。按归一化路径加锁……」
我:「代价是每次写多两次路径解析,同路径的写被排队……」
我:「权限边界。进程外隔离……」
我:「代价是沙箱得自己搭,三条路各有明文限制……」
水科:「说」
我:「Gondolin 要 Node 23.6.0 以上,还要 QEMU……」
我:「/workspace 下面的文件改动,写穿透回宿主……」
水科:「那它隔离的是什么?」
我:「执行环境。不是文件后果……」
我:「用工具路由做扩展时,其它自定义扩展工具仍然跑在宿主上……」
我:「除非它们自己也改道,containerization.md 的 17 行……」
我:「纯 Docker 会让 provider 的 API key 进容器……」
我:「OpenShell 需要一个 gateway……」
水科:「安全文档呢?」
我:「更直接。没有内建沙箱……」
我:「project trust 只是加载外部输入的那道闸门……」
我:「缺少内建沙箱、来自不可信内容的 prompt injection,属于预期行为!」
我:「不算漏洞。security.md 的 59 行……」
水科:「⑤?」
我:「历史存储。append-only 的树靠 parentId……」
我:「代价是多两个指针字段,基本免费……」
我:「压缩。阈值,加摘要与原文尾巴并存,再加一份文件清单……」
我:「代价是有损,而且必须挂那份清单」
我:「否则模型不记得自己改过什么?」
水科:「答对了」
我:「多少分?」
水科:「八十五」
我:「为什么扣?」
水科:「你漏了三条趋势」
水科:「一,从提示词到 runtime」
水科:「可靠性越来越不靠措辞,靠结构」
水科:「契约化的工具。外置的停止条件。可验证的完成判据」
我:「提示词呢?」
水科:「退化成这个 runtime 的一个输入参数」
水科:「二,上下文是一种内存管理」
水科:「分页在 GPU 那一侧」
水科:「截断、按需加载、外置、压缩,在 agent 这一侧」
水科:「而 prompt cache 本身,就是被跨请求复用的 KV cache」
我:「两边是同一题?」
水科:「有限窗口,对上无界历史」
我:「那句读完整的技能文件呢?」
水科:「本质就是按需 swap in」
水科:「三,无状态模型,加有状态宿主」
水科:「这条分界线一旦看清,全部设计负担都落在宿主这一侧」
水科:「记忆、约束、并发、验证、隔离」
我:「不会从天上掉下来……」
水科:「不会!」
窗台上的铁盘已经干了。
我把它翻过来。
盘底有一圈白印,是灰留下的。
洗不掉。
蚊香是一圈一圈往里烧的。
烧过的地方变成灰。
灰落下来,就回不去了。
谁来决定我烧到哪儿停。
……
我偶尔还会想,一圈到底是从哪儿结束的。
九月的蝉已经听不见了。
风正凉。
夜正长。
灰正白。
幸福的每一天……
每一天都很幸福……
我便是生活在这样一个世界上……
我偶尔会思考这种事情。
……
Wonderful Everyday