LangChain 学习笔记:从零搭建 RAG 知识库,掌握 Chroma、文本拆分与 Chain 编排
大家好,我是小智。最近在折腾 LangChain,把学习过程中踩过的坑和总结的经验整理成这篇笔记,希望能帮到同样在学习的你,如有错误还请见谅。
本文涵盖:Chroma 向量库、RecursiveCharacterTextSplitter 文本拆分、Prompt 模板、Chain 编排四大核心模块。
Chroma
轻量级向量数据库,大模型记不住海量私有数据,向量数据库负责「帮模型快速找相关资料」,实现 RAG 知识库问答,还能省成本、防幻觉。
from langchain_chroma import Chroma
Chroma 核心参数
Chroma的参数有很多,但比较常用的就这四个
self.chroma = Chroma(
collection_name="你的集合名", # 1. 必须
embedding_function=你的嵌入模型, # 2. 必须
persist_directory="./存储路径", # 3. 必须(持久化)
client_settings=None, # 4. 可选(高级)
)
Chroma 增删改查
增
metadata = {
"source": filename,
"create_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"operator": "小智",
}
self.chroma.add_texts( # 内容就加载到向量库中了
# iterable -> list \ tuple
knowledge_chunks,
metadatas=[metadata for _ in knowledge_chunks],
)
删
按条件删
chroma.delete(filter={"operator" : "小智"})
清空整个库
chroma._client.reset()
改
Chroma 无直接 update,标准做法:先删后增
# 1. 删除该用户所有旧记忆
chroma.delete(filter={"operator" : "小智"})
# 2. 写入更新后的新记忆
chroma.add_texts(texts=["用户现在喝拿铁"], metadatas=[{"operator" : "小智"}])
查
基础查
# k=3:取最相似3条
docs = chroma.similarity_search(query="用户喝咖啡偏好", k=3)
for d in docs:
print(d.page_content, d.metadata)
带过滤查
# 只查 小智 的记忆
docs = chroma.similarity_search(
query="用户作息",
k=2,
filter={"operator":"小智"}
)
RecursiveCharacterTextSplitter
递归字符文本拆分器,大模型 有最大输入长度限制(比如 4k、8k、32k token)。
你直接丢一本小说进去,模型装不下,会直接报错。
所以必须把长文本切成小片段,才能:
- 存入向量库
- 送给模型做参考问答
这就是文本拆分。
from langchain_text_splitters import RecursiveCharacterTextSplitter
Splitter 核心参数
Splitter的参数有很多,但比较常用的就这四个
这些参数都有默认值,但是建议根据拆分目标设置
# 1. 初始化拆分器(3个核心参数)
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 1. 每个片段的最大长度(按 token 估算,不是字符数)
chunk_overlap=200, # 2. 片段间重叠长度(避免拆分丢失上下文)
length_function=len, # 3. 长度计算方式(默认 len 按字符,也可按 token 算)
separators=["\n\n", "\n", "。", " ", ""] # 4. 拆分优先级(可自定义,尤其是中文文档)
)
文本分割
# 输入:纯文本字符串(long_text)
# 输出:纯文本片段列表(knowledge_chunks)
knowledge_chunks = text_splitter.split_text(long_text)
文档分割
如果你的文档需要带 元数据(比如来源、页码、作者),必须用这个方法,拆分后会保留元数据,存入 Chroma 时能精准区分每个片段的属性。
# 1. 构建“带元数据的文档”(不是纯文本,而是 Document 对象)
doc = Document(
page_content=long_text, # 文档正文
metadata={ # 文档元数据(后续会传递给每个拆分后的片段)
"source": "微信Agent岗位JD", # 文档来源
"page_num": 1, # 页码
"user_id": "u1001" # 所属用户
}
)
# 2. 用 split_documents 拆分(输入 Document 对象,输出 Document 列表)
docs = text_splitter.split_documents([doc]) # 注意:输入是列表
# 3. 存入 Chroma(直接传 docs,自动提取文本和元数据)
self.chroma.add_documents(docs) # 不用再手动传 metadatas!
如果你的 Agent 需要加载 多份文档(比如同时加载 “岗位 JD”“产品手册”“用户手册”),可以批量传入 Document 列表,一次拆分完。
# 1. 构建多个带元数据的文档
doc1 = Document(
page_content="微信Agent岗位要求...",
metadata={"source": "微信Agent岗位JD", "user_id": "u1001"}
)
doc2 = Document(
page_content="AI Agent 开发指南...",
metadata={"source": "AI Agent开发手册", "user_id": "u1001"}
)
# 2. 批量拆分(输入多个 Document 对象)
docs = text_splitter.split_documents([doc1, doc2])
# 3. 存入 Chroma(自动区分不同来源的片段)
self.chroma.add_documents(docs)
# 4. 检索时可按来源过滤(比如只查岗位JD的内容)
results = self.chroma.similarity_search(
query="Agent需要什么技术",
filter={"source": "微信Agent岗位JD"}
)
文件分割
如果你的资料是 本地文件(比如 TXT、Markdown、PDF),不用手动读文件,直接用 LangChain 的 “文档加载器” 加载后拆分,一步到位(RAG 实战最常用)。
from langchain_community.document_loaders import TextLoader # TXT加载器
# 1. 加载本地 TXT 文件(自动读内容,生成 Document 对象)
loader = TextLoader("微信Agent岗位JD.txt") # 文件路径
documents = loader.load() # 输出:Document 列表(每个文件对应一个 Document)
# 2. 拆分文件内容(直接用 split_documents)
docs = text_splitter.split_documents(documents)
# 3. 存入 Chroma(和之前一样)
self.chroma.add_documents(docs)
扩展:支持其他文件格式
- Markdown 文件:用
UnstructuredMarkdownLoader; - PDF 文件:用
PyPDFLoader(需要先装pip install pypdf); - 代码文件:用
TextLoader直接加载。
PromptTemplate
PromptTemplate 是 LangChain 中最核心的提示词模板工具,此处介绍几个基础的Prompt模板
PromptTemplate
纯文本字符串模板,用 {变量名} 占位,支持任意自定义变量替换,通用所有大模型。
from langchain.prompts import PromptTemplate
# 定义模板:{question} 是变量
template = PromptTemplate(
input_variables=["question"], # 声明变量,可以不用写,会在template里自动识别,如果需要额外传输参数,那就要写。
template="请回答问题:{question}"
)
# 传入变量生成最终提示词
prompt = template.format(question="Python是什么?")
print(prompt)
ChatPromptTemplate
专门适配聊天大模型(GPT、文心一言、通义千问等),支持角色区分(system/assistant/user),搭配 MessagesPlaceholder 实现多轮上下文记忆。
from langchain.prompts import ChatPromptTemplate
# 企业智能客服标准模板
customer_service_prompt = ChatPromptTemplate.from_messages([
("system", "你是公司官方售后客服,语气礼貌专业,仅限解答产品售后、订单、退款问题,不闲聊。"),
MessagesPlaceholder(variable_name="chat_history"), # 历史对话插槽
("human", "{user_input}")
])
# 注入历史记录调用
history = [
HumanMessage("我的订单怎么退款?"),
AIMessage("请提供订单编号,我帮你核查~")
]
final_msg = customer_service_prompt.format_messages(
chat_history=history,
user_input="订单号20260403001"
)
简写兼容写法
ChatPromptTemplate.from_messages([ ("system", "专业运维助手"), ("placeholder", "{chat_history}"), ("human", "{input}")])
FewShotPromptTemplate
纯文本少样本,一般用于给模型喂标准示例,约束输出格式、分类规则、话术标准。
from langchain.prompts import FewShotPromptTemplate
examples = [
{"content": "物流太慢", "label": "投诉"},
{"content": "怎么收货", "label": "咨询"}
]
example_tpl = PromptTemplate(
input_variables=["content","label"],
template="文本:{content}\n分类:{label}"
)
cls_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_tpl,
prefix="严格按示例分类,只输出标签:",
suffix="文本:{input_text}\n分类:",
input_variables=["input_text"]
)
此处的prefix和suffix是在examples前后的引导句,如无需要可以不加。
prefix = 开头的引导语
examples = 中间的一堆示例
suffix = 结尾的提问句
FewShotChatMessagePromptTemplate
聊天模型专用少样本,用对话形式给示例,比纯文本更贴合 Chat 模型理解。
from langchain.prompts import FewShotChatMessagePromptTemplate
# 1. 定义少量示例(Few-shot examples)
examples = [
{"input": "这个产品太差了,发货慢死了", "output": "负面"},
{"input": "东西很好,客服态度也不错", "output": "正面"},
{"input": "一般般吧,能用就行", "output": "中性"},
]
# 2. 定义示例的消息格式(聊天格式)
example_prompt = ChatPromptTemplate.from_messages([
("human", "{input}"), # 用户说的话
("ai", "{output}"), # AI 应该输出的标签
])
# 3. 核心:创建 FewShotChatMessagePromptTemplate
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples,
)
# 4. 组装最终模板(系统提示 + 少样本 + 用户问题)
final_prompt = ChatPromptTemplate.from_messages([
("system", "你是专业情感分类器,只能输出:正面/负面/中性"),
few_shot_prompt, # 把少样本直接插进来
("human", "{user_input}"),
])
注意,这个模板没有prefix和suffix。
FewShotPromptTemplate = 给文本模型用 → 是纯字符串拼接,所以需要 prefix/suffix 包起来。
FewShotChatMessagePromptTemplate = 给聊天模型用 → 是消息列表(system /human/ai) ,不是字符串拼接。
所以它不需要开头结尾文字,而是直接插进对话消息里。也可以理解为prefix和suffix已经写在了ChatPromptTemplate中。
另外,我们可以看到,在final_prompt中嵌套了few_shot_prompt,等同于嵌套了一堆历史对话。
模型实际看到的是:
[系统] 你是专业情感分类器,只能输出:正面/负面/中性
[用户] 这个产品太差了,发货慢死了
[AI] 负面
[用户] 东西很好,客服态度也不错
[AI] 正面
[用户] 一般般吧,能用就行
[AI] 中性
[用户] {user_input} ← 你的问题
Chain
Runnable
Runnable = LangChain 里所有组件的 “统一接口”
只要是 Runnable,就能用 | 连起来!
最核心的 3 个方法
invoke() → 正常调用(同步)
chain.invoke({"input": "你好"})
stream() → 流式输出(打字机效果)
for chunk in chain.stream({"input": "你好"}):
print(chunk)
batch() → 批量处理
chain.batch([{"input": "1"},{"input": "2"}])
常用的 Runnable 家族
RunnablePassthrough
原样把数据传下去
"input": RunnablePassthrough()
RunnableLambda
把普通函数变成 Runnable
RunnableLambda(my_function)
RunnableParallel
并行执行多个 Runnable
chain = (
{ #这里的字典就是RunnableParallel
"input": RunnablePassthrough(),
"context": RunnableLambda(format_for_retriever) | retriever | format_document
}
| RunnableLambda(format_for_prompt_template)
| self.prompt_template
| print_prompt
| self.chat_model
| StrOutputParser()
)
本质上是
chain = (
RunnableParallel({
"input": RunnablePassthrough(),
"context": RunnableLambda(format_for_retriever) | retriever | format_document
})
| RunnableLambda(format_for_prompt_template)
| self.prompt_template
| print_prompt
| self.chat_model
| StrOutputParser()
)
Parser
StrOutputParser
把模型返回的 AIMessage 对象 → 纯字符串
LLM/ChatModel 原生返回是:
AIMessage(
content="答案xxx",
additional_kwargs={...},
usage_metadata={...}
)
StrOutputParser()会自动帮你取:.content
直接输出:
答案xxx(纯文字)
在链里标准用法
chain = prompt | chat_model | StrOutputParser()
JsonOutputParser
模型返回 JSON 字符串 → 自动转 Python 字典,做数据抽取、表单提取、Agent 工具调用必用。
chain = prompt | llm | JsonOutputParser()
输出:
{"name": "小明", "age": 20}
StructuredOutputParser
定义字段 → 强制模型按格式输出 → 自动解析成字典,企业最常用,比 JsonOutputParser 更稳定、更规范。
# 1. 定义你要抽取的字段
response_schemas = [
ResponseSchema(name="order_id", description="订单号"),
ResponseSchema(name="money", description="金额"),
ResponseSchema(name="username", description="用户名"),
]
# 2. 创建解析器
parser = StructuredOutputParser.from_response_schemas(response_schemas)
# 3. 获取格式提示指令
format_instructions = parser.get_format_instructions()
# 4. 写 Prompt 模板
# 必须把 {format_instructions} 塞进去!
prompt = ChatPromptTemplate.from_messages([
("system", "从用户输入中提取信息,严格按要求的格式输出。\n{format_instructions}"),
("human", "{user_input}")
])
# 5. 模型 + 解析器
model = ChatOpenAI(model="gpt-3.5-turbo")
chain = prompt | model | parser # 直接拼接,大模型虽然按要求返回,但本质还是String,要转化为字典
PydanticOutputParser
PydanticOutputParser = 最强结构化输出解析器
它能让大模型严格按你定义的格式输出,并且自动类型校验,错了直接报错!
比 StructuredOutputParser 更强大、更工程化、企业生产 90% 都用它。
# 1. 定义你要的输出结构(最重要!)
class UserInfo(BaseModel):
"""用户信息提取结果"""
name: str = Field(description="用户姓名")
age: Optional[int] = Field(None, description="年龄,没有就返回null")
phone: str = Field(description="手机号码")
order_id: str = Field(description="订单号")
# 2. 创建解析器
parser = PydanticOutputParser(pydantic_object=UserInfo)
# 3. 写 Prompt(必须加格式指令)
prompt = ChatPromptTemplate.from_messages([
("system", "从用户输入中提取信息,严格按要求格式输出。\n{format_instructions}"),
("human", "{input}")
])
# 4. 组装链
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
chain = prompt | llm | parser # 直接拼接!
# 5. 调用测试
user_input = "我是张三,今年22岁,电话13800138000,订单号20260403001"
result = chain.invoke({
"input": user_input,
"format_instructions": parser.get_format_instructions()
})