基于RAGFlow實現(xiàn)「亂序」協(xié)議差異對比:Diff算法+向量相似度

7 月初知識星球的會員微信群中,有幾個星友問到一個條款存在內容和順序差異的協(xié)議對比問題,以及如何進一步封裝一個可視化頁面進行實現(xiàn)的需求。我在過去的咨詢項目中做過一個類似 demo,但是不是很完善。過去兩天花了點時間做了一些工程調參的優(yōu)化,初步效果比較穩(wěn)定了,這篇來做個思路分享。

這個需求可能會有盆友第一反應是,WORD中不是有合同對比的功能嗎,為什么還要重復造輪子?這是因為傳統(tǒng)的文本比對工具(如 WORD自帶的比較)在處理順序不一致的文檔時,會產生大量混亂的“偽差異”,幾乎無法使用。

解決這個順序差異的關鍵,就是要結合嵌入模型的“語義比對” 功能(本篇以 RAGFlow 的知識庫為依托實現(xiàn))。也就是先是通過計算向量相似度來建立不同文檔順序差異段落間的正確對應關系,再進行傳統(tǒng)的精細化差異分析(Diff 算法),就能準確地高亮出文字的增、刪、改。
這篇試圖說清楚:
測試文檔的使用場景和實際修改設計、WORD中的比較 vs 該系統(tǒng)的實現(xiàn)效果、系統(tǒng)的核心架構、核心環(huán)節(jié)拆解、工程實踐總結等內容。
以下,enjoy:
1、測試場景說明
測試文檔包含兩份協(xié)議,協(xié)議 A 是原合同,協(xié)議 B 則是修改后的版本。設定的使用場景是當下比較應景的甲方委托乙方進行大模型應用技術開發(fā)。

在協(xié)議修訂設計上, 為了盡量模擬真實場景中的談判要點,設計了以下幾處增刪改內容:
條款主題 | 協(xié)議 A(甲方初稿) | 協(xié)議 B(乙方修訂稿) | 差異分析 |
總費用 | 50 萬元 | 55 萬元 | 乙方漲價 5 萬元 |
支付方式 | 50%首付 + 50%驗收付 | 30%啟動+30%PoC+40%驗收 | 乙方分階段收款降低風險 |
知識產權歸屬 | 全部歸甲方 | 應用使用權歸甲方,底層技術歸乙方 | 乙方保留核心技術所有權 |
源代碼托管 | 強制要求托管 | 刪除該條款 | 乙方拒絕技術公開 |
甲方責任 | 未明確 | 新增數(shù)據(jù)提供、API 支持等義務 | 乙方要求甲方承諾資源支持 |
功能要求 | 未提及 PoC 階段 | 明確 PoC 驗收節(jié)點 | 乙方增加階段性驗證 |
2、實現(xiàn)效果對比
通過實際的效果截圖,來先直觀對比下 WORD 自帶對比工具和該系統(tǒng)的實際分析結果,最后再來總結下核心對比差異:
2.1WORD 實現(xiàn)效果


2.2該系統(tǒng)實現(xiàn)效果




檢測場景 | 該系統(tǒng)表現(xiàn) | 與傳統(tǒng)工具對比 |
1. 條款徹底刪除 | ? 主動標記刪除 | ? Word:僅顯示條款消失,無原因說明 |
2. 條款內容替換 | ? 識別實質替換 | ? Word:誤判為“刪除舊條款”+“新增無關條款”,掩蓋乙方移除甲方技術控制權的意圖 |
3. 支付條款重組 | ? 數(shù)值變更捕捉+結構分析 | ? Word:整段標黃,需人工核對數(shù)字;無法識別分期支付帶來的現(xiàn)金流風險 |
4. 新增限制性條款 | ? 語義關聯(lián)分析 | ? Word:孤立顯示“新增條款”,未關聯(lián)刪除動作,無法發(fā)現(xiàn)乙方用技術保留條款替代甲方所有權 |
5. 條款位移無修改 | ? 智能消歧 | ? Word:錯誤標記為“刪除+新增”,制造虛假變更點 |
3、系統(tǒng)架構一覽
整個協(xié)議比對分析流程可以大致分為三個主要階段:

