modelfunction-cli 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/SCHEMA.md +4 -1
- package/lib/lint.js +105 -0
- package/package.json +1 -1
- package/skills/mf-cases/SKILL.md +7 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y modelfunction-cli@0.6.
|
|
8
|
+
npx -y modelfunction-cli@0.6.1 --version
|
|
9
9
|
# 或
|
|
10
10
|
npm i -D modelfunction-cli
|
|
11
11
|
npx mf --version
|
|
@@ -80,7 +80,7 @@ INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
|
|
|
80
80
|
| `mf case add` | 写入用例(默认 `source=agent`、`reviewed=false`) |
|
|
81
81
|
| `mf coverage` | 每工具用例数、参数覆盖、负例比例、疑似混淆对;有 `profiles` 时按组统计 |
|
|
82
82
|
| `mf audit` | 一次静态审计:lint + 混淆族边界 + 用例覆盖 + 挂载检查,输出可转发的 Markdown/JSON;不调用模型 |
|
|
83
|
-
| `mf lint` | 静态检查工具描述与参数 schema,以及 skills 的 name/description
|
|
83
|
+
| `mf lint` | 静态检查工具描述与参数 schema,以及 skills 的 name/description(不调用模型);0.6.1 起含「可选参数祈使句」与「悬空工具名引用」两条 info 规则;重复规则自动合并成一条;有 `profiles` 时给出「这些组不要合并挂载」 |
|
|
84
84
|
| `mf check` | 安装门禁:一个或一包新能力 × 已装能力(`--manifest` / `--manifests` + `--installed`),纯静态、返回 pass/warn、不阻断;门禁语境只对混淆对/必填来源/结构性错误报 warn,其余降 info;也可 `import { check, checkMany } from "modelfunction-cli"` |
|
|
85
85
|
| `mf compare` | 跨版本比较、干扰归因、混淆矩阵;两份报告都有分组时按组对比 |
|
|
86
86
|
| `mf ablate` | 留一法:去掉某个工具再跑一遍,量化它是否干扰其他工具;`--profile` 限定在某个挂载组内 |
|
|
@@ -126,7 +126,7 @@ mf eval --suite mf.suite.json --models internal-ds \
|
|
|
126
126
|
"mcpServers": {
|
|
127
127
|
"modelfunction": {
|
|
128
128
|
"command": "npx",
|
|
129
|
-
"args": ["-y", "modelfunction-cli@0.6.
|
|
129
|
+
"args": ["-y", "modelfunction-cli@0.6.1", "mcp"]
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -138,7 +138,7 @@ mf eval --suite mf.suite.json --models internal-ds \
|
|
|
138
138
|
|
|
139
139
|
对外 JSON 字段、`check()` 返回结构、MCP 参数与退出码见 [SCHEMA.md](SCHEMA.md),自 0.4.3 起冻结为 v1:v1 内只做加法,重命名或删除字段需 major 并提前两个 minor 或 90 天弃用。`check()` 的类型声明随包发布。
|
|
140
140
|
|
|
141
|
-
`0.6.
|
|
141
|
+
`0.6.1` 待 npm 发布:新增 `param-optional-imperative` 与 `desc-dangling-tool` 两条 info lint,并把「具名否定 / 泛化否定 / 可选参数祈使句 / 改名引用 / 话术范围」写进 `skills/mf-cases/SKILL.md`。`0.6.0` 已发布到 npm(latest):token 成为主口径(评分卡新增 `totalTokens`),模型注册表支持 `currency`(CNY/USD,默认 CNY),成本字段改为 `totalCost` / `avgCost` + `costCurrency`(`totalCostCny` / `avgCostCny` 仅在 CNY 时保留),新增 `mf eval --max-tokens <n>` 与 MCP `maxTokens`;`--fail-on-regression` 在基线模型缺失或本轮全部调用失败时改为 exit 2(无法判定 ≠ 无回归),自动选模型时在 stderr 打 note;本地门禁模板改用退出码判定、只在成功 eval 后更新 state,`.cmd` 固定 CRLF。`0.5.5` 含 eval `--retry <n>` / `--min-interval <ms>`、评分卡 `retries`、CI 模板 static 层 `cluster --json`、`templates/local/` 与 `docs/local-gate.md`(内网/非 GitHub 环境的 pre-push 静态门禁、变更门、定时回归)。`0.5.4` 含用例级 `excludeFromBase` / `excludeFromProfiles`、评分卡 `errorKind` / `errorsByKind` / `scoredTotal` / `selectionAccuracyExcludingErrors`、挂载对比错误不计 jammed、audit 分组 `neg%` 与分组 token 用量聚合。`0.5.3` 及更早的能力见下:凭证与本地反馈、usage `ci` 标记、eval `--progress`/`--max-cost`、`mf audit`、`scan --probe` streamable HTTP、MCP 静态工具 `lint`/`check`/`cluster`/`compare`、`checkMany` 一包批量检查、门禁严重度收敛、静态规则性能(真实 100 能力清单 `checkMany` 471ms→40ms)、`installed` 形状校验与 `profiles` 平台形态兼容。示例统一使用 `@0.6.0`。
|
|
142
142
|
|
|
143
143
|
## 数据边界
|
|
144
144
|
|
|
@@ -167,7 +167,7 @@ mf feedback --out out.json # 导出脱敏反馈包(聚合值,不含原始
|
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
169
|
# 1. 把 mf 挂成 MCP server
|
|
170
|
-
claude mcp add modelfunction -- npx -y modelfunction-cli@0.6.
|
|
170
|
+
claude mcp add modelfunction -- npx -y modelfunction-cli@0.6.1 mcp
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
```text
|
package/SCHEMA.md
CHANGED
|
@@ -141,8 +141,11 @@ report:`root`、`tools[]`(`name` / `description` / `parameters` / `origin` /
|
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
- `kind: "skill"` 时用 `skill` 字段代替 `tool`。
|
|
144
|
-
- `param` 出现在参数级发现上;`pair`
|
|
144
|
+
- `param` 出现在参数级发现上;`pair` 出现在工具对/技能对发现上;`ref` / `similarTo` 出现在悬空工具名引用上。
|
|
145
145
|
- `profilePairs` 只在套件含 `profiles` 时出现。
|
|
146
|
+
- 0.6.1 新增两条 advisory(均为 `info`,不影响 `check()` 的 pass/warn):
|
|
147
|
+
- `param-optional-imperative`:可选参数的描述用了祈使句;用户输入没提供该参数时可能抑制调用。
|
|
148
|
+
- `desc-dangling-tool`:描述引用了套件中不存在的工具名;`similarTo` 给出共享领域词的现有工具,通常来自改名后遗留的引用。
|
|
146
149
|
|
|
147
150
|
## 5. cluster JSON(`mf cluster --json`)
|
|
148
151
|
|
package/lib/lint.js
CHANGED
|
@@ -56,6 +56,70 @@ function idStrength(name) {
|
|
|
56
56
|
const BOUNDARY_HINT = /(不适用|不要用于|不用于|不能用于|而非|区别于|not for|do not use)/i;
|
|
57
57
|
// Chinese descriptions are compared as bigrams, so mid-0.3s similarity is noise.
|
|
58
58
|
const NO_BOUNDARY_SIMILARITY = 0.5;
|
|
59
|
+
// Two advisory checks from real description experiments: imperatives aimed at
|
|
60
|
+
// optional params can suppress the call entirely, and renamed tools leave stale
|
|
61
|
+
// names in sibling descriptions. Both are info-only; they depend on wording.
|
|
62
|
+
const OPTIONAL_HINT =
|
|
63
|
+
/(可选|optional|非必填|如有|如果需要|可提供|if [^.\n]{0,30}\b(provided|available|present|omitted)\b|when [^.\n]{0,30}\b(provided|available|present)\b)/i;
|
|
64
|
+
const IMPERATIVE_EN = /\b(pass|provide|supply|include|set|specify|add|send|put)\b/i;
|
|
65
|
+
const IMPERATIVE_ZH = /(传入|传|提供|带上|设置|指定|填写|附上|注明)/;
|
|
66
|
+
const IMPERATIVE_START_EN = /^(pass|provide|supply|include|set|specify|add|send|put)\b/i;
|
|
67
|
+
const IMPERATIVE_START_ZH = /^(传入|传|提供|带上|设置|指定|填写|附上|注明)/;
|
|
68
|
+
const BACKTICK_ID = /`([a-z][a-z0-9_-]{2,})`/g;
|
|
69
|
+
const SNAKE_ID = /\b([a-z][a-z0-9]*(?:_[a-z0-9]+)+)\b/g;
|
|
70
|
+
const NON_TOOL_SUFFIX =
|
|
71
|
+
/_(id|ids|key|token|path|name|url|uri|type|schema|count|size|format|length|text|json|yaml|yml|md|txt|html|htm|pdf|csv)$/;
|
|
72
|
+
const NON_TOOL_NAMES = new Set([
|
|
73
|
+
"api_key", "access_token", "base_url", "content_type", "status_code", "error_code",
|
|
74
|
+
"http_status", "input_schema", "output_schema", "json_schema", "file_path", "file_name",
|
|
75
|
+
"user_id", "session_id", "tenant_id", "request_id", "trace_id", "tool_name", "mcp_server"
|
|
76
|
+
]);
|
|
77
|
+
const ACTION_TOKENS = new Set([
|
|
78
|
+
"get", "set", "list", "read", "write", "search", "create", "delete", "update", "send",
|
|
79
|
+
"fetch", "query", "run", "exec", "execute", "open", "close", "add", "remove", "check",
|
|
80
|
+
"scan", "parse", "score", "match", "generate", "render", "save", "load", "find", "lookup",
|
|
81
|
+
"call", "invoke", "publish", "edit", "rewrite", "analyze", "analyse", "summarize",
|
|
82
|
+
"summarise", "translate", "tool", "tools", "info", "data", "result", "results", "item",
|
|
83
|
+
"items", "skill", "skills", "mcp"
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function escapeRegExp(value) {
|
|
87
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mentionsImperatively(text, name) {
|
|
91
|
+
if (!text || !name) return false;
|
|
92
|
+
const index = text.indexOf(name);
|
|
93
|
+
if (index === -1) return false;
|
|
94
|
+
const around = text.slice(Math.max(0, index - 40), Math.min(text.length, index + name.length + 40));
|
|
95
|
+
if (OPTIONAL_HINT.test(around)) return false;
|
|
96
|
+
return IMPERATIVE_EN.test(around) || IMPERATIVE_ZH.test(around);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function startsImperatively(text) {
|
|
100
|
+
const trimmed = String(text ?? "").trim();
|
|
101
|
+
if (!trimmed || OPTIONAL_HINT.test(trimmed)) return false;
|
|
102
|
+
return IMPERATIVE_START_EN.test(trimmed) || IMPERATIVE_START_ZH.test(trimmed);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function referencedNames(text) {
|
|
106
|
+
const found = new Set();
|
|
107
|
+
for (const match of String(text ?? "").matchAll(BACKTICK_ID)) found.add(match[1]);
|
|
108
|
+
for (const match of String(text ?? "").matchAll(SNAKE_ID)) found.add(match[1]);
|
|
109
|
+
return found;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sharedDomainToken(candidate, toolNames) {
|
|
113
|
+
const candidateTokens = new Set(nameTokens(candidate).filter((token) => !ACTION_TOKENS.has(token)));
|
|
114
|
+
if (candidateTokens.size === 0) return null;
|
|
115
|
+
for (const known of toolNames) {
|
|
116
|
+
const knownTokens = new Set(nameTokens(known).filter((token) => !ACTION_TOKENS.has(token)));
|
|
117
|
+
for (const token of candidateTokens) {
|
|
118
|
+
if (knownTokens.has(token)) return { name: known, token };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
59
123
|
|
|
60
124
|
// `pairScope` (a Set of names) is for incremental gates: only emit pair
|
|
61
125
|
// findings that touch one of the new capabilities. Installed × installed pairs
|
|
@@ -63,6 +127,12 @@ const NO_BOUNDARY_SIMILARITY = 0.5;
|
|
|
63
127
|
export function lintSuite(suite, { pairScope } = {}) {
|
|
64
128
|
const inScope = (pair) => !pairScope || pairScope.has(pair.a) || pairScope.has(pair.b);
|
|
65
129
|
const toolByName = new Map(suite.tools.map((tool) => [tool.name, tool]));
|
|
130
|
+
const toolNamesList = suite.tools.map((tool) => tool.name);
|
|
131
|
+
const knownNames = new Set([
|
|
132
|
+
...toolNamesList,
|
|
133
|
+
...(suite.skills ?? []).map((skill) => skill.name),
|
|
134
|
+
...suite.tools.flatMap((tool) => Object.keys(tool.parameters?.properties ?? {}))
|
|
135
|
+
]);
|
|
66
136
|
const findings = [];
|
|
67
137
|
const add = (severity, code, tool, message, extra = {}) =>
|
|
68
138
|
findings.push({ severity, code, kind: "tool", tool, message, ...extra });
|
|
@@ -207,6 +277,41 @@ export function lintSuite(suite, { pairScope } = {}) {
|
|
|
207
277
|
add("warn", "param-array-no-items", tool.name, `参数 ${name} 是数组但未定义 items`, { param: name });
|
|
208
278
|
}
|
|
209
279
|
}
|
|
280
|
+
|
|
281
|
+
for (const name of names) {
|
|
282
|
+
if (required.includes(name)) continue;
|
|
283
|
+
const schema = properties[name] ?? {};
|
|
284
|
+
if (mentionsImperatively(description, name) || startsImperatively(schema.description)) {
|
|
285
|
+
add(
|
|
286
|
+
"info",
|
|
287
|
+
"param-optional-imperative",
|
|
288
|
+
tool.name,
|
|
289
|
+
`可选参数 ${name} 用了祈使句提示;用户输入没提供它时,模型可能因此干脆不调用`,
|
|
290
|
+
{
|
|
291
|
+
param: name,
|
|
292
|
+
suggestion: `改成条件式(例如「如有${name}可传入」),或把 ${name} 改为必填`
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const ref of referencedNames(description)) {
|
|
299
|
+
if (ref === tool.name || knownNames.has(ref) || NON_TOOL_NAMES.has(ref) || NON_TOOL_SUFFIX.test(ref)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const similar = sharedDomainToken(ref, toolNamesList);
|
|
303
|
+
const backticked = new RegExp(`\`${escapeRegExp(ref)}\``).test(description);
|
|
304
|
+
if (!similar && !backticked) continue;
|
|
305
|
+
add(
|
|
306
|
+
"info",
|
|
307
|
+
"desc-dangling-tool",
|
|
308
|
+
tool.name,
|
|
309
|
+
similar
|
|
310
|
+
? `description 提到 ${ref},但套件里没有这个工具;最接近的是 ${similar.name}(共享 ${similar.token})。若是改名,请同步描述引用`
|
|
311
|
+
: `description 用代码格式提到 ${ref},但套件里没有这个工具;确认不是改名后遗留的悬空引用`,
|
|
312
|
+
{ ref, ...(similar ? { similarTo: similar.name } : {}) }
|
|
313
|
+
);
|
|
314
|
+
}
|
|
210
315
|
}
|
|
211
316
|
|
|
212
317
|
for (const pair of suspectedPairs(suite.tools)) {
|
package/package.json
CHANGED
package/skills/mf-cases/SKILL.md
CHANGED
|
@@ -31,6 +31,13 @@ description: 为 ModelFunction 评测套件撰写 function-calling 用例。当
|
|
|
31
31
|
- `{ "$exists": true|false }`(只关心有没有)
|
|
32
32
|
6. **不要用被测模型生成用例**,也不要让被测模型参与出题。
|
|
33
33
|
|
|
34
|
+
# 描述边界写法(实测)
|
|
35
|
+
|
|
36
|
+
- 排除具体竞争者时**点名**:`do NOT call file_read` 有效;泛化否定(`no separate file reads`)几乎压不住任何工具。
|
|
37
|
+
- 可选参数不要用祈使句(“传一个 title”);用户没给该参数时,模型可能干脆不调用。改成条件式:“如有 title 可传入”。
|
|
38
|
+
- 改名工具后,检查所有 description 是否还引用旧名;悬空引用会继续误导模型。
|
|
39
|
+
- 用例话术必须提交范围(公网 / 本地 / 会话历史);“上周那个记录”这类歧义无法靠描述修复,只能改话术。
|
|
40
|
+
|
|
34
41
|
# 输出格式
|
|
35
42
|
|
|
36
43
|
```json
|