跳到主要内容

工具路由

本章讲 NLWeb「不止搜索」的那一面:怎么把「问菜谱配料」「对比两部电影」这类不同意图,路由到不同的处理器。机制是「XML 声明工具 + LLM 给工具打分 + 动态加载处理器」。

路径基准提示:本章 core/...methods/....py 路径相对仓库的 AskAgent/python/;而 config/tools.xmlconfig/config_nlweb.yaml 相对仓库根config/ 在根目录,不在 AskAgent/python/ 下)。

3.1 为什么要工具,而不只是搜索

「找 20 分钟能做的意面」是搜索;但「鸡肉意面里放了什么」是问某个具体条目的细节,「A 和 B 哪个评分高」是对比。这些都不该走「检索一堆候选再排序」,而该有专门处理逻辑。

NLWeb 把这些处理逻辑抽象成工具(Tool),让一个 ToolSelector 先判断「这句话该用哪个工具」。

3.2 工具是 XML 声明的

工具不写死在代码里,而是声明在 config/tools.xml(相对仓库根)。每个工具带:名字、示例、一段给 LLM 的判定 prompt、返回结构、以及处理器类路径。

看真实的 search 工具声明(config/tools.xmldefault 站的 Item 类型下):它的 prompt 教 LLM「搜索工具适合按属性找东西,不适合问某个具体条目的细节」,返回结构是 {score, search_query}。而 details 工具的 prompt 则教「用户点名某条目要细节时打 80-100,想搜索时打 0-30」,并配了 handler 标签指向 methods.item_details.ItemDetailsHandler

加载逻辑在 _load_tools_from_filerouter.py:52):用 xml.etree 解析,按 <Site id=...> 找站点(找不到回退到 defaultrouter.py:75),再遍历每个 schema 类型下的 <Tool>,解析出 Tool dataclass(router.py:26)。

Tool 的字段router.py:26):namepromptreturn_structurehandler_class 等。handler_class 是字符串形式的模块路径——这是后面动态加载的关键。

3.3 按 schema 类型继承工具

站点问的是菜谱(Recipe)还是商品(Product),决定了可用的工具集。get_tools_by_type 处理类型继承(router.py:293):

  • 有一张 TYPE_HIERARCHYrouter.py:167):RecipeMovieProduct… 都继承自 Item
  • 取工具时,先收 Item 级的通用工具,再用具体类型(如 Recipe)的工具覆盖同名的(router.py:321 注释 "specific type tools override general ones")。

所以 Recipe 站既能用通用 search,又能用 Recipe 专属的 details。

3.4 LLM 给工具打分 + 早停

思路。 每个工具有自己的判定 prompt,并发问 LLM「这工具对这句话合不合适,打分」,谁分高用谁。

早停优化_evaluate_tools_with_early_terminationrouter.py:232):用 asyncio.as_completed 边完成边看,一旦某工具分数 ≥ 阈值(90,router.py:505 传入),立刻 cancel 其余 task 直接返回(router.py:262-272)。常见意图往往有个工具明显高分,早停省下其余 LLM 调用。

单工具短路:若某类型只有一个工具,直接 100 分用它,跳过 LLM(router.py:468,注释 "saving API call")。

# 示意,非源码:工具选择的决策骨架
tools = get_tools_by_type(schema_type) # 按类型拿工具(含继承)
if len(tools) == 1:
selected = tools[0] # 唯一工具,省一次 LLM
else:
results = evaluate_with_early_termination(query, tools, threshold=90)
results = [r for r in results if r.score >= 70] # MIN_TOOL_SCORE_THRESHOLD
if not results:
selected = search_tool # 没工具过线 → 兜底 search

门槛与兜底MIN_TOOL_SCORE_THRESHOLD = 70router.py:163),低于它的工具被滤掉;若全都没过线,回退到 search 工具(router.py:520-527)。这保证「再不济也能搜一下」。

3.5 选中非 search → 叫停 FastTrack

关键联动:如果排第一的工具不是 search,说明这查询不该走普通搜索管道,于是 abort_fast_track_event.set()router.py:531-535)。这正是 02 章里 FastTrack「赌输」的第二种情形。

反过来,prepare 里若请求显式指定 tool 参数,就跳过整个 LLM 选择,直接用指定工具(baseHandler.py:360-368)。

3.6 动态加载处理器

选定工具后,route_query_based_on_tools 看它有没有 handler_classbaseHandler.py:451):

# 示意,非源码:动态加载并执行处理器
if tool.handler_class:
if tool_name != "search":
self.final_retrieved_items = [] # 非搜索工具,清掉 FastTrack 攒的检索
module_path, class_name = tool.handler_class.rsplit('.', 1)
module = importlib.import_module(module_path)
handler_class = getattr(module, class_name)
await handler_class(params, self).do() # 统一的 do() 约定
else:
await self.get_ranked_answers() # search 没处理器类 → 直接排序

注意两点:① 非 search 工具会先清空检索结果(baseHandler.py:454),因为 FastTrack 攒的搜索候选对「问细节/对比」没用;② 所有处理器遵守统一的 do() 方法约定(baseHandler.py:471),比如 methods/item_details.pyItemDetailsHandler.doitem_details.py:38)。出错则兜底回 search(baseHandler.py:474-480)。

巧妙之处

  • 工具即数据。新增工具只要在 tools.xml 加一段 <Tool> + 写个带 do() 的处理器类,无需改路由代码——handler_class 字符串 + importlib 实现了配置驱动的扩展点(router.py:131baseHandler.py:460)。
  • 判定 prompt 写进声明。每个工具自带「我适合什么」的自然语言说明,LLM 直接读它来打分。工具的「能力描述」和「实现」放在一起,符合直觉。
  • 早停省钱as_completed + 高分即取消,把「问 N 个工具」在常见情况下降到「问到第一个明显合适的就停」(router.py:250-272)。

边界与局限

  • TYPE_HIERARCHY 是硬编码的占位实现,代码自己标了 TODO「需要真正的 schema.org 类型层级」(router.py:166)。目前只支持一层「X 继承 Item」。
  • 工具选择本身要花一次(或多次)高 level 的 LLM 调用(router.py:564-567),对延迟敏感场景可整体关闭(tool_selection_enabled: falseconfig/config_nlweb.yaml:16,相对仓库根;逻辑见 router.py:396)。

代码地图

主题文件符号
工具数据结构core/router.pyTool (dataclass)
从 XML 加载工具core/router.py_load_tools_from_file
类型继承取工具core/router.pyToolSelector.get_tools_by_type / TYPE_HIERARCHY
并发打分 + 早停core/router.py_evaluate_tools_with_early_termination
选择主流程 / 门槛core/router.pyToolSelector.do / MIN_TOOL_SCORE_THRESHOLD
叫停 FastTrackcore/router.pyToolSelector.doabort_fast_track_event.set
动态加载处理器core/baseHandler.pyNLWebHandler.route_query_based_on_tools
工具声明文件(仓库根)config/tools.xml`<Tool name="search
处理器示例methods/item_details.pyItemDetailsHandler.do