☰
从聊天到干活:LangChain与ChromaDB构建RAG知识库实战
2026/10/8 11:40:10 网站建设 项目流程

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 的核心就四步,我用一个图书馆的类比讲清楚:

  1. 切块(Chunking):把厚书拆成一页页卡片。文档太长,模型一次读不完,得切。
  2. 向量化(Embedding):给每张卡片编一个"语义坐标",内容相近的卡片坐标也相近。
  3. 检索(Retrieval):用户提问时,把问题也转成坐标,找出坐标最近的几张卡片。
  4. 生成(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参数没设。这种低级错误,往往比算法问题更耗时间。所以我的建议是:每引入一个新数据源,先打印前几段文本肉眼确认,再往下走。这个习惯帮我省下了无数次返工。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询