Zec-Etch/tiny-skill-toolcall

Python

2

28 commits

updated Aug 23, 2026

See the code

README

TSTP — Tiny Skill Tool Call Proxy

一个轻量的 AI 代理(Python / FastAPI)。核心用途:压缩发给模型的工具信息,减少 token 消耗,同时提升工具调用的性能与准确率。 *目前仅支持 Responses API(/v1/responses)格式——客户端与上游均为该格式(如 LM Studio /v1/responses)。


为什么压缩工具信息

工具调用类任务的输入开销主要来自 tools 字段:每把工具都要携带完整的 description 和 parameters(JSON Schema)。当工具数量达到几十上百个时:

  • token 浪费:每次请求都携带全部工具的完整定义,输入 token 随工具数线性膨胀;
  • 选择变差:模型面对一长串工具定义容易"挑花眼",漏选、错选相关工具,多轮来回反而更慢。

TSTP 的做法是 两跳式工具选择(calltool 藏工具)

  1. 第一跳 · 打包:把"工具大全"(默认 benchmark/TOOLS_API_unique.json,73 个工具)里的工具全部藏起来,只给模型一个 call_tool 选择工具。call_tool 的描述里只列出工具名(不含描述、不含参数),提示词引导模型一次选出所有与请求相关的工具(可多选;除非任务明显只与某一个工具紧密相关,才单选)——第一跳的输入开销被压到最低;
  2. 第二跳 · 亮出:模型选好后,代理在单个续跳里一次性亮出全部选中工具的完整定义,模型直接发起真实调用(可并行多条、可同一工具多次调用);
  3. 对外只返回最终一跳:客户端收到的响应与直连上游一致,正文不含 call_tool 等中间过程,客户端零改动

效果:输入 token 大幅下降(不再每次携带全部工具 schema),且模型每次只聚焦于相关工具的完整定义,选择更准、调用更稳。第一跳还能用 summary_limit_chars 进一步压缩。


快速开始

# 1) 安装依赖(建议虚拟环境)
python -m venv .venv
.venv\Scripts\activate          # Windows;Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt

# 2) 配置上游
copy .env.example .env          # Windows;Linux/macOS: cp .env.example .env
#    编辑 .env:填 UPSTREAM_BASE_URL(默认 https://api.openai.com/v1)和 UPSTREAM_API_KEY

# 3) 启动
python run.py                   # 默认 http://127.0.0.1:8000

启动后可访问:

  • http://127.0.0.1:8000/docs — Swagger 交互文档
  • http://127.0.0.1:8000/api/tools/rules — 查看当前 tools 修改规则

客户端接入

客户端零改动:把 base_url 指向本代理即可(官方 openai SDK 无缝接入),代理会把请求里的 tools 按规则修改后再转发上游。

from openai import OpenAI

# 代理地址(.env 中 HOST/PORT 默认 127.0.0.1:8000)
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="占位即可")

resp = client.responses.create(
    model="gpt-4o-mini",
    input=[{"role": "user", "content": "查一下今天的 AI 新闻"}],
    tools=[...],          # 客户端实际发送的工具(Responses API 平铺格式);代理会按规则过滤/改名/注入/藏起
    tool_choice="auto",
)

完整示例见 examples/client_responses.py(含流式与非流式两种调用)。


接口一览

代理接口

方法路径说明
POST/v1/responses工具调用代理(Responses API):修改 tools → 转发上游 /v1/responses → 对外只渲染最终一跳(流式 SSE / 非流式 JSON 均支持)
GET/v1/models透传上游模型列表(方便 SDK 探测)

管理接口(可选 ADMIN_API_KEY 鉴权)

