方案 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 无回归
116 lines
3.9 KiB
Python
116 lines
3.9 KiB
Python
"""
|
||
测试: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([], "任意输入") == []
|