ARTICLE · 1102202
AI Harness 工具延迟加载:几万字工具说明书,压缩成几十行,token 成本究竟是怎么降下来的?


👉目录
1 上下文窗口的常驻开销构成:工具定义,而非对话历史
2 延迟加载的机制本质:schema 与名称分离
3 工具自带的暴露声明:可延迟、钉死常驻、缺省兜底三态
4 只读场景下的工具暴露:以问答模式为例,仅 5 个读搜工具
5 请求体的三段结构:system、tools、messages 各自装什么
6 工具查询的 JSON 形态:tool_search 的精确查与模糊查
7 工具执行的 JSON 形态:defer_execute、参数校验、权限与钩子
8 延迟加载的成本收益核算:省在每轮常驻,付在偶发往返
9 延迟加载与确定性的解耦设计:不确定性外置,缓存前缀内守
10 写在最后
工具数量与选择准确率成反比。
行业侧的材料反复提到同一个拐点:工具数超过三十到五十个之后,挑错工具、给错参数的比率明显上升;两个工具名字相近时更严重,比如通知发送里的 send-user 与 send-channel。
比选错更贵的是常驻开销,每个工具都自带一份说明书——用途、参数、类型与必填性,它必须放在模型能看见的地方,而那个位置是整份请求里最贵的。
于是就有了这条路:只把名字留在上下文里,把说明书请出去,等真要用再取回来。
这条路的通用名是渐进式披露(Progressive Disclosure);
落到工具定义这一层,我们称之为延迟加载(Deferred Tool Loading),defer_loading 就是给单个工具打这个标记的开关。
在AI Harness 里,它落到两个动作上
用tool_search 查、用 defer_execute 调,下面从账算起,一路讲到底层设计。
01
上下文窗口的焦虑,多数花在了对话历史的长短上:聊太久了要不要压缩、历史消息要不要摘要、几轮之前的文件内容该不该丢。
这些当然要管,但真正每轮次恒定占用、一开口就已经占住的那一块,是工具定义。
量级先摆出来。
相关发布方文档里给过一个示例:一台机器接上五个服务端、拼出五十八个工具,说明书加起来约五万五千词元,用户还没打字,这笔钱已经付掉了;其中单个服务端最大的一份,三十五个工具吃掉约两万六千词元。
假设一次实测取到六十条工具描述,合计十一万一千一百一十九个字符;最长的一条一万八千六百七十一个字符,占掉可延迟部分总额的百分之四十五。
这笔开销的性质是每轮次固定支付:不随对话推进而衰减,也不管这一轮用不用得上,这一句只是想改个错别字,那份视频生成的说明书照样在场。
第二重代价落在选择准确率上:发布方公布的自测里,同一批基准任务在工具全量常驻时选择准确率百分之四十九,改成按需发现后升到百分之七十四;另一代模型上从七十九点五升到八十八点一。
延迟加载省下的不只是钱,还让模型在选择时少看一批无关选项。
两重代价对应三个要解的问题
一是省 token:说明书每一轮都在付钱;
二是控窗口:工具定义是固定占用,还会随接入的工具集增长,压住它才能把窗口留给对话;
三是稳前缀缓存(Prefix Cache):它位于请求最前、体量最大,而它的字节必须逐轮不变,才谈得上命中。
顺带破一个误解:很多人以为工具清单藏在注入块里,就是请求开头那一大段系统提醒、身份上下文之类的文本,显然不是。
tools[] 与 messages[] 是同级的顶层字段,工具清单压根不在 messages 里面;宿主注入的那几个文本块逐个插到最后一条用户消息的内容数组最前面,里面没有工具清单,清单走的是另一条通道,在哪是下一章的事。
工具定义是每轮次固定支付的常驻开销,比对话历史更长、更贵,也更少被计入上下文的账;它不只花钱,还拖低模型的选择准确率。
02
Progressive Disclosure | ||
Deferred Tool Loading | ||
defer_loading | ||
Defer(X) | ||
tool_search | ||
defer_execute | ||
tool_names | ||
queries |
属于工程的是四件——覆盖层、多层合并、取回工具自动补全、分级降级,后面几章分别落到它们。
机制本身一句话:让名称留在上下文,让 schema 离开上下文。
落地方式有两处反常识的分离。
第一处:那份工具名单既不是一条消息,也不是一个注入块,而是 tool_search 自己 description 字段里的一段文本
名字经工具描述这条通道送到模型眼前,根本不经过 messages[]。这就是上一章那个悬念的答案。
第二处更直接:被打了延迟标记的工具,它的完整 parameters 块会整条从 tools[] 数组里消失。
模型能看见的,只剩一个名字,用 JSON 把这两处分离画在同一张图上:
{"tools": [{"type": "function","function": {"name": "ImageGen","description": "这一段在延迟之后不再进入上下文","parameters": { "...": "这一整块从数组里消失" }}},{"type": "function","function": {"name": "tool_search","description": {"lines": ["Load tool schemas before invoking deferred tools.","…","The following deferred tools are available via tool_search…","<available_deferred_tools>","TaskStop","SkillManage","LSP","ImageGen","VideoGen","Workflow","Monitor","</available_deferred_tools>"]},"parameters": {"tool_names": ["ImageGen"],"queries": ["图片生成", "image generation"]}}}]}
第二段那个 description 就是名单本体,一个工具的自我介绍里夹带的一张清单,上面为了便于理解,把它按行拆成了数组;实际它是一整串文本,各行之间用换行符连接。
顺带一个后果:名单既然长在工具描述里,它就和其他工具定义一样落在 tools[] 数组、落在缓存前缀里——名单的字节一变,整段缓存作废。
而这段描述并非硬编码字符串,是一段带条件判断的模板渲染出来的,比如WorkBuddy使用Python Jinja作为模板引擎,我们可以使用AI Harness 动态治理引擎:为什么是Starlark?
名单只是模板里的一个变量:注册表里有什么,它就列出什么,而注册表在运行期会被改动。
也就是说,延迟加载把 schema 移出了缓存前缀,却把名字留在了里面,省下的是重的那部分,留下的是一个必须逐轮稳定的轻量索引,改动从哪来,下一章讲。
一处容易被忽略的自保:覆盖层里只要出现一条 Defer(X),tool_search 与 defer_execute 就会被自动补进工具名集并钉死为不延迟。
否则即成自举死锁,把工具延迟了,又把取回与调用的入口也延迟了,谁都取不回来。
延迟加载不是把工具藏起来,而是把 schema 移出常驻——名称留在上下文里供检索,schema 命中之后才展开。
03
上一章讲延迟怎么发生,这一章回答更前置的问题:谁有资格被延迟? 工具自己说了算。
每个工具在随包配置里自带一个暴露属性 defer_loading,这个属性是三态,不是两态。
defer_loading: true | |
defer_loading: false | |
不是所有工具都能延迟。
可延迟的那些,集中在低频且重型的那些——多模态生成、定时任务、组织协作、工作树切换、可观测性上报、深度检索代理这一类,共同点是 schema 很长,但一轮对话里可能一次都用不上。
而高频核心工具留在常驻位,读写文件、搜索、执行命令、抓取网页这些,每一轮都可能被用到。把它们延迟掉,等于把多一次往返的成本,累加到最热的那条路径上——省下来的上下文,还不够赔上反复检索的延迟。
这就是按使用频率分层:判断线不是工具重不重要,而是它这一轮会不会被用到,一条能力当然重要,但三个月才用一次,让它每轮都在场就不划算。
除频率之外还有第二条判断线:来源——随包固定的工具,说明书一个版本一个样子;
运行期由模型自建的工具,说明书是模型现写的一段自由文本,长度、措辞、结构都不受控。
前者可以进常驻,后者只能进延迟集合,后者的理由不只是省空间,还有字节稳定性,第九章单独讲。
三态声明落到 JSON 上,是这样:
[{ "name": "标为按需的工具", "defer_loading": true },{ "name": "钉死常驻的工具", "defer_loading": false },{ "name": "缺省由总开关兜底", "...": "不写 defer_loading 这个键" }]
补一档前面没提的:查询用的 tool_search 与执行用的 defer_execute,在目录里都不写这个键
它们落在缺省那一档,真正让它们每轮在场的是覆盖层,不是自身声明(第二章那处自保讲的就是这件事)。
除工具自身声明,覆盖层还提供 Defer(X) 这类工具名级语法,支持通配与两端锚定,优先级法则是不延迟优先于延迟——与权限系统里拒绝优先于允许同理,否决权高于强制权。
但暴露声明只回答了一半:它决定说明书放不放进常驻,没决定这次会话允许模型看见哪些工具,后者才真正体现工程判断,用一个具体场景来讲。
04
工具除了暴露属性 defer_loading,还要暴露只读,只写等属性,为什么呢?
因为有一类场景很明确:用户只是提问、聊天,要的是一个答案,不是要你动手——问答模式。这个场景下模型被允许看见几个工具?基本上只有如下5个工具:Read、Glob、Grep、WebFetch、WebSearch
五个清一色是读与搜,一个写工具都没有,会改变系统的那些——Write、Edit、Bash、PowerShell——根本不出现:不是排在后面,不是被劝告少用,是不在表里。
这里有一条容易被忽略的分界:只读是工具级的隔离,不是提示词里的劝说,在系统提示词里写一句你只能读、不能改,那叫软约束——它的失败率随交互轮数累积上升:
假设单轮内模型被说服、去改一个只读场景下的文件,概率只有百分之一——这已经很乐观:
{"单轮失败概率": 0.01,"50 轮后至少失败一次的累计概率": "39.5%","100 轮后的累计概率": "63.4%"}
单轮概率压到千分之一,乐观十倍;但轮数也涨十倍:
{"单轮失败概率": 0.001,"1000 轮后的累计概率": "63.2%"}
单轮概率砍到十分之一,只要轮数也涨十倍,总失败率几乎不动, 这条曲线只跟这段对话有多长有关,跟那句劝告写得多严厉几乎无关。
结构约束的失败概率是零——不是更小,是根本没有这条路径:写工具不在名单里,模型连提议调用它的机会都没有。
这才是最小权限的字面意思:不是允许但劝你别做,是根本没给。
两种名单并列在 JSON 里:
[{"模式": "问答","tools": ["Read", "WebFetch", "WebSearch", "Glob", "Grep"],"含写类工具": false},{"模式": "含写能力的模式","tools": ["...", "Write", "Edit", "Bash", "PowerShell", "..."],"含写类工具": true}]
差的就是 Write、Edit、Bash、PowerShell 这几个名字。它们在不在表里,决定了模型有没有能力改你的文件——这个决定发生在配置里,不发生在那句劝告里。
一处代价:工具被移出名单后,出问题的表现不是权限被拒,而是模型说自己没这个能力——没有任何一条拒绝日志,你甚至不知道它曾经想调用过。
白名单是硬的,提示词是软的,白名单让模型没有写入路径,提示词只让它表现得像只读。
05
前四章讲机制——工具怎么被打标记、怎么被收窄、怎么被延迟。
但这里还有个断层:这些东西最终长在请求体的哪个位置? 一份发给模型的请求,顶层至少有三个关键字段:
{"model": "...","system": "固定系统指令(唯一一份,位于请求最前)","tools": [{"type": "function","function": { "name": "Read", "parameters": {} }},{"type": "function","function": {"name": "tool_search","description": {"lines": ["…","<available_deferred_tools>","TaskStop","SkillManage","LSP","ImageGen","VideoGen","Workflow","Monitor","</available_deferred_tools>"]}}},{"type": "function","function": { "name": "defer_execute" }}],"messages": [{"role": "user","content": [{ "type": "input_text", "text": "宿主注入块(排在用户原文之前)" },{ "type": "input_text", "text": "<user_query>用户原文(数组最后一位)</user_query>" }]}]}
核心结论一句话:tools[] 与 messages[] 是同级顶层字段,tools 不在 messages 里。
三段各司其职:system 装那一份固定的系统指令;tools 装全部工具定义,含延迟名单;messages 装对话内容与宿主注入的块。
三处细节:注入块逐个插进最后一条用户消息的内容数组最前面,所以宿主注入全部排在用户原文之前、用户原文在数组最后,顺序本身就是设计,注入是前置声明,提问是最后一件事;
位置还有一层含义,是后面算账的前提:工具的 schema 位于请求最前面(排序是 system、tools、messages),而这个位置几乎逐轮不变,意味着它是整份请求里缓存命中率最高的一段。
把说明书放在最前面,等于用最贵的位置放最不常用的信息;更麻烦的是它同时是前缀里最大的一块,工具集一变(服务端重连、配置调整、模型自建了新工具),整段前缀缓存就跟着作废,移出去之后,前缀更小,也更不容易失效。
前缀缓存的命中条件与失效边界,另起一篇专讲。
工具定义占的不只是空间,而是请求里最靠前、且几乎逐轮不变的一段——也即缓存命中率最高的位置。
06
第一步:模型怎么把 schema 取回来。
负责这件事的工具叫 tool_search,常驻、永不延迟,假设它自身描述八百八十六个字符,就是用这点代价换回几万字符的常驻开销。
它提供两条通路:
tool_names | |||
queries |
为什么精确查优先?名字是确定的,关键词是猜的,能报出名字就不该让它猜。
也因此,模糊查询额外要求同时给中英文关键词:分词对中文并不友好,单给一个中文词很容易漏召回本该命中的工具。返回条数有默认值与上限,上限只对模糊查询生效;
单次返回还有字符上限——这两条约束第八章算账时会用上,两条通路还是互斥的:恰好给一个,都不给或都给都算参数校验失败,把该用哪个模式收成一条入参约束,比在说明里写一句优先用精确查更可靠。
用 JSON 走完这一步,例子取那条图片生成工具的说明——名字 ImageGen。这里的 arguments 实际是一段 JSON 字符串,为了读得清,文中把它展开成了对象。
第一种,精确查询:名字已经确定,直接报给 tool_names。
{"type": "function_call","name": "tool_search","arguments": {"tool_names": ["ImageGen"]}}
你问WorkBuddy它自己有哪些tool_names,它的回答是:




