在实际应用中,我们经常需要:

  • 🔄 将多个处理步骤串联起来
  • 📊 让数据在不同组件之间流转
  • 🎯 构建复杂的业务逻辑

举个例子
假设你要开发一个"文章润色助手":

  1. 第一步:分析文章的主题
  2. 第二步:根据主题生成改进建议
  3. 第三步:根据建议重写文章

问题是:如果每一步都单独调用模型,代码会非常冗长且难以维护:

# ❌ 传统方式:代码重复,难以维护
topic_response = llm.invoke("分析这篇文章的主题:" + article)
topic = topic_response.content

suggestions_response = llm.invoke(f"为{topic}主题的文章提供改进建议")
suggestions = suggestions_response.content

final_response = llm.invoke(f"根据建议{suggestions}重写文章")
result = final_response.content

LangChain的Chains就是为了解决这个问题!

它让你可以:

# ✅ Chains方式:简洁、可复用
chain = analyze_chain | suggest_chain | rewrite_chain
result = chain.invoke({"article": article})

Chain是将多个组件按顺序连接起来的处理流程。数据从第一个组件流向最后一个组件,每个组件的输出作为下一个组件的输入。

类比理解

Chain就像工厂的生产线:

原材料 → [切割][打磨][上漆] → 成品
   ↓         ↓        ↓        ↓       ↓
输入     组件1    组件2    组件3    输出
┌──────────────────────────────────────┐
│      LangChain Chains 体系           │
├──────────────────────────────────────┤
│                                      │
│  1. LLMChain(基础链)                │
│     └─ 单个LLM + Prompt的组合        │
│                                      │
│  2. SequentialChain(顺序链)         │
│     ├─ SimpleSequentialChain         │
│     │  └─ 简单的串行执行             │
│     └─ SequentialChain               │
│        └─ 支持多输入输出的串行       │
│                                      │
│  3. RouterChain(路由链)             │
│     └─ 根据输入选择不同的处理分支    │
│                                      │
│  4. LCEL(LangChain表达式语言)       │
│     └─ 用 | 操作符连接组件(推荐)   │
│                                      │
└──────────────────────────────────────┘

LLMChain - 最基础的链

【概念名称】:LLMChain
【来源】:langchain.chains.LLMChain
【作用】:将PromptTemplate和LLM组合在一起的最基本链

【核心组成】:

LLMChain = PromptTemplate + LLM + (可选)OutputParser

工作流程:
输入变量 → PromptTemplate格式化 → LLM处理 → 输出

【核心参数】:

LLMChain(
    llm=ChatOpenAI(...),        # 必需:要使用的语言模型
    prompt=PromptTemplate(...),  # 必需:提示词模板
    output_parser=None,          # 可选:输出解析器
    verbose=False,               # 可选:是否打印中间过程
    memory=None                  # 可选:记忆模块(后续课程讲解)
)

参数详解

  1. llm(语言模型对象)

    • 类型:BaseLanguageModel(如ChatOpenAI)
    • 作用:执行实际的推理任务
    • 示例:ChatOpenAI(model_name="gpt-3.5-turbo")
  2. prompt(提示词模板)

    • 类型:PromptTemplate或ChatPromptTemplate
    • 作用:定义如何格式化输入
    • 必须包含:template和input_variables
  3. verbose(详细输出模式)

    • 类型:布尔值
    • 作用:是否打印执行过程
    • 值:
      • True:打印中间步骤(调试时有用)
      • False:仅返回最终结果
    • 示例输出:
     > Entering new LLMChain chain...
       Prompt after formatting:
       请翻译:Hello
     > Finished chain.
  1. output_parser(输出解析器)
    • 类型:BaseOutputParser
    • 作用:将模型输出转换为特定格式
    • 示例:CommaSeparatedListOutputParser()

SequentialChain - 顺序链

【概念名称】:SequentialChain
【作用】:按顺序执行多个链,每个链的输出可以传递给下一个链

【与SimpleSequentialChain的区别】:

SimpleSequentialChain:
  ├─ 限制:每个链只能有1个输入和1个输出
  ├─ 优点:简单直观
  └─ 适用场景:简单的串行任务

链1[输出] → 链2[输出] → 链3

SequentialChain:
  ├─ 支持:每个链可以有多个输入和多个输出
  ├─ 优点:灵活强大
  └─ 适用场景:复杂的业务逻辑

链1[输出A, 输出B] → 链2(使用A)[输出C] → 链3(使用B和C)

【核心参数】:

SequentialChain(
    chains=[chain1, chain2, chain3],  # 要执行的链列表
    input_variables=["input"],        # 初始输入变量名
    output_variables=["output"],      # 最终输出变量名
    verbose=False                     # 是否打印过程
)

RouterChain - 路由链

【概念名称】:RouterChain
【作用】:根据输入内容动态选择要执行的链

【工作原理】:

                    输入
                     ↓
              [路由决策器]
                   / | \
                  /  |  \
        链A(技术) 链B(法律) 链C(医疗)
                  \  |  /
                   \ | /
                    输出

【使用场景】:

  • 多领域问答系统(根据问题类型选择专家模型)
  • 智能客服分流(根据问题分配给不同部门)
  • 多语言处理(根据语言选择不同的处理链)

LCEL - LangChain表达式语言(推荐)

