戰(zhàn)復(fù)盤)
1. 為什么我要用RAG給A股選股這件事加一層腦子先說結(jié)論單純把行情數(shù)據(jù)丟給大模型做選股基本等于讓一個(gè)從沒看過財(cái)報(bào)的人去當(dāng)基金經(jīng)理。我最初也是這么干的把一堆K線數(shù)據(jù)、財(cái)務(wù)指標(biāo)塞進(jìn)prompt里讓模型判斷結(jié)果它給出的理由全是該股票近期表現(xiàn)良好建議關(guān)注這種正確的廢話。問題出在哪大模型的知識(shí)是訓(xùn)練時(shí)凍結(jié)的它不知道昨天剛出的業(yè)績預(yù)告也不清楚某家公司剛發(fā)的減持公告更沒法把營收增速和行業(yè)景氣度這兩件本來該關(guān)聯(lián)的事串起來。RAG檢索增強(qiáng)生成解決的正是這個(gè)斷層。它的核心邏輯不復(fù)雜在模型生成答案之前先從外部知識(shí)庫里檢索出相關(guān)的事實(shí)片段把這些片段作為上下文一起喂給模型讓模型基于看得見的事實(shí)來推理而不是靠記憶瞎編。放到A股選股這個(gè)場(chǎng)景里知識(shí)庫就是財(cái)報(bào)文本、公告、研報(bào)摘要、行業(yè)新聞這些非結(jié)構(gòu)化數(shù)據(jù)檢索器負(fù)責(zé)在用戶提問時(shí)把最相關(guān)的片段撈出來生成器負(fù)責(zé)把這些片段和量化指標(biāo)結(jié)合起來輸出一份有依據(jù)的選股分析。這個(gè)項(xiàng)目我斷斷續(xù)續(xù)做了將近兩個(gè)月中間推翻重來過兩次架構(gòu)。第一版想直接用在線API但A股數(shù)據(jù)敏感性和調(diào)用成本讓我很快放棄第二版轉(zhuǎn)向本地部署用Ollama跑量化后的模型用Chroma做向量存儲(chǔ)用LangChain串起整個(gè)鏈路。最終跑通的這套方案單次完整分析從提問到輸出報(bào)告在消費(fèi)級(jí)顯卡上大約40秒檢索命中率經(jīng)過調(diào)優(yōu)后能穩(wěn)定在80%以上。適合誰來參考這篇復(fù)盤如果你已經(jīng)會(huì)用Python做數(shù)據(jù)處理對(duì)LLM有基本概念想做一個(gè)真正能跑起來的RAG項(xiàng)目而不是停留在demo階段那這篇內(nèi)容應(yīng)該能幫你省掉不少彎路。如果你完全沒接觸過向量數(shù)據(jù)庫和智能體編排建議先把LangChain的基礎(chǔ)鏈路跑通再回來看不然中間很多設(shè)計(jì)取舍你會(huì)覺得莫名其妙。提示這個(gè)項(xiàng)目涉及的所有數(shù)據(jù)均來自公開渠道分析結(jié)果僅用于技術(shù)驗(yàn)證不構(gòu)成任何投資建議。選股這件事本身風(fēng)險(xiǎn)極高技術(shù)方案再完善也不能替代獨(dú)立判斷。2. 技術(shù)選型為什么是Ollama Chroma LangChain這套組合2.1 本地部署模型這件事繞不開的取舍選Ollama最直接的原因是它把模型下載、量化、推理服務(wù)這三件事打包成了一個(gè)命令行工具。你不需要自己配CUDA環(huán)境、不需要手動(dòng)轉(zhuǎn)換模型格式、不需要寫推理服務(wù)ollama run qwen2.5:7b一行命令就能跑起來。對(duì)于我這種主要精力放在業(yè)務(wù)邏輯上、不想在環(huán)境配置上耗三天的人來說這個(gè)便利性是決定性的。但本地部署有個(gè)硬約束顯存。我手頭是一張12G顯存的卡7B參數(shù)的模型用Q4量化后大約占4.5G留給上下文和向量檢索的空間還算充裕。如果你要跑14B甚至32B的模型要么上更大的卡要么接受推理速度降到難以忍受的程度。我實(shí)測(cè)過14B Q4單次生成要一分半以上交互體驗(yàn)直接崩掉所以最終鎖定在7B級(jí)別。模型選擇上我試過三個(gè)qwen2.5:7b、llama3.1:8b、deepseek-r1:7b。qwen2.5在中文金融文本的理解上明顯更穩(wěn)尤其是對(duì)歸母凈利潤扣非ROE這類術(shù)語的把握llama3.1偶爾會(huì)把它們混為一談。deepseek-r1的推理鏈更長適合做復(fù)雜分析但輸出速度慢而且它的思考過程會(huì)占用大量token在RAG場(chǎng)景下反而拖累了整體響應(yīng)。最終主力模型用qwen2.5:7b復(fù)雜分析任務(wù)再切到deepseek-r1。關(guān)于Ollama下載慢的問題這個(gè)幾乎是每個(gè)國內(nèi)開發(fā)者都會(huì)遇到的。我的做法是配置鏡像源在環(huán)境變量里設(shè)置OLLAMA_HOST指向國內(nèi)可訪問的鏡像地址下載速度能從幾十KB/s提到幾MB/s。具體配置方式因網(wǎng)絡(luò)環(huán)境而異核心思路就是讓模型拉取走一條更通暢的通道。離線安裝包也是個(gè)選擇如果你有多臺(tái)機(jī)器要部署提前下好模型文件再分發(fā)會(huì)省很多事。2.2 Chroma為什么比FAISS更適合這個(gè)項(xiàng)目向量數(shù)據(jù)庫這塊我對(duì)比過FAISS、Chroma、Milvus三個(gè)。FAISS性能最強(qiáng)但它是純索引庫元數(shù)據(jù)管理、持久化、增刪改查都要自己寫一層封裝對(duì)于需要頻繁更新財(cái)報(bào)數(shù)據(jù)的場(chǎng)景來說太麻煩。Milvus功能全但部署重單機(jī)跑要起好幾個(gè)容器對(duì)一個(gè)個(gè)人項(xiàng)目來說屬于殺雞用牛刀。Chroma的定位剛好卡在中間它自帶持久化、支持元數(shù)據(jù)過濾、API設(shè)計(jì)極簡而且和LangChain的集成是官方級(jí)別的。我用它存了三類數(shù)據(jù)財(cái)報(bào)文本片段、公告摘要、研報(bào)觀點(diǎn)。每條記錄除了向量本身還帶了stock_code、report_date、doc_type這些元數(shù)據(jù)字段。這樣在檢索時(shí)可以加過濾條件比如只查2024年之后的、屬于新能源行業(yè)的公告避免把三年前的舊聞?chuàng)瞥鰜砀蓴_判斷。Chroma的默認(rèn)嵌入模型是all-MiniLM-L6-v2英文為主中文效果一般。我換成了BGE-M3這個(gè)模型對(duì)中文語義的捕捉明顯更好尤其是金融領(lǐng)域的專業(yè)表述。代價(jià)是嵌入速度慢一些但離線預(yù)處理階段可以接受。這里有個(gè)細(xì)節(jié)嵌入模型和生成模型是兩回事嵌入模型負(fù)責(zé)把文本轉(zhuǎn)成向量生成模型負(fù)責(zé)基于檢索結(jié)果寫分析兩者可以獨(dú)立替換。2.3 LangChain在鏈路里到底扮演什么角色很多人覺得LangChain是套殼但在這個(gè)項(xiàng)目里它確實(shí)省了不少事。最核心的價(jià)值是它把文檔加載→切分→嵌入→存儲(chǔ)→檢索→生成這條鏈路標(biāo)準(zhǔn)化了。我只需要繼承幾個(gè)基類就能把自定義的數(shù)據(jù)源接進(jìn)去不用自己寫調(diào)度邏輯。具體用到的組件DocumentLoader負(fù)責(zé)從PDF財(cái)報(bào)和CSV公告里抽文本TextSplitter做分塊VectorStore封裝Chroma的讀寫RetrievalQA把檢索和生成串起來。其中TextSplitter的參數(shù)調(diào)優(yōu)花了我不少時(shí)間后面會(huì)專門講。不過LangChain也有坑。它的版本迭代太快不同版本之間API差異很大網(wǎng)上搜到的教程經(jīng)常對(duì)不上。我的建議是鎖定一個(gè)穩(wěn)定版本把依賴寫死在requirements里不要盲目升級(jí)。另外它的默認(rèn)prompt模板偏通用金融場(chǎng)景需要自己重寫這個(gè)后面也會(huì)展開。3. 知識(shí)庫構(gòu)建從原始財(cái)報(bào)文本到可檢索的向量片段3.1 數(shù)據(jù)源的清洗比想象中臟得多A股財(cái)報(bào)的PDF格式五花八門有的帶復(fù)雜表格有的掃描件質(zhì)量差有的頁眉頁腳混在正文里。我一開始用PyPDF2直接抽文本結(jié)果表格全亂套數(shù)字和單位對(duì)不上這種數(shù)據(jù)喂給模型只會(huì)產(chǎn)生幻覺。后來換成了pdfplumber它對(duì)表格的解析能力強(qiáng)很多能保留行列結(jié)構(gòu)。但即便如此還是需要一套清洗規(guī)則去掉重復(fù)的頁眉頁腳、合并被換行切斷的句子、把單位萬元這類信息提取出來作為元數(shù)據(jù)、過濾掉純頁碼行。這套規(guī)則我寫了大概200行代碼覆蓋了80%的常見格式剩下的20%靠人工抽檢修正。公告數(shù)據(jù)相對(duì)規(guī)整因?yàn)榻灰姿墓娓袷奖容^統(tǒng)一用正則就能提取出標(biāo)題、日期、正文。研報(bào)摘要我主要從公開的行業(yè)報(bào)告里摘這部分量不大但質(zhì)量高作為補(bǔ)充知識(shí)源很有價(jià)值。注意數(shù)據(jù)清洗階段一定要保留原始文件的來源信息后面排查檢索問題時(shí)需要回溯到具體是哪份文件、哪一頁出的問題。我吃過這個(gè)虧有次檢索結(jié)果明顯不對(duì)但因?yàn)闆]記錄來源花了半天才定位到是一份格式錯(cuò)亂的PDF污染了向量庫。3.2 文本切分的粒度直接決定檢索質(zhì)量切分這件事看起來簡單實(shí)際上是最影響RAG效果的環(huán)節(jié)之一。切太大一個(gè)片段里混了好幾個(gè)主題檢索時(shí)噪聲大切太小上下文不完整模型拿到半句話沒法推理。我試過三種策略固定長度切分、按段落切分、按語義切分。固定長度比如500字實(shí)現(xiàn)最簡單但經(jīng)常把一句話從中間切斷。按段落切分保留了語義完整性但段落長度差異極大有的公告一段就20個(gè)字有的研報(bào)一段800字。最終我用的是遞歸切分優(yōu)先按段落切如果段落超過800字就按句子切如果句子還超就按字符切同時(shí)設(shè)置200字的重疊區(qū)保證跨片段的語義連貫。針對(duì)財(cái)報(bào)這種結(jié)構(gòu)化文本我還加了一條特殊規(guī)則把資產(chǎn)負(fù)債表利潤表現(xiàn)金流量表作為硬分隔符確保同一張表的數(shù)據(jù)盡量落在同一個(gè)片段里。這個(gè)調(diào)整讓財(cái)務(wù)指標(biāo)相關(guān)的檢索準(zhǔn)確率提升了大概15個(gè)百分點(diǎn)。3.3 元數(shù)據(jù)設(shè)計(jì)讓檢索能按條件撈光有向量相似度還不夠因?yàn)橛脩舻膯栴}往往帶條件。比如幫我找一下最近三個(gè)月新能源行業(yè)營收增長超過30%的公司這里最近三個(gè)月新能源行業(yè)營收增長超過30%都是過濾條件純向量檢索沒法處理。我的元數(shù)據(jù)字段設(shè)計(jì)如下字段名類型用途stock_codestring股票代碼用于精確匹配stock_namestring股票名稱industrystring所屬行業(yè)支持行業(yè)過濾report_datedate報(bào)告日期支持時(shí)間范圍過濾doc_typestring文檔類型財(cái)報(bào)/公告/研報(bào)sectionstring所屬章節(jié)如管理層討論metric_tagslist涉及的財(cái)務(wù)指標(biāo)標(biāo)簽有了這些字段檢索時(shí)可以先做元數(shù)據(jù)過濾縮小范圍再做向量相似度排序效率和準(zhǔn)確率都上來了。Chroma的where參數(shù)支持這種組合查詢寫起來也不復(fù)雜。4. 檢索鏈路調(diào)優(yōu)命中率從50%提到80%的實(shí)操過程4.1 最初的檢索為什么總撈不到對(duì)的東西項(xiàng)目剛跑通時(shí)我拿分析一下比亞迪2024年三季度的盈利能力這個(gè)問題測(cè)試檢索出來的片段里居然有寧德時(shí)代的內(nèi)容。排查后發(fā)現(xiàn)兩個(gè)問題一是嵌入模型對(duì)比亞迪和寧德時(shí)代的向量區(qū)分度不夠因?yàn)閮烧叨际切履茉窜嚻笪谋菊Z境相似二是沒有做元數(shù)據(jù)過濾檢索時(shí)把整個(gè)庫都掃了一遍。第一個(gè)問題的解法是換嵌入模型。BGE-M3相比默認(rèn)模型在中文實(shí)體區(qū)分上強(qiáng)不少換完之后比亞迪相關(guān)的片段基本不會(huì)再混入其他公司。第二個(gè)問題的解法是在檢索前先做實(shí)體識(shí)別把問題里的股票名稱或代碼提取出來作為元數(shù)據(jù)過濾條件傳給Chroma。4.2 混合檢索向量關(guān)鍵詞的雙保險(xiǎn)純向量檢索有個(gè)天然缺陷它對(duì)精確匹配不敏感。比如用戶問ROE連續(xù)五年大于15%的公司向量檢索可能返回一堆講盈利能力的片段但未必精確命中ROE這個(gè)指標(biāo)。這時(shí)候關(guān)鍵詞檢索BM25就派上用場(chǎng)了。我的方案是混合檢索向量檢索取Top 20BM25取Top 20然后用RRF倒數(shù)排名融合算法合并兩個(gè)結(jié)果取融合后的Top 10作為最終上下文。RRF的核心思想是一個(gè)片段如果在兩個(gè)檢索器里都排得靠前那它大概率是真正相關(guān)的。實(shí)測(cè)下來混合檢索比單用向量檢索的命中率高了將近20個(gè)百分點(diǎn)。實(shí)現(xiàn)上LangChain有現(xiàn)成的EnsembleRetriever可以組合多個(gè)檢索器我只需要把Chroma的向量檢索器和BM25檢索器傳進(jìn)去設(shè)置好權(quán)重就行。權(quán)重這塊我調(diào)了幾輪最終向量檢索占0.6、BM25占0.4這個(gè)比例在金融文本場(chǎng)景下比較平衡。4.3 重排序最后一公里的精度提升檢索出來的Top 10片段順序未必是最優(yōu)的。重排序Rerank的作用就是用一個(gè)小模型對(duì)這10個(gè)片段重新打分把最相關(guān)的排到最前面。我用的是BGE-Reranker它比嵌入模型更擅長判斷問題和片段的相關(guān)性因?yàn)樗芸吹絾栴}和片段的完整交互而不是各自獨(dú)立的向量。這一步的代價(jià)是增加了一次模型推理單次大約多花200毫秒。但收益很明顯重排序后最相關(guān)的片段基本能穩(wěn)定排在前三位模型拿到的上下文質(zhì)量高了一個(gè)檔次。如果你的場(chǎng)景對(duì)延遲不敏感強(qiáng)烈建議加上這一步。4.4 檢索失敗時(shí)的兜底策略再好的檢索也有失手的時(shí)候。我的兜底邏輯是如果重排序后的最高分低于某個(gè)閾值我設(shè)的是0.3就判定為檢索失敗這時(shí)候不硬答而是返回當(dāng)前知識(shí)庫中沒有找到足夠相關(guān)的信息請(qǐng)補(bǔ)充更多細(xì)節(jié)或換個(gè)問法。這個(gè)設(shè)計(jì)避免了模型在缺乏依據(jù)時(shí)胡編亂造雖然用戶體驗(yàn)上會(huì)多一次交互但比給出錯(cuò)誤分析要好得多。5. 智能體編排讓選股分析從一問一答變成多步推理5.1 為什么單輪RAG不夠用單輪RAG的流程是用戶提問→檢索→生成→返回。但選股分析往往需要多步先確定分析維度盈利能力、成長性、估值再針對(duì)每個(gè)維度檢索數(shù)據(jù)然后綜合各維度給出結(jié)論。如果把這些都塞進(jìn)一次檢索上下文會(huì)非常長而且不同維度的信息會(huì)互相干擾。所以我引入了智能體編排把整個(gè)分析拆成幾個(gè)子任務(wù)每個(gè)子任務(wù)獨(dú)立檢索、獨(dú)立生成最后由一個(gè)匯總節(jié)點(diǎn)整合。這樣每個(gè)子任務(wù)的上下文更聚焦生成質(zhì)量更高。5.2 用LangGraph搭一個(gè)簡單的分析流水線LangGraph是LangChain生態(tài)里做智能體編排的庫核心概念是節(jié)點(diǎn)和邊。我定義了幾個(gè)節(jié)點(diǎn)意圖識(shí)別節(jié)點(diǎn)判斷用戶的問題屬于哪類分析財(cái)務(wù)分析、行業(yè)對(duì)比、事件驅(qū)動(dòng)等維度拆解節(jié)點(diǎn)把問題拆成具體的分析維度檢索節(jié)點(diǎn)針對(duì)每個(gè)維度執(zhí)行混合檢索分析節(jié)點(diǎn)基于檢索結(jié)果生成該維度的分析匯總節(jié)點(diǎn)整合各維度分析輸出最終報(bào)告節(jié)點(diǎn)之間用條件邊連接比如意圖識(shí)別后根據(jù)類型走不同的分支。整個(gè)圖跑下來一次完整的選股分析大約經(jīng)過5到7個(gè)節(jié)點(diǎn)耗時(shí)40秒左右。這里有個(gè)設(shè)計(jì)取舍要不要讓智能體自己決定檢索什么我試過讓模型自主規(guī)劃檢索策略但效果不穩(wěn)定有時(shí)候它會(huì)漏掉關(guān)鍵維度。后來改成半自動(dòng)維度拆解由模型做但檢索策略是預(yù)設(shè)好的每個(gè)維度對(duì)應(yīng)固定的檢索模板。這樣犧牲了一點(diǎn)靈活性換來了穩(wěn)定性。5.3 提示詞工程讓模型說人話而不是套話金融場(chǎng)景的提示詞有個(gè)特殊要求既要專業(yè)準(zhǔn)確又要避免模棱兩可的表述。我最初的提示詞寫得太寬松模型輸出全是建議關(guān)注值得留意這種廢話。后來加了幾條硬約束每個(gè)結(jié)論必須引用具體的檢索片段作為依據(jù)涉及數(shù)字的地方必須標(biāo)注來源哪份財(cái)報(bào)、哪個(gè)日期禁止使用可能或許建議關(guān)注等模糊表述如果數(shù)據(jù)不足以支撐結(jié)論明確說數(shù)據(jù)不足這幾條約束加上去之后輸出質(zhì)量明顯提升。模型開始會(huì)說根據(jù)2024年三季報(bào)公司營收同比增長24.3%主要驅(qū)動(dòng)因素是...而不是公司業(yè)績表現(xiàn)良好。提示提示詞里的約束要具體、可驗(yàn)證不要寫請(qǐng)專業(yè)一點(diǎn)這種沒法執(zhí)行的要求。我一般會(huì)把約束寫成檢查清單的形式讓模型逐條對(duì)照。6. 實(shí)測(cè)中暴露的問題和我的修復(fù)方案6.1 模型幻覺數(shù)字對(duì)不上是最危險(xiǎn)的即便有RAG模型還是會(huì)在某些情況下編造數(shù)字。我遇到過最嚴(yán)重的一次檢索片段里明明寫的是凈利潤3.2億模型輸出成了凈利潤5.2億。排查后發(fā)現(xiàn)模型在整合多個(gè)片段時(shí)把另一家公司的數(shù)字串了進(jìn)來。修復(fù)方案有兩層一是在提示詞里強(qiáng)制要求每個(gè)數(shù)字必須緊跟著標(biāo)注來源片段編號(hào)這樣輸出后可以用腳本校驗(yàn)數(shù)字是否與來源一致二是在匯總節(jié)點(diǎn)加一個(gè)數(shù)字核對(duì)步驟用規(guī)則引擎把輸出里的數(shù)字和檢索片段里的數(shù)字做比對(duì)不一致就標(biāo)記出來讓模型重新生成。6.2 上下文超長導(dǎo)致的截?cái)?B模型的上下文窗口有限當(dāng)檢索片段太多時(shí)后面的內(nèi)容會(huì)被截?cái)?。我最初的配置是Top 10片段全塞進(jìn)去結(jié)果經(jīng)常超限。后來改成動(dòng)態(tài)控制根據(jù)每個(gè)片段的token數(shù)從高到低累加直到接近窗口上限就停止保證最相關(guān)的片段一定在上下文里。另外長片段我會(huì)做二次壓縮用一個(gè)小模型把片段里的冗余信息去掉只保留和問題相關(guān)的部分。這個(gè)壓縮步驟能減少30%到40%的token消耗讓更多片段能塞進(jìn)上下文。6.3 檢索延遲的優(yōu)化混合檢索加重排序單次檢索要跑三個(gè)模型嵌入、BM25、重排序延遲加起來有1秒多。對(duì)于交互式應(yīng)用來說有點(diǎn)慢。我的優(yōu)化手段是緩存把常見問題的檢索結(jié)果緩存起來下次同樣的問題直接命中緩存。緩存key用問題的嵌入向量做近似匹配相似度超過0.95就認(rèn)為是同一個(gè)問題。這個(gè)優(yōu)化讓重復(fù)問題的響應(yīng)時(shí)間降到了200毫秒以內(nèi)。6.4 Ollama服務(wù)偶發(fā)的500錯(cuò)誤跑長任務(wù)時(shí)遇到過幾次500 internal server error日志顯示是llama-server進(jìn)程崩了。排查下來主要是兩個(gè)原因一是并發(fā)請(qǐng)求太多Ollama默認(rèn)的并發(fā)數(shù)有限二是顯存不足長上下文把顯存吃滿了。解法是限制并發(fā)數(shù)在Ollama配置里設(shè)OLLAMA_NUM_PARALLEL1以及在代碼里加顯存監(jiān)控接近上限時(shí)主動(dòng)釋放緩存。7. 這套方案還能往哪些方向繼續(xù)打磨項(xiàng)目跑通之后我梳理了幾個(gè)可以繼續(xù)深化的點(diǎn)。第一個(gè)是引入GraphRAG把公司之間的股權(quán)關(guān)系、供應(yīng)鏈關(guān)系建成圖這樣檢索時(shí)能沿著關(guān)系鏈擴(kuò)展回答某公司的上游供應(yīng)商有哪些這類問題會(huì)更準(zhǔn)。第二個(gè)是加入時(shí)序分析把財(cái)務(wù)數(shù)據(jù)按季度對(duì)齊讓模型能識(shí)別趨勢(shì)而不只是看單點(diǎn)。第三個(gè)是接入實(shí)時(shí)行情目前知識(shí)庫是離線更新的如果能對(duì)接實(shí)時(shí)數(shù)據(jù)流分析的時(shí)效性會(huì)強(qiáng)很多。不過這些都是錦上添花核心鏈路已經(jīng)能穩(wěn)定跑通。我個(gè)人的體會(huì)是RAG項(xiàng)目最難的不是把鏈路搭起來而是把檢索質(zhì)量調(diào)上去。檢索不準(zhǔn)后面生成再花哨都是空中樓閣。所以如果你的項(xiàng)目效果不理想先別急著換模型回頭看看切分策略、嵌入模型、元數(shù)據(jù)設(shè)計(jì)這幾個(gè)環(huán)節(jié)大概率問題出在那里。最后分享一個(gè)我在調(diào)試時(shí)常用的小技巧把每次檢索的Top 10片段和最終輸出都存下來定期人工抽檢。看多了你會(huì)發(fā)現(xiàn)一些規(guī)律比如某類問題總是檢索不準(zhǔn)某個(gè)數(shù)據(jù)源的片段總是排在后面。這些觀察比任何自動(dòng)化指標(biāo)都更能指導(dǎo)優(yōu)化方向。