feat: v1.1.0 — 上下文引擎重构 + 技能系统升级 + SOUL.md 支持

- 版本号更新: 1.0.0 → 1.1.0,全量同步 package.json/lock/README/docs/UI
- 上下文长度自动检测: 切换模型时从 model_info 读取实际 context_length
- SOUL.md 支持: 每次发送自动扫描工作空间 SOUL.md,注入为首条 system 消息且不可压缩
- 系统提示词卡片: AI 回复顶部可折叠展示实际发送给模型的完整 system prompt
- Token 校准: 利用 Ollama 返回的实际计数动态修正估算器(EMA)
- 消息重要性评分: 纯规则打分,trim/summarize 按重要性而非时间顺序
- 结构化压缩: LLM 压缩输出 JSON 格式(topics/decisions/pendingTasks/constraints)
- 技能系统 v1.1: 语义匹配(embedding) + 参数自优化 + 未使用衰减 + 技能链合并
- 修复: 100+ TypeScript strict 模式编译错误清零
This commit is contained in:
thzxx
2026-06-04 21:47:18 +08:00
parent b12837433b
commit db6cb8d2cc
27 changed files with 646 additions and 139 deletions
+45 -9
View File
@@ -18,14 +18,15 @@ import { showToast } from '../components/toast.js';
import { logInfo, logWarn, logSuccess, logError, logToolStart, logToolResult, logAgentLoop, logModelResponse } from './log-service.js';
import { getWorkspaceDirPath } from '../components/workspace-panel.js';
import { generateId } from '../utils/utils.js';
import { buildContext, estimateTokens, shouldAutoCompress, compressWithLLM, AUTO_COMPRESS_THRESHOLD } from './context-manager.js';
import { buildContext, estimateTokens, shouldAutoCompress, compressWithLLM, AUTO_COMPRESS_THRESHOLD, recordActualTokens } from './context-manager.js';
import type {
OllamaMessage,
OllamaStreamChunk,
ToolCall,
ToolResult,
ToolCallRecord,
TraceEntry
TraceEntry,
ChatSession
} from '../types.js';
const MAX_RETRIES = 2; // 工具错误自动重试次数
@@ -173,7 +174,7 @@ async function inferUserProfile(userMsg: string, assistantMsg: string): Promise<
const bridge = window.metonaDesktop;
if (!bridge?.db?.getSetting) return;
const profile: Record<string, unknown> = await bridge.db.getSetting('user_profile', {}) || {};
const profile: Record<string, any> = await bridge.db.getSetting('user_profile', {}) || {};
// 技术栈检测
const techPatterns: Record<string, RegExp> = {
@@ -363,7 +364,7 @@ export interface AgentCallbacks {
}
/** 保存执行轨迹到 SQLite */
async function saveTrace(trace: Omit<TraceEntry, 'id' | 'createdAt'>): Promise<void> {
async function saveTrace(trace: Record<string, any>): Promise<void> {
try {
const bridge = window.metonaDesktop;
if (!bridge?.db) return;
@@ -390,7 +391,7 @@ export async function runAgentLoop(
): Promise<void> {
const api = state.get<OllamaAPI>(KEYS.API);
const model = state.get<string>('_defaultModel', '');
const currentSession = state.get(KEYS.CURRENT_SESSION);
const currentSession = state.get<ChatSession | null>(KEYS.CURRENT_SESSION);
const sessionId = currentSession?.id || 'unknown';
if (!api || !model) {
@@ -411,6 +412,24 @@ export async function runAgentLoop(
const messages: OllamaMessage[] = [];
let systemPromptParts: string[] = [];
// ── 扫描工作空间 SOUL.md ──
const workspaceDir = getWorkspaceDirPath();
if (workspaceDir) {
try {
const soulResult = await window.metonaDesktop?.workspace.readFile(
workspaceDir.replace(/\/+$/, '') + '/SOUL.md'
);
if (soulResult?.success && soulResult.content) {
// SOUL.md 注入为独立 system 消息,标记为不可压缩
messages.push({
role: 'system',
content: `[SOUL.md] ${soulResult.content}`,
});
logInfo('SOUL.md 已注入系统提示词', `${soulResult.lines || 0}`);
}
} catch { /* SOUL.md 不存在或读取失败,静默跳过 */ }
}
// 注入记忆上下文
if (isMemoryEnabled() && userContent) {
const relevantMemories = searchMemories(userContent, 6);
@@ -423,7 +442,6 @@ export async function runAgentLoop(
}
// 注入工作空间上下文
const workspaceDir = getWorkspaceDirPath();
if (workspaceDir) {
systemPromptParts.push(`【工作空间】
当前工作空间目录: ${workspaceDir}
@@ -447,7 +465,7 @@ export async function runAgentLoop(
const db = state.get<ChatDB | null>(KEYS.DB);
if (db) {
try {
const userProfile = await db.getSetting('user_profile', null);
const userProfile = await db.getSetting('user_profile', null) as Record<string, any> | null;
if (userProfile && typeof userProfile === 'object') {
const parts: string[] = [];
if (userProfile.tech_stack?.length) parts.push(`技术栈: ${userProfile.tech_stack.join(', ')}`);
@@ -475,6 +493,9 @@ export async function runAgentLoop(
messages.push({ role: 'system', content: fullSystemPrompt });
// 将完整系统提示词存入 state,供 input-area 在助理消息上展示
state.set('_lastSystemPrompt', fullSystemPrompt);
// 添加历史消息
for (const msg of historyMessages) {
messages.push({
@@ -503,7 +524,7 @@ export async function runAgentLoop(
logInfo(`自动上下文压缩触发: tokens≈${estimateTokens(messages.map(m => m.content || '').join(''))} > ${Math.floor(numCtx * AUTO_COMPRESS_THRESHOLD)} (50% of ${numCtx})`);
const compressAC = state.get<AbortController | null>(KEYS.ABORT_CONTROLLER) || new AbortController();
try {
const compressed = await compressWithLLM(messages, api, model, { abortController: compressAC });
const compressed = await compressWithLLM(messages, api as any, model, { abortController: compressAC });
if (compressed.length < messages.length || estimateTokens(compressed.map(m => m.content || '').join('')) < estimateTokens(messages.map(m => m.content || '').join(''))) {
messages.length = 0;
messages.push(...compressed);
@@ -633,10 +654,25 @@ export async function runAgentLoop(
totalPromptEvalCount += loopPromptEvalCount;
totalInferenceNs += loopInferenceNs;
state.set('_currentEvalCount', totalEvalCount);
// Token 校准:用 Ollama 返回的实际计数修正估算器(在重置前保存本轮值)
const thisLoopEval = loopEvalCount;
const thisLoopPrompt = loopPromptEvalCount;
// 重置本轮计数器(下一轮重新从 chunk 收集)
loopEvalCount = 0;
loopPromptEvalCount = 0;
loopInferenceNs = 0;
// Token 校准:用 Ollama 返回的实际计数自动修正估算器
try {
if (thisLoopEval > 0 || thisLoopPrompt > 0) {
const estimatedThisLoop = estimateTokens(messages.map(m => m.content || '').join(''));
if (estimatedThisLoop > 0) {
recordActualTokens(thisLoopPrompt, thisLoopEval, estimatedThisLoop);
}
}
} catch { /* 校准失败不阻塞主流程 */ }
} catch (err) {
if (abortController.signal.aborted) {
logInfo('流式调用已中止');
@@ -722,7 +758,7 @@ export async function runAgentLoop(
// v4.2 自动提取技能
if (allToolRecords.length >= 2) {
try {
await extractSkillsFromToolRecords(allToolRecords, userContent, currentSession?.title || '');
await extractSkillsFromToolRecords(allToolRecords, userContent, (currentSession as ChatSession)?.title || '');
} catch { /* 不阻塞 */ }
}