【概念名称】:LCEL (LangChain Expression Language)
【作用】:使用 | 操作符连接组件的现代化语法

【为什么推荐LCEL?】

  1. 代码更简洁

    # ❌ 传统LLMChain方式
    chain = LLMChain(llm=llm, prompt=prompt)
    result = chain.invoke({"input": "..."})
    
    # ✅ LCEL方式
    chain = prompt | llm | output_parser
    result = chain.invoke({"input": "..."})
    
  2. 更易组合

    # 轻松添加新组件
    chain = prompt | llm | output_parser | custom_processor
    
  3. 支持流式输出

    # 实时获取模型输出(类似ChatGPT打字效果)
    for chunk in chain.stream({"input": "..."}):
        print(chunk, end="", flush=True)
    
  4. 更好的类型检查

    • LCEL会自动检查组件间的输入输出是否匹配

 


Memory

Memory就是为了解决无记忆!

它让模型能够:

  • 💬 记住之前的对话
  • 🧠 理解上下文关系
  • 🔄 进行连贯的多轮对话

Memory的工作原理:

用户输入 → Memory读取历史 → 组合成完整Prompt → LLM处理 → Memory保存新对话 → 输出
          ↑                                                      ↓
          └──────────────────── 存储空间 ←──────────────────────┘

Memory模块的核心组成:

BaseMemory(抽象基类)
    ├─ memory_variables: 定义传递给Prompt的变量名
    ├─ load_memory_variables(): 读取记忆数据
    ├─ save_context(): 保存对话到记忆
    └─ clear(): 清空记忆

BaseMemory接口

【概念名称】:BaseMemory
【来源】:langchain.schema.BaseMemory
【作用】:所有Memory类的抽象基类,定义了Memory必须实现的接口

【核心方法】:

  1. memory_variables(属性)

    @property
    @abstractmethod
    def memory_variables(self) -> List[str]:
        """返回这个Memory类会添加到链输入的变量名列表"""
    
    • 作用:定义Memory会向Prompt注入哪些变量
    • 示例:返回["history"]表示会注入{history}变量
  2. load_memory_variables(方法)

    @abstractmethod
    def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
        """根据输入加载相关的记忆数据"""
    
    • 作用:读取存储的记忆,返回要注入Prompt的内容
    • 参数:inputs - 当前输入(可用于过滤相关记忆)
    • 返回:字典,键为memory_variables中定义的变量名
  3. save_context(方法)

    @abstractmethod
    def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
        """保存这次对话到记忆中"""
    
    • 作用:将输入和输出保存到记忆存储
    • 参数:
      • inputs:用户输入
      • outputs:模型输出
  4. clear(方法)

    @abstractmethod
    def clear(self) -> None:
        """清空所有记忆"""
    

实现自定义Memory的步骤

步骤1:继承BaseMemory
   ↓
步骤2:定义存储变量(如dict、list)
   ↓
步骤3:实现memory_variables属性
   ↓
步骤4:实现load_memory_variables()
   ↓
步骤5:实现save_context()
   ↓
步骤6:实现clear()

LangChain内置Memory类型

内置Memory家族
├─ ConversationBufferMemory(最基础)
│  └─ 完整保存所有对话历史
├─ ConversationBufferWindowMemory(有限历史)
│  └─ 只保存最近K轮对话
├─ ConversationEntityMemory(实体记忆)
│  └─ 基于实体提取的智能记忆
├─ ConversationSummaryMemory(摘要记忆)
│  └─ 用LLM压缩历史为摘要
└─ ChatMessageHistory(消息历史)
   └─ 底层消息存储类

ConversationBufferMemory

【概念】:最简单的Memory,保存所有对话历史

【原理】:

1轮:用户: Hello
      AI: Hi
      ↓
Memory: "Human: Hello\nAI: Hi"2轮:用户: How are you?
      AI: I'm fine
      ↓
Memory: "Human: Hello\nAI: Hi\nHuman: How are you?\nAI: I'm fine"

【代码示例】:

from langchain.memory import ConversationBufferMemory
from langchain_openai import ChatOpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate

# 【创建Memory】
memory = ConversationBufferMemory()

# 【手动测试Memory】
# 保存对话
memory.save_context(
    {"input": "你好"},  # 用户输入
    {"output": "你好,有什么可以帮你的?"}  # AI输出
)

# 加载历史
history = memory.load_memory_variables({})
print(history)
# 输出:{'history': 'Human: 你好\nAI: 你好,有什么可以帮你的?'}

# 【集成到Chain】
llm = ChatOpenAI(model_name="gpt-3.5-turbo")

prompt = PromptTemplate(
    input_variables=["history", "input"],
    template="""以下是对话历史:
{history}

人类: {input}
AI:"""
)

chain = LLMChain(
    llm=llm,
    prompt=prompt,
    memory=ConversationBufferMemory(memory_key="history"),  # 指定memory_key
    verbose=True
)

# 对话
chain.invoke({"input": "我叫张三"})
chain.invoke({"input": "我叫什么名字?"})  # AI会记得"张三"

【参数详解】:

  • memory_key:在Prompt中的变量名(默认"history")
  • return_messages:是否返回消息对象(默认False)
    # return_messages=False(默认)
    {'history': 'Human: 你好\nAI: 你好'}
    
    # return_messages=True
    {'history': [HumanMessage(content='你好'), AIMessage(content='你好')]}
    

