Files
AIclinicalresearch/backend/src/services/conversationService.ts
2025-10-11 21:42:38 +08:00

506 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from '../config/database.js';
import { LLMFactory } from '../adapters/LLMFactory.js';
import { Message, ModelType, StreamChunk } from '../adapters/types.js';
import { agentService } from './agentService.js';
import * as knowledgeBaseService from './knowledgeBaseService.js';
interface CreateConversationData {
userId: string;
projectId: string;
agentId: string;
title?: string;
}
interface SendMessageData {
conversationId: string;
content: string;
modelType: ModelType;
knowledgeBaseIds?: string[];
}
export class ConversationService {
/**
* 创建新对话
*/
async createConversation(data: CreateConversationData) {
const { userId, projectId, agentId, title } = data;
// 验证智能体是否存在
const agent = agentService.getAgentById(agentId);
if (!agent) {
throw new Error('智能体不存在');
}
// 验证项目是否存在
const project = await prisma.project.findFirst({
where: {
id: projectId,
userId: userId,
deletedAt: null,
},
});
if (!project) {
throw new Error('项目不存在或无权访问');
}
// 创建对话
const conversation = await prisma.conversation.create({
data: {
userId,
projectId,
agentId,
title: title || `${agent.name}的对话`,
metadata: {
agentName: agent.name,
agentCategory: agent.category,
},
},
});
return conversation;
}
/**
* 获取对话列表
*/
async getConversations(userId: string, projectId?: string) {
const where: any = {
userId,
deletedAt: null,
};
if (projectId) {
where.projectId = projectId;
}
const conversations = await prisma.conversation.findMany({
where,
include: {
project: {
select: {
id: true,
name: true,
},
},
_count: {
select: {
messages: true,
},
},
},
orderBy: {
updatedAt: 'desc',
},
});
return conversations;
}
/**
* 获取对话详情(包含消息)
*/
async getConversationById(conversationId: string, userId: string) {
const conversation = await prisma.conversation.findFirst({
where: {
id: conversationId,
userId,
deletedAt: null,
},
include: {
project: {
select: {
id: true,
name: true,
background: true,
researchType: true,
},
},
messages: {
orderBy: {
createdAt: 'asc',
},
},
},
});
if (!conversation) {
throw new Error('对话不存在或无权访问');
}
return conversation;
}
/**
* 组装上下文消息
*/
private async assembleContext(
conversationId: string,
agentId: string,
projectBackground: string,
userInput: string,
knowledgeBaseContext?: string
): Promise<Message[]> {
console.log('🔧 [assembleContext] 开始组装上下文', {
conversationId,
agentId,
hasKnowledgeBaseContext: !!knowledgeBaseContext,
knowledgeBaseContextLength: knowledgeBaseContext?.length || 0
});
// 获取系统Prompt
const systemPrompt = agentService.getSystemPrompt(agentId);
// 获取历史消息最近100条约50轮对话
// DeepSeek-V3支持64K tokens实际可容纳100-200轮对话
const historyMessages = await prisma.message.findMany({
where: {
conversationId,
},
orderBy: {
createdAt: 'desc',
},
take: 100,
});
// 反转顺序(最早的在前)
historyMessages.reverse();
// 判断是否是第一条消息
const isFirstMessage = historyMessages.length === 0;
console.log(`📜 [assembleContext] 历史消息数: ${historyMessages.length}, 是否首次: ${isFirstMessage}`);
// 渲染用户Prompt
let userPromptContent: string;
if (isFirstMessage) {
// 第一条消息:使用完整模板(包含项目背景)
userPromptContent = agentService.renderUserPrompt(agentId, {
projectBackground,
userInput,
knowledgeBaseContext,
});
console.log(`📝 [assembleContext] 首次消息,使用完整模板,长度: ${userPromptContent.length}`);
console.log(`📋 [assembleContext] userPromptContent完整内容:\n${userPromptContent}`);
console.log(`🔍 [assembleContext] 是否包含"参考文献": ${userPromptContent.includes('参考文献')}`);
console.log(`🔍 [assembleContext] 是否包含知识库内容: ${userPromptContent.includes('阿尔兹海默症')}`);
} else {
// 后续消息:只发送用户输入和知识库上下文(如果有)
if (knowledgeBaseContext) {
userPromptContent = `${userInput}\n\n## 参考文献(来自知识库)\n${knowledgeBaseContext}`;
console.log(`📝 [assembleContext] 后续消息+知识库,总长度: ${userPromptContent.length}`);
console.log(`📋 [assembleContext] userPromptContent预览:\n${userPromptContent.substring(0, 300)}...`);
} else {
userPromptContent = userInput;
console.log(`📝 [assembleContext] 后续消息,仅用户输入: ${userPromptContent}`);
}
}
// 组装消息数组
const messages: Message[] = [
{
role: 'system',
content: systemPrompt,
},
];
// 添加历史消息
for (const msg of historyMessages) {
messages.push({
role: msg.role as 'user' | 'assistant',
content: msg.content,
});
}
// 添加当前用户输入
messages.push({
role: 'user',
content: userPromptContent,
});
console.log(`✅ [assembleContext] 组装完成,消息总数: ${messages.length}`);
return messages;
}
/**
* 发送消息(非流式)
*/
async sendMessage(data: SendMessageData, userId: string) {
const { conversationId, content, modelType, knowledgeBaseIds } = data;
// 获取对话信息
const conversation = await this.getConversationById(conversationId, userId);
// 获取知识库上下文(如果有@知识库)
let knowledgeBaseContext = '';
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
const knowledgeResults: string[] = [];
// 对每个知识库进行检索
for (const kbId of knowledgeBaseIds) {
try {
const searchResult = await knowledgeBaseService.searchKnowledgeBase(
userId,
kbId,
content,
3 // 每个知识库返回3个最相关的段落
);
// 格式化检索结果
if (searchResult.records && searchResult.records.length > 0) {
const kbInfo = await prisma.knowledgeBase.findUnique({
where: { id: kbId },
select: { name: true },
});
knowledgeResults.push(
`【知识库:${kbInfo?.name || '未命名'}\n` +
searchResult.records
.map((record: any, index: number) => {
const score = (record.score * 100).toFixed(1);
return `${index + 1}. [相关度${score}%] ${record.segment.content}`;
})
.join('\n\n')
);
}
} catch (error) {
console.error(`Failed to search knowledge base ${kbId}:`, error);
// 检索失败不阻止对话,继续处理
}
}
if (knowledgeResults.length > 0) {
knowledgeBaseContext = knowledgeResults.join('\n\n---\n\n');
}
}
// 组装上下文
const messages = await this.assembleContext(
conversationId,
conversation.agentId,
conversation.project?.background || '',
content,
knowledgeBaseContext
);
// 获取LLM适配器
const adapter = LLMFactory.getAdapter(modelType);
// 获取智能体配置的模型参数
const agent = agentService.getAgentById(conversation.agentId);
const modelConfig = agent?.models?.[modelType];
// 调用LLM
const response = await adapter.chat(messages, {
temperature: modelConfig?.temperature,
maxTokens: modelConfig?.maxTokens,
topP: modelConfig?.topP,
});
// 保存用户消息
const userMessage = await prisma.message.create({
data: {
conversationId,
role: 'user',
content,
metadata: {
knowledgeBaseIds,
},
},
});
// 保存助手回复
const assistantMessage = await prisma.message.create({
data: {
conversationId,
role: 'assistant',
content: response.content,
model: response.model,
tokens: response.usage?.totalTokens,
metadata: {
usage: response.usage,
finishReason: response.finishReason,
},
},
});
// 更新对话的最后更新时间
await prisma.conversation.update({
where: { id: conversationId },
data: { updatedAt: new Date() },
});
return {
userMessage,
assistantMessage,
usage: response.usage,
};
}
/**
* 发送消息(流式)
*/
async *sendMessageStream(
data: SendMessageData,
userId: string
): AsyncGenerator<StreamChunk, void, unknown> {
const { conversationId, content, modelType, knowledgeBaseIds } = data;
// 获取对话信息
const conversation = await this.getConversationById(conversationId, userId);
// 获取知识库上下文(如果有@知识库)
console.log('📚 [sendMessageStream] 开始处理知识库', { knowledgeBaseIds });
let knowledgeBaseContext = '';
if (knowledgeBaseIds && knowledgeBaseIds.length > 0) {
const knowledgeResults: string[] = [];
// 对每个知识库进行检索
for (const kbId of knowledgeBaseIds) {
try {
console.log(`🔎 [sendMessageStream] 检索知识库 ${kbId}`);
const searchResult = await knowledgeBaseService.searchKnowledgeBase(
userId,
kbId,
content,
3 // 每个知识库返回3个最相关的段落
);
console.log(`✅ [sendMessageStream] 检索结果`, {
kbId,
recordCount: searchResult.records?.length || 0
});
// 格式化检索结果
if (searchResult.records && searchResult.records.length > 0) {
const kbInfo = await prisma.knowledgeBase.findUnique({
where: { id: kbId },
select: { name: true },
});
const formattedResult = `【知识库:${kbInfo?.name || '未命名'}\n` +
searchResult.records
.map((record: any, index: number) => {
const score = (record.score * 100).toFixed(1);
return `${index + 1}. [相关度${score}%] ${record.segment.content}`;
})
.join('\n\n');
console.log(`📄 [sendMessageStream] 格式化结果长度: ${formattedResult.length} 字符`);
knowledgeResults.push(formattedResult);
} else {
console.warn(`⚠️ [sendMessageStream] 知识库 ${kbId} 没有检索到记录`);
}
} catch (error) {
console.error(`❌ [sendMessageStream] 检索知识库失败 ${kbId}:`, error);
// 检索失败不阻止对话,继续处理
}
}
if (knowledgeResults.length > 0) {
knowledgeBaseContext = knowledgeResults.join('\n\n---\n\n');
console.log(`💾 [sendMessageStream] 知识库上下文总长度: ${knowledgeBaseContext.length} 字符`);
console.log(`📋 [sendMessageStream] 知识库上下文预览:\n${knowledgeBaseContext.substring(0, 500)}...`);
} else {
console.warn('⚠️ [sendMessageStream] 没有构建任何知识库上下文');
}
} else {
console.log(' [sendMessageStream] 未选择知识库');
}
// 组装上下文
const messages = await this.assembleContext(
conversationId,
conversation.agentId,
conversation.project?.background || '',
content,
knowledgeBaseContext
);
// 获取LLM适配器
const adapter = LLMFactory.getAdapter(modelType);
// 获取智能体配置的模型参数
const agent = agentService.getAgentById(conversation.agentId);
const modelConfig = agent?.models?.[modelType];
// 保存用户消息
await prisma.message.create({
data: {
conversationId,
role: 'user',
content,
metadata: {
knowledgeBaseIds,
},
},
});
// 用于累积完整的回复内容
let fullContent = '';
let usage: any = null;
// 流式调用LLM
for await (const chunk of adapter.chatStream(messages, {
temperature: modelConfig?.temperature,
maxTokens: modelConfig?.maxTokens,
topP: modelConfig?.topP,
})) {
fullContent += chunk.content;
if (chunk.usage) {
usage = chunk.usage;
}
yield chunk;
}
// 流式输出完成后,保存助手回复
await prisma.message.create({
data: {
conversationId,
role: 'assistant',
content: fullContent,
model: modelType,
tokens: usage?.totalTokens,
metadata: {
usage,
},
},
});
// 更新对话的最后更新时间
await prisma.conversation.update({
where: { id: conversationId },
data: { updatedAt: new Date() },
});
}
/**
* 删除对话(软删除)
*/
async deleteConversation(conversationId: string, userId: string) {
const conversation = await prisma.conversation.findFirst({
where: {
id: conversationId,
userId,
deletedAt: null,
},
});
if (!conversation) {
throw new Error('对话不存在或无权访问');
}
await prisma.conversation.update({
where: { id: conversationId },
data: { deletedAt: new Date() },
});
return { success: true };
}
}
export const conversationService = new ConversationService();