方法路径说明
GET/api/tools/rules查看当前 tools 修改规则
PUT/api/tools/rules整体替换规则({"rules": {...}}),立即生效并落盘
PATCH/api/tools/rules局部更新规则(深度合并,{"patch": {...}}
POST/api/tools/reload从磁盘重载 config/tools_rules.json 与"工具大全"目录
POST/api/tools/applydry-run:传入任意请求体,返回 tools 被修改前后的对比与最终请求体,不真正转发
GET/api/audit最近 N 条修改审计(logs/audit.jsonl

apply 示例(不连上游,直接看改了什么):

curl -X POST http://127.0.0.1:8000/api/tools/apply \
  -H "Content-Type: application/json" \
  -d '{"request_body": {"model": "gpt-4o-mini", "messages": [], "tools": [{"type":"function","function":{"name":"delete_all","description":"删库","parameters":{"type":"object","properties":{}}}}]}}'

返回 original_tools / modified_tools / operations / final_request 的完整对比。 完整示例见 examples/admin_api_demo.py


详细使用教程

1. 规则文件(config/tools_rules.json)

规则按以下顺序应用:

规则说明示例
deny黑名单过滤(支持 *前缀* 通配)["delete_*", "dangerous_tool_*"]
allow白名单;配置后只保留名单内工具["get_weather"]
rename重命名(改名后的名字参与后续所有步骤){"search_web": "web_search_v2"}
description_override覆盖描述;或 {"description":…, "append":…} 覆盖并追加{"web_search_v2": "搜索互联网。"}
parameters_patch深度合并 JSON Schema(properties 逐字段合并){"get_weather": {"properties": {"unit": {...}}}}
drop_fields删除 function 内指定字段(如 strict["strict"]
inject注入自定义工具(同名自动去重)完整 tool 对象数组
tool_choice强制 none/auto/required/指定工具null 表示删除客户端传值"auto"
parallel_tool_callstrue/falsenull 表示删除false
calltool藏工具(核心省 token 功能):把"工具大全"里的工具藏起来,换成唯一的 call_tool 选择工具(见下){"enabled": true, "catalog_file": "benchmark/TOOLS_API_unique.json"}

自适应处理:若过滤后工具为空,自动移除 tools/tool_choice,避免上游 4xx;若 tool_choice 指定的工具被过滤掉,自动降级为 auto 并记录审计。

2. calltool 藏工具(两跳流程)

开启后,凡是出现在"工具大全"(默认 benchmark/TOOLS_API_unique.json,73 个工具)里的工具都会被代理藏起来,避免模型面对几十个工具定义烧 token 又挑花眼:

  1. 打包(第一跳):请求里的"大全工具"全部移除,换成唯一的 call_tool 小工具。 call_tool 的参数是 names(字符串数组,一次可传多个工具名),描述为英文, 引导模型"选择所有与请求相关的工具"(Select ALL tools that are relevant to the user's request;除非任务明显只与某一个工具紧密相关才单选),并在描述里逐条列出 本次请求中被藏工具的工具名(按名字去重、只列名字——不含描述、不含参数,默认不截断)。 不在大全里的自定义工具保持原样、模型可直接调用。
  2. 亮出(第二跳):模型用 call_tool({"names": [...]}) 选好一个或多个工具后, 代理在单个续跳里一次性把全部选中工具的完整定义亮给模型(tools 里只有这些 真实工具、没有 call_tool),并自动补齐 call_tool 的执行结果: Please pick one or more tools you need to call from the detailed tool list. If necessary, a single tool may be called multiple times. 模型在该轮直接发起所有需要的真实调用(可并行多条、可同一工具多次调用)。 若模型填的名字不在此次请求中(乱编),则维持打包状态让模型重选(上限 max_continue_rounds,超限返回 422)。
  3. 回到第一步:模型正式调用完真实工具、客户端带回结果继续对话时,由于最近一次 调用不再是 call_tool,代理会重新打包。整个判断只看"这次请求聊天记录里最近的 一次模型工具调用",不保存任何服务器状态,可随意重启、多开。

配置项(挂在 rules["calltool"] 下):

配置默认说明
enabledfalse总开关
catalog_filebenchmark/TOOLS_API_unique.json"工具大全"文件(相对项目根或绝对路径)
call_tool_namecall_tool选择工具的名字
param_namename兼容回退键:旧格式 {"name": "X"} 仍可解析;新格式主参数是 names(数组)
result_messagePlease pick one or more tools you need to call from the detailed tool list. If necessary, a single tool may be called multiple times.选择工具的"执行结果"文案
override_client_resulttrue客户端已传选择结果时是否用上述文案覆盖
list_all_in_requestfalsetrue 时列出本次请求全部工具false 只列被藏的
summary_limit_chars(不限制)第一跳工具名列表总字数上限;默认不设置 = 被藏工具的工具名全部列出、不做任何截断;显式设置后超长截断并注明省略
auto_continuetrue代理恒为接管模式:模型调 call_tool 后,代理在服务器端自动续跑下一轮,对外只返回真实工具调用,客户端无需配合。该配置保留仅为兼容,实际恒开启
max_continue_rounds3接管模式下内部续跳上限;模型连续多轮只调 call_tool 不收敛 → 返回 422 tool_selection_not_converged
hop_thinkingoff续跳思考等级off=续跳请求 reasoning:{"effort":"none"}(responses 模式实测完全关闭思考,0 reasoning tokens);low/medium/high=对应 effort;inherit=保持客户端。只影响内部续跳——第一跳保留思考、不做任何额外限制(不关、不限长),其他请求保持客户端等级

一键启用:把 config/tools_rules_calltool_demo.json 的内容 PUT 到 /api/tools/rules (或直接编辑 config/tools_rules.jsoncalltool.enabled 改为 truePOST /api/tools/reload)。

3. 一次多选(模型一次调用传多个工具名)

模型可以在第一跳用一条 call_tool 调用传入多个工具名({"names": ["A", "B"]}, 也可发多条调用)。代理会:

  1. 单个续跳一次性亮出全部选中工具的完整定义;模型在这一轮直接发起所有需要的 真实调用——可并行多条、可同一工具多次调用(如查广州和北京天气都调 get_current_weather,一条续跳里发两次)。
  2. 续跳返回的全部真实 function_call 合并为对外一条 Responses API 响应 (output 里的 function_call 项,usage 累加;流式同样以 Responses API SSE 事件输出)。
  3. 名字去重保序;names 里重复的名字只亮出一次。仅当所选名字全部无效(乱编)时 维持打包态让模型重选。

4. 上游协议(固定为 Responses API)

代理只支持 Responses API:客户端与上游都是 /v1/responses 格式。上游需要支持 自定义 tools 与 reasoning 关思考(如 LM Studio /v1/responses;OpenAI /v1/responses 亦可)。

说明:续跳请求默认不回传历史(输入 = 客户端原始消息,上下文干净);代理对外 始终渲染"最终一跳",中间过程只写审计,不泄漏给客户端。

5. 测试

python -m pytest tests/ -v    # 规则引擎 / 目录 / 转发层纯逻辑单测,无网络依赖

另有 mock 上游集成冒烟测试(scripts/mock_upstream.py + scripts/smoke_test.py 等)与 BFCL 跑分脚本(benchmark/,见 benchmark/USE.md)。


配置项(.env)

变量默认说明
UPSTREAM_BASE_URLhttp://127.0.0.1:1234/v1上游地址(需支持 /v1/responses,如 LM Studio)
UPSTREAM_API_KEY上游 Key;留空则把客户端 Authorization 透传上游
PROXY_API_KEY设置后客户端访问 /v1/* 需带 Bearer <该值>
ADMIN_API_KEY设置后访问 /api/* 需带 Bearer <该值>
HOST / PORT127.0.0.1 / 8000监听地址
RULES_FILEconfig/tools_rules.json规则文件路径
AUDIT_FILElogs/audit.jsonl审计日志路径
CATALOG_FILEbenchmark/TOOLS_API_unique.json"工具大全"文件(calltool 藏工具用)
UPSTREAM_TIMEOUT1200上游请求超时(秒),本地模型排队时给足

注意事项

  • 模型只认识修改后的工具名:重命名/注入后,响应里的 function_call.name 是修改后的名字, 客户端按「代理暴露的工具集」(可用 /api/tools/apply 预览)来分派即可,无需关心原始名。
  • 审计日志记录每次修改的 tools_in → tools_out 与全部操作,方便排查「上游到底收到了什么」。

实测(LM Studio 本地模型)与复现

BFCL v4 题组(15 题 = multiple×5 + parallel_multiple×4 + live_multiple×4 + live_parallel_multiple×2)上, 用本机 LM Studio(http://127.0.0.1:1234,Gemma 4) 实测「直连」vs「经代理(calltool 藏工具 + 接管 + 续跳关思考)」 各一轮(直连 capture_direct/run3、代理 capture_proxy/run7):

性能提升(正确率)

指标直连经代理变化
BFCL 正确率13/15 (86.7%)14/15 (93.3%)+1 题(+6.7pp)
错题parallel_multiple_9(少发调用)、live_multiple_5-3-0(参数缺 , Chinalive_parallel_multiple_1-1-0(中文参数 '广州, 中国' 应为 'Guangzhou, China'直连 2 错全被代理纠正,错题不重叠

直连的两处稳定错误(并行调用个数、地名格式)经代理「藏工具 → 按 call_tool 逐步选定 → 续跳精确传参」后均被纠正; 代理唯一错题为中文题干下的参数取值问题,属模型侧取值细节。

成本下降(token 与费用)

指标直连经代理变化
输入 token12,0376,957-42.2%(藏工具收益)
输出 token2,0972,911+38.8%(两跳累计)
总 token14,1349,868-30.2%
思考 token2,0972,308+10.1%

费用模型:令输入单价 = 输出单价 × R,代理总成本更低 ⇔ R > 输出增量/输入节省量(本组实测门槛 R ≈ 0.16)。 即只要输入单价 ≥ 输出单价的 0.16 倍(绝大多数 API 定价都满足),经代理的总调用成本即低于直连: 按 R=1.0(输入:输出 = 1:1)计价约省 30%,R=0.5 约省 21%,R=2.0 约省 36%(输入越贵,藏工具省输入的价值越大)。

成本优势的核心来自大工具集场景:如 37 工具题输入 token 从 5,264 → 652(-88%);排除该题后门槛升至 1.62 倍。代理输出增加部分中,思考 token 仅占约 1/4(非思考的续跳正式调用输出占约 3/4)。

复现

# 前置:LM Studio 已在 1234 加载 google/gemma-4-12b-qat
# 跑分(直连一轮 + 代理一轮):
pwsh -File benchmark\re_run_capture.ps1 -Way capture_direct -Round 3 -Model local/gemma-4-12b-qat-FC-RESP
pwsh -File benchmark\re_run_capture.ps1 -Way capture_proxy -Round 7 -UseProxy -Model local/gemma-4-12b-qat-FC
# 一键对比两侧最新有效结果(自动跳过失败 run,生成 HTML/MD 报告 + 逐题详情):
pwsh -File benchmark\compare_latest.ps1

完整逐题对比报告(含思考全文对比、工具定义、每题"排除"联动统计、成本计算器)见 benchmark/runs/compares/compare_capture_direct_run3_vs_capture_proxy_run7.html(同目录 .md 摘要; 逐题详情在 details_* 子目录)。代理转发正确性冒烟测试(过滤/重命名/注入、SSE)可用 scripts\run_live_proxy.ps1 一键复跑(检查 1234 → 启动代理 → 跑全部断言 → 清理)。

目录结构

app/
  main.py            FastAPI 应用:代理端点(POST /v1/responses)+ tools 管理接口
  rules_engine.py    tools 修改规则引擎(核心,含 calltool 藏工具阶段)
  catalog.py         "工具大全"索引(calltool 用,只出工具名列表)
  upstream.py        上游转发(非流式)
  config.py          配置加载 + 规则文件读写
  audit.py           审计日志
  toolcall_relay.py  calltool 接管循环的纯辅助函数
  responses_relay.py /v1/responses 转发层(唯一协议:客户端与上游均为该格式)
config/tools_rules.json                 默认规则(calltool 默认关闭)
config/tools_rules_calltool_demo.json   calltool 一键启用示例
benchmark/                             工具大全数据与 BFCL 题库/跑分(见 benchmark/USE.md)
examples/                 演示:SDK 客户端(responses)+ 管理接口
scripts/                  mock 上游与冒烟测试
tests/                    单元测试

Contributors

Zec-Etch

28 commits

Zec-Etch/tiny-skill-toolcall

Python

2

28 commits

updated Aug 23, 2026

See the code

README

TSTP — Tiny Skill Tool Call Proxy

一个轻量的 AI 代理(Python / FastAPI)。核心用途:压缩发给模型的工具信息,减少 token 消耗,同时提升工具调用的性能与准确率。 *目前仅支持 Responses API(/v1/responses)格式——客户端与上游均为该格式(如 LM Studio /v1/responses)。


为什么压缩工具信息

工具调用类任务的输入开销主要来自 tools 字段:每把工具都要携带完整的 description 和 parameters(JSON Schema)。当工具数量达到几十上百个时:

  • token 浪费:每次请求都携带全部工具的完整定义,输入 token 随工具数线性膨胀;
  • 选择变差:模型面对一长串工具定义容易"挑花眼",漏选、错选相关工具,多轮来回反而更慢。

TSTP 的做法是 两跳式工具选择(calltool 藏工具)

  1. 第一跳 · 打包:把"工具大全"(默认 benchmark/TOOLS_API_unique.json,73 个工具)里的工具全部藏起来,只给模型一个 call_tool 选择工具。call_tool 的描述里只列出工具名(不含描述、不含参数),提示词引导模型一次选出所有与请求相关的工具(可多选;除非任务明显只与某一个工具紧密相关,才单选)——第一跳的输入开销被压到最低;
  2. 第二跳 · 亮出:模型选好后,代理在单个续跳里一次性亮出全部选中工具的完整定义,模型直接发起真实调用(可并行多条、可同一工具多次调用);
  3. 对外只返回最终一跳:客户端收到的响应与直连上游一致,正文不含 call_tool 等中间过程,客户端零改动

效果:输入 token 大幅下降(不再每次携带全部工具 schema),且模型每次只聚焦于相关工具的完整定义,选择更准、调用更稳。第一跳还能用 summary_limit_chars 进一步压缩。


快速开始

# 1) 安装依赖(建议虚拟环境)
python -m venv .venv
.venv\Scripts\activate          # Windows;Linux/macOS: source .venv/bin/activate
pip install -r requirements.txt

# 2) 配置上游
copy .env.example .env          # Windows;Linux/macOS: cp .env.example .env
#    编辑 .env:填 UPSTREAM_BASE_URL(默认 https://api.openai.com/v1)和 UPSTREAM_API_KEY

# 3) 启动
python run.py                   # 默认 http://127.0.0.1:8000

启动后可访问:

  • http://127.0.0.1:8000/docs — Swagger 交互文档
  • http://127.0.0.1:8000/api/tools/rules — 查看当前 tools 修改规则

客户端接入

客户端零改动:把 base_url 指向本代理即可(官方 openai SDK 无缝接入),代理会把请求里的 tools 按规则修改后再转发上游。

from openai import OpenAI

# 代理地址(.env 中 HOST/PORT 默认 127.0.0.1:8000)
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="占位即可")

resp = client.responses.create(
    model="gpt-4o-mini",
    input=[{"role": "user", "content": "查一下今天的 AI 新闻"}],
    tools=[...],          # 客户端实际发送的工具(Responses API 平铺格式);代理会按规则过滤/改名/注入/藏起
    tool_choice="auto",
)

完整示例见 examples/client_responses.py(含流式与非流式两种调用)。


接口一览

代理接口

方法路径说明
POST/v1/responses工具调用代理(Responses API):修改 tools → 转发上游 /v1/responses → 对外只渲染最终一跳(流式 SSE / 非流式 JSON 均支持)
GET/v1/models透传上游模型列表(方便 SDK 探测)

管理接口(可选 ADMIN_API_KEY 鉴权)

方法路径说明
GET/api/tools/rules查看当前 tools 修改规则
PUT/api/tools/rules整体替换规则({"rules": {...}}),立即生效并落盘
PATCH/api/tools/rules局部更新规则(深度合并,{"patch": {...}}
POST/api/tools/reload从磁盘重载 config/tools_rules.json 与"工具大全"目录
POST/api/tools/applydry-run:传入任意请求体,返回 tools 被修改前后的对比与最终请求体,不真正转发
GET/api/audit最近 N 条修改审计(logs/audit.jsonl

apply 示例(不连上游,直接看改了什么):

curl -X POST http://127.0.0.1:8000/api/tools/apply \
  -H "Content-Type: application/json" \
  -d '{"request_body": {"model": "gpt-4o-mini", "messages": [], "tools": [{"type":"function","function":{"name":"delete_all","description":"删库","parameters":{"type":"object","properties":{}}}}]}}'

返回 original_tools / modified_tools / operations / final_request 的完整对比。 完整示例见 examples/admin_api_demo.py


详细使用教程

1. 规则文件(config/tools_rules.json)

规则按以下顺序应用:

规则说明示例
deny黑名单过滤(支持 *前缀* 通配)["delete_*", "dangerous_tool_*"]
allow白名单;配置后只保留名单内工具["get_weather"]
rename重命名(改名后的名字参与后续所有步骤){"search_web": "web_search_v2"}
description_override覆盖描述;或 {"description":…, "append":…} 覆盖并追加{"web_search_v2": "搜索互联网。"}
parameters_patch深度合并 JSON Schema(properties 逐字段合并){"get_weather": {"properties": {"unit": {...}}}}
drop_fields删除 function 内指定字段(如 strict["strict"]
inject注入自定义工具(同名自动去重)完整 tool 对象数组
tool_choice强制 none/auto/required/指定工具null 表示删除客户端传值"auto"
parallel_tool_callstrue/falsenull 表示删除false
calltool藏工具(核心省 token 功能):把"工具大全"里的工具藏起来,换成唯一的 call_tool 选择工具(见下){"enabled": true, "catalog_file": "benchmark/TOOLS_API_unique.json"}

自适应处理:若过滤后工具为空,自动移除 tools/tool_choice,避免上游 4xx;若 tool_choice 指定的工具被过滤掉,自动降级为 auto 并记录审计。

2. calltool 藏工具(两跳流程)

开启后,凡是出现在"工具大全"(默认 benchmark/TOOLS_API_unique.json,73 个工具)里的工具都会被代理藏起来,避免模型面对几十个工具定义烧 token 又挑花眼:

  1. 打包(第一跳):请求里的"大全工具"全部移除,换成唯一的 call_tool 小工具。 call_tool 的参数是 names(字符串数组,一次可传多个工具名),描述为英文, 引导模型"选择所有与请求相关的工具"(Select ALL tools that are relevant to the user's request;除非任务明显只与某一个工具紧密相关才单选),并在描述里逐条列出 本次请求中被藏工具的工具名(按名字去重、只列名字——不含描述、不含参数,默认不截断)。 不在大全里的自定义工具保持原样、模型可直接调用。
  2. 亮出(第二跳):模型用 call_tool({"names": [...]}) 选好一个或多个工具后, 代理在单个续跳里一次性把全部选中工具的完整定义亮给模型(tools 里只有这些 真实工具、没有 call_tool),并自动补齐 call_tool 的执行结果: Please pick one or more tools you need to call from the detailed tool list. If necessary, a single tool may be called multiple times. 模型在该轮直接发起所有需要的真实调用(可并行多条、可同一工具多次调用)。 若模型填的名字不在此次请求中(乱编),则维持打包状态让模型重选(上限 max_continue_rounds,超限返回 422)。
  3. 回到第一步:模型正式调用完真实工具、客户端带回结果继续对话时,由于最近一次 调用不再是 call_tool,代理会重新打包。整个判断只看"这次请求聊天记录里最近的 一次模型工具调用",不保存任何服务器状态,可随意重启、多开。

配置项(挂在 rules["calltool"] 下):

配置默认说明
enabledfalse总开关
catalog_filebenchmark/TOOLS_API_unique.json"工具大全"文件(相对项目根或绝对路径)
call_tool_namecall_tool选择工具的名字
param_namename兼容回退键:旧格式 {"name": "X"} 仍可解析;新格式主参数是 names(数组)
result_messagePlease pick one or more tools you need to call from the detailed tool list. If necessary, a single tool may be called multiple times.选择工具的"执行结果"文案
override_client_resulttrue客户端已传选择结果时是否用上述文案覆盖
list_all_in_requestfalsetrue 时列出本次请求全部工具false 只列被藏的
summary_limit_chars(不限制)第一跳工具名列表总字数上限;默认不设置 = 被藏工具的工具名全部列出、不做任何截断;显式设置后超长截断并注明省略
auto_continuetrue代理恒为接管模式:模型调 call_tool 后,代理在服务器端自动续跑下一轮,对外只返回真实工具调用,客户端无需配合。该配置保留仅为兼容,实际恒开启
max_continue_rounds3接管模式下内部续跳上限;模型连续多轮只调 call_tool 不收敛 → 返回 422 tool_selection_not_converged
hop_thinkingoff续跳思考等级off=续跳请求 reasoning:{"effort":"none"}(responses 模式实测完全关闭思考,0 reasoning tokens);low/medium/high=对应 effort;inherit=保持客户端。只影响内部续跳——第一跳保留思考、不做任何额外限制(不关、不限长),其他请求保持客户端等级

一键启用:把 config/tools_rules_calltool_demo.json 的内容 PUT 到 /api/tools/rules (或直接编辑 config/tools_rules.jsoncalltool.enabled 改为 truePOST /api/tools/reload)。

3. 一次多选(模型一次调用传多个工具名)

模型可以在第一跳用一条 call_tool 调用传入多个工具名({"names": ["A", "B"]}, 也可发多条调用)。代理会:

  1. 单个续跳一次性亮出全部选中工具的完整定义;模型在这一轮直接发起所有需要的 真实调用——可并行多条、可同一工具多次调用(如查广州和北京天气都调 get_current_weather,一条续跳里发两次)。
  2. 续跳返回的全部真实 function_call 合并为对外一条 Responses API 响应 (output 里的 function_call 项,usage 累加;流式同样以 Responses API SSE 事件输出)。
  3. 名字去重保序;names 里重复的名字只亮出一次。仅当所选名字全部无效(乱编)时 维持打包态让模型重选。

4. 上游协议(固定为 Responses API)

代理只支持 Responses API:客户端与上游都是 /v1/responses 格式。上游需要支持 自定义 tools 与 reasoning 关思考(如 LM Studio /v1/responses;OpenAI /v1/responses 亦可)。

说明:续跳请求默认不回传历史(输入 = 客户端原始消息,上下文干净);代理对外 始终渲染"最终一跳",中间过程只写审计,不泄漏给客户端。

5. 测试

python -m pytest tests/ -v    # 规则引擎 / 目录 / 转发层纯逻辑单测,无网络依赖

另有 mock 上游集成冒烟测试(scripts/mock_upstream.py + scripts/smoke_test.py 等)与 BFCL 跑分脚本(benchmark/,见 benchmark/USE.md)。


配置项(.env)

变量默认说明
UPSTREAM_BASE_URLhttp://127.0.0.1:1234/v1上游地址(需支持 /v1/responses,如 LM Studio)
UPSTREAM_API_KEY上游 Key;留空则把客户端 Authorization 透传上游
PROXY_API_KEY设置后客户端访问 /v1/* 需带 Bearer <该值>
ADMIN_API_KEY设置后访问 /api/* 需带 Bearer <该值>
HOST / PORT127.0.0.1 / 8000监听地址
RULES_FILEconfig/tools_rules.json规则文件路径
AUDIT_FILElogs/audit.jsonl审计日志路径
CATALOG_FILEbenchmark/TOOLS_API_unique.json"工具大全"文件(calltool 藏工具用)
UPSTREAM_TIMEOUT1200上游请求超时(秒),本地模型排队时给足

注意事项

  • 模型只认识修改后的工具名:重命名/注入后,响应里的 function_call.name 是修改后的名字, 客户端按「代理暴露的工具集」(可用 /api/tools/apply 预览)来分派即可,无需关心原始名。
  • 审计日志记录每次修改的 tools_in → tools_out 与全部操作,方便排查「上游到底收到了什么」。

实测(LM Studio 本地模型)与复现

BFCL v4 题组(15 题 = multiple×5 + parallel_multiple×4 + live_multiple×4 + live_parallel_multiple×2)上, 用本机 LM Studio(http://127.0.0.1:1234,Gemma 4) 实测「直连」vs「经代理(calltool 藏工具 + 接管 + 续跳关思考)」 各一轮(直连 capture_direct/run3、代理 capture_proxy/run7):

性能提升(正确率)

指标直连经代理变化
BFCL 正确率13/15 (86.7%)14/15 (93.3%)+1 题(+6.7pp)
错题parallel_multiple_9(少发调用)、live_multiple_5-3-0(参数缺 , Chinalive_parallel_multiple_1-1-0(中文参数 '广州, 中国' 应为 'Guangzhou, China'直连 2 错全被代理纠正,错题不重叠

直连的两处稳定错误(并行调用个数、地名格式)经代理「藏工具 → 按 call_tool 逐步选定 → 续跳精确传参」后均被纠正; 代理唯一错题为中文题干下的参数取值问题,属模型侧取值细节。

成本下降(token 与费用)

指标直连经代理变化
输入 token12,0376,957-42.2%(藏工具收益)
输出 token2,0972,911+38.8%(两跳累计)
总 token14,1349,868-30.2%
思考 token2,0972,308+10.1%

费用模型:令输入单价 = 输出单价 × R,代理总成本更低 ⇔ R > 输出增量/输入节省量(本组实测门槛 R ≈ 0.16)。 即只要输入单价 ≥ 输出单价的 0.16 倍(绝大多数 API 定价都满足),经代理的总调用成本即低于直连: 按 R=1.0(输入:输出 = 1:1)计价约省 30%,R=0.5 约省 21%,R=2.0 约省 36%(输入越贵,藏工具省输入的价值越大)。

成本优势的核心来自大工具集场景:如 37 工具题输入 token 从 5,264 → 652(-88%);排除该题后门槛升至 1.62 倍。代理输出增加部分中,思考 token 仅占约 1/4(非思考的续跳正式调用输出占约 3/4)。

复现

# 前置:LM Studio 已在 1234 加载 google/gemma-4-12b-qat
# 跑分(直连一轮 + 代理一轮):
pwsh -File benchmark\re_run_capture.ps1 -Way capture_direct -Round 3 -Model local/gemma-4-12b-qat-FC-RESP
pwsh -File benchmark\re_run_capture.ps1 -Way capture_proxy -Round 7 -UseProxy -Model local/gemma-4-12b-qat-FC
# 一键对比两侧最新有效结果(自动跳过失败 run,生成 HTML/MD 报告 + 逐题详情):
pwsh -File benchmark\compare_latest.ps1

完整逐题对比报告(含思考全文对比、工具定义、每题"排除"联动统计、成本计算器)见 benchmark/runs/compares/compare_capture_direct_run3_vs_capture_proxy_run7.html(同目录 .md 摘要; 逐题详情在 details_* 子目录)。代理转发正确性冒烟测试(过滤/重命名/注入、SSE)可用 scripts\run_live_proxy.ps1 一键复跑(检查 1234 → 启动代理 → 跑全部断言 → 清理)。

目录结构

app/
  main.py            FastAPI 应用:代理端点(POST /v1/responses)+ tools 管理接口
  rules_engine.py    tools 修改规则引擎(核心,含 calltool 藏工具阶段)
  catalog.py         "工具大全"索引(calltool 用,只出工具名列表)
  upstream.py        上游转发(非流式)
  config.py          配置加载 + 规则文件读写
  audit.py           审计日志
  toolcall_relay.py  calltool 接管循环的纯辅助函数
  responses_relay.py /v1/responses 转发层(唯一协议:客户端与上游均为该格式)
config/tools_rules.json                 默认规则(calltool 默认关闭)
config/tools_rules_calltool_demo.json   calltool 一键启用示例
benchmark/                             工具大全数据与 BFCL 题库/跑分(见 benchmark/USE.md)
examples/                 演示:SDK 客户端(responses)+ 管理接口
scripts/                  mock 上游与冒烟测试
tests/                    单元测试

Contributors

Zec-Etch

28 commits

Languages

Python

98.1%

PowerShell

1.8%