Files
metona-ollama-desktop/src/renderer/services/completion-gate.ts
T
thzxx 2a9d8606ba feat: v0.14.0 生产级引擎+渲染全面优化
Engine 核心优化 (P0-P3):
- 智能工具重试: 永久错误(ENOENT/EACCES)立即返回, 瞬态错误指数退避
- 路径依赖检测: write→read/create→write 自动串行化, 避免并行竞态
- AbortController 集中生命周期管理, 防泄漏+竞态
- 上下文压缩: 200条硬上限 + 120条增量压缩, 防止 Ollama OOM
- 流式分级超时: 空闲30s + 总300s, 区分思考/卡死
- 工具缓存: default TTL 60s, 各工具独立TTL, 不再永久缓存
- 结果截断: 2000字智能截断, 优先JSON/换行边界保护
- Plan Mode 断点续传: 中止后自动恢复未完成计划
- 幻觉检测: 中英双语规则, agent-engine+completion-gate 统一共享
- Trace 缓冲批量写入 + 3次重试
- Agent Metrics JSON/Prometheus 双格式导出

渲染优化 (P0-P2):
- 流式纯文本追加, 流结束才 Markdown, 消除 O(n²) 性能瓶颈
- 终端增量 DOM appendChild, 替代全量 innerHTML 重建
- 消息 diff: Set<number> 索引替代 DOM querySelectorAll 计数
- ANSI 解析: 正则批量替换替代逐字遍历
- 终端截断保留系统消息行

Qwen3 兼容:
- system 消息始终唯一且位于首位
- SOUL.md 合并到统一 system prompt
- 中途注入 system→user, 避免触发热模板 400

上下文长度控制:
- 设置面板下拉(128K/256K/512K/1M), 默认128K
- 模型栏显示配置值, 下拉框显示模型自身ctx
- 全局硬编码 24576→131072

版本号 v0.13.8 → v0.14.0
README.md + 帮助面板全面更新
2026-06-28 12:54:56 +08:00

308 lines
12 KiB
TypeScript

