feat: 完善两阶段 Agent 自进化 Pipeline、Skills 与 Benchmark (#129)

This commit is contained in:
Jingzhe Xu
2026-07-30 15:59:58 +08:00
committed by GitHub
parent 3bd0a7208a
commit 081fe2d172
34 changed files with 2093 additions and 763 deletions
+63 -42
View File
@@ -1,47 +1,32 @@
/**
* Unit tests for benchmark-metrics.ts: metric switching on the evaluation-center
* chart (score / cost / duration) — value extraction (missing -> null), gap
* segmentation (skipped points: connect within a segment, break between
* segments, a lone point still forms its own segment), and the y-axis max.
* Unit tests for the Evaluation center's Score-only chart helpers: Score extraction,
* gap segmentation across runtime series, y-axis max, and runtime grouping.
*/
import { describe, expect, it } from "vitest";
import {
lineSegments,
metricMax,
metricValues,
modelSeries,
scoreValues,
seriesValues,
} from "../src/features/benchmark/benchmark-metrics";
const evaluations = [
{ score: 6, cost: 0.12, durationMs: 90_000 },
{ score: 7.5 }, // legacy record: no cost / durationMs
{ score: 8.5, cost: 0.2, durationMs: 60_000 },
];
const evaluations = [{ score: 60 }, { score: 75.25 }, { score: 85.5 }];
describe("metricValues (value extraction by metric, missing → null)", () => {
it("score is always present", () => {
expect(metricValues(evaluations, "score")).toEqual([6, 7.5, 8.5]);
});
it("missing cost / duration yields null (skipped point)", () => {
expect(metricValues(evaluations, "cost")).toEqual([0.12, null, 0.2]);
expect(metricValues(evaluations, "duration")).toEqual([90_000, null, 60_000]);
});
it("non-finite values (NaN / Infinity) are treated as missing", () => {
expect(metricValues([{ score: 1, cost: Number.NaN }], "cost")).toEqual([null]);
expect(metricValues([{ score: 1, durationMs: Infinity }], "duration")).toEqual([null]);
describe("scoreValues", () => {
it("extracts stored Scores and treats non-finite malformed input as a gap", () => {
expect(scoreValues(evaluations)).toEqual([60, 75.25, 85.5]);
expect(scoreValues([{ score: Number.NaN }, { score: Infinity }])).toEqual([null, null]);
});
});
describe("lineSegments (gap segmentation)", () => {
it("no gaps: one segment with everything (consecutive indexes)", () => {
expect(lineSegments([6, 7.5, 8.5])).toEqual([
expect(lineSegments([60, 75.25, 85.5])).toEqual([
[
{ index: 0, value: 6 },
{ index: 1, value: 7.5 },
{ index: 2, value: 8.5 },
{ index: 0, value: 60 },
{ index: 1, value: 75.25 },
{ index: 2, value: 85.5 },
],
]);
});
@@ -77,42 +62,78 @@ describe("metricMax (y-axis upper bound)", () => {
});
});
describe("modelSeries / seriesValues (curves split into series by model)", () => {
describe("modelSeries / seriesValues (curves split by model ID and thinking level)", () => {
const mixed = [
{ score: 6, provider: "deepseek", modelId: "deepseek-v4-flash" },
{ score: 7 }, // legacy record: no model tagged -> trailing gray series
{ score: 7.5, provider: "deepseek", modelId: "deepseek-v4-pro" },
{ score: 8.5, provider: "deepseek", modelId: "deepseek-v4-pro" },
{
score: 6,
provider: "deepseek",
modelId: "deepseek-v4-flash",
thinkingLevel: "medium",
},
{ score: 7 }, // Defensive untagged input -> trailing gray series.
{
score: 7.5,
provider: "deepseek",
modelId: "deepseek-v4-pro",
thinkingLevel: "xhigh",
},
{
score: 8.5,
provider: "deepseek",
modelId: "deepseek-v4-pro",
thinkingLevel: "xhigh",
},
];
it("groups by (provider, modelId) in first-appearance order; untagged records go to a trailing unnamed series", () => {
it("groups by (modelId, thinkingLevel) in first-appearance order; untagged records go to a trailing unnamed series", () => {
const series = modelSeries(mixed);
expect(series.map((s) => s.modelId)).toEqual([
"deepseek-v4-flash",
"deepseek-v4-pro",
undefined,
]);
expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh", undefined]);
expect(series.map((s) => s.indices)).toEqual([[0], [2, 3], [1]]);
expect(series[2]!.key).toBe("");
});
it("the same modelId across providers forms separate series (paired grouping, no concatenation semantics)", () => {
const dup = [
{ score: 1, provider: "moonshot", modelId: "kimi-k2.6" },
{ score: 2, provider: "siliconflow", modelId: "kimi-k2.6" },
it("the same model ID and thinking level across providers stays in one series", () => {
const sameRuntime = [
{
score: 1,
provider: "moonshot",
modelId: "kimi-k2.6",
thinkingLevel: "medium",
},
{
score: 2,
provider: "siliconflow",
modelId: "kimi-k2.6",
thinkingLevel: "medium",
},
];
const series = modelSeries(dup);
const series = modelSeries(sameRuntime);
expect(series).toHaveLength(1);
expect(series[0]!.indices).toEqual([0, 1]);
});
it("the same model ID at different thinking levels forms separate series", () => {
const levels = [
{ score: 1, modelId: "deepseek-v4-pro", thinkingLevel: "medium" },
{ score: 2, modelId: "deepseek-v4-pro", thinkingLevel: "xhigh" },
];
const series = modelSeries(levels);
expect(series).toHaveLength(2);
expect(series.map((s) => s.provider)).toEqual(["moonshot", "siliconflow"]);
expect(series.map((s) => s.thinkingLevel)).toEqual(["medium", "xhigh"]);
});
it("seriesValues: indexes outside the series are null (skipped points), keeping the global time axis", () => {
const series = modelSeries(mixed);
expect(seriesValues(mixed, series[1]!, "score")).toEqual([null, null, 7.5, 8.5]);
expect(seriesValues(mixed, series[2]!, "score")).toEqual([null, 7, null, null]);
expect(seriesValues(mixed, series[1]!)).toEqual([null, null, 7.5, 8.5]);
expect(seriesValues(mixed, series[2]!)).toEqual([null, 7, null, null]);
});
it("all untagged: just one unnamed series (legacy data still draws as a single series)", () => {
it("all untagged defensive input forms one unnamed series", () => {
const series = modelSeries([{}, {}]);
expect(series).toHaveLength(1);
expect(series[0]!.key).toBe("");
+105
View File
@@ -0,0 +1,105 @@
import { describe, expect, it } from "vitest";
import { EXAMPLE_TASKS } from "../src/features/chat/example-tasks";
import { buildSkillsMessage } from "../src/features/chat/skill-use";
import { en } from "../src/lib/strings-en";
import { zh } from "../src/lib/strings";
describe("draft example tasks", () => {
it.each(["agentBenchmarkBuild", "agentOptimization"] as const)(
"submits the %s prompt without an implicit Skill block",
(id) => {
const task = EXAMPLE_TASKS.find((candidate) => candidate.id === id);
expect(task).toBeDefined();
expect(task?.skills).toEqual([]);
expect(buildSkillsMessage([...(task?.skills ?? [])], zh.chat.exampleTasks[id].prompt)).toBe(
zh.chat.exampleTasks[id].prompt,
);
},
);
it.each([
{
locale: "zh",
buildPrompt: zh.chat.exampleTasks.agentBenchmarkBuild.prompt,
optimizationPrompt: zh.chat.exampleTasks.agentOptimization.prompt,
buildMarkers: [
"依次使用 `agent-creation` 和 `benchmark-design`",
"id:`finite_choice_agent`",
"installed_skills:`[]`",
"id:`contextual-choice-adaptation`",
"desired_baseline_score:`<75`",
"pilot_iteration_limit:`5`",
"足球投注决策",
"售后政策与工单事实",
"投资策略、历史市场与当前指标",
],
buildForbiddenMarkers: ["thinking_level", "provider", "model_id"],
optimizationMarkers: [
"使用 `agent-optimization`",
"test_agent_id:`finite_choice_agent`",
"benchmark_id:`contextual-choice-adaptation`",
"提高信息不完整、规则冲突和有限选项决策中的稳定性",
"desired_score:`>=95`",
"candidate_round_limit:`5`",
],
},
{
locale: "en",
buildPrompt: en.chat.exampleTasks.agentBenchmarkBuild.prompt,
optimizationPrompt: en.chat.exampleTasks.agentOptimization.prompt,
buildMarkers: [
"Use `agent-creation` followed by `benchmark-design`",
"id: `finite_choice_agent`",
"installed_skills: `[]`",
"id: `contextual-choice-adaptation`",
"desired_baseline_score: `<75`",
"pilot_iteration_limit: `5`",
"football betting decisions",
"policy and ticket facts",
"strategy, historical markets, and current indicators",
],
buildForbiddenMarkers: ["thinking_level", "provider", "model_id"],
optimizationMarkers: [
"Use `agent-optimization`",
"test_agent_id: `finite_choice_agent`",
"benchmark_id: `contextual-choice-adaptation`",
"improve stability under incomplete information, conflicting rules, and finite choices",
"desired_score: `>=95`",
"candidate_round_limit: `5`",
],
},
])(
"$locale preserves the two-session agent evolution contract",
({
buildPrompt,
optimizationPrompt,
buildMarkers,
buildForbiddenMarkers,
optimizationMarkers,
}) => {
const normalizedBuild = buildPrompt.replace(/\s+/g, " ");
const normalizedOptimization = optimizationPrompt.replace(/\s+/g, " ");
for (const marker of buildMarkers) {
expect(normalizedBuild).toContain(marker);
}
for (const marker of buildForbiddenMarkers) {
expect(normalizedBuild).not.toContain(marker);
}
for (const marker of optimizationMarkers) {
expect(normalizedOptimization).toContain(marker);
}
expect(normalizedBuild).not.toContain("penguin run");
expect(normalizedOptimization).not.toContain("penguin run");
expect(normalizedBuild).not.toContain("run_subagent");
expect(normalizedOptimization).not.toContain("run_subagent");
expect(normalizedBuild).not.toContain("Scoreboard");
expect(normalizedOptimization).not.toContain("Scoreboard");
expect(normalizedBuild.length).toBeLessThan(1600);
expect(normalizedOptimization.length).toBeLessThan(600);
expect(normalizedBuild).not.toContain("Phase 1");
expect(normalizedOptimization).not.toContain("Phase 3");
},
);
});
+9
View File
@@ -12,12 +12,21 @@ import {
formatPercent,
formatRelativeDate,
formatRelativeDays,
formatScore,
formatTps,
humanizeDuration,
humanizeTokens,
signedDelta,
} from "../src/lib/format";
describe("formatScore", () => {
it("keeps up to the stored two decimal places", () => {
expect(formatScore(72)).toBe("72");
expect(formatScore(72.5)).toBe("72.5");
expect(formatScore(72.35)).toBe("72.35");
});
});
describe("humanizeTokens", () => {
it("below 1000 unchanged", () => {
expect(humanizeTokens(0)).toBe("0");