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

1422 lines
45 KiB
Markdown
Raw 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 分层注入系统 实施计划
> **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 串行。