docs: Day 12-13 completion summary and milestone update
This commit is contained in:
150
backend/src/adapters/DeepSeekAdapter.ts
Normal file
150
backend/src/adapters/DeepSeekAdapter.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import axios from 'axios';
|
||||
import { ILLMAdapter, Message, LLMOptions, LLMResponse, StreamChunk } from './types.js';
|
||||
import { config } from '../config/env.js';
|
||||
|
||||
export class DeepSeekAdapter implements ILLMAdapter {
|
||||
modelName: string;
|
||||
private apiKey: string;
|
||||
private baseURL: string;
|
||||
|
||||
constructor(modelName: string = 'deepseek-chat') {
|
||||
this.modelName = modelName;
|
||||
this.apiKey = config.deepseekApiKey || '';
|
||||
this.baseURL = 'https://api.deepseek.com/v1';
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error('DeepSeek API key is not configured');
|
||||
}
|
||||
}
|
||||
|
||||
// 非流式调用
|
||||
async chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.baseURL}/chat/completions`,
|
||||
{
|
||||
model: this.modelName,
|
||||
messages: messages,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
max_tokens: options?.maxTokens ?? 2000,
|
||||
top_p: options?.topP ?? 0.9,
|
||||
stream: false,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
timeout: 60000, // 60秒超时
|
||||
}
|
||||
);
|
||||
|
||||
const choice = response.data.choices[0];
|
||||
|
||||
return {
|
||||
content: choice.message.content,
|
||||
model: response.data.model,
|
||||
usage: {
|
||||
promptTokens: response.data.usage.prompt_tokens,
|
||||
completionTokens: response.data.usage.completion_tokens,
|
||||
totalTokens: response.data.usage.total_tokens,
|
||||
},
|
||||
finishReason: choice.finish_reason,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
console.error('DeepSeek API Error:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(
|
||||
`DeepSeek API调用失败: ${error.response?.data?.error?.message || error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 流式调用
|
||||
async *chatStream(
|
||||
messages: Message[],
|
||||
options?: LLMOptions,
|
||||
onChunk?: (chunk: StreamChunk) => void
|
||||
): AsyncGenerator<StreamChunk, void, unknown> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.baseURL}/chat/completions`,
|
||||
{
|
||||
model: this.modelName,
|
||||
messages: messages,
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
max_tokens: options?.maxTokens ?? 2000,
|
||||
top_p: options?.topP ?? 0.9,
|
||||
stream: true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
responseType: 'stream',
|
||||
timeout: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
const stream = response.data;
|
||||
let buffer = '';
|
||||
|
||||
for await (const chunk of stream) {
|
||||
buffer += chunk.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine === 'data: [DONE]') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('data: ')) {
|
||||
try {
|
||||
const jsonStr = trimmedLine.slice(6);
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
const choice = data.choices[0];
|
||||
const content = choice.delta?.content || '';
|
||||
|
||||
const streamChunk: StreamChunk = {
|
||||
content: content,
|
||||
done: choice.finish_reason === 'stop',
|
||||
model: data.model,
|
||||
};
|
||||
|
||||
if (choice.finish_reason === 'stop' && data.usage) {
|
||||
streamChunk.usage = {
|
||||
promptTokens: data.usage.prompt_tokens,
|
||||
completionTokens: data.usage.completion_tokens,
|
||||
totalTokens: data.usage.total_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
if (onChunk) {
|
||||
onChunk(streamChunk);
|
||||
}
|
||||
|
||||
yield streamChunk;
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse SSE data:', parseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('DeepSeek Stream Error:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(
|
||||
`DeepSeek流式调用失败: ${error.response?.data?.error?.message || error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
77
backend/src/adapters/LLMFactory.ts
Normal file
77
backend/src/adapters/LLMFactory.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { ILLMAdapter, ModelType } from './types.js';
|
||||
import { DeepSeekAdapter } from './DeepSeekAdapter.js';
|
||||
import { QwenAdapter } from './QwenAdapter.js';
|
||||
|
||||
/**
|
||||
* LLM工厂类
|
||||
* 根据模型类型创建相应的适配器实例
|
||||
*/
|
||||
export class LLMFactory {
|
||||
private static adapters: Map<string, ILLMAdapter> = new Map();
|
||||
|
||||
/**
|
||||
* 获取LLM适配器实例(单例模式)
|
||||
* @param modelType 模型类型
|
||||
* @returns LLM适配器实例
|
||||
*/
|
||||
static getAdapter(modelType: ModelType): ILLMAdapter {
|
||||
// 如果已经创建过该适配器,直接返回
|
||||
if (this.adapters.has(modelType)) {
|
||||
return this.adapters.get(modelType)!;
|
||||
}
|
||||
|
||||
// 根据模型类型创建适配器
|
||||
let adapter: ILLMAdapter;
|
||||
|
||||
switch (modelType) {
|
||||
case 'deepseek-v3':
|
||||
adapter = new DeepSeekAdapter('deepseek-chat');
|
||||
break;
|
||||
|
||||
case 'qwen3-72b':
|
||||
adapter = new QwenAdapter('qwen-max'); // Qwen3-72B对应的模型名
|
||||
break;
|
||||
|
||||
case 'gemini-pro':
|
||||
// TODO: 实现Gemini适配器
|
||||
throw new Error('Gemini adapter is not implemented yet');
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported model type: ${modelType}`);
|
||||
}
|
||||
|
||||
// 缓存适配器实例
|
||||
this.adapters.set(modelType, adapter);
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除适配器缓存
|
||||
* @param modelType 可选,指定清除某个模型的适配器,不传则清除所有
|
||||
*/
|
||||
static clearCache(modelType?: ModelType): void {
|
||||
if (modelType) {
|
||||
this.adapters.delete(modelType);
|
||||
} else {
|
||||
this.adapters.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否支持
|
||||
* @param modelType 模型类型
|
||||
* @returns 是否支持
|
||||
*/
|
||||
static isSupported(modelType: string): boolean {
|
||||
return ['deepseek-v3', 'qwen3-72b', 'gemini-pro'].includes(modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的模型列表
|
||||
* @returns 支持的模型列表
|
||||
*/
|
||||
static getSupportedModels(): ModelType[] {
|
||||
return ['deepseek-v3', 'qwen3-72b', 'gemini-pro'];
|
||||
}
|
||||
}
|
||||
|
||||
162
backend/src/adapters/QwenAdapter.ts
Normal file
162
backend/src/adapters/QwenAdapter.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import axios from 'axios';
|
||||
import { ILLMAdapter, Message, LLMOptions, LLMResponse, StreamChunk } from './types.js';
|
||||
import { config } from '../config/env.js';
|
||||
|
||||
export class QwenAdapter implements ILLMAdapter {
|
||||
modelName: string;
|
||||
private apiKey: string;
|
||||
private baseURL: string;
|
||||
|
||||
constructor(modelName: string = 'qwen-turbo') {
|
||||
this.modelName = modelName;
|
||||
this.apiKey = config.qwenApiKey || '';
|
||||
this.baseURL = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
|
||||
|
||||
if (!this.apiKey) {
|
||||
throw new Error('Qwen API key is not configured');
|
||||
}
|
||||
}
|
||||
|
||||
// 非流式调用
|
||||
async chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
this.baseURL,
|
||||
{
|
||||
model: this.modelName,
|
||||
input: {
|
||||
messages: messages,
|
||||
},
|
||||
parameters: {
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
max_tokens: options?.maxTokens ?? 2000,
|
||||
top_p: options?.topP ?? 0.9,
|
||||
result_format: 'message',
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
timeout: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
const output = response.data.output;
|
||||
const usage = response.data.usage;
|
||||
|
||||
return {
|
||||
content: output.choices[0].message.content,
|
||||
model: this.modelName,
|
||||
usage: {
|
||||
promptTokens: usage.input_tokens,
|
||||
completionTokens: usage.output_tokens,
|
||||
totalTokens: usage.total_tokens || usage.input_tokens + usage.output_tokens,
|
||||
},
|
||||
finishReason: output.choices[0].finish_reason,
|
||||
};
|
||||
} catch (error: unknown) {
|
||||
console.error('Qwen API Error:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(
|
||||
`Qwen API调用失败: ${error.response?.data?.message || error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 流式调用
|
||||
async *chatStream(
|
||||
messages: Message[],
|
||||
options?: LLMOptions,
|
||||
onChunk?: (chunk: StreamChunk) => void
|
||||
): AsyncGenerator<StreamChunk, void, unknown> {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
this.baseURL,
|
||||
{
|
||||
model: this.modelName,
|
||||
input: {
|
||||
messages: messages,
|
||||
},
|
||||
parameters: {
|
||||
temperature: options?.temperature ?? 0.7,
|
||||
max_tokens: options?.maxTokens ?? 2000,
|
||||
top_p: options?.topP ?? 0.9,
|
||||
result_format: 'message',
|
||||
incremental_output: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
'X-DashScope-SSE': 'enable',
|
||||
},
|
||||
responseType: 'stream',
|
||||
timeout: 60000,
|
||||
}
|
||||
);
|
||||
|
||||
const stream = response.data;
|
||||
let buffer = '';
|
||||
|
||||
for await (const chunk of stream) {
|
||||
buffer += chunk.toString();
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (!trimmedLine || trimmedLine.startsWith(':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('data:')) {
|
||||
try {
|
||||
const jsonStr = trimmedLine.slice(5).trim();
|
||||
const data = JSON.parse(jsonStr);
|
||||
|
||||
const output = data.output;
|
||||
const choice = output.choices[0];
|
||||
const content = choice.message?.content || '';
|
||||
|
||||
const streamChunk: StreamChunk = {
|
||||
content: content,
|
||||
done: choice.finish_reason === 'stop',
|
||||
model: this.modelName,
|
||||
};
|
||||
|
||||
if (choice.finish_reason === 'stop' && data.usage) {
|
||||
streamChunk.usage = {
|
||||
promptTokens: data.usage.input_tokens,
|
||||
completionTokens: data.usage.output_tokens,
|
||||
totalTokens: data.usage.total_tokens || data.usage.input_tokens + data.usage.output_tokens,
|
||||
};
|
||||
}
|
||||
|
||||
if (onChunk) {
|
||||
onChunk(streamChunk);
|
||||
}
|
||||
|
||||
yield streamChunk;
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse Qwen SSE data:', parseError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Qwen Stream Error:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
throw new Error(
|
||||
`Qwen流式调用失败: ${error.response?.data?.message || error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
backend/src/adapters/types.ts
Normal file
55
backend/src/adapters/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// LLM适配器类型定义
|
||||
|
||||
export interface Message {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface LLMOptions {
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
topP?: number;
|
||||
stream?: boolean;
|
||||
}
|
||||
|
||||
export interface LLMResponse {
|
||||
content: string;
|
||||
model: string;
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
finishReason?: string;
|
||||
}
|
||||
|
||||
export interface StreamChunk {
|
||||
content: string;
|
||||
done: boolean;
|
||||
model?: string;
|
||||
usage?: {
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
};
|
||||
}
|
||||
|
||||
// LLM适配器接口
|
||||
export interface ILLMAdapter {
|
||||
// 模型名称
|
||||
modelName: string;
|
||||
|
||||
// 非流式调用
|
||||
chat(messages: Message[], options?: LLMOptions): Promise<LLMResponse>;
|
||||
|
||||
// 流式调用
|
||||
chatStream(
|
||||
messages: Message[],
|
||||
options?: LLMOptions,
|
||||
onChunk?: (chunk: StreamChunk) => void
|
||||
): AsyncGenerator<StreamChunk, void, unknown>;
|
||||
}
|
||||
|
||||
// 支持的模型类型
|
||||
export type ModelType = 'deepseek-v3' | 'qwen3-72b' | 'gemini-pro';
|
||||
|
||||
Reference in New Issue
Block a user