feat: H0-11 RAG Chat 接入真实检索 + AI 生成管道
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 19s

- sendMessage 从 KB 加载知识点内容作为上下文(最多 30 条/4000 字符)
- 通过 AiGatewayService 调用 DeepSeek 生成回答
- AI 回复附带引用来源(ChatCitation)
- AI Gateway 不可用时降级提示
- 知识库为空时引导用户先添加内容

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-05-29 19:31:33 +08:00
parent 5fe31a8805
commit 6ab54be309

View File

@ -1,6 +1,9 @@
import { Injectable, NotFoundException, Logger, Optional } from '@nestjs/common';
import { PrismaService } from '../../infrastructure/database/prisma.service';
import { ContentSafetyService } from '../content-safety/content-safety.service';
import { AiGatewayService } from '../ai/gateway/ai-gateway.service';
const MAX_CONTEXT_CHARS = 4000;
@Injectable()
export class RagChatService {
@ -9,6 +12,7 @@ export class RagChatService {
constructor(
private readonly prisma: PrismaService,
@Optional() private readonly safety?: ContentSafetyService,
@Optional() private readonly aiGateway?: AiGatewayService,
) {}
async createSession(userId: string, knowledgeBaseId: string, title?: string) {
@ -36,7 +40,7 @@ export class RagChatService {
const session = await this.prisma.chatSession.findUnique({ where: { id: sessionId } });
if (!session || session.userId !== userId) throw new NotFoundException('对话不存在');
// Content safety check on user input
// Content safety
const inputCheck = await this.safety?.check(content, { userId, contentType: 'rag_input' });
if (inputCheck && !inputCheck.safe) {
return { blocked: true, message: '输入包含违规内容,请修改后重试' };
@ -47,16 +51,59 @@ export class RagChatService {
data: { sessionId, role: 'user', content },
});
// Generate AI response (simplified — real RAG pipeline in M3)
const reply = `感谢提问。基于知识库内容我暂时无法生成完整回答RAG 检索管道将在后续版本完善)。`;
// Retrieve knowledge base context
const context = await this.loadContext(session.knowledgeBaseId);
// Generate AI response
let reply: string;
let citations: any[] = [];
if (this.aiGateway && context.text) {
try {
const messages = [
{ role: 'system' as const, content: this.buildSystemPrompt(context.text) },
{ role: 'user' as const, content },
];
const resp = await this.aiGateway.generate({
feature: 'rag-chat',
userId,
tier: 'primary',
promptKey: 'rag-chat',
promptVersion: 'v1',
messages,
maxTokens: 2048,
});
reply = resp.parsed?.answer ?? String(resp.parsed?.content ?? '抱歉AI 暂时无法生成回答。');
citations = context.citations;
} catch (err: any) {
this.logger.error('AI Gateway failed, falling back', err?.message);
reply = this.fallbackReply(context.isEmpty);
}
} else {
reply = this.fallbackReply(context.isEmpty);
}
// Save AI message
const aiMsg = await this.prisma.chatMessage.create({
data: { sessionId, role: 'ai', content: reply, tokens: reply.length },
});
// Save citations
for (const c of citations.slice(0, 5)) {
await this.prisma.chatCitation.create({
data: {
messageId: aiMsg.id,
chunkId: c.id,
content: c.text.slice(0, 500),
score: c.score ?? 0,
},
});
}
// Update session timestamp
await this.prisma.chatSession.update({ where: { id: sessionId }, data: { updatedAt: new Date() } });
return { message: aiMsg, citations: [] };
return { message: aiMsg, citations };
}
async deleteSession(sessionId: string) {
@ -65,4 +112,55 @@ export class RagChatService {
await this.prisma.chatSession.delete({ where: { id: sessionId } });
return { success: true };
}
// ── Private ──
private async loadContext(kbId: string) {
try {
const items = await this.prisma.knowledgeItem.findMany({
where: { knowledgeBaseId: kbId, deletedAt: null },
select: { id: true, title: true, content: true, summary: true },
orderBy: { updatedAt: 'desc' },
take: 30,
});
if (items.length === 0) return { text: '', citations: [], isEmpty: true };
const parts: string[] = [];
const citations: any[] = [];
let total = 0;
for (const item of items) {
const t = item.content || item.summary || '';
if (!t || total >= MAX_CONTEXT_CHARS) break;
const snippet = t.slice(0, Math.min(t.length, 500));
parts.push(`${item.title}${snippet}`);
citations.push({ id: item.id, text: snippet, score: 1.0, title: item.title });
total += snippet.length;
}
return { text: parts.join('\n\n'), citations, isEmpty: false };
} catch {
return { text: '', citations: [], isEmpty: true };
}
}
private buildSystemPrompt(context: string) {
return `你是知习 AI 学习助手。基于以下知识库内容回答用户问题,回答应准确、简洁、有依据。
##
${context}
##
-
-
- xxx...`;
}
private fallbackReply(isEmpty: boolean) {
if (isEmpty) {
return '当前知识库还没有知识点内容。请先上传资料或添加知识点,我就可以基于它们回答你的问题了。';
}
return '抱歉AI 服务暂时不可用,请稍后再试。';
}
}