CoolFace
Apppublic

FinRAG-Agent/FinRAG-Pro

sourceHugging Facemitupdated 4mo agoView on Hugging Face
1likes
rag_pipeline.py237 linesDownload Raw Back to backend
1"""2金融投研RAG系统 - RAG检索与生成模块(终极防弹合体版)3工作流:双线检索 → 智能清洗 → 上下文融合 → 终极生成4已修复初始化死锁问题!5"""6 7import os8import json9import pickle10import numpy as np11import datetime12 13BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))14INDEX_DIR = os.path.join(BASE_DIR, "data", "index")15PROCESSED_DIR = os.path.join(BASE_DIR, "data", "processed")16TOP_K = 417 18class RAGPipeline:19    def __init__(self):20        self.index = None21        self.chunks = None22        self.metadata = None23        self.encoder = None24        self.embedding_method = "unknown"25        self.initialized = False26 27    def initialize(self):28        """初始化:强力重试机制"""29        try:30            import faiss31            index_path = os.path.join(INDEX_DIR, "faiss_index.bin")32            if not os.path.exists(index_path):33                raise FileNotFoundError(f"FAISS索引文件不存在: {index_path}")34 35            self.index = faiss.read_index(index_path)36            37            chunks_path = os.path.join(PROCESSED_DIR, "chunks.json")38            with open(chunks_path, "r", encoding="utf-8") as f:39                self.chunks = json.load(f)40 41            meta_path = os.path.join(PROCESSED_DIR, "chunk_metadata.json")42            with open(meta_path, "r", encoding="utf-8") as f:43                self.metadata = json.load(f)44 45            self._load_encoder()46            47            self.initialized = True48            print(f"🚀 RAG引擎初始化成功!已加载 {self.index.ntotal} 条向量。")49            return True50        except Exception as e:51            self.initialized = False52            print(f"❌ RAG初始化中止: {e}")53            raise e54 55    def _load_encoder(self):56        tfidf_path = os.path.join(INDEX_DIR, "tfidf_vectorizer.pkl")57        if os.path.exists(tfidf_path):58            from sklearn.feature_extraction.text import TfidfVectorizer59            with open(tfidf_path, "rb") as f:60                self.encoder = pickle.load(f)61            self.embedding_method = "tfidf"62        else:63            from sentence_transformers import SentenceTransformer64            self.encoder = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")65            self.embedding_method = "sentence-transformers"66 67    def _encode(self, texts: list[str]) -> np.ndarray:68        if self.embedding_method == "sentence-transformers":69            return self.encoder.encode(texts, show_progress_bar=False).astype(np.float32)70        else:71            return self.encoder.transform(texts).toarray().astype(np.float32)72 73    # ==================== 第一步:本地检索 ====================74    def retrieve(self, query: str, top_k: int = TOP_K) -> list[dict]:75        if not self.initialized:76            print("⚠️ 检测到引擎未就绪,尝试紧急初始化...")77            self.initialize()78 79        import faiss80        query_vec = self._encode([query])81        faiss.normalize_L2(query_vec)82        scores, indices = self.index.search(query_vec, top_k)83 84        results = []85        for i, (score, idx) in enumerate(zip(scores[0], indices[0])):86            if idx < 0 or idx >= len(self.chunks): continue87            meta = self.metadata[idx] if self.metadata else {}88            results.append({89                "rank": i + 1,90                "score": float(score),91                "chunk_id": meta.get("chunk_id", f"chunk_{idx}"),92                "doc_title": meta.get("doc_title", "未知"),93                "source": meta.get("source", "未知"),94                "source_type": meta.get("source_type", "未知来源"),95                "source_dir": meta.get("source_dir", ""),96                "content": self.chunks[idx],97            })98        return results99 100    # ==================== 第二步:Tavily全网搜索 ====================101    def search_web_tavily(self, query: str, tavily_api_key: str) -> dict:102        if not tavily_api_key or tavily_api_key.startswith("tvly-你的"):103            return {"success": False, "raw_text": "", "results": [], "error": "Tavily API Key未配置"}104 105        try:106            from tavily import TavilyClient107            client = TavilyClient(api_key=tavily_api_key)108            response = client.search(query=query, search_depth="advanced", max_results=5)109 110            results = response.get("results", [])111            if not results:112                return {"success": True, "raw_text": "全网未检索到相关内容", "results": []}113 114            raw_text = ""115            formatted = []116            for idx, r in enumerate(results):117                title = r.get("title", "无标题")118                content = r.get("content", "")119                url = r.get("url", "")120                raw_text += f"【线索{idx+1}】标题: {title}\n内容: {content}\n来源: {url}\n\n"121                formatted.append({"title": title, "content": content[:500], "url": url})122 123            return {"success": True, "raw_text": raw_text, "results": formatted}124        except Exception as e:125            return {"success": False, "raw_text": "", "results": [], "error": f"Tavily搜索失败: {str(e)}"}126 127    # ==================== 第三步:噪音过滤判别器 ====================128    def impact_filter(self, raw_news: str, deepseek_api_key: str, api_type: str = "deepseek", model: str = "", base_url: str = "") -> dict:129        if not raw_news.strip(): return {"success": True, "filtered_text": "", "kept_count": 0, "total_count": 0}130        if not deepseek_api_key: return {"success": False, "filtered_text": raw_news, "kept_count": 0, "total_count": 0, "error": "DeepSeek API Key未配置"}131 132        today = datetime.datetime.now().strftime("%Y年%m月%d日")133        system_prompt = f"""当前真实时间是 {today}。你是一个冷酷的金融噪音过滤机器。134请你严格评估用户提供的实时搜索资讯的'市场情绪影响因子'(满分 10 分)。135任务要求:逐条评估,直接剔除低于 6 分的资讯!只输出保留下来的高价值信息。"""136 137        if not base_url: base_url = "https://api.deepseek.com" if api_type == "deepseek" else "https://api.siliconflow.cn/v1"138        if not model: model = "deepseek-chat" if api_type == "deepseek" else "deepseek-ai/DeepSeek-V2.5"139 140        try:141            import requests142            url = f"{base_url.rstrip('/')}/chat/completions"143            headers = {"Authorization": f"Bearer {deepseek_api_key}", "Content-Type": "application/json"}144            payload = {"model": model, "messages": [{"role": "system", "content": system_prompt}, {"role": "user", "content": raw_news}], "temperature": 0.1, "max_tokens": 2048}145            response = requests.post(url, headers=headers, json=payload, timeout=60)146 147            if response.status_code == 200:148                filtered = response.json()["choices"][0]["message"]["content"]149                kept = filtered.count("【线索") if "【线索" in filtered else (1 if len(filtered) > 50 else 0)150                total = raw_news.count("【线索")151                return {"success": True, "filtered_text": filtered, "kept_count": kept, "total_count": total}152            else:153                return {"success": False, "filtered_text": raw_news, "kept_count": 0, "total_count": 0, "error": f"HTTP {response.status_code}"}154        except Exception as e:155            return {"success": False, "filtered_text": raw_news, "kept_count": 0, "total_count": 0, "error": str(e)}156 157    # ==================== Prompt构建(支持双源上下文) ====================158    def build_fusion_prompt(self, query: str, local_chunks: list[dict], web_filtered: str, web_formatted: list[dict] = None) -> str:159        local_context = ""160        if local_chunks:161            for chunk in local_chunks:162                local_context += f"[来源类型: {chunk.get('source_type', '未知')} | 文件: {chunk.get('source', '未知')}]\n内容: {chunk['content']}\n\n"163 164        web_context = web_filtered if web_filtered else "(本次未获取到全网高价值资讯)"165        166        seen_local = set()167        local_sources_list = []168        for c in local_chunks:169            key = (c.get("source_type", "未知"), c.get("source", ""))170            if key not in seen_local:171                local_sources_list.append(f"📄 [{key[0]}] {key[1]}")172                seen_local.add(key)173 174        web_sources_list = [f"🌐 {w.get('title','未知')} - {w.get('url','')}" for w in (web_formatted or [])]175        all_sources = "\n".join(local_sources_list + web_sources_list)176        today = datetime.datetime.now().strftime("%Y年%m月%d日")177 178        prompt = f"""# Role179【最高指令:当前的真实世界时间是 {today}!你的所有分析、估值和预测都必须基于这个时间点!如果检索到的研报或资讯严重滞后(例如2025年的旧闻),你必须在回答中严厉指出“数据滞后”,并拒绝将其作为最新行情的判断依据!】180你是一位拥有10年以上经验的资深金融分析师,精通企业财报分析、宏观经济研究与行业研报解读。181 182# 参考资料说明183一、【本地深度研报】(来自上市公司年报/行业研究报告)184{local_context}185二、【全网实时高价值资讯】(经过AI噪音过滤筛选后的最新动态)186{web_context}187 188# 用户问题189{query}190 191# 回答要求1921. 绝对忠于原文:财务数据必须严格基于知识库片段,不得捏造。1932. 标注来源:回答中必须使用格式「[来源类型: xxx | 文件: xxx]」标注每个数据的出处。1943. 交叉验证:发现官方年报预期与第三方客观分析冲突时,重点指出【认知预期差】。1954. 强制风险提示:在所有回答的最后,必须单列一段明确的投资风险提示。196 197# 回答格式198- **核心结论**199- **数据与分析**200- **认知预期差**201- **风险提示**202 203# 参考来源总清单204{all_sources}205"""206        return prompt207 208    # ==================== LLM调用 ====================209    def query_llm(self, prompt: str, api_type: str, api_key: str, model: str = "", base_url: str = "") -> str:210        if not api_key: return "[错误] 未配置API密钥"211        if not base_url: base_url = {"deepseek": "https://api.deepseek.com", "siliconflow": "https://api.siliconflow.cn/v1", "openai": "https://api.openai.com/v1"}.get(api_type, "https://api.deepseek.com")212        if not model: model = {"deepseek": "deepseek-chat", "siliconflow": "deepseek-ai/DeepSeek-V2.5", "openai": "gpt-3.5-turbo"}.get(api_type, "deepseek-chat")213 214        try:215            import requests216            url = f"{base_url.rstrip('/')}/chat/completions"217            headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}218            payload = {"model": model, "messages": [{"role": "user", "content": prompt}], "temperature": 0.1, "max_tokens": 4096}219            response = requests.post(url, headers=headers, json=payload, timeout=90)220            if response.status_code == 200:221                return response.json()["choices"][0]["message"]["content"]222            return f"[错误] API请求失败 (HTTP {response.status_code}): {response.text[:300]}"223        except Exception as e:224            return f"[错误] 请求异常: {str(e)}"225 226# ==================== 单例模式修复 ====================227_rag_pipeline = None228 229def get_rag_pipeline() -> "RAGPipeline":230    global _rag_pipeline231    if _rag_pipeline is None:232        p = RAGPipeline()233        p.initialize()234        _rag_pipeline = p235    elif not _rag_pipeline.initialized:236        _rag_pipeline.initialize()237    return _rag_pipeline