feat: 初始化 OnceLove GraphRAG 项目基础架构
- 添加完整的项目结构,包括前端(Vue3 + Vite)、后端(Fastify)和基础设施配置 - 实现核心 GraphRAG 服务,集成 Neo4j 图数据库和 Qdrant 向量数据库 - 添加用户认证系统和管理员登录界面 - 提供 Docker 容器化部署方案和开发环境配置 - 包含项目文档、API 文档(Swagger)和测试脚本
This commit is contained in:
127
OnceLove/oncelove-graphrag/api/src/services/llm.service.js
Normal file
127
OnceLove/oncelove-graphrag/api/src/services/llm.service.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const createHttpError = (statusCode, message) => {
|
||||
const error = new Error(message);
|
||||
error.statusCode = statusCode;
|
||||
return error;
|
||||
};
|
||||
|
||||
export class LLMService {
|
||||
constructor(env) {
|
||||
this.baseUrl = (env.LLM_BASE_URL ?? "").replace(/\/+$/, "");
|
||||
this.apiKey = env.LLM_API_KEY ?? "";
|
||||
this.model = env.LLM_MODEL_NAME ?? "";
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return Boolean(this.baseUrl && this.apiKey && this.model);
|
||||
}
|
||||
|
||||
async chat(messages, temperature = 0.7) {
|
||||
if (!this.isEnabled()) {
|
||||
throw createHttpError(400, "LLM 服务未配置,请提供 LLM_BASE_URL/LLM_API_KEY/LLM_MODEL_NAME");
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: this.model,
|
||||
messages: messages,
|
||||
temperature: temperature
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw createHttpError(response.status, `LLM 请求失败:${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
async analyzeText(text) {
|
||||
if (!text?.trim()) {
|
||||
throw createHttpError(400, "分析文本不能为空");
|
||||
}
|
||||
|
||||
const systemPrompt = `你是一个实体关系分析专家。请分析用户输入的文本,提取人物、事件、主题、关系。
|
||||
|
||||
## 输出格式
|
||||
{
|
||||
"persons": [{"id": "p1", "name": "人物名称", "description": "人物描述"}],
|
||||
"events": [{"id": "e1", "type": "事件类型", "summary": "事件摘要", "occurred_at": "ISO 时间", "participants": ["p1"], "topics": ["t1"], "importance": 5}],
|
||||
"topics": [{"id": "t1", "name": "主题名称"}],
|
||||
"relations": [{"source": "p1", "target": "p2", "type": "关系类型", "description": "关系描述"}]
|
||||
}
|
||||
|
||||
## 注意
|
||||
- 时间用 ISO 格式,如文本没明确时间用当前时间
|
||||
- importance 是重要性评分 1-10
|
||||
- 关系类型:PARTICIPATES_IN, ABOUT, LOVES, FIGHTS_WITH, GIVES, PROPOSES_TO 等
|
||||
- 如果文本涉及"我",推断另一个角色(如"她")
|
||||
- 即使文本很短也要提取信息,不要返回空数组
|
||||
|
||||
只返回 JSON,不要有其他文字。`;
|
||||
|
||||
const messages = [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: text }
|
||||
];
|
||||
console.log("[DEBUG] LLM request messages:", JSON.stringify(messages));
|
||||
const result = await this.chat(messages, 0.3);
|
||||
|
||||
const content = result?.choices?.[0]?.message?.content;
|
||||
console.log("[DEBUG] LLM raw response:", content);
|
||||
if (!content) {
|
||||
throw createHttpError(500, "LLM 返回内容为空");
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : JSON.parse(content);
|
||||
} catch (e) {
|
||||
throw createHttpError(500, `LLM 返回格式错误:${content.substring(0, 200)}`);
|
||||
}
|
||||
|
||||
if (this.isEmptyAnalysis(parsed)) {
|
||||
const retryMessages = [
|
||||
{
|
||||
role: "system",
|
||||
content: "你是信息抽取器。必须输出非空 JSON:{persons:[{id,name,description}],events:[{id,type,summary,occurred_at,participants,topics,importance}],topics:[{id,name}],relations:[{source,target,type,description}]}"
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `从下列文本提取实体关系,至少给出 2 个 persons、1 个 event、1 个 relation,且仅返回 JSON:${text}`
|
||||
}
|
||||
];
|
||||
const retryResult = await this.chat(retryMessages, 0.2);
|
||||
const retryContent = retryResult?.choices?.[0]?.message?.content;
|
||||
if (!retryContent) {
|
||||
return parsed;
|
||||
}
|
||||
try {
|
||||
const retryJsonMatch = retryContent.match(/\{[\s\S]*\}/);
|
||||
const retryParsed = retryJsonMatch ? JSON.parse(retryJsonMatch[0]) : JSON.parse(retryContent);
|
||||
if (!this.isEmptyAnalysis(retryParsed)) {
|
||||
return retryParsed;
|
||||
}
|
||||
} catch (_) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
isEmptyAnalysis(data) {
|
||||
return !data
|
||||
|| (!Array.isArray(data.persons) || data.persons.length === 0)
|
||||
&& (!Array.isArray(data.events) || data.events.length === 0)
|
||||
&& (!Array.isArray(data.topics) || data.topics.length === 0)
|
||||
&& (!Array.isArray(data.relations) || data.relations.length === 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user