【优点】:
✅ 简单易用
✅ 保留完整上下文

【缺点】:
❌ 历史过长会超出Token限制
❌ 成本随对话增加而增加

 


ConversationBufferWindowMemory

【概念】:只保存最近K轮对话的Memory

【原理】:

设置k=2(只保存最近2轮)

第1轮:用户: A, AI: B
Memory: A, B

第2轮:用户: C, AI: D
Memory: A, B, C, D

第3轮:用户: E, AI: F
Memory: C, D, E, F  ← 删除了A, B(最旧的)

【代码示例】:

from langchain.memory import ConversationBufferWindowMemory

# 【创建WindowMemory】
memory = ConversationBufferWindowMemory(
    k=2  # 只保留最近2轮对话
)

# 【测试】
memory.save_context({"input": "第1轮"}, {"output": "回复1"})
memory.save_context({"input": "第2轮"}, {"output": "回复2"})
memory.save_context({"input": "第3轮"}, {"output": "回复3"})

history = memory.load_memory_variables({})
print(history)
# 输出:只包含第2轮和第3轮(第1轮被丢弃)

# 【集成到Chain】
chain = LLMChain(
    llm=llm,
    prompt=prompt,
    memory=ConversationBufferWindowMemory(k=3, memory_key="history"),
    verbose=True
)

【适用场景】:

  • ✅ 长期对话(避免超Token)
  • ✅ 注重最近上下文的应用
  • ✅ 需要控制成本

【权衡】:

  • 设置太小(k=1-2):可能丢失重要信息
  • 设置太大(k=10+):与BufferMemory差别不大

ConversationEntityMemory

【概念】:基于实体提取的智能Memory(使用LLM识别实体)

【原理】:

输入:"马云在杭州创办了阿里巴巴"
  ↓ LLM提取实体
实体:
  - 马云: "在杭州创办了阿里巴巴"
  - 杭州: "马云在杭州创办了阿里巴巴"
  - 阿里巴巴: "马云在杭州创办"

下次提到"马云"时,自动加载相关信息

【代码示例】:

from langchain.memory import ConversationEntityMemory

# 【创建EntityMemory】
# 需要LLM来提取实体
memory = ConversationEntityMemory(llm=llm)

# 【测试】
memory.save_context(
    {"input": "孙悟空和猪八戒正在做西天取经项目"},
    {"output": "听起来很棒!"}
)

# 查询时,如果提到实体,会加载相关信息
result = memory.load_memory_variables({"input": "谁是孙悟空?"})
print(result)
# 输出包含:孙悟空相关的上下文信息

# 【集成到Chain】
from langchain.prompts import PromptTemplate

template = """相关实体信息:
{entities}

当前对话:
{history}

人类: {input}
AI:"""

prompt = PromptTemplate(
    input_variables=["entities", "history", "input"],
    template=template
)

chain = LLMChain(
    llm=llm,
    prompt=prompt,
    memory=ConversationEntityMemory(llm=llm),
    verbose=True
)

【优点】:
✅ 智能定位相关信息
✅ 适合长期对话
✅ 避免无关历史干扰

【缺点】:
❌ 需要额外LLM调用(成本更高)
❌ 实体提取可能不准确

ConversationSummaryMemory

【概念】:使用LLM将历史对话压缩为摘要

【原理】:

历史对话(500 tokens):
  "Human: 我叫张三
   AI: 你好张三
   Human: 我在北京工作
   AI: 北京是个好地方
   ..."
  ↓ LLM压缩
摘要(50 tokens):
  "人类介绍自己叫张三,在北京工作。"

【代码示例】:

from langchain.memory import ConversationSummaryMemory

# 【创建SummaryMemory】
# 需要LLM来生成摘要
memory = ConversationSummaryMemory(llm=llm)

# 【测试】
memory.save_context(
    {"input": "我是孙悟空,正在西天取经"},
    {"output": "祝你旅途顺利"}
)
memory.save_context(
    {"input": "我已经经历了九九八十一难"},
    {"output": "真不容易!"}
)

# 查看摘要
summary = memory.load_memory_variables({})
print(summary)
# 输出:压缩后的摘要,而不是完整对话

# 【自定义摘要Prompt】
from langchain.prompts import PromptTemplate

# 定义如何生成摘要
SUMMARY_PROMPT = PromptTemplate(
    input_variables=["summary", "new_lines"],
    template="""当前摘要:
{summary}

新对话:
{new_lines}

生成新摘要(简洁、准确):"""
)

memory = ConversationSummaryMemory(
    llm=llm,
    prompt=SUMMARY_PROMPT
)

# 【集成到Chain】
chain = LLMChain(
    llm=llm,
    prompt=prompt,
    memory=ConversationSummaryMemory(llm=llm),
    verbose=True
)

【优点】:
✅ 大幅节省Token
✅ 适合超长对话
✅ 保留关键信息

【缺点】:
❌ 每次都调用LLM(成本)
❌ 可能丢失细节
❌ 摘要质量依赖LLM能力

ChatMessageHistory

【概念】:底层消息存储类,其他Memory的基础

【代码示例】:

from langchain.memory import ChatMessageHistory

