KirkHan commited on
Commit
f43bc38
·
verified ·
1 Parent(s): 7706e61

Delete rag_engine.py

Browse files
Files changed (1) hide show
  1. rag_engine.py +0 -480
rag_engine.py DELETED
@@ -1,480 +0,0 @@
1
- """
2
- RAG引擎:实现传统RAG和GraphRAG的检索逻辑
3
- """
4
- from typing import List, Dict, Tuple
5
- # 优先使用轻量级版本(避免超过 Vercel 250MB 限制)
6
- try:
7
- from database_setup_lite import SimpleGraphDB, VectorDB
8
- except ImportError:
9
- from database_setup import SimpleGraphDB, VectorDB
10
- import json
11
- import requests
12
-
13
- # LLM配置(从环境变量读取,确保安全)
14
- import os
15
- LLM_API_BASE = os.getenv("LLM_API_BASE", "https://api.ai-gaochao.cn/v1")
16
- LLM_API_KEY = os.getenv("LLM_API_KEY", "")
17
- LLM_MODEL = os.getenv("LLM_MODEL", "gemini-2.5-flash")
18
-
19
- if not LLM_API_KEY:
20
- raise ValueError("LLM_API_KEY 环境变量未设置!请在 .env 文件中设置 LLM_API_KEY")
21
-
22
- class TraditionalRAG:
23
- """传统语义RAG"""
24
- def __init__(self, vector_db: VectorDB, graph_db: SimpleGraphDB = None):
25
- self.vector_db = vector_db
26
- self.graph_db = graph_db # 用于限制搜索范围
27
-
28
- def retrieve(self, query: str, product_name: str = None, style_name: str = None, n_results: int = 5) -> Dict:
29
- """语义检索(传统RAG:直接向量搜索,不利用图结构,返回片段句子)"""
30
- # 传统RAG的特点:直接进行语义相似度搜索,不利用图结构
31
- # 使用相同的文案数据库,但只返回相似的片段句子(而不是完整文案)
32
-
33
- # 直接进行向量搜索(传统RAG的特点)
34
- # 传统RAG限制结果数量,只返回最相关的2-3个结果
35
- limited_results = min(3, n_results) # 最多返回3个结果
36
- all_results = self.vector_db.search(query, n_results=limited_results * 2) # 多搜索一些,用于提取片段
37
-
38
- # 从完整文案中提取与查询最相关的片段句子
39
- processed_results = []
40
- query_keywords = set(query.lower().split())
41
-
42
- for result in all_results[:limited_results * 2]:
43
- full_content = result.get("content", "")
44
- if not full_content:
45
- continue
46
-
47
- # 将文案按句子分割(中文句号、英文句号、感叹号、问号)
48
- import re
49
- sentences = re.split(r'[。!?.!?]', full_content)
50
- sentences = [s.strip() for s in sentences if s.strip()]
51
-
52
- # 找到与查询最相关的句子片段
53
- best_sentences = []
54
- for sentence in sentences:
55
- # 计算句子与查询的相关度(简单关键词匹配)
56
- sentence_lower = sentence.lower()
57
- keyword_matches = sum(1 for keyword in query_keywords if keyword in sentence_lower)
58
- if keyword_matches > 0:
59
- best_sentences.append((sentence, keyword_matches))
60
-
61
- # 按相关度排序,取前2-3个最相关的句子
62
- best_sentences.sort(key=lambda x: x[1], reverse=True)
63
- selected_sentences = [s[0] for s in best_sentences[:3]]
64
-
65
- # 如果没有找到相关句子,取前3个句子作为片段
66
- if not selected_sentences and sentences:
67
- selected_sentences = sentences[:3]
68
-
69
- # 组合成片段(最多150字,确保有足够内容)
70
- snippet = "。".join(selected_sentences)
71
- if not snippet and sentences:
72
- # 如果还是空的,至少取前3个句子
73
- snippet = "。".join(sentences[:3])
74
- if len(snippet) > 150:
75
- snippet = snippet[:150] + "..."
76
- elif len(snippet) < 30 and len(sentences) > 0:
77
- # 如果片段太短,至少取前2-3个句子
78
- snippet = "。".join(sentences[:min(3, len(sentences))])
79
- if len(snippet) > 150:
80
- snippet = snippet[:150] + "..."
81
-
82
- if snippet:
83
- processed_results.append({
84
- "content": snippet, # 返回片段而不是完整文案
85
- "full_content": full_content, # 保留完整内容用于显示
86
- "metadata": result.get("metadata", {}),
87
- "distance": result.get("distance", 0),
88
- "is_snippet": True # 标记这是片段
89
- })
90
-
91
- if len(processed_results) >= limited_results:
92
- break
93
-
94
- # 如果结果太少,至少返回1-2个语义相似的结果
95
- if len(processed_results) < 1:
96
- # 如果提取片段失败,至少返回一些结果
97
- for result in all_results[:max(1, limited_results)]:
98
- content = result.get("content", "")
99
- if content:
100
- # 简单截取前150字作为片段
101
- snippet = content[:150] + "..." if len(content) > 150 else content
102
- processed_results.append({
103
- "content": snippet,
104
- "full_content": content,
105
- "metadata": result.get("metadata", {}),
106
- "distance": result.get("distance", 0),
107
- "is_snippet": True
108
- })
109
- if len(processed_results) >= limited_results:
110
- break
111
-
112
- return {
113
- "method": "语义检索",
114
- "query": query,
115
- "product": product_name,
116
- "style": style_name,
117
- "results": processed_results[:limited_results],
118
- "retrieval_path": [
119
- "向量相似度搜索(传统RAG:不利用图结构)",
120
- f"找到 {len(processed_results)} 个语义相似的片段",
121
- "⚠️ 局限性:只返回片段句子,没有图结构,无法找到跨品类的风格相关文案"
122
- ],
123
- "explanation": "传统RAG直接通过语义相似度搜索相关文案,使用相同的文案数据库,但只返回与查询最相关的片段句子(而不是完整文案)。没有图结构,无法找到跨品类的风格相关文案。"
124
- }
125
-
126
- class GraphRAG:
127
- """图增强RAG"""
128
- def __init__(self, graph_db: SimpleGraphDB, vector_db: VectorDB):
129
- self.graph_db = graph_db
130
- self.vector_db = vector_db
131
-
132
- def retrieve(self, query: str, product_name: str = None, style_name: str = None, n_results: int = 5) -> Dict:
133
- """图增强检索"""
134
- retrieval_path = []
135
- retrieved_docs = []
136
-
137
- # 步骤1: 尝试找到风格节点
138
- style_node = None
139
- if style_name:
140
- style_node = self.graph_db.find_node_by_property("Style", "name", style_name)
141
- if style_node:
142
- retrieval_path.append(f"定位风格节点: {style_node['properties']['name']}")
143
-
144
- # 步骤2: 通过风格节点找到相关文案(跨品类)
145
- if style_node:
146
- # 反向查找:找到连接到风格的文案节点
147
- for edge in self.graph_db.edges:
148
- if edge["target"] == style_node["id"] and edge["relationship"] == "HAS_STYLE":
149
- copy_node = self.graph_db.nodes.get(edge["source"])
150
- if copy_node and copy_node["type"] == "Copywriting":
151
- content = copy_node["properties"]["content"]
152
- # 获取该文案关联的产品(HAS_COPY关系:Product -> Copywriting)
153
- product_id = None
154
- for e in self.graph_db.edges:
155
- if e["target"] == edge["source"] and e["relationship"] == "HAS_COPY":
156
- product_id = e["source"]
157
- break
158
-
159
- product_info = self.graph_db.nodes.get(product_id, {}).get("properties", {})
160
- retrieved_docs.append({
161
- "content": content,
162
- "source": "图遍历",
163
- "product": product_info.get("name", "未知"),
164
- "style": style_name,
165
- "tag": copy_node["properties"].get("tag", ""),
166
- "retrieval_reason": f"通过风格节点'{style_name}'找到的跨品类文案(来自产品:{product_info.get('name', '未知')})"
167
- })
168
-
169
- if retrieved_docs:
170
- retrieval_path.append(f"通过风格节点遍历找到 {len(retrieved_docs)} 个相关文案")
171
- else:
172
- retrieval_path.append("未找到该风格的相关文案")
173
-
174
- # 步骤3: 如果指定了产品,查找产品特征
175
- product_features = []
176
- if product_name:
177
- product_node = self.graph_db.find_node_by_property("Product", "name", product_name)
178
- if product_node:
179
- retrieval_path.append(f"定位产品节点: {product_name}")
180
- features = product_node["properties"].get("features", [])
181
- keywords = product_node["properties"].get("keywords", [])
182
- product_features = features + keywords
183
- retrieval_path.append(f"提取产品特征: {', '.join(product_features[:5])}")
184
-
185
- # 步骤4: 如果图检索结果不足,用向量检索补充
186
- if len(retrieved_docs) < n_results:
187
- vector_results = self.vector_db.search(query, n_results=n_results - len(retrieved_docs))
188
- for result in vector_results:
189
- # 避免重复
190
- if not any(doc["content"] == result["content"] for doc in retrieved_docs):
191
- retrieved_docs.append({
192
- "content": result["content"],
193
- "source": "向量检索补充",
194
- "product": result["metadata"].get("product_id", "未知"),
195
- "style": result["metadata"].get("style_id", "未知"),
196
- "tag": result["metadata"].get("tag", ""),
197
- "retrieval_reason": "语义���似度补充检索"
198
- })
199
- if vector_results:
200
- retrieval_path.append(f"向量检索补充 {len(vector_results)} 个结果")
201
-
202
- return {
203
- "method": "图增强检索",
204
- "query": query,
205
- "product": product_name,
206
- "style": style_name,
207
- "product_features": product_features,
208
- "results": retrieved_docs[:n_results],
209
- "retrieval_path": retrieval_path,
210
- "explanation": "通过图结构找到跨品类的风格相关文案,即使产品不同,但风格相通,可以借鉴文案模板。"
211
- }
212
-
213
- class RAGEngine:
214
- """RAG引擎主类"""
215
- def __init__(self, graph_db: SimpleGraphDB, vector_db: VectorDB):
216
- self.graph_db = graph_db
217
- self.traditional_rag = TraditionalRAG(vector_db, graph_db)
218
- self.graph_rag = GraphRAG(graph_db, vector_db)
219
-
220
- def compare_retrieval(self, query: str, product_name: str = None, style_name: str = None) -> Dict:
221
- """对比传统RAG和GraphRAG的检索结果"""
222
- traditional_result = self.traditional_rag.retrieve(query, product_name, style_name)
223
- graph_result = self.graph_rag.retrieve(query, product_name, style_name)
224
-
225
- return {
226
- "traditional_rag": traditional_result,
227
- "graph_rag": graph_result,
228
- "comparison": {
229
- "traditional_count": len(traditional_result["results"]),
230
- "graph_count": len(graph_result["results"]),
231
- "graph_cross_category": len([r for r in graph_result["results"] if r.get("source") == "图遍历"])
232
- }
233
- }
234
-
235
- def generate_copywriting(self, query: str, product_name: str, style_name: str, use_graph: bool = True) -> Dict:
236
- """生成文案(使用LLM)"""
237
- if use_graph:
238
- retrieval_result = self.graph_rag.retrieve(query, product_name, style_name)
239
- else:
240
- retrieval_result = self.traditional_rag.retrieve(query, product_name, style_name)
241
-
242
- # 获取检索到的参考文案
243
- retrieved_texts = [r["content"] for r in retrieval_result["results"][:5]] # 取前5个作为参考
244
-
245
- # 统计信息
246
- cross_category_count = len([r for r in retrieval_result["results"] if r.get("source") == "图遍历"]) if use_graph else 0
247
-
248
- # 获取产品特征(用于GraphRAG)
249
- product_features = []
250
- if use_graph and retrieval_result.get("product_features"):
251
- product_features = retrieval_result["product_features"]
252
-
253
- # 调用LLM生成文案
254
- try:
255
- llm_generated = self._call_llm_generate(
256
- product_name=product_name,
257
- style_name=style_name,
258
- reference_texts=retrieved_texts,
259
- product_features=product_features,
260
- use_graph=use_graph,
261
- cross_category_count=cross_category_count
262
- )
263
- except Exception as e:
264
- print(f"LLM生成失败: {e}")
265
- # 如果LLM失败,使用模板生成
266
- llm_generated = self._generate_template(retrieved_texts, product_name, style_name)
267
-
268
- # 组装最终输出
269
- if use_graph and product_features:
270
- features = ", ".join(product_features[:3])
271
- reference_sources = ', '.join([r.get('product', '未知') for r in retrieval_result["results"][:3]])
272
- generated_text = f"""基于图增强检索生成的文案:
273
-
274
- ✨ 检索策略:通过图结构找到跨品类的风格相关文案
275
- 📊 检索结果:找到 {len(retrieved_texts)} 个相关文案,其中 {cross_category_count} 个来自跨品类(通过风格节点关联)
276
- 🎯 产品特征:{features}
277
- 📝 参考文案来源:{reference_sources}
278
-
279
- 【{style_name}风格】{product_name}文案:
280
-
281
- {llm_generated}
282
-
283
- 💡 说明:GraphRAG 通过风格节点找到了跨品类的参考文案(如香薰蜡烛的清冷避世风文案),即使产品不同,但风格相通,可以借鉴文案模板。"""
284
- else:
285
- generated_text = f"""基于传统语义检索生成的文案:
286
-
287
- 🔍 检索策略:直接通过语义相似度搜索
288
- 📊 检索结果:找到 {len(retrieved_texts)} 个语义相似的文案
289
- ⚠️ 局限性:如果数据库中没有相似内容,可能返回不相关的结果
290
-
291
- 【{style_name}风格】{product_name}文案:
292
-
293
- {llm_generated}
294
-
295
- 💡 说明:传统 RAG 只能找到语义相似的文案,如果数据库中没有该产品的该风格文案,可能无法生成合适的文案。"""
296
-
297
- return {
298
- "generated_text": generated_text,
299
- "retrieval_result": retrieval_result,
300
- "method": "GraphRAG" if use_graph else "Traditional RAG"
301
- }
302
-
303
- def _call_llm_generate(self, product_name: str, style_name: str, reference_texts: List[str],
304
- product_features: List[str] = None, use_graph: bool = True,
305
- cross_category_count: int = 0) -> str:
306
- """调用LLM生成文案"""
307
- headers = {
308
- "Content-Type": "application/json",
309
- "Authorization": f"Bearer {LLM_API_KEY}"
310
- }
311
- url = f"{LLM_API_BASE}/chat/completions"
312
-
313
- # 构建参考文案说明
314
- reference_context = ""
315
- if reference_texts:
316
- reference_context = "\n\n参考文案(用于学习风格和句式):\n"
317
- for i, text in enumerate(reference_texts[:3], 1):
318
- reference_context += f"{i}. {text}\n"
319
- else:
320
- reference_context = "\n\n⚠️ 注意:没有找到相关参考文案,请根据产品特征和风格要求创作。"
321
-
322
- # 构建产品特征说明
323
- features_context = ""
324
- if product_features:
325
- features_context = f"\n产品特征:{', '.join(product_features[:5])}"
326
-
327
- # 构建prompt
328
- if use_graph and cross_category_count > 0:
329
- prompt = f"""你是一名擅长小红书文案写作的创意编辑。请根据以下信息,生成一篇适合在小红书发布的文案(200-300字,要求内容丰富、有细节感)。
330
-
331
- 产品名称:{product_name}
332
- 目标风格:{style_name}
333
- {features_context}
334
-
335
- {reference_context}
336
-
337
- 重要提示:
338
- 1. 这些参考文案来自其他产品(跨品类),但风格相同,请学习它们的句式、语气和情感表达方式
339
- 2. 将参考文案的风格和句式应用到目标产品上
340
- 3. 文案要有细节感、人情味,符合小红书用户的阅读习惯
341
- 4. 保持{style_name}的风格特征
342
- 5. 文案长度要求200-300字,要有丰富的内容和细节描述,可以包含使用场景、情感体验、产品特色等多个方面
343
- 6. 请确保文案完整,不要被截断,以完整的句子结尾
344
-
345
- **必须遵守的输出格式要求:**
346
- - 你必须使用中英对照格式输出文案,按段落进行中英对照
347
- - 格式:中文段落(换行)English paragraph(再换行)
348
- - 每个中文段落后面必须换行,然后添加对应的英文段落翻译,英文段落后再换行
349
- - 示例格式:
350
- 这款真丝眼罩真的太舒服了,遮光效果特别好,戴上之后整个世界都安静了。
351
- This silk eye mask is really comfortable, with excellent light-blocking effect. After putting it on, the whole world becomes quiet.
352
-
353
- 每天晚上睡前戴上它,就像给自己创造了一个专属的避风港。
354
- Every night before sleep, putting it on is like creating a personal sanctuary for yourself.
355
-
356
- 材质柔软亲肤,完全不会压迫眼睛,真的爱了。
357
- The material is soft and skin-friendly, completely non-pressuring on the eyes, I really love it.
358
- - 不要只输出中文,必须每个段落都包含对应的英文翻译
359
- - 可以一个段落包含多句话,然后整体翻译成英文
360
- - 每个中文段落和英文段落之间必须换行,段落之间用空行分隔
361
-
362
- 请直接输出文案内容,不要包含"好的"、"没问题"等前缀,也不要使用markdown格式。只输出文案正文,确保内容完整,并且严格按照以下格式输出:中文段落(换行)English paragraph(换行)。"""
363
- else:
364
- prompt = f"""你是一名擅长小红书文案写作的创意编辑。请根据以下信息,生成一篇适合在小红书发布的文案(200-300字,要求内容丰富、有细节感)。
365
-
366
- 产品名称:{product_name}
367
- 目标风格:{style_name}
368
- {features_context}
369
-
370
- {reference_context}
371
-
372
- 重要提示:
373
- 1. 参考文案可能有限或不够相关,请根据产品特征和风格要求创作
374
- 2. 文案要有细节感、人情味,符合小红书用户的阅读习惯
375
- 3. 保持{style_name}的风格特征
376
- 4. 文案长度要求200-300字,要有丰富的内容和细节描述,可以包含使用场景、情感体验、产品特色等多个方面
377
- 5. 请确保文案完整,不要被截断,以完整的句子结尾
378
-
379
- **必须遵守的输出格式要求:**
380
- - 你必须使用中英对照格式输出文案,按段落进行中英对照
381
- - 格式:中文段落(换行)English paragraph(再换行)
382
- - 每个中文段落后面必须换行,然后添加对应的英文段落翻译,英文段落后再换行
383
- - 示例格式:
384
- 这款真丝眼罩真的太舒服了,遮光效果特别好,戴上之后整个世界都安静了。
385
- This silk eye mask is really comfortable, with excellent light-blocking effect. After putting it on, the whole world becomes quiet.
386
-
387
- 每天晚上睡前戴上它,就像给自己创造了一个专属的避风港。
388
- Every night before sleep, putting it on is like creating a personal sanctuary for yourself.
389
-
390
- 材质柔软亲肤,完全不会压迫眼睛,真的爱了。
391
- The material is soft and skin-friendly, completely non-pressuring on the eyes, I really love it.
392
- - 不要只输出中文,必须每个段落都包含对应的英文翻译
393
- - 可以一个段落包含多句话,然后整体翻译成英文
394
- - 每个中��段落和英文段落之间必须换行,段落之间用空行分隔
395
-
396
- 请直接输出文案内容,不要包含"好的"、"没问题"等前缀,也不要使用markdown格式。只输出文案正文,确保内容完整,并且严格按照以下格式输出:中文段落(换行)English paragraph(换行)。"""
397
-
398
- body = {
399
- "model": LLM_MODEL,
400
- "messages": [
401
- {
402
- "role": "system",
403
- "content": "你是一名擅长文案写作的创意编辑,擅长创作小红书风格的文案。你必须使用中英对照格式输出所有文案内容,按段落进行中英对照,每个中文段落后面换行添加对应的英文翻译。格式:中文段落(换行)English paragraph"
404
- },
405
- {
406
- "role": "user",
407
- "content": prompt
408
- }
409
- ],
410
- "max_tokens": 4000, # 增加token限制以支持更长的文案(200-300字约需要800-1200 tokens,设置4000确保完整输出)
411
- "temperature": 0.9
412
- }
413
-
414
- resp = requests.post(url, headers=headers, json=body, timeout=60)
415
- resp.raise_for_status()
416
- data = resp.json()
417
- generated = data["choices"][0]["message"]["content"].strip()
418
-
419
- # 清理生成的内容
420
- # 移除常见的前缀(只移除开头的前缀,不要截断内容)
421
- prefixes_to_remove = [
422
- "好的,没问题!",
423
- "好的,",
424
- "没问题!",
425
- "好的!",
426
- ]
427
- for prefix in prefixes_to_remove:
428
- if generated.startswith(prefix):
429
- generated = generated[len(prefix):].strip()
430
-
431
- # 移除markdown格式符号(但保留内容)
432
- generated = generated.replace("**", "").replace("*", "").strip()
433
-
434
- return generated
435
-
436
- def _generate_template(self, reference_texts: List[str], product_name: str, style_name: str) -> str:
437
- """生成文案模板(简化版,实际应调用LLM)"""
438
- # 如果有参考文案,提取关键句式
439
- key_phrases = []
440
- if reference_texts:
441
- for text in reference_texts[:2]: # 只取前2个参考
442
- # 提取关键句式(简单提取)
443
- if "避难所" in text:
444
- key_phrases.append("避难所")
445
- if "安静" in text:
446
- key_phrases.append("安静")
447
- if "唯一" in text:
448
- key_phrases.append("唯一")
449
- if "绝绝子" in text:
450
- key_phrases.append("绝绝子")
451
-
452
- # 根据风格和产品生成
453
- if "清冷避世风" in style_name or "深夜emo" in style_name.lower():
454
- if "眼罩" in product_name:
455
- if key_phrases:
456
- # GraphRAG:使用参考文案的句式
457
- return f"戴上眼罩的这片刻漆黑,是我在繁杂城市里唯一的{'避难所' if '避难所' in key_phrases else '避风港'}。物理意义上的关灯,也是心理上的断联。世界终于{'安静了' if '安静' in key_phrases else '静下来了'},今晚只属于我自己。"
458
- else:
459
- # 传统RAG:没有参考,使用通用模板
460
- return f"这个{product_name}真的很不错,遮光效果好,推荐给大家使用。"
461
- elif "CCD" in product_name or "相机" in product_name:
462
- return "深夜拿起它,在颗粒感的画面里,所有的情绪都有了出口。低像素不是缺陷,是另一种真实。"
463
- else:
464
- if key_phrases:
465
- return f"每一个与{product_name}的瞬间,都是我与世界的{'唯一连接' if '唯一' in key_phrases else '连接'}。"
466
- else:
467
- return f"这个{product_name}真的很不错,推荐给大家。"
468
- elif "疯狂种草" in style_name:
469
- if key_phrases and "绝绝子" in key_phrases:
470
- # GraphRAG:使用参考文案的语气
471
- return f"家人们谁懂啊!这个{product_name}真的绝绝子,一秒沦陷!必须人手一个!"
472
- else:
473
- # 传统RAG:没有参考,使用通用语气
474
- return f"这个{product_name}真的很不错,推荐给大家购买!"
475
- else:
476
- if key_phrases:
477
- return f"这个{product_name}真的很不错,{'强烈推荐' if '绝绝子' in key_phrases else '推荐'}给大家!"
478
- else:
479
- return f"这个{product_name}真的很不错,推荐给大家!"
480
-