第三个窗口大户进场
上一篇把多 Agent 的共享边界划清:黑板只放断言、过程留在私域。同一条"稀缺窗口只配给信息量"的原则,今天要施给一个长期被豁免的对象——工具定义。复盘上下文开销,多数团队只盯着对话历史与检索注入,工具定义被当成"反正也长不到哪去"的静态背景。一旦工具上了两位数,这笔豁免就破产了:一个带完整参数 schema、枚举值与描述的工具定义普遍有几百个计量单位,四十个工具就是两万六,而且每一步都全额重发——它不是记忆,是税。更糟的是这笔税还有连带的智商税:候选工具越多,模型选错工具、乱填参数的概率越高。本篇把工具定义从"静态配置"改造成"有生命周期的资源":注册、准入、驻留、按需注入、退役,每一环都有可量化的账。
静态全量注入的三笔亏
第一笔,固定税。对话组装时把全部工具的 schema 摊进请求,与这一步用不用工具无关。按第一篇的预算法,两万六的常驻开销可能直接吃掉窗口的三分之一,从历史区和检索区里扣。第二笔,选型污染。名称相似、描述重叠的工具同时在场,模型会在两个 schema 间拼参数;候选从 4 个加到 40 个,挑错概率非线性上涨,这部分损耗不出现在任何账单上,只出现在 badcase 里。第三笔,演化僵死。工具是活物:接口改版、参数更名、后端下线。静态全量注入意味着每次请求都拖着化石——半年没人调用的工具,过期 schema 依然每步准时到场,还时不时把模型引向已废弃的字段。三笔亏的共同根源:把"注册表"当"运行时"——所有注册过的定义无条件进每次请求。
实验一:三种注入方案对账
模拟一个 40 工具的系统跑 100 步,需求头部集中(热点三件套吃掉约六成调用),对比:A 静态全量;B 一行目录+每步按需注入 top-8 完整 schema;C 目录+热点驻留+冷门按需。目录指每个工具只留"名字+一句摘要"的索引行;语义检索质量假设"正确工具进 top-8"的概率为 0.90——生产中这个数必须拿自有工具集实测,这里只做机制演示。
importmathimportrandom rng=random.Random(493)N_TOOLS=40STEPS=100INDEX_LINE=45# 一行工具目录的体积(名字+一句摘要)TOP_M=8# 按需阶段每步注入的候选数P_RANK=0.90# 语义检索把正确工具排进 top-8 的概率schemas={i:rng.randint(380,950)foriinrange(1,N_TOOLS+1)}# 完整 schema 体积total_all=sum(schemas.values())avg_schema=total_all/N_TOOLS hot_ids=sorted(schemas,key=schemas.get,reverse=True)[:3]# 热点三件套hot_bytes=sum(schemas[i]foriinhot_ids)second=[iforiinschemasifinotinhot_ids][:7]demand=[]for_inrange(STEPS):x=rng.random()ifx<0.60:demand.append(rng.choice(hot_ids))elifx<0.85:demand.append(rng.choice(second))else:demand.append(rng.choice(list(schemas)))p_hot=sum(1fordindemandifdinhot_ids)/STEPS p_cold=sum(1fordindemandifdnotinhot_ids)/STEPSprint("方案 每步平均注入 相对静态节省 正确工具在场率")print("%-26s %10.0f %8s %.0f%%"%("A 静态全量",total_all,"—",100))cost_b=N_TOOLS*INDEX_LINE+TOP_M*avg_schemaprint("%-26s %10.0f %7.1f%% %.0f%%"%("B 目录+每步top8",cost_b,(1-cost_b/total_all)*100,P_RANK*100))cost_c=N_TOOLS*INDEX_LINE+hot_bytes+p_cold*TOP_M*avg_schema recall_c=(p_hot+p_cold*P_RANK)*100print("%-26s %10.0f %7.1f%% %.0f%%"%("C 目录+热点驻留+按需",cost_c,(1-cost_c/total_all)*100,recall_c))print("\n账本: 全量 schema %d, 目录 %d, 热点三件套 %d, 平均单工具 schema %.0f"%(total_all,N_TOOLS*INDEX_LINE,hot_bytes,avg_schema))print("\n按需注入的候选数 vs 模型选错率(确定性模型 0.03+0.06*ln(k)):")forkin(2,4,8,16,32):err=0.03+0.06*math.log(k)print(" top-%2d: 选错率 %4.1f%% 每步 schema 开销 %6.0f"%(k,err*100,k*avg_schema))print("TOP_M=%d 是成本与挑错率的折中; 目录行只保证'知道有这个工具', 不保证'选对它'。"%TOP_M)运行输出:
方案 每步平均注入 相对静态节省 正确工具在场率 A 静态全量 25716 — 100% B 目录+每步top8 6943 73.0% 90% C 目录+热点驻留+按需 6132 76.2% 97% 账本: 全量 schema 25716, 目录 1800, 热点三件套 2738, 平均单工具 schema 643 按需注入的候选数 vs 模型选错率(确定性模型 0.03+0.06*ln(k)): top- 2: 选错率 7.2% 每步 schema 开销 1286 top- 4: 选错率 11.3% 每步 schema 开销 2572 top- 8: 选错率 15.5% 每步 schema 开销 5143 top-16: 选错率 19.6% 每步 schema 开销 10286 top-32: 选错率 23.8% 每步 schema 开销 20573 TOP_M=8 是成本与挑错率的折中; 目录行只保证'知道有这个工具', 不保证'选对它'。方案 B 一刀砍掉 73% 的税,代价是把"正确工具在场率"折在检索质量上——检索漏了,模型连选的机会都没有,表现为"Agent 说我不会"或硬调一个不相干工具。方案 C 用 2738 单位把热点三件套买断常驻,在场率从 90% 回到 97%,每步成本反而比 B 再低一成二:六成调用根本不触发按需检索,省掉了那部分 top-8 注入。分层是免费的午餐:头部行为确定性强,值得付驻留费;尾部行为稀,按需付检索费。选错率表回答"为什么不在 top-8 里塞 16 个更保险":候选翻倍,在场率涨了,选错率也从 15.5% 爬到 19.6%——裁剪过头与不足都是税,平衡点只在评测里,不在直觉里。还有一笔隐性账:按需注入多一跳检索与一次判断,高频简单任务上这跳可能比省下的预算贵,所以热点驻留同时也是延迟设计。
实验二:热池的换血治理
真实工具池会漂移:业务改版,热点搬家。模拟 40 工具跑 300 步、第 150 步热点从工具 1-10 迁到 21-30,另有 15% 的均匀长尾流量。对比三种治理:全池常驻、朴素 LRU 被动换入换出、指数衰减频次统计驱动的主动重排。
importrandom rng=random.Random(4933)N_TOOLS=40STEPS=300POOL=10# 热池容量(常驻可注入的工位数)REBALANCE=10# 每 10 步按衰减频次统计重排一次热池defneed_at(step):# 需求热点在第 150 步从 1-10 漂移到 21-30base=list(range(1,11))ifstep<150elselist(range(21,31))returnrng.choice(base)ifrng.random()<0.85elserng.randint(1,N_TOOLS)DEMAND=[need_at(step)forstepinrange(1,STEPS+1)]defrun_naive_lru():usage={i:-999foriinrange(1,N_TOOLS+1)}hot=set(range(1,11))present=promotes=0forstep,needinenumerate(DEMAND,1):usage[need]=stepifneedinhot:present+=1else:promotes+=1hot.add(need)victim=min(hot,key=lambdat:(usage[t],t))hot.discard(victim)returnpresent/STEPS,promotesdefrun_ewma():"""指数衰减频次统计驱动的主动重排: 冷门单发进不了前 10, 漂移后快速追上。"""score={i:(10.0ifi<=10else0.0)foriinrange(1,N_TOOLS+1)}usage={i:-999foriinrange(1,N_TOOLS+1)}hot=set(range(1,11))present=rebalances=recovered=0forstep,needinenumerate(DEMAND,1):usage[need]=stepifneedinhot:present+=1else:recovered+=1fortinscore:score[t]*=0.95score[need]+=1.0ifstep%REBALANCE==0:# 定期按衰减频次重排热池hot=set(sorted(score,key=lambdat:(-score[t],-usage[t],t))[:POOL])rebalances+=1returnpresent/STEPS,rebalances,recovered p1,m1=run_naive_lru()p2,rb,rec=run_ewma()print("策略 需求在场率 换池动作")print("全池常驻 100.0% 0 次(无治理)")print("朴素LRU %6.1f%% %5d 次升池(被动换入换出)"%(p1*100,m1))print("衰减频次重排 %6.1f%% %5d 次重排(主动再平衡), 缺席步走按需召回 %d 次"%(p2*100,rb,rec))full_cost=N_TOOLS*667*STEPS hot_cost=POOL*667*STEPSprint("\n常驻开销: 全池 %d, 热池(10 工位) %d, 固定税降 %.0f%%"%(full_cost,hot_cost,(1-hot_cost/full_cost)*100))print("长尾去向: 不在池的需求走'目录+按需召回'兜底(实验一方案C), 不为 15% 的长尾养 30 个常驻位。")print("\n教训一: 朴素 LRU 被一次性冷门调用冲刷, %d 次换池只买到 %.1f%% 在场率, 越治理越 churn;"%(m1,p1*100))print("教训二: 热池要靠统计驱动的主动重排——缺席 %d 步全部可由按需召回兜底,"%rec)print(" 而换池动作比被动 LRU 少一半以上(%d 对 %d)。"%(rb,m1))运行输出:
策略 需求在场率 换池动作 全池常驻 100.0% 0 次(无治理) 朴素LRU 72.0% 84 次升池(被动换入换出) 衰减频次重排 78.3% 30 次重排(主动再平衡), 缺席步走按需召回 65 次 常驻开销: 全池 8004000, 热池(10 工位) 2001000, 固定税降 75% 长尾去向: 不在池的需求走'目录+按需召回'兜底(实验一方案C), 不为 15% 的长尾养 30 个常驻位。 教训一: 朴素 LRU 被一次性冷门调用冲刷, 84 次换池只买到 72.0% 在场率, 越治理越 churn; 教训二: 热池要靠统计驱动的主动重排——缺席 65 步全部可由按需召回兜底, 而换池动作比被动 LRU 少一半以上(30 对 84)。朴素 LRU 的表现值得细看:300 步里升池 84 次,平均 3.6 步换一次血,在场率却只有 72%——因为 15% 的均匀长尾里每个冷门工具都"恰好没见过",一次调用就把它挤进热池、顺手挤掉一个真正的热点,下次冷门换人、热点再被换出。这正是缓存文献里研究了几十年的"扫描污染"问题:一次性访问不该进缓存,工具热池同理。衰减频次重排把每次调用记成会过期的分数(每步乘 0.95),每 10 步取前十重排:单发冷门顶不动分数,热点换防后一两分钟内自动补位,在场率抬到 78.3%、重排动作只有 30 次。剩下的 22% 缺席并不是事故——它们全部由"目录可见+按需召回"通道兜底(实验一方案 C 的 0.90 在场率),叠加后实际覆盖约 98%。热池管效率,目录管下限,两层结构缺一不可;而固定税已经降掉 75%。
工程化改进:把注册表改造成调度器
准入关:新工具注册必须带三样元数据——一行目录摘要(含"何时该用")、检索关键词集(给按需召回)、schema 体积预估(给预算)。缺任何一样不予上架,因为没有元数据的工具进不了任何分层策略。驻留调度:热池按衰减调用密度每 N 步主动重排,进出留日志;重排窗口和衰减系数拿历史流量回放调,不看感觉。按需通道:目录常驻头部,正文走检索注入;再给模型开request_tool元工具,允许它在目录里看到"要的工具不在场"时主动召回——把缺席从"任务失败"降级为"多一跳"。退役通道:归一化调用密度低于阈值且冷却期满,schema 移出可注入池,但注册表保留、归档目录里"它存在过"仍可见——下线是治理动作,不是物理删除。特别提醒失败计数器的用法:工具连败三次,可能不是它该退役,而是参数模板过期或鉴权坏了;先隔离并生成诊断记录,修复验证后回池,否则退役机制会系统性处刑"恰好坏了的热门工具"。最后一环是 schema 瘦身:枚举值收进校验器而不是提示词、互斥字段拆成两个工具、描述去行话——注入省下的每一单位,每一步都在省。
常见陷阱
一是把 40 个工具平铺进一个 Agent"图省事":选型污染与固定税双杀;正确姿势先按职责域拆 Agent,域内再上分层注入。二是对话中途热改工具列表:缓存失效,且模型拿着上一轮见过的工具名硬调本轮已变化的 schema;工具列表变更要放在会话边界。三是按需检索从不测"在场率":检索一漏,Agent 表现得像突然失忆,排障的人还在查记忆模块——三篇之内说过两次:症状在记忆,病因在检索。四是用朴素"最近用过就留下"当热池策略:被长尾冲刷,池子天天换血谁都没留住。五是目录行只写名字:不给"何时该用",模型看过目录也不知道该召回谁,按需通道形同虚设。
落地清单
- 工具注册强制三元数据:目录摘要(含适用时机)、检索关键词、schema 体积预估
- 注入三层:全量目录常驻 + 热池按衰减频次主动重排 + 冷门按需 top-M 注入
- 为模型开 request_tool 元工具,缺席可见、可追;会话边界才允许变更工具列表
- 退役按"归一化调用密度+冷却期",失败先隔离诊断;注册表永不物理删除
- 看板四指标联动:每步工具注入占比、在场率、选错率、按需召回延迟,据此调 TOP_M 与重排周期
工具的账清了,窗口三大户——历史、检索、工具——都有了预算方案。但还有一类开销既不是记忆也不是工具,而是任务自身的骨架:长任务分解成几十个子任务后,计划树与执行状态怎么占窗口、失败沿依赖边怎么传播。下一篇《AI Agent 记忆与上下文工程实战(7):子任务分解与状态机:Plan-and-Execute 的失败面》拆解计划树的窗口代价与失败传染。
参考来源
- Anthropic Engineering, Writing Effective Tools for Agents:https://www.anthropic.com/engineering/writing-tools-for-agents
- OpenAI Platform Docs, Function calling:https://platform.openai.com/docs/guides/function-calling
- Wikipedia, Cache replacement policies:https://en.wikipedia.org/wiki/Cache_replacement_policies
- Schick et al., Toolformer: Language Models Can Teach Themselves to Use Tools:https://arxiv.org/abs/2302.04761
- Qin et al., ToolLLM: Facilitating Large Language Models to Master 16000+ Real-world APIs:https://arxiv.org/abs/2307.16789