# 【创建MessageHistory】
history = ChatMessageHistory()

# 【添加消息】
history.add_user_message("你好")  # 用户消息
history.add_ai_message("你好,有什么可以帮你的?")  # AI消息

# 【查看消息】
print(history.messages)
# 输出:[HumanMessage(content='你好'), AIMessage(content='你好,有什么可以帮你的?')]

# 【单独使用】
# 传给LLM
llm = ChatOpenAI()
response = llm.invoke(history.messages)
print(response.content)

【使用场景】:

  • 需要完全自定义Memory逻辑
  • 与其他Memory类组合使用
Memory类型 存储内容 Token消耗 适用场景 额外成本
ConversationBuffer 完整历史 随对话增长 短对话
ConversationBufferWindow 最近K轮 固定上限 长对话
ConversationEntity 实体信息 中等 需要记住特定信息 LLM调用
ConversationSummary 摘要 最少 超长对话 LLM调用

选择建议

对话轮次 < 10轮:
  └─ ConversationBufferMemory(简单直接)

10< 对话轮次 < 50轮:
  └─ ConversationBufferWindowMemory(平衡性能)

对话轮次 > 50轮:
  └─ ConversationSummaryMemory(节省Token)

需要精确记住特定信息(如人名、地点):
  └─ ConversationEntityMemory

 


Model I/O

直接调用OpenAI API虽然可行,但存在以下挑战:

  1. 提示词难以复用和管理
  2. 输出格式不统一,难以解析
  3. 需要手动处理API调用的各种细节

LangChain的Model I/O模块就是为了解决这些问题而设计的!

🏗️ 它把常用的操作封装成标准化的组件,让你更高效地构建AI应用

LangChain的核心价值

原始方式:
你 → 写提示词 → 调用API → 手动解析 → 得到结果

LangChain方式:
你 → 使用PromptTemplate → 调用LLM → 使用OutputParser → 得到结构化结果
        ↓                    ↓                ↓
      可复用            统一接口         自动解析

核心组件:

┌─────────────────────────────────────┐
│         LangChain Model I/O        │
├─────────────────────────────────────┤
│  1. Prompts(提示词管理)           │
│     └─ PromptTemplate              │
│     └─ ChatPromptTemplate          │
│                                     │
│  2. Language Models(语言模型)     │
│     └─ ChatOpenAI                  │
│     └─ OpenAI                      │
│                                     │
│  3. Output Parsers(输出解析器)    │
│     └─ StrOutputParser             │
│     └─ PydanticOutputParser        │
│     └─ CommaSeparatedListParser    │
└─────────────────────────────────────┘
PromptTemplate核心概念

【概念名称】:PromptTemplate
【来源】:langchain.prompts 模块
【作用】:创建可复用的提示词模板,支持变量替换

【核心参数】:

  • template(str):包含占位符的模板字符串,使用{变量名}标记占位符
  • input_variables(List[str]):模板中使用的变量名列表
  • template_format(str):模板格式,默认为"f-string"(Python f-string格式)

【工作原理】:

模板定义:
"你是{role},请{task}"
      ↓
变量替换:
role="翻译专家", task="翻译以下文本"
      ↓
最终提示词:
"你是翻译专家,请翻译以下文本"
ChatPromptTemplate - 对话式提示词

【概念名称】:ChatPromptTemplate
【来源】:langchain.prompts 模块
【作用】:专门为聊天模型(如GPT-3.5/4)设计的提示词模板

【与PromptTemplate的区别】:

PromptTemplate:
  └─ 适用于:补全模型(Completion Models)
  └─ 格式:单一字符串
  └─ 示例:"请回答:{question}"

ChatPromptTemplate:
  └─ 适用于:聊天模型(Chat Models)
  └─ 格式:消息列表(system, human, ai)
  └─ 示例:[
       {"role": "system", "content": "你是AI助手"},
       {"role": "human", "content": "{question}"}
     ]

【消息角色说明】:

  • system:系统消息,定义AI的行为和角色
  • human(或user):用户消息,来自人类的输入
  • ai(或assistant):AI消息,模型的回复
ChatOpenAI核心类

【概念名称】:ChatOpenAI
【来源】:langchain_openai 包
【作用】:LangChain对OpenAI聊天模型的封装类

【核心参数详解】:

ChatOpenAI(
    model_name="gpt-3.5-turbo",  # 使用的模型名称
    temperature=0.7,              # 创造性参数
    max_tokens=None,              # 最大生成token数
    openai_api_key=None,          # API密钥
    openai_api_base=None,         # API基础URL
    streaming=False,              # 是否流式输出
    verbose=False                 # 是否打印详细信息
)

