modelfunction-cli 0.1.1 → 0.1.3
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 +10 -3
- package/lib/cases.js +50 -2
- package/lib/lint.js +100 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -5,12 +5,19 @@
|
|
|
5
5
|
## 安装
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx -y modelfunction-cli@0.1.
|
|
8
|
+
npx -y modelfunction-cli@0.1.3 --version
|
|
9
9
|
# 或
|
|
10
10
|
npm i -D modelfunction-cli
|
|
11
11
|
npx mf --version
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
全局安装(可选)后命令为 `mf`,也可以用 `modelfunction`——避开装了 TeX 的机器上 `mf` 与 Metafont 同名:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm i -g modelfunction-cli
|
|
18
|
+
modelfunction --version
|
|
19
|
+
```
|
|
20
|
+
|
|
14
21
|
## 五步上手
|
|
15
22
|
|
|
16
23
|
```bash
|
|
@@ -76,7 +83,7 @@ INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
|
|
|
76
83
|
"mcpServers": {
|
|
77
84
|
"modelfunction": {
|
|
78
85
|
"command": "npx",
|
|
79
|
-
"args": ["-y", "modelfunction-cli@0.1.
|
|
86
|
+
"args": ["-y", "modelfunction-cli@0.1.3", "mcp"]
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
}
|
|
@@ -98,7 +105,7 @@ INTERNAL_LLM_KEY=... mf eval --suite mf.suite.json \
|
|
|
98
105
|
|
|
99
106
|
```bash
|
|
100
107
|
# 1. 把 mf 挂成 MCP server
|
|
101
|
-
claude mcp add modelfunction -- npx -y modelfunction-cli@0.1.
|
|
108
|
+
claude mcp add modelfunction -- npx -y modelfunction-cli@0.1.3 mcp
|
|
102
109
|
```
|
|
103
110
|
|
|
104
111
|
```text
|
package/lib/cases.js
CHANGED
|
@@ -167,15 +167,63 @@ export function suspectedPairs(tools) {
|
|
|
167
167
|
if (union.size === 0) continue;
|
|
168
168
|
let shared = 0;
|
|
169
169
|
for (const token of union) if (a.has(token) && b.has(token)) shared += 1;
|
|
170
|
-
const
|
|
170
|
+
const lexical = shared / union.size;
|
|
171
|
+
const structural = structuralScore(tools[i], tools[j]);
|
|
172
|
+
const similarity = Math.max(lexical, structural);
|
|
171
173
|
if (similarity >= PAIR_SIMILARITY) {
|
|
172
|
-
pairs.push({
|
|
174
|
+
pairs.push({
|
|
175
|
+
a: tools[i].name,
|
|
176
|
+
b: tools[j].name,
|
|
177
|
+
similarity,
|
|
178
|
+
basis: structural > lexical ? "structural" : "lexical"
|
|
179
|
+
});
|
|
173
180
|
}
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
return pairs.sort((x, y) => y.similarity - x.similarity);
|
|
177
184
|
}
|
|
178
185
|
|
|
186
|
+
// Tools that share part of the name (search / file / read...) and the same
|
|
187
|
+
// parameter shape confuse the model even when the wording differs, which the
|
|
188
|
+
// lexical score alone misses. Name order is unreliable: `tavily_search` leads
|
|
189
|
+
// with the vendor, `search_docs` with the action.
|
|
190
|
+
function structuralScore(x, y) {
|
|
191
|
+
const left = nameTokens(x.name);
|
|
192
|
+
const right = nameTokens(y.name);
|
|
193
|
+
const sharedName = left.some((token) => right.includes(token));
|
|
194
|
+
if (!sharedName) return 0;
|
|
195
|
+
const params = paramNames(x);
|
|
196
|
+
const otherParams = paramNames(y);
|
|
197
|
+
if (params.size === 0 && otherParams.size === 0) return 0;
|
|
198
|
+
const paramOverlap = jaccard(params, otherParams);
|
|
199
|
+
if (paramOverlap < 0.34) return 0;
|
|
200
|
+
const requiredOverlap = jaccard(
|
|
201
|
+
new Set(x.parameters?.required ?? []),
|
|
202
|
+
new Set(y.parameters?.required ?? [])
|
|
203
|
+
);
|
|
204
|
+
return 0.5 + 0.5 * (0.6 * paramOverlap + 0.4 * requiredOverlap);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nameTokens(name) {
|
|
208
|
+
return String(name ?? "")
|
|
209
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
210
|
+
.toLowerCase()
|
|
211
|
+
.split(/[^a-z0-9]+/)
|
|
212
|
+
.filter(Boolean);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function paramNames(tool) {
|
|
216
|
+
return new Set(Object.keys(tool.parameters?.properties ?? {}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function jaccard(a, b) {
|
|
220
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
221
|
+
const union = new Set([...a, ...b]);
|
|
222
|
+
let shared = 0;
|
|
223
|
+
for (const item of union) if (a.has(item) && b.has(item)) shared += 1;
|
|
224
|
+
return shared / union.size;
|
|
225
|
+
}
|
|
226
|
+
|
|
179
227
|
function tokens(text) {
|
|
180
228
|
const set = new Set();
|
|
181
229
|
const lowered = String(text ?? "").toLowerCase();
|
package/lib/lint.js
CHANGED
|
@@ -15,6 +15,42 @@ const GENERIC_NAMES = new Set([
|
|
|
15
15
|
|
|
16
16
|
const WHEN_HINT = /(用于|用来|当|如果|适用|适合|use when|use this|helps? )/i;
|
|
17
17
|
|
|
18
|
+
// Identifier tokens, matched on token boundaries only: `request_text` is text,
|
|
19
|
+
// `request_id` is an identifier. Strong = platform-injected (session/tenant/trace
|
|
20
|
+
// class); weak = business id the user may or may not supply.
|
|
21
|
+
const STRONG_ID_TOKENS = new Set([
|
|
22
|
+
"uuid", "guid", "sid", "nonce", "token", "signature",
|
|
23
|
+
"sessionid", "tenantid", "traceid", "threadid", "conversationid", "correlationid", "requestid",
|
|
24
|
+
"session", "tenant", "trace", "thread", "conversation", "correlation"
|
|
25
|
+
]);
|
|
26
|
+
const WEAK_ID_TOKENS = new Set(["id", "ids"]);
|
|
27
|
+
const OPAQUE_ID_SCHEMA = /uuid|[0-9a-f]{8}-[0-9a-f]{4}/i;
|
|
28
|
+
const DESCRIPTION_ID_HINT = /(会话\s*id|session\s*id|租户|tenant|trace|请求\s*id|request\s*id|签名|signature|\btoken\b|nonce|uuid|标识|编号)/i;
|
|
29
|
+
const ID_SOURCE_HINT = /(来源|取自|来自|传入|注入|平台|系统|上下文|请求头|header|无需|自动|会提供|由调用方|上一步)/i;
|
|
30
|
+
const FIXED_ID_LENGTHS = new Set([8, 16, 24, 32, 36, 40, 64]);
|
|
31
|
+
|
|
32
|
+
function nameTokens(name) {
|
|
33
|
+
return String(name)
|
|
34
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
35
|
+
.split(/[^A-Za-z0-9]+/)
|
|
36
|
+
.map((token) => token.toLowerCase())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function idStrength(name) {
|
|
41
|
+
const tokens = nameTokens(name);
|
|
42
|
+
if (tokens.length === 0) return null;
|
|
43
|
+
const last = tokens[tokens.length - 1];
|
|
44
|
+
const joinedTail = tokens.slice(-2).join("");
|
|
45
|
+
if (STRONG_ID_TOKENS.has(last) || STRONG_ID_TOKENS.has(joinedTail)) return "strong";
|
|
46
|
+
if (WEAK_ID_TOKENS.has(last)) return "weak";
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const BOUNDARY_HINT = /(不适用|不要用于|不用于|不能用于|而非|区别于|not for|do not use)/i;
|
|
51
|
+
// Chinese descriptions are compared as bigrams, so mid-0.3s similarity is noise.
|
|
52
|
+
const NO_BOUNDARY_SIMILARITY = 0.5;
|
|
53
|
+
|
|
18
54
|
export function lintSuite(suite) {
|
|
19
55
|
const findings = [];
|
|
20
56
|
const add = (severity, code, tool, message, extra = {}) =>
|
|
@@ -63,6 +99,55 @@ export function lintSuite(suite) {
|
|
|
63
99
|
});
|
|
64
100
|
}
|
|
65
101
|
|
|
102
|
+
for (const name of required) {
|
|
103
|
+
const schema = properties[name];
|
|
104
|
+
if (!schema) continue;
|
|
105
|
+
const description = String(schema.description ?? "");
|
|
106
|
+
const fixedLength =
|
|
107
|
+
typeof schema.minLength === "number" &&
|
|
108
|
+
schema.minLength === schema.maxLength &&
|
|
109
|
+
FIXED_ID_LENGTHS.has(schema.minLength);
|
|
110
|
+
const schemaOpaque =
|
|
111
|
+
(typeof schema.format === "string" && OPAQUE_ID_SCHEMA.test(schema.format)) ||
|
|
112
|
+
(typeof schema.pattern === "string" && OPAQUE_ID_SCHEMA.test(schema.pattern)) ||
|
|
113
|
+
fixedLength;
|
|
114
|
+
const nameStrength = idStrength(name);
|
|
115
|
+
// Name tokens alone are a weak signal: the author picks the names. Schema
|
|
116
|
+
// shape and the description are what the model actually reasons from.
|
|
117
|
+
const strong = schemaOpaque || DESCRIPTION_ID_HINT.test(description) || nameStrength === "strong";
|
|
118
|
+
const weak = nameStrength === "weak";
|
|
119
|
+
const scalar = schema.type === undefined || schema.type === "string";
|
|
120
|
+
if ((strong || weak) && scalar && !ID_SOURCE_HINT.test(description)) {
|
|
121
|
+
add(
|
|
122
|
+
strong ? "warn" : "info",
|
|
123
|
+
"required-unsourced",
|
|
124
|
+
tool.name,
|
|
125
|
+
strong
|
|
126
|
+
? `必填参数 ${name} 像是外部注入的标识,但没说明从哪里获取;模型拿不到它时可能干脆不调用任何工具`
|
|
127
|
+
: `必填参数 ${name} 是业务 ID;如果用户不会在对话里给出,模型可能编造或干脆不调用`,
|
|
128
|
+
{
|
|
129
|
+
param: name,
|
|
130
|
+
suggestion: `改为平台注入(移出 schema),或在 description 写明来源,例如「由平台在会话开始时注入,模型无需生成」`
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const toolCases = (suite.cases ?? []).filter((testCase) => testCase.expectedTool === tool.name);
|
|
137
|
+
if (toolCases.length > 0) {
|
|
138
|
+
for (const name of required) {
|
|
139
|
+
const covered = toolCases.some(
|
|
140
|
+
(testCase) => testCase.expectedArgs && Object.prototype.hasOwnProperty.call(testCase.expectedArgs, name)
|
|
141
|
+
);
|
|
142
|
+
if (!covered) {
|
|
143
|
+
add("info", "required-uncovered", tool.name, `必填参数 ${name} 没有任何用例校验过,模型是否真能填对它还没有证据`, {
|
|
144
|
+
param: name,
|
|
145
|
+
suggestion: `补一条用例在 expectedArgs 里断言 ${name},或把它降为可选 / 改为平台注入`
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
66
151
|
for (const [name, schema] of Object.entries(properties)) {
|
|
67
152
|
if (!schema?.description) {
|
|
68
153
|
add("warn", "param-desc-missing", tool.name, `参数 ${name} 没有说明`, {
|
|
@@ -83,9 +168,22 @@ export function lintSuite(suite) {
|
|
|
83
168
|
}
|
|
84
169
|
|
|
85
170
|
for (const pair of suspectedPairs(suite.tools)) {
|
|
86
|
-
|
|
87
|
-
|
|
171
|
+
const similarity = pair.similarity.toFixed(2);
|
|
172
|
+
const declaresBoundary = [pair.a, pair.b].some((name) => {
|
|
173
|
+
const tool = suite.tools.find((item) => item.name === name);
|
|
174
|
+
return BOUNDARY_HINT.test(tool?.description ?? "");
|
|
88
175
|
});
|
|
176
|
+
if (declaresBoundary) {
|
|
177
|
+
add("info", "pair-overlap", pair.a, `与 ${pair.b} 的描述相似度 ${similarity},已写互斥边界`, {
|
|
178
|
+
pair: [pair.a, pair.b]
|
|
179
|
+
});
|
|
180
|
+
} else if (pair.similarity >= NO_BOUNDARY_SIMILARITY) {
|
|
181
|
+
const reason = pair.basis === "structural" ? "名字与参数形状接近" : "描述相近";
|
|
182
|
+
add("warn", "pair-no-boundary", pair.a, `与 ${pair.b} ${reason}(相似度 ${similarity}),且两边都没写“不适用于”边界,模型只能靠相似度硬猜`, {
|
|
183
|
+
pair: [pair.a, pair.b],
|
|
184
|
+
suggestion: `在 ${pair.a} 和 ${pair.b} 的 description 里各写一句“不适用于什么”,并补一条歧义用例`
|
|
185
|
+
});
|
|
186
|
+
}
|
|
89
187
|
}
|
|
90
188
|
|
|
91
189
|
const counts = { error: 0, warn: 0, info: 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelfunction-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "mf — function-calling accuracy scorecard for AI agents",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"agent"
|
|
14
14
|
],
|
|
15
15
|
"bin": {
|
|
16
|
-
"mf": "mf.js"
|
|
16
|
+
"mf": "mf.js",
|
|
17
|
+
"modelfunction": "mf.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
19
20
|
"mf.js",
|