diff --git a/apps/server/src/modules/agent/challenge.ts b/apps/server/src/modules/agent/challenge.ts index 595850a8..48a23081 100644 --- a/apps/server/src/modules/agent/challenge.ts +++ b/apps/server/src/modules/agent/challenge.ts @@ -292,6 +292,7 @@ async function runChild( const systemPrompt = buildSystemPrompt({ role: "网页挑战处理员", goal, + audience: "agent", constraints: [ `只能用这些工具:${VISION_CAPABILITIES.join(", ")};每个工具都要传 sessionId = ${input.sessionId}。`, "先 browser.screenshot 看页面;坐标一律以最近一张截图的像素为准,点击 / 输入 / 滚动之后会返回新截图,按新截图继续。", diff --git a/apps/server/src/modules/agent/runner.ts b/apps/server/src/modules/agent/runner.ts index 19ee3d1a..d0ab3663 100644 --- a/apps/server/src/modules/agent/runner.ts +++ b/apps/server/src/modules/agent/runner.ts @@ -291,6 +291,8 @@ function systemPromptFor( role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化智能体", goal: run.goal, constraints, + // 子会话汇报给父会话汇总(可带对象 ID);其余会话按「汇报格式」写给用户 + audience: run.parent_run_id ? "agent" : "user", // 自动化优先:用户会话默认执行,先出步骤清单;有取舍时给方案;"先看方案再执行"是会话选项 ...(canAskUser(run) ? { diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts index 2304d95a..db224065 100644 --- a/packages/agent/src/index.ts +++ b/packages/agent/src/index.ts @@ -62,4 +62,4 @@ export { resolveAgentSettings, THINKING_LEVELS, } from "./settings.ts"; -export { buildSystemPrompt, type SystemPromptInput } from "./system-prompt.ts"; +export { buildSystemPrompt, interactionRules, REPORT_FORMAT, type SystemPromptInput } from "./system-prompt.ts"; diff --git a/packages/agent/src/system-prompt.ts b/packages/agent/src/system-prompt.ts index 3be20ae9..9ea09039 100644 --- a/packages/agent/src/system-prompt.ts +++ b/packages/agent/src/system-prompt.ts @@ -16,6 +16,11 @@ export interface SystemPromptInput { /** 需审批的能力在步骤处出审批卡(控制台管理员会话) */ approvals?: boolean; }; + /** + * 汇报给谁看:user(缺省)按「汇报格式」写给用户;agent = 子会话 / 视觉子流程,汇报给父会话汇总, + * 可以带对象 ID,不加用户汇报格式。 + */ + audience?: "user" | "agent"; } /** "工作方式":自动化优先——先出步骤清单再执行,执行中更新;有取舍时给方案;需授权的动作等审批 */ @@ -23,11 +28,15 @@ export function interactionRules(i: NonNullable `${i + 1}. ${r}`), ]; + if (audience === "user") lines.push("", "## 汇报格式", ...REPORT_FORMAT.map((r) => `- ${r}`)); const ways = interaction ? interactionRules(interaction) : []; if (ways.length > 0) lines.push("", "## 工作方式", ...ways.map((r) => `- ${r}`)); if (constraints.length > 0) { diff --git a/packages/agent/test/redact.test.ts b/packages/agent/test/redact.test.ts index 4756c547..202dace3 100644 --- a/packages/agent/test/redact.test.ts +++ b/packages/agent/test/redact.test.ts @@ -77,7 +77,7 @@ describe("buildSystemPrompt", () => { expect(p).toContain("download.inspect"); expect(p).toContain("思考、步骤说明和汇报都用中文"); expect(p).toContain("【预算提醒】"); - expect(p).toContain("不要写 HTML"); + expect(p).toContain("不写 HTML"); expect(p).toContain("- 只给方案,不执行"); }); }); diff --git a/packages/agent/test/system-prompt.test.ts b/packages/agent/test/system-prompt.test.ts new file mode 100644 index 00000000..1e9fce11 --- /dev/null +++ b/packages/agent/test/system-prompt.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { buildSystemPrompt, interactionRules, REPORT_FORMAT } from "../src/index.ts"; + +// AGUI-3 prompt 回归:用户在会话里看到的汇报就是模型写的文字,格式约束靠这里守住。 +// 改提示词时先想清楚是否放宽了这些约束,再同步改断言。 + +const userPrompt = () => + buildSystemPrompt({ + role: "媒体库自动化智能体", + goal: "看看有哪些新 4K", + interaction: { plan: true, options: true, approvals: true }, + }); + +/** 取提示词里的一节(## 标题到下一个 ## 之前) */ +function section(p: string, title: string): string { + const start = p.indexOf(`## ${title}`); + if (start < 0) return ""; + const rest = p.slice(start); + const next = rest.indexOf("\n## ", 3); + return next < 0 ? rest : rest.slice(0, next); +} + +describe("汇报格式(AGUI-3)", () => { + it("关键约束句:结论先行、用名字不用函数名 / 字段名 / UUID、不超过 10 行、多个资源才用表格", () => { + const f = section(userPrompt(), "汇报格式"); + expect(f).toMatchInlineSnapshot(` + "## 汇报格式 + - 第一句写结论:做成了什么、没做成什么、为什么。 + - 对象用名字:作品名、站点名、资源标题、文件名;不写函数名、工具名、字段名、参数、UUID 或 hash。 + - 不超过 10 行;不复述过程,不写下一步打算,除非需要用户做什么。 + - 只有列举多个资源(3 个以上的资源、任务或文件)时才用表格,其余用短句或列表。 + - 可以用 Markdown 列表、表格和粗体,不写标题,不写 HTML。 + " + `); + }); + + it("汇报格式与已有硬规则不冲突:证据写名称和数值,不再要求引用对象 ID", () => { + const p = userPrompt(); + expect(p).not.toContain("对象 ID"); + expect(p).toContain("写出查到的对象名称和关键数值"); + // 旧的"最后用简短中文汇报…可以用 Markdown(标题…)"已换成汇报格式一节 + expect(p).not.toContain("最后用简短中文汇报"); + expect(REPORT_FORMAT.length).toBe(5); + }); + + it("子会话 / 视觉子流程汇报给父会话:不加用户汇报格式", () => { + const p = buildSystemPrompt({ role: "网页挑战处理员", goal: "过验证", audience: "agent" }); + expect(p).not.toContain("## 汇报格式"); + }); +}); + +describe("工作方式(AGUI-3)", () => { + it("只在真有分歧时给方案;查询、盘点不给方案", () => { + const rules = interactionRules({ options: true }).join("\n"); + expect(rules).toContain("只有真的存在分歧时"); + expect(rules).toContain("只是查询、盘点或结论明确时不要给方案"); + }); + + it("选定即批准:方案绑定计划,approvedAction 直接执行;删除、覆盖类仍单独确认;只在能审批的会话里说", () => { + const rules = interactionRules({ options: true, approvals: true }).join("\n"); + expect(rules).toContain("把计划 ID 填进这个方案的 action"); + expect(rules).toContain("用户选定即批准这份计划"); + expect(rules).toContain("删除、覆盖类动作选定后仍会单独确认"); + expect(interactionRules({ options: true }).join("\n")).not.toContain("approvedAction"); + }); + + it("步骤标题写要做的事,不写工具名", () => { + expect(interactionRules({ plan: true }).join("\n")).toContain("不写工具名"); + }); + + it("先看方案(/先给方案)只对本会话生效的约束句保留", () => { + expect(interactionRules({ options: true, proposeFirst: true }).join("\n")).toContain("本会话要求先看方案再执行"); + }); +});