feat: 预算警告 & Shadowing 防护 & 记忆容量管理 (v5.1.2)

- 预算警告临时层:Agent Loop 剩余 ≤5 轮注入 WARNING,≤2 轮注入 CRITICAL
- MCP Shadowing 防护:MCP 工具注册时检测重名,自动跳过并警告
- 记忆容量限制:上限 500 条,超限自动清理低价值条目(rule 受保护)
- 版本号更新至 5.1.2
This commit is contained in:
Metona Build
2026-04-19 18:10:26 +08:00
parent bea52bf1b7
commit 71bd6e19ea
10 changed files with 114 additions and 10 deletions
+20
View File
@@ -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() : '';
+48
View File
@@ -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;
}
// ── 安全扫描 ──
/** 记忆内容安全扫描 */
+14 -3
View File
@@ -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);