# 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: `# 执行纪律
<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() 构建基础 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<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 的指导为空字符串）
