Archived
工具总线装配面测试:三路各穿各的缝
守开环 Agent 最容易出事的三条: - 写类工具**过程中不落地**(只登记意向,并如实告诉 Agent 世界还没变)—— 骗它说已执行,它会据此往下推而世界一步没动 - 调人 = 发工单 + 立监听,卡号记进 trace(引擎据此挂起,不重复发第二张) - LLM 五件经模型网关、用活动声明的用途名;提示词归系统 六例全绿;顺带钉住 Mastra 的 tool id 规矩(点号 → 双下划线)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 工具总线的**装配面** —— 三路各穿各的缝,一道都不许绕。
|
||||
*
|
||||
* 这里守的是开环 Agent 最容易出事的三条:
|
||||
* ① 写类工具**过程中不落地**(只登记意向)—— 骗 Agent 说已执行,它会据此往下推,
|
||||
* 而世界上什么都没发生;
|
||||
* ② 调人把卡号记进 trace —— 引擎据此把这一拍挂起,不重复发第二张卡;
|
||||
* ③ LLM 工具经模型网关(不是自己直连),用途名照活动声明走。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { StaffId, TaskId } from '@contracts';
|
||||
import { HUMAN_TOOL, LLM_TOOLS } from '@modules/toolbox';
|
||||
import type { BusTool } from '@modules/toolbox';
|
||||
import { buildMastraTools } from '../tool-bus.ts';
|
||||
import type { ToolBusPorts, ToolBusTrace } from '../tool-bus.ts';
|
||||
|
||||
const TASK = 't-1' as TaskId;
|
||||
const OWNER = 's-1' as StaffId;
|
||||
|
||||
const READ_TOOL: BusTool = {
|
||||
id: 'crm.history-price-read',
|
||||
description: '读历史价',
|
||||
input: { type: 'object', required: ['pn'], properties: { pn: { type: 'string' } } },
|
||||
kind: 'function',
|
||||
effect: 'read',
|
||||
};
|
||||
const WRITE_TOOL: BusTool = {
|
||||
id: 'mail.send',
|
||||
description: '发邮件',
|
||||
input: { type: 'object', required: ['to'], properties: { to: { type: 'string' } } },
|
||||
kind: 'function',
|
||||
effect: 'write',
|
||||
};
|
||||
|
||||
type Calls = {
|
||||
invoked: Array<{ tool: string; input: Record<string, unknown> }>;
|
||||
intents: Array<{ tool: string; input: Record<string, unknown> }>;
|
||||
cards: Array<{ workOrder: string; card: Record<string, unknown> }>;
|
||||
listens: Array<{ keyFilter: Record<string, string> }>;
|
||||
generated: Array<{ model: string; label: string; prompt: string }>;
|
||||
};
|
||||
|
||||
function harness(writeTools: string[] = ['mail.send']) {
|
||||
const calls: Calls = { invoked: [], intents: [], cards: [], listens: [], generated: [] };
|
||||
const ports: ToolBusPorts = {
|
||||
invoke: async (input) => {
|
||||
calls.invoked.push({ tool: input.tool, input: input.input });
|
||||
return { ok: true, output: { historyDays: 12 } };
|
||||
},
|
||||
isWriteEffect: async (tool) => writeTools.includes(tool),
|
||||
registerIntent: async (input) => {
|
||||
calls.intents.push({ tool: input.tool, input: input.input });
|
||||
},
|
||||
issueCard: async (input) => {
|
||||
calls.cards.push({ workOrder: input.workOrder, card: input.card });
|
||||
return { id: `card-${calls.cards.length}` };
|
||||
},
|
||||
listen: async (input) => {
|
||||
calls.listens.push({ keyFilter: input.keyFilter });
|
||||
return { id: 'lsn-1' };
|
||||
},
|
||||
generate: async (input) => {
|
||||
calls.generated.push({ model: input.model, label: input.label, prompt: input.prompt });
|
||||
return { text: '{"label":"C"}', object: { label: 'C', rationale: '据规则' } };
|
||||
},
|
||||
};
|
||||
const trace: ToolBusTrace = {};
|
||||
return { calls, ports, trace };
|
||||
}
|
||||
|
||||
/** Mastra tool 的 execute 走真实签名:(inputData, context) */
|
||||
const run = async (tool: unknown, args: Record<string, unknown>) =>
|
||||
(await (tool as { execute: (a: unknown, b: unknown) => Promise<unknown> }).execute(args, {})) as Record<string, unknown>;
|
||||
|
||||
describe('装配:功能工具', () => {
|
||||
it('读类当场调,出参原样带回', async () => {
|
||||
const h = harness();
|
||||
const tools = buildMastraTools([READ_TOOL], h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const out = await run(tools['crm__history_price_read'] ?? Object.values(tools)[0], { pn: 'PN-1' });
|
||||
expect(h.calls.invoked).toEqual([{ tool: 'crm.history-price-read', input: { pn: 'PN-1' } }]);
|
||||
expect(out).toMatchObject({ ok: true, historyDays: 12 });
|
||||
expect(h.calls.intents).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('写类**不当场落地**:只登记动作意向,并如实告诉 Agent 世界还没变', async () => {
|
||||
const h = harness();
|
||||
const tools = buildMastraTools([WRITE_TOOL], h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const out = await run(Object.values(tools)[0], { to: 'a@b.c' });
|
||||
expect(h.calls.intents).toEqual([{ tool: 'mail.send', input: { to: 'a@b.c' } }]);
|
||||
expect(h.calls.invoked).toHaveLength(0);
|
||||
expect(out['registered']).toBe(true);
|
||||
expect(String(out['note'])).toContain('放行时才真正执行');
|
||||
});
|
||||
|
||||
it('工具调用失败如实回报,不抛掉整拍', async () => {
|
||||
const h = harness();
|
||||
h.ports.invoke = async () => ({ ok: false, error: { message: '端点不可达' } });
|
||||
const tools = buildMastraTools([READ_TOOL], h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const out = await run(Object.values(tools)[0], { pn: 'PN-1' });
|
||||
expect(out).toEqual({ ok: false, error: '端点不可达' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('装配:人类工具(系统内化)', () => {
|
||||
it('调人 = 发工单 + 立监听,卡号记进 trace 供引擎收尾挂起', async () => {
|
||||
const h = harness();
|
||||
const tools = buildMastraTools([HUMAN_TOOL], h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
const out = await run(Object.values(tools)[0], { title: '请确认底价', ask: '这一单要不要走寻源?' });
|
||||
|
||||
expect(h.calls.cards).toEqual([
|
||||
{ workOrder: 'execution', card: { title: '请确认底价', ask: '这一单要不要走寻源?' } },
|
||||
]);
|
||||
expect(h.calls.listens).toEqual([{ keyFilter: { cardId: 'card-1' } }]);
|
||||
expect(h.trace.askedCardId).toBe('card-1');
|
||||
expect(out).toEqual({ asked: true, cardId: 'card-1' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('装配:LLM 五件(系统内化)', () => {
|
||||
it('经模型网关调,用活动声明的用途名;提示词归系统', async () => {
|
||||
const h = harness();
|
||||
const tools = buildMastraTools(LLM_TOOLS, h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'quote-extractor' }, h.trace);
|
||||
const classify = tools['llm__classify'];
|
||||
expect(classify).toBeDefined();
|
||||
|
||||
const out = await run(classify, { text: '这是一颗停产料', labels: ['A', 'B', 'C'] });
|
||||
expect(h.calls.generated).toHaveLength(1);
|
||||
expect(h.calls.generated[0]!.model).toBe('quote-extractor');
|
||||
expect(h.calls.generated[0]!.label).toBe('llm.classify');
|
||||
expect(h.calls.generated[0]!.prompt).toContain('这是一颗停产料');
|
||||
expect(out).toEqual({ label: 'C', rationale: '据规则' });
|
||||
});
|
||||
|
||||
it('五件都装得出来,id 里的点号换成双下划线(Mastra 的 id 规矩)', () => {
|
||||
const h = harness();
|
||||
const tools = buildMastraTools(LLM_TOOLS, h.ports, { taskId: TASK, ownerId: OWNER, humanCallCap: 9, model: 'm' }, h.trace);
|
||||
expect(Object.keys(tools).sort()).toEqual(
|
||||
['llm__classify', 'llm__extract', 'llm__judge', 'llm__rewrite', 'llm__summarize'].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user