Files
OnceLove-New/OnceLove/oncelove-graphrag/api/src/services/llm.service.js
KOSHM-Pig adabd63769 feat: 新增多用户支持、关系历史查询与恋爱决策建议功能
- 新增用户服务,支持多用户数据隔离与认证
- 新增关系历史查询接口,支持按冲突、积极、时间线等类型过滤
- 新增恋爱决策建议接口,基于图谱分析生成关系健康报告
- 优化前端图谱可视化,增加节点详情面板、图例和边标签显示
- 改进文本分析逻辑,支持实体去重和情感标注
- 新增完整流程测试脚本,验证分析、入库、查询全链路
2026-03-23 22:09:40 +08:00

277 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
const createHttpError = (statusCode, message) => {
const error = new Error(message);
error.statusCode = statusCode;
return error;
};
/**
* 文本预处理
*/
const preprocessText = (text) => {
return text
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0)
.join('\n')
.trim();
};
/**
* 将文本分块(适合长文本)
*/
const splitTextIntoChunks = (text, chunkSize = 500, overlap = 50) => {
const chunks = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + chunkSize, text.length);
let chunkEnd = end;
// 尝试在句子边界处切断
if (end < text.length) {
const lastPeriod = text.lastIndexOf('.', end);
const lastQuestion = text.lastIndexOf('?', end);
const lastExclamation = text.lastIndexOf('!', end);
const lastNewline = text.lastIndexOf('\n', end);
const breakPoint = Math.max(lastPeriod, lastQuestion, lastExclamation, lastNewline);
if (breakPoint > start + chunkSize / 2) {
chunkEnd = breakPoint + 1;
}
}
chunks.push(text.slice(start, chunkEnd).trim());
start = chunkEnd - overlap;
}
return chunks;
};
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,
max_tokens: 4096
})
});
if (!response.ok) {
const errorText = await response.text();
throw createHttpError(response.status, `LLM 请求失败:${errorText}`);
}
const data = await response.json();
return data;
}
/**
* 分析文本并提取详细的实体和关系MiroFish 风格)
* @param {string} text - 用户输入的文本
* @param {object} existingEntities - 现有实体列表(用于识别是否已存在)
*/
async analyzeText(text, existingEntities = {}) {
if (!text?.trim()) {
throw createHttpError(400, "分析文本不能为空");
}
const existingContext = existingEntities.persons?.length > 0 || existingEntities.organizations?.length > 0
? `
## 已有实体列表(极其重要!)
**如果文本中提到的人/组织已存在于下方列表中,必须复用相同的 ID不要创建新实体**
已有的人物:
${(existingEntities.persons || []).map(p => `- ID: "${p.id}", 名字:"${p.name}", 描述:${p.summary}`).join('\n')}
已有的组织:
${(existingEntities.organizations || []).map(o => `- ID: "${o.id}", 名字:"${o.name}", 描述:${o.summary}`).join('\n')}
**代词解析指南**:
- "我女朋友" = 已有实体中的"女朋友"(如果有)
- "她" = 根据上下文推断指代哪个女性角色
- "他" = 根据上下文推断指代哪个男性角色
- "丽丽" = 如果已有实体中有"丽丽",复用 ID`
: '';
const systemPrompt = `你是一个恋爱关系知识图谱构建专家。从用户输入的文本中提取实体和关系,用于后续的恋爱决策建议。
## 核心原则
1. **重点关注**:用户本人("我")和恋爱对象(女朋友/男朋友/心仪对象)需要详细记录
2. **其他人物**:朋友、闺蜜、同事等只需要记录基本信息(名字、与用户的关系)
3. **事件细节**:记录争吵、约会、礼物、重要对话等影响关系的事件
4. **情感线索**:提取情绪变化、态度、期望等软性信息
## 输出格式(严格 JSON
{
"persons": [
{
"id": "p1",
"name": "人物名称",
"summary": "人物描述",
"role": "用户 | 恋爱对象 | 朋友 | 家人 | 同事 | 其他"
}
],
"events": [
{
"id": "e1",
"type": "事件类型",
"summary": "事件描述(包含情感细节)",
"occurred_at": "ISO 时间",
"participants": ["p1"],
"emotional_tone": "positive | neutral | negative",
"importance": 1-10
}
],
"topics": [
{
"id": "t1",
"name": "主题名称"
}
],
"relations": [
{
"source": "p1",
"target": "p2",
"type": "关系类型",
"summary": "关系描述"
}
]
}
## 关系类型参考
- 用户与恋爱对象LOVES, DATING, MARRIED_TO, ENGAGED_TO, BROKEN_UP_WITH, CONFLICT_WITH
- 用户与他人FRIENDS_WITH, COLLEAGUE_OF, CLASSMATE_OF, SIBLING_OF
- 恋爱对象与他人FRIENDS_WITH, COLLEAGUE_OF, FAMILY_OF, CONFLICT_WITH
## 重要规则
1. **用户识别**"我"=用户本人,固定 ID 为"user"
2. **恋爱对象**"女朋友/男朋友/她/他"=恋爱对象,固定 ID 为"partner"
3. **其他人物**:不需要详细描述,只记录名字和与核心人物的关系
4. **实体去重**:如果文本中提到的人已存在于"已有实体"列表中,**复用相同的 ID**
5. **时间标准化**occurred_at 使用 ISO 格式
6. **情感标注**emotional_tone 标注事件的情感倾向positive/neutral/negative
只返回 JSON不要有其他文字。`;
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: `${existingContext}\n\n## 待分析文本\n${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)}`);
}
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);
}
/**
* 基于图谱数据生成恋爱决策建议
*/
async generateRelationshipAdvice(data) {
const systemPrompt = `你是一个恋爱关系咨询师,擅长分析情侣关系模式并给出专业建议。
请根据以下数据生成恋爱建议:
1. 事件统计:各类事件的数量和情感倾向
2. 最近事件:最近发生的 10 个事件
3. 第三方影响:朋友/家人对关系的参与程度
## 输出格式JSON
{
"relationship_health": "healthy | neutral | concerning",
"summary": "关系状态总结100 字以内)",
"patterns": [
{
"pattern": "识别出的模式(如'频繁争吵'、'缺乏沟通'",
"evidence": "支持该模式的事件或数据",
"suggestion": "针对性建议"
}
],
"third_party_influence": "第三方影响分析",
"action_items": [
"具体可执行的建议 1",
"具体可执行的建议 2",
"具体可执行的建议 3"
],
"positive_notes": "关系中积极的方面(鼓励)"
}`;
const userMessage = `
## 事件统计
${JSON.stringify(data.eventStats, null, 2)}
## 最近事件
${JSON.stringify(data.recentEvents, null, 2)}
## 第三方参与
${JSON.stringify(data.thirdParty, null, 2)}
请分析这段恋爱关系的健康状况,并给出专业建议。`;
const result = await this.chat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage }
], 0.5);
const content = result?.choices?.[0]?.message?.content;
try {
const jsonMatch = content.match(/\{[\s\S]*\}/);
return jsonMatch ? JSON.parse(jsonMatch[0]) : JSON.parse(content);
} catch (e) {
return {
relationship_health: 'neutral',
summary: content || '无法生成详细分析',
patterns: [],
action_items: ['建议与伴侣坦诚沟通', '关注彼此的情感需求'],
positive_notes: '每段关系都有起伏,重要的是共同努力'
};
}
}
}