GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-15-prompt-builder-design.md
2026-05-20 21:39:12 +08:00

19 KiB
Raw Permalink Blame History

Prompt Builder 分层注入系统设计

背景

当前 Neta Agent 的系统提示词组装非常简单gateway/server.ts 中 4 层拼接),导致:

  1. 模型使用工具时频繁犯基础错误(不调工具只描述、参数格式错误、跳过 read_skill
  2. 不同模型的工具调用能力差异大,但系统提示词完全相同
  3. 用户手写的 systemPrompt 缺乏工具使用指导,质量参差不齐

参考 Hermes Agent 的 prompt_builder.py989 行10 层分层架构),在保持 Neta 渐进披露架构不变的前提下,新建 prompt_builder.ts 模块实现分层注入。

核心原则

  • 渐进披露不变Skill 仍然只在索引中展示 name + description + category + tags模型需要调 read_skill 加载完整指令
  • 运行时注入:用户的 systemPrompt 保持干净存数据库,增强部分在运行时动态拼接
  • 按模型名称匹配:不依赖 supplier 字段,用模型名子串匹配决定注入哪套指导(解决火山引擎跑多种模型的问题)
  • 条件注入:只有当工具实际可用时才注入对应的行为策略

分层结构

Layer 1: Agent 身份(用户写的 systemPrompt原样保留
Layer 2: 工具使用纪律(通用,所有有工具的 Agent 都注入)
Layer 3: 模型特定指导(按模型名称子串匹配)
Layer 4: 工具行为策略(按可用工具条件注入)
Layer 5: Skill 索引(增强版:分类 + 标签 + 强制措辞)
Layer 6: 记忆系统MEMORY_SYSTEM_PROMPT + memory-context
Layer 7: Crew 编排上下文(仅 Crew 场景:团队成员、委派提示、工具说明)
Layer 8: 元信息(时间戳、模型身份)

Layer 7 仅在 Crew 场景下注入(通过 crewContext 参数传入)。普通对话和 REST API 调用时此层为空。

关键文件变更

新建文件

文件 职责
packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts 系统提示词分层组装引擎
packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts 所有指导文本常量(工具纪律、模型指导、工具策略)

修改文件

文件 变更
packages/backend/src/modules/netaclaw/gateway/server.ts handleChat() 中替换手动拼接为调用 prompt_builder
packages/backend/src/modules/netaclaw/service/agent_executor.ts execute() 中同样替换
packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts buildEnhancedPrompt() 改为调用 builder + 追加 Crew 专属层
packages/backend/src/modules/netaclaw/service/crew_delegate.ts buildSubAgentPrompt() 改为调用 builder
packages/backend/src/modules/netaclaw/service/skill_loader.ts buildSkillsPrompt() 增强索引格式
packages/backend/src/modules/netaclaw/runtime/agent.ts runAgent() 简化,移除 memoryContext 拼接(已由 builder 处理)
packages/backend/src/modules/netaclaw/controller/agent.ts 新增 previewPrompt 接口,注入 skillLoader + channelService
packages/frontend/src/modules/agent/views/agent-edit.vue 新增折叠面板预览最终提示词

详细设计

0. availableToolNames 构建辅助函数

todo 工具在 runAgent() 中动态创建,不在 defaultTools 里,但它始终可用。为避免各调用点硬编码不完整的工具列表,提供辅助函数:

// prompt_builder.ts

/** 基础内置工具(始终可用) */
const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo'];

/** 构建完整的可用工具名列表 */
export function collectAvailableToolNames(opts: {
  memoryEnabled?: boolean;
  hasSkills?: boolean;
  crewRole?: 'master' | 'sub';  // Crew 场景
}): string[] {
  const names = [...BASE_TOOLS];
  if (opts.memoryEnabled) names.push('memory_save', 'memory_recall');
  if (opts.hasSkills) names.push('read_skill', 'read_skill_file', 'skill_manage');
  if (opts.crewRole === 'master') names.push('delegate_task', 'delegate_parallel', 'escalate');
  return names;
}

1. prompt_builder.ts — 核心接口

interface BuildSystemPromptParams {
  agentSystemPrompt: string;       // 用户写的系统提示词
  modelId: string;                 // 模型名称(如 'MiniMax-M2.7-highspeed'
  availableToolNames: string[];    // 当前可用工具名列表(用 collectAvailableToolNames 构建)
  skills: string[];                // 已选 skill 名称列表
  skillLoader: SkillLoaderService; // Skill 加载器实例
  memoryEnabled: boolean;
  memoryContext?: string;          // 召回的记忆内容
  crewContext?: string;            // Crew 编排专属上下文(团队成员、委派提示等)
}

interface BuildResult {
  prompt: string;                  // 拼装后的完整系统提示词
  layers: Array<{                  // 各层明细(供前端预览)
    name: string;
    content: string;
  }>;
}

function buildSystemPrompt(params: BuildSystemPromptParams): BuildResult;

调用位置(共 5 处,全部覆盖):

  • gateway/server.ts handleChat() — WebSocket 对话入口
  • service/agent_executor.ts execute() — REST API / 渠道触发入口
  • service/crew_orchestrator.ts buildEnhancedPrompt() — Crew 主 Agent 编排入口
  • service/crew_delegate.ts buildSubAgentPrompt() — Crew 子 Agent 委派入口
  • controller/agent.ts previewPrompt() — 前端预览接口(新增)

2. prompt_guidance.ts — 指导文本常量

Layer 2: 工具使用纪律(通用)

export const TOOL_USE_ENFORCEMENT = `# 工具使用规范
你必须通过工具采取行动 - 不要只描述你打算做什么。当你说"我来检查一下"、"让我执行"时,必须在同一回复中立即发起对应的工具调用。不要以"下一步我会..."结束回复 - 现在就执行。

持续工作直到任务真正完成。不要停在计划阶段。如果你有可用的工具能完成任务,使用它们,而不是告诉用户你会怎么做。

每条回复要么 (a) 包含推进任务的工具调用,要么 (b) 向用户交付最终结果。仅描述意图而不行动的回复是不可接受的。`;

Layer 3: 模型特定指导(按模型名匹配)

const MODEL_GUIDANCE_RULES: Array<{
  patterns: string[];
  guidance: string;
}> = [
  {
    patterns: ['claude'],
    guidance: '',  // Claude 工具调用能力强,不需要额外指导
  },
  {
    patterns: ['gpt', 'o1', 'o3', 'o4'],
    guidance: `# 执行纪律
<tool_persistence>
- 工具能提升正确性时必须使用,不要提前停止
- 工具返回空结果时换一种查询策略重试
- 持续调用工具直到:(1) 任务完成 且 (2) 已验证结果
</tool_persistence>
<mandatory_tool_use>
以下问题绝不能凭记忆回答,必须使用工具:
- 算术计算 → bash
- 文件内容、大小 → read_file / list_dir
- 当前时间日期 → bash
</mandatory_tool_use>`,
  },
  {
    patterns: ['deepseek'],
    guidance: `# 操作规范
- 支持并行工具调用:多个独立操作可在同一回复中发起
- 工具参数必须是合法 JSON注意字符串转义
- 不要在工具调用的同时输出大段解释,先执行再说明`,
  },
  {
    patterns: ['minimax', 'abab'],
    guidance: `# 操作规范
- 算术、日期、文件内容等事实性问题必须使用工具获取,不要凭记忆回答
- 调用工具时严格按照参数 schema 传参,不要添加 schema 中不存在的字段
- 一次只调用一个工具,等待结果后再决定下一步
- 如果工具返回错误,换一种方式重试,不要放弃`,
  },
  {
    patterns: ['doubao', 'skylark'],
    guidance: `# 操作规范
- 工具参数必须严格匹配 schema 定义的类型
- 使用 bash 工具时加 -y 等非交互标志,避免命令挂起
- 先用 read_file/list_dir 了解现状,再做修改操作`,
  },
  {
    patterns: ['qwen'],
    guidance: `# 操作规范
- 工具调用参数必须严格匹配 schema不要添加额外字段
- 使用 bash 工具时加 -y 等非交互标志,避免命令挂起
- 先用 read_file/list_dir 了解现状,再做修改操作`,
  },
  {
    patterns: ['glm', 'chatglm'],
    guidance: `# 操作规范
- 工具调用参数必须严格匹配 schema 定义的类型string/number/boolean
- 文件路径使用绝对路径
- 每次工具调用后检查返回结果,确认操作成功后再继续`,
  },
];

// 未匹配到的模型 → 通用保守版指导
export const DEFAULT_MODEL_GUIDANCE = `# 操作规范
- 事实性问题(算术、日期、文件内容)必须使用工具获取,不要凭记忆回答
- 调用工具时严格按照参数 schema 传参
- 一次只调用一个工具,等待结果后再决定下一步
- 如果工具返回错误,换一种方式重试`;

export function getModelGuidance(modelId: string): string {
  const lower = modelId.toLowerCase();
  for (const rule of MODEL_GUIDANCE_RULES) {
    if (rule.patterns.some(p => lower.includes(p))) {
      return rule.guidance;
    }
  }
  return DEFAULT_MODEL_GUIDANCE;
}

Layer 4: 工具行为策略(条件注入)

// 工具名 → 行为策略的映射
export const TOOL_BEHAVIOR_GUIDANCE: Record<string, string> = {
  memory_save: `## 记忆使用策略
使用 memory_save 存储对未来对话有价值的持久信息:用户偏好、环境细节、项目约束。
保持记忆紧凑,聚焦于减少用户未来重复纠正的事实。
不要存储任务进度、会话结果、临时 TODO 状态。更新已有记忆而非创建重复条目。`,

  todo: `## 任务规划策略
面对3步以上的复杂任务或用户提供多个任务时先用 todo 工具创建任务列表。
同一时间只标记一个任务为 in_progress。完成后立即标记 completed。`,

  skill_manage: `## 技能管理策略
完成复杂任务5步以上工具调用考虑用 skill_manage 将方法保存为技能以便复用。
使用技能时如发现过时或错误,立即修补,不要等用户要求。`,
};

export function getToolBehaviorGuidance(toolNames: string[]): string {
  const parts: string[] = [];
  for (const [toolName, guidance] of Object.entries(TOOL_BEHAVIOR_GUIDANCE)) {
    if (toolNames.includes(toolName)) {
      parts.push(guidance);
    }
  }
  return parts.join('\n\n');
}

3. skill_loader.ts — 索引格式增强

改造 buildSkillsPrompt() 方法:

// 之前
`<skill>\n  <name>${s.name}</name>\n  <description>${s.description}</description>\n</skill>`

// 之后
const category = s.metadata?.category || '通用';
const tags = (s.metadata?.tags || []).join(',');
`<skill name="${s.name}" category="${category}" tags="${tags}">\n  ${s.description}\n</skill>`

索引头部措辞从被动改为强制:

// 之前
`当你需要使用某个 skill 时,调用 read_skill 工具读取完整指令后再执行。`

// 之后
`## 技能(必须扫描)
回复前,扫描以下技能列表。如果某个技能明确匹配当前任务,用 read_skill 工具加载完整指令后严格遵循。`

4. 后端预览接口

// controller/admin/agent.ts
@Post('/previewPrompt', { summary: '预览最终系统提示词' })
async previewPrompt(@Body() body: {
  systemPrompt: string;
  skills: string[];
  modelConfig: { channelId?: number; modelId?: string };
}) {
  let modelId = body.modelConfig?.modelId || 'unknown';

  // 如果有 channelId解析渠道获取完整信息
  if (body.modelConfig?.channelId) {
    const resolved = await this.channelService.resolveForAgent(
      body.modelConfig.channelId, modelId
    );
    modelId = resolved.model;
  }

  const result = buildSystemPrompt({
    agentSystemPrompt: body.systemPrompt || '',
    modelId,
    availableToolNames: ['bash', 'read_file', 'write_file', 'list_dir', 'todo'],
    skills: body.skills || [],
    skillLoader: this.skillLoader,
    memoryEnabled: false,  // 预览时不含记忆上下文
  });

  return this.ok(result);  // { prompt, layers }
}

5. 前端预览组件

agent-edit.vue 的系统提示词 Tab 下方新增折叠面板:

<el-collapse v-if="form.modelConfig?.channelId">
  <el-collapse-item>
    <template #title>
      <span>预览最终提示词</span>
      <el-tag size="small" type="info" style="margin-left: 8px">
        运行时实际发送给模型的内容
      </el-tag>
    </template>
    <el-button size="small" @click="loadPreview" :loading="previewLoading">
      刷新预览
    </el-button>
    <div v-if="previewLayers.length" class="prompt-preview">
      <div v-for="layer in previewLayers" :key="layer.name" class="layer-block">
        <div class="layer-label">{{ layer.name }}</div>
        <pre class="layer-content">{{ layer.content }}</pre>
      </div>
    </div>
  </el-collapse-item>
</el-collapse>

调用逻辑:

const previewLoading = ref(false);
const previewLayers = ref<Array<{ name: string; content: string }>>([]);

async function loadPreview() {
  previewLoading.value = true;
  try {
    const res = await service.request({
      url: '/admin/netaclaw/agent/previewPrompt',
      method: 'POST',
      data: {
        systemPrompt: form.systemPrompt,
        skills: form.skills,
        modelConfig: form.modelConfig,
      },
    });
    previewLayers.value = res.layers || [];
  } finally {
    previewLoading.value = false;
  }
}

改造影响范围

MEMORY_SYSTEM_PROMPT 统一

当前 gateway/server.ts第 25-42 行)和 agent_executor.ts第 20-24 行)各有一份 MEMORY_SYSTEM_PROMPT内容不一致gateway 版本更详细)。改造后统一到 prompt_guidance.ts 中,两处都删除本地常量。

