GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-04-16-tool-registry-patch-clarify-design.md
2026-05-20 21:39:12 +08:00

315 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

# 工具注册表 + Patch/Clarify 工具 + 提示词优化 设计文档 (v2)
> 日期: 2026-04-16
> 参考: Hermes Agent (Nous Research) 工具系统架构
> v2: 架构审查后修订,移除过度设计,修正遗漏
> v3: 架构师自审修复 3 个 Critical + 2 个 Important 问题
## 1. 目标
1. 引入轻量级**工具目录** (`tools/catalog.ts`),统一工具 schema 注册和名称查询,不管理工具实例
2. 新增 **patch 工具**9 级模糊匹配策略链
3. 新增 **clarify 工具**:阻塞式用户交互(仅 WebSocket 入口可用REST 入口降级)
4. **重写提示词**:参考 Hermes优化工具使用纪律和 todo 触发率
5. 统一 todo_tool.ts 的 schema 为 TypeBox 格式
## 2. 架构决策:为什么不用 Hermes 式 Registry
Hermes 的 registry 模式要求工具在模块加载时完成注册schema + handler
Neta 的工具有两类,生命周期完全不同:
| 类型 | 工具 | 创建时机 | 依赖 |
|------|------|----------|------|
| 静态工具 | bash, read_file, write_file, list_dir, **patch** | 模块加载时 | 无 |
| 工厂工具 | memory_*, skill_*, delegate_*, todo, **clarify** | 每次会话/请求 | provider, skillLoader, CrewRunContext, TodoStore, WebSocket 回调 |
工厂工具的 `execute` 函数通过闭包捕获运行时上下文,无法在模块加载时注册完整实例。
强行用 registry 只会增加间接层,不解决实际问题。
**替代方案:工具目录 (Tool Catalog)**
- 只注册 schemaname, description, parameters, toolset不注册 handler
- 用于 prompt_builder 查询可用工具名列表,替代硬编码的 BASE_TOOLS
- 工具实例的组装仍由 gateway/server.ts 和 agent_executor.ts 负责
## 3. 工具目录 (`tools/catalog.ts`)
```typescript
interface ToolSchema {
name: string;
toolset: string;
description: string;
parameters: any; // TypeBox schema 或 plain object
}
const catalog = new Map<string, ToolSchema>();
export function registerSchema(schema: ToolSchema): void;
export function getSchemasByToolset(toolset: string): ToolSchema[];
export function getAllToolNames(toolsets: string[]): string[];
```
工具集定义(内联在 catalog.ts 中,不单独建文件):
```typescript
export const TOOLSET_DEFAULTS = ['base', 'planning', 'interaction'] as const;
// base: bash, read_file, write_file, list_dir, patch
// planning: todo
// interaction: clarify
// memory: memory_save, memory_recall条件启用
// skill: read_skill, read_skill_file, skill_manage条件启用
// crew: delegate_task, delegate_parallel, escalate条件启用
```
**注册入口集中化**catalog.ts 自身 import 所有工具文件触发注册,消费方只需 `import { getToolNamesByToolsets } from '../tools/catalog.js'`,无需在 server.ts/agent_executor.ts 添加 side-effect import。
## 4. Patch 工具
### 4.1 Schema (`tools/builtin/patch.ts`)
```typescript
const PatchParams = Type.Object({
path: Type.String({ description: '文件绝对路径' }),
old_string: Type.String({ description: '要查找的文本片段' }),
new_string: Type.String({ description: '替换为的文本' }),
replace_all: Type.Optional(Type.Boolean({ description: '替换所有匹配,默认 false' })),
});
```
归入 `base` 工具集,作为静态工具与 bash/read_file/write_file/list_dir 同级。
### 4.2 九级模糊匹配 (`utils/fuzzy_match.ts`)
| 级别 | 策略 | 算法 |
|------|------|------|
| 1 | exact | `content.indexOf(search)` |
| 2 | line_trimmed | 每行 `.trim()` 后匹配 |
| 3 | whitespace_normalized | `/\s+/g` → 单空格 |
| 4 | indent_flexible | 去除行首所有空白 |
| 5 | escape_normalized | `\\n``\n`, `\\t``\t` |
| 6 | trimmed_boundary | 仅首尾行 trim |
| 7 | unicode_normalized | 智能引号/em-dash/省略号 → ASCII |
| 8 | block_anchor | 首尾行锚定 + 中间 Levenshtein 相似度 ≥60% |
| 9 | context_aware | 逐行相似度 ≥80%,整体 ≥50% |
接口:
```typescript
interface FuzzyMatchResult {
strategy: string;
startIndex: number;
endIndex: number;
matchedText: string;
}
// 返回所有匹配(供 replace_all 和唯一性检查使用)
function fuzzyFindAll(content: string, search: string): FuzzyMatchResult[];
```
### 4.3 执行流程
1. `fs.readFile(path)` 读取文件
2. `fuzzyFindAll(content, old_string)` 查找所有匹配
3. 匹配数 = 0 → 返回错误 "未找到匹配,请检查 old_string"
4. 匹配数 > 1 且 `replace_all=false` → 返回错误 "找到 N 处匹配,请提供更多上下文或设置 replace_all=true"
5. 从后往前替换(避免索引偏移)
6. `fs.writeFile(path, result)` 写回
7. 返回简要 diff 摘要
## 5. Clarify 工具
### 5.1 两个入口点的差异处理
| 入口 | 通道 | clarify 行为 |
|------|------|-------------|
| `gateway/server.ts` (WebSocket) | 有实时双向通道 | 阻塞式:推送问题,等待回答 |
| `service/agent_executor.ts` (REST) | 无实时通道 | 降级返回问题文本Agent 本轮结束 |
### 5.2 Schema (`tools/builtin/clarify.ts`)
```typescript
const ClarifyParams = Type.Object({
question: Type.String({ description: '要问用户的问题' }),
choices: Type.Optional(Type.Array(Type.String(), { maxItems: 4, description: '预设选项' })),
});
```
工具本身只定义 schema不包含阻塞逻辑。阻塞由外部注入的回调控制。
### 5.3 阻塞机制实现
**attempt.ts 改动**:新增 `onClarifyRequest` 回调参数
```typescript
interface AttemptParams {
// ... 现有参数不变 ...
onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}
```
工具执行处attempt.ts 第 59-86 行区域)增加分支:
```typescript
if (tc.name === 'clarify' && params.onClarifyRequest) {
const args = JSON.parse(tc.arguments);
resultText = await params.onClarifyRequest(args.question, args.choices);
} else {
// 原有的 tool.execute 逻辑
}
```
**gateway/server.ts 改动**
> **Critical**: Midway.js `@WSController` 是 request-scope每连接一个实例`clarifyResolvers` 必须是模块级 Map否则 clarify_response 可能路由到不同实例导致 resolve 永远不被调用。
```typescript
// 模块级 Map不是实例属性
const clarifyResolvers = new Map<string, {
resolve: (answer: string) => void;
timer: NodeJS.Timeout;
}>();
// runAgent 调用时注入回调
onClarifyRequest: async (question, choices) => {
const requestId = crypto.randomUUID();
this.send({ type: 'clarify_request', sessionId: sid, data: { requestId, question, choices } });
return new Promise<string>((resolve) => {
const timer = setTimeout(() => {
this.clarifyResolvers.delete(requestId);
resolve('用户未在规定时间内回答。请根据已有信息自行判断并继续执行。');
}, 20_000); // 前端适配前用短超时快速降级
this.clarifyResolvers.set(requestId, { resolve, timer });
});
},
// onMessage 新增分支
if (msg.type === 'clarify_response') {
const entry = this.clarifyResolvers.get(msg.requestId);
if (entry) {
clearTimeout(entry.timer);
this.clarifyResolvers.delete(msg.requestId);
entry.resolve(msg.answer);
}
}
```
**agent_executor.ts 改动**:不注入 `onClarifyRequest`
`onClarifyRequest` 未提供时attempt.ts 走正常的 `tool.execute` 路径。
clarify 工具的默认 execute 返回:`"[clarify] 问题: {question}\n选项: {choices}"`
Agent 会将此作为回复输出,用户下次消息自然回答。
### 5.4 WebSocket 协议扩展
```typescript
// protocol.ts 新增
// 服务端 → 客户端
interface ServerClarifyRequestEvent {
type: 'clarify_request';
sessionId: string;
data: { requestId: string; question: string; choices?: string[] };
}
// 客户端 → 服务端
interface ClientClarifyResponseMessage {
type: 'clarify_response';
sessionId: string;
requestId: string;
answer: string;
}
```
## 6. 提示词重写
### 6.1 TOOL_USE_ENFORCEMENT 改为动态函数
> **Critical**: 原设计硬编码了 clarify/patch 工具名Crew 子 Agent 没有这些工具会被误导调用不存在的工具。改为根据实际可用工具列表动态生成。
```typescript
export function getToolUseEnforcement(toolNames: string[]): string {
const rules = `# 工具使用规范
你必须通过工具采取行动 - 不要只描述你打算做什么。
## 强制规则
- 当你说"我来检查"、"让我执行"时,必须在同一回复中立即发起工具调用
- 不要以"下一步我会..."结束回复 - 现在就执行
- 持续工作直到任务真正完成,不要停在计划阶段
- 每条回复要么包含推进任务的工具调用,要么向用户交付最终结果`;
const scenarios: string[] = [];
if (toolNames.includes('read_file')) scenarios.push('需要读取文件内容时 → read_file不要凭记忆猜测');
if (toolNames.includes('patch')) scenarios.push('需要修改已有文件时 → patch局部修改或 write_file新建/全量重写)');
else if (toolNames.includes('write_file')) scenarios.push('需要修改文件时 → write_file');
if (toolNames.includes('list_dir')) scenarios.push('需要了解目录结构时 → list_dir');
if (toolNames.includes('bash')) scenarios.push('需要执行命令时 → bash');
if (toolNames.includes('clarify')) scenarios.push('任务需求不明确时 → clarify主动提问不要猜测');
if (toolNames.includes('todo')) scenarios.push('复杂任务开始前 → todo创建任务列表');
return scenarios.length > 0
? `${rules}\n\n## 必须使用工具的场景\n${scenarios.map(s => `- ${s}`).join('\n')}\n\n## 操作前先确认\n- 修改文件前先 read_file 确认当前内容\n- 执行命令前确认工作目录`
: rules;
}
```
对应 `prompt_builder.ts` 的调用从 `TOOL_USE_ENFORCEMENT` 常量改为 `getToolUseEnforcement(params.availableToolNames)` 函数调用。
### 6.2 TOOL_BEHAVIOR 新增/修改
```typescript
const TOOL_BEHAVIOR: Record<string, string> = {
todo: `## 任务规划策略
收到用户请求后,先评估任务复杂度:
**必须使用 todo** 涉及 2 个以上步骤、修改多个文件、需要先调研再实施、用户一次提出多个需求。
**无需使用 todo** 单步操作(查看文件、回答问题、一条命令)、简单单文件小修改。
列表顺序=优先级。同一时间只标记一个 in_progress。完成立即标记 completed。
任务列表是你的工作契约 - 它让用户看到你的计划并跟踪进度。`,
patch: `## 文件编辑策略
修改已有文件时,优先使用 patch 进行局部替换,而非 write_file 全量重写。
**用 patch** 修改函数、修复 bug、调整配置、添加/删除代码片段。
**用 write_file** 创建新文件、文件需要完全重写。
old_string 提供足够上下文确保唯一匹配。不需要精确匹配缩进和空白。`,
clarify: `## 主动提问策略
任务需求不明确、存在多种合理解读、或缺少关键信息时,使用 clarify 向用户提问。
**应该提问:** 指令模糊有多种解读、缺少关键参数、操作有风险需确认。
**不应提问:** 任务明确可直接执行、可从上下文推断、琐碎实现细节。
提供 choices 选项让用户快速选择。一次只问一个问题。`,
// memory_save 和 skill_manage 保持不变
};
```
### 6.3 todo_tool.ts TypeBox 迁移
`todo_tool.ts` 的 plain object schema 改为 TypeBox 格式,与其他 12 个工具保持一致。
## 7. 文件变更清单
| 操作 | 文件 | 说明 |
|------|------|------|
| 新建 | `tools/catalog.ts` | 轻量工具目录schema 注册 + 工具集查询) |
| 新建 | `tools/builtin/patch.ts` | patch 工具TypeBox schema + execute |
| 新建 | `tools/builtin/clarify.ts` | clarify 工具TypeBox schema + 降级 execute |
| 新建 | `utils/fuzzy_match.ts` | 9 级模糊匹配引擎 |
| 修改 | `tools/todo_tool.ts` | schema 迁移到 TypeBox + 注册到 catalog |
| 修改 | `tools/builtin/bash.ts` | 底部加 `registerSchema()` |
| 修改 | `tools/builtin/file.ts` | 底部加 `registerSchema()` |
| 修改 | `tools/builtin/memory.ts` | 底部加 `registerSchema()` |
| 修改 | `runtime/attempt.ts` | 新增 `onClarifyRequest` 回调 + clarify 分支 |
| 修改 | `runtime/prompt_guidance.ts` | TOOL_USE_ENFORCEMENT 改为动态函数 + 重写 todo + 新增 patch/clarify 策略 |
| 修改 | `runtime/prompt_builder.ts` | `collectAvailableToolNames` 从 catalog 查询Layer 2 调用 `getToolUseEnforcement(toolNames)` |
| 修改 | `gateway/protocol.ts` | 新增 clarify_request/clarify_response 事件 |
| 修改 | `gateway/server.ts` | defaultTools 加 patch/clarify模块级 clarifyResolvers + 响应处理 |
| 修改 | `service/agent_executor.ts` | defaultTools 加 patch不注入 clarify 回调) |
| 修改 | `service/crew_orchestrator.ts` | builtinTools 加 patch不加 clarifyCrew 无用户交互通道) |
## 8. 不做的事
- **不引入 Hermes 式 registry**Neta 工厂工具多registry 管不了运行时依赖,是过度设计
- **不引入 V4A 多文件补丁格式**Neta 场景不需要原子多文件操作
- **不修改前端代码**clarify UI 需前端配合,不在本次后端设计范围
- **不重构现有工具的导出方式**bash/file/memory 仍保持现有的 export 模式,只追加 catalog 注册
- **不新建 toolsets.ts**:工具集定义内联在 catalog.ts 中,不值得单独一个文件