3.1處理與入庫
數(shù)據(jù)輸入與解析:系統(tǒng)首先接收“協(xié)議 A.docx”和“協(xié)議 B.docx”作為“數(shù)據(jù)源”。接著,通過“解析與提取”步驟,把兩份文檔結構化,提取出獨立的條款或段落。
向量化與建庫:提取出的文本塊經過“向量化”處理,被存入 RAGFlow 知識庫,為后續(xù)的語義檢索做好準備。
3.2匹配與分析
語義檢索與匹配:以協(xié)議 A 的條款為基準,在知識庫中進行“語義搜索”,以查找協(xié)議 B 中與之最相似的對應條款,從而找出所有可能的新增、刪除和修改項。
評分與結果生成:搜索到的條款對會經過“多維評分”系統(tǒng),根據(jù)語義相似度等多個維度進行打分,最終形成明確的“匹配結果”。
智能分析與評估:匹配結果(即差異項)被送入“Gemini 分析”(本案例使用的是Gemini-2.5-flash)模塊,評估這些差異可能帶來的潛在影響。
3.3呈現(xiàn)與監(jiān)控
報告生成與展示:通過Web 用戶界面呈現(xiàn)給用戶。用戶可以在界面上看到類似傳統(tǒng) Diff 工具的高亮對比、由Gemini 生成的變更摘要以及深度的影響分析。
4、核心環(huán)節(jié)拆解
為了更清晰的展示每個核心環(huán)節(jié)的實現(xiàn)要點,下面按照數(shù)據(jù)流逐一進行拆解說明:DOCX 解析 → 條款分塊 → RAGFlow 向量化 → 語義匹配 → LLM 分析 → HTML 展示
4.1DOCX 解析階段 (universal_contract_parser.py)
功能: 從 Word 文檔提取結構化條款
def extract_clean_clauses(content: str, source_filename: str) -> list:
"""
從docx內容中提取干凈的條款信息
返回: [{'title': '第一條 服務內容', 'content': '純文本內容', 'source': '文件名'}, ...]
"""
lines = [line.strip() for line in content.split('\n') if line.strip()]
clauses = []
current_clause = None
for line in lines:
# 檢查是否是新條款的開始 - 增強正則表達式
match1 = re.match(r'^(第[一二三四五六七八九十百]+條[^\d]*)', line)
match2 = re.match(r'^(服務內容|項目周期|費用與支付方式|知識產權|保密義務|違約責任|爭議解決|其他)\s*$', line)雙重正則匹配:match1 處理標準格式,match2 處理缺失編號的情況
容錯機制:自動補充缺失的條款編號
4.2條款分塊階段(universal_contract_parser.py)
def process_contract_with_clean_extraction(docx_path: str) -> str:
"""處理協(xié)議文檔,提取條款并生成RAGFlow友好的格式"""
# 1. 解析DOCX文檔
doc = Document(docx_path)
# 2. 提取條款并分離內容與元數(shù)據(jù)
clauses = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text.startswith('第') and '條' in text:
# 提取條款標題作為元數(shù)據(jù)
clause_title = text
clause_content = ""
# 收集條款內容(直到下一個條款標題)
for next_para in doc.paragraphs[doc.paragraphs.index(paragraph)+1:]:
next_text = next_para.text.strip()
if next_text.startswith('第') and '條' in next_text:
break
if next_text:
clause_content += next_text + "\n"
# 存儲條款信息
clauses.append({
'title': clause_title,
'content': clause_content.strip(),
'source_document': os.path.basename(docx_path)
})元數(shù)據(jù)分離:條款標題等元數(shù)據(jù)單獨存儲,不參與向量化