gateway/server.ts 变更

改造要点:

  1. 移除 MEMORY_SYSTEM_PROMPT 常量(第 25-42 行)
  2. buildSkillContext() 调用保留但只取 skillTools(不再取 skillPromptskill 索引由 builder 内部处理
  3. 记忆系统代码(第 192-210 行)需要提前到 buildSystemPrompt 调用之前,因为 builder 需要 memoryEnabledmemoryContext
  4. memoryConfig 直接从 cfgagentInfo.config)提取,不再重新查询 agentInfo2(消除冗余数据库查询)
  5. 传给 builder 的 modelId 必须是纯模型名(mc.modelId),不是 provider:model 格式
// 之前(分散在多处,第 167-177 行 + 第 192-210 行)
agentConfig = {
  systemPrompt: (agentInfo.systemPrompt || '') + skillPrompt,
  ...
};
// ... 中间隔了十几行 ...
const agentInfo2 = agentId ? await this.agentService.info(agentId) : null;
const memoryConfig = (agentInfo2?.config as any)?.memory;
if (memoryConfig?.enabled) {
  // ... prefetch ...
  agentConfig.systemPrompt += MEMORY_SYSTEM_PROMPT;
}

// 之后(统一到一处)
// 1. 记忆系统提前处理(从 cfg 直接取,不再重新查询)
const memoryConfig: AgentMemoryConfig | undefined = cfg?.memory;
let memoryContext: string | undefined;
let memoryTools: AnyAgentTool[] = [];
if (memoryConfig?.enabled) {
  const provider = createMemoryProvider(memoryConfig, this.memoryRepo);
  const userId = 'anonymous';
  memoryContext = await prefetchMemory(provider, content, agentConfig.name, userId, memoryConfig.prefetchLimit);
  memoryTools = [createMemorySaveTool(provider, agentConfig.name, userId), createMemoryRecallTool(provider, agentConfig.name, userId)];
}

