RAG实战-LangChain
-
- [一、什么是 LangChain](#一、什么是 LangChain)
- 二、Models
-
- [1 LLMs](#1 LLMs)
- [2 Chat Models](#2 Chat Models)
- [3 Embeddings Models](#3 Embeddings Models)
- 三、Prompts
-
- [1 通用 prompt](#1 通用 prompt)
- [2 PromptTemplate 是什么](#2 PromptTemplate 是什么)
- [3 few-shot 提示方式](#3 few-shot 提示方式)
- [4 ChatPrompts](#4 ChatPrompts)
- [5 通用提示词模板和 ChatPrompts 的区别](#5 通用提示词模板和 ChatPrompts 的区别)
- 四、Chains
-
- [1 串行链](#1 串行链)
- [2 并行链](#2 并行链)
- [3 分支与 RunnablePassthrough](#3 分支与 RunnablePassthrough)
- [4 RunnableLambda 自定义](#4 RunnableLambda 自定义)
- [五、Output Parsers](#五、Output Parsers)
-
- [1 字符串解析器](#1 字符串解析器)
- [2 列表解析器](#2 列表解析器)
- [3 JSON 解析器](#3 JSON 解析器)
- [4 Pydantic 解析器](#4 Pydantic 解析器)
- [5 自定义解析器](#5 自定义解析器)
- 六、Memory
-
- [1 使用 ChatMessageHistory 手动添加上下文](#1 使用 ChatMessageHistory 手动添加上下文)
- [2 短期记忆(Short-term Memory)](#2 短期记忆(Short-term Memory))
- [3 长期记忆](#3 长期记忆)
- [4 ConversationBufferMemory](#4 ConversationBufferMemory)
- [5 ConversationBufferWindowMemory](#5 ConversationBufferWindowMemory)
- [6 ConversationSummaryMemory](#6 ConversationSummaryMemory)
- 七、Indexes
-
- [1 文档加载器](#1 文档加载器)
- [2 文档分割器](#2 文档分割器)
- [3 VectorStores](#3 VectorStores)
- [4 检索器](#4 检索器)
-
- [4.1 LangChain 的检索器定义](#4.1 LangChain 的检索器定义)
- [4.2 检索器的工作原理](#4.2 检索器的工作原理)
- [4.3 检索器类型](#4.3 检索器类型)
- [4.4 其他几种常用的检索器](#4.4 其他几种常用的检索器)
一、什么是 LangChain
LangChain 是一套工具和库,帮助开发人员将 LLMs 集成到软件项目中。LLMs 是将文本作为输入并生成新文本作为输出的 AI 模型。商业 LLMs 具有专有接口;它们的输入长度有限,必须遵循称为提示的合理结构来迫使模型产生有用的输出。因此,集成此类模型可能会变得相当复杂,特别是如果想构建可投入生产的集成。
LangChain 通过规范化 LLM 接口、帮助进行提示和文档管理,以及支持多个 LLM 交互的链式操作(无论是硬编码还是 AI 控制)来解决这些问题。
LangChain 是一个强大的开源框架,用于构建基于大语言模型(LLM)的应用。它将大模型开发中的常用功能、工具和流程封装为模块化组件,使开发者能够像搭积木一样快速构建适用于不同场景的大模型应用。
LangChain 的首个版本于 2022 年 10 月开源。从一个开源 Python/TS 框架逐渐发展,形成包括"链"和"代理"等核心组件,现在已走向企业级阶段,发展成了 LangChain AI,其拥有目前 Agent 技术领域最大的开源生态,衍生出了多个开源项目框架。
LangChain AI 热门开源项目:
| 项目名称 | 技术栈 | 核心用途 |
|---|---|---|
| LangChain | Python/TS | 构建 LLM 应用基础组件(链式编排、RAG、嵌入、文档处理等) |
| langchainjs | JS/TS | 前端/Node 环境中构造 LLM 应用 |
| langgraph | Python | 用图编排复杂 Agent 流程 |
| local-deep-researcher | Python | 自动化、多轮本地 Web 研究工具 |
| opengpts | Python + Go + 前端 | 可定制化 GPT 平台,支持 RAG 和 Agent 开发 |
LangChain 官网 :https://python.langchain.com/docs/introduction/
LangChain 官网中文版:https://www.langchain.com.cn/
主要组件:
- Models:模型,各种类型的模型和模型集成,比如 GPT-4
- Prompts:提示,包括提示管理、提示优化和提示序列化
- Chains:链,一系列对各种组件的调用
- Memory:记忆,用来保存和模型交互时的上下文状态
- Indexes:索引,用来结构化文档,以便和模型交互
- Agents:代理,决定模型采取哪些行动,执行并且观察流程,直到完成为止
LangChain 核心包:
- langchain-core:聊天模型和其他组件的基础抽象。
- 集成包(例如 langchain-openai、langchain-anthropic 等):重要的集成被拆分为轻量级的独立包,由 LangChain 团队和集成方共同维护。
- langchain:包含链(chains)、智能体(agents)和检索策略,这些构成了应用的认知架构。
- langchain-community:由社区维护的第三方集成。
- langgraph:一个编排框架,用于将 LangChain 组件组合成可用于生产的应用,支持持久化、流式处理及其他关键特性。
LangChain 核心模块:
- LLM 和提示(Prompt):LangChain 对所有 LLM 大模型进行了 API 抽象,统一了大模型访问 API,同时提供了 Prompt 提示模板管理机制。
- 输出解析器 (Output Parsers):Langchain 接受大模型 (llm) 返回的文本内容之后,可以使用专门的输出解析器对文本内容进行格式化,例如解析 json、或者将 llm 输出的内容转成 python 对象。
- 链 (Chain):Langchain 对一些常见的场景封装了一些现成的模块,例如:基于上下文信息的问答系统,自然语言生成 SQL 查询等等,因为实现这些任务的过程就像工作流一样,一步一步的执行,所以叫链 (chain)。
- LCEL:LangChain Expression Language (LCEL),langchain 新版本的核心特性,用于解决工作流编排问题,通过 LCEL 表达式,可以灵活的自定义 AI 任务处理流程,也就是灵活自定义链 (Chain)。
- 数据增强生成 (RAG):因为大模型 (LLM) 不了解新的信息,无法回答新的问题,所以可以将新的信息导入到 LLM,用于增强 LLM 生成内容的质量,这种模式叫做 RAG 模式(Retrieval Augmented Generation)。
- Agents:是一种基于大模型(LLM)的应用设计模式,利用 LLM 的自然语言理解和推理能力(LLM 作为大脑),根据用户的需求自动调用外部系统、设备共同去完成任务。
- 模型记忆(memory):让大模型 (llm) 记住之前的对话内容,这种能力称为模型记忆(memory)。
环境准备:
bash
pip install langchain
pip show langchain
以 LangChain+Qwen 进行学习,需要提前安装:
bash
pip install openai
pip install langchain
pip install modelscope
借助阿里云-百炼平台(需要申请 API Key 以及 Secret Key):https://bailian.console.aliyun.com/#/home
二、Models
LangChain 目前支持三种类型的模型:LLMs、Chat Models(聊天模型)、Embeddings Models(嵌入模型)。
- LLMs:大语言模型接收文本字符作为输入,返回的也是文本字符。
- 聊天模型:基于 LLMs,不同的是它接收聊天消息(一种特定格式的数据)作为输入,返回的也是聊天消息。
- 文本嵌入模型:文本嵌入模型接收文本作为输入,返回的是浮点数列表。
1 LLMs
LLMs(大语言模型)使用场景最多,常用大模型的下载库:
方式一:通过 ChatOpenAI 进行调用
python
from langchain_openai import ChatOpenAI
import os
# 实例化模型
model = ChatOpenAI(
base_url=os.environ.get('base_url','https://dashscope.aliyuncs.com/compatible-mode/v1'),
model='qwen-plus',
openai_api_key=os.environ.get('api_key','sk-XXXXXX'),
max_tokens=1000,
temperature=0
)
# 获取问答结果
result = model.invoke("帮我讲个笑话吧")
print(result.content)
方式二:通过 Ollama 进行调用
Ollama 支持模型:https://ollama.com/
python
# from langchain_community.llms import Ollama # Langchain 0.x版本使用
from langchain_ollama import OllamaLLM
# 实例化模型
# model = Ollama(model="qwen2.5:7b") # Langchain 0.x版本使用
model = OllamaLLM(model="qwen2.5:7b")
# 获取问答结果
result = model.invoke("请给我讲个笑话吧")
print(result)
openai 方式接入大模型:
OpenAI 是一个基本的类,主要用来调用 OpenAI 的传统文本生成模型(比如早期的 text-davinci-003),适合处理简单的文本完成任务。
特点:
- 专注于生成单段文本,比如回答问题或写一段文字。
- 不太适合对话场景,因为它没有内置的对话上下文管理。
- 使用场景:如果只是想让模型生成一段独立的文本(比如写一篇短文或解释某个概念),可以用这个。
python
from config import Config
conf=Config()
# openai方式连接大模型
from openai import OpenAI
# 初始化DeepSeek的API客户端
client = OpenAI(api_key=conf.API_KEY, base_url="https://api.deepseek.com")
# 调用DeepSeek的API,生成回答
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "你是传智教育的助手传智小智,请根据用户的问题给出回答"},
{"role": "user", "content": "你好,请你介绍一下你自己。"},
],
)
# 打印模型最终的响应结果
print(response.choices[0].message.content)
langchain 方式接入大模型:
langchain 中 ChatOpenAI 是一个更高级的类,专门为对话模型设计(比如 deepseek、gpt-3.5-turbo 或 gpt-4),支持多轮对话。
特点:
- 能记住之前的对话内容,适合构建聊天机器人或需要上下文的场景。
- 通过消息列表(messages)来管理对话,比如用户输入和模型回应。
- 支持流式输出和更多参数调整。
- 使用场景:如果想做一个能聊天的应用,比如客服机器人或问答系统,ChatOpenAI 是首选。
python
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
# 初始化 ChatOpenAI,配置 DeepSeek API
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
messages = [
SystemMessage(content="你是传智教育的助手传智小智2号,请根据用户的问题给出回答"),
HumanMessage(content="你好,请你介绍一下你自己。")
]
result = llm.invoke(messages)
print(result.content)
百炼大模型接入:
python
from langchain_community.chat_models.tongyi import ChatTongyi
"""
社区贡献的包(PyPI 上独立安装的包),里面集成了许多第三方模型和服务,比如 Tongyi、Ollama 等。这个包不是 LangChain 官方维护的,而是社区驱动的,允许开发者快速添加新集成。
"""
load_dotenv(override=True)
model = ChatTongyi(api_key=os.getenv("DASHSCOPE_API_KEY"))
question = "你好,请你介绍一下你自己。"
result = model.invoke(question)
print(result.content)
2 Chat Models
聊天模型,聊天消息包含下面几种类型,使用时需要按照约定传入合适的值:
- AIMessage:就是 AI 输出的消息,可以是针对问题的回答。
- HumanMessage:人类消息就是用户信息,由人给出的信息发送给 LLMs 的提示信息,比如"实现一个快速排序方法"。
- SystemMessage:可以用于指定模型具体所处的环境和背景,如角色扮演等。可以在这里给出具体的指示,比如"作为一个代码专家",或者"返回 json 格式"。
- ChatMessage:Chat 消息可以接受任意角色的参数,但是在大多数时间,应该使用上面的三种类型。
LangChain 支持大量的 chat 模型,可以通过官网查询:https://python.langchain.com/docs/integrations/chat/
也可以通过 langchain 源码查看。
SystemMessage+HumanMessage+AIMessage:
python
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
# from langchain_community.chat_models import ChatOllama # Langchain 0.x版本使用
from langchain_ollama import ChatOllama
# 实例化模型
model = ChatOllama(model="qwen2.5:7b")
# 定义提示词
messages = [
SystemMessage(content="现在你是一个著名的诗人"),
HumanMessage(content="给我写一首唐诗")]
# 获取问答结果
result = model.invoke(messages)
print(result.content)
# 也可以使用api调用
from langchain_openai import ChatOpenAI
# 以调用 DeepSeek 远程 API 为例
model = ChatOpenAI(
model="deepseek-chat", # 模型名称
api_key="你的API_KEY", # 你的 API 密钥
base_url="https://api.deepseek.com/v1" # API 请求的基础 URL
)
# 简单调用
response = model.invoke("你好,请介绍一下你自己。")
print(response.content)
3 Embeddings Models
Embeddings Models(嵌入模型)特点:将字符串作为输入,返回一个浮动数的列表。在 NLP 中,Embedding 的作用就是将数据进行文本向量化。
Embeddings Models 可以为文本创建向量映射,这样就能在向量空间里去考虑文本,执行诸如语义搜索之类的操作,比如说寻找相似的文本片段。
https://python.langchain.com/docs/integrations/text_embedding/
不同的 Embedding 模型对多语言支持和文本类型有不同的特点:
-
多语言支持:
text-embedding-ada-002:支持多种语言,但对中文等亚洲语言的支持相对较弱bge-large-zh:对中文有很好的支持multilingual-e5-large:对多语言都有较好的支持- mxbai-embed-large
-
文本类型适用性:
- 代码文本:建议使用专门的代码 Embedding 模型,如
CodeBERT - 通用文本:可以使用
text-embedding-ada-002或bge-large-zh - 专业领域文本:建议使用该领域的专门模型
- 代码文本:建议使用专门的代码 Embedding 模型,如
可以参考 MTEB(大规模文本嵌入基准)排行榜以获取最新模型效果:https://huggingface.co/spaces/mteb/leaderboard
python
# from langchain_community.embeddings import OllamaEmbeddings # Langchain 0.x版本使用
from langchain_ollama import OllamaEmbeddings
# 初始化Ollama嵌入模型,使用mxbai-embed-large模型,温度设置为0
model = OllamaEmbeddings(model="mxbai-embed-large", temperature=0)
# 对单个查询文本进行嵌入编码
res1 = model.embed_query('这是第一个测试文档')
print(f'result1->{res1}')
print(f'result1的长度->{len(res1)}') # 生成的长度一定是128的倍数
# 对多个文档进行批量嵌入编码
res2 = model.embed_documents(['这是第一个测试文档', '这是第二个测试文档'])
print(res2)
from langchain_openai import OpenAIEmbeddings
# 以调用兼容 OpenAI 接口的远程嵌入服务为例
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small", # 模型名称
api_key="你的API_KEY", # API 密钥
base_url="https://api.your-provider.com/v1" # 服务商 API 地址
)
# 对查询文本生成向量
query_vector = embeddings.embed_query("这是一段测试文本")
# 对文档列表生成向量
doc_vectors = embeddings.embed_documents(["文档1", "文档2"])
三、Prompts
1 通用 prompt
Prompt 是指当用户输入信息给模型时加入的提示,这个提示的形式可以是 zero-shot 或者 few-shot 等方式,目的是让模型理解更为复杂的业务场景以便更好的解决问题。
提示模板:如果有了一个起作用的提示,可能想把它作为一个模板用于解决其他问题,LangChain 就提供了 PromptTemplates 组件,它可以帮助更方便的构建提示。
zero-shot 提示方式:
python
# from langchain import PromptTemplate # Langchain 0.x版本使用
from langchain_core.prompts import PromptTemplate
# from langchain_community.llms import Ollama # Langchain 0.x版本使用
from langchain_ollama import OllamaLLM
# model = Ollama(model="qwen2.5:7b") # Langchain 0.x版本使用
model = OllamaLLM(model="qwen2.5:7b")
# 定义模板
template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字" #在字符串中的{}为占位符
# 占位符里面的就是变量名 后面创建PromptTemplate对象时传入input_variables这个列表里面的一个元素
prompt = PromptTemplate(
input_variables=["lastname"], # 输入变量
template=template)
prompt_text = prompt.format(lastname="王") #传参
print(prompt_text)
# result: 我的邻居姓王,他生了个儿子,给他儿子起个名字
result = model.invoke(prompt_text)
print(result)
2 PromptTemplate 是什么
PromptTemplate 是 LangChain 中用于构建带变量的提示文本的类。它把固定模板和动态变量分离,让可以复用同一套提示结构,只需替换变量即可生成不同的最终提示。
核心作用:
- 定义模板字符串,用
{}标记变量占位符 - 声明需要哪些变量(
input_variables) - 调用
format/invoke时传入变量值,得到最终字符串
导入方式:
python
# LangChain 0.x
# from langchain import PromptTemplate
# LangChain 1.x(推荐)
from langchain_core.prompts import PromptTemplate
创建 PromptTemplate:
方式一:构造函数(显式指定 input_variables)
| 参数 | 说明 |
|---|---|
template |
模板字符串,用 {} 表示占位符 |
input_variables |
列表,列出模板中所有变量名,必须与占位符一致 |
python
template_str = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字"
prompt_manual = PromptTemplate(
input_variables=["lastname"], # 必须包含模板中所有变量
template=template_str,
)
print("【手动创建】模板内容:", prompt_manual.template)
print("【手动创建】输入变量:", prompt_manual.input_variables)
方式二:from_template(自动推断变量名,推荐)
from_template 会自动扫描模板字符串中的 {} 占位符,提取变量名并生成 input_variables,无需手动编写。
优点: 减少出错,模板修改后无需同步更新变量列表。
python
prompt_auto = PromptTemplate.from_template(template_str)
print("【自动创建】输入变量:", prompt_auto.input_variables)
# 输出:['lastname']
占位符语法与变量名规则:
- 占位符使用一对花括号:
{变量名} - 变量名建议使用字母、数字、下划线,且不以数字开头
- 如果模板中需要显示真实的花括号,需用双花括号 转义:
{``{和}} - 同一个变量可以在模板中出现多次,
format时只需传一次
python
template_multi = "你好{name},欢迎{name}来到{place}。这里用 {{花括号}} 转义。"
prompt_multi = PromptTemplate.from_template(template_multi)
print("【多变量与转义】输入变量:", prompt_multi.input_variables)
# 输出:['name', 'place']
print("【多变量与转义】格式化结果:", prompt_multi.format(name="张三", place="北京"))
# 输出:你好张三,欢迎张三来到北京。这里用 {花括号} 转义。
格式化与调用:
| 方法 | 返回值 | 说明 |
|---|---|---|
format(**kwargs) |
str |
传入关键字参数,返回最终字符串 |
format_prompt(**kwargs) |
PromptValue |
LangChain 内部使用 |
invoke(dict) |
PromptValue |
Runnable 接口 |
注意:
format返回普通字符串,适合直接打印或传给普通函数format_prompt/invoke返回PromptValue,可继续传给模型或解析器
python
filled_str = prompt_auto.format(lastname="王")
print("【format】结果:", filled_str)
# 输出:我的邻居姓王,他生了个儿子,给他儿子起个名字
prompt_value = prompt_auto.format_prompt(lastname="李")
print("【format_prompt】类型:", type(prompt_value))
print("【format_prompt】转字符串:", prompt_value.to_string())
prompt_value2 = prompt_auto.invoke({"lastname": "张"})
print("【invoke】类型:", type(prompt_value2))
print("【invoke】转字符串:", prompt_value2.to_string())
部分变量填充(partial):
当一个模板中有多个变量,但其中一些是固定值 时,可以用 partial 预先填入,生成新的 PromptTemplate。新模板只需传入剩余变量即可。
python
template_partial = "公司名称:{company},员工姓名:{name},职位:{position}"
prompt_base = PromptTemplate.from_template(template_partial)
# 预先固定 company 变量
prompt_with_company = prompt_base.partial(company="LangChain 科技")
print("【partial】原变量:", prompt_base.input_variables)
# 输出:['company', 'name', 'position']
print("【partial】新变量:", prompt_with_company.input_variables)
# 输出:['name', 'position']
result_partial = prompt_with_company.format(name="小明", position="工程师")
print("【partial】结果:", result_partial)
# 输出:公司名称:LangChain 科技,员工姓名:小明,职位:工程师
模板复用与组合:
同一个 PromptTemplate 对象可以被多次调用,每次传入不同变量值,生成不同的最终提示,非常适合批量处理。
python
names = ["王", "李", "张"]
print("【复用】批量生成:")
for n in names:
print(" ", prompt_auto.format(lastname=n))
与 ChatPromptTemplate 的对比:
| 特性 | PromptTemplate |
ChatPromptTemplate |
|---|---|---|
| 输出类型 | 纯文本字符串 | 消息列表(System/Human/AI) |
| 适用模型 | LLM(纯文本补全) | ChatModel(聊天模型) |
| 创建方式 | from_template |
from_messages |
python
from langchain_core.prompts import ChatPromptTemplate
chat_prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的翻译助手。"),
("human", "请将以下内容翻译成英文:{text}"),
])
print("【ChatPromptTemplate】输入变量:", chat_prompt.input_variables)
# 输出:['text']
chat_value = chat_prompt.format_prompt(text="你好,世界!")
print("【ChatPromptTemplate】消息列表:")
for msg in chat_value.to_messages():
print(" ", msg.type, "->", msg.content)
# 输出:
# system -> 你是一个专业的翻译助手。
# human -> 请将以下内容翻译成英文:你好,世界!
在 LCEL 中作为 Runnable 使用:
PromptTemplate 实现了 Runnable 接口,可以用 | 管道与其他组件组合。
python
chain = prompt_auto | (lambda x: x.to_string())
chain_result = chain.invoke({"lastname": "赵"})
print("【LCEL】链式调用结果:", chain_result)
# 输出:我的邻居姓赵,他生了个儿子,给他儿子起个名字
常见错误与注意事项:
| 序号 | 错误场景 | 报错信息 | 解决方案 |
|---|---|---|---|
| 1 | input_variables 与模板占位符不匹配 |
KeyError 或 ValueError |
使用 from_template 自动推断,或仔细核对变量名 |
| 2 | 忘记传变量 | KeyError: 'lastname' |
format/invoke 时传入所有必需的变量 |
| 3 | 模板中需要显示花括号 | 被误当作占位符 | 必须写成 {``{ 和 }} 转义 |
| 4 | 变量名包含特殊字符 | 解析失败 | 建议只用字母、数字、下划线,且不以数字开头 |
| 5 | 混淆 format 与 format_prompt |
类型错误 | format 返回 str,format_prompt 返回 PromptValue;传给模型通常用 format_prompt 或 invoke |
| 6 | 在 ChatModel 场景误用 PromptTemplate |
格式不兼容 | 聊天模型推荐 ChatPromptTemplate;PromptTemplate 也可用于纯文本补全模型(LLM) |
3 few-shot 提示方式
python
# 旧版 LangChain 0.x 的导入方式,已注释掉
# from langchain import PromptTemplate, FewShotPromptTemplate # 0.x 从顶层 langchain 导入
# LangChain 1.x 从 langchain_core.prompts 导入核心提示模板类
from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
# 从 langchain_ollama 导入本地 Ollama 聊天模型封装
from langchain_ollama import OllamaLLM
# 初始化本地 Ollama 模型,指定模型名称
model = OllamaLLM(model="qwen2.5:7b")
# 定义少样本示例列表,每个示例是一个字典
examples = [
{"word": "开心", "antonym": "难过"},
{"word": "高", "antonym": "矮"}
]
# 定义单个示例的格式化模板,用于将每个示例字典渲染成文本
example_template = """
单词: {word}
反义词: {antonym}
"""
# 创建 PromptTemplate 实例,用于格式化单个示例
example_prompt = PromptTemplate(
input_variables=["word", "antonym"],
template=example_template,
)
# 创建 FewShotPromptTemplate 实例,组合示例、示例模板、前缀、后缀等
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
prefix="给出每个单词的反义词",
suffix="单词: {input}\n反义词:",
input_variables=["input"],
example_separator="\n",
)
# 使用 few_shot_prompt 格式化,传入最终输入变量 input="粗"
prompt_text = few_shot_prompt.format(input="粗")
# 调用模型,传入提示文本,打印模型返回结果
print(model.invoke(prompt_text))
few-shot 少量提示词调用过程:
这个 FewShotPromptTemplate 的提示词构建过程,本质上就是先准备"示例数据"和"单例渲染模板",再设定头尾和分隔符,最后由模板自动拼接、格式化并交给模型。
具体来说,首先定义 examples = [{"word": "开心", "antonym": "难过"}, {"word": "高", "antonym": "矮"}],每个字典代表一个"输入→输出"的示范;然后用 PromptTemplate(input_variables=["word", "antonym"], template="单词: {word}\n反义词: {antonym}") 创建 example_prompt,它的作用是把任意一个示例字典渲染成一段标准文本,比如把第一个示例变成"单词: 开心 / 反义词: 难过"。这一步的关键是 example_prompt 只负责单个示例的格式化,而 examples 中的键名必须与它的 input_variables 完全一致,否则会报错。
接着设定少样本模板的固定部分:prefix = "给出每个单词的反义词" 放在所有示例之前,用来告诉模型任务目标;suffix = "单词: {input}\n反义词:" 放在所有示例之后,其中 {input} 是最终要用户传入的变量;example_separator = "\n\n" 决定示例之间的间隔;input_variables = ["input"] 则声明最终提示只需要用户提供 input 一个变量。然后把这些部件一起传给 FewShotPromptTemplate,也就是 few_shot_prompt = FewShotPromptTemplate(examples=examples, example_prompt=example_prompt, prefix=prefix, suffix=suffix, input_variables=input_variables, example_separator=example_separator)。此时模板并没有生成最终文本,只是记录了"先放 prefix,再依次渲染每个示例并用分隔符隔开,最后放 suffix"的拼接规则。
当调用 prompt_text = few_shot_prompt.format(input="粗") 时,模板才真正开始工作:它先把 prefix 输出,然后用 example_prompt 把两个示例分别渲染成"单词: 开心 / 反义词: 难过"和"单词: 高 / 反义词: 矮",中间用两个换行隔开,最后接上 suffix,并把其中的 {input} 替换成"粗"。最终生成的完整提示就是:先一行"给出每个单词的反义词",空行后是第一个示例,再空行后是第二个示例,最后是"单词: 粗 / 反义词:"。模型看到前面两个"单词→反义词"的示范后,就会模仿这个格式,在"反义词:"后面补全出"细"。
这里需要特别注意,原代码里写的 \\n 在 Python 字符串中表示字面反斜杠加字母 n,并不是真正的换行;只有写成 \n 才会产生换行效果,否则最终提示里会出现多余的 \n 字符,影响格式清晰度。
最后,把生成的 prompt_text 传给 model.invoke(prompt_text),也就是把这段完整提示发给本地 Ollama 的 qwen2.5:7b 模型,模型根据示例模式推理出"粗"的反义词是"细"并返回。
整个流程可以概括为:examples 和 example_prompt 负责提供并渲染示范,prefix、suffix、example_separator 和 input_variables 负责定义提示的头尾与变量,FewShotPromptTemplate 负责按规则拼接,format 负责填充最终变量,model.invoke 负责生成答案。这种少样本提示的核心价值,就是让模型通过模仿几个示例来完成任务,而不需要对模型做任何微调。
4 ChatPrompts
适合交互式对话应用,如聊天机器人、智能客服等,这些应用需要处理用户和 LLM 之间的多轮对话。
ChatPromptTemplate
SystemMessagePromptTemplate
HumanMessagePromptTemplate
history=("system","..."),('human',"..."),("ai","...")
直接提问:
提示模板就是把一些常见的提示整理成模板,用户只需要修改模板中特定的词语,就能快速准确地告诉模型自己的需求。
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 定义提示词模版
template_str = "帮我讲个关于{name}笑话吧"
prompt_template = ChatPromptTemplate.from_template(template_str)
prompt = prompt_template.format_messages(name="气球")
print(f'prompt-->{prompt}')
# 调用模型
result = model.invoke(prompt)
print(f'result-->{result}')
zero-shot 提示方式:
python
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
from langchain_core.messages import SystemMessage
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 系统信息
system_prompt = SystemMessage("你是取名专家。")
# 用户信息模版
human_str = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字。"
human_template = HumanMessagePromptTemplate.from_template(human_str)
# 组装
chat_template = ChatPromptTemplate.from_messages([system_prompt, human_template])
# 生成最终的提示词
prompt = chat_template.format_messages(lastname="王")
print(f'prompt-->{prompt}')
# 调用模型
result = model.invoke(prompt)
print(f'result-->{result}')
few-shot 提示方式:
python
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建 prompt 模版
prompt_template = ChatPromptTemplate.from_messages(
[
("system", "给出每个单词的反义词"),
MessagesPlaceholder("history"),
("human", "{question}")
]
)
# 创建 few-shot prompt
history = [("human", "开心"), ("ai", "难过"),("human", "高"), ("ai", "矮")]
prompt = prompt_template.format_messages(history=history, question="富有")
print(f"prompt-->{prompt}")
# 调用模型
result = model.invoke(prompt)
print(f'result-->{result}')
5 通用提示词模板和 ChatPrompts 的区别
在 LangChain 中,"通用提示词模板"(通常指 PromptTemplate、FewShotPromptTemplate)和"Chat 提示词模板"(ChatPromptTemplate、FewShotChatMessagePromptTemplate)最核心的区别在于:前者生成的是纯文本字符串,面向纯文本补全模型(LLM);后者生成的是带角色的消息列表,面向聊天模型(ChatModel)。
1. 输出结构不同
-
通用提示词模板 :调用
format()后返回一个普通字符串。例如:pythonprompt.format(text="你好") # 返回 "请翻译:你好"它内部对应的是
StringPromptValue,本质就是一段文本。 -
Chat 提示词模板 :调用
format_prompt()后返回ChatPromptValue,其中包含一个消息列表,每个消息有角色(system、human、ai 等)。例如:pythonprompt.format_prompt(text="你好").to_messages() # [SystemMessage(content="你是翻译助手"), HumanMessage(content="请翻译:你好")]它生成的是结构化对话,而不是单一字符串。
2. 适用模型不同
- 通用提示词模板 :适合
LLM接口的模型,比如OllamaLLM、旧的OpenAI(纯文本补全)。这些模型接收字符串、返回字符串。 - Chat 提示词模板 :适合
ChatModel接口的模型,比如ChatOllama、ChatOpenAI、ChatDeepSeek。这些模型接收消息列表,能区分 system、user、assistant 角色,对话效果更好,也是目前主流。
3. 构建方式不同
-
通用模板用
PromptTemplate.from_template("...")或构造函数,模板里用{变量}占位。 -
Chat 模板用
ChatPromptTemplate.from_messages([...]),列表里每个元素是(角色, 内容模板),例如:pythonChatPromptTemplate.from_messages([ ("system", "你是一个翻译助手"), ("human", "请翻译:{text}") ])其中只有
human消息里的{text}是变量,system消息是固定角色设定。
4. 少样本场景的对应类
- 通用少样本:
FewShotPromptTemplate,用example_prompt渲染示例,拼成纯文本。 - Chat 少样本:
FewShotChatMessagePromptTemplate,用example_prompt渲染成消息对(human/ai),再拼成消息列表。它更适合聊天模型,因为示例本身就是一问一答的对话形式。
5. 在 LCEL 中的表现
两者都是 Runnable,都可以用 | 组合成链:
python
chain = prompt | model
区别在于:
PromptTemplate输出StringPromptValue,LLM能直接接收。ChatPromptTemplate输出ChatPromptValue,ChatModel能直接接收。
LangChain 会自动做适配,所以写法上几乎一样,但语义和最终传给模型的内容不同。
6. 变量声明与填充
- 通用模板的
input_variables对应模板中所有{变量},format时传关键字参数。 - Chat 模板的
input_variables是所有消息内容里变量的并集,format_prompt时同样传关键字参数,但填充后每个变量会出现在对应角色的消息里。
7. 相互转换
ChatPromptTemplate可以调用.format_prompt(...).to_string()得到纯文本,但会丢失角色信息。PromptTemplate生成的是纯字符串,无法直接还原成带角色的消息列表,除非手动拆分成 System/Human 消息。
8. 如何选择
- 如果用的是 ChatModel (如
ChatOllama、ChatOpenAI),优先用ChatPromptTemplate,因为它能明确区分 system 指令和用户输入,模型理解更准确,也支持多轮对话历史(通过MessagesPlaceholder)。 - 如果用的是 纯文本 LLM (如
OllamaLLM),只能用PromptTemplate,因为 LLM 只接受字符串。 - 在 LangChain 1.x 中,官方推荐尽量使用 ChatModel + ChatPromptTemplate,因为这是更现代、更强大的接口。
一句话总结:
通用提示词模板是"把变量填进一段文本,输出字符串",面向纯文本补全;Chat 提示词模板是"把变量填进一组带角色的消息,输出消息列表",面向聊天模型。前者简单直接,后者更结构化、更贴近真实对话,也是当前 LangChain 的主流用法。
四、Chains
在 LangChain 中,Chains 描述了将 LLM 与其他组件结合起来完成一个应用程序的过程。
针对上一小节的提示模版例子,zero-shot 里面,可以用链来连接提示模版组件和模型,进而可以实现代码的更改,主要使用 LCEL 方法。
LCEL(Lang Chain Expression Language)是一种声明式的方法,用于轻松组合链条。
LCEL 的基本语法规则是使用 | 符号将不同的组件连接起来,形成一个链式结构。| 符号类似于 Unix 的管道操作符,它将一个组件的输出作为下一个组件的输入,从而实现数据的传递和处理。
上一个组件的输出作为下一个组件的输入,输出和输入的类型必须保持一致,否则不能连接。
Chain(链)与 LCEL 的关系:
- Chain(链):是 LangChain 中处理流程的抽象概念,指将多个组件(模型、工具、逻辑)串联成一个可执行的任务序列。
- LangChain Expression Language(LCEL)是一种声明式语言,可轻松组合不同的调用顺序构成 Chain。LCEL 自创立之初就被设计为能够支持将原型投入生产环境,无需代码更改,从最简单的"提示+LLM"链到最复杂的链。
在本文中 LCEL 产生的对象,被叫做 runnable 或 chain,经常两种叫法混用。本质就是一个自定义调用流程。
一个最基本的 Chain 结构,是由 Model 和 OutputParser 两个组件构成的,其中 Model 是用来调用大模型的,OutputParser 是用来解析大模型的响应结果的。
1 串行链
如下构建一条"接收主题 -> 生成诗句 -> 提取文本"的简单串行流水线。
python
from langchain.prompts import PromptTemplate
from config import Config
from langchain_openai import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda,RunnableSequence
# 初始化 ChatOpenAI,配置 DeepSeek API
conf=Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
print("--- 1. 串行链 (Sequential Chain) 示例 ---")
# 定义流水线的三个"工位"
prompt = ChatPromptTemplate.from_template("写一句关于"{topic}"的七言绝句。")
parser = StrOutputParser()
# 使用 LCEL `|` 管道符,将三个工位连接成一条串行流水线
serial_chain = prompt | llm | parser
print(type(serial_chain))
# 启动流水线,投入原材料
input_data = {"topic": "月色"}
result = serial_chain.invoke(input_data)
print(f"【输入】: {input_data}")
print(f"【最终输出】: {result}")
结论:数据按照 prompt -> llm -> parser 的顺序依次处理,非常直观。
2 并行链
并行链允许同一个输入数据同时流向多个独立的处理站(或子流水线),然后将所有处理站的结果汇集到一个字典中。
python
# langchain_4_2_chain.py (Corrected)
from langchain.prompts import PromptTemplate
from config import Config
import json
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel
# 初始化 ChatOpenAI,配置 DeepSeek API
conf=Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
print("\n--- 2. 并行链 (Parallel Chain) 示例 ---")
# 定义两条独立的子流水线
poem_chain = ChatPromptTemplate.from_template("写一首关于"{topic}"的诗。") | llm | StrOutputParser()
joke_chain = ChatPromptTemplate.from_template("讲一个关于"{topic}"的俏皮话。") | llm | StrOutputParser()
# 使用 RunnableParallel 将字典结构转换为一个可执行的并行链
parallel_chain = RunnableParallel({
"poem": poem_chain,
"joke": joke_chain
})
# 启动并行流水线
input_data = {"topic": "程序员"}
result = parallel_chain.invoke(input_data)
print(f"【输入】: {input_data}")
print("【最终输出】:")
print(json.dumps(result, indent=2, ensure_ascii=False))
3 分支与 RunnablePassthrough
流水线的"Y 型分线器"。
python
chain = {
# "context" 分支:对输入运行检索器
"context": fake_retriever, ## 环节1A
# "question" 分支:直接"透传"原始输入
"question": RunnablePassthrough() ## 环节1B
} | rag_prompt | llm | StrOutputParser() ## 环节2
# 4. 执行链
user_question = "LCEL"
result = chain.invoke(user_question)
RunnablePassthrough 的作用是在不改变原始数据的情况下,将其"复制"一份到并行处理的另一条分支。
比喻:
- 原始文件(用户的 LCEL)
- 需要把这份文件复印一份,交给图书管理员(fake_retriever),让他根据文件内容去查找相关资料得到(context)。
- 同时,必须保留好原始文件本身,因为最后需要把"原始文件(LCEL)"和"相关资料(context)"钉在一起,交给领导(rag_prompt)审阅。
RunnablePassthrough 就是那台"复印机",它确保了在进行"查找资料"这个额外操作后,原始文件没有丢失。
完整代码:
python
# langchain_4_2_chain.py
from langchain.prompts import PromptTemplate
from config import Config
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
# 初始化 ChatOpenAI,配置 DeepSeek API
conf=Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
print("\n--- 3. 分支与 RunnablePassthrough 示例 ---")
# 1. 模拟一个检索器
def fake_retriever(query: str) -> str:
"""一个模拟的检索器,根据查询返回固定的上下文。"""
return f"关于"{query}"的背景知识是:这是一个非常重要的概念。"
# 2. 定义需要同时接收 context 和 question 的 Prompt
rag_prompt = ChatPromptTemplate.from_template(
"根据以下上下文回答问题。\n上下文: {context}\n问题: {question}"
)
# 3. 构建包含 Passthrough 的并行链
chain = {
"context": fake_retriever,
"question": RunnablePassthrough()
} | rag_prompt | llm | StrOutputParser()
# 4. 执行链
user_question = "LCEL"
result = chain.invoke(user_question)
print(f"【输入】: '{user_question}'")
print(f"【最终输出】:\n{result}")
4 RunnableLambda 自定义
RunnableLambda 是一个"适配器",它可以将任何普通的 Python 函数包装成一个标准的 LangChain 组件,让它可以无缝地接入到 LCEL | 流水线中。
python
# langchain_4_3_chain.py
from langchain.prompts import PromptTemplate
from config import Config
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
# 初始化 ChatOpenAI,配置 DeepSeek API
conf=Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
print("\n--- 4. RunnableLambda 示例 ---")
# 1. 定义一个普通的 Python 函数,它不是标准的 LangChain 组件
def add_comment(text: str) -> str:
"""在一个句子的末尾加上一句俏皮的评论。"""
return text.strip() + "\n 关于更多课程,欢迎咨询:https://www.itcast.cn/"
# 2. 使用 RunnableLambda 将其包装成一个"标准工位"
custom_processor = RunnableLambda(add_comment)
# 3. 构建一条包含自定义工位的串行链
chain = (
ChatPromptTemplate.from_template("请解释一下"{concept}"是什么。")
| llm
| StrOutputParser()
| custom_processor # 在这里接入我们的自定义函数
)
# 4. 执行链
result = chain.invoke({"concept": "大模型"})
print("【最终输出】:")
print(result)
显式转换写法与隐式转换对比:
python
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
from langchain_core.runnables import RunnableLambda
template = "我的邻居姓{lastname},他生了个儿子,给他儿子起个名字"
first_prompt = PromptTemplate(
input_variables=["lastname"],
template=template,
)
llm = OllamaLLM(model="qwen2.5:7b")
first_chain = first_prompt | llm
second_prompt = PromptTemplate(
input_variables=["child_name"],
template="邻居的儿子名字叫{child_name},给他起一个小名",
)
second_chain = second_prompt | llm
# 直接串联两条链
overall_chain = first_chain | second_chain
print(overall_chain)
print('*' * 80)
print(overall_chain.invoke("王"))
# 显式转换写法
to_child_dict = RunnableLambda(lambda name: {"child_name": name})
overall_chain_v2 = first_chain | to_child_dict | second_chain
print("\n显式转换写法:")
print(overall_chain_v2.invoke({"lastname": "王"}))
为什么更推荐使用显式转换:
-
意图清晰:隐式转换依赖"模板只有一个变量"这个前提,读代码时很难一眼看出 second_chain 到底需要什么输入。显式写 RunnableLambda(lambda name: {"child_name": name}),读者立刻就知道:这里是把上一步的输出转成 child_name。
-
不依赖隐式行为:隐式转换是 LangChain 的内部规则,不是 Python 语法。如果以后 LangChain 版本调整了这条规则,隐式写法可能突然报错,而显式写法的行为是确定的。
-
模板变量增加时不会突然失效:如果 second_prompt 以后改成两个变量,比如 "邻居的儿子名字叫{child_name},{gender},给他起一个小名",隐式写法立刻报错,必须回头补转换层。显式写法本来就在做转换,只需要在字典里多加一个键即可。
使用字典传递的好处:
-
不受变量个数限制:模板有一个变量时可以传字符串,有多个变量时必须传字典。统一用字典,就不用关心模板到底有几个变量。
-
键名和模板变量一一对应,可读性强:传 {"child_name": "王小明"} 时,一眼就能看出这个值会填到模板的哪个占位符里。传字符串 "王小明" 则要靠猜。
-
便于扩展:以后模板增加变量,只需要往字典里加键值对,调用方式和链的结构都不用改。
-
与大多数 LangChain 组件的输入约定一致:PromptTemplate、RunnableParallel、RunnablePassthrough 等在组合场景下默认都以字典为输入输出,统一用字典能减少类型不匹配的问题。
为什么既可以传值也可以传字典:
PromptTemplate 内部在接收输入时,会做一次判断:
- 如果收到的是字典,就按变量名逐个取值填充;
- 如果收到的是字符串(或其它非字典值),且模板只有一个变量,就把这个值当作那个变量的值;如果模板有多个变量,则报错。
这个规则是为了兼顾两种场景:
- 单变量、快速调试时,直接传字符串更方便;
- 多变量、组合链时,用字典表达更清晰、更通用。
所以同一个 PromptTemplate,在只有一个变量时既能接受 "王" 也能接受 {"lastname": "王"};一旦变量变成两个,就只能接受字典了。
基于这一点,推荐始终用字典传参,这样不管模板以后怎么改,调用方式都是稳定的。
五、Output Parsers
LLM 的输出是自然语言文本,但在应用开发中,经常需要将这些文本转换为结构化的数据格式,如列表、字典或对象。LangChain 输出解析器负责获取 LLM 的输出并将其转换为更合适的格式。
| 解析器名称 | 核心功能 | 输出的 Python 类型 | 工业级应用场景 |
|---|---|---|---|
| StrOutputParser | 默认解析器。将 LLM 的输出直接解析为字符串。 | str | 只需要原始回答时(如问答任务、对话场景) |
| CommaSeparatedListParser | 将 LLM 输出的、用逗号分隔的文本解析为列表。 | liststr | 列表/枚举型输出 |
| JsonOutputParser | 极其常用。将 LLM 输出的 JSON 字符串解析为 Python 字典。 | dict | 结构化 JSON 输出 |
| PydanticOutputParser | 极其常用。将 LLM 输出解析为预先定义的 Pydantic 对象,提供类型安全和数据验证。 | 自定义的 pydantic.BaseModel 对象 | 输出需要严格结构化(JSON-like)数据时 |
| DatetimeOutputParser | 从文本中智能地解析出日期和时间信息。 | datetime.datetime | 需要时间格式时 |
1 字符串解析器
StrOutputParser,最简单的解析器,用于提取模型返回的原始文本:
python
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建简单链
prompt = ChatPromptTemplate.from_template("解释{topic}是什么?回答控制20字以内")
# ============================================================
# chain1:不加解析器
# ============================================================
chain1 = prompt | model
result1 = chain1.invoke({"topic": "ai"})
print(f"result1-->{result1}")
print(f"result1 的类型:{type(result1)}")
# ============================================================
# chain2:加解析器
# ============================================================
parser = StrOutputParser()
chain2 = prompt | model | parser
result2 = chain2.invoke({"topic": "ai"})
print(f"result2-->{result2}")
print(f"result2 的类型:{type(result2)}")
加了 StrOutputParser 和没加的区别:
-
输出类型不同
- 不加解析器:拿到的是模型返回的原始对象。如果模型是 ChatModel(如 ChatOllama、ChatOpenAI),返回的是 AIMessage,需要用 .content 才能取到文本。
- 加了解析器:返回的是纯字符串 str,直接就能打印、拼接、写文件、传给下一个组件。
-
对 OllamaLLM 来说,区别不明显
OllamaLLM 属于 LLM 接口(不是 ChatModel),invoke 返回的本来就是字符串,所以加不加 StrOutputParser,打印出来的结果看起来一样。但仍建议加上。
-
对 ChatModel 来说,区别很明显
如果把 model 换成 ChatOllama,不加解析器时:
pythonresult1 = chain1.invoke({"topic": "ai"}) print(result1) # 打印 AIMessage(...) print(result1.content) # 才是真正的文本加了解析器后:
pythonresult2 = chain2.invoke({"topic": "ai"}) print(result2) # 直接就是文本 -
统一接口,便于替换模型
写链时加上 StrOutputParser,以后把 OllamaLLM 换成 ChatOllama、ChatOpenAI、ChatDeepSeek,下游代码(打印、拼接、存文件)都不用改,因为拿到的永远是字符串。
-
便于和后续组件组合
如果链后面还要接别的 Runnable,比如再拼一个 PromptTemplate、或者做字符串处理,它们期望的输入是字符串而不是消息对象,这时 StrOutputParser 就是必需的衔接层。
总结: 加 StrOutputParser 的核心作用是"把模型输出统一成字符串",对 LLM 接口的模型(如 OllamaLLM)只是锦上添花,对 ChatModel 接口的模型(如 ChatOllama)几乎是必备。在写可复用、可替换模型的链时,推荐始终加上。
2 列表解析器
CommaSeparatedListOutputParser,将逗号分隔的文本转换为 Python 列表:
python
from langchain_core.output_parsers import CommaSeparatedListOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化本地 Ollama 模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建列表解析器:负责把模型输出的字符串解析成 Python 列表
parser = CommaSeparatedListOutputParser()
# 获取解析器自带的"格式说明"文本
format_instructions = parser.get_format_instructions()
# 创建提示模板,把格式说明作为模板的一部分传给模型
prompt = ChatPromptTemplate.from_template(
"用中文列出{topic}的五个最重要特点。\n{format_instructions}"
)
# 组合链:提示 -> 模型 -> 解析器
chain = prompt | model | parser
# 调用链
result = chain.invoke({
"topic": "大模型",
"format_instructions": format_instructions
})
print(result)
format_instructions = parser.get_format_instructions() 是干什么的?
一句话:它返回一段"告诉模型应该按什么格式输出"的说明文本。
CommaSeparatedListOutputParser 这个解析器,期望模型输出的是类似 "特点1, 特点2, 特点3" 这种以逗号分隔的字符串,它才能用 split(",") 把它切成 Python 列表。
但如果只告诉模型"列出五个特点",模型可能输出:
- 分点列出(1. 2. 3.)
- 每行一个特点
- 用中文逗号、顿号分隔
- 带解释性文字
这样解析器就切不出预期的列表。
所以解析器提供了一个方法 get_format_instructions(),它返回一段固定的英文说明,内容大致是:
"Your response should be a list of comma separated values, eg: foo, bar, baz"
把这段说明塞进提示里,就是在"教"模型按逗号分隔的格式输出,让模型的输出能和解析器的期望对齐。
调用逻辑(从创建解析器到最终拿到列表):
第 1 步:parser = CommaSeparatedListOutputParser()
创建一个解析器对象,它内部知道"逗号分隔"的解析规则,同时也知道"应该提示模型按逗号分隔输出"的说明文本。
第 2 步:format_instructions = parser.get_format_instructions()
调用解析器的方法,拿到那段说明文本。此时还没有和模型发生任何交互,只是从解析器里"取出"一段预置的提示词。
第 3 步:把 format_instructions 作为变量放进 PromptTemplate
模板变成:
"用中文列出{topic}的五个最重要特点。\nYour response should be a list of comma separated values, eg: foo, bar, baz"
第 4 步:chain.invoke({"topic": "大模型", "format_instructions": ...})
模板把两个变量都填进去,生成完整提示:
"用中文列出大模型的五个最重要特点。\nYour response should be a list of comma separated values, eg: foo, bar, baz"
然后这段提示被送给模型。
第 5 步:模型按提示要求输出,比如:
"参数规模大, 泛化能力强, 需要大量算力, 可微调, 支持多任务"
第 6 步:parser 收到这段字符串,按逗号切分并去掉空白,返回 Python 列表:
"参数规模大", "泛化能力强", "需要大量算力", "可微调", "支持多任务"
第 7 步:print(result) 打印这个列表。
为什么要把 format_instructions 传进模板,而不是直接写死?
-
解耦:格式要求和解析逻辑绑定在同一个解析器上。以后换成别的解析器(比如 JsonOutputParser),只需要换 parser,format_instructions 会自动跟着变,提示模板不用改。
-
复用:同一个解析器的说明文本可以用在多个提示里。
-
一致性:解析器期望什么格式,提示里就告诉模型什么格式,两边天然对齐,避免"模型输出格式"和"解析器期望格式"不一致。
易错点:
-
format_instructions 是模板变量,invoke 时必须传。很多人会忘记传,导致 KeyError: 'format_instructions'。如果不想每次传,可以用 partial 预先绑定:
pythonprompt = prompt.partial(format_instructions=format_instructions)或者直接把 format_instructions 拼进模板字符串里。
-
解析器依赖模型严格按格式输出。如果模型没按逗号分隔输出,解析结果可能不符合预期。实际使用中可以在提示里再强调一次"只输出逗号分隔的内容,不要编号和解释"。
-
不同解析器的 get_format_instructions() 返回内容不同:
- CommaSeparatedListOutputParser:逗号分隔列表说明
- JsonOutputParser:JSON 格式说明
- PydanticOutputParser:按 Pydantic 模型生成的 JSON Schema 说明
它们的作用都是一样的:把"期望格式"告诉模型。
3 JSON 解析器
JsonOutputParser,将 JSON 格式文本转换为 Python 字典或列表:
python
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建JSON解析器
json_parser = JsonOutputParser()
# 创建带格式说明的提示模板
json_format_instructions = json_parser.get_format_instructions()
json_prompt = ChatPromptTemplate.from_template(
"生成一个包含{person}基本信息的JSON。应包括姓名、职业、年龄和技能列表, 不要包含任何注释或额外说明。\n{format_instructions}"
)
# 组合组件
json_chain = json_prompt | model | json_parser
# 调用链
result = json_chain.invoke({
"person": "雷军",
"format_instructions": json_format_instructions
})
print(result)
4 Pydantic 解析器
PydanticOutputParser,使用 Pydantic 模型定义输出结构:
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
from langchain_ollama import OllamaLLM
# 实例化本地 Ollama 模型
model = OllamaLLM(model="qwen2.5:7b")
# ============================================================
# 定义 Pydantic 模型:描述期望输出的结构
# ============================================================
class Movie(BaseModel):
title: str = Field(description="电影标题")
director: str = Field(description="导演姓名")
year: int = Field(description="上映年份")
genre: List[str] = Field(description="电影类型")
rating: float = Field(description="评分(1-10)")
# ============================================================
# 为什么要用 Pydantic 解析器
# ============================================================
# 先看不用它会怎样。
# 直接调用 model.invoke("生成一部科幻电影的信息,包含标题、导演、年份、类型、评分"),
# 模型可能返回:
# 好的,以下是一部科幻电影的信息:
# 标题:《星际穿越》
# 导演:克里斯托弗·诺兰
# 上映年份:2014年
# 类型:科幻、冒险、剧情
# 评分:9.3分
#
# 这是一个字符串。如果程序后面要用这些数据(存数据库、做推荐、传前端),
# 就得写正则或字符串切割把每个字段抠出来,
# 还要处理"2014年"带单位、"科幻、冒险"用顿号分隔这类情况。
# 一旦模型换了措辞(比如把"导演"写成"导演姓名"),解析代码就崩了。
#
# 问题本质:
# 模型输出是"非结构化的自然语言",
# 程序需要的是"结构化的数据",
# 中间缺少一层可靠的转换。
#
# Pydantic 解析器就是这层转换,它做了两件事:
# 1. 事前约束:根据 Movie 类生成 JSON Schema 说明,塞进提示,
# 告诉模型该输出什么字段、什么类型,而不是让它自由发挥。
# 2. 事后解析:模型输出 JSON 字符串后,自动解析成 dict,
# 再构造 Movie 实例,并做类型转换和校验。
# 最终拿到的不是字符串,而是一个 Movie 对象,可以直接 .title 访问。
# ============================================================
# 创建 Pydantic 解析器
# ============================================================
pydantic_parser = PydanticOutputParser(pydantic_object=Movie)
# ============================================================
# 获取格式说明并放进提示模板
# ============================================================
format_instructions = pydantic_parser.get_format_instructions()
pydantic_prompt = ChatPromptTemplate.from_template(
"生成一部{genre}电影的信息。\n{format_instructions}"
)
# ============================================================
# 组合链:提示 -> 模型 -> 解析器
# ============================================================
pydantic_chain = pydantic_prompt | model | pydantic_parser
# ============================================================
# 调用链
# ============================================================
movie_data = pydantic_chain.invoke({
"genre": "科幻",
"format_instructions": format_instructions
})
print(movie_data)
print(type(movie_data)) # <class '__main__.Movie'>
print(movie_data.title) # 直接按字段取值
print(movie_data.director)
print(movie_data.genre) # 是 Python 列表
print(movie_data.rating) # 是 float,不是字符串
# ============================================================
# Pydantic 解析器的核心好处
# ============================================================
# 1. 输出结构化,程序可直接使用
# 2. 有类型校验,错误早暴露
# 3. 用 schema 描述意图,可读性强
# 4. 格式说明自动生成,提示与解析天然对齐
# 5. 便于后续组合
# 6. 与 Pydantic 生态无缝衔接
# ============================================================
# 什么时候该用 Pydantic 解析器
# ============================================================
# 推荐用:
# - 需要把模型输出直接入库或调 API
# - 字段多、类型多,手工解析容易出错
# - 希望模型输出格式稳定、可校验
# - 项目已经用 Pydantic(比如 FastAPI)
#
# 可以不用的场景:
# - 只是聊天、翻译、摘要,输出本来就是自然语言
# - 输出非常简单(就一个词、一个数),StrOutputParser 足够
# - 模型能力弱,难以稳定输出合法 JSON,用了反而频繁报错
多部电影的 Pydantic 模型:
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
from langchain_ollama import OllamaLLM
# 实例化本地 Ollama 模型
model = OllamaLLM(model="qwen2.5:7b")
# ============================================================
# 单个电影的 Pydantic 模型(复用 demo1 的定义)
# ============================================================
class Movie(BaseModel):
title: str = Field(description="电影标题")
director: str = Field(description="导演姓名")
year: int = Field(description="上映年份")
genre: List[str] = Field(description="电影类型")
rating: float = Field(description="评分(1-10)")
# ============================================================
# 多部电影的 Pydantic 模型
# ============================================================
class MovieList(BaseModel):
movies: List[Movie] = Field(description="电影列表")
# ============================================================
# demo2:输出多部电影
# ============================================================
list_parser = PydanticOutputParser(pydantic_object=MovieList)
list_format_instructions = list_parser.get_format_instructions()
list_prompt = ChatPromptTemplate.from_template(
"生成3部{genre}电影的信息。\n{format_instructions}"
)
list_chain = list_prompt | model | list_parser
movie_list_data = list_chain.invoke({
"genre": "科幻",
"format_instructions": list_format_instructions
})
print(movie_list_data)
print(type(movie_list_data)) # <class '__main__.MovieList'>
print("电影数量:", len(movie_list_data.movies))
for i, m in enumerate(movie_list_data.movies, start=1):
print(f"\n第{i}部电影:")
print(" 标题:", m.title)
print(" 导演:", m.director)
print(" 年份:", m.year)
print(" 类型:", m.genre) # 是 Python 列表
print(" 评分:", m.rating) # 是 float
5 自定义解析器
创建自定义输出解析器:
python
from langchain_core.output_parsers import BaseOutputParser
from typing import Dict, Any
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import OllamaLLM
# 实例化模型
model = OllamaLLM(model="qwen2.5:7b")
class CustomKeyValueParser(BaseOutputParser[Dict[str, Any]]):
"""解析形如'key: value'的文本"""
def parse(self, text: str) -> Dict[str, Any]:
"""从文本中解析键值对"""
result = {}
lines = text.strip().split('\n')
for line in lines:
if ':' in line:
key, value = line.split(':', 1)
result[key.strip()] = value.strip()
return result
def get_format_instructions(self) -> str:
"""提供格式指导给模型"""
return """请以'键: 值'的格式返回信息,每行一个键值对。
例如:
名称: 爱因斯坦
职业: 物理学家
贡献: 相对论"""
# 使用自定义解析器
custom_parser = CustomKeyValueParser()
custom_prompt = ChatPromptTemplate.from_template(
"提供关于{person}的基本信息。\n{format_instructions}"
)
# 组合组件
custom_chain = custom_prompt | model | custom_parser
# 调用模型
result = custom_chain.invoke({
"person": "屠呦呦",
"format_instructions": custom_parser.get_format_instructions()
})
print(result)
六、Memory
大模型本身不具备上下文的概念,它并不保存上次交互的内容,ChatGPT 之所以能够和人正常沟通对话,因为它进行了一层封装,将历史记录回传给了模型。
因此 LangChain 也提供了 Memory 组件,Memory 分为两种类型:短期记忆和长期记忆。短期记忆一般指单一会话时传递数据,长期记忆则是处理多个会话时获取和更新信息。
1 使用 ChatMessageHistory 手动添加上下文
python
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.messages import messages_to_dict, messages_from_dict
import json
# 1.创建一个ChatMessageHistory对象,用来存储对话信息
history = ChatMessageHistory()
# 添加用户消息
history.add_user_message("在吗?")
# 添加大模型消息
history.add_ai_message("在")
# 打印所有消息
print(f'history.messages-->{history.messages}')
# 2.可以将history.messages中的信息保存到字典中,然后保存到数据库或者文件中,方便后续读取
# 2.1 messages_to_dict()方法将history.messages中的信息转换成字典
dicts = messages_to_dict(history.messages)
print(f'dicts-->{dicts}')
# 2.2 这里将dicts保存到文件中
with open('history.json', 'w', encoding='utf-8') as f:
f.write(json.dumps(dicts, indent=2, ensure_ascii=False))
# 3.从文件中读取出字典,然后将字典转换成消息
# 3.1 读取文件
messages = json.load(open('history.json', 'r', encoding='utf-8'))
# 3.2 然后将字典转换成消息
chat_messages = messages_from_dict(messages)
print(f'chat_messages-->{chat_messages}')
2 短期记忆(Short-term Memory)
短期记忆的本质是线程级状态管理。在 LangGraph 中:
短期记忆 = Agent 状态(State) + 检查点持久化(Checkpointer) + 线程标识(thread_id)。
(1)状态(State)
通常是一个包含 messages 字段的字典或 Pydantic 模型(如 MessagesState 或自定义 CustomState),用于存储当前会话的所有消息、中间变量、工具调用结果等。
(2)检查点器(Checkpointer)
负责将状态序列化并持久化到内存、SQLite、PostgreSQL 等后端。每次状态变更(如新增一条消息)都会触发一次检查点保存。
(3)会话 ID(thread_id)
作为会话的唯一标识符,确保不同用户或不同对话之间的状态完全隔离。即使多个用户并发交互,也不会发生记忆混淆。
python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from langchain.messages import HumanMessage
from langchain_ollama import OllamaLLM
import time
# 初始化大模型,这里使用的是 Qwen 的聊天模型
model = OllamaLLM(model="qwen2.5:7b")
# 创建内存保存器 (Checkpointer)
# 这是 LangGraph 的核心概念之一,用于在对话过程中保存和恢复状态(记忆)
# 它内部维护了一个字典,结构大致是 {thread_id: 该会话的消息历史}
# 每次 Agent 被调用时,会根据传入的 thread_id 找到对应的历史记录,
# 把新消息追加进去,再把整个历史一起交给模型,
# 这样模型就能"记住"之前说过什么。
checkpointer = InMemorySaver()
# 定义 Agent 的配置
# "thread_id" (线程ID) 非常关键:它相当于对话的 ID,用于区分不同的会话记忆
# 使用时间戳作为 ID,确保每次运行脚本时的线程 ID 不同(但在同一次运行中复用)
# config 是一个字典,通过 configurable 键传运行时参数,
# 这里的 thread_id 就是告诉 checkpointer 用哪个会话的存档。
timestamp = time.time()
config = {
"configurable": {"thread_id": f"{int(timestamp)}"}
}
# 创建 Agent 实例
# model: 指定使用的底层大模型
# tools: 工具列表,这里为空,表示 Agent 只能进行对话,不能调用外部工具(如搜索、计算器等)
# 如果传入工具,Agent 会在需要时自动决定调用哪个工具,拿到结果后再继续回答
# checkpointer: 传入上面的内存保存器,赋予 Agent 记忆能力
# system_prompt: 设定 Agent 的角色和行为模式
# 这里设为"翻译官",模型会在后续回答中尽量贴合这个身份
agent = create_agent(
model=model,
tools=[],
checkpointer=checkpointer,
system_prompt="你是一个翻译官,擅长中英互译。"
)
# 第一轮对话输入
# Agent 的输入格式是一个字典,messages 键下是一个消息列表。
# 这里用 HumanMessage 包装用户说的话,表示这是"人"发出的消息。
# 之所以是列表,是为了支持一次传入多条消息(比如历史 + 新消息)。
input_1 = {
"messages": [HumanMessage("你好,我叫小呆。")]
}
# 调用 Agent (Response A)
# 使用 config,这意味着这次对话会记录在 thread_id 对应的内存中
# Agent 会记住 "我叫小呆" 这个信息
# 返回值是一个字典,messages 键下是本次交互产生的完整消息列表
# (包含用户消息、模型回复,有时还会有工具调用消息)。
response_a = agent.invoke(
input=input_1,
config=config
)
# 第二轮对话输入(测试是否记得之前的信息)
# 换一个问法,看 Agent 能不能从记忆里找到"小呆"这个名字
input_2 = {
"messages": [HumanMessage("你好,我叫什么?")]
}
# 定义一个新的配置 (config_2),使用完全不同的 thread_id
# 这代表开启了一个全新的、独立的对话会话
# 由于 thread_id 不同,checkpointer 里找不到任何历史记录,
# Agent 就像一个刚被创建、什么都没听说过的新助手。
config_2 = {
"configurable": {"thread_id": f"{int(time.time())}"}
}
# 调用 Agent (Response B)
# 使用新的 config_2,Agent 看不到 config_1 中的历史记录
# 因此,Agent 应该不知道用户叫 "小呆",因为它处于一个新的 "线程" 中
# 它只会基于当前这一句"你好,我叫什么?"来回答,
# 通常会反问"请问您叫什么?"或表示不知道。
response_b = agent.invoke(
input=input_2,
config=config_2
)
# 调用 Agent (Response C)
# 再次使用最初的 config
# 这会恢复到第一个会话的记忆中
# checkpointer 根据最初那个 thread_id 取出历史记录,
# 里面包含了"我叫小呆"这条信息,一起交给模型,
# Agent 应该能回想起用户叫 "小呆",因为它共享同一个 thread_id
response_c = agent.invoke(
input=input_2,
config=config
)
# 输出结果进行对比
# Response A:第一轮,Agent 记录了用户叫小呆
# Response B:新线程,没有历史,Agent 不知道用户叫什么
# Response C:回到原线程,有历史,Agent 记得用户叫小呆
print("Response A (第一次对话):")
print(response_a)
print("\nResponse B (新线程,无记忆):")
print(response_b)
print("\nResponse C (回到原线程,有记忆):")
print(response_c)
代码运行逻辑:
- checkpointer (记忆核心):这是 LangGraph 区别于传统简单 API 调用的关键。它允许 Agent 在多次 invoke 调用之间保存状态。
- thread_id (记忆索引) :可以把 thread_id 想象成数据库中的主键。
- Response A:向 ID 为 100 的记录里写入了名字。
- Response B:向 ID 为 200 的新记录提问,因为 200 是空的,所以 Agent 不知道名字。
- Response C:又回到 ID 为 100 的记录提问,Agent 读取了历史记录,所以知道名字。
- System Prompt:虽然输入是在对话,但因为设定了"你是一个翻译官",Agent 可能会试图在回答的同时进行翻译,或者在回复格式上符合翻译官的身份。
3 长期记忆
对于需要长期运行和可靠记忆的应用,推荐使用数据库进行持久化。详见后面阶段 LangGraph 部分。
4 ConversationBufferMemory
ConversationBufferMemory 会将所有历史对话原封不动地完整保存下来。当进行新的对话时,它会将全部历史记录和新问题一起发送给 LLM。
- 优点:
- 信息完整:保留了所有原始对话细节,没有任何信息丢失。
- 简单直观:实现和理解都非常容易。
- 缺点:
- 成本高/Token 消耗大:随着对话轮次增加,历史记录会变得非常长,导致 API 请求的 Token 数量急剧增加,不仅提高成本,还可能超出模型的上下文窗口限制而报错。
- 适用场景:
- 短时、简短的对话。
- 用于学习和调试 LangChain 记忆功能的入门场景。
python
# -*- coding: utf-8 -*-
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
# --- DeepSeek API 配置 ---
API_KEY = "sk-52e226ac3cac46838cb282b45b1a648e"
API_URL = "https://api.deepseek.com/v1"
MODEL = "deepseek-chat"
# 1. 初始化 LLM
llm = ChatOpenAI(
model=MODEL,
api_key=API_KEY,
base_url=API_URL,
temperature=0.7
)
# 2. 初始化 ConversationBufferMemory
# 这是最简单的记忆类型,它会存储所有对话历史。
# memory_key="history" 指定了在提示模板中,用 "history" 这个变量名来引用记忆内容。
memory = ConversationBufferMemory(memory_key="history")
# 3. 创建 ConversationChain
# ConversationChain 是一个将 LLM 和 Memory 链接在一起的链。
# verbose=True 会在控制台打印出完整的提示(Prompt)和模型的思考过程,非常有助于理解和调试。
conversation_chain = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
# --- 开始对话 ---
print("--- 开始使用 ConversationBufferMemory ---")
# 第一次对话
response1 = conversation_chain.predict(input="你好,我叫小明。")
print("AI:", response1)
# 第二次对话
# Memory 会自动将第一次的对话内容加入到 Prompt 中
response2 = conversation_chain.predict(input="你还记得我叫什么名字吗?")
print("AI:", response2)
5 ConversationBufferWindowMemory
ConversationBufferWindowMemory 只会保留最近 K 轮的对话历史。它像一个滑动的窗口,当新的对话产生时,最早的对话就会被丢弃,从而保证了历史记录的长度是固定的。
- 优点:
- 控制成本和 Token:有效地将每次请求的 Token 数量限制在一个可控范围内,避免超出上下文限制。
- 性能均衡:在保留上下文和控制资源消耗之间取得了很好的平衡。
- 缺点:
- 丢失早期信息:如果关键信息出现在很早以前(超出了窗口大小),那么这部分记忆将会丢失。
- 适用场景:
- 绝大多数通用的聊天机器人应用。
- 对话的重点主要集中在最近的几次交互中。
python
# -*- coding: utf-8 -*-
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory
API_KEY = "sk-52e226ac3cac46838cb282b45b1a648e"
API_URL = "https://api.deepseek.com/v1"
MODEL = "deepseek-chat"
llm = ChatOpenAI(model=MODEL, api_key=API_KEY, base_url=API_URL, temperature=0.7)
# 2. 初始化 ConversationBufferWindowMemory
# 核心参数是 k,它定义了要保留的对话轮次(1轮 = 1次用户提问 + 1次AI回答)。
# 这里我们设置 k=1,意味着只保留最近的1轮对话。
memory_window = ConversationBufferWindowMemory(k=1, memory_key="history")
# 3. 创建 ConversationChain
conversation_chain_window = ConversationChain(
llm=llm,
memory=memory_window,
verbose=True
)
# --- 开始对话 ---
print("\n--- 开始使用 ConversationBufferWindowMemory (k=1) ---")
# 第1轮对话
response1 = conversation_chain_window.predict(input="你好,我叫小明,我最喜欢的水果是苹果。")
print("AI:", response1)
# 第2轮对话
response2 = conversation_chain_window.predict(input="我最喜欢的运动是篮球。")
print("AI:", response2)
# 第3轮对话
# 此时第1轮对话已经被挤出窗口,模型应该已经忘记了我的名字和喜欢的水果。
response3 = conversation_chain_window.predict(input="你还记得我叫什么,最喜欢什么水果吗?")
print("AI:", response3)
6 ConversationSummaryMemory
ConversationSummaryMemory 会使用一个 LLM 来不断地总结对话历史,而不是简单地保存全部原文。当新的对话发生时,它会将新内容与旧的摘要合并,生成一个新的、更简洁的摘要。
- 优点:
- 节省 Token:摘要比原文短得多,可以大幅减少 Token 消耗。
- 保留长期信息:即使对话很长,早期的核心信息也能以摘要的形式被保留下来。
- 缺点:
- 信息损失:总结过程会丢失细节,可能影响对具体问题的回答。
- 额外开销:每次更新摘要都需要调用一次 LLM,增加了额外的计算成本和时间。
- 适用场景:
- 需要长时间运行、但对早期对话细节要求不高的场景。
- 希望在保留核心信息的同时,控制 Token 成本。
python
# -*- coding: utf-8 -*-
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationSummaryMemory
from langchain.prompts import PromptTemplate
# --- LLM 配置 ---
API_KEY = "sk-52e226ac3cac46838cb282b45b1a648e"
API_URL = "https://api.deepseek.com/v1"
MODEL = "deepseek-chat"
llm = ChatOpenAI(model=MODEL, api_key=API_KEY, base_url=API_URL, temperature=0.7)
# 中文摘要提示模板
template = """请将以下对话进行总结...
当前摘要:
{summary}
新对话内容:
{new_lines}
新的中文摘要:"""
CHINESE_SUMMARY_PROMPT = PromptTemplate(
input_variables=["summary", "new_lines"],
template=template
)
# 初始化内存
memory = ConversationSummaryMemory(
llm=llm,
prompt=CHINESE_SUMMARY_PROMPT,
memory_key="history"
)
# 创建对话链
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=False
)
# --- 测试 ---
print("=== 初始状态 ===")
print("记忆变量:", memory.load_memory_variables({}))
print("缓冲区:", memory.buffer)
# 进行对话
response1 = conversation.predict(input="你好,我叫小明")
print("\n=== 第一次对话后 ===")
print("记忆变量:", memory.load_memory_variables({}))
print("缓冲区:", memory.buffer)
response2 = conversation.predict(input="我喜欢编程")
print("\n=== 第二次对话后 ===")
print("记忆变量:", memory.load_memory_variables({}))
print("缓冲区:", memory.buffer)
print("\nAI:")
print(response2)
七、Indexes
Indexes 组件的目的是让 LangChain 具备处理文档处理的能力,包括:文档加载、检索等。注意,这里的文档不局限于 txt、pdf 等文本类内容,还涵盖 email、区块链、视频等内容。
Indexes 组件主要包含类型:
- 文档加载器
- 文本分割器
- VectorStores
- 检索器
1 文档加载器
文档加载器主要基于 Unstructured 包,Unstructured 是一个 python 包,可以把各种类型的文件转换成文本。文档加载器使用起来很简单,只需要引入相应的 loader 工具。
LangChain 支持的文档加载器 (部分):
示例代码:
需要安装的包:
bash
pip install unstructured
pip install langchain-unstructured
python
from langchain_unstructured import UnstructuredLoader
# 创建 UnstructuredLoader 对象
loader = UnstructuredLoader('./data/衣服属性.txt', encoding='utf8')
docs = loader.load()
print(f'docs-->{docs}')
print(f'len-->{len(docs)}')
print(f'第一行数据-->{docs[0].page_content}')
print('*' * 100)
from langchain_community.document_loaders import TextLoader
# 创建 TextLoader 对象
loader = TextLoader('./data/衣服属性.txt', encoding='utf8')
docs = loader.load()
print(f'docs-->{docs}')
print(f'len-->{len(docs)}')
print('第一行数据-->{}'.format(docs[0].page_content.split('\n')[0]))
TextLoader 示例:
python
from langchain_community.document_loaders import TextLoader
from config import Config
from langchain_openai import ChatOpenAI
from datetime import datetime
# 初始化配置和模型
conf = Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
# Document Loaders 示例:加载文档并接入大模型总结
loader = TextLoader(r"D:\LLM_Codes\Chapter3_RAG\rag_base_frame\data\林青霞.txt", encoding="utf-8")
documents = loader.load()
doc=documents[0]
print("\n--- 1. 加载后的原始元信息 ---")
print(doc.metadata)
# 1.2 像操作字典一样,为 Document 对象添加自定义元信息
print("\n--- 2. 添加自定义元信息 ---")
doc.metadata['author'] = 'DT.L'
doc.metadata['version'] = '1.1'
doc.metadata['processed_at'] = datetime.now().isoformat()
doc.metadata['tags'] = ['test', 'loader', 'metadata']
print("更新后的元信息:")
print(doc.metadata)
print("\n--- 3. 访问特定的元信息 ---")
print(f"Author: {doc.metadata.get('author', 'Unknown')}")
print(f"Tags: {doc.metadata.get('tags')}")
print("\n--- 4. 删除元信息 ---")
del doc.metadata['version']
print("更新后的元信息:")
print(doc.metadata)
print("\n--- 5. 获取文本信息 ---")
print(doc.page_content)
# --- 6. 清空文本信息 -不能采用del ---
print("\n--- 6. 清空文本信息 ---")
doc.page_content = "" # 正确做法:赋值为空字符串,而不是删除属性
print("更新后的信息:")
print(doc)
PyPDFLoader 示例:
python
from langchain_community.document_loaders import TextLoader,PyPDFLoader
from config import Config
from langchain_openai import ChatOpenAI
# 初始化配置和模型
conf = Config()
llm = ChatOpenAI(
model=conf.MODEL,
api_key=conf.API_KEY,
base_url=conf.API_URL,
temperature=0.7,
max_tokens=150
)
# Document Loaders 示例:加载文档并接入大模型总结
loader = PyPDFLoader(r"D:\LLM_Codes\Chapter3_RAG\rag_base_frame\data\test_resume.pdf")
documents = loader.load()
content = documents[0].page_content
print("原始简历内容:",content)
# 使用 LLM 总结
result = llm.invoke(f"总结以下内容:{content[:500]}") # 取前500字符
print("Document Loaders 示例结果:", result.content)
DirectoryLoader 批量加载:
TextLoader、PyPDFLoader 等本身是为加载单个文本文件而设计的,因此它不可以一次性批量加载多个文件。要实现批量加载,应该使用 LangChain 提供的 DirectoryLoader。
DirectoryLoader 的作用:
- 针对一种文件类型,它可以加载一个指定目录(文件夹)下的所有文件。
- 它可以处理目录中的多种不同类型的文件。
如果一个文件夹有多个不同文件类型要构建多个 DirectoryLoader 来进行批量加载。
python
from langchain_community.document_loaders import DirectoryLoader, TextLoader
# 1. 指定包含多个 .txt 文件的目录路径
directory_path = r"D:\LLM_Codes\Chapter3_RAG\rag_base_frame\data"
# 2. 创建一个 DirectoryLoader 实例
# - 第一个参数是目录路径
# - glob="**/*.txt" 参数告诉加载器只加载所有以 .txt 结尾的文件
# - loader_cls=TextLoader 指定使用 TextLoader 来处理这些 .txt 文件
# - loader_kwargs={'encoding': 'utf-8'} 将参数传递给 TextLoader,确保使用正确的编码
loader = DirectoryLoader(
directory_path,
glob="**/*.txt",
loader_cls=TextLoader,
loader_kwargs={'encoding': 'utf-8'}
)
# 3. 调用 load() 方法,批量加载所有匹配的文档
documents = loader.load()
print(f"成功批量加载了 {len(documents)} 个 .txt 文件。")
# 可以查看第一个加载的文档
if documents:
print("第一个文档的内容:")
print(documents[0].page_content)
print("第一个文档的元数据:")
print(documents[0].metadata)
document_loaders 特点:
- 多格式支持:处理多种文件格式(如 PDF、TXT、MD、DOCX),适应不同数据源。
- 统一数据结构:将不同来源的数据转换为标准 Document 对象,便于后续处理(如分块、嵌入、检索)。
- 元数据管理:自动提取或添加元数据(如文件路径、时间戳),支持追踪和上下文管理。
- 工业应用:在工业场景中,加载器用于解析简历文本、行业报告、日志文件、技术文档等,结合 LLM 和 OutputParser 生成结构化数据或洞察。
document_loaders 重要性:
- 灵活性:支持本地文件、云存储、网页、数据库等多种数据源,适配企业级需求。
- 加载器与 LangChain 的其他模块(如 TextSplitter、OutputParser)无缝集成,构建复杂数据处理管道。
- 鲁棒性:内置错误处理和编码支持(如 UTF-8),确保加载过程稳定。
- 工业场景:在智能制造、IoT、文档自动化中,加载器可提取非结构化数据(如 PDF 报告、Markdown 技术文档),为 AI 系统提供输入。
2 文档分割器
由于模型对输入的字符长度有限制,在碰到很长的文本时,需要把文本分割成多个小的文本片段。
文本分割最简单的方式是按照字符长度进行分割,但是这会带来很多问题,比如说如果文本是一段代码,一个函数被分割到两段之后就成了没有意义的字符,所以整体的原则是把语义相关的文本片段放在一起。
LangChain 中最基本的文本分割器是 CharacterTextSplitter,它按照指定的分隔符(默认"\n\n")进行分割,并且考虑文本片段的最大长度。
python
from langchain_core.documents import Document
from langchain_text_splitters import CharacterTextSplitter
# 创建分词器
text_splitter = CharacterTextSplitter(separator=" ", chunk_size=5, chunk_overlap=1)
# ============================================================
# 方法 1:split_text
# ============================================================
result1 = text_splitter.split_text("a b c d e f")
print(f'result1--->{result1}')
# ============================================================
# 方法 2:create_documents
# ============================================================
result2 = text_splitter.create_documents(["a b c d e f", "e f g h"])
print(f'result2--->{result2}')
# ============================================================
# 方法 3:split_documents
# ============================================================
result3 = text_splitter.split_documents(
[Document(page_content="a b c d e f", metadata={"id": "1"})]
)
print(f'result3--->{result3}')
三者的区别总结:
-
输入类型不同
split_text:strcreate_documents:Liststrsplit_documents:ListDocument
-
输出类型不同
split_text:Liststrcreate_documents:ListDocument(metadata 默认为空或由你传入)split_documents:ListDocument(保留原 Document 的 metadata)
-
使用场景不同
split_text:只有纯文本,不需要元数据,只想拿到分块后的字符串create_documents:有若干条纯文本,想直接得到 Document 列表(便于后续向量化、检索)split_documents:已有 Document(通常来自 Loader),想在保留元数据的前提下分块
-
本质关系
split_text是最底层的方法,只做"切分"create_documents是在split_text的基础上,把结果包装成 Documentsplit_documents是在split_text的基础上,既包装成 Document,又把原 Document 的 metadata 复制到每个块上
可以粗略理解为:
split_text:文本 -> 文本块列表create_documents:文本列表 -> Document 列表(元数据为空)split_documents:Document 列表 -> Document 列表(元数据保留)
CharacterTextSplitter 的分块逻辑:
它并不是简单地"每 chunk_size 个字符切一刀",而是:
- 先按 separator(这里是空格)把文本切成小片段
- 依次把小片段拼起来,直到再加一个就会超过 chunk_size
- 输出当前块,然后从 chunk_overlap 处回退一点,继续拼下一块
所以最终每块的长度通常接近但不一定等于 chunk_size,chunk_overlap 用来让相邻块之间有一部分内容重叠,避免上下文断裂。
除了 CharacterTextSplitter 分割器,LangChain 还支持其他文档分割器 (部分):
| 分割器名称 | 功能描述 | 类型 | 工业场景应用 |
|---|---|---|---|
| CharacterTextSplitter | 简单按指定分隔符(如换行、逗号)直接分割。 | 基础字符解析 | 简单字符串或 CSV 数据处理,如传感器数据日志。 |
| RecursiveCharacterTextSplitter(使用最多) | 递归按字符分割,先尝试自然边界(如段落、句子),太大则继续细分。 | 通用字符解析 | 通用文本处理,如日志、报告、PDF 文档分割,便于 RAG 检索。 |
| TokenTextSplitter | 按 token(词元)分割,支持 LLM token 计数。 | Token 基于解析 | LLM 输入优化,如处理 API 响应或长查询,控制 token 限制。 |
| SentenceTextSplitter | 按句子边界分割,使用 NLP 识别句子(包括标点)。 | 语义解析 | 自然语言文本,如文章或对话分析,保持句子完整。 |
| SpacyTextSplitter | 使用 SpaCy NLP 库按句子或实体分割(需安装 SpaCy)。 | 语义解析 | 高级 NLP 场景,如实体提取或生物医学文本。 |
| NLTKTextSplitter | 使用 NLTK 库按句子或词分割(需安装 NLTK)。 | 语义解析 | 文本研究或分析,如时间序列数据描述。 |
| MarkdownHeaderTextSplitter | 按 Markdown 结构(如标题、列表)智能分割。 | 结构化解析 | Markdown 文档分割,保留语义结构,用于知识库构建。 |
| HTMLSplitter | 按 HTML 标签(如 、)分割网页内容。 | 结构化解析 | 网页数据爬取,如在线技术文档或新闻提取。 |
| LatexTextSplitter | 按 LaTeX 结构(如章节、公式)分割。 | 结构化解析 | 学术论文或数学文档处理。 |
| PythonCodeTextSplitter | 按 Python 代码结构(如函数、类)分割。 | 代码解析 | 源代码文件分析,如脚本调试或代码库管理。 |
递归字符文本分割器(RecursiveCharacterTextSplitter):
递归字符文本分割器是一种更智能的分割方法,它尝试在特定分隔符处分割文本,以保持更好的语义完整性。
特点:
- 尝试在自然断点处分割文本
- 比简单的字符分割更能保持语义完整性
- 适用于结构化程度较高的文本,如 Markdown、HTML 等
运行流程:
- 首先尝试使用第一个分隔符(如 "\n\n")分割文本
- 如果分割后的块仍然过大,则使用下一个分隔符继续分割
- 重复此过程,直到达到指定的 chunk_size 或用完所有分隔符
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=20,
chunk_overlap=6,
length_function=len,
separators=["\n\n", "\n", " ", ""]
)
text = """
人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。
它们可以帮助人们进行写作、代码生成、甚至是科研探索。
相比之下,新能源的发展同样重要。
电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。
"""
docs = text_splitter.split_text(text)
print(docs)
语义文档分割器(SemanticChunker):
语义文档分割器使用语义理解来分割文本,这是一种更高级的分割方法。
特点:
- 基于语义相似性分割文本
- 能够更好地保持语义完整性
- 计算成本较高,处理大量文本时可能效率较低
- 适用于需要高度语义理解的场景
python
from langchain_experimental.text_splitter import SemanticChunker
from langchain_ollama import OllamaEmbeddings
embed = OllamaEmbeddings(model="mxbai-embed-large")
text_splitter = SemanticChunker(
embeddings=embed,
breakpoint_threshold_type='percentile',
breakpoint_threshold_amount=70.0,
sentence_split_regex=r'(?<=[。!?.!?])\s*',
min_chunk_size=10
)
text = """
人工智能正在快速发展,尤其是大语言模型的应用,正在改变人类的工作方式。
它们可以帮助人们进行写作、代码生成、甚至是科研探索。
相比之下,新能源的发展同样重要。
电动车和太阳能正在逐渐替代传统能源,减少碳排放,对全球环境保护至关重要。
"""
docs = text_splitter.split_text(text)
for i, d in enumerate(docs):
print(f"------ Chunk {i+1} ------")
print(d.strip())
print()
MarkdownHeaderTextSplitter(Markdown 文档切割器):
适用于 Markdown 文档,按照标题进行拆分。
python
from langchain_text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
markdown_text = "# Header 1\nSome text\n## Header 2\nMore text\n### Header 3\nEven more text"
docs = markdown_splitter.split_text(markdown_text)
print(docs)
MarkdownTextSplitter:
MarkdownTextSplitter 按照 Markdown 的语法元素进行层次化切分。它的默认切分规则优先级如下:
- 一级标题 (#)
- 二级标题 (##)
- 三级标题 (###)
- 代码块 (```)
- 水平分割线 (---, ***等)
- 最后,再按 \n (换行) 和 (空格) 作为补充。
切分规则:优先按 Markdown 的标题、代码块等结构进行切分,但如果切分后的块依然大于 chunk_size,则会在该块内部继续切分以满足长度限制。【chunk_size=100 这个参数仍然是硬性约束。】
HTMLHeaderTextSplitter:
HTMLHeaderTextSplitter 分割器的规则完全由在初始化时提供的 headers_to_split_on 参数定义。
python
headers_to_split_on=[("h1", "Header 1"), ("h2", "Header 2")] #即标签
切分规则:严格按照用户指定的 HTML 标题标签 (h1, h2 等) 进行切分,并将标题内容作为上下文存入每个块的 metadata 中。这对于需要保留文档层级信息的 RAG 应用非常有用。
tips:没有 chunk_size 参数。
LatexTextSplitter:
LatexTextSplitter 的规则与 MarkdownTextSplitter 类似,但它识别的是 LaTeX 的文档结构命令。
它可以智能地分割用 LaTeX 格式编写的文档。与普通分割器(如按字符数分割)不同,LatexTextSplitter 能够"基于"LaTeX 的语法结构。它会寻找并优先在以下这些结构化命令处进行切分:
- 章节命令:\section{...}、\subsection{...} 等。
- 环境命令:\begin{itemize}(项目列表)、\begin{enumerate}(编号列表)等。
- 文档声明:\documentclass{...}
目标:LatexTextSplitter 的目标是生成在语义上完整的文本块(chunks)。例如,它会尽量将一个 \section 下的所有内容放在一个块里,而不是从一句话中间粗暴地切开。这对于后续将文本送入大语言模型进行理解和处理至关重要,因为它保留了原始文档的逻辑上下文。
简单来说,它是一把懂得学术论文结构的"智能文档分割器"。注意:LatexTextSplitter 只对以下源码进行切割,对于 LaTeX 格式文档转为 pdf 就无法采用这种切割方式,那么如何含有公式的 pdf 则可以考虑先用 facebook 的 https://huggingface.co/facebook/nougat-base 模型,Nougat 会输出一个结构化的 Markdown 文本。在这个 Markdown 中,普通文本被完美保留,而公式会被自动识别并转换成 LaTeX 代码,表格也会被转换成 Markdown 表格格式。
PythonCodeTextSplitter(代码分割器):
PythonCodeTextSplitter(代码分割器)专门用于分割源代码文件的分割器,它能按代码的语法结构(如函数、类)进行分割。
代码的语义单元是函数或类,而不是段落或句子。按这些语法结构分割,可以确保检索到的代码块是功能完整的,便于模型理解和分析。
适用场景与数据特点:
- 场景:源代码分析、代码库问答、脚本调试。
- 数据特点:具有严格、明确的编程语法,如 Python 的缩进和 def/class 关键字。
优势:
- 代码块完整:确保函数或类的定义不被切分。
- 提升代码理解:为 LLM 提供结构完整的代码上下文。
劣势:
- 语言特定:通常需要为不同编程语言选择对应的分割器。
NLP 语义分割器:
SentenceTextSplitter / SpacyTextSplitter / NLTKTextSplitter (NLP 语义分割器)是利用 NLP (自然语言处理) 库(如 NLTK, spaCy)的能力,按句子边界来分割文本。
句子是自然语言中最基本的完整语义单元。按句子分割可以最大限度地避免"断章取义",确保每个块都至少包含一句完整的话。
适用场景与数据特点:
- 场景:处理自然语言文章、对话记录、法律合同等对句子完整性要求高的文本。
- 数据特点:文本由符合语法规则的句子构成。这类分割器对于处理复杂的长句或不规范的标点符号比简单的正则匹配更鲁棒。
优势:
- 语义完整性高:确保分割边界在句子层面。
- 处理复杂句子:NLP 库能更好地处理各种标点和句子结构。
劣势:
- 计算开销大:需要加载 NLP 模型,比简单的字符分割慢。
- 依赖外部库:需要安装和配置 NLTK, spaCy 等。
- 句子长度不一:可能产生非常短或非常长的块(单个句子)。
Chunk 的目的:
Chunk 是长文本被分割后的小片段,每个 chunk 是一个独立的文本单元(通常 100-1000 字符),在 LangChain 中以 Document 对象形式存储。Chunk 的主要目的包括:
- 便于 LLM 处理:LLM(如 ChatOpenAI、DeepSeekLLM)有输入长度限制(如 4096 tokens),长文本无法一次性输入。Chunk 将大文档分成小块,确保每个块适合 LLM 处理。
- 用于向量嵌入(Embedding):在 RAG 系统中,chunk 是向量化的基本单位。每个 chunk 被转换为向量(embedding),存储到向量数据库(如 milvus、Chroma、FAISS)。检索时,只需搜索相关 chunk,减少计算量,提高效率。
- 优化检索:小块更适合精确检索。例如,搜索"AI 发展历史"时,只返回包含相关内容的 chunk,而不是整个文档,提升 RAG 系统的准确性和速度。
- 内存和计算效率:大文档直接处理可能导致内存溢出或高计算成本。Chunk 化后,只处理必要部分,节省资源。
- 工业场景:在智能制造或 IoT 中,chunk 化日志或报告便于快速定位异常(如设备故障),或生成结构化数据(如 JSON)用于分析。
Chunk_size 和 Chunk_overlap:
chunk_size:每个 chunk 的最大长度(通常按字符数或 token 数)。例如,chunk_size=200 表示每个块不超过 200 字符。大小影响处理效率:
- 太小:块太碎,上下文丢失,检索效果差。
- 太大:块过长,可能超 LLM 输入限制或增加计算成本。
Chunk_overlap:相邻 chunk 之间的重叠字符数。例如,chunk_overlap=50 表示每个 chunk 的末尾 50 字符会与下一个 chunk 的开头重复。重叠确保:
- 句子或段落不被硬切断,保留语义完整性。
- 检索时上下文连贯,类似书页重叠,避免"断章取义"。
通俗理解:Chunk 像把一本厚书切成小章节。每个章节(chunk)短小精悍,方便阅读(LLM 处理)、搜索(检索)和存储(嵌入向量)。重叠(overlap)像章节间重复几句话,确保不丢剧情。
如果不 chunk 会怎么样?
直接处理长文本可能导致内存溢出、LLM 混乱、检索效率低或信息丢失。
3 VectorStores
VectorStores 是一种特殊类型的数据库,它的作用是存储由嵌入创建的向量,提供相似查询等功能。
LangChain 支持的 VectorStore 有 https://python.langchain.com/docs/integrations/vectorstores/,常见的如下:
使用其中一个 Chroma 组件作为例子:
bash
pip install chromadb
pip install langchain-chroma
python
from langchain_text_splitters import CharacterTextSplitter
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_ollama import OllamaEmbeddings
# 1.加载文档
loader = TextLoader('./data/pku.txt', encoding='utf-8')
docs = loader.load()
# 2.将文档进行分块
text_splitter = CharacterTextSplitter(separator="\n\n", chunk_size=200, chunk_overlap=30)
split_docs = text_splitter.split_documents(docs)
print(f'split_docs-->{split_docs}')
# 3.将分割后的文档存储到向量数据库中
embedding = OllamaEmbeddings(model="mxbai-embed-large")
chromadaDB = Chroma.from_documents(documents=split_docs,
embedding=embedding,
persist_directory='./chroma_db')
# 假如向量数据库已经存在,那么可以直接加载
# chromadaDB = Chroma(persist_directory='./chroma_db', embedding_function=embedding)
# 4.使用向量数据库进行查询
query = "1937年北京大学发生了什么?"
result = chromadaDB.similarity_search(query, k=2)
print(f'result-->{result}')
4 检索器
4.1 LangChain 的检索器定义
检索器是 LangChain 中负责信息检索的模块,通常与索引(Indexes)模块(如向量存储、嵌入模型)结合使用。它的核心功能是:
- 输入:接收用户查询(通常是文本)。
- 处理:根据查询从数据源中检索相关内容。
- 输出:返回一组相关文档或文本片段(通常是 Document 对象列表)。
检索器在以下场景中扮演关键角色:
- 问答系统:从文档或知识库中检索答案的上下文。
- 语义搜索:根据查询的语义返回相关结果。
- 上下文增强:为语言模型提供外部知识,解决其知识局限。
4.2 检索器的工作原理
检索器通常与向量存储(Vector Stores)配合,通过嵌入模型(Embedding Models)将查询和文档转为向量,基于相似性进行检索。工作流程可以分为以下步骤:
- 查询嵌入:将用户查询通过嵌入模型(如 OpenAIEmbeddings)转为向量表示
- 相似性搜索:在向量存储中查找与查询向量最相似的文档向量。
- 文档返回:返回匹配的文档(包含内容、元数据等)。
- 后处理(可选):对检索结果进行排序、过滤或重新排名。
检索器的核心依赖:
- 嵌入模型:将文本转为向量(如 OpenAIEmbeddings, HuggingFaceEmbeddings)。
- 向量存储:存储文档向量(如 Chroma、FAISS、Pinecone)。
- 相似性度量:如余弦相似度、欧几里得距离
4.3 检索器类型
langchain 支持很多检索器 https://python.langchain.com/docs/integrations/retrievers/,部分如下:
此处讲解 VectorStoreRetriever。
在 LangChain 中,as_retriever() 方法的 search_type 参数决定了向量检索的具体算法和行为。
python
retriever = vector_store.as_retriever(
search_type="similarity", # 可选 "similarity"|"mmr"|"similarity_score_threshold"
search_kwargs={
"k": 5, # 返回结果数量
"score_threshold": 0.7, # 仅当search_type="similarity_score_threshold"时有效,低于阈值的都丢弃。
"filter": {"source": "重要文档.pdf"}, # 元数据过滤,只会检索满足条件的文档。
"lambda_mult": 0.25 # 仅MMR搜索有效(控制多样性):接近 0 则更强调和查询的相关性;接近 1 则更强调结果之间的差异性
}
)
示例代码:
python
from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings
embedding = OllamaEmbeddings(model="mxbai-embed-large")
# 向量数据库已经存在,那么可以直接加载
chromadaDB = Chroma(persist_directory='./chroma_db', embedding_function=embedding)
# 使用向量数据库进行查询
query = "1937年北京大学发生了什么?"
# 使用 as_retriever 方法返回 Retriever 对象,然后调用 invoke 方法进行查询
retriever = chromadaDB.as_retriever(search_kwargs={"k":2})
result = retriever.invoke(query)
print(f'result-->{result}')
拓展:
vectordb.as_retriever() 和 vectordb.similarity_search() 都是用于从向量数据库中检索相关文档的方法,它们有什么异同:
-
相同
- 核心功能:两者都基于向量相似度(如余弦相似度)从向量数据库中检索与查询最相关的文档。
- 底层技术:通常使用相同的嵌入模型和相似度计算方式(如 FAISS、Chroma、Pinecone 等)。
-
不同
| 特性 | vectordb.as_retriever() |
vectordb.similarity_search() |
|---|---|---|
| 返回值 | Retriever 对象 | Document 列表 |
| 调用方式 | retriever.invoke(query) |
vectordb.similarity_search(query, k) |
| 适用场景 | LCEL 链式组合 | 直接获取结果 |
| 可配置性 | 支持 search_type、search_kwargs | 参数较少 |
4.4 其他几种常用的检索器
TFIDFRetriever:
功能:基于 TF-IDF(词频-逆文档频率)的检索器。
特点:
- 使用 TF-IDF 向量表示文档和查询。
- 适合快速构建原型。
- 不支持语义搜索。
适用场景:
- 文本搜索
- 关键词提取
python
from langchain_community.retrievers import TFIDFRetriever
from langchain_core.documents import Document
docs = [
Document(page_content="量子计算是一种基于量子力学的计算范式。"),
Document(page_content="人工智能是模拟人类智能的技术。")
]
retriever = TFIDFRetriever.from_documents(docs)
retriever.k = 1 # 返回 1 个结果
results = retriever.invoke("量子计算")
print(results)
BM25Retriever:
BM25 算法:BM25 是对 TF-IDF 的改进版本,在 TF-IDF 基础上做了归一化(防止长文档优势)与饱和控制(防止词频无限增长)。
功能:基于 BM25 算法的关键词检索器,适合基于词频的搜索。
特点:
- 不依赖嵌入模型,使用词频和逆文档频率(TF-IDF)计算相关性。
- 适合关键词匹配场景,计算成本低。
- 不支持语义搜索,效果依赖文本的字面匹配。
适用场景:
- 传统搜索场景。
- 关键词驱动的问答。
环境依赖:
bash
pip install rank_bm25
python
from langchain_community.retrievers import BM25Retriever
from langchain_core.documents import Document
docs = [
Document(page_content="量子计算是一种基于量子力学的计算范式。"),
Document(page_content="人工智能是模拟人类智能的技术。")
]
retriever = BM25Retriever.from_documents(docs)
retriever.k = 1 # 返回 1 个结果
results = retriever.invoke("量子计算")
print(results)
MultiQueryRetriever:
功能:它借助 LLM 自动生成多个语义等价的改写查询,把用户的问题扩展成多个角度,然后对每个改写进行检索,最后合并结果,以提高召回率。
特点:
- 使用语言模型生成查询的多种表达方式
- 从向量存储中检索所有变体的结果并合并
- 提高召回率,适合复杂查询
适用场景:
- 查询表达不明确或需要覆盖多种语义
- 提高检索的全面性
EnsembleRetriever:
功能:结合多种检索器(如 BM25 和向量存储),融合结果。
特点:
- 结合关键词搜索和语义搜索的优点
- 支持加权融合,调整不同检索器的权重
- 提高召回率和精准度
适用场景:
- 需要综合关键词和语义的搜索
- 复杂查询场景
ContextualCompressionRetriever:
功能:对检索结果进行压缩,提取最相关的内容
特点:
- 使用语言模型对检索到的文档进行重新排序或精炼
- 减少无关内容,提高结果质量
- 增加计算开销,但提升精准度
适用场景:
- 文档内容冗长,需要提取关键信息
- 提高问答系统的答案质量
Custom Retriever:
功能:开发者可以自定义检索逻辑,适配特定数据源或算法
特点:
- 继承 BaseRetriever 类,实现 get_relevant_documents 方法
- 支持任意数据源(如数据库、API)
适用场景:
- 特定领域的数据源(如内部数据库)
- 自定义检索算法