feat(context): 优化对话压缩系统

- 添加独立摘要模型配置支持(SUMMARY_PROVIDER/MODEL/API_KEY/BASE_URL)
- 添加 CompressionStatus 枚举和 DetailedCompressionResult 详细返回类型
- 实现压缩失败检测(空摘要、token膨胀)
- 添加首条 user-assistant 对保护,确保上下文连贯性
- CompressionManager 支持独立摘要模型(优先使用小模型降低成本)
- Agent 自动压缩时显示详细状态信息
- 更新相关测试用例
This commit is contained in:
2025-12-13 11:13:20 +08:00
parent 9ff2934089
commit f54f24b079
10 changed files with 495 additions and 102 deletions
@@ -178,7 +178,8 @@ describe('simpleCompact - 简单压缩', () => {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
}
const result = simpleCompact(messages, testConfig);
// 禁用首条保护以便测试摘要消息在第一位
const result = simpleCompact(messages, testConfig, { protectFirstPair: false });
if (result.freedTokens > 0) {
// 第一条消息应该是摘要
@@ -192,7 +193,8 @@ describe('simpleCompact - 简单压缩', () => {
messages.push(createUserMessage(`Message ${i}: ${'a'.repeat(100)}`));
}
const result = simpleCompact(messages, testConfig);
// 禁用首条保护以便测试摘要消息
const result = simpleCompact(messages, testConfig, { protectFirstPair: false });
if (result.freedTokens > 0) {
const summaryContent = result.messages[0].content as string;
@@ -1,5 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { ModelMessage, LanguageModel } from 'ai';
import { CompressionStatus } from '../../../src/context/types.js';
// Mock prune module
const mockPrune = vi.fn();
@@ -45,13 +46,13 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
vi.clearAllMocks();
manager = new CompressionManager();
// 默认 mock 返回值
// 默认 mock 返回值 - 使用 DetailedCompressionResult
mockEstimateMessages.mockReturnValue(1000);
mockFormat.mockReturnValue('1K');
mockPrune.mockReturnValue({ messages: [], freedTokens: 0 });
mockFilterCompacted.mockImplementation((msgs) => msgs);
mockCompact.mockResolvedValue({ messages: [], freedTokens: 0 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 0 });
mockCompact.mockResolvedValue({ messages: [], freedTokens: 0, type: 'none', status: CompressionStatus.NOOP });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 0, type: 'none', status: CompressionStatus.NOOP });
mockIsSummaryMessage.mockReturnValue(false);
});
@@ -174,7 +175,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
it('有模型时使用 AI 压缩', async () => {
const mockModel = {} as LanguageModel;
manager.setModel(mockModel);
mockCompact.mockResolvedValue({ messages: [], freedTokens: 2000 });
mockCompact.mockResolvedValue({ messages: [], freedTokens: 2000, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(5);
const result = await manager.compact(messages);
@@ -184,7 +185,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
});
it('无模型时使用简单压缩', async () => {
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 500 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 500, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(5);
const result = await manager.compact(messages);
@@ -196,7 +197,9 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
describe('compress - 自动压缩', () => {
it('先 prune 后不需要 compact', async () => {
mockEstimateMessages.mockReturnValue(10000); // 低于阈值
mockEstimateMessages
.mockReturnValueOnce(150000) // shouldCompress check - 高于阈值
.mockReturnValueOnce(10000); // 第二次 shouldCompress check - prune 后低于阈值
mockPrune.mockReturnValue({ messages: [], freedTokens: 500 });
const messages = createMessages(5);
@@ -210,7 +213,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
it('prune 后仍需 compact', async () => {
mockEstimateMessages.mockReturnValue(150000); // 高于阈值
mockPrune.mockReturnValue({ messages: createMessages(3), freedTokens: 500 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(5);
const result = await manager.compress(messages);
@@ -222,7 +225,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
it('只 compact 时类型为 compaction', async () => {
mockEstimateMessages.mockReturnValue(150000);
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 0 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000 });
mockSimpleCompact.mockReturnValue({ messages: [], freedTokens: 1000, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(5);
const result = await manager.compress(messages);
@@ -243,7 +246,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
it('足够消息时执行压缩', async () => {
mockEstimateMessages.mockReturnValue(10000);
mockPrune.mockReturnValue({ messages: createMessages(3), freedTokens: 500 });
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 300 });
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 300, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(10);
const result = await manager.forceCompress(messages);
@@ -256,7 +259,7 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
manager.setModel(mockModel);
mockEstimateMessages.mockReturnValue(10000);
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 500 });
mockCompact.mockResolvedValue({ messages: createMessages(2), freedTokens: 1000 });
mockCompact.mockResolvedValue({ messages: createMessages(2), freedTokens: 1000, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(10);
await manager.forceCompress(messages);
@@ -269,8 +272,9 @@ describe('CompressionManager - 压缩管理器扩展测试', () => {
manager.setModel(mockModel);
mockEstimateMessages.mockReturnValue(10000);
mockPrune.mockReturnValue({ messages: createMessages(5), freedTokens: 500 });
mockCompact.mockRejectedValue(new Error('AI error'));
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 800 });
// 使用 FAILED 状态而不是 reject
mockCompact.mockResolvedValue({ messages: createMessages(5), freedTokens: 0, type: 'none', status: CompressionStatus.FAILED_ERROR });
mockSimpleCompact.mockReturnValue({ messages: createMessages(2), freedTokens: 800, type: 'compaction', status: CompressionStatus.SUCCESS });
const messages = createMessages(10);
const result = await manager.forceCompress(messages);
@@ -213,7 +213,8 @@ describe('CompressionManager - 压缩管理器', () => {
const result = await manager.compress(messages);
expect(['prune', 'compaction', 'both']).toContain(result.type);
// 小对话不压缩时返回 'none'
expect(['prune', 'compaction', 'both', 'none']).toContain(result.type);
});
});