feat(loop-engine): analyze 动词 —— 系统组装入参、声明出参 schema、系统把结果落表

AI 在这里只是一个纯函数:不给工具、不给记忆、不多轮。
行数不由它决定 —— 出参是拿主键去 lookup 的一张查表,它漏一条那一格就是空,
required 立刻把它变成 _missing > 0,提交闸与放行门当场拦住。
出参不成形就地重问一次,再不成抛出去交人。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
xcs
2026-08-29 03:54:14 +08:00
co-authored by Claude Opus 5
parent b0a3cc3a55
commit fe8de5319c
7 changed files with 228 additions and 4 deletions
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import { taskIdSchema } from '@contracts';
import { createAnalyzeGateway } from '../wiring/analyze-gateway.ts';
const TASK = taskIdSchema.parse('11111111-1111-4111-8111-111111111111');
const SCHEMA = { type: 'object', required: ['items'], properties: { items: { type: 'array' } } };
const call = { taskId: TASK, model: 'deepseek-v4-flash', instruction: '逐条判四态', payload: [{ p: 'A' }], outputSchema: SCHEMA, label: 'S2#1' };
describe('analyze 网关', () => {
it('出参是对象就直接回,入参 JSON 进 prompt、instruction 进 system', async () => {
const seen: Array<Record<string, unknown>> = [];
const gateway = createAnalyzeGateway({
generate: (c: Record<string, unknown>) => {
seen.push(c);
return Promise.resolve({ text: '', object: { items: [{ p: 'A' }] }, usage: { inputTokens: 1, outputTokens: 1, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' });
},
} as never);
const out = await gateway.run(call);
expect(out.object).toEqual({ items: [{ p: 'A' }] });
expect(seen[0]?.['system']).toBe('逐条判四态');
expect(String(seen[0]?.['prompt'])).toContain('"p": "A"');
expect(seen[0]?.['reasoning']).toBe('off');
});
it('出参不成形 → **就地重问一次**,第二次成了就用它', async () => {
let n = 0;
const gateway = createAnalyzeGateway({
generate: () => {
n += 1;
return Promise.resolve(
n === 1
? { text: 'not json', usage: { inputTokens: 1, outputTokens: 1, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' }
: { text: '', object: { items: [] }, usage: { inputTokens: 1, outputTokens: 1, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' },
);
},
} as never);
const out = await gateway.run(call);
expect(n).toBe(2);
expect(out.object).toEqual({ items: [] });
});
it('重问了还是不成形 → 抛,交人;**不重问第三次**', async () => {
let n = 0;
const gateway = createAnalyzeGateway({
generate: () => {
n += 1;
return Promise.resolve({ text: 'nope', usage: { inputTokens: 1, outputTokens: 1, latencyMs: 1 }, binding: { name: 'x', provider: 'deepseek', model: 'y' }, mode: 'live' });
},
} as never);
await expect(gateway.run(call)).rejects.toThrow(/出参不成形/);
expect(n).toBe(2);
});
});