計(jì)到可解釋的報(bào)告生成)
上周末整理電腦里的文檔我再一次體會(huì)到了那種“明明都存了卻找不到”的抓狂某個(gè)項(xiàng)目的過(guò)程記錄散在三個(gè)txt里命名分別是“v1.txt”“備份-注意”“新建文檔(3).txt”。每份文件內(nèi)容我都記得大概但要定位某一段細(xì)節(jié)就得逐個(gè)打開(kāi)翻。放下鼠標(biāo)那一刻我下了決心與其繼續(xù)忍受這種“存了等于沒(méi)存”的狀態(tài)不如寫(xiě)一個(gè)本地小工具把這些txt統(tǒng)一掃一遍自動(dòng)告訴我每份文檔在講什么、高頻詞是什么、值得先讀哪幾句。這個(gè)東西我給它起名叫rea就是read-extract-annotate三個(gè)動(dòng)作的首字母組合讀取全文、抽取關(guān)鍵信息、給文檔做標(biāo)注。rea解決的核心問(wèn)題并不是“搜索”而是“不帶關(guān)鍵詞的預(yù)覽”——搜索需要我知道要找什么而rea反過(guò)來(lái)它讓我在不知道要找什么的情況下也能快速知道手上這堆文檔的主題分布。這個(gè)工具適合所有經(jīng)常和本地文本打交道的人尤其是普通開(kāi)發(fā)者、內(nèi)容運(yùn)營(yíng)、文檔整理強(qiáng)迫癥患者以及不放心把內(nèi)部資料傳到云端的人。1. 先說(shuō)清楚rea是拿來(lái)解決什么問(wèn)題的1.1 rea的定位不是搜索引擎是文檔體檢員rea從設(shè)計(jì)第一天就劃清了三件事read、extract、annotate。read負(fù)責(zé)遍歷文件夾把txt讀進(jìn)來(lái)extract負(fù)責(zé)算詞頻、給句子打分、抽關(guān)鍵詞annotate負(fù)責(zé)把結(jié)果整理成一份可閱讀的Markdown報(bào)告。整個(gè)鏈路都在本地完成不依賴任何網(wǎng)絡(luò)服務(wù)。rea不做什么這一點(diǎn)反而更重要不做全文檢索因?yàn)槟切枰ㄋ饕蛔稣Z(yǔ)義相似度匹配那是另一個(gè)深坑不做知識(shí)圖譜那是給自己找罪受。rea只在一個(gè)很窄的范圍里發(fā)力把一堆純文本快速變成“摘要關(guān)鍵詞重點(diǎn)句子”的索引報(bào)告。邊界劃得小代碼才可能短出了問(wèn)題也才能馬上定位。這其實(shí)是我做小工具的一個(gè)核心心得一開(kāi)始就把“不做什么”寫(xiě)明白比“能做什么”更省時(shí)間。沒(méi)有邊界的工具很快會(huì)變成一個(gè)裝滿了各種半成品功能的垃圾桶最后連自己都不敢用了。1.2 為什么不用現(xiàn)成方案非要自己寫(xiě)決定動(dòng)手前我把能走的路都過(guò)了一遍在線摘要API效果確實(shí)好但要把內(nèi)部文本傳出去光是第一關(guān)我就過(guò)不了開(kāi)源NLP流水線能力很強(qiáng)可為了跑通它得下載模型文件還要解決依賴版本對(duì)我“幾十個(gè)txt要快速出摘要”這個(gè)需求來(lái)說(shuō)明顯超重至于本地大模型自己部署硬件扛不住調(diào)用平臺(tái)又劃不來(lái)。最后我給自己定了一條原則所有邏輯必須在本地跑通依賴盡量只用Python標(biāo)準(zhǔn)庫(kù)每一步生成的結(jié)果都要能被人類看懂。這樣萬(wàn)一輸出不對(duì)我能順著分?jǐn)?shù)和中間結(jié)果一路查回去而不是對(duì)著一個(gè)黑盒模型發(fā)呆。rea的定位就是本地、輕量、可解釋這個(gè)定位決定了后面所有設(shè)計(jì)。1.3 第一版目標(biāo)不是“完美”而是“有反饋”最早我在紙上寫(xiě)目標(biāo)時(shí)只有一句話“輸入一個(gè)文件夾輸出一個(gè)txt報(bào)告別崩就行?!贝蟾哦写a跑通之后rea的第一版輸出雖然很粗糙——把每個(gè)文件的最高分兩三句拼成摘要、列出幾個(gè)高頻詞——但它讓我第一次看到了真實(shí)的中間反饋。有了反饋才知道該改哪里摘要選出的句子排序是亂的、關(guān)鍵詞里有一堆無(wú)意義單字、部分文件因?yàn)榫幋a問(wèn)題直接讀崩了。這些問(wèn)題是靠“想象”永遠(yuǎn)發(fā)現(xiàn)不了的只有真正跑起來(lái)才會(huì)浮出水面。我的習(xí)慣是先做一個(gè)丑但能跑的最小版把結(jié)果亮出來(lái)再?zèng)Q定下一步優(yōu)化方向。2. 第一版跑通讀文件、清洗、粗分句2.1 編碼問(wèn)題別讓第一關(guān)就翻車現(xiàn)實(shí)世界里的txt遠(yuǎn)比我們想的野。尤其從舊系統(tǒng)、某些導(dǎo)出工具里拿到的文本很多是GBK或GB18030編碼用utf-8硬讀輕則亂碼重則直接拋UnicodeDecodeError。rea讀文件時(shí)按順序嘗試utf-8、gbk、gb18030、latin-1成功就返回內(nèi)容同時(shí)記下實(shí)際用的編碼。這樣不管文件當(dāng)初怎么存的至少能先打開(kāi)。這段邏輯是我后來(lái)反復(fù)受益的rea有一次跑批處理十個(gè)文件里混進(jìn)了一個(gè)GBK編碼如果沒(méi)有這層兜底整個(gè)批處理會(huì)在跑到第三個(gè)文件時(shí)就中斷后面全是白干。多試幾種編碼的代價(jià)幾乎為零收益卻是“不中斷”太值了。def read_file(path): for enc in (utf-8, gbk, gb18030, latin-1): try: with open(path, r, encodingenc) as f: return f.read(), enc except UnicodeDecodeError: continue return , None2.2 清洗和分句做減法比做加法重要清洗這一步我只做了三件事全角空格換成半角、多個(gè)連續(xù)空格合并、去掉空行。很多人喜歡在這里加一堆復(fù)雜規(guī)則比如去特殊符號(hào)、去超鏈接、做繁體轉(zhuǎn)簡(jiǎn)體但我的經(jīng)驗(yàn)是清洗越重越容易出問(wèn)題你根本不知道哪些信息后面還有用。分句我用了一個(gè)很樸素的正則按中文和英文的句子結(jié)束標(biāo)點(diǎn)切分并且保留標(biāo)點(diǎn)。面對(duì)沒(méi)有空格的中文這可能是性價(jià)比最高的分句方式。雖然偶爾會(huì)把引號(hào)內(nèi)的句子攔腰截?cái)嗟趓ea“找重點(diǎn)句”這個(gè)場(chǎng)景下完全夠用。這也再次印證了那個(gè)原則先解決90%的常規(guī)情況剩下的5%等真實(shí)遇到了再處理。def split_sentences(text): parts re.split(r(?[。!?])\s*, text) return [s.strip() for s in parts if s.strip()]2.3 詞頻統(tǒng)計(jì)從連續(xù)漢字塊開(kāi)始rea初始版本沒(méi)有接任何分詞庫(kù)而是用一個(gè)更土的辦法把文本里的連續(xù)漢字片段找出來(lái)然后對(duì)每個(gè)片段做1到3個(gè)字的滑動(dòng)窗口統(tǒng)計(jì)。這樣做的好處是不受詞典覆蓋率限制任何文本進(jìn)來(lái)都能算出一個(gè)“頻率分布”。代價(jià)是會(huì)產(chǎn)生一些像“效果圖”這種切得不完美的候選但對(duì)于統(tǒng)計(jì)熱點(diǎn)詞這種場(chǎng)景半詞并不會(huì)帶來(lái)災(zāi)難。def collect_candidates(text): candidates collections.Counter() segments re.findall(r[\u4e00-\u9fa5]{2,}, text) for seg in segments: chars list(seg) for n in (1, 2, 3): if len(chars) n: continue for i in range(len(chars) - n 1): cand .join(chars[i:i n]) candidates[cand] 1 return candidates用真實(shí)語(yǔ)料測(cè)了一下“的數(shù)據(jù)”“問(wèn)題”這類泛化的三字片斷會(huì)排在很前面后來(lái)我加了停用詞過(guò)濾和長(zhǎng)度懲罰才逐漸壓住它們。3. 句子打分我用的是“局部熱度啟發(fā)式加成”3.1 為什么在小語(yǔ)料上先別用TF-IDF主流的文本摘要方法都會(huì)提到TF-IDF它的本意是降低“在越多文檔里出現(xiàn)過(guò)的詞”的權(quán)重讓只在一處出現(xiàn)的專屬詞有更高地位。聽(tīng)起來(lái)很合理但rea面對(duì)的是單篇文檔或最多十來(lái)篇的文件夾文檔集合太小。所謂IDF在這個(gè)規(guī)模下只代表“這個(gè)詞恰好沒(méi)有出現(xiàn)在另一篇里”而不是“這個(gè)詞在整個(gè)語(yǔ)料里少見(jiàn)”數(shù)值隨機(jī)性太高。打個(gè)比方學(xué)校里某個(gè)班就十個(gè)人你統(tǒng)計(jì)“誰(shuí)成績(jī)好”本來(lái)要看年級(jí)排名現(xiàn)在只能看班里排名班里沒(méi)選那門課的同學(xué)根本沒(méi)進(jìn)排名池子這個(gè)排名自然失真。rea在這里很務(wù)實(shí)直接使用“詞在本文檔里的出現(xiàn)次數(shù)”作為熱度指標(biāo)也就是局部TF。單篇文檔里反復(fù)出現(xiàn)的詞很大概率就是這篇文檔真正在講的東西。等語(yǔ)料真的龐大到某個(gè)量級(jí)再引入IDF才有意義。3.2 三個(gè)啟發(fā)式加成詞熱度、標(biāo)題詞、句長(zhǎng)區(qū)間給句子打分時(shí)rea用了三個(gè)成分的加權(quán)組合。核心是詞熱度把句子拆成1到3字的窗口每個(gè)窗口去查候選計(jì)數(shù)器累加它們的頻次句子命中主題詞的次數(shù)越多熱度自然越高。第二是標(biāo)題加成如果句子含標(biāo)題里的詞得分乘一個(gè)加倍數(shù)這相當(dāng)于給“直接點(diǎn)名主題”的句子開(kāi)了綠燈。第三是句長(zhǎng)區(qū)間10到60字之間的句子給固定加成太短可能信息量不足太長(zhǎng)則不適合當(dāng)摘要句。def score_sentence(sent, candidates, title_words): chars re.findall(r[\u4e00-\u9fa5], sent) hot 0 for n in (1, 2, 3): if len(chars) n: continue for i in range(len(chars) - n 1): w .join(chars[i:i n]) hot candidates.get(w, 0) if any(w in sent for w in title_words): hot * 2.0 if 10 len(sent) 60: hot 1.2 return hot這里面每個(gè)數(shù)字都不是拍腦袋拍出來(lái)的標(biāo)題加成倍數(shù)設(shè)2.0是為了讓一句“點(diǎn)名主題”的話能明顯超過(guò)普通高熱度句句長(zhǎng)只在合適區(qū)間給固定加成而不是按長(zhǎng)度線性加分是避免那種“又臭又長(zhǎng)”的堆砌句靠詞多混進(jìn)TopK。3.3 可解釋性也是產(chǎn)品性能的一部分rea堅(jiān)持用這套簡(jiǎn)單算法的最大理由是可以解釋。當(dāng)摘要選錯(cuò)句子時(shí)我卻可以把分?jǐn)?shù)拆開(kāi)看是詞熱度項(xiàng)拉高了還是標(biāo)題加成起了作用還是句長(zhǎng)區(qū)間加的分剛好把它送進(jìn)了前三。把誤差拆到具體成分之后調(diào)權(quán)重就變成了一個(gè)很安穩(wěn)的過(guò)程而不是對(duì)著黑盒反復(fù)試。我后來(lái)接手過(guò)幾個(gè)基于大模型做摘要的項(xiàng)目效果雖然好但一旦輸出不符合預(yù)期排查鏈路長(zhǎng)到讓人崩潰。rea這個(gè)項(xiàng)目反而讓我重新感受到一句話的含金量越簡(jiǎn)單的東西越容易被人接管。4. 關(guān)鍵詞抽取先算熱度再做去重合并4.1 候選池從1字窗口到3字窗口rea的關(guān)鍵詞候選池直接復(fù)用collect_candidates的統(tǒng)計(jì)結(jié)果里面同時(shí)包含單字、雙字、三字。單字里有大量“的、了、是、在”這類虛詞需要用一個(gè)盡量小的停用詞表過(guò)濾。我強(qiáng)烈建議停用詞表不要貪大——50到100個(gè)常用虛詞就夠了搞一個(gè)幾千詞的巨型表容易把“的士”“目的地”這種有效組合也一起誤傷。我本地維護(hù)的停用詞表大概是這樣一個(gè)小集合的了是在有和我你就他都它們這那一個(gè)也很于與及把被讓向從對(duì)為而或若但并。別看就這么點(diǎn)配合后面的長(zhǎng)度懲罰已經(jīng)能壓住絕大多數(shù)噪聲。4.2 打分與同前綴去重候選詞的分值按“頻次乘以長(zhǎng)度指數(shù)”計(jì)算長(zhǎng)度指數(shù)取1.4。為什么不是直接取平方或者立方因?yàn)橹笖?shù)太大長(zhǎng)詞會(huì)徹底碾壓短詞導(dǎo)致關(guān)鍵詞列表全是三字組合指數(shù)太小長(zhǎng)度又起不到區(qū)分作用。1.4這個(gè)值是我在幾篇不同風(fēng)格的文本上調(diào)出來(lái)的能讓同頻次的二、三字詞稍微排到前面但又不至于喧賓奪主。真正比較關(guān)鍵的是去重策略。中文文本有個(gè)特別常見(jiàn)的現(xiàn)象一個(gè)核心詞會(huì)帶出一堆同族詞比如“效果”“效果圖”“效果展示”“效果不錯(cuò)”如果不處理Top10關(guān)鍵詞會(huì)被這些同前綴詞霸占大半看著就很蠢。rea做了一個(gè)特別簡(jiǎn)單的合并只要兩個(gè)候選的前兩個(gè)字相同就當(dāng)作一族只保留熱度分更高的那一個(gè)。def top_keywords(candidates, topk10): scored [(freq * (len(w) ** 1.4), w) for w, freq in candidates.items()] scored.sort(reverseTrue) seen [] out [] for _, w in scored: if any(w[:2] old[:2] for old in seen): continue seen.append(w) out.append(w) if len(out) topk: break return out雖然“前兩個(gè)字相同就合并”粗暴得有點(diǎn)不像話但在rea的場(chǎng)景里非常好用。它不依賴任何詞向量或語(yǔ)義知識(shí)只靠字符串前綴做歸并結(jié)果關(guān)鍵詞列表很快就變得干凈了。4.3 什么時(shí)候這套玩法會(huì)失效要說(shuō)這套關(guān)鍵詞方案的短板也很明顯如果輸入的是跨領(lǐng)域綜述、密集引用型學(xué)術(shù)文章候選詞會(huì)出現(xiàn)大量“研究”“分析”“問(wèn)題”這種泛化詞因?yàn)樗鼈兇_實(shí)高頻但并不能代表文檔特征。另外rea不真正理解同義詞“數(shù)據(jù)”和“信息”在它眼里是兩個(gè)完全獨(dú)立的詞。真遇到這種場(chǎng)景我就得換帶詞典的分詞工具甚至引入詞向量做主題聚類那就不在今天這版rea的討論范圍里了。5. 摘要生成與報(bào)告輸出不只要準(zhǔn)還要能讀5.1 選句之后的順序還原拿到句子分?jǐn)?shù)之后最直接的做法是把得分最高的三句按分?jǐn)?shù)從高到低拼在一起。rea第一版就這么干過(guò)結(jié)果摘要看起來(lái)像三塊完全獨(dú)立的碎片拼貼思維跳躍嚴(yán)重人根本沒(méi)法一次讀下來(lái)。后來(lái)我加了一條規(guī)則先按分?jǐn)?shù)選出前K句然后按它們?cè)谠闹械南群箜樞蛑匦屡帕?。這個(gè)“順序還原”看似簡(jiǎn)單卻是摘要可讀性的分水嶺。閱讀體驗(yàn)遵循的是原文敘事邏輯而不是重要性順序。句子A是第3段的高分句句子B是第1段的高分句如果按分?jǐn)?shù)拼就是B后A前按原順序拼才是第1段B在第3段A前面讀起來(lái)自然很多。def make_summary(sents, scores, title, k3): ranked sorted(range(len(sents)), keylambda i: scores[i], reverseTrue)[:k] ranked.sort() summary .join(sents[i] for i in ranked) title_words re.findall(r[\u4e00-\u9fa5]{2,}, title or ) if title_words and not any(w in summary for w in title_words[:2]): summary sents[0] summary return summary[:500] or 未生成摘要句子長(zhǎng)度限制這里也順便處理了超過(guò)60字的句子基本不選除非整篇都找不到短句。長(zhǎng)句雖然可能信息密集但塞進(jìn)摘要里非常難讀而且容易把三個(gè)重點(diǎn)句的名額全部吃掉。5.2 完整度自查摘要里必須出現(xiàn)標(biāo)題詞rea有一個(gè)笨辦法做摘要質(zhì)量兜底如果生成的摘要里沒(méi)有出現(xiàn)標(biāo)題里的前兩個(gè)連續(xù)漢字詞就把原文第一句拼到最前面。為什么取“前兩個(gè)”因?yàn)橹形臉?biāo)題通常把主題詞放在最前面比如“數(shù)據(jù)采集模塊的斷點(diǎn)續(xù)傳設(shè)計(jì)”核心就是“數(shù)據(jù)采集”或“斷點(diǎn)續(xù)傳”摘要要是連這個(gè)都沒(méi)提到那它根本沒(méi)抓住文檔要義。這個(gè)規(guī)則有時(shí)候會(huì)引入不夠精準(zhǔn)的第一句但它保底的意義更大至少保證摘要與標(biāo)題有可驗(yàn)證的關(guān)聯(lián)。做內(nèi)容處理工具我寧可要“每天穩(wěn)定工作但有5%能看出問(wèn)題”也不要“偶爾驚艷但不可復(fù)現(xiàn)”。穩(wěn)定性本身就是輸出質(zhì)量的一部分。5.3 把結(jié)果落成Markdown報(bào)告rea的輸出格式從一開(kāi)始就定成Markdown不搞自定義格式。每篇文檔生成一個(gè)固定結(jié)構(gòu)的報(bào)告塊包括標(biāo)題、摘要、關(guān)鍵詞、高分句Top5、基礎(chǔ)統(tǒng)計(jì)。多個(gè)文件跑完所有報(bào)告塊合并起來(lái)就是一份文檔索引可以直接存成.md后續(xù)丟給其他工具繼續(xù)加工也方便。輸出模板大概是這個(gè)樣子的# 文件數(shù)據(jù)采集模塊v2.txt ## 摘要 該產(chǎn)品主要用于數(shù)據(jù)采集支持?jǐn)帱c(diǎn)續(xù)傳…… ## 關(guān)鍵詞 數(shù)據(jù)采集斷點(diǎn)續(xù)傳模塊 ## 高分句 Top5 1. 該產(chǎn)品主要用于數(shù)據(jù)采集…… 2. 斷點(diǎn)續(xù)傳功能在上次聯(lián)調(diào)中已通過(guò)……用Markdown還有個(gè)額外好處可以用Git做版本管理。我每周跑一次rea報(bào)告文件的變化就能通過(guò)Git歷史看到文本主題是怎么漂移的這個(gè)意外收獲算是做了一個(gè)小小的文本時(shí)間軸。6. 實(shí)測(cè)三份文檔被打臉之后我調(diào)了三個(gè)參數(shù)6.1 第一次跑摘要里混進(jìn)了一句正確的廢話第一版rea跑完三份測(cè)試文檔結(jié)果有驚喜也有翻車。驚喜是大部分摘要確實(shí)抓到了文檔主題翻車集中在第二份“某項(xiàng)目例會(huì)記錄”上它的摘要里出現(xiàn)了一句“這個(gè)功能很不錯(cuò)”。從單句看“這個(gè)功能很不錯(cuò)”確實(shí)是高分句句子短、長(zhǎng)度合適、包含“功能”這個(gè)中等熱度詞三項(xiàng)疊加剛好把它送進(jìn)前三。但它是一句正確的廢話對(duì)了解會(huì)議重點(diǎn)毫無(wú)幫助。這個(gè)案例特別典型因?yàn)樗┞冻鲆粋€(gè)本質(zhì)問(wèn)題句子打分時(shí)位置和主題關(guān)聯(lián)度的權(quán)重沒(méi)拉開(kāi)導(dǎo)致“說(shuō)起來(lái)通順但信息量低”的句子混進(jìn)了摘要。機(jī)器只看到了詞頻統(tǒng)計(jì)并沒(méi)意識(shí)到這句話是評(píng)價(jià)語(yǔ)而不是事實(shí)陳述。6.2 拆誤差是哪個(gè)成分把它送進(jìn)去的我做了個(gè)簡(jiǎn)單的誤差剖析。把“這個(gè)功能很不錯(cuò)”的得分拆開(kāi)看詞熱度中等句長(zhǎng)區(qū)間拿了固定加成標(biāo)題加成因?yàn)椴缓瑯?biāo)題詞而沒(méi)有觸發(fā)。三項(xiàng)疊加總分剛好卡進(jìn)前三。這時(shí)候我意識(shí)到標(biāo)題加成和位置加成太弱了弱到?jīng)]法把真正的主題句和這種“順口溜式句子”區(qū)分開(kāi)。隨后做了三個(gè)調(diào)整。第一標(biāo)題詞加成倍數(shù)從1.5提到2.0讓直接說(shuō)主題的句子明顯占優(yōu)。第二給每個(gè)自然段的段首句額外加15%權(quán)重中文寫(xiě)作習(xí)慣里段首句經(jīng)常承擔(dān)“總起”功能。第三句長(zhǎng)加成從線性加分改成“只在合適區(qū)間給固定分不在區(qū)間則為0”避免長(zhǎng)句靠詞多自然高分也避免短句靠句子短被高估。6.3 調(diào)參后的對(duì)比肉眼可見(jiàn)的收斂同一個(gè)文件夾里再跑一次第二份文檔的摘要變成了“本周重點(diǎn)確認(rèn)數(shù)據(jù)同步方案計(jì)劃周五前完成接口聯(lián)調(diào)”關(guān)鍵詞也從“功能”“數(shù)據(jù)”“同步”變成了“數(shù)據(jù)同步”“接口聯(lián)調(diào)”“方案確認(rèn)”信息密度明顯提高。表格對(duì)比一下調(diào)參前后的差異維度調(diào)整前調(diào)整后摘要是否含標(biāo)題詞偶爾缺失完整度自查兜底摘要中口水句比例中等明顯下降關(guān)鍵詞同族重復(fù)常見(jiàn)同前綴合并后基本消失長(zhǎng)句誤選概率較高句長(zhǎng)約束后下降參數(shù)敏感性方面我也把三個(gè)重要參數(shù)都試了一遍選句數(shù)量topK在3到5之間最穩(wěn)太少會(huì)漏重點(diǎn)、太多會(huì)稀釋閱讀前兩個(gè)字相同就合并效果最好前一個(gè)字相同會(huì)把正常雙字詞誤傷句長(zhǎng)下限設(shè)在10個(gè)字比較穩(wěn)低于這個(gè)值基本都是碎片。這三個(gè)參數(shù)后來(lái)我都暴露成命令行參數(shù)方便不同文件夾切換不同配置。7. 從“能跑”到“好用”批量、定時(shí)、可視化7.1 批量掃描別讓工具只能處理一個(gè)文件第一版rea一次只能處理單個(gè)文件后來(lái)加了一個(gè)scan_folder函數(shù)用os.walk遞歸遍歷整個(gè)文件夾只處理.txt結(jié)尾的文件并且主動(dòng)忽略以“rea_report”開(kāi)頭的輸出文件避免把自己生成的報(bào)告當(dāng)成輸入又跑一遍。這個(gè)細(xì)節(jié)看似無(wú)關(guān)緊要實(shí)際上非常有價(jià)值一旦忽略條件漏了下一次跑批就會(huì)把上一份報(bào)告也納入統(tǒng)計(jì)關(guān)鍵詞和摘要全都會(huì)被污染。def scan_folder(folder): for root, _, files in os.walk(folder): for name in files: if name.endswith(.txt) and not name.startswith(rea_report): yield os.path.join(root, name)有了批量能力之后rea就和系統(tǒng)計(jì)劃任務(wù)接上了每周定時(shí)跑一次自動(dòng)掃描指定文檔目錄生成一份“本周文檔速覽”報(bào)告。我不需要打開(kāi)任何文件只看報(bào)告就知道這周哪些文檔新增了、核心話題是什么。7.2 用標(biāo)準(zhǔn)庫(kù)做可讀的HTML報(bào)告Markdown報(bào)告雖然好但要給別人看還是HTML最通用。rea加了一個(gè)非常輕的輸出模式把所有Markdown塊套進(jìn)一個(gè)簡(jiǎn)單的HTML模板CSS全部?jī)?nèi)聯(lián)不依賴任何CDN資源生成一個(gè)雙擊就能在瀏覽器打開(kāi)的單文件頁(yè)面。這樣即使對(duì)方電腦里什么都沒(méi)有也能直接看報(bào)告??梢暬沧隽藗€(gè)特別樸素的字符詞云把關(guān)鍵詞按頻次排序高頻詞用更大的字號(hào)展示甚至可以生成一個(gè)字符矩陣讓詞頻決定字符重復(fù)次數(shù)。雖然丑但在“不想裝任何繪圖庫(kù)”的約束下非常實(shí)用。做小工具的樂(lè)趣就在這種地方——逼自己用最少的依賴把最核心的體驗(yàn)做出來(lái)。7.3 后續(xù)還想繼續(xù)做的幾個(gè)方向rea目前的版本還只處理純文本我在備忘里記了幾個(gè)想做的方向一是把會(huì)議記錄里的“以上”“也就是說(shuō)”“這塊”這類口頭過(guò)渡句識(shí)別出來(lái)并降權(quán)會(huì)議類文檔里它們占比極高二是支持docx和md解析讓rea的處理范圍從txt擴(kuò)展到日常工作的主流格式三是做相似句合并避免摘要里同一信息點(diǎn)出現(xiàn)兩遍。不過(guò)我對(duì)這些方向有一條底線保持rea的簡(jiǎn)單絕不因?yàn)榧庸δ芫捅成现匦鸵蕾嚒C考尤胍粋€(gè)組件都要先問(wèn)它是否值得用“依賴體積”和“排查復(fù)雜度”來(lái)?yè)Q取。到現(xiàn)在為止rea依然能在沒(méi)有任何第三方庫(kù)的Python環(huán)境里直接跑起來(lái)這就是我最滿意的狀態(tài)。如果你也想做一個(gè)類似的小工具我的建議是先別糾結(jié)模型選型也先別追求一次到位。拿最簡(jiǎn)單的方式把你的需求跑通讓結(jié)果先可見(jiàn)再根據(jù)真實(shí)輸出決定調(diào)權(quán)重、換算法還是加功能。工具是拿來(lái)解決問(wèn)題的不是拿來(lái)炫技的。哪怕它丑了點(diǎn)、土了點(diǎn)只要每次都能在關(guān)鍵時(shí)刻幫你省下十分鐘它就是一個(gè)值得繼續(xù)打磨的好工具。