# Prompt Builder 分层注入系统设计 ## 背景 当前 Neta Agent 的系统提示词组装非常简单(gateway/server.ts 中 4 层拼接),导致: 1. 模型使用工具时频繁犯基础错误(不调工具只描述、参数格式错误、跳过 read_skill) 2. 不同模型的工具调用能力差异大,但系统提示词完全相同 3. 用户手写的 systemPrompt 缺乏工具使用指导,质量参差不齐 参考 Hermes Agent 的 prompt_builder.py(989 行,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 里,但它始终可用。为避免各调用点硬编码不完整的工具列表,提供辅助函数: ```typescript // 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 — 核心接口 ```typescript 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: 工具使用纪律(通用) ```typescript export const TOOL_USE_ENFORCEMENT = `# 工具使用规范 你必须通过工具采取行动 - 不要只描述你打算做什么。当你说"我来检查一下"、"让我执行"时,必须在同一回复中立即发起对应的工具调用。不要以"下一步我会..."结束回复 - 现在就执行。 持续工作直到任务真正完成。不要停在计划阶段。如果你有可用的工具能完成任务,使用它们,而不是告诉用户你会怎么做。 每条回复要么 (a) 包含推进任务的工具调用,要么 (b) 向用户交付最终结果。仅描述意图而不行动的回复是不可接受的。`; ``` #### Layer 3: 模型特定指导(按模型名匹配) ```typescript const MODEL_GUIDANCE_RULES: Array<{ patterns: string[]; guidance: string; }> = [ { patterns: ['claude'], guidance: '', // Claude 工具调用能力强,不需要额外指导 }, { patterns: ['gpt', 'o1', 'o3', 'o4'], guidance: `# 执行纪律 - 工具能提升正确性时必须使用,不要提前停止 - 工具返回空结果时换一种查询策略重试 - 持续调用工具直到:(1) 任务完成 且 (2) 已验证结果 以下问题绝不能凭记忆回答,必须使用工具: - 算术计算 → bash - 文件内容、大小 → read_file / list_dir - 当前时间日期 → bash `, }, { 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: 工具行为策略(条件注入) ```typescript // 工具名 → 行为策略的映射 export const TOOL_BEHAVIOR_GUIDANCE: Record = { 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()` 方法: ```typescript // 之前 `\n ${s.name}\n ${s.description}\n` // 之后 const category = s.metadata?.category || '通用'; const tags = (s.metadata?.tags || []).join(','); `\n ${s.description}\n` ``` 索引头部措辞从被动改为强制: ```typescript // 之前 `当你需要使用某个 skill 时,调用 read_skill 工具读取完整指令后再执行。` // 之后 `## 技能(必须扫描) 回复前,扫描以下技能列表。如果某个技能明确匹配当前任务,用 read_skill 工具加载完整指令后严格遵循。` ``` ### 4. 后端预览接口 ```typescript // 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 下方新增折叠面板: ```vue 刷新预览
{{ layer.name }}
{{ layer.content }}
``` 调用逻辑: ```typescript const previewLoading = ref(false); const previewLayers = ref>([]); 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`(不再取 `skillPrompt`),skill 索引由 builder 内部处理 3. 记忆系统代码(第 192-210 行)需要提前到 `buildSystemPrompt` 调用之前,因为 builder 需要 `memoryEnabled` 和 `memoryContext` 4. `memoryConfig` 直接从 `cfg`(`agentInfo.config`)提取,不再重新查询 `agentInfo2`(消除冗余数据库查询) 5. 传给 builder 的 `modelId` 必须是纯模型名(`mc.modelId`),不是 `provider:model` 格式 ```typescript // 之前(分散在多处,第 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() 构建基础 prompt(Layer 1-6, 8) - 将现有的团队成员信息、委派提示、工具说明作为 crewContext 参数传入(Layer 7) ```typescript // 之前 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 行)改造: ```typescript // 之前 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 处理): ```typescript // 之前 const systemContent = memoryContext ? `${agentConfig.systemPrompt}\n\n\n${memoryContext}\n` : agentConfig.systemPrompt; // 之后 const systemContent = agentConfig.systemPrompt; // builder 已处理所有层 ``` ## 验证方案 1. **单元测试**:对 prompt_builder.ts 写测试,验证不同模型名、不同工具组合下的输出 2. **预览接口测试**:在前端编辑 Agent 时点击预览,确认各层内容正确 3. **端到端测试**:用 MiniMax-M2.7-highspeed 模型对话,观察工具调用行为是否改善 4. **回归测试**:用 Claude/GPT 模型对话,确认不受负面影响(Claude 的指导为空字符串)