Skip to content

skills: apply tool_filter in SkillToolSet.get_tools - #353

Closed
kinolo wants to merge 1 commit into
trpc-group:mainfrom
kinolo:fix/skill-toolset-filter-352
Closed

kinolo wants to merge 1 commit into
trpc-group:mainfrom
kinolo:fix/skill-toolset-filter-352

Conversation

@kinolo

@kinolo kinolo commented Sep 30, 2026

Copy link
Copy Markdown

问题与修复

Fixes #352

SkillToolSet.get_tools() 在显式设置 is_include_all_tools=False 后,仍未应用 tool_filter。例如,配置 tool_filter=["skill_load"] 时,首次调用和缓存调用都会返回全部工具。

本次修复在两条返回路径中应用 _is_tool_selected(tool, invocation_context),并保留完整工具缓存,使谓词过滤器可以根据当前调用上下文重新选择工具。

默认 is_include_all_tools=True、未配置过滤器及空列表过滤器的既有行为保持不变。

测试

新增 14 个参数化回归用例,覆盖 SkillToolSet 和 SkillToolSetWithDynamicTools:

  • 名称列表过滤在首次调用和缓存调用中均生效。
  • 谓词拒绝全部工具后,后续上下文仍可选择其他工具。
  • 谓词接收显式传入或当前隐式 invocation context。
  • 未启用过滤时保持原有行为。

原代码上新增用例为 6 failed, 8 passed;修复后全部通过。

本地验证:

pytest tests/skills tests/abc/test_toolset.py \
  tests/tools/test_tool_adapter.py \
  tests/agents/core/test_tools_processor.py
# 720 passed, 1 warning

yapf --diff trpc_agent_sdk/skills/_toolset.py
flake8 trpc_agent_sdk/skills/_toolset.py tests/skills/test_toolset.py
git diff --check

其中 1 项 warning 来自已有流式并发测试 test_streaming_tool_runs_outside_parallel_batch:线程池退出等待约 300 秒后产生超时警告,测试最终通过。退出阻塞也在未修改的 02509b2 源码上单独复现;本 PR 未修改相关并发逻辑。

Release notes

设置 is_include_all_tools=False 时,SkillToolSet 现在会正确应用工具名称或谓词过滤器。

Filter both initial and cached tool lists using the current invocation context while retaining the full tool cache.

Fixes trpc-group#352

RELEASE NOTES: SkillToolSet now honors configured tool filters when is_include_all_tools is false.
@helloopenworld

Copy link
Copy Markdown
Contributor

AI Code Review

审查结论

不通过

审查范围:base 02509b2..head e24e06c,共 2 个文件(+67/-4):trpc_agent_sdk/skills/_toolset.py 在 get_tools() 的首次构建路径与 _default_tools 缓存路径中均接入 _is_tool_selected 过滤;tests/skills/test_toolset.py 新增 63 行参数化测试覆盖 SkillToolSet 与 SkillToolSetWithDynamicTools。\n\n计划符合性:核心意图(在 SkillToolSet.get_tools 中应用 tool_filter)已实现,list/predicate 两种过滤形式、缓存路径过滤、is_include_all_tools 兼容语义均正确,新增测试覆盖首次/缓存调用、predicate 接收当前上下文、隐式上下文、过滤后行为保持等场景。\n\n主要风险:1) 缓存快照语义由 invocation_context 决定但该上下文未参与缓存键——首次调用发生在无显式上下文的位置(_agui_agent.py 的 get_tools(None))或与后续真实请求不同的上下文时,后续所有请求的工具集被首调快照固化,工具被错误隐藏或暴露(SEVERE);2) get_tools(None) 在无隐式上下文时依赖全局 ContextVar 残留值,且 SkillToolSetWithDynamicTools 首调过滤基于未完成扩展的 _function_tools(既有缺陷被过滤暴露,动态工具名过滤结果不稳定)(MODERATE)。\n\n测试充分性:新增测试对意图覆盖良好,但所有测试都显式传入同构 MagicMock 上下文或通过 set_invocation_ctx 设置同一上下文,未覆盖"首次调用无显式上下文/上下文不一致"场景,无法捕获缓存快照问题。\n\n门禁结论:FAILED——存在 1 个高置信 SEVERE(缓存快照固化过滤结果,与计划的上下文感知过滤意图冲突)和 1 个 MODERATE(隐式 ContextVar 回退与既有首调 _function_tools 延迟扩展交互)。

发现的问题

严重

trpc_agent_sdk/skills/_toolset.py:169-170

问题: _default_tools 缓存路径(新增行 170)在缓存命中时按首个调用的 invocation_context 过滤后整体返回快照。_is_tool_selected 对 ToolPredicate 会调用 self._tool_filter(tool, invocation_context),因此过滤结果依赖调用时的上下文,而缓存列表本身不区分上下文——首次调用固化的过滤结果,后续请求无论传入什么上下文都读取同一份快照。