参数详细说明

  1. model_name(字符串)

    • 作用:指定使用的OpenAI模型
    • 常用值:
      • "gpt-3.5-turbo":性价比高,适合日常对话
      • "gpt-4":能力更强,推理能力好
      • "gpt-4-turbo":速度更快的GPT-4
    • 默认值:"gpt-3.5-turbo"
  2. temperature(浮点数,范围0-2)

    • 作用:控制输出的随机性和创造性
    • 取值说明:
      • 0.0:确定性输出,每次结果几乎相同
      • 0.5-0.7:平衡的创造性(推荐)
      • 1.0-2.0:高度创造性,输出更多样化
    • 使用建议:
      • 事实性任务(如翻译):使用低温度(0-0.3)
      • 创意任务(如写作):使用高温度(0.7-1.0)
    • 默认值:0.7
  3. max_tokens(整数或None)

    • 作用:限制模型生成的最大token数量
    • 说明:1个token ≈ 4个英文字符 或 1-2个中文字符
    • 示例:
      • max_tokens=100:约生成100-200个中文字
      • max_tokens=None:使用模型默认限制
    • 注意:设置过小可能导致回答不完整
  4. streaming(布尔值)

    • 作用:是否启用流式输出(像ChatGPT一样逐字显示)
    • 值:
      • True:启用流式输出
      • False:等待完整响应后一次性返回
    • 使用场景:实时聊天应用建议开启
模型调用方式

LangChain的ChatOpenAI提供两种调用方式:

# 方式1:invoke() - 同步调用(推荐用于简单场景)
response = llm.invoke("你好")

# 方式2:ainvoke() - 异步调用(推荐用于高并发场景)
response = await llm.ainvoke("你好")

Output Parsers - 输出解析器

为什么需要输出解析器?

问题场景
模型的原始输出是字符串,但程序往往需要结构化数据。

# 模型输出(字符串)
"推荐的水果有:苹果, 香蕉, 橙子"

# 程序需要的格式(列表)
['苹果', '香蕉', '橙子']

OutputParser就是做这个转换的!

常用输出解析器

【概念名称】:StrOutputParser
【作用】:将模型输出转换为字符串(最简单的解析器)
【使用场景】:只需要文本结果,不需要特殊处理

【概念名称】:CommaSeparatedListOutputParser
【作用】:将逗号分隔的字符串解析为列表
【输入示例】:"苹果, 香蕉, 橙子"
【输出结果】:['苹果', '香蕉', '橙子']

【概念名称】:PydanticOutputParser
【作用】:将输出解析为Pydantic模型(结构化对象)
【使用场景】:需要复杂的结构化数据(如JSON对象)

Retrieval

在实际的AI应用开发中,我们经常遇到这样的需求:

  • 📚 让大模型回答基于企业私有知识库的问题
  • 💼 构建能够理解和检索大量文档的智能系统
  • 🔍 实现精准的信息检索和问答系统

问题是:大模型虽然强大,但它存在几个关键限制:

  1. 知识截止日期:模型只知道训练时的数据,无法获取最新信息
  2. 无法访问私域数据:企业内部文档、数据库等私有信息模型无法直接访问
  3. 上下文窗口限制:即使是最强的模型也有Token数量限制,无法一次性处理海量文档
  4. 幻觉问题:模型可能生成看似合理但实际错误的信息

RAG(检索增强生成)就是为了解决这些问题而设计的!

完整RAG流程图

1. 文档加载
   TextLoader → Document对象
   ↓
2. 文本分割
   CharacterTextSplitter → 多个小块
   ↓
3. 向量化
   OpenAIEmbeddings → 每块转为向量
   ↓
4. 存储
   Chroma → 向量数据库
   ↓
5. 检索
   用户问题 → 向量化 → 相似度搜索 → Top-K文档
   ↓
6. 生成
   检索结果 + 问题 → LLM → 答案

Retrieval 体系:

┌─────────────────────────────────────────┐
│      LangChain Retrieval 体系           │
├─────────────────────────────────────────┤
│  1. Document Loaders(文档加载器)       │
│     └─ TextLoader, PDFLoader, JSONLoader│
│                                          │
│  2. Text Splitters(文本分割器)         │
│     └─ RecursiveCharacterTextSplitter   │
│     └─ TokenTextSplitter                │
│                                          │
│  3. Text Embeddings(文本向量化)        │
│     └─ OpenAIEmbeddings                 │
│                                          │
│  4. Vector Stores(向量存储)            │
│     └─ Chroma, FAISS                    │
│                                          │
│  5. Retrievers(检索器)                 │
│     └─ VectorStoreRetriever             │
└─────────────────────────────────────────┘

LangChain Retrieval模块的五大组件

  • Document Loaders:统一加载各种格式文档
  • Text Splitters:智能分割文本,兼顾语义和长度
  • Text Embeddings:将文本转为向量表示
  • Vector Stores:高效存储和检索向量
  • Retrievers:统一的检索接口

Document Loaders - 文档加载器

为什么需要Document Loaders?

问题场景
企业的知识库通常包含多种格式的文档:

  • 📄 PDF技术文档
  • 📝 TXT文本文件
  • 📊 JSON数据文件
  • 🌐 网页内容
  • 💾 数据库记录

如果没有统一的加载机制

# ❌ 每种格式需要不同的处理方式
pdf_content = extract_pdf(file)        # PDF专用方法
txt_content = read_text(file)          # TXT专用方法
json_content = parse_json(file)        # JSON专用方法
# 代码重复、难以维护

Document Loaders的解决方案

# ✅ 统一的加载接口
from langchain.document_loaders import PDFLoader, TextLoader, JSONLoader

# 所有Loader都使用相同的.load()方法
pdf_docs = PDFLoader("file.pdf").load()
txt_docs = TextLoader("file.txt").load()
json_docs = JSONLoader("file.json").load()

# 返回统一的Document对象
Document对象详解