/**
* Completion Gate — 完成门控模块
* Harness Engineering: 在 Agent 输出最终答案前进行结构化检查
*
* 设计理念:
* - 计算性反馈优先(规则驱动、毫秒级、100% 可靠)
* - 推理性反馈补充(AI 判断、秒级、非确定)
* - 所有检查项可独立开关
*/
import { logWarn, logInfo } from './log-service.js';
import { estimateTokens } from './context-manager.js';
import { HALLUCINATION_RULES } from './agent-engine.js';
import type { LoopContext, CompletionCheck } from '../types.js';
// ═══════════════════════════════════════════════════════════════
// 内置检查项
// ═══════════════════════════════════════════════════════════════
/** 内容质量检查:回复不应过短或不完整 */
const contentQualityCheck: CompletionCheck = {
name: 'contentQuality',
description: '检查回复长度和质量是否达到最低标准',
check: async (ctx: LoopContext) => {
if (ctx.content.length < 50) {
return { passed: false, reason: '回复内容过短(<50字),可能不完整。请给出更详细的最终回答。' };
}
return { passed: true, reason: '' };
},
};
/** 工具结果审查:是否存在未处理的工具错误 */
const toolResultReviewCheck: CompletionCheck = {
name: 'toolResultReview',
description: '检查是否存在未处理的工具执行错误',
check: async (ctx: LoopContext) => {
const errorRecords = ctx.allToolRecords.filter(r => r.status === 'error');
if (errorRecords.length === 0) return { passed: true, reason: '' };
// 检查这些错误是否在最近的上下文中被讨论过
const recentContent = ctx.messages.slice(-5).map(m => m.content || '').join(' ');
const unresolvedErrors = errorRecords.filter(r => {
const errorName = r.name;
return !recentContent.includes(errorName + ' 失败')
&& !recentContent.includes(errorName + ' error')
&& !recentContent.includes(errorName + ' 错误');
});
if (unresolvedErrors.length > 0) {
return {
passed: false,
reason: `存在 ${unresolvedErrors.length} 个未处理的工具错误: ${unresolvedErrors.map(r => r.name).join(', ')}。请说明这些错误的影响或提供替代方案。`
};
}
return { passed: true, reason: '' };
},
};
/** 思考阶段检测:回复不应仍在"思考中"状态 */
const notThinkingCheck: CompletionCheck = {
name: 'notThinking',
description: '检查回复是否已从思考阶段过渡到结论阶段',
check: async (ctx: LoopContext) => {
// 仅检查短回复(<200字)——长回复几乎肯定是完整回答
if (ctx.content.length > 200) return { passed: true, reason: '' };
const contentLower = ctx.content.slice(0, 100).toLowerCase();
const thinkingMarkers = ['让我看看', '观察一下', '先检查一下', '我需要先', '正在分析', '让我再'];
const continuationSignals = ctx.content.endsWith('...') || ctx.content.endsWith('等等') || ctx.content.endsWith('…');
const conclusionMarkers = [
/Final\s*Answer/i, /最终答案/, /最终回答/, /总结/, /任务完成/, /以上就是/,
/以上[是为]/, /我的能力/, /可以帮你/, /能够/, /我是/, /以下[是为]/,
];
const hasConclusion = conclusionMarkers.some(p => p.test(ctx.content));
if (hasConclusion) return { passed: true, reason: '' };
const thinkingCount = thinkingMarkers.filter(m => contentLower.includes(m)).length;
const isClearlyThinking = thinkingCount >= 2 || (thinkingCount >= 1 && continuationSignals);
if (isClearlyThinking) {
return {
passed: false,
reason: '回复看起来仍在思考阶段(短回复 + 思考动词 + 无结论标志),请基于已有结果给出明确的结论。'
};
}
return { passed: true, reason: '' };
},
};
/** 上下文效率检查:是否存在过度调用工具 */
const contextEfficiencyCheck: CompletionCheck = {
name: 'contextEfficiency',
description: '检查工具调用效率是否合理(避免过度调用)',
check: async (ctx: LoopContext) => {
const toolMessages = ctx.messages.filter(m => m.role === 'tool');
const nonToolMessages = ctx.messages.filter(m => m.role !== 'tool' && m.role !== 'system');
if (toolMessages.length > nonToolMessages.length * 3 && ctx.loopCount > 5) {
return {
passed: false,
reason: `工具调用过多(${toolMessages.length} 次 vs ${nonToolMessages.length} 次非工具消息),可能存在重复或低效的工具调用。请检查是否可以直接基于已有结果给出回答。`
};
}
return { passed: true, reason: '' };
},
};
/** 系统提示词注入检测:防止模型在回复中尝试注入 prompt */
const promptInjectionCheck: CompletionCheck = {
name: 'promptInjection',
description: '检测回复中是否包含疑似 prompt injection 内容',
check: async (ctx: LoopContext) => {
const injectionPatterns = [
/ignore\s+(all\s+)?(previous|above)\s+instructions/i,
/you\s+are\s+now\s+(a|an)\s+/i,
/new\s+system\s*prompt/i,
/forget\s+(all\s+)?(previous|above)/i,
/\[SOUL\.md\]/,
/\[AGENT\.md\]/,
];
for (const pattern of injectionPatterns) {
if (pattern.test(ctx.content)) {
return {
passed: false,
reason: '回复中包含疑似 prompt injection 内容(试图覆盖系统提示词)。请移除相关内容后重新回答。'
};
}
}
return { passed: true, reason: '' };
},
};
/** 工具幻觉检测:模型声称完成了某操作但该工具从未被调用 */
const toolHallucinationCheck: CompletionCheck = {
name: 'toolHallucination',
description: '检测模型是否编造了工具调用结果',
check: async (ctx: LoopContext) => {
const calledTools = new Set(ctx.allToolRecords.map(r => r.name));
const content = ctx.content;
for (const rule of HALLUCINATION_RULES) {
for (const pattern of rule.patterns) {
if (pattern.test(content)) {
const anyCalled = rule.requiredTools.some(t => calledTools.has(t));
if (!anyCalled) {
if (rule.label.includes('读取') && calledTools.has('write_file')) {
return { passed: true, reason: '' };
}
const toolList = rule.requiredTools.join(' / ');
return {
passed: false,
reason: `回复声称进行了"${rule.label}"操作,但 ${toolList} 工具从未被调用。请实际调用 ${toolList} 来完成任务,不要编造结果。`
};
}
break;
}
}
}
return { passed: true, reason: '' };
},
};
// ═══════════════════════════════════════════════════════════════
// 检查项注册与管理
// ═══════════════════════════════════════════════════════════════
/** 默认启用的检查项 */
const DEFAULT_ENABLED_CHECKS = new Set([
'contentQuality',
'toolResultReview',
'notThinking',
'promptInjection',
'toolHallucination',
]);
/** 阻断级检查:不通过时强制模型重新回答 */
const CRITICAL_CHECKS = new Set([
'toolHallucination',
'promptInjection',
]);
/** 注册表 */
const checkRegistry = new Map<string, CompletionCheck>([
['contentQuality', contentQualityCheck],
['toolResultReview', toolResultReviewCheck],
['notThinking', notThinkingCheck],
['contextEfficiency', contextEfficiencyCheck],
['promptInjection', promptInjectionCheck],
['toolHallucination', toolHallucinationCheck],
]);
/** 已启用的检查项 */
const enabledChecks = new Set(DEFAULT_ENABLED_CHECKS);
/** 注册自定义检查项 */
export function registerCompletionCheck(check: CompletionCheck): void {
checkRegistry.set(check.name, check);
logInfo(`Completion Gate: 注册检查项 "${check.name}"`);
}
/** 移除检查项 */
export function unregisterCompletionCheck(name: string): void {
checkRegistry.delete(name);
enabledChecks.delete(name);
}
/** 启用/禁用检查项 */
export function setCheckEnabled(name: string, enabled: boolean): void {
if (enabled) {
if (checkRegistry.has(name)) enabledChecks.add(name);
} else {
enabledChecks.delete(name);
}
}
/** 获取所有已注册检查项 */
export function getRegisteredChecks(): CompletionCheck[] {
return Array.from(checkRegistry.values());
}
/** 获取已启用的检查项 */
export function getEnabledChecks(): CompletionCheck[] {
return getRegisteredChecks().filter(c => enabledChecks.has(c.name));
}
// ═══════════════════════════════════════════════════════════════
// 执行门控
// ═══════════════════════════════════════════════════════════════
export interface GateResult {
passed: boolean;
reason: string;
/** 是否为阻断级失败(必须重新回答) */
critical: boolean;
/** 各项检查的详细结果 */
details: Array<{ name: string; passed: boolean; reason: string; durationMs: number }>;
}
/**
* 运行完成门控检查
*/
export async function runCompletionGate(ctx: LoopContext): Promise<GateResult> {
const checks = getEnabledChecks();
if (checks.length === 0) {
return { passed: true, reason: '', critical: false, details: [] };
}
const details: GateResult['details'] = [];
// 关键检查优先(幻觉/注入),咨询检查在后
const sorted = [...checks].sort((a, b) => {
const aCrit = CRITICAL_CHECKS.has(a.name) ? 0 : 1;
const bCrit = CRITICAL_CHECKS.has(b.name) ? 0 : 1;
return aCrit - bCrit;
});
for (const check of sorted) {
const start = Date.now();
try {
const result = await check.check(ctx);
details.push({
name: check.name,
passed: result.passed,
reason: result.reason,
durationMs: Date.now() - start,
});
if (!result.passed) {
const isCritical = CRITICAL_CHECKS.has(check.name);
logWarn(`Completion Gate: "${check.name}" ${isCritical ? '🔴阻断' : '🟡咨询'}${result.reason}`);
return {
passed: false,
reason: result.reason,
critical: isCritical,
details,
};
}
} catch (err) {
details.push({
name: check.name,
passed: true,
reason: `检查异常: ${(err as Error).message}`,
durationMs: Date.now() - start,
});
}
}
return { passed: true, reason: '', critical: false, details };
}
/**
* 生成门控报告(供日志/调试使用)
*/
export function formatGateReport(result: GateResult): string {
const status = result.passed ? '✅ 通过' : '❌ 未通过';
let report = `Completion Gate ${status}`;
if (!result.passed) {
report += ` — ${result.reason}`;
}
report += `\n${'─'.repeat(40)}`;
for (const d of result.details) {
const icon = d.passed ? '✅' : '❌';
report += `\n${icon} ${d.name}: ${d.reason || '通过'} (${d.durationMs}ms)`;
}
return report;
}