下面给你一个“企业内部文档 RAG”的完整叙事:从选择 → 设计 → 实现 → 原理 → 常见坑 → 解决方案。我会分别给出两套可落地的技术方案:
- 方案 A:NLTK 偏传统 IR + 轻量 NLP 的 RAG
- 方案 B:spaCy 偏工程化信息抽取 + 结构化增强的 RAG
并在每一步明确:NLTK/spaCy 在 RAG 里到底负责什么。
0. 先把 RAG 的“骨架”讲清楚(你后面所有设计都围绕它)
企业内部文档 RAG 的通用流水线:
- Ingestion:抓取/同步文档(SharePoint/Confluence/网盘/本地目录/数据库…)
- Extraction:把 PDF/DOCX/HTML/扫描件变成“尽量保真”的文本 + 结构(段落/标题/表格)
- Cleaning & Normalization:去页眉页脚、断行合并、列表/表格处理、编码统一、语言检测
- Chunking(切分):按策略切段 + 加 overlap + 绑定元数据
- Enrichment(可选但企业强烈建议):实体、关键词、部门/系统名、时间、权限标签、主题标签…
- Indexing:
- 向量索引(embedding → vector DB)
- 可选:关键词索引(BM25/倒排)
- Query Understanding:改写/扩展/实体抽取/过滤器生成(按部门、时间、系统、权限)
- Retrieval:向量召回 + 关键词召回(Hybrid) → 合并去重
- Rerank/Compression:交叉编码器重排、MMR 多样性、上下文压缩(只保留相关句子)
- Generation:把证据块塞进提示词,生成回答 + 引用
- Eval & Monitoring:召回率、引用正确率、幻觉率、权限泄露、延迟、成本
NLTK/spaCy 主要出现在 4) chunking、5) enrichment、7) query understanding、9) compression 这几步。
1. 选型:什么时候用 NLTK,什么时候用 spaCy(企业里最现实)
你选择的关键不是“哪个更强”,而是“你需要 NLP 到什么深度”
选 NLTK 的典型条件
- 你更重视 快速搭一个能跑的 RAG,NLP 只做:
- 句子切分
- 简单 token/关键词
- 停用词/词干/词形
- 传统 IR(BM25)侧的文本处理
- 文档以 英文为主(NLTK 对中文/日文不算强项)
- 你要做 可解释的关键词检索增强(企业 FAQ、制度条款很适合)
选 spaCy 的典型条件
- 你想做 结构化增强(企业非常常见):
- NER(系统名、产品名、部门、人名、地点、日期)
- noun chunks(名词短语)
- 依存句法(可选)
- 规则抽取(EntityRuler / Matcher)
- 你要一个 生产级 pipeline:速度、组件化、可扩展、可加自定义组件
- 你要做 “按实体/时间/部门过滤”的检索体验(用户问“上季度某系统的报销政策”)
2. 共同设计:企业内部文档 RAG 的“正确 chunk”长什么样
不管 NLTK 还是 spaCy,你最终要得到的基本数据结构都类似:
Chunk {
chunk_id
doc_id
text
metadata: {
title, section_path, page_range, url, source_system,
created_at, updated_at,
acl_groups / permissions,
language,
extracted_entities (optional),
keywords (optional)
}
embedding_vector
bm25_terms (optional)
}企业 RAG 里,chunk 设计成败的关键:
- chunk 不能太大(否则召回不准、上下文塞不下)
- chunk 不能太小(否则缺上下文、答案拼不起来)
- chunk 必须带结构元数据(标题/章节路径/页码/链接)
- chunk 必须带权限信息(ACL)并在检索时强过滤
经验阈值(给你一个工程起点)
- 以“模型 tokenizer 的 token”计:
- chunk:300–800 tokens
- overlap:50–150 tokens
- 文档是规章制度/合同:倾向更大 chunk(避免断章取义)
- 文档是 FAQ/操作手册:倾向更小 chunk(定位更精准)
注意:不要用“词数/字符数”当真实 token,企业里多语言 + 符号很容易失真。解决方案是用目标 LLM 的 tokenizer 做 token 计数。
3. 方案 A:NLTK 技术方案(传统 IR 强、轻量可解释)
A1) 设计理念
NLTK 方案适合做一个“hybrid RAG:BM25 + 向量”,其中:
- NLTK 负责:句子切分、token 处理、关键词提取、(可选)同义词扩展、文本规范化
- 向量模型负责:语义召回(embedding)
- 两路召回合并,然后 rerank
这个方案的优点:
- 工程复杂度低、解释性强(为什么召回这段:关键词命中)
- 对“制度/政策/条款”类文档非常稳(很多问法就是关键词)
缺点:
- 结构化抽取弱(实体/关系靠你自己写或额外模型)
- 多语言支持一般(中文/日文要引入额外分词/句子切分器)
A2) 实现步骤(从 0 到可用)
Step A:文档抽取与清洗(关键是“保结构”)
你抽取时要尽量保留:
- 标题层级(H1/H2/H3)
- 列表项
- 表格(至少转成“行文本”)
- 页码/段落边界
常见问题
- PDF 断行:每行硬换行导致句子切碎 解决:合并短行;遇到句号/冒号/列表符才断段
- 页眉页脚重复:召回时大量噪声 解决:检测高频重复行并移除
- 表格丢语义:表头对齐丢失
解决:表格转成
表头: 值的键值文本
(这部分通常用 unstructured / tika / pdfminer / docx 等做,NLTK 不负责抽取)
Step B:NLTK 句子切分 + chunk 组装
核心是:先按句子切,再聚合成 token 限制的 chunk。
import re
import nltk
from nltk.tokenize import sent_tokenize, word_tokenize
# nltk.download("punkt") # 初次需要
def normalize(text: str) -> str:
text = re.sub(r"[ \t]+", " ", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()
def build_chunks(text: str, max_words=220, overlap_words=40):
text = normalize(text)
sents = sent_tokenize(text) # 英文效果很好;中文需换方案
chunks = []
cur = []
cur_len = 0
for s in sents:
w = word_tokenize(s)
n = len(w)
if cur_len + n > max_words and cur:
chunk_text = " ".join(cur)
chunks.append(chunk_text)
# overlap:拿最后 overlap_words 词作为下一块开头
tail_words = word_tokenize(chunk_text)[-overlap_words:]
cur = [" ".join(tail_words)]
cur_len = len(tail_words)
cur.append(s)
cur_len += n
if cur:
chunks.append(" ".join(cur))
return chunks原理:RAG chunk 的“语义边界”最好落在句子边界;聚合到 token 上限确保可控。
Step C:NLTK 做关键词/术语抽取(给 BM25 & 元数据)
NLTK 在企业 RAG 里非常常用的一点是:可解释的关键词索引。
from nltk.corpus import stopwords
from nltk.stem import SnowballStemmer
# nltk.download("stopwords")
stop = set(stopwords.words("english"))
stemmer = SnowballStemmer("english")
def bm25_terms(text: str):
tokens = [t.lower() for t in word_tokenize(text)]
tokens = [t for t in tokens if t.isalnum() and t not in stop]
tokens = [stemmer.stem(t) for t in tokens]
return tokens用途:
- 建倒排/BM25(或给搜索引擎如 Elasticsearch)
- 给 chunk 生成
keywords元数据(调试/解释非常好用)
可选增强:WordNet 同义词扩展(适合 query expansion,但要小心引入噪声)。
Step D:Indexing(向量 + BM25 混合)
企业推荐 Hybrid:
- 向量召回:抓“语义相近”
- BM25 召回:抓“术语/条款/编号/系统名”
- 合并去重后 rerank
NLTK 在这里提供的价值:让 BM25 那路的文本处理更稳(停用词、词形、词干)。
Step E:Query Understanding(NLTK 版)
NLTK 可以做轻量 query 处理:
- 拼写规范化、去停用词、词干化
- Query 分句(复杂问题拆解成子查询)
- 简单规则抽取过滤器(如正则提取日期/编号)
企业里大量查询是“关键词 + 条件”,NLTK 足够支撑第一版。
A3) 常见坑与解决(NLTK 方案)
- 中文/日文切句/分词不靠谱
- 解决:中文用专用句子分割/分词器(即便你主框架是 NLTK)
- 或者:RAG 直接用字符长度 + 标点断句(很多情况下足够)
- token 计数与 LLM tokenizer 不一致
- 解决:用目标模型 tokenizer 来截断(不要用 word_count)
- 制度条款被切碎导致引用断章取义
- 解决:对“条款/编号/小节”使用结构化 chunk(按条款号/标题路径切)
- 重复页眉页脚进入索引导致召回污染
- 解决:抽取阶段做重复行检测;或 chunk 阶段过滤高频短句
4. 方案 B:spaCy 技术方案(结构化增强 + 可扩展流水线)
B1) 设计理念
spaCy 方案的目标是让 RAG 不只是“切块 + 向量”,而是:
- chunk 自带 结构标签(章节、段落类型、列表、表格行)
- chunk 自带 实体/术语元数据(系统名、部门、人名、日期、产品线)
- query 也做实体抽取 → 生成过滤器(faceted retrieval)
- 可做 上下文压缩:只从 chunk 中抽取与 query 相关的句子/实体邻域
这在企业内网非常有用:用户往往不是只问“是什么”,还会问“哪个部门/哪个系统/哪条政策/什么时候生效”。
B2) 实现步骤(从选择到实现)
Step A:语言与模型选择(这一步决定 spaCy 能不能发挥)
- 英文:
en_core_web_*通常可用(NER/依存/NP chunk 都成熟) - 中文/日文:要谨慎评估
- 你可以用
spacy.blank("zh")/spacy.blank("ja")+ 自定义分句/规则抽取 - 或用社区/第三方分词器(SudachiPy 等)接入 spaCy tokenizer
- 你可以用
企业真实情况往往是多语言混杂:最稳的策略是 language detection → 按语言走不同 pipeline。
Step B:Sentence Boundary(分句)先搞稳(RAG chunk 的地基)
spaCy 可以只用轻量 sentencizer(更快更可控),也可用 parser(更准但更重)。
import spacy
nlp = spacy.blank("en")
nlp.add_pipe("sentencizer") # 轻量分句
doc = nlp("Step 1: Do this. Step 2: Do that.\n- Bullet A\n- Bullet B")
for s in doc.sents:
print(s.text)企业文档的坑:缩写(e.g., “U.S.”)、编号(“1.2.3”)、列表符号会破坏分句。 解决:为特定格式加规则(比如对“1.”开头行强制断句;对常见缩写加入 tokenizer 例外)。
Step C:实体抽取(NER)+ 规则增强(企业核心)
企业内网最关键的是“自定义实体”:
- 系统名:OA、ERP、CRM、Jira…
- 部门名:财务部、人事部…
- 产品线/项目名:内部代号
- 文档类型:制度、SOP、公告、合同模板
- 时间表达:生效日期、版本号
spaCy 有两种路线:
- 直接用模型 NER(通用实体)
- 用 EntityRuler/Matcher 加规则(企业落地最常见、最稳定)
import spacy
from spacy.pipeline import EntityRuler
nlp = spacy.load("en_core_web_sm")
ruler = nlp.add_pipe("entity_ruler", before="ner")
patterns = [
{"label": "SYSTEM", "pattern": "ERP"},
{"label": "SYSTEM", "pattern": "CRM"},
{"label": "DOC_TYPE", "pattern": [{"LOWER": "expense"}, {"LOWER": "policy"}]},
]
ruler.add_patterns(patterns)
doc = nlp("According to the expense policy, ERP access requires approval.")
print([(ent.text, ent.label_) for ent in doc.ents])原理:企业术语是闭集/半闭集,规则命中率极高,且可维护。
Step D:基于结构 + 实体的“更聪明 chunking”
你可以把 chunking 做成“层级化”:
- 先按 标题/章节路径 切(Extraction 阶段拿到)
- 章节内按句子聚合到 token 上限
- overlap 不只是固定窗口,还可以对 实体密集处 保留更多上下文
spaCy 在这里的价值是:你可以把 chunk 的元数据做得很“企业化”:
entities: chunk 内出现的 SYSTEM/DEPT/DATEnoun_phrases: 用于关键词解释或 rerank 特征section_path: “财务制度 > 报销 > 差旅 > 2025版”
Step E:Query Understanding(spaCy 版的“检索前处理”更强)
同样一句 query:
“上季度 ERP 报销的机票标准是多少?”
spaCy 你可以抽:
- SYSTEM = ERP
- TIME = 上季度
- TOPIC = 报销 / 机票 / 标准(noun chunks / keywords)
然后构造检索策略:
- metadata filter:只搜 SYSTEM=ERP 的 chunk
- 时间优先:只搜最新版本(按 updated_at 或版本号)
- 向量召回 + 关键词召回双路合并
这会比纯向量召回稳得多(企业问题经常有“特定系统/特定版本”的硬约束)。
Step F:上下文压缩(Contextual Compression)
spaCy 可以把候选 chunk 进一步压缩:只保留与 query 相关的句子。 常见做法:
- 在 chunk 内对每句做相似度(embedding 或关键词命中)
- 保留 top-n 句子 + 其邻近句
- 让 LLM 看到更少噪声 → 更少幻觉
spaCy 帮你做的:句子边界稳定 + 实体定位准确,压缩不会破坏引用完整性。
B3) 常见坑与解决(spaCy 方案)
- 多语言模型不稳 / 无模型
- 解决:用
blank(lang)+ sentencizer + 规则抽取先落地 - NER 用 EntityRuler(企业术语闭集)替代通用模型
- 解决:用
- 企业文档格式怪(表格/编号/列表)导致 sents 乱
- 解决:在 extraction 阶段保留列表项为独立段;sentencizer + 自定义规则
- NER 抽取“漏/错”
- 解决:模型 NER 只做兜底;核心术语全部进 EntityRuler;版本迭代维护术语表
- 性能与吞吐
- 解决:只开启你需要的管线组件(别默认全开 parser/ner);批处理
nlp.pipe();缓存处理结果
- 解决:只开启你需要的管线组件(别默认全开 parser/ner);批处理
5. 两套方案怎么组合(企业里最常见的最终形态)
很多企业最后不是二选一,而是:
- spaCy 做结构化与实体层(过滤器、元数据、压缩)
- NLTK 做传统 IR 文本处理(停用词/词干/关键词 → BM25 更稳)
- 再加上向量索引(embedding)作为语义召回主力
一个很典型的“最终形态”:
- Index:
- 向量:embedding(chunk.text)
- 倒排:BM25(chunk.text_terms) ← 这里 NLTK 很顺手
- 元数据:entities/section_path/acl ← 这里 spaCy 很顺手
- Query:
- spaCy 抽实体 → metadata filter
- NLTK 处理关键词 → BM25 query terms
- hybrid retrieval → rerank → 压缩 → LLM 生成
6. 你最可能踩的企业级“硬坑”(不踩就不叫企业 RAG)
(1) 权限(ACL)不做强过滤 = 事故
必须:chunk 级别携带 acl_groups,检索时先过滤再算相似度(或至少在候选集阶段强过滤)。
不要指望 LLM “自己不说”。
(2) 版本与时效:制度每年改
必须在元数据里放:
effective_date / updated_at / version检索时默认“最新优先”,但允许用户指定“按某版本”。
(3) 证据引用链:回答必须可追溯
chunk 必须能回到:
- 原文链接、页码、章节路径 否则企业用户不会信,也无法审计。
(4) 评估:不要只看“回答像不像”
企业 RAG 的指标优先级通常是:
- 引用是否正确(groundedness)
- 是否遗漏关键条款(recall)
- 是否越权泄露(security)
- 才是文风与流畅度
7. 你要我给你“可直接开工”的落地清单(下一步怎么做)
如果你愿意按企业落地来做,我建议你先定 5 个输入(不用问我也能直接开干):
- 文档语言:中文/英文/混合?
- 文档类型:制度/手册/FAQ/技术文档/合同?
- 权限来源:AD 组?LDAP?还是文档系统自带?
- 目标 LLM 上下文长度:8k/32k/128k?
- 预期延迟:交互式(<3s)还是离线问答?
在你没给这些前,我给你一个默认可落地的推荐:
- 英文/混合且要结构化:以 spaCy 为主(sentencizer + EntityRuler + 自定义组件)
- 同时加 NLTK 做 BM25 terms(或接 Elasticsearch)
- hybrid 检索 + rerank + 句子级压缩
如果你告诉我你们内部文档主要是 中文还是英文、以及文档系统是什么(Confluence/SharePoint/飞书/语雀/本地 PDF),我可以把上面叙事进一步落到“具体实现模块划分 + 数据结构 + 关键代码骨架 + 推荐 chunk 策略参数”。