多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

【Bug已解决】`ProviderToolSearchMiddleware` defers tools as flat top-level functions instead of wrapping…

【Bug已解决】`ProviderToolSearchMiddleware` defers tools as flat top-level functions instead of wrapping… 【Bug已解决】ProviderToolSearchMiddlewaredefers tools as flat top-level functions instead of wrapping them in anamespace— no token savings一、现象长什么样ProviderToolSearchMiddleware一个把按需搜索并加载工具延迟化的中间件的设计初衷是省 token不要一次性把所有工具定义塞进系统提示而是把工具按 provider/namespace 分组只有被搜索命中时才把对应那组工具加载进来。这样模型上下文里始终只有当前需要的少量工具省下大量 token。但实际实现却把延迟加载的工具当作扁平的顶层函数注册进工具箱tools是一个平铺列表而不是包在namespace下。后果没有 namespace 分组搜索命中后仍然要全部展开或无法按组隔离token 省不下来标题里的 no token savings。不同 provider 的同名工具互相冲突扁平列表里名字必须全局唯一加载时覆盖或报错。中间件名不副实叫search/defer却没利用 namespace 做真正的按需隔离。一句话本该靠 namespace 实现的按组延迟加载省 token被实现成了把所有工具平铺架构目标落空。二、背景工具多了以后把所有 tool schema 都放进每次请求的 system prompt 非常烧 token。常见优化是namespace 延迟加载工具按 provider 分 namespace如github.*、slack.*。模型先调用一个search_tools(query)工具命中某 namespace再加载那一组。只有被加载的 namespace 的工具进上下文其余不出现。ProviderToolSearchMiddleware应负责搜索→按 namespace 加载。但它注册工具时直接toolbox.add(flat_func)而非toolbox.add_namespace(github, funcs)于是所有工具平铺namespace 维度丢失延迟加载的颗粒度退化为全有或全无token 优化失效。三、根因根因两点未用 namespace 容器中间件把工具当扁平函数注册丢失了分组信息无法只加载一组。token 目标与实现脱节设计文档说按 namespace 省 token实现却平铺没有机制保证未命中 namespace 不进上下文。本质把分组延迟加载退化成了扁平全量注册namespace 这一实现关键被遗漏导致核心收益token 节省、同名隔离都没有。四、最小可运行复现下面缩略逻辑复现平铺导致无法按组隔离class Toolbox: def add(self, tool): # 扁平注册 self.tools[tool.name] tool mw ProviderToolSearchMiddleware() mw.defer(github, [gh_issue, gh_pr]) # 期望按 github namespace 延迟 mw.defer(slack, [slack_msg]) # 实际注册 for t in gh_issue, gh_pr, slack_msg: toolbox.add(t) # 平铺namespace 信息丢失 # 搜索命中 github 时仍无法只加载 github 组token 没省修复用 namespace 容器命中时才加载该组。class Toolbox: def add_namespace(self, ns, tools): self.namespaces[ns] tools def on_search_hit(ns): for t in toolbox.namespaces[ns]: # 只加载命中组 active_tools.add(t)五、解决方案第一层最小直接修复最小修法中间件改为按 namespace 注册与加载命中搜索时才把该 namespace 的工具加入活跃集。class ProviderToolSearchMiddleware: def __init__(self): self._namespaces {} self._active [] def defer(self, namespace: str, tools): self._namespaces[namespace] tools # 按组存不立即进上下文 def on_search(self, query) - list: ns self._search_ns(query) self._active list(self._namespaces.get(ns, [])) # 只加载命中组 return self._active这一层恢复按 namespace 延迟加载未命中组不进上下文token 省回来。六、解决方案第二层结构化改进把namespace 延迟加载策略固化成策略对象作为单一事实来源明确分组、命中加载、token 目标。from dataclasses import dataclass, field from typing import Dict, List dataclass(frozenTrue) class LangChainProviderToolSearchPolicy: ProviderToolSearchMiddleware namespace 策略的单一事实来源。 use_namespace: bool True load_only_on_hit: bool True token_saving_goal: bool True flat_registration_forbidden: bool True def register(self, store: Dict[str, List], ns: str, tools) - None: if self.flat_registration_forbidden and not self.use_namespace: raise AssertionError(flat registration breaks token saving) store[ns] list(tools) def load_on_hit(self, store: Dict[str, List], ns: str) - List: if not self.load_only_on_hit: # 全量加载则无 token 收益 out [] for v in store.values(): out v return out return list(store.get(ns, [])) def validate(self) - None: if self.token_saving_goal and self.flat_registration_forbidden is False: raise AssertionError(token saving requires namespace, not flat)中间件读policy强制 namespace 注册与按需加载。七、解决方案第三层断言 / CI 守护用 pytest 锁死 namespace 行为import pytest from policy import LangChainProviderToolSearchPolicy as P def test_namespace_isolation(): p P() store {} p.register(store, github, [gh_issue]) p.register(store, slack, [slack_msg]) hit p.load_on_hit(store, github) assert hit [gh_issue] # 只加载 github 组 assert slack_msg not in hit # slack 未进上下文 def test_flat_forbidden(): with pytest.raises(AssertionError): P(use_namespaceFalse, flat_registration_forbiddenTrue).register({}, x, []) def test_token_goal_requires_namespace(): with pytest.raises(AssertionError): P(token_saving_goalTrue, flat_registration_forbiddenFalse).validate() def test_full_load_no_saving(): p P(load_only_on_hitFalse) store {a: [x], b: [y]} assert set(p.load_on_hit(store, a)) {x, y} # 全量无省CI 加一条断言命中某 namespace 时活跃工具集不含其他 namespace且总 tokenschema 字符数显著低于全量。八、排查清单工具多了 token 还是爆→ 中间件平铺注册没用 namespace 隔离。同名工具冲突→ 扁平列表全局唯一需 namespace 分组。搜索命中后是否只加载一组→ 必须按 namespace 按需加载。是否真有 token 收益→ 测活跃 schema 字符数对比全量。中间件是否名不副实→ search/defer 必须落到 namespace 加载上。其他 provider 是否同样处理→ 所有 defer 都走 namespace。九、小结ProviderToolSearchMiddleware本应靠 namespace 实现按组延迟加载省 token却把工具当扁平顶层函数注册丢失分组维度导致 token 优化与同名隔离双双失效。根因是 namespace 这一关键实现被遗漏架构目标与实现脱节。第一层改为按 namespace 注册、命中时才加载该组第二层用LangChainProviderToolSearchPolicy把分组/加载策略固化成单一事实来源第三层用 pytest 守护 namespace 隔离与 token 收益。工具延迟加载的通用原则省 token 必须靠 namespace 分组与按需加载平铺注册等于放弃了核心收益。
返回列表