@eggjs/skills 4.1.2-beta.4 → 4.1.2-beta.6

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/PLAN.md DELETED
@@ -1,396 +0,0 @@
1
- # Skills 评测方案设计
2
-
3
- ## 目标
4
-
5
- 为 `packages/skills/` 设计一套评测体系,覆盖两个层面:
6
-
7
- 1. **静态校验** — 验证 Skill 文件的结构正确性、引用完整性
8
- 2. **动态评测** — 用 LLM-as-Judge 评估 AI 基于 Skill 生成回答的质量
9
-
10
- 全部手动触发运行,不集成 CI。
11
-
12
- ---
13
-
14
- ## 目录结构
15
-
16
- ```
17
- packages/skills/
18
- ├── egg/
19
- ├── controller/
20
- ├── tegg-core/
21
- ├── eval/ # 新增:评测目录
22
- │ ├── static/
23
- │ │ └── validate.test.ts # 静态校验测试
24
- │ ├── dynamic/
25
- │ │ ├── routing.eval.ts # 入口路由评测
26
- │ │ └── quality.eval.ts # 内容质量评测
27
- │ ├── fixtures/
28
- │ │ ├── routing-cases.ts # 路由测试用例
29
- │ │ └── quality-cases.ts # 质量测试用例
30
- │ └── lib/
31
- │ ├── skill-loader.ts # Skill 文件加载器
32
- │ ├── judge.ts # LLM-as-Judge 核心逻辑
33
- │ └── types.ts # 共享类型定义
34
- ├── vitest.config.ts
35
- ├── package.json
36
- └── tsconfig.json
37
- ```
38
-
39
- ---
40
-
41
- ## 第一部分:静态校验
42
-
43
- ### 校验项
44
-
45
- | 校验项 | 说明 |
46
- | -------------------- | ------------------------------------------------------------- |
47
- | **Frontmatter 格式** | 每个 SKILL.md 必须包含 `name`、`description`、`allowed-tools` |
48
- | **引用文件存在性** | SKILL.md 中提到的 `references/*.md` 文件必须存在 |
49
- | **交叉引用一致性** | 入口 skill 提到的子 skill 目录必须存在且包含 SKILL.md |
50
- | **Markdown 结构** | 标题层级合理(以 `# ` 开头,不跳级) |
51
- | **决策表完整性** | 入口 skill 的路由表中每个 skill 都有对应目录 |
52
-
53
- ### 实现方式
54
-
55
- 使用 vitest + node:assert 编写测试,通过 Node.js fs API 读取文件并解析:
56
-
57
- ```typescript
58
- // eval/static/validate.test.ts
59
- import { describe, it } from 'vitest';
60
- import assert from 'node:assert/strict';
61
- import { loadAllSkills } from '../lib/skill-loader.ts';
62
-
63
- describe('Skill 静态校验', () => {
64
- describe('Frontmatter', () => {
65
- it('每个 SKILL.md 包含必填字段: name, description, allowed-tools', ...);
66
- });
67
-
68
- describe('引用完整性', () => {
69
- it('SKILL.md 中引用的 references/ 文件均存在', ...);
70
- it('入口 skill 引用的子 skill 目录均存在', ...);
71
- });
72
-
73
- describe('Markdown 结构', () => {
74
- it('标题层级不跳级', ...);
75
- });
76
- });
77
- ```
78
-
79
- ---
80
-
81
- ## 第二部分:动态评测(LLM-as-Judge)
82
-
83
- ### 评测维度
84
-
85
- 动态评测分为两个子场景:
86
-
87
- #### 2.1 路由评测 — 入口 Skill 是否正确路由
88
-
89
- 测试 `egg/SKILL.md` 的决策逻辑:给定用户查询,判断 AI 是否路由到正确的子 skill。
90
-
91
- **测试用例结构:**
92
-
93
- ```typescript
94
- // eval/fixtures/routing-cases.ts
95
- export const routingCases: RoutingCase[] = [
96
- {
97
- query: '如何创建 HTTP controller?',
98
- expectedSkill: 'controller',
99
- reason: '明确提到 controller,属于协议实现',
100
- },
101
- {
102
- query: '@SingletonProto 和 @ContextProto 有什么区别?',
103
- expectedSkill: 'tegg-core',
104
- reason: '关于对象生命周期,属于核心概念',
105
- },
106
- {
107
- query: '我需要创建一个可以被 HTTP 控制器使用的服务',
108
- expectedSkill: 'tegg-core',
109
- reason: '模糊意图,按规则 1(基础优先)应路由到 core',
110
- },
111
- // ... 更多用例
112
- ];
113
- ```
114
-
115
- **测试实现:**
116
-
117
- ```typescript
118
- // eval/dynamic/routing.eval.ts
119
- import { describe, it } from 'vitest';
120
- import assert from 'node:assert/strict';
121
- import Anthropic from '@anthropic-ai/sdk';
122
- import { loadSkillContent } from '../lib/skill-loader.ts';
123
- import { routingCases } from '../fixtures/routing-cases.ts';
124
-
125
- const client = new Anthropic();
126
- const AVAILABLE_SKILLS = ['controller', 'tegg-core'];
127
-
128
- describe('路由评测', () => {
129
- // 加载入口 skill 作为 system prompt
130
- const entrySkillContent = loadSkillContent('egg');
131
-
132
- for (const { query, expectedSkill, reason } of routingCases) {
133
- it(`"${query}" → ${expectedSkill}`, async () => {
134
- // 1. 将 SKILL.md 作为 system prompt,发送用户查询
135
- const response = await client.messages.create({
136
- model: 'claude-sonnet-4-20250514',
137
- max_tokens: 1024,
138
- system: [
139
- entrySkillContent,
140
- // 约束输出格式,让 AI 只做路由决策
141
- `你是 EGG 框架技能路由器。根据上面的决策指南,分析用户查询并选择应该加载的技能。`,
142
- `可选技能: ${AVAILABLE_SKILLS.join(', ')}`,
143
- `只输出 JSON: {"skill": "<技能名>", "reason": "<简要理由>"}`,
144
- ].join('\n\n'),
145
- messages: [{ role: 'user', content: query }],
146
- });
147
-
148
- // 2. 解析 AI 回答中的路由选择
149
- const text = response.content[0].type === 'text' ? response.content[0].text : '';
150
- const parsed = JSON.parse(text);
151
-
152
- // 3. 断言路由正确性
153
- assert.equal(parsed.skill, expectedSkill,
154
- `路由错误: 期望 "${expectedSkill}" 但得到 "${parsed.skill}"` +
155
- `\n 用例理由: ${reason}` +
156
- `\n AI 理由: ${parsed.reason}`
157
- );
158
- });
159
- }
160
- });
161
- ```
162
-
163
- #### 2.2 内容质量评测 — 子 Skill 回答质量
164
-
165
- 测试各子 skill 对领域问题的回答质量。
166
-
167
- **测试用例结构:**
168
-
169
- ```typescript
170
- // eval/fixtures/quality-cases.ts
171
- export const qualityCases: QualityCase[] = [
172
- {
173
- skill: 'controller',
174
- query: '如何创建一个 POST 接口接收 JSON body?',
175
- criteria: [
176
- '使用 @HTTPController 装饰器',
177
- '使用 @HTTPMethod 且 method 为 POST',
178
- '使用 @HTTPBody() 获取请求体',
179
- '包含完整可运行的代码示例',
180
- ],
181
- references: ['references/http-controller.md'], // 需要加载的参考文档
182
- },
183
- {
184
- skill: 'tegg-core',
185
- query: '如何让一个服务可以被其他模块访问?',
186
- criteria: [
187
- '提到 AccessLevel.PUBLIC',
188
- '使用 @SingletonProto 装饰器',
189
- '解释跨模块访问机制',
190
- ],
191
- references: [],
192
- },
193
- // ... 更多用例
194
- ];
195
- ```
196
-
197
- **测试实现:**
198
-
199
- ```typescript
200
- // eval/dynamic/quality.eval.ts
201
- import { describe, it } from 'vitest';
202
- import assert from 'node:assert/strict';
203
- import Anthropic from '@anthropic-ai/sdk';
204
- import { loadSkillContent, loadReference } from '../lib/skill-loader.ts';
205
- import { qualityCases } from '../fixtures/quality-cases.ts';
206
- import { judge } from '../lib/judge.ts';
207
-
208
- const client = new Anthropic();
209
-
210
- describe('内容质量评测', () => {
211
- for (const testCase of qualityCases) {
212
- describe(`[${testCase.skill}] ${testCase.query}`, () => {
213
- let aiResponse: string;
214
-
215
- // Step 1: 加载 skill 内容作为 system prompt,向被测 LLM 提问
216
- it('生成回答', async () => {
217
- const skillContent = loadSkillContent(testCase.skill);
218
- const refContents = testCase.references
219
- .map(ref => loadReference(testCase.skill, ref));
220
-
221
- const systemPrompt = [skillContent, ...refContents].join('\n\n---\n\n');
222
-
223
- const response = await client.messages.create({
224
- model: 'claude-sonnet-4-20250514',
225
- max_tokens: 2048,
226
- system: systemPrompt,
227
- messages: [{ role: 'user', content: testCase.query }],
228
- });
229
-
230
- aiResponse = response.content[0].type === 'text' ? response.content[0].text : '';
231
- assert.ok(aiResponse.length > 0, 'AI 应该返回非空回答');
232
- });
233
-
234
- // Step 2: 用 Judge LLM 对回答逐项评分
235
- it('通过质量评审', async () => {
236
- const result = await judge(client, {
237
- query: testCase.query,
238
- response: aiResponse,
239
- criteria: testCase.criteria,
240
- });
241
-
242
- // 输出详细评分到 console 供人工查看
243
- console.log(` 得分: ${result.totalScore} (${result.passed}/${result.total})`);
244
- for (const item of result.details) {
245
- const icon = item.score === 1 ? '✓' : '✗';
246
- console.log(` ${icon} ${item.criterion}: ${item.reason}`);
247
- }
248
-
249
- // 断言:所有 criteria 都应满足
250
- assert.ok(result.totalScore >= 0.8,
251
- `质量不达标: ${result.totalScore} < 0.8\n` +
252
- result.details
253
- .filter(d => d.score === 0)
254
- .map(d => ` ✗ ${d.criterion}: ${d.reason}`)
255
- .join('\n')
256
- );
257
- });
258
- });
259
- }
260
- });
261
- ```
262
-
263
- ### LLM-as-Judge 实现
264
-
265
- ```typescript
266
- // eval/lib/judge.ts
267
- import type Anthropic from '@anthropic-ai/sdk';
268
- import type { JudgeInput, JudgeResult, JudgeDetail } from './types.ts';
269
-
270
- export async function judge(
271
- client: Anthropic,
272
- input: JudgeInput,
273
- ): Promise<JudgeResult> {
274
- const criteriaList = input.criteria
275
- .map((c, i) => `${i + 1}. ${c}`)
276
- .join('\n');
277
-
278
- const response = await client.messages.create({
279
- model: 'claude-sonnet-4-20250514',
280
- max_tokens: 1024,
281
- system: '你是 AI 回答质量评估专家。严格按照 JSON 格式输出评分结果。',
282
- messages: [{
283
- role: 'user',
284
- content: `请根据评分标准,对以下 AI 回答逐项评分。
285
-
286
- ## 评分标准
287
- ${criteriaList}
288
-
289
- ## 用户问题
290
- ${input.query}
291
-
292
- ## AI 回答
293
- ${input.response}
294
-
295
- ## 输出格式(严格 JSON)
296
- {
297
- "details": [
298
- { "criterion": "标准内容", "score": 0 或 1, "reason": "简要理由" }
299
- ]
300
- }`,
301
- }],
302
- });
303
-
304
- const text = response.content[0].type === 'text' ? response.content[0].text : '';
305
- const parsed = JSON.parse(text);
306
- const details: JudgeDetail[] = parsed.details;
307
- const passed = details.filter(d => d.score === 1).length;
308
-
309
- return {
310
- details,
311
- passed,
312
- total: details.length,
313
- totalScore: passed / details.length,
314
- };
315
- }
316
- ```
317
-
318
- ### 评测报告
319
-
320
- 运行评测后生成 JSON 报告:
321
-
322
- ```json
323
- {
324
- "timestamp": "2026-02-05T10:00:00Z",
325
- "routing": {
326
- "total": 10,
327
- "correct": 9,
328
- "accuracy": 0.9,
329
- "failures": [
330
- {
331
- "query": "...",
332
- "expected": "tegg-core",
333
- "actual": "controller",
334
- "reason": "..."
335
- }
336
- ]
337
- },
338
- "quality": {
339
- "controller": {
340
- "cases": 5,
341
- "avg_score": 0.85,
342
- "details": [...]
343
- },
344
- "tegg-core": {
345
- "cases": 5,
346
- "avg_score": 0.90,
347
- "details": [...]
348
- }
349
- }
350
- }
351
- ```
352
-
353
- ---
354
-
355
- ## 第三部分:技术选型与依赖
356
-
357
- | 组件 | 选型 | 理由 |
358
- | --------------------- | ------------------ | ------------------------------ |
359
- | 测试框架 | vitest | 遵循 monorepo 标准 |
360
- | 断言库 | node:assert/strict | Node.js 内置,零依赖 |
361
- | YAML frontmatter 解析 | gray-matter | 成熟的 frontmatter 解析库 |
362
- | LLM 调用 | @anthropic-ai/sdk | 使用 Claude API 做评测和 Judge |
363
- | 报告输出 | JSON 文件 | 简单可读,方便后续扩展为可视化 |
364
-
365
- ### package.json scripts
366
-
367
- ```json
368
- {
369
- "scripts": {
370
- "test": "vitest run --config vitest.config.ts eval/static/",
371
- "eval": "vitest run --config vitest.config.ts eval/dynamic/",
372
- "eval:routing": "vitest run --config vitest.config.ts eval/dynamic/routing.eval.ts",
373
- "eval:quality": "vitest run --config vitest.config.ts eval/dynamic/quality.eval.ts"
374
- }
375
- }
376
- ```
377
-
378
- - `test` — 运行静态校验(快速,无 API 调用)
379
- - `eval` — 运行全部动态评测
380
- - `eval:routing` — 仅运行路由评测
381
- - `eval:quality` — 仅运行内容质量评测
382
-
383
- 动态评测需设置 `ANTHROPIC_API_KEY` 环境变量。
384
-
385
- ---
386
-
387
- ## 实施步骤
388
-
389
- 1. 在 worktree (`egg-skills-eval`) 中添加依赖:vitest、gray-matter、@anthropic-ai/sdk
390
- 2. 添加 `vitest.config.ts` 和更新 `package.json` scripts
391
- 3. 创建 `eval/lib/` 基础工具:skill-loader、types、judge
392
- 4. 实现 `eval/static/validate.test.ts` 静态校验
393
- 5. 编写路由测试用例 `eval/fixtures/routing-cases.ts`
394
- 6. 实现 `eval/dynamic/routing.eval.ts` 路由评测
395
- 7. 编写质量测试用例 `eval/fixtures/quality-cases.ts`
396
- 8. 实现 `eval/dynamic/quality.eval.ts` 内容质量评测