/**
* LLM 总结服务
*/
import axios from 'axios';
import type { SummaryRequest, SummaryResponse } from '../types/index.js';
import { logger } from '../utils/logger.js';
export interface SummarizerConfig {
apiKey: string;
baseUrl: string;
model: string;
maxTokens: number;
timeout?: number;
maxRetries?: number;
}
import {
DEFAULT_SUMMARY_PROMPT,
SEARCH_SUMMARY_PROMPT
} from '../prompts/summary/index.js';
// 重新导出以保持向后兼容
export { SEARCH_SUMMARY_PROMPT };
export class SummarizerService {
private config: SummarizerConfig;
constructor(config: SummarizerConfig) {
this.config = {
timeout: 120000, // 默认 120 秒超时
maxRetries: 2, // 默认重试 2 次
...config,
};
}
async summarize(request: SummaryRequest): Promise<SummaryResponse> {
const { content, prompt, maxTokens } = request;
const maxRetries = this.config.maxRetries || 2;
const systemPrompt = prompt || DEFAULT_SUMMARY_PROMPT;
const userContent = systemPrompt.replace('{content}', content);
logger.info('Starting LLM summarization', {
contentLength: content.length,
model: this.config.model,
maxRetries,
});
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
try {
const response = await axios.post<{
choices: Array<{
message: {
content: string;
};
}>;
usage?: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}>(
`${this.config.baseUrl}/chat/completions`,
{
model: this.config.model,
messages: [
{
role: 'system',
content: '你是一个专业的内容总结助手,擅长提取和整合网页信息。',
},
{
role: 'user',
content: userContent,
},
],
max_tokens: maxTokens || this.config.maxTokens,
temperature: 0.3,
},
{
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiKey}`,
},
}
);
const summary = response.data.choices[0]?.message?.content || '';
const usage = response.data.usage;
logger.info('LLM summarization completed', {
summaryLength: summary.length,
tokenUsage: usage?.total_tokens,
attempt,
});
return {
summary,
tokenUsage: usage
? {
prompt: usage.prompt_tokens,
completion: usage.completion_tokens,
total: usage.total_tokens,
}
: undefined,
};
} catch (error) {
lastError = error as Error;
if (axios.isAxiosError(error)) {
const statusCode = error.response?.status;
const errorMessage = error.response?.data?.error?.message || error.message;
logger.warn('LLM summarization attempt failed', {
attempt,
totalAttempts: maxRetries + 1,
statusCode,
errorMessage,
errorType: error.code,
});
// 对于某些错误,不应该重试
if (statusCode === 401 || statusCode === 403) {
// 认证错误,重试无意义
throw new Error(`LLM summarization failed: Authentication error (${statusCode})`);
}
if (statusCode === 400) {
// 请求格式错误,重试无意义
throw new Error(`LLM summarization failed: Bad request (${statusCode}): ${errorMessage}`);
}
// 对于 aborted 错误或网络错误,重试
if (error.code === 'ECONNABORTED' || error.code === 'ECONNRESET' ||
error.message.includes('aborted') || error.message.includes('timeout')) {
if (attempt <= maxRetries) {
logger.info('Retrying LLM summarization due to network/timeout error', {
attempt,
maxRetries,
waitTime: attempt * 2000, // 指数退避
});
// 指数退避等待
await this.sleep(attempt * 2000);
continue;
}
}
}
// 非网络错误或已达到最大重试次数
if (attempt > maxRetries) {
const message = axios.isAxiosError(error)
? error.response?.data?.error?.message || error.message
: (error as Error).message;
logger.error('LLM summarization failed after all retries', {
totalAttempts: maxRetries + 1,
finalError: message,
});
throw new Error(`LLM summarization failed: ${message}`);
}
}
}
// 理论上不应该到达这里
throw lastError || new Error('LLM summarization failed: Unknown error');
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async summarizeSearchResults(
results: Array<{ title: string; url: string; content: string }>
): Promise<SummaryResponse> {
// 格式化搜索结果,为每个来源添加清晰的标识
const formattedContent = results
.map((r, i) => `【来源${i + 1}】${r.title}\n链接:${r.url}\n内容:${r.content}`)
.join('\n\n---\n\n');
return this.summarize({
content: formattedContent,
prompt: SEARCH_SUMMARY_PROMPT,
});
}
}