【概念名称】:Document
【来源】:langchain_core.documents.base
【作用】:LangChain中统一的文档数据结构

【核心属性】:

class Document:
    page_content: str      # 文档的实际内容
    metadata: dict         # 文档的元数据信息

属性详解

  1. page_content(字符串)

    • 作用:存储文档的实际文本内容
    • 示例:
    doc.page_content = "LangChain是一个用于开发大模型应用的框架..."
    
  2. metadata(字典)

    • 作用:存储文档的附加信息
    • 常见字段:
      • source:文档来源路径
      • page:页码(PDF文档)
      • author:作者
      • created_at:创建时间
    • 示例:
    doc.metadata = {
        "source": "./data/langchain.txt",
        "author": "Claude",
        "page": 1
    }
    
BaseLoader基类

【概念名称】:BaseLoader
【作用】:所有文档加载器的抽象基类,定义统一接口

【核心方法】:

class BaseLoader(ABC):
    @abstractmethod
    def load(self) -> List[Document]:
        """加载文档并返回Document对象列表"""
        pass
    
    def load_and_split(
        self, 
        text_splitter: Optional[TextSplitter] = None
    ) -> List[Document]:
        """加载文档并自动分割成块"""
        pass

方法说明

  1. load()

    • 作用:从数据源加载文档
    • 返回:List[Document]
    • 必须实现:是(抽象方法)
  2. load_and_split()

    • 作用:加载文档并自动使用TextSplitter分割
    • 参数:
      • text_splitter:可选,默认使用RecursiveCharacterTextSplitter
    • 返回:分割后的List[Document]

Text Splitters - 文本分割器

为什么需要Text Splitters?

问题1:相关性问题

场景:一个100页的PDF技术文档

问题:用户问"什么是LangChain?"
     答案在第5页

如果不分割:
    ❌ 整个100页都送入模型
    ❌ 大量无关信息干扰
    ❌ 模型可能找不到重点

如果分割:
    ✅ 只检索第5页的相关段落
    ✅ 信息精准、干扰少
    ✅ 模型回答更准确

问题2:Token限制

GPT-4模型限制:最多8192个Token

一个100页PDF ≈ 50,000 Token
    ↓
❌ 无法一次性输入

分割后每块 ≈ 500 Token
    ↓
✅ 可以检索并输入相关块
Chunking策略对比
策略 原理 优点 缺点 适用场景
按句子分割 使用标点符号 语义完整 长度不均 结构化文本
固定字符数 每N个字符切一次 长度统一 可能破坏语义 日志文件
固定字符+重叠 字符数+窗口重叠 保留上下文 有冗余 一般文本
递归分割 按层级标记切分 兼顾语义和长度 稍复杂 推荐方案
语义分割 基于语义理解 语义最完整 效率低 高精度需求
RecursiveCharacterTextSplitter详解

【概念名称】:RecursiveCharacterTextSplitter
【来源】:langchain_text_splitters
【作用】:LangChain默认的文本分割器,递归按分隔符分割

【核心参数】:

RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", " ", ""],  # 分隔符列表(优先级从高到低)
    chunk_size=1000,                      # 每块的最大字符数
    chunk_overlap=200,                    # 块之间的重叠字符数
    length_function=len,                  # 计算长度的函数
    add_start_index=True                  # 是否添加起始索引到metadata
)

参数详解

  1. separators(分隔符列表)

    • 类型:List[str]
    • 作用:定义分割文本的标记,按优先级使用
    • 默认值:["\n\n", "\n", " ", ""]
    • 工作原理:
      先尝试用"\n\n"(段落)分割
         ↓ 如果块还是太大
      再用"\n"(行)分割
         ↓ 如果还是太大
      再用" "(空格)分割
         ↓ 最后
      按字符""分割
      
  2. chunk_size(块大小)

    • 类型:int
    • 作用:每个文本块的最大长度
    • 建议值:
      • 短文档:500-1000
      • 长文档:1000-2000
      • 技术文档:1500-2500
    • 注意:这是触发分割的阈值,实际块可能略大
  3. chunk_overlap(重叠长度)

    • 类型:int
    • 作用:相邻块之间重叠的字符数
    • 为什么需要重叠:
      1...重要信息在这里→|2|←这里继续...
               ↑
           重叠区域(保留上下文)
      
    • 建议值:chunk_size的10-20%
  4. length_function(长度函数)

    • 类型:Callable[[str], int]
    • 作用:如何计算文本长度
    • 选项:
      • len:按字符数(默认)
      • 自定义Token计数函数
  5. add_start_index(添加起始索引)

    • 类型:bool
    • 作用:是否在metadata中记录块在原文的起始位置
    • 用途:可追溯信息来源的精确位置
按Token分割

【概念名称】:Token
【定义】:模型处理文本的最小单位,约等于一个词或子词

Token与字符的关系

英文:1 Token ≈ 4 个字符
     "Hello World"2 Tokens

中文:1 个汉字 ≈ 1-2 Tokens
     "你好世界"4 Tokens

为什么按Token分割

GPT-4的限制:8192 Tokens(不是字符!)

如果按字符分割:
    chunk_size=1000字符 ≈ 250 Tokens(英文)
    ❌ 可能无法充分利用模型容量

如果按Token分割:
    chunk_size=1000 Tokens
    ✅ 精确控制输入量
    ✅ 避免超出限制
    ✅ 成本计算准确