触发条件: 构造时配置了 tool_filter 为谓词(is_include_all_tools=False),且 get_tools() 首次调用发生在无显式上下文的位置(如 server/ag_ui/_core/_agui_agent.py:411 的 tool.get_tools(None)、_llm_agent.py 的工具预取),或首次调用与后续调用传入不同上下文。

实际影响: 后续所有真实请求的工具名集合与请求自身上下文无关:本应在的动词或工具(如 skill_run、workspace_exec)在 LLM 请求中缺失(convert_toolunion_to_tool_list->get_tools(context) 返回快照后,process_request 无法声明未包含的工具),LLM 无法调用;反之不应暴露的工具被固化暴露。缓存路径连 predicate 都不再被调用,违背计划"上下文感知过滤"意图。

修正方向: 将按请求上下文过滤的结果与缓存分离——缓存仅保存构建好的全量工具列表(_default_tools.copy() 原行为),每次调用都对该副本执行 _is_tool_selected;或确保首次调用带真实上下文,并为缓存结果记录生成时上下文、命中时重新过滤。

中等

trpc_agent_sdk/skills/_toolset.py:161-170

问题: 新增行 170 与 187 在 invocation_context 为 None 时依赖 get_invocation_ctx() 的回退值来执行 _is_tool_selected,但 predicate 被设计为"每个请求上下文"过滤;而全局 ContextVar 的残留值不一定对应当前请求。同时 SkillToolSetWithDynamicTools 的既有首调缺陷被过滤行为放大:其 get_tools 仅在 not self._default_tools 时才扩展 _function_tools(_dynamic_toolset.py:414-416),本次变更使首个调用就执行过滤,导致首个调用时 skill_list_tools/skill_select_tools 未进入待过滤集合,被 predicate 拒绝后永远不出现,而这对普通 SkillToolSet 不存在。

触发条件: ① 无隐式上下文路径(get_tools(None),如 _agui_agent.py)中,ContextVar 残留上一个异步任务设置的上下文,predicate 基于该残留值过滤当前请求;② 对 SkillToolSetWithDynamicTools 配置选择型/predicate 过滤,首个调用过滤掉 skill_list_tools 后,后续调用虽缓存未变,过滤结果同样缺失该工具,导致动态选择工具不可用。

实际影响: 工具集内容跨请求串扰(上下文 A 的过滤结果影响上下文 B 的请求);隐藏缺失的动态选择工具时,代理无法使用 skill_list_tools/skill_select_tools 管理技能工具。

修正方向: 在无显式上下文时不要回退到全局 ContextVar 作为过滤上下文(is None 时按空过滤或跳过过滤);将 SkillToolSetWithDynamicTools 的动态辅助函数扩展移到 __init__ 中无条件完成,避免首次调用过滤基类集合。

Comment on lines 169 to +170
if self._default_tools:
return self._default_tools.copy()
return [tool for tool in self._default_tools if self._is_tool_selected(tool, invocation_context)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题: _default_tools 缓存路径(新增行 170)在缓存命中时按首个调用的 invocation_context 过滤后整体返回快照。_is_tool_selected 对 ToolPredicate 会调用 self._tool_filter(tool, invocation_context),因此过滤结果依赖调用时的上下文,而缓存列表本身不区分上下文——首次调用固化的过滤结果,后续请求无论传入什么上下文都读取同一份快照。

触发条件: 构造时配置了 tool_filter 为谓词(is_include_all_tools=False),且 get_tools() 首次调用发生在无显式上下文的位置(如 server/ag_ui/_core/_agui_agent.py:411 的 tool.get_tools(None)、_llm_agent.py 的工具预取),或首次调用与后续调用传入不同上下文。

实际影响: 后续所有真实请求的工具名集合与请求自身上下文无关:本应在的动词或工具(如 skill_run、workspace_exec)在 LLM 请求中缺失(convert_toolunion_to_tool_list->get_tools(context) 返回快照后,process_request 无法声明未包含的工具),LLM 无法调用;反之不应暴露的工具被固化暴露。缓存路径连 predicate 都不再被调用,违背计划"上下文感知过滤"意图。

修正方向: 将按请求上下文过滤的结果与缓存分离——缓存仅保存构建好的全量工具列表(_default_tools.copy() 原行为),每次调用都对该副本执行 _is_tool_selected;或确保首次调用带真实上下文,并为缓存结果记录生成时上下文、命中时重新过滤。

@raychen911

Copy link
Copy Markdown
Contributor

提交了相同的pr:https://github.com/trpc-group/trpc-agent-python/pull/357, 带有文档修改

@raychen911 raychen911 closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] SkillToolSet.get_tools() 在 is_include_all_tools=False 时仍忽略 tool_filter

3 participants