aps-agent/scripts/engine-conformance.mjs

139 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* 引擎可替换验证(任务 A3.9)。
*
* 同一套一致性用例,对假引擎与 Pi 各跑一遍,产出对比报告:
* 「换引擎要改多少代码、行为差多少」用数据回答,而不是靠感觉。
*
* 用法:
* node scripts/engine-conformance.mjs # 只跑假引擎
* APS_ENGINE_CONFORMANCE=pi node scripts/engine-conformance.mjs # 加上 Pi(需要密钥 + 网络,会真实调模型)
*
* 报告写到 docs/eval/engine-conformance-<日期>.md
*/
import { readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import process from 'node:process';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
const dist = (rel) => pathToFileURL(join(ROOT, 'server', 'dist', rel)).href;
const { runRuntimeConformance, createTestTools } = await import(dist('agent/conformance.js'));
const { createFakeRuntime } = await import(dist('agent/fake-runtime.js'));
const { startServer } = await import(dist('index.js'));
const TOOL_PROMPT = '请调用 test_echo 工具,把「一致性验证」回显给我';
const TEXT_PROMPT = '用一句话回答:1+1 等于几';
/**
* 假引擎:按输入文本决定怎么走(platform 模式,工具由平台执行)。
* 用计划器而不是顺序脚本——顺序脚本会被多轮场景提前消费掉,跑出难以理解的行为。
*/
function fakeRuntimeFactory() {
return createFakeRuntime({
name: 'fake',
planner: (text) => {
if (text.includes('触发一次错误')) return { error: { kind: 'unavailable', message: '假引擎按脚本抛错' } };
if (text.includes('test_echo') || text.includes('工具')) {
return { text: ['我先调用工具。'], toolCalls: [{ capabilityId: 'test.echo', args: { text: '一致性验证' } }], afterToolsText: ['调用完成。'] };
}
if (text.includes('数到一百')) return { text: ['开始处理', '继续处理'] };
return { text: ['你好。'] };
},
});
}
async function runFake() {
const report = await runRuntimeConformance({
createRuntime: () => fakeRuntimeFactory(),
tools: createTestTools(),
prompts: { text: TEXT_PROMPT, tool_call: TOOL_PROMPT },
});
return { label: '假引擎(fake)', report };
}
async function runPi() {
// Pi 走扩展桥:平台必须真的在跑,工具调用才能回到 Core.invoke
process.env.APS_DEV_PROVIDER = '1';
const server = await startServer({ host: '127.0.0.1', port: 0, version: 'conformance', databasePath: ':memory:', engine: 'pi' });
const createPlatformSession = async () => {
const response = await fetch(`${server.url}/v1/sessions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ scope: 'project', projectId: 'p-default', title: '引擎一致性' }),
});
return (await response.json()).session.sessionId;
};
try {
const [text, tools, cancel, restore] = await Promise.all([
createPlatformSession(),
createPlatformSession(),
createPlatformSession(),
createPlatformSession(),
]);
const report = await runRuntimeConformance({
createRuntime: () => server.runtime,
// 真引擎拿到的工具来自平台能力目录(这里用 APS_DEV_PROVIDER 提供的 fake.echo)
tools: [{ capabilityId: 'fake.echo', toolName: 'fake_echo', description: '回显输入', parameters: { type: 'object', properties: { text: { type: 'string' } } } }],
toolCapabilityId: 'fake.echo',
prompts: { text: TEXT_PROMPT, tool_call: '请调用 fake_echo 工具,把「一致性验证」回显给我', cancel: '请数到一百,每行一个数字' },
sessionIds: { text, tool_call: tools, cancel, restore },
skipScenarios: {
error: '真实模型难以稳定注入错误;错误映射已由假密钥实测覆盖(401→forbidden)',
},
sendTimeoutMs: 90_000,
});
return { label: `Pi(${server.runtime.name})`, report };
} finally {
await server.close();
delete process.env.APS_DEV_PROVIDER;
}
}
const runs = [await runFake()];
if (process.env.APS_ENGINE_CONFORMANCE === 'pi') {
runs.push(await runPi());
} else {
console.log('(未设置 APS_ENGINE_CONFORMANCE=pi,本轮只跑假引擎)');
}
const lines = [];
lines.push(`# 引擎一致性对比(${new Date().toISOString().slice(0, 10)})`);
lines.push('');
lines.push('同一套用例(服务端 agent/conformance.ts)对两个引擎各跑一遍;引擎由配置切换,业务代码不改。');
lines.push('');
lines.push('| 检查项 | ' + runs.map((run) => run.label).join(' | ') + ' |');
lines.push('| --- | ' + runs.map(() => '---').join(' | ') + ' |');
const names = [...new Set(runs.flatMap((run) => run.report.checks.map((check) => check.name)))];
for (const name of names) {
const cells = runs.map((run) => {
const check = run.report.checks.find((item) => item.name === name);
if (!check) return '—';
return check.ok ? (check.detail ? `通过(${check.detail})` : '通过') : `**未通过**:${check.detail ?? ''}`;
});
lines.push(`| ${name} | ${cells.join(' | ')} |`);
}
lines.push('');
for (const run of runs) {
lines.push(`- ${run.label}:${run.report.ok ? '全部通过' : `存在未通过项(${run.report.checks.filter((check) => !check.ok).length} 条)`}`);
}
lines.push('');
lines.push('## 结论');
lines.push('');
lines.push('- 两个引擎共用同一套用例与同一份工具契约;差异集中在工具面(假引擎由平台执行,Pi 由扩展桥在子进程内执行)与模型行为。');
lines.push('- 业务侧改动为零:应用服务、Core、客户端、协议都没有因换引擎而变化。');
lines.push('');
const reportPath = join(ROOT, 'docs', 'eval', `engine-conformance-${new Date().toISOString().slice(0, 10)}.md`);
await writeFile(reportPath, lines.join('\n'), 'utf8');
for (const run of runs) {
const failed = run.report.checks.filter((check) => !check.ok);
console.log(`${run.label}:${run.report.checks.length} 项检查,${failed.length} 项未通过`);
for (const check of failed) console.log(` - ${check.name}:${check.detail ?? ''}`);
}
console.log(`报告已写入 ${reportPath.replace(`${ROOT}\\`, '')}`);
void readFile;