Text Embeddings - 文本向量化

什么是Embedding?

【概念名称】:Embedding(嵌入向量)
【作用】:将文本转换为数字向量,使计算机能够理解文本语义

类比理解

文本 = 人类语言
向量 = 计算机语言

"苹果"[0.2, -0.5, 0.8, ...]  # 1536维向量
"香蕉"[0.3, -0.4, 0.7, ...]  # 相似的向量
"汽车"[-0.8, 0.9, -0.1, ...] # 不相似的向量

为什么需要Embedding

问题:如何判断两段文本是否相关?

字符匹配:
    "LangChain是一个框架"
    "框架是什么?"
    ❌ 几乎没有相同字符,但语义相关

向量相似度:
    向量1 = [0.1, 0.9, ...]
    向量2 = [0.2, 0.8, ...]
    ✅ 余弦相似度 = 0.95(高度相关)
OpenAIEmbeddings

【概念名称】:OpenAIEmbeddings
【来源】:langchain_openai
【作用】:使用OpenAI的embedding模型将文本转为向量

【核心参数】:

OpenAIEmbeddings(
    model="text-embedding-ada-002",    # 模型名称
    openai_api_key="your-key",         # API密钥
    openai_api_base="https://..."      # API基础URL
)

主要方法

  1. embed_documents(texts: List[str])

    • 作用:批量向量化多个文档
    • 参数:文本列表
    • 返回:向量列表
    • 用途:处理知识库文档
  2. embed_query(text: str)

    • 作用:向量化单个查询
    • 参数:查询文本
    • 返回:单个向量
    • 用途:处理用户问题

Vector Stores - 向量存储

为什么需要Vector Store?

问题场景

知识库有10,000个文档块
用户问:"LangChain是什么?"

如果没有向量存储:
    ❌ 需要遍历10,000个文档
    ❌ 逐个计算相似度
    ❌ 速度极慢

有向量存储:
    ✅ 预先计算并索引所有向量
    ✅ 快速检索最相似的Top-K
    ✅ 毫秒级响应
Chroma向量数据库

【概念名称】:Chroma
【来源】:langchain_chroma
【作用】:轻量级的向量数据库,适合开发和小规模应用

【核心方法】:

# 创建向量存储
db = Chroma.from_documents(
    documents=docs,          # Document对象列表
    embedding=embeddings     # Embedding模型
)

# 相似度搜索
results = db.similarity_search(
    query="查询文本",        # 查询内容
    k=4                      # 返回Top-K个结果
)

Retrievers - 检索器

【概念名称】:Retriever
【作用】:统一的检索接口,从向量存储中检索相关文档

核心方法

retriever = db.as_retriever()
docs = retriever.invoke("查询文本")

实战案例1:加载不同格式的文档

# ========================================
# 示例:使用Document Loaders加载多种格式文档
# ========================================

# 【导入必要模块】
from langchain.document_loaders import TextLoader
from langchain_community.document_loaders import PyPDFLoader, JSONLoader

# ==================== 加载TXT文档 ====================
# 【创建TextLoader】
# 参数:
#   - file_path:文件路径
#   - encoding:文件编码(中文文件必须指定utf-8)
txt_loader = TextLoader(
    './data/langchain.txt', 
    encoding="utf-8"
)

# 【调用load方法】
# 作用:读取文件并转换为Document对象
txt_docs = txt_loader.load()

# 【查看结果】
print("=== TXT文档加载结果 ===")
print(f"文档数量:{len(txt_docs)}")           # 输出:1
print(f"文档类型:{type(txt_docs[0])}")        # 输出:Document
print(f"文档内容(前30字符):{txt_docs[0].page_content[:30]}")
print(f"文档元数据:{txt_docs[0].metadata}")
# 输出:{'source': './data/langchain.txt'}

# ==================== 加载PDF文档 ====================
# 【创建PyPDFLoader】
# 注意:PyPDFLoader会自动将PDF的每一页作为一个Document
pdf_loader = PyPDFLoader("./data/本地知识库.pdf")

# 【load方法】:加载所有页
pdf_docs = pdf_loader.load()

# 【load_and_split方法】:加载并分割
# 作用:加载PDF的同时进行文本分割
pdf_docs_split = pdf_loader.load_and_split()

# 【查看结果】
print("\n=== PDF文档加载结果 ===")
print(f"总页数:{len(pdf_docs)}")
print(f"第一页内容(前50字符):{pdf_docs[0].page_content[:50]}")
print(f"第一页元数据:{pdf_docs[0].metadata}")
# 输出:{'source': './data/本地知识库.pdf', 'page': 0}

# ==================== 加载JSON文档 ====================
# 【安装jq库】
# 在命令行执行:pip install jq

# 【创建JSONLoader】
# 参数:
#   - file_path:JSON文件路径
#   - jq_schema:jq语法表达式,用于提取JSON中的特定字段
#   - text_content:是否将结果作为文本内容

# 示例:提取JSON中的commentary字段
json_loader = JSONLoader(
    file_path='./data/test.json',
    jq_schema='.[].commentary',  # jq语法:提取数组中每个元素的commentary字段
    text_content=False
)

# 【加载数据】
json_docs = json_loader.load()