// 2. 统一调用 builder
const toolNames = collectAvailableToolNames({
  memoryEnabled: !!memoryConfig?.enabled,
  hasSkills: !!agentInfo.skills?.length,
});
const { prompt: systemPrompt } = buildSystemPrompt({
  agentSystemPrompt: agentInfo.systemPrompt || '',
  modelId: mc.modelId || '',  // 纯模型名,不含 provider: 前缀
  availableToolNames: toolNames,
  skills: agentInfo.skills || [],
  skillLoader: this.skillLoader,
  memoryEnabled: !!memoryConfig?.enabled,
  memoryContext,
});
agentConfig = { systemPrompt, ... };

service/agent_executor.ts 变更

execute() 方法中同样替换手动拼接(第 74-137 行),改为调用 buildSystemPrompt()。移除该文件中的 MEMORY_SYSTEM_PROMPT 常量和 memoryContext 拼接逻辑。

service/crew_orchestrator.ts 变更

buildEnhancedPrompt() 方法(第 224-257 行)改造:

  • 先用 buildSystemPrompt() 构建基础 promptLayer 1-6, 8
  • 将现有的团队成员信息、委派提示、工具说明作为 crewContext 参数传入Layer 7
// 之前
private buildEnhancedPrompt(masterAgent, ctx, crew): string {
  const parts = [masterAgent.systemPrompt || ''];
  parts.push(`\n## 你的团队成员\n${memberList}...`);
  parts.push(`\n## 调度建议\n${crew.delegateHints?.hints}`);
  parts.push(`\n## 编排工具说明\n...`);
  return parts.join('\n');
}

