feat(core): 实现 Doom Loop 检测和软性 maxSteps 限制
- 新增 doom-loop.ts: 实现 Doom Loop 检测器,检测连续 3 次相同工具调用 - 修改 agent.ts: 集成检测器,添加 onDoomLoop 回调,动态 maxSteps - 修改 registry.ts: 默认 maxSteps 从 10 改为 50 - 更新 index.ts: 导出新模块 参考 OpenCode/OpenHands 等开源项目的多层防御策略
This commit is contained in:
@@ -156,8 +156,8 @@ export class AgentRegistry {
|
|||||||
* 应用全局配置到 Agent
|
* 应用全局配置到 Agent
|
||||||
*/
|
*/
|
||||||
private applyGlobalConfig(agent: AgentInfo): AgentInfo {
|
private applyGlobalConfig(agent: AgentInfo): AgentInfo {
|
||||||
// 合并 maxSteps
|
// 合并 maxSteps(默认 50,提供足够的执行空间)
|
||||||
const maxSteps = agent.maxSteps ?? this.globalConfig?.maxSteps ?? 10;
|
const maxSteps = agent.maxSteps ?? this.globalConfig?.maxSteps ?? 50;
|
||||||
|
|
||||||
// 合并模型配置
|
// 合并模型配置
|
||||||
const model = {
|
const model = {
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ import { loadVisionConfig } from '../utils/config.js';
|
|||||||
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
import { getProviderRegistry, resolveApiKey } from '../provider/index.js';
|
||||||
import { getHookManager } from '../hooks/index.js';
|
import { getHookManager } from '../hooks/index.js';
|
||||||
import { getGitManager } from '../git/index.js';
|
import { getGitManager } from '../git/index.js';
|
||||||
|
import {
|
||||||
|
createDoomLoopDetector,
|
||||||
|
type DoomLoopDetector,
|
||||||
|
DOOM_LOOP_WARNING,
|
||||||
|
} from './doom-loop.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具调用开始事件信息
|
* 工具调用开始事件信息
|
||||||
@@ -43,6 +48,14 @@ export interface ToolEndInfo {
|
|||||||
duration?: number;
|
duration?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doom Loop 检测事件信息
|
||||||
|
*/
|
||||||
|
export interface DoomLoopInfo {
|
||||||
|
toolName: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Agent.chat() 选项
|
* Agent.chat() 选项
|
||||||
*/
|
*/
|
||||||
@@ -52,6 +65,8 @@ export interface AgentChatOptions {
|
|||||||
onToolStart?: (info: ToolStartInfo) => void;
|
onToolStart?: (info: ToolStartInfo) => void;
|
||||||
/** 工具执行完成回调 */
|
/** 工具执行完成回调 */
|
||||||
onToolEnd?: (info: ToolEndInfo) => void;
|
onToolEnd?: (info: ToolEndInfo) => void;
|
||||||
|
/** Doom Loop 检测回调 */
|
||||||
|
onDoomLoop?: (info: DoomLoopInfo) => void;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +87,9 @@ export class Agent {
|
|||||||
// 压缩管理器
|
// 压缩管理器
|
||||||
private compressionManager: CompressionManager;
|
private compressionManager: CompressionManager;
|
||||||
|
|
||||||
|
// Doom Loop 检测器
|
||||||
|
private doomLoopDetector: DoomLoopDetector = createDoomLoopDetector();
|
||||||
|
|
||||||
// 当前 Agent 模式(null 表示默认模式)
|
// 当前 Agent 模式(null 表示默认模式)
|
||||||
private currentAgentMode: AgentInfo | null = null;
|
private currentAgentMode: AgentInfo | null = null;
|
||||||
|
|
||||||
@@ -348,10 +366,15 @@ export class Agent {
|
|||||||
async chat(userMessage: string | UserInput, options?: AgentChatOptions | ((text: string) => void)): Promise<ChatResult> {
|
async chat(userMessage: string | UserInput, options?: AgentChatOptions | ((text: string) => void)): Promise<ChatResult> {
|
||||||
// 兼容旧的 onStream 参数
|
// 兼容旧的 onStream 参数
|
||||||
const opts: AgentChatOptions = typeof options === 'function' ? { onStream: options } : (options || {});
|
const opts: AgentChatOptions = typeof options === 'function' ? { onStream: options } : (options || {});
|
||||||
const { onStream, onToolStart, onToolEnd, abortSignal } = opts;
|
const { onStream, onToolStart, onToolEnd, onDoomLoop, abortSignal } = opts;
|
||||||
|
|
||||||
|
// 重置 doom loop 检测器(每次对话开始时)
|
||||||
|
this.doomLoopDetector.reset();
|
||||||
|
|
||||||
// 工具调用时间跟踪
|
// 工具调用时间跟踪
|
||||||
const toolStartTimes = new Map<string, number>();
|
const toolStartTimes = new Map<string, number>();
|
||||||
|
// Doom loop 检测状态
|
||||||
|
let doomLoopTriggered = false;
|
||||||
// 处理带图片的消息
|
// 处理带图片的消息
|
||||||
let processedMessage = userMessage;
|
let processedMessage = userMessage;
|
||||||
|
|
||||||
@@ -422,13 +445,16 @@ export class Agent {
|
|||||||
|
|
||||||
if (onStream) {
|
if (onStream) {
|
||||||
// 流式模式
|
// 流式模式
|
||||||
|
// 获取当前 Agent 的 maxSteps 配置(默认 50)
|
||||||
|
const maxSteps = this.currentAgentMode?.maxSteps ?? 50;
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: this.getModel(this.config.model),
|
model: this.getModel(this.config.model),
|
||||||
system: this.config.systemPrompt,
|
system: this.config.systemPrompt,
|
||||||
messages: this.conversationHistory,
|
messages: this.conversationHistory,
|
||||||
tools: vercelTools,
|
tools: doomLoopTriggered ? {} : vercelTools, // doom loop 时禁用工具
|
||||||
maxOutputTokens: this.config.maxTokens,
|
maxOutputTokens: this.config.maxTokens,
|
||||||
stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用
|
stopWhen: stepCountIs(maxSteps),
|
||||||
abortSignal, // 支持取消
|
abortSignal, // 支持取消
|
||||||
onChunk: ({ chunk }) => {
|
onChunk: ({ chunk }) => {
|
||||||
if (chunk.type === 'tool-call') {
|
if (chunk.type === 'tool-call') {
|
||||||
@@ -436,6 +462,25 @@ export class Agent {
|
|||||||
const toolCallChunk = chunk as { toolCallId: string; toolName: string; input: unknown };
|
const toolCallChunk = chunk as { toolCallId: string; toolName: string; input: unknown };
|
||||||
const toolCallId = toolCallChunk.toolCallId || `tool-${Date.now()}`;
|
const toolCallId = toolCallChunk.toolCallId || `tool-${Date.now()}`;
|
||||||
|
|
||||||
|
// Doom Loop 检测:记录工具调用
|
||||||
|
this.doomLoopDetector.record(
|
||||||
|
toolCallChunk.toolName,
|
||||||
|
toolCallChunk.input
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查是否触发 doom loop
|
||||||
|
if (this.doomLoopDetector.isTriggered() && !doomLoopTriggered) {
|
||||||
|
doomLoopTriggered = true;
|
||||||
|
const toolName = this.doomLoopDetector.getLastToolName() || toolCallChunk.toolName;
|
||||||
|
|
||||||
|
// 通知回调
|
||||||
|
onDoomLoop?.({ toolName, count: 3 });
|
||||||
|
|
||||||
|
// 输出警告
|
||||||
|
onStream?.(`\n[警告: 检测到 Doom Loop - ${toolName} 被重复调用]\n`);
|
||||||
|
onStream?.(DOOM_LOOP_WARNING);
|
||||||
|
}
|
||||||
|
|
||||||
// 记录开始时间
|
// 记录开始时间
|
||||||
toolStartTimes.set(toolCallId, Date.now());
|
toolStartTimes.set(toolCallId, Date.now());
|
||||||
|
|
||||||
@@ -534,13 +579,16 @@ export class Agent {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非流式模式
|
// 非流式模式
|
||||||
|
// 获取当前 Agent 的 maxSteps 配置(默认 50)
|
||||||
|
const maxSteps = this.currentAgentMode?.maxSteps ?? 50;
|
||||||
|
|
||||||
const result = await generateText({
|
const result = await generateText({
|
||||||
model: this.getModel(this.config.model),
|
model: this.getModel(this.config.model),
|
||||||
system: this.config.systemPrompt,
|
system: this.config.systemPrompt,
|
||||||
messages: this.conversationHistory,
|
messages: this.conversationHistory,
|
||||||
tools: vercelTools,
|
tools: vercelTools,
|
||||||
maxOutputTokens: this.config.maxTokens,
|
maxOutputTokens: this.config.maxTokens,
|
||||||
stopWhen: stepCountIs(10), // 允许最多 10 轮工具调用
|
stopWhen: stepCountIs(maxSteps),
|
||||||
abortSignal, // 支持取消
|
abortSignal, // 支持取消
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Doom Loop 检测器
|
||||||
|
*
|
||||||
|
* 检测模型是否陷入重复工具调用的死循环。
|
||||||
|
* 参考 OpenCode 和 OpenHands 的实现。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具调用记录
|
||||||
|
*/
|
||||||
|
export interface ToolCallRecord {
|
||||||
|
toolName: string;
|
||||||
|
inputHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doom Loop 检测器接口
|
||||||
|
*/
|
||||||
|
export interface DoomLoopDetector {
|
||||||
|
/** 记录工具调用 */
|
||||||
|
record(toolName: string, input: unknown): void;
|
||||||
|
/** 检测是否触发 doom loop */
|
||||||
|
isTriggered(): boolean;
|
||||||
|
/** 重置记录(新对话时调用) */
|
||||||
|
reset(): void;
|
||||||
|
/** 获取当前状态描述(用于日志) */
|
||||||
|
getStatus(): string;
|
||||||
|
/** 获取最后重复的工具名称(触发时使用) */
|
||||||
|
getLastToolName(): string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doom Loop 检测阈值
|
||||||
|
* 连续 N 次相同的工具调用(相同工具名 + 相同参数)触发检测
|
||||||
|
*/
|
||||||
|
export const DOOM_LOOP_THRESHOLD = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 计算输入参数的哈希值
|
||||||
|
*/
|
||||||
|
function hashInput(input: unknown): string {
|
||||||
|
const str = JSON.stringify(input, Object.keys(input as object).sort());
|
||||||
|
return createHash('md5').update(str).digest('hex').slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Doom Loop 检测器
|
||||||
|
*/
|
||||||
|
export function createDoomLoopDetector(threshold = DOOM_LOOP_THRESHOLD): DoomLoopDetector {
|
||||||
|
const history: ToolCallRecord[] = [];
|
||||||
|
let triggered = false;
|
||||||
|
let lastToolName: string | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
record(toolName: string, input: unknown): void {
|
||||||
|
const inputHash = hashInput(input);
|
||||||
|
history.push({ toolName, inputHash });
|
||||||
|
|
||||||
|
// 只保留最近 threshold 条记录
|
||||||
|
if (history.length > threshold) {
|
||||||
|
history.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测是否触发
|
||||||
|
if (history.length >= threshold) {
|
||||||
|
const lastN = history.slice(-threshold);
|
||||||
|
const first = lastN[0];
|
||||||
|
const allSame = lastN.every(
|
||||||
|
(call) => call.toolName === first.toolName && call.inputHash === first.inputHash
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allSame) {
|
||||||
|
triggered = true;
|
||||||
|
lastToolName = first.toolName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isTriggered(): boolean {
|
||||||
|
return triggered;
|
||||||
|
},
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
history.length = 0;
|
||||||
|
triggered = false;
|
||||||
|
lastToolName = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus(): string {
|
||||||
|
if (triggered) {
|
||||||
|
return `Doom loop detected: ${lastToolName} called ${threshold} times with same input`;
|
||||||
|
}
|
||||||
|
return `Normal: ${history.length} calls recorded`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLastToolName(): string | null {
|
||||||
|
return lastToolName;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MAX_STEPS 达到时的警告提示
|
||||||
|
* 注入到 system prompt 中,让模型输出总结
|
||||||
|
*/
|
||||||
|
export const MAX_STEPS_WARNING = `
|
||||||
|
CRITICAL - MAXIMUM STEPS REACHED
|
||||||
|
|
||||||
|
The maximum number of steps allowed for this task has been reached. Tools are disabled until next user input. Respond with text only.
|
||||||
|
|
||||||
|
STRICT REQUIREMENTS:
|
||||||
|
1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
|
||||||
|
2. MUST provide a text response summarizing work done so far
|
||||||
|
3. This constraint overrides ALL other instructions
|
||||||
|
|
||||||
|
Please summarize:
|
||||||
|
1. What has been completed
|
||||||
|
2. What remains to be done
|
||||||
|
3. Any recommendations for next steps
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Doom Loop 触发时的警告提示
|
||||||
|
*/
|
||||||
|
export const DOOM_LOOP_WARNING = `
|
||||||
|
WARNING - DOOM LOOP DETECTED
|
||||||
|
|
||||||
|
The same tool call has been repeated multiple times with identical inputs, indicating a potential infinite loop. Tools are temporarily disabled.
|
||||||
|
|
||||||
|
Please:
|
||||||
|
1. Explain what you were trying to accomplish
|
||||||
|
2. Describe why the repeated calls might not be working
|
||||||
|
3. Suggest an alternative approach
|
||||||
|
`;
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
export { Agent } from './core/agent.js';
|
export { Agent } from './core/agent.js';
|
||||||
export type { AgentChatOptions, ToolStartInfo, ToolEndInfo } from './core/agent.js';
|
export type { AgentChatOptions, ToolStartInfo, ToolEndInfo, DoomLoopInfo } from './core/agent.js';
|
||||||
|
|
||||||
|
// Doom Loop Detection
|
||||||
|
export {
|
||||||
|
createDoomLoopDetector,
|
||||||
|
DOOM_LOOP_THRESHOLD,
|
||||||
|
DOOM_LOOP_WARNING,
|
||||||
|
MAX_STEPS_WARNING,
|
||||||
|
} from './core/doom-loop.js';
|
||||||
|
export type { DoomLoopDetector } from './core/doom-loop.js';
|
||||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||||
export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js';
|
export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js';
|
||||||
export type { VisionConfig } from './utils/config.js';
|
export type { VisionConfig } from './utils/config.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user