feat: 预算警告 & Shadowing 防护 & 记忆容量管理 (v5.1.2)
- 预算警告临时层:Agent Loop 剩余 ≤5 轮注入 WARNING,≤2 轮注入 CRITICAL - MCP Shadowing 防护:MCP 工具注册时检测重名,自动跳过并警告 - 记忆容量限制:上限 500 条,超限自动清理低价值条目(rule 受保护) - 版本号更新至 5.1.2
This commit is contained in:
@@ -529,6 +529,17 @@ export async function runAgentLoop(
|
||||
const toolCalls: ToolCall[] = [];
|
||||
allCallsThisLoop = [];
|
||||
|
||||
// v5.1.2 预算警告:接近迭代上限时注入临时提示
|
||||
let budgetWarningIdx = -1;
|
||||
const remaining = maxLoops - loopCount + 1;
|
||||
if (remaining <= 5 && remaining > 0) {
|
||||
const warning = remaining <= 2
|
||||
? `\n⚠️ CRITICAL: You have only ${remaining} iteration(s) left. Stop using tools and provide your final answer NOW.`
|
||||
: `\n⚠️ WARNING: You have approximately ${remaining} iterations remaining. Start wrapping up and prepare your final answer.`;
|
||||
messages.push({ role: 'system', content: warning });
|
||||
budgetWarningIdx = messages.length - 1;
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
state.set(KEYS.ABORT_CONTROLLER, abortController);
|
||||
|
||||
@@ -587,6 +598,10 @@ export async function runAgentLoop(
|
||||
return;
|
||||
}
|
||||
logError('流式调用异常', (err as Error).message);
|
||||
// 清理预算警告临时消息
|
||||
if (budgetWarningIdx >= 0) {
|
||||
messages.splice(budgetWarningIdx, 1);
|
||||
}
|
||||
if (content || thinking) {
|
||||
messages.push({ role: 'assistant', content: content || '(模型响应异常)', ...(thinking && { thinking }) });
|
||||
}
|
||||
@@ -594,6 +609,11 @@ export async function runAgentLoop(
|
||||
return;
|
||||
}
|
||||
|
||||
// 清理预算警告临时消息
|
||||
if (budgetWarningIdx >= 0) {
|
||||
messages.splice(budgetWarningIdx, 1);
|
||||
}
|
||||
|
||||
// 提取 ReAct 思考过程
|
||||
const thoughtMatch = content.match(/\*\*Thought:\*\*\s*([\s\S]*?)(?=\*\*Action:\*\*|\*\*Final Answer:\*\*|$)/i);
|
||||
const thought = thoughtMatch ? thoughtMatch[1].trim() : '';
|
||||
|
||||
@@ -255,6 +255,15 @@ export async function addMemory(data: {
|
||||
throw new Error(`记忆内容被安全规则拦截: ${securityCheck.reason}`);
|
||||
}
|
||||
|
||||
// v5.1.2 记忆容量限制:超过上限自动清理低价值条目
|
||||
const MAX_MEMORIES = 500;
|
||||
if (memoryCache.length >= MAX_MEMORIES) {
|
||||
const evicted = autoCleanMemories();
|
||||
if (evicted > 0) {
|
||||
logInfo('记忆容量清理', `已清理 ${evicted} 条低价值记忆(上限 ${MAX_MEMORIES})`);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查重复(内容相似度 > 80% 则跳过)
|
||||
const existing = memoryCache.find(e => {
|
||||
if (e.type !== data.type) return false;
|
||||
@@ -300,6 +309,45 @@ export async function addMemory(data: {
|
||||
return entry;
|
||||
}
|
||||
|
||||
// ── 容量管理 ──
|
||||
|
||||
/**
|
||||
* 自动清理低价值记忆条目
|
||||
* 策略:按综合评分排序,清理后 20% 的条目
|
||||
* 评分 = importance * 2 + useCount * 3 + recencyBonus(最近 7 天使用过 +5)
|
||||
* rule 类型记忆受保护不被清理
|
||||
* @returns 清理的条目数
|
||||
*/
|
||||
function autoCleanMemories(): number {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
const now = Date.now();
|
||||
const SEVEN_DAYS = 7 * 24 * 3600 * 1000;
|
||||
|
||||
// 计算综合评分
|
||||
const scored = memoryCache
|
||||
.map((entry, idx) => {
|
||||
const recencyBonus = (now - entry.lastUsedAt) < SEVEN_DAYS ? 5 : 0;
|
||||
const score = entry.importance * 2 + entry.useCount * 3 + recencyBonus;
|
||||
return { entry, idx, score };
|
||||
})
|
||||
// rule 类型受保护
|
||||
.filter(s => s.entry.type !== 'rule')
|
||||
.sort((a, b) => a.score - b.score);
|
||||
|
||||
const evictCount = Math.max(1, Math.floor(memoryCache.length * 0.2));
|
||||
const toEvict = scored.slice(0, evictCount);
|
||||
|
||||
// 从缓存和数据库中移除
|
||||
const evictIds = new Set(toEvict.map(s => s.entry.id));
|
||||
for (const id of evictIds) {
|
||||
if (db) db.deleteMemory(id);
|
||||
}
|
||||
memoryCache = memoryCache.filter(e => !evictIds.has(e.id));
|
||||
state.set('memoryEntries', [...memoryCache]);
|
||||
|
||||
return evictIds.size;
|
||||
}
|
||||
|
||||
// ── 安全扫描 ──
|
||||
|
||||
/** 记忆内容安全扫描 */
|
||||
|
||||
@@ -641,9 +641,20 @@ let _mcpToolCache: ToolDefinition[] = [];
|
||||
/** 刷新 MCP 工具缓存(在 MCP 服务器配置变更时调用) */
|
||||
export async function refreshMCPTools(): Promise<void> {
|
||||
try {
|
||||
_mcpToolCache = await getMCPToolDefinitions();
|
||||
if (_mcpToolCache.length > 0) {
|
||||
logInfo(`MCP 工具已注册: ${_mcpToolCache.length} 个`, _mcpToolCache.map(t => t.function.name).join(', '));
|
||||
const rawTools = await getMCPToolDefinitions();
|
||||
// v5.1.2 Shadowing 防护:MCP 工具不能覆盖内置工具
|
||||
const builtInNames = new Set(TOOL_DEFINITIONS.map(d => d.function.name));
|
||||
const filtered = rawTools.filter(t => {
|
||||
if (builtInNames.has(t.function.name)) {
|
||||
logWarn(`MCP 工具 "${t.function.name}" 与内置工具重名,已跳过`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const skipped = rawTools.length - filtered.length;
|
||||
_mcpToolCache = filtered;
|
||||
if (filtered.length > 0) {
|
||||
logInfo(`MCP 工具已注册: ${filtered.length} 个${skipped > 0 ? `(${skipped} 个因重名被跳过)` : ''}`, filtered.map(t => t.function.name).join(', '));
|
||||
}
|
||||
} catch (err) {
|
||||
logError('MCP 工具刷新失败', (err as Error).message);
|
||||
|
||||
Reference in New Issue
Block a user