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

504 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 里,但它始终可用。为避免各调用点硬编码不完整的工具列表,提供辅助函数:
```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: `# 执行纪律
<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: 工具行为策略(条件注入)
```typescript
// 工具名 → 行为策略的映射
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()` 方法:
```typescript
// 之前
`<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>`
```
索引头部措辞从被动改为强制:
```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
<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>
```
调用逻辑:
```typescript
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`(不再取 `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() 构建基础 promptLayer 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<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 的指导为空字符串)