分隔符設計:使用""分隔條款,避免向量化時的干擾
結構化存儲:每個條款包含 title、content、source_document 等字段
4.3RAGFlow 向量化階段(ragflow_correct.py)
def search_similar_clauses(self, kb_id: str, query: str, top_k: int = 10) -> List[Dict]:
"""基于向量相似度搜索條款"""
# 1. 將查詢文本轉換為向量
query_vector = self.model.encode(query)
# 2. 在知識庫中搜索相似向量
results = self.ragflow_client.search(
knowledge_base_id=kb_id,
query=query,
top_k=top_k,
score_threshold=0.3 # 相似度閾值
)
# 3. 返回相似度排序的結果
return [
{
'content': result.content,
'score': result.score,
'metadata': result.metadata
}
for result in results
]
向量化模型:使用 embedding 模型為每個條款生成語義
向量相似度計算:支持跨位置的語義相似度匹配
分塊策略:使用"naive"分塊保持條款完整性
閾值控制:通過 score_threshold 控制匹配精度
4.4語義匹配階段 (contract_comparer.py )
# 對于標題一致的,降低閾值要求;標題不一致的,提高閾值
threshold = 0.3 if title_consistent else 0.5
if best_match and best_score >= threshold:
content_a = clause_a.content.strip()
content_b = best_match.content.strip()
if content_a == content_b:
change_type = ChangeType.UNCHANGED
else:
change_type = ChangeType.MODIFIED
matches.append(ClauseMatch(
source_clause=clause_a,
target_clause=best_match,
similarity_score=best_score,
change_type=change_type
))
else:
# 條款A在協(xié)議B中不存在,標記為刪除
matches.append(ClauseMatch(
source_clause=clause_a,
change_type=ChangeType.DELETED
))
except Exception as e:
logger.error(f"匹配條款失敗 {clause_a.title}: {e}")
matches.append(ClauseMatch(
source_clause=clause_a,
change_type=ChangeType.DELETED
))腳本遍歷協(xié)議 A 的所有條款,在協(xié)議 B 的知識庫中搜索相似條款
通過相似度閾值判斷是否匹配
匹配成功且內容相同 → UNCHANGED
匹配成功但內容不同 → MODIFIED
匹配失敗 → DELETED
檢查哪些 B 條款沒有被匹配過,直接標記為ADDED。
4.5LLM 分析階段(gemini_client.py)
prompt = f"""
你是一位專業(yè)的法務分析師。請對以下 {len(clause_pairs)} 個條款對,依次進行對比分析。
{clauses_to_analyze_str}
請嚴格按照以下JSON格式返回一個包含 {len(clause_pairs)} 個分析結果的數(shù)組。每個分析結果對象都必須包含以下字段:
1. summary: 用一句話簡潔總結主要變化(不超過30字)。
2. key_changes: 關鍵變化點列表,每個變化點包含 'description', 'category', 'before', 'after'。
3. impact_level: 影響程度 (low/medium/high)。
4. change_type: 變化類型 (substantive/procedural/clarification)。
5. legal_risk: 法律風險評估 (如有)。
返回格式必須是包裹在json代碼塊中的JSON數(shù)組:
```json
[
{{
"summary": "條款對1的分析總結",
"key_changes": [
{{"description": "...", "category": "...", "before": "...", "after": "..."}}
],
"impact_level": "...",
"change_type": "...",
"legal_risk": "..."
}}
]
```
"""
return prompt4.6HTML 展示階段 (htmlreporteroptimized.py )
智能排序:核心變更優(yōu)先顯示,高風險條款突出顯示
精確對比:句子級和詞級的雙重diff算法
def _get_diff_html(self, text1: str, text2: str) -> (str, str):
"""增強的diff算法,支持細粒度的詞級對比"""
if not text1 and not text2:
return "", ""
if not text1:
return "", f'<ins>{html.escape(text2)}</ins>'
if not text2:
return f'<del>{html.escape(text1)}</del>', ""
# 按句子分割,更精確的對比
import re
sentence_pattern = r'[。;:]'
sentences1 = [s.strip() for s in re.split(sentence_pattern, text1) if s.strip()]
sentences2 = [s.strip() for s in re.split(sentence_pattern, text2) if s.strip()]
matcher = difflib.SequenceMatcher(None, sentences1, sentences2)
html1, html2 = [], []
for tag, i1, i2, j1, j2 in matcher.get_opcodes():
if tag == 'equal':
for sentence in sentences1[i1:i2]:
html1.append(html.escape(sentence) + '。')
for sentence in sentences2[j1:j2]:
html2.append(html.escape(sentence) + '。')
elif tag == 'replace':
# 進一步對比詞級差異
for idx, (old_s, new_s) in enumerate(zip(sentences1[i1:i2], sentences2[j1:j2])):
old_diff, new_diff = self._get_word_diff(old_s, new_s)
html1.append(old_diff + '。')
html2.append(new_diff + '。')
elif tag == 'delete':
for sentence in sentences1[i1:i2]:
html1.append(f'<del>{html.escape(sentence)}。</del>')
elif tag == 'insert':
for sentence in sentences2[j1:j2]:
html2.append(f'<ins>{html.escape(sentence)}。</ins>')
return '<br>'.join(html1), '<br>'.join(html2)5、工程實踐梳理
5.1分塊不準的問題
預處理之后 markdown 文檔雖然是按照 RAGFlow 的 naive 模式的默認的雙換行符進行分塊,默認的分塊大小是 128,第一次測試時發(fā)現(xiàn)兩份協(xié)議中的 10 個分段并不是被分成了 10 個塊。而是協(xié)議 A 生成 6 個分塊,協(xié)議 B 生成 7 個分塊。這說明 RAGFlow 在處理時自動合并了一些過短的條款。
過短分塊的情況
協(xié)議 A 有 6 個分塊長度 < 100 字符
協(xié)議 B 有 4 個分塊長度 < 100 字符
最短的只有 43 字符(第七條 違約責任)
RAGFlow 合并邏輯
RAGFlow 猜測是有最小分塊大小限制(比如 100 字符)當檢測到過短的分塊時,會與相鄰分塊合并。本著工程優(yōu)化的理念,需要通過調整參數(shù)來解決這個自動合并的問題,而不是對抗系統(tǒng)邏輯。
總的來說,RAGFlow 的分塊預設邏輯是默認先按分隔符切分,然后根據(jù) chunk_token_num 目標大小合并小片段。如果設置很小的 chunk_token_num,系統(tǒng)就傾向于保持原始分隔符的分割結果。為了更好的測試分塊的最佳數(shù)值,我寫了一個測試腳本 test_ragflow_chunk_size.py 進行了對比測試結果如下:
配置 | 協(xié)議 A 分塊數(shù) | 協(xié)議 B 分塊數(shù) | 效果 |
默認配置 128 | 6 | 7 | 分塊合并嚴重 |
chunk_size=30 | 10 | 10 | ? |
chunk_size=20 | 10 | 10 | ? |
chunk_size=10 | 10 | 10 | ? |
經過測試最后選擇了 30 作為分塊大小。需要說明的是,這個分塊選擇比較小和我設計的這個測試用例有直接關系,各位再自己手頭項目進行復現(xiàn)時,要根據(jù)實際情況調整。目標是確定的,就是要保證按照預處理的條款進行單獨的分塊,否則會影響后續(xù)流程的準確性。
logger.info("正在應用優(yōu)化的分塊配置...")
parser_config = {
"chunk_token_num": 30,
"delimiter": "\n\n",
"html4excel": False,
"layout_recognize": True,
"raptor": {"use_raptor": False}
}
doc.update({"parser_config": parser_config})5.2召回率和精度平衡的問題
在協(xié)議對比場景中,召回率=正確匹配的條款對數(shù)量/實際應該匹配的條款對總數(shù),預期目標肯定是盡可能找到所有真實的條款對應關系。關于召回率的高低,首先需要考慮大致三種情形:
小幅修訂:85-95%(大部分條款應該找到對應)
中等修訂:70-85%(有一些刪除重組但主體保留)
大幅重構:50-70%(結構性變化較大)
對于協(xié)議對比場景來說,核心原則是寧可多召回讓 LLM 判斷,也不要遺漏真實匹配。通過多維度評分過濾低質量匹配。同樣的,經過專門的測試腳本(test_matching_params.py )對 Top-k 和相似度閾值多組對照之后發(fā)現(xiàn):
?? 參數(shù)效果可視化(文本版)
================================================================================
1. Top-K 參數(shù)對匹配率的影響:
--------------------------------------------------
Top-K= 1: 0.0% (范圍: 0.0%-0.0%)
Top-K= 3: ██████████ 20.0% (范圍: 0.0%-30.0%)
Top-K= 5: ████████████ 25.0% (范圍: 10.0%-40.0%)
Top-K= 7: ████████████████████ 40.0% (范圍: 40.0%-40.0%)
Top-K=10: ██████████████████████ 45.0% (范圍: 30.0%-60.0%)
Top-K=15: █████████████████████████████████████████████ 90.0% (范圍: 90.0%-90.0%)
Top-K=20: ████████████████████ 40.0% (范圍: 40.0%-40.0%)
2. 相似度閾值對平均相似度的影響:
--------------------------------------------------
閾值=0.25: ████████████████████████ 0.493 (范圍: 0.493-0.493)
閾值=0.30: ██████████████████████████████ 0.605 (范圍: 0.605-0.605)
閾值=0.40: ████████████████████████████████████ 0.732 (范圍: 0.732-0.732)
閾值=0.50: ████████████████████████████████████ 0.732 (范圍: 0.732-0.732)
閾值=0.55: ████████████████████████████████████ 0.732 (范圍: 0.732-0.732)
閾值=0.60: ██████████████████████████████████████ 0.778 (范圍: 0.778-0.778)
閾值=0.65: ██████████████████████████████████████ 0.778 (范圍: 0.778-0.778)
閾值=0.70: ██████████████████████████████████████ 0.778 (范圍: 0.778-0.778)
閾值=0.80: █████████████████████████████████████████ 0.821 (范圍: 0.821-0.821)
閾值=0.85: 0.000 (范圍: 0.000-0.000)
閾值=0.90: 0.000 (范圍: 0.000-0.000)
3. 參數(shù)組合效果矩陣 (匹配率%):
--------------------------------------------------
閾值\Top-K 1 3 5 7 10 15 20
0.25 -- -- -- -- -- 90.0 --
0.30 -- -- -- -- 60.0 -- --
0.40 -- -- -- -- -- -- 40.0
0.50 -- -- -- 40.0 -- -- --
0.55 -- -- 40.0 -- -- -- --
0.60 -- 30.0 -- -- -- -- --
0.65 -- -- -- -- 30.0 -- --
0.70 -- 30.0 -- -- -- -- --
0.80 -- -- 10.0 -- -- -- --
0.85 -- 0.0 -- -- -- -- --
0.90 0.0 -- -- -- -- -- --總之,Top_k 并非越大越好,雖然增加找到正確匹配的概率,尤其是對于位置變化大的條款更有效。但會增加計算開銷,可能引入更多噪音,也可能會降低匹配精度(如果閾值設置不當)。
try:
chunks = kb.list_chunks(document_id=doc.id)
logger.info(f"分塊完成,共生成 {len(chunks)} 個分塊")
for i, chunk in enumerate(chunks[:3]):
content = chunk.get('content_with_weight', chunk.get('content', ''))
logger.info(f"分塊 {i+1} 預覽: {content[:100]}...")
except Exception as e1:
logger.info(f"list_chunks方法失敗: {e1}")
# 方法2: 嘗試通過搜索獲取分塊信息
try:
search_results = kb.search("", top_k=10) # 空查詢獲取所有分塊
logger.info(f"通過搜索發(fā)現(xiàn) {len(search_results)} 個分塊")
for i, result in enumerate(search_results[:3]):
logger.info(f"分塊 {i+1} 預覽: {result.get('content', '')[:100]}...")
except Exception as e2:
logger.info(f"搜索方法也失敗: {e2}")
# 方法3: 檢查文檔狀態(tài)
try:
doc_list = kb.list_documents(id=doc.id)
if doc_list:
doc_info = doc_list[0]
logger.info(f"文檔狀態(tài)詳情: run={doc_info.run}, progress={doc_info.progress_msg}")
if hasattr(doc_info, 'chunk_num'):
logger.info(f"文檔分塊數(shù): {doc_info.chunk_num}")
except Exception as e3:
logger.error(f"獲取文檔詳情失敗: {e3}")5.3優(yōu)化效果驗證問題
有了前兩步的調優(yōu)后,還差個最后一步閉環(huán)驗證。通過 verify_optimization.py腳本最后評估下:量化指標驗證(匹配率、變更分布、問題密度)、多維度質量評估(不僅看召回率,還要看誤匹配率、可疑匹配數(shù))、智能診斷機制(基于經驗模式識別問題并提供具體建議)、基準線建立(為后續(xù)迭代提供質量基準)。
def quick_verify_optimization():
"""快速驗證參數(shù)優(yōu)化效果"""
# 1. 使用優(yōu)化后的參數(shù)進行完整測試
matches = comparer.compare_contracts(contract_a, contract_b)
# 2. 計算關鍵性能指標
total_clauses = len(matches)
matched_clauses = len([m for m in matches if m.target_clause is not None])
match_rate = (matched_clauses / total_clauses) * 100
# 3. 分析變更分布
unchanged = len([m for m in matches if m.change_type.value == "unchanged"])
modified = len([m for m in matches if m.change_type.value == "modified"])
deleted = len([m for m in matches if m.change_type.value == "deleted"])
added = len([m for m in matches if m.change_type.value == "added"])
# 4. 基于經驗閾值評估效果
if match_rate >= 60:
print(" ? 優(yōu)秀:匹配率已達到期望水平")
elif match_rate >= 40:
print(" ? 良好:匹配率有明顯提升")
elif match_rate >= 30:
print(" ?? 一般:有輕微提升,需要進一步優(yōu)化")
else:
print(" ? 需改進:匹配率仍然較低,需要深層優(yōu)化")
# 5. 智能診斷和后續(xù)建議
if deleted > total_clauses * 0.3:
print(" 1. 刪除條款過多,考慮進一步降低閾值")
if match_rate < 50:
print(" 2. 考慮實施二階段匹配策略")
if modified > unchanged:
print(" 3. 大量修改可能包含誤匹配,建議人工抽查")這種驗證驅動的調試模式確保了理論優(yōu)化到實際效果的轉化,防止過度優(yōu)化破壞系統(tǒng)穩(wěn)定性,同時把調試經驗固化為可執(zhí)行的驗證邏輯,形成完整的"測試→優(yōu)化→驗證"工程閉環(huán)。
6、寫在最后
6.1技術路線迭代
傳統(tǒng)算法(如 Myers差分、Levenshtein 距離)本質是文本序列比對工具,其技術基因決定了其局限性。上述示例展示的協(xié)議對比優(yōu)化效果總結如下:
- 語義理解: 識別內容相同但位置不同的條款
- 智能匹配: 處理條款編號變化和結構調整
- 深度分析: 區(qū)分實質性修改 vs 格式調整
- 可視化: 提供清晰的差異展示和統(tǒng)計
數(shù)據(jù)驅動決策
在眾多工程優(yōu)化方向中,一種經典且有效的做法就是通過實際測試找到最優(yōu)配置。在這個協(xié)議對比場景中,可以為不同類型的協(xié)議找到合適的參數(shù),也建議將測試無誤的最優(yōu)參數(shù)保存為配置文件以便長期維護復用。




























