1. 从"能聊天"到"能干活":对话式AI的第四道分水岭
如果你已经跟着这个系列一路做下来,前三篇里我们聊的都是"怎么让模型把话说顺"——提示词怎么写、上下文怎么管、多轮对话怎么不串味。但到了第四篇,事情开始变得不一样了:我们要解决的是模型怎么知道它原本不知道的事。
这个问题的专业叫法是 RAG(Retrieval-Augmented Generation,检索增强生成)。说白了就是:大模型的知识截止在训练那一天,你问它公司内部文档、最新产品手册、私有数据库里的内容,它要么答不上来,要么一本正经地胡说八道。RAG 的思路很朴素——先去你的资料库里把相关内容捞出来,再连同问题一起塞给模型,让它"看着材料答题"。
我见过太多人卡在这一步:LangChain 的文档翻了三遍,ChromaDB 装好了,向量也存进去了,结果一问还是答非所问。问题往往不在模型,而在检索这一环。这篇就把对话式 AI 从"闲聊玩具"升级成"业务助手"的完整链路拆开讲,包括 LangChain 的组件怎么选、ChromaDB 怎么用才不踩坑、RAG 的瓶颈到底卡在哪、以及知识库该怎么分类。适合已经跑通过基础对话、想往实用方向推进的开发者,也适合被"检索不准"折磨过的朋友。
2. RAG 到底在解决什么问题:先想清楚再动手
2.1 大模型的三条知识边界
在动手写代码之前,得先明白模型的能力边界在哪。我把它归纳成三条线:
- 训练数据边界:模型只知道训练语料里出现过的东西。你公司上周刚定的报销制度,它不可能知道。
- 时效边界:即使训练数据里有,也是截止到某个时间点。今天发生的新闻,它答不了。
- 私有数据边界:内部文档、客户资料、代码仓库,这些从来就不在公开训练集里。
RAG 针对的正是后两条。第一条其实也能缓解——只要你的私有资料被检索进来,模型就能"临时学会"。这就是为什么 RAG 成了企业落地大模型最主流的方案:不用重新训练模型,成本低、更新快、可溯源。
2.2 为什么不是微调,而是检索
经常有人问:我直接拿自己的数据微调一个模型不行吗?行,但要看场景。微调适合改变模型的风格和行为模式,比如让它说话更像某个客服;而 RAG 适合注入事实性知识。原因有三:
第一,微调的知识更新成本极高。你改一份文档就得重新训一遍,而 RAG 只要更新向量库里的那一条记录。第二,微调容易"灾难性遗忘",新知识学进去,旧能力可能掉下来。第三,RAG 能给出引用来源,用户能看到答案是从哪段材料来的,这在客服、法务、医疗场景里几乎是刚需。
所以我的建议很明确:知识类需求优先上 RAG,风格类需求才考虑微调,两者可以叠加。
2.3 一个最小可用的 RAG 心智模型
别被各种框架名词吓到,RAG 的核心就四步,我用一个图书馆的类比讲清楚:
- 切块(Chunking):把厚书拆成一页页卡片。文档太长,模型一次读不完,得切。
- 向量化(Embedding):给每张卡片编一个"语义坐标",内容相近的卡片坐标也相近。
- 检索(Retrieval):用户提问时,把问题也转成坐标,找出坐标最近的几张卡片。
- 生成(Generation):把卡片和问题一起交给模型,让它基于卡片作答。
LangChain 干的事,就是把这四步用统一的接口串起来;ChromaDB 干的事,是当那个存卡片坐标的仓库。理解了这四步,后面所有配置你都能对上号。
3. LangChain 组件选型:别被"全家桶"绑架
3.1 为什么很多人用 LangChain 反而更乱
LangChain 最大的问题是抽象层太多。一个简单的检索问答,它能给你整出 DocumentLoader、TextSplitter、Embeddings、VectorStore、Retriever、Chain、Agent 七八个概念。新手一上来就想把所有组件都用上,结果调试时根本不知道哪一层出了问题。
我的经验是:先用最少的组件跑通,再按需加。一个能用的 RAG,其实只需要四样东西——加载器、切分器、向量库、模型。其余的 Retriever 封装、Chain 编排,等你发现原生写法不够用了再引入。
3.2 核心组件的实际取舍
下面这张表是我在实际项目里反复验证过的选型参考:
| 组件 | 常见选项 | 我的推荐场景 |
|---|---|---|
| 文档加载 | PyPDF、Unstructured、TextLoader | 纯文本用 TextLoader,PDF 优先 Unstructured |
| 文本切分 | RecursiveCharacterTextSplitter | 九成场景够用,按语义层级递归切 |
| 向量化 | OpenAI Embeddings、本地 BGE | 有预算用云端,数据敏感用本地模型 |
| 向量库 | ChromaDB、FAISS、Milvus | 中小规模 ChromaDB,超大规模上 Milvus |
这里重点说切分。很多人栽在 chunk_size 上,随手设个 1000 就不管了。实际上切分大小直接决定检索质量:切太大,一块里混了好几个主题,检索出来噪声多;切太小,一句话被拦腰截断,语义不完整。我一般从 500 字符起步,配合 50 到 100 的重叠(overlap),保证跨块的句子不被切断。
3.3 一个不绕弯的 LangChain 骨架
抛开那些花哨封装,核心代码其实很直白:
from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings, ChatOpenAI # 1. 加载 loader = TextLoader("knowledge.txt", encoding="utf-8") docs = loader.load() # 2. 切分 splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=80, separators=["\n\n", "\n", "。", "!", "?", " ", ""] ) chunks = splitter.split_documents(docs) # 3. 入库 vectorstore = Chroma.from_documents( documents=chunks, embedding=OpenAIEmbeddings(), persist_directory="./chroma_db" ) # 4. 检索 + 生成 retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) question = "报销流程是怎样的?" context_docs = retriever.invoke(question) context = "\n\n".join([d.page_content for d in context_docs]) llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) prompt = f"仅根据以下材料回答问题,材料中没有的信息就说不知道。\n\n材料:\n{context}\n\n问题:{question}" print(llm.invoke(prompt).content)注意separators里我加了中文标点。默认的切分器只认英文标点和空格,处理中文文档时经常在句子中间断开,加上中文句号、问号、感叹号后,切出来的块语义完整度高很多。这是中文场景下特别容易被忽略的一个细节。
4. ChromaDB 实战:向量库用对了才叫 RAG
4.1 为什么选 ChromaDB 而不是别的
向量库这个赛道选择很多,FAISS 快但不管持久化,Milvus 强但部署重。ChromaDB 的定位很讨巧:轻量、能持久化、API 简单、支持元数据过滤。对于个人项目、中小团队、原型验证,它几乎是零门槛——pip install chromadb就能跑,数据默认落盘到本地目录。
我特别看重它的元数据过滤能力。比如你有一堆文档,想只检索"2024年之后"且"属于技术部"的内容,ChromaDB 的where条件能直接搞定,不用自己写过滤逻辑。这在多租户或者多分类知识库场景里非常实用。
4.2 持久化与增量更新:别每次都重建
新手最常见的浪费是:每次启动程序都重新from_documents一遍,把整个知识库重新向量化。这既费钱又费时。正确做法是判断库是否存在,存在就加载,不存在才创建:
import os from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings PERSIST_DIR = "./chroma_db" embeddings = OpenAIEmbeddings() if os.path.exists(PERSIST_DIR) and os.listdir(PERSIST_DIR): vectorstore = Chroma( persist_directory=PERSIST_DIR, embedding_function=embeddings ) print("已加载现有向量库") else: vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory=PERSIST_DIR ) print("新建向量库完成")增量更新时,用add_documents追加新内容即可。但要注意:ChromaDB 不会自动去重。同一份文档如果被加了两次,检索时会返回重复结果,白白占用上下文窗口。我的做法是给每个 chunk 生成一个基于内容哈希的 ID,入库前先查一下这个 ID 存不存在。
4.3 检索参数调优:k 值和相似度阈值
k值(返回几条结果)不是越大越好。k 太小,可能漏掉关键信息;k 太大,噪声挤占上下文,模型反而抓不住重点。我的经验区间是3 到 6,具体看你的 chunk 大小——块小就多取几条,块大就少取几条。
更进阶的是加相似度阈值。ChromaDB 默认返回距离最近的 k 条,哪怕这些结果其实和问题八竿子打不着。加一个阈值过滤,能有效避免"检索不到就硬编"的情况:
retriever = vectorstore.as_retriever( search_type="similarity_score_threshold", search_kwargs={"k": 5, "score_threshold": 0.3} )阈值设多少要试。设太高,正常问题也检索不到;设太低,等于没过滤。我一般从 0.3 开始,根据实际问答效果微调。这一步做完,你会发现"答非所问"的比例明显下降。
5. RAG 的瓶颈到底卡在哪:三个真实故障现场
5.1 检索到了,但模型没用上
这是最隐蔽的瓶颈。你明明检索出了正确文档,模型却还是按自己的记忆回答。原因通常是提示词没约束好。如果你只说"参考以下材料回答",模型很可能把材料当背景音,继续用自己的知识。
解决办法是把约束写死:"仅根据以下材料回答,材料中没有的信息,直接回答'根据现有资料无法确定'"。这句话看着简单,但能挡掉大量幻觉。另外,把材料放在问题前面,模型对靠前内容的注意力通常更高。
5.2 检索不到,因为问法和文档用词不一致
用户问"怎么请假",文档里写的是"休假申请流程"。字面没有重叠,纯关键词检索就废了。这正是向量检索的价值——它比的是语义相似度,不是字面匹配。但向量检索也不是万能,如果 embedding 模型对中文支持不好,语义相近的词也可能距离很远。
我的应对策略是混合检索:向量检索 + 关键词检索(BM25)各取一批结果,再合并去重。LangChain 里有EnsembleRetriever可以直接做这件事。实测下来,混合检索在专业术语多的场景里,召回率比纯向量高出一截。
5.3 上下文塞太满,模型"中间失忆"
有个被反复验证的现象:当上下文很长时,模型对开头和结尾的信息记得牢,中间部分容易忽略,这叫"迷失在中间"(Lost in the Middle)。如果你一次塞进去十几条检索结果,关键那条恰好排在中间,模型很可能视而不见。
对策有两个:一是控制 k 值,别贪多;二是重排序(Rerank)——先粗检索一批,再用一个专门的重排模型精排,把最相关的放到最前面。重排模型比向量检索慢,但只对少量候选做,成本可控,效果提升明显。
6. 知识库不是一锅炖:三类知识库的区分与选型
6.1 RAG 知识库、KG 知识库、结构化知识库
很多人把"知识库"当成一个东西,其实至少要分三类,它们的存储方式、检索方式和适用场景完全不同:
| 类型 | 存储形式 | 检索方式 | 典型场景 |
|---|---|---|---|
| RAG 知识库 | 向量 + 原文块 | 语义相似度 | 文档问答、客服 |
| KG 知识库 | 实体-关系三元组 | 图遍历、路径查询 | 关系推理、风控 |
| 结构化知识库 | 表 / SQL | 精确查询、聚合 | 报表、统计、订单 |
RAG 知识库擅长"从大段文字里找答案",但它不擅长推理。你问"张三的上级的部门经理是谁",这种多跳关系问题,向量检索基本抓瞎,而知识图谱(KG)能顺着关系链一路查下去。结构化知识库则适合"上个月销售额是多少"这类精确计算。
6.2 什么时候该上知识图谱
不是所有项目都需要 KG。我的判断标准是:如果你的问题涉及多跳关系、需要精确的实体连接,才考虑 KG。比如企业组织架构、供应链关系、医疗诊断路径。如果只是"文档里说了什么",RAG 就够了,硬上 KG 是过度设计。
现在也有 Ontology RAG 这种融合思路——用本体(Ontology)来约束和增强检索,让向量检索带上结构化的语义。这属于进阶玩法,建议先把基础 RAG 跑稳再研究。
6.3 混合架构:让三类知识库各司其职
真实的企业助手往往是混合的。用户问"帮我查一下上季度华东区的销售情况,并解释一下为什么下滑",这个问题同时需要结构化查询(销售数据)和 RAG(分析报告)。我的做法是让一个 Agent 做路由:先判断问题类型,再决定调用哪个知识库,最后把结果汇总给模型生成回答。
这就是 Agent 框架的价值所在。LangChain、Dify、CrewAI 各有侧重:LangChain 灵活但偏底层,Dify 可视化适合快速搭建,CrewAI 擅长多智能体协作。选哪个取决于你的团队——要快速出原型选 Dify,要深度定制选 LangChain,要做多角色协作看 CrewAI。
7. 从零跑通一个可用的 RAG 助手:完整实操链路
7.1 环境准备与依赖安装
先把地基打好。Python 建议 3.10 以上,太老的版本有些库装不上:
python -m venv rag_env source rag_env/bin/activate # Windows 用 rag_env\Scripts\activate pip install langchain langchain-community langchain-openai chromadb如果你要用本地 embedding 模型,还得装sentence-transformers。这里提醒一句:别在全局环境里装这些库,依赖冲突能让你排查到怀疑人生,虚拟环境是底线。
7.2 文档预处理:清洗比切分更重要
很多人跳过清洗直接切分,结果检索质量一塌糊涂。PDF 提取出来的文本经常带页眉页脚、乱码、断行。我的预处理清单:
- 去掉重复的页眉页脚和页码
- 合并被硬换行切断的句子
- 统一全角半角标点
- 去掉多余的空格和空行
这一步花的时间,会在检索效果上加倍还回来。清洗完再切分,chunk 的语义完整度会好很多。
7.3 构建与验证:怎么判断 RAG 好不好用
跑通不等于好用。我一般准备一组测试问题,覆盖三类:能直接检索到的、需要跨文档综合的、知识库里根本没有的。第三类最关键——好的 RAG 应该老实说"不知道",而不是编一个答案。
验证时重点看两个指标:召回率(该找到的有没有找到)和准确率(找到的对不对)。如果召回率低,调切分和检索参数;如果准确率高但回答还是错,问题多半在提示词或模型。
7.4 上线前必须做的几件事
最后分享几个上线前容易漏掉的点。第一,加日志,把每次的检索结果和最终回答都记下来,出问题能回溯。第二,做缓存,相同问题不必重复检索和调用模型,省钱又提速。第三,设兜底,检索为空或模型超时的时候,给用户一个体面的回复,而不是报错。第四,定期更新向量库,知识是有保质期的。
我在实际项目里踩过最深的一个坑,是忘了处理编码问题。中文文档用默认编码读进来全是乱码,向量化出来的结果自然一塌糊涂,排查了半天才发现是encoding参数没设。这种低级错误,往往比算法问题更耗时间。所以我的建议是:每引入一个新数据源,先打印前几段文本肉眼确认,再往下走。这个习惯帮我省下了无数次返工。