# 【查看结果】
print("\n=== JSON文档加载结果 ===")
print(f"文档数量:{len(json_docs)}")
print(f"第一个文档内容(前100字符):{json_docs[0].page_content[:100]}")

关键知识点

  1. 统一的接口设计

    # 所有Loader都有相同的方法
    docs = loader.load()              # 加载文档
    docs = loader.load_and_split()    # 加载并分割
    
  2. Document对象的访问

    doc = docs[0]
    content = doc.page_content    # 访问内容
    meta = doc.metadata           # 访问元数据
    
  3. jq语法基础

    # JSON结构:[{"text": "..."}, {"text": "..."}]
    jq_schema = ".[].text"  # 提取所有text字段
    
    # JSON结构:{"key": [{"text": "..."}, ...]}
    jq_schema = ".key[].text"  # 提取key下所有text字段
    

MCP

实际应用中常遇到这样的需求:

  • 🔄 需要同时使用多个不同的服务(天气、数据库、Python执行等)
  • 🌐 每个服务可能运行在不同的进程中
  • 🔗 需要统一管理和调用这些服务

问题场景:

用户:"请查询北京天气,并将结果存入数据库"

需要调用:
❌ 单一服务无法完成
✅ 需要天气服务 + 数据库服务 + Python服务配合

MCP多服务器架构就是为了解决这个问题而设计的!

什么是MCP多服务器架构?

核心定义:

MCP(Model Context Protocol)多服务器架构是一种能够同时管理多个独立MCP服务器的客户端架构。每个服务器提供特定领域的工具,客户端负责统一调度和协调。

工作流程:

                  【MultiServerMCPClient】
                         ↓
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
   [SQL Server]    [Python Server]  [Weather Server]
        ↓                ↓                ↓
   sql_inter()     python_inter()   query_weather()
        ↓                ↓                ↓
   MySQL数据库      Python运行时     天气API

类比理解:

MCP多服务器架构 就像一个项目经理:

单服务器 = 一个专家独立工作
    ↓
    问题:能力有限,无法处理跨领域任务

多服务器 = 项目经理协调多个专家
    ↓
    1. 识别任务需要哪些专家(工具选择)
    2. 分配任务给不同专家(工具调用)
    3. 汇总结果(结果整合)

核心组件

MCP多服务器架构组成
├─ MultiServerMCPClient (客户端)
│  ├─ 管理多个MCP服务器连接
│  ├─ 统一工具列表
│  ├─ Function Calling集成
│  └─ 异步通信管理
│
├─ MCP服务器 (Server)
│  ├─ FastMCP框架
│  ├─ @mcp.tool()装饰器
│  └─ stdio通信
│
└─ OpenAI Function Calling
   └─ 智能工具选择和调用

FastMCP框架

【概念名称】:FastMCP
【来源】:mcp.server.fastmcp
【作用】:快速构建MCP服务器的框架

【核心特性】:

from mcp.server.fastmcp import FastMCP

# 【创建MCP服务器】
mcp = FastMCP("ServerName")  # ServerName:服务器标识

# 【定义工具】使用装饰器
@mcp.tool()
async def tool_name(param1, param2):
    """
    工具描述(会被LLM看到,用于理解工具功能)

    :param param1: 参数1说明
    :param param2: 参数2说明
    :return: 返回值说明
    """
    # 工具实现逻辑
    return result

# 【启动服务器】
if __name__ == "__main__":
    mcp.run(transport='stdio')  # 使用标准输入输出通信

StdioServerParameters

【概念名称】:StdioServerParameters
【作用】:配置MCP服务器子进程的启动参数

【核心参数】:

from mcp import StdioServerParameters

server_params = StdioServerParameters(
    command="python",           # 执行命令(python或node)
    args=["server_script.py"],  # 脚本路径
    env=None                    # 环境变量(None表示继承父进程)
)

参数详解:

  1. command(字符串)

    • 作用:指定服务器脚本的解释器
    • 选项:
      • "python":Python脚本
      • "node":JavaScript脚本
    • 示例:"python"
  2. args(列表)

    • 作用:传递给命令的参数
    • 通常是脚本文件路径
    • 示例:["SQL_server.py"]
  3. env(字典或None)

    • 作用:设置环境变量
    • None:继承父进程环境
    • 示例:{"API_KEY": "xxx"}

ClientSession

【概念名称】:ClientSession
【作用】:MCP客户端与服务器的通信会话

【核心方法】:

from mcp import ClientSession

# 1. 初始化会话
await session.initialize()

# 2. 列出服务器提供的工具
response = await session.list_tools()
tools = response.tools  # 工具列表

# 3. 调用工具
result = await session.call_tool(
    name="tool_name",      # 工具名称
    arguments={"param": "value"}  # 参数字典
)

# 4. 获取结果
content = result.content

Function Calling格式转换

【问题】:Claude和OpenAI的Function Calling格式不同

Claude格式:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询天气",
        "input_schema": {           # ⚠️ Claude使用input_schema
            "type": "object",
            "properties": {...},
            "required": [...]
        }
    }
}

OpenAI格式:

{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询天气",
        "parameters": {             # ⚠️ OpenAI使用parameters
            "type": "object",
            "properties": {...},
            "required": [...]
        }
    }
}

转换逻辑:

# input_schema → parameters
# 其他字段保持不变
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