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 无回归
This commit is contained in:
1 parent
1054932921
commit
b577b0ff21
3 files changed
+239
-13
No files matched your search
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
测试: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([], "任意输入") == []
|
||||
Reference in new issue
Block a user