// 之后
private buildEnhancedPrompt(masterAgent, ctx, crew): string {
  // 构建 Crew 专属上下文
  const crewParts = [];
  crewParts.push(`## 你的团队成员\n${memberList}...`);
  if (crew.delegateHints?.hints) crewParts.push(`## 调度建议\n${crew.delegateHints.hints}`);
  crewParts.push(`## 编排工具说明\n...`);

  const toolNames = collectAvailableToolNames({
    hasSkills: !!masterAgent.skills?.length,
    crewRole: 'master',
  });
  const { prompt } = buildSystemPrompt({
    agentSystemPrompt: masterAgent.systemPrompt || '',
    modelId: resolvedModelId,
    availableToolNames: toolNames,
    skills: masterAgent.skills || [],
    skillLoader: this.skillLoader,
    memoryEnabled: false,
    crewContext: crewParts.join('\n\n'),
  });
  return prompt;
}

service/crew_delegate.ts 变更

buildSubAgentPrompt() 方法(第 47-53 行)改造:

// 之前
private buildSubAgentPrompt(systemPrompt, role, agentSkills?): string {
  const skillCtx = buildSkillContext(this.skillLoader, agentSkills, BUILTIN_TOOL_NAMES);
  const parts = [systemPrompt];
  if (role) parts.push(`\n你在团队中的角色: ${role}`);
  if (skillCtx.skillPrompt) parts.push(`\n${skillCtx.skillPrompt}`);
  return parts.join('\n');
}

