# 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 的指导为空字符串)