Files
xiaoli-cli/tests/test_prune_tools.py
T
shuiyu67 b577b0ff21 feat: FC 工具去重 + 按场景动态裁剪,降低 AI 每轮负担
方案 A(去重):
- mcp_to_openai_tools 末尾按 function.name 去重,消除 cache+builtin 叠加
  造成的同名重复(memory/scheduler/lsp__* 等),FC 工具 42→36,去掉
  约 14% 的垃圾 schema token
- get_liugin_usage_prompts 的 plugins 段由逐条 name:desc 改为工具名清单,
  消除与 FC tools 字段的双重曝光,保留无 FC 模型的回退清单

方案 B(分层裁剪):
- 新增 prune_tools(fc_tools, user_input): 常驻 ALWAYS(tool_search/memory/
  scheduler) + 外部桥接(mcp__)默认保留 + 按关键词信号追加对应工具
  (代码/文件->code_editor+file_manager+lsp__*; git/shell/浏览器/搜索/子agent/
  工程化各按信号)
- cli_core 两处 FC 构建点(TUI/CLI)接入 prune_tools(current_input)

实测(AICLI): 纯聊天 FC token ~4310->285(降93%),写代码 ~973(降77%)

测试: 新增 tests/test_prune_tools.py (7 passed),全量 324 passed 无回归
2026-08-06 20:06:51 +08:00

116 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
测试:FC 工具去重 + 按场景动态裁剪(方案 A+B)。
不依赖真实插件/引擎加载,用构造的 fake 数据验证:
- mcp_to_openai_tools 按 name 去重(消除 cache + builtin 叠加重复)
- prune_tools 分层裁剪:纯聊天极简、代码信号追加 lsp__*、外部 mcp 默认保留
"""
import pytest
from xcli_core.fc_tools import mcp_to_openai_tools, prune_tools
def _fc(name, desc="d"):
return {
"type": "function",
"function": {
"name": name,
"description": desc,
"parameters": {"type": "object", "properties": {}, "required": []},
},
}
def _mcp(name, desc="d"):
"""MCP 格式(get_mcp_tools 返回的就是这种)"""
return {
"name": name,
"description": desc,
"inputSchema": {"type": "object", "properties": {}, "required": []},
}
class _FakePM:
"""最小 plugin_manager:get_mcp_tools 返回预置列表。"""
def __init__(self, tools):
self._tools = tools
def get_mcp_tools(self):
return self._tools
# ───────────── 去重 ─────────────
def test_mcp_to_openai_tools_dedup():
# 模拟真实场景:cache 里已有 memory/scheduler,_get_builtin_tools 又追加一遍
pm = _FakePM([
_mcp("a"), _mcp("a"), # 同名重复
_mcp("b"),
_mcp("memory"), _mcp("scheduler"), # cache 已有
])
out = mcp_to_openai_tools(pm)
names = [t["function"]["name"] for t in out]
assert names.count("a") == 1, "同名工具应去重为 1"
assert names.count("memory") == 1, "builtin 与 cache 重复应去重"
assert names.count("scheduler") == 1
assert "b" in names
# builtin 追加的 memory/scheduler 不应再产生第二份
assert names.count("memory") == 1
# ───────────── 分层裁剪 ─────────────
_CORE = ["tool_search", "memory", "scheduler", "code_editor", "code_search",
"file_manager", "lsp__diagnostics", "lsp__hover", "lsp__definition",
"lsp__references", "lsp__completion", "browser_auto", "network_tools",
"ai_search", "git_tools", "cmd_executor", "sub_agent", "auto_engineer"]
def _full_set():
return [_fc(n) for n in _CORE]
def test_prune_pure_chat_keeps_only_always():
out = prune_tools(_full_set(), "你好啊,今天天气真不错")
names = {t["function"]["name"] for t in out}
assert names == {"tool_search", "memory", "scheduler"}, \
f"纯聊天应只留常驻工具,实际: {names}"
def test_prune_code_signal_adds_lsp():
out = prune_tools(_full_set(), "帮我写一个 python 脚本读取文件")
names = {t["function"]["name"] for t in out}
assert "code_editor" in names
assert "file_manager" in names
assert "lsp__diagnostics" in names, "写代码应追加 pylsp 的 lsp__* 工具"
assert "lsp__completion" in names
assert "tool_search" in names
def test_prune_git_signal():
out = prune_tools(_full_set(), "提交代码并查看 git 状态")
names = {t["function"]["name"] for t in out}
assert "git_tools" in names
assert "tool_search" in names
def test_prune_external_mcp_kept_even_in_chat():
tools = [_fc("my_mcp__search"), _fc("code_editor"), _fc("tool_search")]
out = prune_tools(tools, "你好")
names = {t["function"]["name"] for t in out}
assert "my_mcp__search" in names, "外部桥接工具(mcp__)应默认保留"
assert "code_editor" not in names, "未命中信号的低频工具应被裁剪"
assert "tool_search" in names
def test_prune_empty_input_no_crash():
out = prune_tools(_full_set(), "")
names = {t["function"]["name"] for t in out}
assert names == {"tool_search", "memory", "scheduler"}
def test_prune_none_tools_returns_none():
assert prune_tools(None, "任意输入") is None
assert prune_tools([], "任意输入") == []