// 之后
private buildSubAgentPrompt(systemPrompt, role, agentSkills?, modelId?): string {
  const rolePrefix = role ? `${systemPrompt}\n\n你在团队中的角色: ${role}` : systemPrompt;
  const toolNames = collectAvailableToolNames({
    hasSkills: !!agentSkills?.length,
    crewRole: 'sub',
  });
  const { prompt } = buildSystemPrompt({
    agentSystemPrompt: rolePrefix,
    modelId: modelId || '',
    availableToolNames: toolNames,
    skills: agentSkills || [],
    skillLoader: this.skillLoader,
    memoryEnabled: false,
  });
  return prompt;
}

runtime/agent.ts 变更

第 64-66 行的 memoryContext 拼接移除(已由 builder 处理):

// 之前
const systemContent = memoryContext
  ? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
  : agentConfig.systemPrompt;

// 之后
const systemContent = agentConfig.systemPrompt;  // builder 已处理所有层

验证方案

  1. 单元测试:对 prompt_builder.ts 写测试,验证不同模型名、不同工具组合下的输出
  2. 预览接口测试:在前端编辑 Agent 时点击预览,确认各层内容正确
  3. 端到端测试:用 MiniMax-M2.7-highspeed 模型对话,观察工具调用行为是否改善
  4. 回归测试:用 Claude/GPT 模型对话确认不受负面影响Claude 的指导为空字符串)