1422 lines
45 KiB
Markdown
1422 lines
45 KiB
Markdown
# Prompt Builder 分层注入系统 实施计划
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 新建 prompt_builder + prompt_guidance 模块,将 5 个调用点的手动提示词拼接替换为统一的 8 层分层注入,并在前端提供预览面板。
|
||
|
||
**Architecture:** 纯函数 `buildSystemPrompt()` 接收 Agent 配置、模型名、可用工具列表等参数,按 8 层顺序组装系统提示词。所有指导文本常量集中在 `prompt_guidance.ts`。5 个调用点(gateway、agent_executor、crew_orchestrator、crew_delegate、controller preview)统一调用 builder,移除各自的手动拼接逻辑。
|
||
|
||
**Tech Stack:** TypeScript, Midway.js, Vue 3 + Element Plus
|
||
|
||
**Spec:** `docs/superpowers/specs/2026-04-15-prompt-builder-design.md`
|
||
|
||
---
|
||
|
||
## 文件结构
|
||
|
||
### 新建文件
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts` | 所有指导文本常量(工具纪律、模型指导、工具策略、统一 MEMORY_SYSTEM_PROMPT) |
|
||
| `packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts` | `buildSystemPrompt()` 纯函数 + `collectAvailableToolNames()` 辅助函数 |
|
||
| `packages/backend/test/prompt_builder.test.ts` | prompt_builder 纯函数单元测试 |
|
||
|
||
### 修改文件
|
||
|
||
| 文件 | 变更 |
|
||
|------|------|
|
||
| `packages/backend/src/modules/netaclaw/service/skill_loader.ts:143-161` | `buildSkillsPrompt()` 增强索引格式(category + tags + 强制措辞) |
|
||
| `packages/backend/src/modules/netaclaw/gateway/server.ts:25-42,167-209` | 移除 MEMORY_SYSTEM_PROMPT,替换手动拼接为调用 builder |
|
||
| `packages/backend/src/modules/netaclaw/service/agent_executor.ts:20-24,98-136` | 移除 MEMORY_SYSTEM_PROMPT,替换手动拼接为调用 builder |
|
||
| `packages/backend/src/modules/netaclaw/runtime/agent.ts:64-66` | 移除 memoryContext 拼接(已由 builder 处理) |
|
||
| `packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts:224-257` | `buildEnhancedPrompt()` 改为调用 builder + crewContext |
|
||
| `packages/backend/src/modules/netaclaw/service/crew_delegate.ts:47-53` | `buildSubAgentPrompt()` 改为调用 builder |
|
||
| `packages/backend/src/modules/netaclaw/controller/agent.ts:6-41` | 新增 `previewPrompt` 接口 |
|
||
| `packages/frontend/src/modules/agent/views/agent-edit.vue:94-102,211-480` | 系统提示词 Tab 下方新增折叠预览面板 |
|
||
|
||
---
|
||
|
||
## Task 1: 创建 prompt_guidance.ts — 指导文本常量
|
||
|
||
**Files:**
|
||
- Create: `packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts`
|
||
|
||
- [ ] **Step 1: 创建 prompt_guidance.ts 文件(第一部分:MEMORY + TOOL_USE_ENFORCEMENT + MODEL_GUIDANCE_RULES 前半)**
|
||
|
||
```typescript
|
||
// packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts
|
||
|
||
/**
|
||
* Prompt Builder 指导文本常量
|
||
* 所有注入到系统提示词的指导文本集中管理于此文件。
|
||
*/
|
||
|
||
// ─── 统一的 MEMORY_SYSTEM_PROMPT(合并 gateway 详细版 + agent_executor 简版) ───
|
||
|
||
export const MEMORY_SYSTEM_PROMPT = `
|
||
|
||
## 记忆系统
|
||
你拥有长期记忆能力。使用 memory_save 工具存储重要信息,使用 memory_recall 工具检索过往记忆。
|
||
|
||
记忆类型:
|
||
- user: 用户画像(偏好、角色、习惯)
|
||
- project: 项目知识(进展、决策、约束)
|
||
- feedback: 行为反馈(用户对你行为的纠正或确认)
|
||
- reference: 引用(外部资源链接、文档地址)
|
||
|
||
存储原则:
|
||
- 当用户透露个人偏好、角色、习惯时,存为 user 类型
|
||
- 当了解到项目进展、决策、约束时,存为 project 类型
|
||
- 当用户纠正或确认你的行为时,存为 feedback 类型
|
||
- 当提到外部资源链接时,存为 reference 类型
|
||
- 更新已有记忆而非创建重复条目
|
||
- 只存储对未来对话有价值的信息`;
|
||
|
||
// ─── Layer 2: 工具使用纪律(通用) ───
|
||
|
||
export const TOOL_USE_ENFORCEMENT = `# 工具使用规范
|
||
你必须通过工具采取行动 - 不要只描述你打算做什么。当你说"我来检查一下"、"让我执行"时,必须在同一回复中立即发起对应的工具调用。不要以"下一步我会..."结束回复 - 现在就执行。
|
||
|
||
持续工作直到任务真正完成。不要停在计划阶段。如果你有可用的工具能完成任务,使用它们,而不是告诉用户你会怎么做。
|
||
|
||
每条回复要么 (a) 包含推进任务的工具调用,要么 (b) 向用户交付最终结果。仅描述意图而不行动的回复是不可接受的。`;
|
||
|
||
// ─── Layer 3: 模型特定指导 ───
|
||
|
||
interface ModelGuidanceRule {
|
||
patterns: string[];
|
||
guidance: string;
|
||
}
|
||
|
||
const MODEL_GUIDANCE_RULES: ModelGuidanceRule[] = [
|
||
{
|
||
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)
|
||
- 文件路径使用绝对路径
|
||
- 每次工具调用后检查返回结果,确认操作成功后再继续`,
|
||
},
|
||
];
|
||
|
||
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;
|
||
}
|
||
|
||
// ─── PLACEHOLDER_TOOL_BEHAVIOR ───
|
||
```
|
||
|
||
- [ ] **Step 2: 追加 prompt_guidance.ts 第二部分(Layer 4 工具行为策略)**
|
||
|
||
在 `// ─── PLACEHOLDER_TOOL_BEHAVIOR ───` 处替换为:
|
||
|
||
```typescript
|
||
// ─── Layer 4: 工具行为策略(条件注入) ───
|
||
|
||
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');
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/runtime/prompt_guidance.ts
|
||
git commit -m "feat(netaclaw): 新建 prompt_guidance.ts — 集中管理所有指导文本常量"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: 创建 prompt_builder.ts — 核心构建函数
|
||
|
||
**Files:**
|
||
- Create: `packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts`
|
||
|
||
- [ ] **Step 1: 创建 prompt_builder.ts 文件(接口定义 + collectAvailableToolNames)**
|
||
|
||
```typescript
|
||
// packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts
|
||
|
||
import { SkillLoaderService } from '../service/skill_loader.js';
|
||
import {
|
||
TOOL_USE_ENFORCEMENT,
|
||
MEMORY_SYSTEM_PROMPT,
|
||
getModelGuidance,
|
||
getToolBehaviorGuidance,
|
||
} from './prompt_guidance.js';
|
||
|
||
/** 基础内置工具(始终可用) */
|
||
const BASE_TOOLS = ['bash', 'read_file', 'write_file', 'list_dir', 'todo'];
|
||
|
||
/** 构建完整的可用工具名列表 */
|
||
export function collectAvailableToolNames(opts: {
|
||
memoryEnabled?: boolean;
|
||
hasSkills?: boolean;
|
||
crewRole?: 'master' | 'sub';
|
||
}): 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;
|
||
}
|
||
|
||
export interface BuildSystemPromptParams {
|
||
agentSystemPrompt: string;
|
||
modelId: string;
|
||
availableToolNames: string[];
|
||
skills: string[];
|
||
skillLoader: SkillLoaderService;
|
||
memoryEnabled: boolean;
|
||
memoryContext?: string;
|
||
crewContext?: string;
|
||
}
|
||
|
||
export interface PromptLayer {
|
||
name: string;
|
||
content: string;
|
||
}
|
||
|
||
export interface BuildResult {
|
||
prompt: string;
|
||
layers: PromptLayer[];
|
||
}
|
||
|
||
// ─── PLACEHOLDER_BUILD_FN ───
|
||
```
|
||
|
||
- [ ] **Step 2: 追加 buildSystemPrompt 函数实现**
|
||
|
||
在 `// ─── PLACEHOLDER_BUILD_FN ───` 处替换为:
|
||
|
||
```typescript
|
||
/** 8 层分层组装系统提示词 */
|
||
export function buildSystemPrompt(params: BuildSystemPromptParams): BuildResult {
|
||
const {
|
||
agentSystemPrompt,
|
||
modelId,
|
||
availableToolNames,
|
||
skills,
|
||
skillLoader,
|
||
memoryEnabled,
|
||
memoryContext,
|
||
crewContext,
|
||
} = params;
|
||
|
||
const layers: PromptLayer[] = [];
|
||
|
||
// Layer 1: Agent 身份(用户写的 systemPrompt,原样保留)
|
||
if (agentSystemPrompt) {
|
||
layers.push({ name: 'Agent 身份', content: agentSystemPrompt });
|
||
}
|
||
|
||
// Layer 2: 工具使用纪律(通用,所有有工具的 Agent 都注入)
|
||
if (availableToolNames.length > 0) {
|
||
layers.push({ name: '工具使用纪律', content: TOOL_USE_ENFORCEMENT });
|
||
}
|
||
|
||
// Layer 3: 模型特定指导(按模型名称子串匹配)
|
||
const modelGuidance = getModelGuidance(modelId);
|
||
if (modelGuidance) {
|
||
layers.push({ name: '模型特定指导', content: modelGuidance });
|
||
}
|
||
|
||
// Layer 4: 工具行为策略(按可用工具条件注入)
|
||
const toolBehavior = getToolBehaviorGuidance(availableToolNames);
|
||
if (toolBehavior) {
|
||
layers.push({ name: '工具行为策略', content: toolBehavior });
|
||
}
|
||
|
||
// Layer 5: Skill 索引(增强版)
|
||
if (skills.length > 0) {
|
||
const skillPrompt = skillLoader.buildSkillsPrompt(skills, availableToolNames);
|
||
if (skillPrompt) {
|
||
layers.push({ name: 'Skill 索引', content: skillPrompt });
|
||
}
|
||
}
|
||
|
||
// Layer 6: 记忆系统
|
||
if (memoryEnabled) {
|
||
let memoryContent = MEMORY_SYSTEM_PROMPT;
|
||
if (memoryContext) {
|
||
memoryContent += `\n\n<memory-context>\n${memoryContext}\n</memory-context>`;
|
||
}
|
||
layers.push({ name: '记忆系统', content: memoryContent });
|
||
}
|
||
|
||
// Layer 7: Crew 编排上下文(仅 Crew 场景)
|
||
if (crewContext) {
|
||
layers.push({ name: 'Crew 编排上下文', content: crewContext });
|
||
}
|
||
|
||
// Layer 8: 元信息
|
||
const meta = `\n---\n当前时间: ${new Date().toISOString()}\n模型: ${modelId}`;
|
||
layers.push({ name: '元信息', content: meta });
|
||
|
||
const prompt = layers.map(l => l.content).join('\n\n');
|
||
return { prompt, layers };
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/runtime/prompt_builder.ts
|
||
git commit -m "feat(netaclaw): 新建 prompt_builder.ts — 8层分层系统提示词组装引擎"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: 单元测试 prompt_builder + prompt_guidance
|
||
|
||
**Files:**
|
||
- Create: `packages/backend/test/prompt_builder.test.ts`
|
||
|
||
项目已有 Jest 基础设施(`jest.config.js` + `ts-jest`),但无实际测试文件。prompt_builder 和 prompt_guidance 是纯函数,不依赖 Midway 框架,可以直接用 Jest 测试。
|
||
|
||
- [ ] **Step 1: 编写测试文件(第一部分:collectAvailableToolNames + getModelGuidance)**
|
||
|
||
```typescript
|
||
// packages/backend/test/prompt_builder.test.ts
|
||
|
||
import { collectAvailableToolNames, buildSystemPrompt } from '../src/modules/netaclaw/runtime/prompt_builder.js';
|
||
import { getModelGuidance, getToolBehaviorGuidance, TOOL_USE_ENFORCEMENT, MEMORY_SYSTEM_PROMPT } from '../src/modules/netaclaw/runtime/prompt_guidance.js';
|
||
|
||
// ─── collectAvailableToolNames ───
|
||
|
||
describe('collectAvailableToolNames', () => {
|
||
test('基础工具始终包含 todo', () => {
|
||
const names = collectAvailableToolNames({});
|
||
expect(names).toContain('bash');
|
||
expect(names).toContain('read_file');
|
||
expect(names).toContain('write_file');
|
||
expect(names).toContain('list_dir');
|
||
expect(names).toContain('todo');
|
||
});
|
||
|
||
test('memoryEnabled 追加记忆工具', () => {
|
||
const names = collectAvailableToolNames({ memoryEnabled: true });
|
||
expect(names).toContain('memory_save');
|
||
expect(names).toContain('memory_recall');
|
||
});
|
||
|
||
test('hasSkills 追加 skill 工具', () => {
|
||
const names = collectAvailableToolNames({ hasSkills: true });
|
||
expect(names).toContain('read_skill');
|
||
expect(names).toContain('read_skill_file');
|
||
expect(names).toContain('skill_manage');
|
||
});
|
||
|
||
test('crewRole=master 追加委派工具', () => {
|
||
const names = collectAvailableToolNames({ crewRole: 'master' });
|
||
expect(names).toContain('delegate_task');
|
||
expect(names).toContain('delegate_parallel');
|
||
expect(names).toContain('escalate');
|
||
});
|
||
|
||
test('crewRole=sub 不追加委派工具', () => {
|
||
const names = collectAvailableToolNames({ crewRole: 'sub' });
|
||
expect(names).not.toContain('delegate_task');
|
||
});
|
||
});
|
||
|
||
// ─── getModelGuidance ───
|
||
|
||
describe('getModelGuidance', () => {
|
||
test('Claude 返回空字符串', () => {
|
||
expect(getModelGuidance('claude-sonnet-4-20250514')).toBe('');
|
||
});
|
||
|
||
test('MiniMax 匹配 minimax 规则', () => {
|
||
const g = getModelGuidance('MiniMax-M2.7-highspeed');
|
||
expect(g).toContain('一次只调用一个工具');
|
||
});
|
||
|
||
test('GPT 匹配 gpt 规则', () => {
|
||
const g = getModelGuidance('gpt-4o-2024-08-06');
|
||
expect(g).toContain('tool_persistence');
|
||
});
|
||
|
||
test('DeepSeek 匹配 deepseek 规则', () => {
|
||
const g = getModelGuidance('deepseek-chat');
|
||
expect(g).toContain('并行工具调用');
|
||
});
|
||
|
||
test('未知模型返回默认指导', () => {
|
||
const g = getModelGuidance('some-unknown-model-v1');
|
||
expect(g).toContain('事实性问题');
|
||
});
|
||
|
||
test('火山引擎跑 doubao 模型匹配 doubao 规则', () => {
|
||
const g = getModelGuidance('doubao-pro-32k');
|
||
expect(g).toContain('非交互标志');
|
||
});
|
||
});
|
||
|
||
// ─── getToolBehaviorGuidance ───
|
||
|
||
describe('getToolBehaviorGuidance', () => {
|
||
test('包含 todo 时注入任务规划策略', () => {
|
||
const g = getToolBehaviorGuidance(['bash', 'todo']);
|
||
expect(g).toContain('任务规划策略');
|
||
});
|
||
|
||
test('包含 memory_save 时注入记忆策略', () => {
|
||
const g = getToolBehaviorGuidance(['memory_save', 'memory_recall']);
|
||
expect(g).toContain('记忆使用策略');
|
||
});
|
||
|
||
test('无匹配工具时返回空字符串', () => {
|
||
const g = getToolBehaviorGuidance(['bash', 'read_file']);
|
||
expect(g).toBe('');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: 追加测试文件第二部分(buildSystemPrompt 集成测试)**
|
||
|
||
在测试文件末尾追加:
|
||
|
||
```typescript
|
||
// ─── buildSystemPrompt ───
|
||
|
||
// 模拟 SkillLoaderService(只需 buildSkillsPrompt 方法)
|
||
function createMockSkillLoader(returnValue: string) {
|
||
return {
|
||
buildSkillsPrompt: jest.fn().mockReturnValue(returnValue),
|
||
} as any;
|
||
}
|
||
|
||
describe('buildSystemPrompt', () => {
|
||
test('基础场景:无 skill、无记忆、无 crew', () => {
|
||
const result = buildSystemPrompt({
|
||
agentSystemPrompt: '你是电商助手',
|
||
modelId: 'MiniMax-M2.7-highspeed',
|
||
availableToolNames: ['bash', 'read_file', 'write_file', 'list_dir', 'todo'],
|
||
skills: [],
|
||
skillLoader: createMockSkillLoader(''),
|
||
memoryEnabled: false,
|
||
});
|
||
|
||
// Layer 1: Agent 身份
|
||
expect(result.prompt).toContain('你是电商助手');
|
||
// Layer 2: 工具纪律
|
||
expect(result.prompt).toContain('工具使用规范');
|
||
// Layer 3: MiniMax 模型指导
|
||
expect(result.prompt).toContain('一次只调用一个工具');
|
||
// Layer 4: todo 工具策略
|
||
expect(result.prompt).toContain('任务规划策略');
|
||
// Layer 8: 元信息
|
||
expect(result.prompt).toContain('模型: MiniMax-M2.7-highspeed');
|
||
// 不含记忆
|
||
expect(result.prompt).not.toContain('记忆系统');
|
||
// layers 数组正确
|
||
expect(result.layers.map(l => l.name)).toEqual([
|
||
'Agent 身份', '工具使用纪律', '模型特定指导', '工具行为策略', '元信息',
|
||
]);
|
||
});
|
||
|
||
test('完整场景:skill + 记忆 + memoryContext', () => {
|
||
const mockLoader = createMockSkillLoader('<available_skills>...</available_skills>');
|
||
const result = buildSystemPrompt({
|
||
agentSystemPrompt: '你是运营助手',
|
||
modelId: 'gpt-4o',
|
||
availableToolNames: ['bash', 'todo', 'memory_save', 'memory_recall', 'read_skill', 'read_skill_file', 'skill_manage'],
|
||
skills: ['playwright-cli'],
|
||
skillLoader: mockLoader,
|
||
memoryEnabled: true,
|
||
memoryContext: '用户偏好:简洁回复',
|
||
});
|
||
|
||
expect(result.prompt).toContain('你是运营助手');
|
||
expect(result.prompt).toContain('tool_persistence'); // GPT 指导
|
||
expect(result.prompt).toContain('available_skills');
|
||
expect(result.prompt).toContain('记忆系统');
|
||
expect(result.prompt).toContain('<memory-context>');
|
||
expect(result.prompt).toContain('用户偏好:简洁回复');
|
||
expect(mockLoader.buildSkillsPrompt).toHaveBeenCalledWith(['playwright-cli'], expect.any(Array));
|
||
});
|
||
|
||
test('Crew 场景:注入 crewContext', () => {
|
||
const result = buildSystemPrompt({
|
||
agentSystemPrompt: '你是主 Agent',
|
||
modelId: 'claude-sonnet-4-20250514',
|
||
availableToolNames: ['bash', 'todo', 'delegate_task', 'delegate_parallel', 'escalate'],
|
||
skills: [],
|
||
skillLoader: createMockSkillLoader(''),
|
||
memoryEnabled: false,
|
||
crewContext: '## 你的团队成员\n- 小明: 数据分析',
|
||
});
|
||
|
||
expect(result.prompt).toContain('你的团队成员');
|
||
expect(result.layers.find(l => l.name === 'Crew 编排上下文')).toBeTruthy();
|
||
// Claude 模型指导为空,不应有 Layer 3
|
||
expect(result.layers.find(l => l.name === '模型特定指导')).toBeFalsy();
|
||
});
|
||
|
||
test('空 agentSystemPrompt 不生成 Layer 1', () => {
|
||
const result = buildSystemPrompt({
|
||
agentSystemPrompt: '',
|
||
modelId: 'deepseek-chat',
|
||
availableToolNames: ['bash', 'todo'],
|
||
skills: [],
|
||
skillLoader: createMockSkillLoader(''),
|
||
memoryEnabled: false,
|
||
});
|
||
|
||
expect(result.layers.find(l => l.name === 'Agent 身份')).toBeFalsy();
|
||
expect(result.layers[0].name).toBe('工具使用纪律');
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: 运行测试确认全部通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx jest test/prompt_builder.test.ts --verbose
|
||
```
|
||
|
||
预期输出:所有 test case PASS。如果 ts-jest 的 ESM 导入有问题(`.js` 后缀),需要在 jest.config.js 中添加 `moduleNameMapper` 将 `.js` 映射到 `.ts`。
|
||
|
||
如果遇到 ESM 问题,在 `jest.config.js` 中添加:
|
||
```javascript
|
||
moduleNameMapper: {
|
||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||
},
|
||
```
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/test/prompt_builder.test.ts
|
||
git add packages/backend/jest.config.js # 如果修改了
|
||
git commit -m "test(netaclaw): prompt_builder + prompt_guidance 纯函数单元测试"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: 增强 skill_loader.ts — 索引格式升级
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/service/skill_loader.ts:143-161`
|
||
|
||
- [ ] **Step 1: 修改 buildSkillsPrompt() 方法**
|
||
|
||
打开 `packages/backend/src/modules/netaclaw/service/skill_loader.ts`,将 `buildSkillsPrompt()` 方法(第 143-161 行)替换为:
|
||
|
||
```typescript
|
||
buildSkillsPrompt(skillNames: string[], availableTools: string[]): string {
|
||
const filtered = this.filterByConditions(skillNames, availableTools);
|
||
if (filtered.length === 0) return '';
|
||
|
||
const lines: string[] = [];
|
||
let totalChars = 0;
|
||
for (const s of filtered) {
|
||
const category = (s.metadata?.category as string) || '通用';
|
||
const tags = Array.isArray(s.metadata?.tags) ? (s.metadata.tags as string[]).join(',') : '';
|
||
const line = ` <skill name="${s.name}" category="${category}" tags="${tags}">\n ${s.description}\n </skill>`;
|
||
if (totalChars + line.length > SkillLoaderService.MAX_SKILLS_PROMPT_CHARS) {
|
||
this.logger.warn('[SkillLoader] Skill 索引超过 %d 字符限制,截断', SkillLoaderService.MAX_SKILLS_PROMPT_CHARS);
|
||
break;
|
||
}
|
||
lines.push(line);
|
||
totalChars += line.length;
|
||
}
|
||
if (lines.length === 0) return '';
|
||
|
||
return `\n\n## 技能(必须扫描)\n回复前,扫描以下技能列表。如果某个技能明确匹配当前任务,用 read_skill 工具加载完整指令后严格遵循。\n\n<available_skills>\n${lines.join('\n')}\n</available_skills>`;
|
||
}
|
||
```
|
||
|
||
变更点:
|
||
1. 每个 `<skill>` 标签增加 `category` 和 `tags` 属性
|
||
2. 索引头部从被动措辞改为强制措辞("必须扫描")
|
||
3. 尾部说明从 `当你需要使用某个 skill 时...` 改为 `回复前,扫描以下技能列表...`
|
||
|
||
- [ ] **Step 2: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -20
|
||
```
|
||
|
||
预期:无新增编译错误。
|
||
|
||
- [ ] **Step 3: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/service/skill_loader.ts
|
||
git commit -m "feat(netaclaw): skill 索引增强 — category/tags 属性 + 强制扫描措辞"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: 改造 gateway/server.ts — 替换手动拼接
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/gateway/server.ts:1-42,131-135,167-177,192-210,226-229`
|
||
|
||
这是最核心的改造,WebSocket 对话入口。
|
||
|
||
- [ ] **Step 1: 添加 import 并移除 MEMORY_SYSTEM_PROMPT**
|
||
|
||
在文件顶部 import 区域添加:
|
||
|
||
```typescript
|
||
import { buildSystemPrompt, collectAvailableToolNames } from '../runtime/prompt_builder.js';
|
||
```
|
||
|
||
删除第 25-42 行的 `MEMORY_SYSTEM_PROMPT` 常量(整个 const 声明)。
|
||
|
||
- [ ] **Step 2: 替换 handleChat() 中的 systemPrompt 组装逻辑**
|
||
|
||
找到 `handleChat()` 方法中构建 `agentConfig` 的部分。
|
||
|
||
当前代码(约第 131-135 行):
|
||
```typescript
|
||
const { skillPrompt, skillTools: agentSkillTools } = buildSkillContext(
|
||
this.skillLoader,
|
||
agentInfo.skills,
|
||
builtinToolNames,
|
||
);
|
||
```
|
||
|
||
替换为:
|
||
```typescript
|
||
const { skillTools: agentSkillTools } = buildSkillContext(
|
||
this.skillLoader,
|
||
agentInfo.skills,
|
||
builtinToolNames,
|
||
);
|
||
```
|
||
|
||
注意:仍然需要 `buildSkillContext` 来获取 `skillTools`(read_skill 等工具实例),但不再需要 `skillPrompt`(由 builder 内部处理)。
|
||
|
||
- [ ] **Step 3: 提前记忆系统代码 + 替换 agentConfig 构建**
|
||
|
||
当前 gateway 中记忆系统代码在第 192-210 行(agentConfig 构建之后),需要提前到 `buildSystemPrompt` 调用之前。同时消除冗余的 `agentInfo2` 重新查询。
|
||
|
||
找到第 192-210 行的记忆系统代码块,**整块删除**(包括 `agentInfo2` 查询)。
|
||
|
||
然后将第 167-177 行的 agentConfig 构建替换为以下代码(记忆处理 + builder 调用 + agentConfig 构建一体化):
|
||
|
||
```typescript
|
||
// --- 记忆系统(从 cfg 直接取,不再重新查询 agentInfo2) ---
|
||
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, agentInfo.name, userId, memoryConfig.prefetchLimit);
|
||
memoryTools = [
|
||
createMemorySaveTool(provider, agentInfo.name, userId),
|
||
createMemoryRecallTool(provider, agentInfo.name, userId),
|
||
];
|
||
}
|
||
|
||
// --- 分层组装系统提示词 ---
|
||
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 = {
|
||
name: agentInfo.name,
|
||
systemPrompt,
|
||
model,
|
||
apiKey,
|
||
baseUrl,
|
||
channelId,
|
||
channelName,
|
||
channelSupplier,
|
||
maxToolRounds: cfg?.middleware?.maxToolRounds ?? cfg?.middleware?.maxToolCalls ?? 20,
|
||
temperature: cfg?.temperature,
|
||
maxTokens: cfg?.maxTokens,
|
||
defaultThinkLevel: cfg?.thinking?.defaultLevel,
|
||
};
|
||
```
|
||
|
||
注意事项:
|
||
- `modelId: mc.modelId || ''` 传纯模型名(如 `MiniMax-M2.7-highspeed`),不是 `model` 变量(`openai:MiniMax-M2.7-highspeed`)
|
||
- `memoryConfig` 从 `cfg?.memory` 取(`cfg` 在第 129 行已有),不再重新查询 `agentInfo2`
|
||
- 原第 193 行的 `const agentInfo2 = agentId ? await this.agentService.info(agentId) : null;` 删除(消除冗余数据库查询)
|
||
|
||
- [ ] **Step 4: 移除 runAgent 调用中的 memoryContext 参数**
|
||
|
||
找到 `runAgent()` 调用(约第 229 行),移除 `memoryContext` 参数:
|
||
|
||
```typescript
|
||
const result = await runAgent({
|
||
agentConfig,
|
||
tools: [...this.defaultTools, ...memoryTools, ...agentSkillTools],
|
||
userMessage: content,
|
||
history: history.slice(0, -1),
|
||
// memoryContext 已移除 — 由 builder 处理
|
||
sessionThinkLevel: sessionThinkLevel ?? undefined,
|
||
onToken: (token) => { /* ... */ },
|
||
// ...
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 5: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
- [ ] **Step 6: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/gateway/server.ts
|
||
git commit -m "refactor(netaclaw): gateway handleChat() 替换手动拼接为 prompt_builder"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: 改造 agent_executor.ts — 替换手动拼接
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/service/agent_executor.ts:1-24,77-100,116-141`
|
||
|
||
- [ ] **Step 1: 添加 import 并移除 MEMORY_SYSTEM_PROMPT**
|
||
|
||
在文件顶部 import 区域添加:
|
||
|
||
```typescript
|
||
import { buildSystemPrompt, collectAvailableToolNames } from '../runtime/prompt_builder.js';
|
||
```
|
||
|
||
删除第 20-24 行的 `MEMORY_SYSTEM_PROMPT` 常量。
|
||
|
||
- [ ] **Step 2: 替换 execute() 中的 systemPrompt 组装**
|
||
|
||
找到 `execute()` 方法中构建 skillPrompt 的部分(约第 72-83 行)。
|
||
|
||
当前代码:
|
||
```typescript
|
||
let skillTools: AnyAgentTool[] = [];
|
||
let skillPrompt = '';
|
||
// ...
|
||
const builtinToolNames = ['bash', 'read_file', 'write_file', 'list_dir'];
|
||
const skillCtx = buildSkillContext(this.skillLoader, agentEntity.skills, builtinToolNames);
|
||
skillPrompt = skillCtx.skillPrompt;
|
||
skillTools = skillCtx.skillTools;
|
||
```
|
||
|
||
替换为:
|
||
```typescript
|
||
let skillTools: AnyAgentTool[] = [];
|
||
// ...
|
||
const builtinToolNames = ['bash', 'read_file', 'write_file', 'list_dir'];
|
||
const skillCtx = buildSkillContext(this.skillLoader, agentEntity.skills, builtinToolNames);
|
||
skillTools = skillCtx.skillTools;
|
||
```
|
||
|
||
移除 `skillPrompt` 变量(不再需要)。
|
||
|
||
- [ ] **Step 3: 替换 agentConfig 构建和记忆拼接**
|
||
|
||
找到 agentConfig 构建(约第 98-105 行)和记忆拼接(约第 116-136 行)。
|
||
|
||
将 agentConfig 构建中的 `systemPrompt: (agentEntity.systemPrompt || '') + skillPrompt` 替换。
|
||
|
||
注意:`model` 变量在渠道解析后变成了 `provider:model` 格式(如 `openai:MiniMax-M2.7-highspeed`),但 builder 需要纯模型名。用 `mc.modelId` 而不是 `model`。
|
||
|
||
在记忆预取完成后,统一构建:
|
||
|
||
```typescript
|
||
// 记忆预取(保持原有逻辑获取 memoryContext 和 memoryTools)
|
||
let memoryContext: string | undefined;
|
||
let memoryTools: AnyAgentTool[] = [];
|
||
const memoryConfig: AgentMemoryConfig | undefined = cfg?.memory;
|
||
if (memoryConfig?.enabled) {
|
||
const provider = createMemoryProvider(memoryConfig, this.memoryEntity);
|
||
memoryContext = await prefetchMemory(provider, params.message, agentEntity.name, params.userId, memoryConfig.prefetchLimit);
|
||
memoryTools = [createMemorySaveTool(provider), createMemoryRecallTool(provider)];
|
||
}
|
||
|
||
// 构建可用工具名列表
|
||
const toolNames = collectAvailableToolNames({
|
||
memoryEnabled: !!memoryConfig?.enabled,
|
||
hasSkills: !!agentEntity.skills?.length,
|
||
});
|
||
|
||
// 分层组装系统提示词
|
||
const { prompt: systemPrompt } = buildSystemPrompt({
|
||
agentSystemPrompt: agentEntity.systemPrompt || '',
|
||
modelId: mc.modelId || '', // 纯模型名,不含 provider: 前缀
|
||
availableToolNames: toolNames,
|
||
skills: agentEntity.skills || [],
|
||
skillLoader: this.skillLoader,
|
||
memoryEnabled: !!memoryConfig?.enabled,
|
||
memoryContext,
|
||
});
|
||
|
||
agentConfig = {
|
||
name: agentEntity.name,
|
||
systemPrompt,
|
||
model,
|
||
apiKey,
|
||
baseUrl,
|
||
maxToolRounds: cfg?.middleware?.maxToolRounds ?? cfg?.middleware?.maxToolCalls ?? 20,
|
||
};
|
||
```
|
||
|
||
同时移除后面的 `agentConfig.systemPrompt += MEMORY_SYSTEM_PROMPT;` 行。
|
||
|
||
- [ ] **Step 4: 移除 runAgent 调用中的 memoryContext 参数**
|
||
|
||
找到 `runAgent()` 调用(约第 141 行),移除 `memoryContext` 参数。
|
||
|
||
- [ ] **Step 5: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
- [ ] **Step 6: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/service/agent_executor.ts
|
||
git commit -m "refactor(netaclaw): agent_executor execute() 替换手动拼接为 prompt_builder"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: 改造 runtime/agent.ts — 移除 memoryContext 拼接
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/runtime/agent.ts:26-40,64-66`
|
||
|
||
- [ ] **Step 1: 从 AgentRunParams 接口移除 memoryContext**
|
||
|
||
打开 `packages/backend/src/modules/netaclaw/runtime/agent.ts`。
|
||
|
||
找到 `AgentRunParams` 接口(第 26-40 行),移除 `memoryContext?: string;` 字段。
|
||
|
||
- [ ] **Step 2: 简化 systemContent 构建**
|
||
|
||
找到第 64-66 行:
|
||
|
||
```typescript
|
||
const systemContent = memoryContext
|
||
? `${agentConfig.systemPrompt}\n\n<memory-context>\n${memoryContext}\n</memory-context>`
|
||
: agentConfig.systemPrompt;
|
||
```
|
||
|
||
替换为:
|
||
|
||
```typescript
|
||
const systemContent = agentConfig.systemPrompt;
|
||
```
|
||
|
||
同时从第 43 行的解构中移除 `memoryContext`:
|
||
|
||
```typescript
|
||
// 之前
|
||
const { agentConfig, tools, userMessage, history = [], memoryContext,
|
||
onToken, onThinking, onToolCall, onToolResult } = params;
|
||
|
||
// 之后
|
||
const { agentConfig, tools, userMessage, history = [],
|
||
onToken, onThinking, onToolCall, onToolResult } = params;
|
||
```
|
||
|
||
- [ ] **Step 3: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
注意:此步骤可能会在 gateway/server.ts 和 agent_executor.ts 中报错(如果它们还在传 memoryContext 参数)。确保 Task 5 和 Task 6 已完成。
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/runtime/agent.ts
|
||
git commit -m "refactor(netaclaw): runAgent 移除 memoryContext — 已由 prompt_builder 统一处理"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: 改造 crew_orchestrator.ts — 主 Agent 提示词
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts:1-23,224-257`
|
||
|
||
- [ ] **Step 1: 添加 import**
|
||
|
||
在文件顶部 import 区域添加:
|
||
|
||
```typescript
|
||
import { buildSystemPrompt, collectAvailableToolNames } from '../runtime/prompt_builder.js';
|
||
```
|
||
|
||
- [ ] **Step 2: 替换 buildEnhancedPrompt() 方法**
|
||
|
||
找到 `buildEnhancedPrompt()` 方法(第 224-257 行),替换为:
|
||
|
||
```typescript
|
||
private buildEnhancedPrompt(
|
||
masterAgent: NetaClawAgentEntity,
|
||
ctx: CrewRunContext,
|
||
crew: NetaClawCrewEntity,
|
||
resolvedModelId: string,
|
||
): string {
|
||
// 构建 Crew 专属上下文(Layer 7)
|
||
const crewParts: string[] = [];
|
||
|
||
// 团队成员信息(含技能)
|
||
const memberList = ctx.memberAgents
|
||
.map(m => {
|
||
const skills = m.agent?.skills?.length
|
||
? `,已具备技能: ${m.agent.skills.join('、')}`
|
||
: '';
|
||
const desc = m.agent?.description ? `,简介: ${m.agent.description}` : '';
|
||
return `- ${m.name} (${m.label}): ${m.role || '无角色描述'}${desc}${skills}`;
|
||
})
|
||
.join('\n');
|
||
crewParts.push(`## 你的团队成员\n${memberList}\n\n注意:委派任务时无需让成员安装工具或环境,他们已具备所列技能,直接描述要完成的目标即可。`);
|
||
|
||
// 委派提示
|
||
if (crew.delegateHints?.hints) {
|
||
crewParts.push(`## 调度建议\n${crew.delegateHints.hints}`);
|
||
}
|
||
|
||
// 工具说明
|
||
crewParts.push(`## 编排工具说明
|
||
- delegate_task: 将任务委派给指定子 Agent 串行执行
|
||
- delegate_parallel: 同时委派多个任务给不同子 Agent 并行执行
|
||
- escalate: 遇到无法解决的问题时升级给人工处理
|
||
|
||
请根据任务需求合理分配工作给团队成员,充分利用并行执行提高效率。`);
|
||
|
||
// 通过 builder 统一组装
|
||
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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 更新 buildEnhancedPrompt 的调用处(第 139 行)**
|
||
|
||
当前代码(`crew_orchestrator.ts` 第 138-139 行):
|
||
```typescript
|
||
// 构建增强系统提示词
|
||
const enhancedPrompt = this.buildEnhancedPrompt(masterAgent, ctx, crew);
|
||
```
|
||
|
||
问题:模型解析在第 141-157 行,在 `buildEnhancedPrompt` 调用之后。需要将调用移到模型解析之后。
|
||
|
||
将第 138-161 行改为:
|
||
```typescript
|
||
// 解析主 Agent 模型配置(移到 buildEnhancedPrompt 之前)
|
||
const mc = (masterAgent.modelConfig || {}) as any;
|
||
let model: string, apiKey: string, baseUrl: string | undefined;
|
||
if (mc.apiKey && mc.modelId) {
|
||
model = mc.modelId;
|
||
apiKey = mc.apiKey;
|
||
baseUrl = mc.apiUrl;
|
||
} else if (mc.channelId) {
|
||
const resolved = await this.modelChannelService.resolveForAgent(mc.channelId, mc.modelId || '');
|
||
model = `${resolved.provider}:${resolved.model}`;
|
||
apiKey = resolved.apiKey;
|
||
baseUrl = resolved.baseUrl;
|
||
} else {
|
||
model = mc.modelId || (process.env.NETACLAW_MODEL ?? 'anthropic:claude-sonnet-4-20250514');
|
||
apiKey = process.env.NETACLAW_API_KEY ?? '';
|
||
baseUrl = mc.apiUrl;
|
||
}
|
||
|
||
// 提取纯 modelId(去掉 provider: 前缀)用于 prompt_builder 模型匹配
|
||
const pureModelId = model.includes(':') ? model.split(':')[1] : model;
|
||
|
||
// 构建增强系统提示词(现在有 modelId 了)
|
||
const enhancedPrompt = this.buildEnhancedPrompt(masterAgent, ctx, crew, pureModelId);
|
||
|
||
const agentConfig: AgentConfig = {
|
||
name: masterAgent.name,
|
||
systemPrompt: enhancedPrompt,
|
||
model,
|
||
apiKey,
|
||
baseUrl,
|
||
// ... 其余字段保持不变
|
||
```
|
||
|
||
- [ ] **Step 4: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/service/crew_orchestrator.ts
|
||
git commit -m "refactor(netaclaw): crew_orchestrator buildEnhancedPrompt 改用 prompt_builder"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: 改造 crew_delegate.ts — 子 Agent 提示词
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/service/crew_delegate.ts:1-13,47-53`
|
||
|
||
- [ ] **Step 1: 添加 import**
|
||
|
||
在文件顶部 import 区域添加:
|
||
|
||
```typescript
|
||
import { buildSystemPrompt, collectAvailableToolNames } from '../runtime/prompt_builder.js';
|
||
```
|
||
|
||
- [ ] **Step 2: 替换 buildSubAgentPrompt() 方法**
|
||
|
||
找到 `buildSubAgentPrompt()` 方法(第 47-53 行),替换为:
|
||
|
||
```typescript
|
||
private buildSubAgentPrompt(
|
||
systemPrompt: string,
|
||
role: string,
|
||
agentSkills?: string[],
|
||
modelId?: string,
|
||
): 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;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 更新 buildSubAgentPrompt 的调用处(第 111 行)**
|
||
|
||
当前代码(`crew_delegate.ts` 第 110-111 行):
|
||
```typescript
|
||
const modelInfo = await this.resolveModelConfig(agent);
|
||
const systemPrompt = this.buildSubAgentPrompt(agent.systemPrompt || '', member.role, agent.skills);
|
||
```
|
||
|
||
`modelInfo.model` 格式为 `provider:modelId`(如 `openai:MiniMax-M2.7-highspeed`),需要提取纯 modelId。
|
||
|
||
替换为:
|
||
```typescript
|
||
const modelInfo = await this.resolveModelConfig(agent);
|
||
// 提取纯 modelId(去掉 provider: 前缀)用于 prompt_builder 模型匹配
|
||
const pureModelId = modelInfo.model.includes(':') ? modelInfo.model.split(':')[1] : modelInfo.model;
|
||
const systemPrompt = this.buildSubAgentPrompt(agent.systemPrompt || '', member.role, agent.skills, pureModelId);
|
||
```
|
||
|
||
- [ ] **Step 4: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/service/crew_delegate.ts
|
||
git commit -m "refactor(netaclaw): crew_delegate buildSubAgentPrompt 改用 prompt_builder"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: 后端预览接口 — controller/agent.ts
|
||
|
||
**Files:**
|
||
- Modify: `packages/backend/src/modules/netaclaw/controller/agent.ts:1-41`
|
||
|
||
- [ ] **Step 1: 添加 import 和注入**
|
||
|
||
在文件顶部添加 import:
|
||
|
||
```typescript
|
||
import { buildSystemPrompt, collectAvailableToolNames } from '../runtime/prompt_builder.js';
|
||
import { SkillLoaderService } from '../service/skill_loader.js';
|
||
import { NetaClawModelChannelService } from '../service/model_channel.js';
|
||
```
|
||
|
||
在 `NetaClawAgentAdminController` 类中添加注入:
|
||
|
||
```typescript
|
||
@Inject()
|
||
skillLoader: SkillLoaderService;
|
||
|
||
@Inject()
|
||
channelService: NetaClawModelChannelService;
|
||
```
|
||
|
||
- [ ] **Step 2: 添加 previewPrompt 接口**
|
||
|
||
在 `NetaClawAgentAdminController` 类中(info() 方法之后)添加:
|
||
|
||
```typescript
|
||
@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 toolNames = collectAvailableToolNames({
|
||
hasSkills: !!body.skills?.length,
|
||
});
|
||
|
||
const result = buildSystemPrompt({
|
||
agentSystemPrompt: body.systemPrompt || '',
|
||
modelId,
|
||
availableToolNames: toolNames,
|
||
skills: body.skills || [],
|
||
skillLoader: this.skillLoader,
|
||
memoryEnabled: false, // 预览时不含记忆上下文
|
||
});
|
||
|
||
return this.ok(result);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 验证编译通过**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty 2>&1 | head -30
|
||
```
|
||
|
||
- [ ] **Step 4: 提交**
|
||
|
||
```bash
|
||
git add packages/backend/src/modules/netaclaw/controller/agent.ts
|
||
git commit -m "feat(netaclaw): 新增 previewPrompt 接口 — 前端预览最终系统提示词"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 11: 前端预览面板 — agent-edit.vue
|
||
|
||
**Files:**
|
||
- Modify: `packages/frontend/src/modules/agent/views/agent-edit.vue:94-102,211-480`
|
||
|
||
- [ ] **Step 1: 在系统提示词 Tab 下方添加折叠预览面板**
|
||
|
||
打开 `packages/frontend/src/modules/agent/views/agent-edit.vue`。
|
||
|
||
找到系统提示词 Tab(第 94-102 行),在 `</el-tab-pane>` 闭合标签之前(第 102 行之前),添加折叠面板:
|
||
|
||
```vue
|
||
<!-- 最终提示词预览 -->
|
||
<el-collapse v-if="modelConfig.channelId" style="margin-top: 16px">
|
||
<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" style="margin-bottom: 12px">
|
||
刷新预览
|
||
</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>
|
||
```
|
||
|
||
- [ ] **Step 2: 在 script 中添加预览相关的响应式变量和方法**
|
||
|
||
在 `<script>` 部分的 `setup()` 或 `<script setup>` 中添加:
|
||
|
||
```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: {
|
||
channelId: modelConfig.channelId,
|
||
modelId: modelConfig.modelId,
|
||
},
|
||
},
|
||
});
|
||
previewLayers.value = res?.layers || [];
|
||
} catch (e) {
|
||
console.error('预览加载失败', e);
|
||
previewLayers.value = [];
|
||
} finally {
|
||
previewLoading.value = false;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: 添加样式**
|
||
|
||
在 `<style scoped>` 中添加:
|
||
|
||
```css
|
||
.prompt-preview {
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.layer-block {
|
||
margin-bottom: 12px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.layer-label {
|
||
padding: 6px 12px;
|
||
background: var(--el-fill-color-light);
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.layer-content {
|
||
padding: 12px;
|
||
margin: 0;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 在浏览器中验证**
|
||
|
||
1. 启动前端开发服务器(用户手动执行 `pnpm --filter @neta/frontend dev`)
|
||
2. 打开 Agent 编辑页面
|
||
3. 选择一个模型渠道
|
||
4. 切换到"系统提示词"Tab
|
||
5. 确认折叠面板出现
|
||
6. 点击"刷新预览"按钮
|
||
7. 确认各层内容正确显示
|
||
|
||
- [ ] **Step 5: 提交**
|
||
|
||
```bash
|
||
git add packages/frontend/src/modules/agent/views/agent-edit.vue
|
||
git commit -m "feat(agent): Agent 编辑页新增最终提示词预览面板"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: 全量编译验证 + 运行测试
|
||
|
||
**Files:** 无新文件
|
||
|
||
- [ ] **Step 1: 全量 TypeScript 编译检查**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx tsc --noEmit --pretty
|
||
```
|
||
|
||
预期:无编译错误。如有错误,逐一修复。
|
||
|
||
- [ ] **Step 2: 运行单元测试**
|
||
|
||
```bash
|
||
cd packages/backend
|
||
npx jest test/prompt_builder.test.ts --verbose
|
||
```
|
||
|
||
预期:所有测试 PASS。
|
||
|
||
- [ ] **Step 3: 检查前端编译**
|
||
|
||
```bash
|
||
cd packages/frontend
|
||
npx vue-tsc --noEmit 2>&1 | head -30
|
||
```
|
||
|
||
预期:无新增编译错误。
|
||
|
||
- [ ] **Step 4: 最终提交(如有修复)**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "fix(netaclaw): prompt_builder 全量编译修复"
|
||
```
|
||
|
||
---
|
||
|
||
## 实施顺序与依赖关系
|
||
|
||
```
|
||
Task 1 (prompt_guidance.ts)
|
||
↓
|
||
Task 2 (prompt_builder.ts) ← 依赖 Task 1
|
||
↓
|
||
Task 3 (单元测试) ← 依赖 Task 1 + 2
|
||
↓
|
||
Task 4 (skill_loader 增强) ← 独立,但 builder 调用它
|
||
↓
|
||
Task 5 (gateway/server.ts) ← 依赖 Task 1 + 2 + 4
|
||
Task 6 (agent_executor.ts) ← 依赖 Task 1 + 2 + 4
|
||
Task 7 (runtime/agent.ts) ← 依赖 Task 5 + 6(移除 memoryContext 后调用方不再传)
|
||
↓
|
||
Task 8 (crew_orchestrator.ts) ← 依赖 Task 1 + 2
|
||
Task 9 (crew_delegate.ts) ← 依赖 Task 1 + 2
|
||
↓
|
||
Task 10 (controller previewPrompt) ← 依赖 Task 1 + 2
|
||
Task 11 (前端预览面板) ← 依赖 Task 10
|
||
↓
|
||
Task 12 (全量验证) ← 依赖所有 Task
|
||
```
|
||
|
||
Task 5/6 可并行,Task 8/9 可并行,Task 10/11 串行。
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|