第二种,模糊查询——不确定哪个工具合适,用关键词试,中英双语各给一份
{"type": "function_call","name": "tool_search","arguments": {"queries": ["图片生成", "image generation"]}}
返回的是一段文本形式的命中结果,其中已经带着目标工具的引用与参数格式——content 同样是一整串带换行的文本,这里按行拆开展示:
{"role": "tool","content": {"lines": ["ImageGen","Parameters:"," prompt (string, required)"," size (string)"," …"]}}
直到这一刻,那份完整的参数 schema 才第一次进入上下文;此前所有轮次里,它都只是一个名字。
查询未命中时有三层降级,每一层都要能写成 JSON 可达的下一步:
一、精确查询没命中,改用模糊查询再兜一次。
二、仍未命中,明确回一句换个说法再试,这一条看着琐碎,但兜底失败也要回人话——静默返回空会被理解成这个能力不存在,模型接着就会告诉用户它做不到。
三、命中了但排在返回上限之外:前若干条给名字加一句短描述,其余只有名字,并附上说明,告诉模型用名字参数可以取全文。
索引层的组装规则是命中与否的前提,名单排法:内建工具组逐行输出名字、按名排序、恒在最前且无首行;外部服务端那一组,首行给服务端描述、超长则截断补省略号,组内只给工具名、不带参数与 schema;一个工具都没有的服务端也单独成组——好让模型知道它存在。
LLM推理响应若超长则截断补省略号?又埋下一个坑,后续单独成篇讲解。
预算是按整组结算的,预算够用时,整组全列——这段名单文本按行拆开是这样的,渲染时以换行符连接成一整串:
{"description_lines": ["<available_deferred_tools>","TaskStop","SkillManage","LSP","ImageGen","VideoGen","Workflow","Monitor","mcp__docs: 文档服务端描述"," create_doc"," read_doc","</available_deferred_tools>"]}
某组放不下,就整组丢弃,末尾追加一行坐标:
{"description_lines": ["<available_deferred_tools>","TaskStop","SkillManage","LSP","ImageGen","VideoGen","Workflow","Monitor","","[other 3 MCP servers with 47 tools can be found in ~/.workbuddy/mcp.json. Use tool_search to discover and invoke them.]","</available_deferred_tools>"]}
一条横贯整个机制的规律:降级的对象是解释,不是入口,名字必留、描述可砍,名字不可推导,
模型不可能从别的信息里猜出一个叫什么的工具存在;描述则是它常能先验猜到的。
砍掉名字,模型不是少知道一点,而是不知道它存在,接下来就会转向编造。
07
查到之后,第二步是把它真正调起来。链路是两步:tool_search 把 schema 拿进上下文,defer_execute 接续调用;两者都常驻,后者的描述假设长度八百二十八字符。
它接续调用的形态,是先发出一条调用,把工具名与参数交给 defer_execute——工具名接着上一步那条,参数按刚取回来的 schema 填:
{"type": "function_call","name": "defer_execute","arguments": {"tool_name": "ImageGen","params": {"prompt": "山顶日落","size": "1024x1024"}}}
然后收到目标工具的执行结果,权限与钩子已经在链路中走过:
{"role": "tool","content": "执行结果"}
这段 JSON 有三处值得讲透。
第一,参数按目标工具的 schema 校验。
跳过查询直接调用一个被延迟的工具,会拿到参数校验错误。这不是配置疏漏,而是机制强制的前后依赖——先取说明书,再调用,是硬性顺序。
报错的样子也值得说:不是一句参数非法了事,而是把该工具的期望 schema 一并回传——模型据此能在一次往返内改对,不必反复试错。
第二,权限检查与钩子照常走。
延迟加载缩短的是上下文,不是治理链路——工具被延迟,不等于权限校验被跳过;defer_execute 转交调用时,目标工具的权限检查与钩子照常执行。
省掉的只是那几万字符的常驻空间,不是任何一道安全关卡。
还有一处细节:守卫看到的是被调用那个工具的名字,不是 defer_execute 这个包装器的名字——否则等于把工具名从守卫的视野里藏起来,所有按名字写的权限规则会整片落空。
第三,代价是一次额外往返。
查询与执行两份结果都会进上下文,这是延迟加载要付的那一笔,换回的是每一轮都省下的固定开销。
这笔往返有个前提:工具的用法,调一次就记得住。
由此有一条推论——凡属执行之后才知道的指引,都该搬到工具结果上而不是留在描述里。
因为描述是延迟披露的,命中了才进上下文;而结果一定会进上下文。
指引要挂在送达率百分之百的那一侧。
最后一处状态性:被命中的 schema 会留在上下文里,供后续轮次复用——取一次描述即等于激活,后面不用再查。
这省掉了重复往返,副作用是若不适时清理就会跨轮次泄漏;确定性也更难推断,因为模型实际能触及的工具面,取决于它此前搜过什么。
延迟加载缩短的是上下文,不是治理链路——它改的是模型看得见什么,不改的是模型被允许做什么。
08
机制讲完,把账算到底,本章遵循第五章那句前提:工具定义占的是请求最前面、最贵且几乎不变的位置。
第一层账,是空间。
"全量常驻": {"字符": 111119,"约合词元": "2.78 万","占 200K 窗口": "13.9%"},"其中可延迟的 23 条": {"字符": 41418,"占全部工具描述": "37.3%"},"延迟后仍常驻": {"字符": 69701,"构成": "其余 37 条描述,含 tool_search 与 defer_execute 两个常驻入口工具"},"换成的名字清单": {"字符": 271,"约合词元": 68,"构成": "23 行名字 248 字符加换行"},"每轮净省": {"算式": "41418 − 271","字符": 41147,"约合词元": "1.03 万","相当于名单自身预算的": "2.06 倍"}}
账目口径也要说清,数字沿用第一章那组假设:净省是从可延迟的四万一千四百一十八个字符里算出来的,不是从全部十一万一千一百一十九个字符里算,余下三十七条描述照旧常驻。
名字清单也不是免费的,它新加进常驻,吃掉两百七十一个字符。
按这组假设,几万字的说明书换成几十行名字,每个轮次净省四万一千一百四十七个字符,注意是每轮次省,不是一次性省。
说明书与名字的比例更能说明问题:全部工具合计一百八十一比一,被延迟的二十三个工具是一百六十七比一,单条最长的那份描述是两千三百三十四比一——同一个工具,说明书一万八千多字符,名字只有几个到几十个字符,相差三个数量级。
同一份材料里的对照可作参照:传统方式下上下文消耗约七万二千词元起步;改成按需发现之后约八千七百词元,保住百分之九十五的窗口,降幅百分之八十五。
第二层账,是选择准确率。
同一批基准任务,准确率从百分之四十九升到百分之七十四(同一组自测),这一层才是延迟加载真正被低估的收益,它把在场候选压到了最小。
权衡落在一句上:省上下文(每轮都省),换多一个往返(偶尔才付)。
代价也要如实说:一次查询的返回上限,已经是那份名单自身预算的一点五倍—,单次取回的信息量比整个索引还大。
而约束信息会晚一步到达:有的工具把使用边界写在描述里,可搜索阶段模型只看得见名字,看不见那句约束,这是一个真实的披露时序缺口。
09
前八章讲的是延迟加载省下多少开销、说明书又怎么被取回来。
它还有一层收益不在账面上:它让模型能改自己的工具集,同时不把请求前缀搅乱。
两端的要求正面冲突。
第五章讲过,工具定义落在请求最靠前、且几乎逐轮不变的一段——也即缓存命中率最高的位置,要命中,条件很硬:同一份输入必须产出同一份字节。
而工具数组恰恰是每轮重算的,不是会话级算一次;数组里任何一个字节变了,从这一段往后全部作废。
另一端是模型自建的工具, AI Harness 允许模型自己写一个新工具并注册进来,说明书由模型当场给出。
它是成品自由文本——长度、措辞、结构都不受控。
一边要求逐轮同字节,一边天然不受控,这就是冲突。
确定性不能靠语言。
一个流传很广的说法是:脚本语言天生没有日期和随机,所以描述天然确定,实测是假的——时间、随机、UUID 三个非确定性内建,在这套沙箱里都已注册、都没有开关。
那句说法来自另一套实现,是在脚本层把日期与随机封掉换来的性质。
换一门语言,这个性质不会自己跟过来,语言靠不住,就只能在渲染路径上动手。
第一道机制是报错桩。
描述渲染只允许一组纯内建
part、tag、raw、join、has、render_section,管的是取片段、打标签、原样插入、连接、判断存在、渲染小节。
其余内置一律替成报错桩:符号还在,一碰就报错。
{"描述渲染期允许": ["part", "tag", "raw", "join", "has", "render_section"],"描述渲染期禁止": {"time": "报错桩","random": "报错桩","uuid": "报错桩","fs": "报错桩","exec": "报错桩","http": "报错桩","env": "报错桩"},"工具脚本执行期": "完整实现,不受桩影响——但输出不进工具数组"}
桩的行为是报错,不是返回零值。这一点是分野:零值会静默产出一个不确定的描述,报错会在第一次渲染就把它暴露出来。
第二道机制是变量来源白名单。
描述里的取值只准来自三档:
| 禁入 |
第三档是显式列出、显式禁入的,不是靠没人会去加,差别在违规的表现:前者会得到一条可读的报错,后者会静默生效。
判据也要说准:它不是确定,而是同一会话内一模一样。
会话级的那些值当然会随会话变,只要会话内部不变,就不影响命中。
求值时机是一次求值、逐轮复用。
描述在会话创建时求值一次并冻结,结果进一份派生视图,不写回工具定义——定义保持纯数据、可比较;每轮只是从冻结结果里取字节。
{"会话创建时(一次)": {"渲染描述": "走纯净通道,报错桩生效","结果": "冻结进派生视图,不写回工具定义"},"每轮次(每次)": {"渲染次数": 0,"动作": "从冻结结果取字节"}}
两条路于是分开:
延迟加载为了省空间,确定性为了保字节,两个独立目标,在这里被同一套约束绑成了一件设计。
延迟加载也就不再只是省 token 的优化:它是唯一能让模型自建工具、又不动摇前缀缓存的机制。
两个真实的反例,这套约束不是想出来的防御,是两个已发生的缺陷倒逼出来的。
一是工具数组从映射表迭代输出
迭代顺序不定,数组字节就每轮不同,修法是在注册表层统一排序一次,数组自动免疫乱序注册:在入口修一次,好过在每个调用点各修一遍。
二是更严重的一处:技能列表从映射表迭代后写进了系统指令。
污染的是请求第一段,体量三十五到三十七 KB,比工具数组乱序更致命,第一段就变,后面整片别想命中。
顺带说一句这类断言该怎么写:断言顺序等于排序后的顺序,而不是断言两次调用结果不同。
后者在元素数少的时候是概率性断言,会间歇性失败;前者是硬断言,一次命中就说明问题。
不确定性外置,确定性内守,延迟加载从一项省 token 的优化,升级成让模型能自建工具的前提。
10
回到标题那个问题:几万字的工具说明书压成几十行,token 成本究竟是怎么降下来的?
答案不是压缩,是推迟。
把那二十三份低频工具的完整 schema 从每轮常驻里移出去,只留名字进上下文;名字负责被检索到,说明书等命中那一刻再展开。
省下来的是四万一千一百四十七个字符的每轮开销(按第一章那组假设),付出去的是一次偶尔发生的往返。
但这件事真正的工程量不在那两处分离上——协议层已经把字段给你了。它在另外几件更笨的事情上:
怎么决定谁有资格被延迟。
靠频率分层,不是靠重要性排序。
怎么在只读场景把工具面收到最小。
靠结构隔离,不是靠提示词劝说——因为软约束的失败率只跟对话长度有关,跟你写得多严厉几乎无关。
怎么让查询一定能命中。
靠索引的组装规则和预算分配,靠名字必留、描述可砍这条底线。
怎么让模型自己改工具集,又不打穿缓存。
靠不确定性外置、确定性内守——延迟集合收下前者,纯净通道守住后者。
以及,怎么在缩短上下文的同时,一道安全关卡都不放过。
这是最容易被误解的地方:延迟加载改的是模型看得见什么,不改的是模型被允许做什么。
上下文工程被误解的地方,是把它当成了提示词工程的下半场,或者历史压缩的别名。
而它真正的样子,是决定什么必须常驻、什么可以推迟、推迟之后怎么取回来——以及这一切最终怎么组装进那份发给模型的请求。
给你一句可迁移的判断标准:看一个 Harness 的上下文工程水平,先看它的常驻开销,再看它的提示词写得多好。
以上全文基于个人理解,欢迎批评指正。

Hook 管道引擎 · 十二事件拦截 · 四动作执法 — AI Harness 运行时护栏设计