python LangChain 框架学习笔记,参考:官方英文文档官方中文文档

# 简介

LangChain 是一个开源的 PythonAl 应用开发框架,它提供了构建基于大模型的 Al 应用所需的模块和工具。通过 LangChain, 开发者可以轻松地与大型语言模型 (LLM) 集成,完成文本生成、问答、翻译、对话等任务。 LangChain 降低了 AI 应用开发的门槛,让任何人都可以基于 LLM 构建属于自己的创意应用。

# LangChain 特性:

  • LLM 和提示 (Prompt):LangChain 对所有 LLM 大模型进行了 API 抽象,统一了大模型访问 API,同时提供了 Prompt 提示模板管理机制。
  • 链 (Chain):Langchain 对一些常见的场景封装了一些现成的模块,例如:基于上下文信息的问答系统,自然语言生成 SQL 查询等等,因为实现这些任务的过程就像工作流一样,一步一步的执行,所以叫链 (chain)。
  • LCEL:LangChain Expression Language (LCEL),langchain 新版本的核心特性,用于解决工作流编排问题,通过 LCEL 表达式,我们可以灵活的自定义 AI 任务处理流程,也就是灵活自定义链 (Chain)。
  • 数据增强生成 (RAG):因为大模型 (LLM) 不了解新的信息,无法回答新的问题,所以我们可以将新的信息导入到 LLM,用于增强 LLM 生成内容的质量,这种模式叫做 RAG 模式 (RetrievalAugmentedGeneration)。
  • Agents:是一种基于大模型 (LLM) 的应用设计模式,利用 LLM 的自然语言理解和推理能力 (LLM 作为大脑),根据用户的需求自动调用外部系统、设备共同去完成任务,例如:用户输入 “明天请假
    一天”,大模型(LLM)自动调用请假系统,发起一个请假申请。
  • 模型记忆 (memory):让大模型 (Im) 记住之前的对话内容,这种能力成为模型记忆 (memory) 。

# LangChain 框架

LangChain 框架由几个部分组成,包括:

  • LangChain 库:Python 和 JavaScript 库。包含接口和集成多种组件的运行时基础,以及现成的链和代理的实现。
  • LangChain 模板:Langchain 官方提供的一些 Al 任务模板。
  • LangServe: 基于 FastAPI 可以将 Langchain 定义的链 (Chain), 发布成为 REST API。
  • LangSmith:开发平台,是个云服务,支持 Langchain debug、任务监控。

# LangChain 库 (Libraries)

LangChain 库本身由几个不同的包组成。

  • langchain-core:基础抽象和 LangChain 表达语言。
  • 1angchain-community:第三方集成,主要包括 langchain 集成的第三方组件。
  • 1angchain:主要包括链 (chain)、代理 (agent) 和检索策略。

langChain 提供一套提示词模板 (prompttemplate) 管理工具,负责处理提示词,然后传递给大模型处理,最后处理大模型返回的结果。

LangChain 对大模型的封装主要包括 LLM 和 ChatModel 两种类型。

  • LLM - 问答模型,模型接收一个文本输入,然后返回一个文本结果。
  • ChatModel - 对话模型,接收一组对话消息,然后返回对话消息,类似聊天消息一样。

# 核心概念

# LLMs

LangChain 封装的基础模型,模型接收一个文本输入,然后返回一个文本结果。

# Chat Models

聊天模型(或者成为对话模型),与 LLMs 不同,这些模型专为对话场景而设计。模型可以接收一组对话消息,然后返回对话消息,类似聊天消息一样。

# 消息 (Message)

指的是聊天模型 (ChatModels) 的消息内容,消息类型包括包括 HumanMessage、AIMessage、SystemMessage、FunctionMessage 和 ToolMessage 等多种类型的消息。

# 提示 (prompts)

LangChain 封装了一组专门用于提示词 (prompts) 管理的工具类,方便我们格式化提示词 (prompts) 内容。

# 输出解析器 (Output Parsers)

Langchain 接受大模型 (Im) 返回的文本内容之后,可以使用专门的输出解析器对文本内容进行格式化,例如解析 json、或者将 LLM 输出的内容转成 python 对象。

# Retrievers

为方便我们将私有数据导入到大模型 (LLM),提高模型回答问题的质量,LangChain 封装了检索框架 (Retrievers),方便我们加载文档数据、切割文档数据、存储和检索文档数据。

# 向量存储 (Vector stores)

为支持私有数据的语义相似搜索,langchain 支持多种向量数据库。

# Agents

智能体 (Agents),通常指的是以大模型 (LLM) 作为决策引擎,根据用户输入的任务,自动调用外部系统、硬件设备共同完成用户的任务,是一种以大模型 (LLM) 为核心的应用设计模式。

# 快速入门

以下示例代码使用 LangChain 实现了一个简单的 LLM api 调用,通过 _config.yml 存储 api_key 内容,并且封装了一个类 ConfigLoader 进行 yml 读取。
代码中还提供了另一种读取方式:在目录下建立一个 .env 文件,通过 dotenv 读取文件中的环境变量

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
# 从环境变量中读取 api_key
# import os
# from dotenv import load_dotenv
# api_key = os.getenv ('DeepSeek_API_KEY')
# 从 _config.yml 中读取 api_key
from Config_Loader import ConfigLoader
config = ConfigLoader.get_instance ()
api_key = config.get ('LLMapis.api_key')
base_url = config.get ('LLMapis.base_url')
chat_model = config.get ('LLMapis.chat_model')
# 初始化模型,openai 接口格式
chat = ChatOpenAI (
    api_key=api_key, 
    base_url=base_url,
    model=chat_model,
    temperature=0.7
)
# 提示模板
prompt = ChatPromptTemplate.from_messages ([
    ("system", "你是一个 AI 助手,用中文回答问题,回答字数在 30 字以内"),
    ("user", "{user_input}"),
])
# 创建对话链
chain = prompt | chat
# 执行对话
response = chain.invoke ({"user_input": "什么是 802.1x 认证技术"})
# 输出结果
print ("AI 回答:", response.content)

# 读取 _config.yml

在示例代码中,我们封装了一个类 ConfigLoader 读取 "_config.yml",_config.yml 配置如下:

LLMapis:
  api_key: "sk-****"
  base_url: "****"
  chat_model: "****"

ConfigLoader 类实例化后,可直接使用字典形式读取 config ['LLMapis.api_key']

import os
import yaml
import logging
from pathlib import Path
from typing import Any, Dict, Optional, Union
# 设置日志记录器
logger = logging.getLogger (__name__)
class ConfigError (Exception):
    """配置相关错误的基类"""
    pass
class ConfigNotFoundError (ConfigError):
    """配置文件不存在错误"""
    pass
class ConfigAccessError (ConfigError):
    """配置访问错误"""
    pass
class ConfigLoader:
    """
    读取 YAML 配置文件
        
    使用示例:
    >>> config = ConfigLoader (enable_logging=True)
    >>> api_key = config ['LLMapis.api_key']
    """
            
    _instance = None  # 单例实例
    _config: Dict [str, Any] = {}  # 配置缓存
    _loaded = False  # 加载状态标记
    _enable_logging = True  # 是否启用日志
    def __new__(cls, config_path: str = None, enable_logging: bool = True):
        if cls._instance is None:
            cls._instance = super (ConfigLoader, cls).__new__(cls)
            cls._instance._initialize (config_path, enable_logging)
        else:
            # 更新已有实例的日志设置
            cls._instance._enable_logging = enable_logging
            if config_path is not None:
                # 如果提供了新的配置路径,则更新现有实例
                cls._instance._config_path = Path (config_path).resolve ()
                cls._instance._load_config ()
        return cls._instance
    def _initialize (self, config_path: str, enable_logging: bool):
        """初始化实例,禁止外部直接调用"""
        self._enable_logging = enable_logging
        
        # 自动检测路径
        if not config_path:
            # 获取当前文件所在目录
            base_dir = Path (__file__).parent
            self._config_path = base_dir / "_config.yml"
        else:
            self._config_path = Path (config_path).resolve ()
        self._load_config ()
    def _log (self, level: str, message: str):
        """根据日志开关决定是输出日志还是打印到控制台"""
        if not self._enable_logging:
            if level in ['ERROR', 'CRITICAL']:
                print (f"[ERROR] {message}")
            return
            
        log_methods = {
            'DEBUG': logger.debug,
            'INFO': logger.info,
            'WARNING': logger.warning,
            'ERROR': logger.error,
            'CRITICAL': logger.critical
        }
        log_method = log_methods.get (level, logger.info)
        log_method (message)
    def _load_config (self):
        """执行实际配置加载操作"""
        try:
            # 前置验证
            self._validate_config_file ()
            
            # 读取文件
            with open (self._config_path, 'r', encoding='utf-8') as f:
                self._config = yaml.safe_load (f) or {}
                self._loaded = True
                self._log ('INFO', f"配置已从 {self._config_path} 加载")
        except Exception as e:
            self._handle_error (e)
            self._config = {}
            self._loaded = False
    def _validate_config_file (self):
        """验证配置文件有效性"""
        if not self._config_path.exists ():
            raise ConfigNotFoundError (f"配置文件 {self._config_path} 不存在")
        if not self._config_path.is_file ():
            raise ConfigError (f"{self._config_path} 不是文件")
        if not os.access (self._config_path, os.R_OK):
            raise ConfigError (f"缺少读取 {self._config_path} 的权限")
    def _handle_error (self, error: Exception):
        """统一错误处理"""
        error_type = type (error).__name__
        error_map = {
            'FileNotFoundError': f"配置文件不存在:{self._config_path}",
            'PermissionError': f"权限不足,请检查文件权限:{self._config_path}",
            'YAMLError': f"YAML 格式错误:{str (error)}",
            'ValueError': f"路径错误:{str (error)}",
            'ConfigNotFoundError': str (error),
            'ConfigError': str (error),
            'ConfigAccessError': str (error)
        }
        error_msg = error_map.get (error_type, f"配置错误:{str (error)}")
        self._log ('ERROR', error_msg)
    def get (self, key_path: str, default: Any = None) -> Optional [Any]:
        """安全获取配置项(支持点分嵌套路径)"""
        if not self._loaded:
            self._log ('WARNING', "配置未成功加载,返回默认值")
            return default
        keys = key_path.split ('.')
        value = self._config
        
        try:
            for key in keys:
                if isinstance (value, dict) and key in value:
                    value = value [key]
                else:
                    self._log ('DEBUG', f"配置路径 '{key_path}' 不完整,在 '{key}' 处终止")
                    return default
            return value
        except (KeyError, TypeError) as e:
            self._log ('DEBUG', f"访问配置 '{key_path}' 错误: {e}")
            return default
    def __getitem__(self, key_path: str) -> Any:
        """字典式访问语法"""
        if not self._loaded:
            error_msg = "配置未成功加载"
            self._log ('ERROR', error_msg)
            raise ConfigError (error_msg)
            
        value = self.get (key_path, object ())
        if value is object ():
            error_msg = f"配置项 '{key_path}' 不存在或无效"
            self._log ('ERROR', error_msg)
            raise ConfigAccessError (error_msg)
        return value
    def get_all (self) -> Dict [str, Any]:
        """获取完整配置的副本"""
        return self._config.copy () if self._config else {}
    @classmethod
    def get_instance (cls, enable_logging: bool = True) -> 'ConfigLoader':
        """获取单例实例"""
        if not cls._instance:
            cls._instance = cls (enable_logging=enable_logging)  # 使用默认路径
        else:
            # 更新日志设置
            cls._instance._enable_logging = enable_logging
        return cls._instance
    def reload (self) -> bool:
        """重新加载配置文件"""
        try:
            self._load_config ()
            return True
        except Exception:
            # 错误已在_load_config 中处理
            return False
    @property
    def is_loaded (self) -> bool:
        """检查配置是否成功加载"""
        return self._loaded
        
    @property
    def enable_logging (self) -> bool:
        """获取日志状态"""
        return self._enable_logging
        
    @enable_logging.setter
    def enable_logging (self, value: bool):
        """设置日志状态"""
        self._enable_logging = value
    def __contains__(self, key_path: str) -> bool:
        """检查配置项是否存在"""
        return self.get (key_path, self) is not self  # 使用哨兵值检测
# 使用示例
if __name__ == "__main__":
    # 配置日志格式
    logging.basicConfig (
        level=logging.INFO,
        format='%(asctime) s - %(name) s - %(levelname) s - %(message) s'
    )
    try:
        # 初始化配置(自动加载)
        config = ConfigLoader (enable_logging=True)  # 默认路径,启用日志
        
        # 或者使用单例模式
        config = ConfigLoader.get_instance (enable_logging=True)
        
        # 检查加载状态
        if not config.is_loaded:
            logger.error ("配置加载失败,请检查错误信息")
            exit (1)
            
        # 获取配置项
        api_key = config.get ('LLMapis.api_key', "默认 API 密钥")  # 读取配置项,不存在则使用默认值
        
        try:
            base_url = config ['LLMapis.base_url']  # 直接访问语法,如果不存在会抛出异常
        except ConfigAccessError as e:
            logger.warning (f"无法访问配置项: {e}")
            base_url = "https://default-api-url.com"
            
        # 检查配置项存在性
        if 'logging.level' in config:
            logger.info ("日志配置已存在")
            
        # 重新加载配置
        if config.reload ():
            logger.info ("配置重载成功")
            
        # 动态切换日志状态
        config.enable_logging = False
        logger.info ("这条日志不会经过 ConfigLoader 处理")
        
    except ConfigError as e:
        logger.critical (f"配置错误: {e}")
        exit (1)

# 安装 LangChain

要安装 LangChain 主软甲包,可以使用 pip 或者 conda 进行安装:

# pip
pip install langchain
# conda
conda install langchain -c conda-forge

安装示例选择器所需的 langchain-chroma

pip install -U langchain-chroma

注:安装完成后,运行 pip check 检查依赖情况,如出现 numpy 版本兼容性问题,请下载指定版本 numpy
pip uninstall numpy -y pip install numpy==1.26.4

某些集成(如 OpenAI 和 Anthropic)有自己的软件包。在使用时要安装对应的集成包,集成包详见 官网集成文档

此处以安装 openai 和 ollama 集成包为例

pip install -U langchain-openai
pip install -U langchain-ollama

其余诸如 LangChain experimentalLangGraphLangServeLangChain CLILangSmith SDK 的安装,详见 官方文档

# 初始化模型

要使用 LangChain 调用 LLM 的 api,需要导入 langchain-openai 集成包,设置 API 作为环境变量,或者直接传递到 OpenAI LLM 类中

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

将 API 密钥设置为环境变量可以参考

# linux, 临时变量
export OPENAI_API_KEY="YOUR_API_KEY"
# windows, set 为临时变量,setx 为永久变量,/m 表示系统变量,无此参数表示用户变量
set "OPENAI_API_KEY"="YOUR_API_KEY" /m

api_key: SecretStr | None = secret_from_env ("OPENAI_API_KEY", default=None)

接下来,初始化模型:

chat = ChatOpenAI (
    api_key=api_key, 
    base_url=base_url,
    model=chat_model,
    temperature=0.7
)

# 使用 LLM

使用 LLM 来回答问题,只需要调用 invoke 方法,并且传入问题即可,此外还可以通过提示词模板 (prompt template) 生成提示词。

下面演示如何构建一个简单的 LLM 链 (chains):

from langchain_core.prompts import ChatPromptTemplate
# 创建一个提示词模板 (prompt template)
prompt = ChatPromptTemplate.from_messages ([
    ("system", "你是一个 AI 助手,用中文回答问题,回答字数在 30 字以内"),
    ("user", "{user_input}"),
])
# 基于 LCEL 表达式构建 LLM 链路,LCEL 语法雷系 linux 的 pipeline 语法,从左往右按顺序执行
chain = prompt | chat
# 执行对话
response = chain.invoke ({"user_input": "什么是 802.1x 认证技术"})

# 输出转换

LLM 输出的 response 是一条消息格式 <class 'langchain_core.messages.ai.AIMessage'> ,所以在代码输出时需要使用 response.content ,为了方便处理结果,可以将消息转换为字符串

from langchain_core.output_parsers import StrOutputParser
# 创建一个字符串输出解释器
output_parser = StrOutputParser ()
# 创建对话链,将输出解释器添加到链
chain = prompt | chat | output_parser
# 执行对话
response = chain.invoke ({"user_input": "什么是 802.1x 认证技术"})
# 输出结果,直接使用 response, 不再需要 response.content
print ("AI 回答:", response)

# LangChain Expression Language (LangChain 表达式)

LangChain 表达式 (LCEL) 是一种创建任意自定义链的方法。它基于 Runnable 协议构建。

# 相关概念

# LCEL

LCEL (LangChainExpressionLanguage) 是一种强大的工作流编排工具,可以从基本组件构建复杂任务链条 (chain),并支持诸如流式处理、并行处理和日志记录等开箱即用的功能。

LCEL 从第一天起就被设计为支持将原型投入生产,无需更改代码,LCEL 有以下几个技术亮点:

  • 一流的流式支持:当您使用 LCEL 构建链时,您将获得可能的最佳时间到第一个标记 (直到输出的第一块内容出现所经过的时间)。对于某些链,这意味着我们直接从 LLM 流式传输标记到流式输出解析器,您将以与 LLM 提供程序输出原始标记的速率相同的速度获得解析的增量输出块。

  • 异步支持:使用 LCEL 构建的任何链都可以使用同步 API(例如,在您的 Jupyter 笔记本中进行原型设计)以及异步 API(例如,在 LangServe 服务器中)进行调用。这使得可以在原型和生产中使用相同的代码,具有出色的性能,并且能够在同一服务器中处理许多并发请求。

  • 优化的并行执行:每当您的 LCEL 链具有可以并行执行的步骤时(例如,如果您从多个检索器中获取文
    档),我们会自动执行,无论是在同步接口还是异步接口中,以获得可能的最小延迟。

  • 重试和回退:为 LCEL 链的任何部分配置重试和回退。这是使您的链在规模上更可靠的好方法。我们目前正在努力为重试 / 回退添加流式支持,这样您就可以获得额外的可靠性而无需任何延迟成本。

  • 访问中间结果:对于更复杂的链,访问中间步骤的结果通常非常有用,即使在生成最终输出之前。这可以用于让最终用户知道正在发生的事情,甚至只是用于调试您的链。您可以流式传输中间结果,并且在每个 LangServe 服务器上都可以使用。

  • 输入和输出模式:输入和输出模式为每个 LCEL 链提供了从链的结构推断出的 Pydantic 和 JSONSchema 模式。这可用于验证输入和输出,并且是 LangServe 的一个组成部分。

# 运行接口

为了尽可能简化创建自定义链的过程,我们实现了一个 "Runnable" 协议。许多 LangChain 组件都实现了 Runnable 协议,包括聊天模型、LLMs、输出解析器、检索器、提示模板等等。此外,还有一些有用的基本组件可用于处理可运行对象,您可以在下面了解更多。

这是一个标准接口,可以轻松定义自定义链,并以标准方式调用它们。标准接口包括:

  • stream : 返回响应的数据块
  • invoke : 对输入调用链
  • batch : 对输入列表调用链

这些还有相应的异步方法,与 asyncio 一起使用 await 语法实现并发:

  • astream : 异步返回响应的数据块
  • ainvoke : 异步对输入调用链
  • abatch : 异步对输入列表调用链
  • astream_log : 异步返回中间步骤,以及最终响应
  • astream_events : beta 流式传输链中发生的事件 (langchain-core 0.1.14 中引入)

输入类型 和 输出类型 因组件而异

组件输入类型输出类型
提示字典提示值
聊天模型单个字符串、聊天列表或提示值聊天消息
LLM个字符串、聊天列表或提示值字符串
输出解释器LLM 或聊天模型的输出取决于解释器
检索器单个字符串文档列表
工具单个字符串或字典、取决于工具取决于工具

所有可运行对象都公开输入和输出模式,以检查输入和输出:

  • input_schema: 从可运行对象结构自动生成的输入 pydantic 模型
  • output_schema: 从可运行对象结构自动生成的输出 pydantic 模型

# 链式运行 (chain runnables)

关于 LangChain 表达式的一点是,任何两个可运行对象可以 “链式” 组合成序列。前一个可运行对象的 .invoke () 调用的输出作为输入传递给下一个可运行对象。这可以使用管道操作符 ( | ) 或更明确的 .pipe () 方法来完成,二者效果相同。

生成的 RunnableSequence 本身就是一个可运行对象,这意味着它可以像其他任何可运行对象一样被调用、流式处理或进一步链式组合。以这种方式链式运行可运行对象的优点是高效的流式处理(序列会在输出可用时立即流式输出),以及使用像 LangSmith 这样的工具进行调试和追踪。

# 管道操作符: |

为了展示这个是如何工作的,让我们通过一个示例来演示。我们使用 LangChain 中的一个常见模式:使用提示词模板将输入格式化为聊天模型,最后将聊天消息输出转换为字符串,使用输出解析器。

from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
prompt = ChatPromptTemplate.from_template ("解释什么是 {protocol} 协议,字数在 20 以内")
parser = StrOutputParser ()
chain = prompt | chat | parser
response = chain.invoke ({"protocol": "IGMP"})
print (response)

Prompts 和 models 都是可运行的,并且 Prompt 调用的输出类型与聊天模型的 input 类型相同,因此我们可以将它们链接在一起。然后,我们可以像任何其他 runnable 一样调用结果序列:

# 强制转换

我们也可以将一个链与更多的可运行项结合起来,创建另一个链。这可能涉及使用其他类型的可运行项进行一些输入 / 输出格式化,具体取决于链组件所需的输入和输出。

例如,假设我们想将生成协议解释的链与另一个链组合,该链评估协议位于 TCP/IP 的哪一层。

我们需要注意如何将输入格式化为下一个链。在下面的示例中,链中的字典会自动解析并转换为 RunnableParallel ,它并行运行所有值并返回一个包含结果的字典。

以下示例中,在原来代码的基础上,加入 composed_chain 链,该链使用 analysis_prompt 并将 chain 的输出作为输入,在最后的 .invoke 方法中,仍然传递 chain 所需的 format 参数

from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
prompt = ChatPromptTemplate.from_template ("解释什么是 {protocol} 协议,字数在 20 以内")
parser = StrOutputParser ()
chain = prompt | chat | parser
analysis_prompt = ChatPromptTemplate.from_template ("这个协议工作在 TCP/IP 的哪一层: {network}")
composed_chain = {"network": chain} | analysis_prompt | chat | parser
response = composed_chain.invoke ({"protocol": "IGMP"})
print (response)

函数也会被强制转换为可运行项,因此您也可以向链中添加自定义逻辑。下面的链产生与之前相同的逻辑流程:

from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
prompt = ChatPromptTemplate.from_template ("解释什么是 {protocol} 协议,字数在 20 以内")
parser = StrOutputParser ()
chain = prompt | chat | parser
analysis_prompt = ChatPromptTemplate.from_template ("这个协议工作在 TCP/IP 的哪一层: {network}")
composed_chain_with_lambda = (
    chain
    | (lambda input: {"network": input})
    | analysis_prompt
    | chat
    | StrOutputParser ()
)
response = composed_chain_with_lambda.invoke ({"protocol": "IGMP"})
print (response)

以这种方式使用函数可能会干扰流式处理等操作,参考 文档

# .pipe () 方法

我们也可以使用 .pipe () 方法组合相同的序列。示例:

from langchain_core.runnables import RunnableParallel
composed_chain_with_pipe = (
    RunnableParallel ({"network": chain})
    .pipe (analysis_prompt)
    .pipe (chat)
    .pipe (parser)
)
response = composed_chain_with_pipe.invoke ({"protocol": "IGMP"})

或者简写成

from langchain_core.runnables import RunnableParallel
composed_chain_with_pipe = RunnableParallel ({"network": chain}).pipe (
    analysis_prompt, chat, parser
)
response = composed_chain_with_pipe.invoke ({"protocol": "IGMP"})

# LangChain prompt (提示词模板)

# Prompt templates

提示模板 (Prompt Templates) 有助于将用户输入和参数转换为语言模型的说明。 这可用于指导模型的响应,帮助它理解上下文并生成相关且连贯的基于语言的输出。

Prompt Templates 将字典作为输入,其中每个键代表 Prompt 模板中要填写的变量。

提示模板输出 PromptValue。此 PromptValue 可以传递给 LLM 或 ChatModel,也可以转换为字符串或消息列表。 此 PromptValue 存在的原因是为了便于在字符串和消息之间切换。

有几种不同类型的提示模板:

# String PromptTemplates

这些提示词模板用于格式化单个字符串,通常用于更简单的输入:

from langchain_core.prompts import PromptTemplate
prompt_template = PromptTemplate.from_template (
    "请解释什么是 {protocol} 协议,回答字数不超过 {wordage}"
)
chain = prompt_template | chat
response = chain.invoke ({"protocol": "capwap", "wordage": "20"})
from langchain_core.prompts import PromptTemplate
prompt_template = PromptTemplate.from_template (
    "请解释什么是 {protocol} 协议,回答字数不超过 {wordage}"
)
prompt_value = prompt_template.invoke ({"protocol": "capwap", "wordage": "20"})
response = chat.invoke (prompt_value)

在 LangChain 框架中,通过 | 连接的组件链需要用 invoke () 触发执行,第一段代码是一个标准的链式调用写法,第二段代码相当于手动分步执行,失去了链式调用的优势。

# ChatPromptTemplates

聊天模型 (Chat Model) 以聊天列表作为消息输入,这个聊条消息列表的内容也可以通过提示词模板进行管理。这些聊天消息与原始字符串不同,每个消息都与 角色 (role) 关联。

例如,在 OpenAI 的 Chat Completion API 中,Openai 的聊天模型给不同的聊天消息定义了三种角色类型:

  • system: 系统消息,通常用于描述 AI 的身份
  • user: 用户消息,用户发送给 AI 的内容
  • assistant: 助手消息,当前消息是 AI 回答的内容
from langchain_core.prompts import ChatPromptTemplate
prompt_template = ChatPromptTemplate ([
    ("system", "你是一个大学生,用户问你问题,你只能回答 '{answer}'"),
    ("user", "解释这个网络协议: {protocol}")
])
chain = prompt_template | chat
response = chain.invoke ({"answer": "不知道", "protocol": "ICMP"})

在上面的示例中,此 ChatPromptTemplate 将在调用时构造两条消息。 第一个是系统消息,将由用户传入的变量 answer 进行格式化。 第二个是 HumanMessage,格式化 protocol 变量。

提示词也可以是一段对话示例:

prompt_template = ChatPromptTemplate ([
    ("system", "你是一个大学生,用户问你问题,你只能回答 '{answer}'"),
    ("user", "什么是 OSPF 协议"),
    ("assistant", "我不知道"),
    ("user", "解释这个网络协议: {protocol}")
])
chain = prompt_template | chat
response = chain.invoke ({"answer": "不知道", "protocol": "ICMP"})

或者,也可以将消息对象直接传入:

from langchain_core.messages import SystemMessage,HumanMessage
from langchain_core.prompts import ChatPromptTemplate, HumanMessagePromptTemplate
prompt_template = ChatPromptTemplate.from_messages ([
    SystemMessage (
        content="你是一个大学生,不管用户问你什么,你都只能回答 '{answer}'"
    ),
    HumanMessagePromptTemplate.from_template ("什么是 {protocol} 协议")
])
chain = prompt_template | chat
response = chain.invoke ({
    "answer": "不知道",
    "protocol": "ICMP"
})

# MessagesPlaceholder

此提示模板负责在特定位置添加消息列表。 在上面的 ChatPromptTemplate 中,我们看到了如何格式化两条消息,每条消息都是一个字符串。
但是,如果我们希望用户传入一个消息列表,我们将其放入特定位置,该怎么办? 这就是使 MessagesPlaceholder 的方式。

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage
prompt_template = ChatPromptTemplate ([
    ("system", "You are a helpful assistant"),
    MessagesPlaceholder ("msgs")
])
prompt_template.invoke ({"msgs": [HumanMessage (content="hi!"), HumanMessage (content="hello!")]})

这将生成一个包含三条消息的列表,第一条是系统消息,第二条和第三条是我们传入的 HumanMessage。 如果我们传入了 n 条消息,那么它总共会产生 n+1 条消息(系统消息加上传入的 n 条消息)。 这对于将邮件列表放入特定位置非常有用。

在不显式使用类的情况下完成相同作的另一种方法是:MessagesPlaceholder

prompt_template = ChatPromptTemplate ([
    ("system", "You are a helpful assistant"),
    ("placeholder", "{msgs}") # <-- This is the changed part
])

# 部分格式化提示词模板

就像部分绑定函数的参数一样,部分格式化提示词模板也是有意义的,例如,传入所需值的一个子集,以创建一个新的提示词模板,该模板只期望剩余值的子集。

LangChain 以两种方式支持这一点:

  • 使用字符串值进行部分格式化。
  • 使用返回字符串值的函数进行部分格式化。

# 使用字符串的部分格式化

想要部分格式化提示词模板的一个常见用例是,如果您在其他变量之前获得了某些变量的访问权限。例如,假设您有一个提示词模板,需要两个变量,foo 和 bar。如果您在链中早期获得了 foo 值,但稍后才获得 baz 值,那么将两个变量传递整个链可能会很不方便。相反,您可以使用 foo 值部分格式化提示词模板,然后将部分格式化的提示词模板传递下去并仅使用它。下面是一个实现此操作的示例:

from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template ("{foo}{bar}")
partial_prompt = prompt.partial (foo="foo")
print (partial_prompt.format (bar="baz"))

还可以仅使用部分变量初始化提示词

prompt = PromptTemplate (
    template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"}
)
print (prompt.format (bar="baz"))

# 使用函数进行部分应用

另一个常见的用法是与函数进行部分应用。使用场景是当您有一个变量,您知道总是想以一种常见的方式获取它。一个典型的例子是日期或时间。想象一下,您有一个提示词,您总是希望它包含当前日期。您不能在提示词中硬编码它,并且将其与其他输入变量一起传递是不方便的。在这种情况下,能够使用一个始终返回当前日期的函数来部分应用提示词是很方便的。

from datetime import datetime
def _get_datetime ():
    now = datetime.now ()
    return now.strftime ("% m/% d/% Y, % H:% M:% S")
prompt = PromptTemplate (
    template="Tell me a {adjective} joke about the day {date}",
    input_variables=["adjective", "date"],
)
partial_prompt = prompt.partial (date=_get_datetime)
print (partial_prompt.format (adjective="funny"))

您还可以仅使用部分变量初始化提示词,这在此工作流程中通常更有意义。

prompt = PromptTemplate (
    template="Tell me a {adjective} joke about the day {date}",
    input_variables=["adjective"],
    partial_variables={"date": _get_datetime},
)
print (prompt.format (adjective="funny"))

# 组合提示词

LangChain 提供了一个用户友好的界面,用于将提示词的不同部分组合在一起。您可以使用字符串提示词或聊天提示词来实现这一点。以这种方式构建提示词可以方便地重用组件。

# 字符串提示词组合

在处理字符串提示词时,每个模板是连接在一起的。您可以直接使用提示词或字符串(列表中的第一个元素需要是一个提示词)。

from langchain_core.prompts import PromptTemplate
prompt = (
    PromptTemplate.from_template ("Tell me a joke about {topic}")
    + ", make it funny"
    + "\n\nand in {language}"
)
prompt.format (topic="sports", language="spanish")

# 聊天提示词组成

聊天提示词由一系列消息组成。与上面的示例类似,我们可以连接聊天提示词模板。每个新元素都是最终提示词中的一条新消息。

首先,让我们用一个 SystemMessage 初始化一个 ChatPromptTemplate

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
prompt = SystemMessage (content="You are a nice pirate")

然后,您可以轻松创建一个管道,将其与其他消息 或 消息模板结合起来。 当没有变量需要格式化时使用 Message,当有变量需要格式化时使用 MessageTemplate。您也可以仅使用一个字符串(注意:这将自动推断为一个 HumanMessagePromptTemplate)。

new_prompt = (
    prompt + HumanMessage (content="hi") + AIMessage (content="what?") + "{input}"
)

在底层,这会创建一个 ChatPromptTemplate 类的实例,因此您可以像之前一样使用它!

new_prompt.format_messages (input="i said hi")

# 使用 PipelinePrompt

LangChain 包含一个名为 PipelinePromptTemplate 的类,当您想重用提示词的部分时,这个类非常有用。PipelinePrompt 由两个主要部分组成:

  • 最终提示词:返回的最终提示词
  • 管道提示词:一个元组列表,由字符串名称和提示词模板组成。每个提示词模板将被格式化,然后作为具有相同名称的变量传递给未来的提示词模板。
from langchain_core.prompts import PipelinePromptTemplate, PromptTemplate
full_template = """{introduction}
{example}
{start}"""
full_prompt = PromptTemplate.from_template (full_template)
introduction_template = """You are impersonating {person}."""
introduction_prompt = PromptTemplate.from_template (introduction_template)
example_template = """Here's an example of an interaction:
Q: {example_q}
A: {example_a}"""
example_prompt = PromptTemplate.from_template (example_template)
start_template = """Now, do this for real!
Q: {input}
A:"""
start_prompt = PromptTemplate.from_template (start_template)
input_prompts = [
    ("introduction", introduction_prompt),
    ("example", example_prompt),
    ("start", start_prompt),
]
pipeline_prompt = PipelinePromptTemplate (
    final_prompt=full_prompt, pipeline_prompts=input_prompts
)
pipeline_prompt.input_variables
print (
    pipeline_prompt.format (
        person="Elon Musk",
        example_q="What's your favorite car?",
        example_a="Tesla",
        input="What's your favorite social media site?",
    )
)

# stream runnables (流式处理)

# 流式处理 (stream runnables)

流式运行对于使基于 LLM 的应用程序对最终用户具有响应性至关重要。重要的 LangChain 原语,如聊天
模型、输出解析器、提示模板、检索器和代理都实现了 LangChainRunnable 接口。该接口提供了两种通用的流式内容方法:

    1. 同步 stream 和异步 astream:流式传输链中的最终输出的默认实现。
    1. 异步 astream_events 和异步 astream_log:这些方法提供了一种从链中流式传输中间步骤和最
      终输出的方式。

# Stream (流)

所有 Runnable 对象都实现了一个名为 stream 的同步方法和一个名为 astream 的异步变体。这些
方法旨在以块的形式流式传输最终输出,尽快返回每个块。只有在程序中的所有步骤都知道如何处理输入流时,才能进行流式传输;即,逐个处理输入块,并产生相应的输出块。这种处理的复杂性可以有所不同,从简单的任务,如发出 LLM 生成的令牌,到更具挑战性的任务,如在整个 JSON 完成之前流式传输
JSON 结果的部分。开始探索流式传输的最佳方法是从 LLM 应用程序中最重要的组件开始 ——LLM 本身!

单个大型语言模型调用的运行时间通常比传统资源请求要长得多。 当你构建更复杂的链或代理,需要多个推理步骤时,这种情况会加剧。

幸运的是,大型语言模型是迭代生成输出的,这意味着可以显示合理的中间结果 在最终响应准备好之前。因此,尽快消费输出已成为用户体验的重要部分 在使用大型语言模型构建应用程序时,以帮助缓解延迟问题,而 LangChain 旨在提供一流的流式处理支持。

# .stream () & .astream ()

LangChain 中的大多数模块都包含 .stream () 方法(以及适用于异步环境的等效 .astream () 方法),作为一种人性化的流式接口。 .stream () 返回一个迭代器,你可以用简单的 for 循环来消费它。以下是一个使用聊天模型的示例:

from langchain_ollama import ChatOllama
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
for chunk in chat.stream ("天空是什么颜色的?"):
    print (chunk.content, end="|", flush=True)

响应的 chunk 是一个 对象,消息块可叠加

chunks = []
for chunk in chat.stream ("天空是什么颜色的?"):
    chunks.append (chunk)
    print (chunk.content, end="|", flush=True)
>>> print (type (chunks [0]))
<class 'langchain_core.messages.ai.AIMessageChunk'>
print (chunks [0] + chunks [1] + chunks [2] + chunks [3])
>>> content=' 晴朗无云 ' additional_kwargs={} response_metadata={} id='run-03be8fa5-832a-4fa4-888c-c603664215a5'

更多流式处理技巧,请参考 [官方文档](https://www.langchain.com.cn/docs/concepts/#% E6% B5%81% E5% BC%8F% E5% A4%84% E7%90%86)

# Chain

几乎所有的 LLM 应用程序都涉及不止一步的操作,而不仅仅是调用语言模型。让我们使用 LangChain
表达式语言(CEL)构建一个简单的链,该链结合了一个提示、模型和解析器,并验证流式传输是否正
常工作。我们将使用 StrOutputParser 来解析模型的输出。这是一个简单的解析器,从 AIMessageChun
k 中提取 content 字段,给出模型返回的 token。

LCEL 是一种声明式的方式,通过将不同的 LangChain 原语链接在一起来指定一个” 程序”。使用 LCEL
创建的链可以自动实现 stream 和 astream,从而实现对最终输出的流式传输。事实上,使用 LCEL 创
建的链实现了整个标准 Runnable 接口。

使用 chain 结合 asyncio 实现流式输出

from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
import asyncio
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
prompt = ChatPromptTemplate.from_template ("解释什么是 {protocol} 协议,字数在 20 以内")
parser = StrOutputParser ()
chain = prompt | chat | parser
async def async_stream ():
    async for chunk in chain.astream ({"protocol": "IGMP"}):
        print (chunk, end="|", flush=True)
asyncio.run (async_stream ())

请注意,即使我们在上面的链的末尾使用了 parser ,我们仍然得到了流式输出。parser 针对每个流式块单独操作。许多 LCEL 原语 也支持这种变换风格的直通流式处理,这在构建应用时非常方便。
自定义函数可以设计为返回生成器,能够在流上操作。
某些可运行对象,如提示词模板和聊天模型,无法处理单个块,而是聚合所有先前的步骤。这些可运行对象可能会中断流式处理过程。

# 处理输入流

# .astream () & .ainvoke

接下来的代码中调用 .astream () 而不是 .ainvoke ,两者区别为:

.ainvoke

  • 异步执行 完整调用流程,一次性返回最终结果。
  • 返回类型 Awaitable [Output]:直接返回完整的解析结果(如字符串)

.ainvoke

  • 异步流式执行,按 token 逐步返回结果。
  • 返回类型 AsyncIterator [OutputChunk]:返回异步生成器,逐个产生输出片段

# 流式传输 json

如果您想在生成时流式传输 JSON,解析器需要在输入流上操作,并尝试 “自动完成” 部分 JSON,使其成为有效状态。

from langchain_core.output_parsers import JsonOutputParser
import asyncio
chain = chat | JsonOutputParser ()
async def async_json_stream ():
    async for text in chain.astream (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`"
    ):
        print (text, flush=True)
asyncio.run (async_json_stream ())

现在,我们中断流式处理,我们将使用之前的示例,并在最后添加一个提取函数,从最终的 JSON 中提取国家名称。

from langchain_core.output_parsers import JsonOutputParser
import asyncio
# 直接对最终输入进行操作,而不是在输入流上操作
def _extract_country_names (inputs):
    """A function that does not operates on input streams and breaks streaming."""
    if not isinstance (inputs, dict):
        return ""
    if "countries" not in inputs:
        return ""
    countries = inputs ["countries"]
    if not isinstance (countries, list):
        return ""
    country_names = [
        country.get ("name") for country in countries if isinstance (country, dict)
    ]
    return country_names
chain = chat | JsonOutputParser () | _extract_country_names
async def async_json_stream ():
    async for text in chain.astream (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`"
    ):
        print (text, end='|', flush=True)
asyncio.run (async_json_stream ())

# 生成器函数

使用一个可以操作输入流的生成器函数来修复流式处理。

生成器函数(使用 yield 的函数)允许编写操作输入流的代码。

from langchain_core.output_parsers import JsonOutputParser
import asyncio
async def _extract_country_names_streaming (input_stream):
    """A function that operates on input streams."""
    country_names_so_far = set ()
    async for input in input_stream:
        if not isinstance (input, dict):
            continue
        if "countries" not in input:
            continue
        countries = input ["countries"]
        if not isinstance (countries, list):
            continue
        for country in countries:
            name = country.get ("name")
            if not name:
                continue
            if name not in country_names_so_far:
                yield name
                country_names_so_far.add (name)
chain = chat | JsonOutputParser () | _extract_country_names_streaming
async def async_json_stream ():
    async for text in chain.astream (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`"
    ):
        print (text, end='|', flush=True)
asyncio.run (async_json_stream ())

由于上面的代码依赖于 JSON 自动补全,您可能会看到部分国家名称(例如,Sp 和 Spain),这并不是提取结果所希望的,但是在此处,我们关注的是流式处理的概念,而不一定是链的结果。

# 非流式组件

一些内置组件,如检索器,并不提供任何 streaming
使用非流式组件构建的 LCEL 链,在很多情况下仍然能够进行流式处理,流式部分输出将在链中最后一个非流式步骤之后开始。

# 流事件

事件流式处理是一个测试版 API。该 API 可能会根据反馈有所变化。

此处演示的 V2 API,要求 langchain-core >= 0.2

为了使 astream_events API 正常工作:

  • 尽可能在代码中使用 async(例如,异步工具等)
  • 如果定义自定义函数 / 可运行程序,请传播回调
  • 在不使用 LCEL 的情况下使用可运行对象时,请确保在大型语言模型(LLMs)上调用 .astream () 而不是 .ainvoke ,以强制 LLM 流式传输令牌。

# 事件参考

下面是一个参考表,显示了各种可运行对象可能发出的某些事件,当流式处理正确实现时,输入到可运行对象的内容在输入流完全消耗之前是未知的。这意味着 inputs 通常仅在 end 事件中包含,而不是在 start 事件中。

event (事件)name (名称)chunk (块)input (输入)output (输出)
on_chat_model_start[model name]
on_chat_model_stream[model name]AIMessageChunk (content="hello")
on_chat_model_end[model name]AIMessageChunk (content="hello world")
on_llm_start[model name]
on_llm_stream[model name]'Hello'
on_llm_end[model name]'Hello human!'
on_chain_startformat_docs
on_chain_streamformat_docs"hello world!, goodbye world!"
on_chain_endformat_docs[Document (...)]"hello world!, goodbye world!"
on_tool_startsome_tool
on_tool_endsome_tool
on_retriever_start[retriever name]
on_retriever_end[retriever name][Document (...), ..]
on_prompt_start[template_name]
on_prompt_end[template_name]ChatPromptValue (messages: [SystemMessage, ...])

# 聊天模型

让我们先看看聊天模型产生的事件。

为了功能演示,下列代码使用了两种方法,两个 task 调用分别调用 LLM ,task1 使用 流事件 astream_events ,task2 使用 chain 式调用

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import ChatOllama
import asyncio
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/"
)
prompt = ChatPromptTemplate.from_template ("解释什么是 {protocol} 协议,字数在 20 以内")
parser = StrOutputParser ()
chain = prompt | chat |parser
events = []
async def async_astream_events (messages):
    async for event in chat.astream_events (messages, version="v2"):
        events.append (event)
async def main ():
    input_data = {"protocol": "BGP"}
    
    input_messages = prompt.invoke (input_data)
    task1 = asyncio.create_task (async_astream_events (messages=input_messages))
    task2 = asyncio.create_task (chain.ainvoke (input_data))
    await task1
    response =  await task2
    print (response)
asyncio.run (main ())

现在我们看看输出的一些事件

# 开始事件
>>> events = [:1]
[{'event': 'on_chat_model_start', 'data': {'input': ChatPromptValue (messages=[HumanMessage (content=' 解释什么是 BGP 协议,字数在 20 以内 ', additional_kwargs={}, response_metadata={})])}, 'name': 'ChatOllama', 'tags': [], 'run_id': '969a56b5-835c-4892-887c-77eda3272cac', 'metadata': {'ls_provider': 'ollama', 'ls_model_name': 'qwen2.5:7b', 'ls_model_type': 'chat', 'ls_temperature': None}, 'parent_ids': []}]
# 结束事件
>>> events = [-2:]
[{'event': 'on_chat_model_stream', 'run_id': '969a56b5-835c-4892-887c-77eda3272cac', 'name': 'ChatOllama', 'tags': [], 'metadata': {'ls_provider': 'ollama', 'ls_model_name': 'qwen2.5:7b', 'ls_model_type': 'chat', 'ls_temperature': None}, 'data': {'chunk': AIMessageChunk (content='', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-03-16T18:13:43.804733Z', 'done': True, 'done_reason': 'stop', 'total_duration': 4678584700, 'load_duration': 2450373700, 'prompt_eval_count': 41, 'prompt_eval_duration': 88516000, 'eval_count': 13, 'eval_duration': 995103000, 'message': Message (role='assistant', content='', images=None, tool_calls=None)}, id='run-969a56b5-835c-4892-887c-77eda3272cac', usage_metadata={'input_tokens': 41, 'output_tokens': 13, 'total_tokens': 54})}, 'parent_ids': []}, {'event': 'on_chat_model_end', 'data': {'output': AIMessageChunk (content='BGP 是边界网关协议,用于互联网路由。', additional_kwargs={}, response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-03-16T18:13:43.804733Z', 'done': True, 'done_reason': 'stop', 'total_duration': 4678584700, 'load_duration': 2450373700, 'prompt_eval_count': 41, 'prompt_eval_duration': 88516000, 'eval_count': 13, 'eval_duration': 995103000, 'message': Message (role='assistant', content='', images=None, tool_calls=None)}, id='run-969a56b5-835c-4892-887c-77eda3272cac', usage_metadata={'input_tokens': 41, 'output_tokens': 13, 'total_tokens': 54})}, 'run_id': '969a56b5-835c-4892-887c-77eda3272cac', 'name': 'ChatOllama', 'tags': [], 'metadata': {'ls_provider': 'ollama', 'ls_model_name': 'qwen2.5:7b', 'ls_model_type': 'chat', 'ls_temperature': None}, 'parent_ids': []}]

# chain

让我们看一下 json 流事件的示例:

chain = chat | JsonOutputParser () 
events = []
async def async_json_stream ():
    async for event in chain.astream_events (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`",
        version="v2",
    ):
        events.append (event)
asyncio.run (async_json_stream ())

在执行代码后,如果查看前几个事件: events [:3] ,可以看到有三个不同的开始事件: chainchatJsonOutputParser ()

让我们稍微修改一下代码,忽略开始事件、结束事件和来自链的事件:

num_events = 0
async def async_json_stream ():
    prompt = """
    output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key`name`and`population`
    """
    async for event in chain.astream_events (prompt,version="v2"):
        kind = event ["event"]
        if kind == "on_chat_model_stream":
            print (
                f"Chat model chunk: {repr (event ['data']['chunk'].content)}",
                flush=True,
            )
        if kind == "on_parser_stream":
            print (f"Parser chunk: {event ['data']['chunk']}", flush=True)
        num_events += 1
        if num_events > 30:
            # Truncate the output
            print ("...")
            break
asyncio.run (async_json_stream ())

因为模型和解析器都支持流式处理,我们可以实时看到来自两个组件的流式事件。

# 过滤事件

此 API 会产生许多事件,有时候我们希望能够对事件进行过滤。

可以通过组件的 nametagstype 进行过滤。

chainchatJsonOutputParser 都可以使用 .with_config ({"run_name":"my_name"}) 定义名称或者 .with_config ({"tags": ["my_name"]}) 定义标签

chain.astream_events 中使用:

  • include_names=["my_parser"] 按名称过滤
  • include_types=["chat_model"] 按类型过滤
  • include_tags=["my_chain"] 按标签过滤

注意中括号:定义 name 时 "run_name:"my_chain" ,定义 tags 时 "tags: ["my_chain"]

# 按名称 (name)

from langchain_core.output_parsers import JsonOutputParser
import asyncio
chain = chat.with_config ({"run_name": "model"}) | JsonOutputParser ().with_config ({"run_name": "my_parser"})
max_events = 0
async def async_json_stream ():
    async for event in chain.astream_events (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`",
        version="v2",include_names=["my_parser"]
    ):
        
        global max_events
        max_events += 1
        if max_events < 10:
            # Truncate output
            print (event)
        elif max_events == 10:
            print ("...")
asyncio.run (async_json_stream ())

# 按类型 (type)

from langchain_core.output_parsers import JsonOutputParser
import asyncio
chain = chat.with_config ({"run_name": "model"}) | JsonOutputParser ().with_config ({"run_name": "my_parser"})
max_events = 0
async def async_json_stream ():
    async for event in chain.astream_events (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`",
        version="v2",include_types=["chat_model"]
    ):
        
        global max_events
        max_events += 1
        if max_events < 10:
            # Truncate output
            print (event)
        elif max_events == 10:
            print ("...")
asyncio.run (async_json_stream ())

# 按标签 (tags)

标签由给定可运行组件的子组件继承。

from langchain_core.output_parsers import JsonOutputParser
import asyncio
chain = (chat | JsonOutputParser ()).with_config ({"tags": ["my_chain"]})
max_events = 0
async def async_json_stream ():
    async for event in chain.astream_events (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`",
        version="v2",include_tags=["my_chain"]
    ):
        
        global max_events
        max_events += 1
        if max_events < 10:
            # Truncate output
            print (event)
        elif max_events == 10:
            print ("...")
asyncio.run (async_json_stream ())

# 非流式组件

有些组件由于不在 输入流 上操作而无法很好地流式处理,虽然这些组件在使用 astream 时可能会中断最终输出的流式处理,但 astream_events 仍会从支持流式处理的中间步骤中产生流式事件

下列代码如果改为 chain.astream 则会看到 astream API 工作不正常,因为 _extract_country_names 不在流上操作,但代码使用 astream_events 时,仍然能看到来自模型和解析器的流式输出:

from langchain_core.output_parsers import JsonOutputParser
import asyncio
def _extract_country_names (inputs):
    """A function that does not operates on input streams and breaks streaming."""
    if not isinstance (inputs, dict):
        return ""
    if "countries" not in inputs:
        return ""
    countries = inputs ["countries"]
    if not isinstance (countries, list):
        return ""
    country_names = [
        country.get ("name") for country in countries if isinstance (country, dict)
    ]
    return country_names
chain = chat | JsonOutputParser () | _extract_country_names
async def async_json_stream ():
    async for event in chain.astream_events (
        "output a list of the countries france, spain and japan and their populations in JSON format."
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key`name`and`population`",
        version="v2"
    ):
        
        print (event, flush=True)
asyncio.run (async_json_stream ())

# 传播回调

如果您在工具中使用可运行的调用,您需要将回调传播到可运行的对象;否则,将不会生成流事件。

使用 RunnableLambdas 或 @chain 装饰器时,回调会在后台自动传播。

from langchain_core.runnables import RunnableLambda
from langchain_core.tools import tool
def reverse_word (word: str):
    return word [::-1]
reverse_word = RunnableLambda (reverse_word)
@tool
def bad_tool (word: str):
    """Custom tool that doesn't propagate callbacks."""
    return reverse_word.invoke (word)
async for event in bad_tool.astream_events ("hello", version="v2"):
    print (event)

这是一个正确传播回调的重新实现。您会注意到现在我们也从 reverse_word 可运行对象中获得事件

@tool
def correct_tool (word: str, callbacks):
    """A tool that correctly propagates callbacks."""
    return reverse_word.invoke (word, {"callbacks": callbacks})
async for event in correct_tool.astream_events ("hello", version="v2"):
    print (event)

如果您从 Runnable Lambdas@chains 中调用可运行对象,则回调将自动代表您传递。

from langchain_core.runnables import RunnableLambda
async def reverse_and_double (word: str):
    return await reverse_word.ainvoke (word) * 2
reverse_and_double = RunnableLambda (reverse_and_double)
await reverse_and_double.ainvoke ("1234")
async for event in reverse_and_double.astream_events ("1234", version="v2"):
    print (event)

并且使用 @chain 装饰器

from langchain_core.runnables import chain
@chain
async def reverse_and_double (word: str):
    return await reverse_word.ainvoke (word) * 2
await reverse_and_double.ainvoke ("1234")
async for event in reverse_and_double.astream_events ("1234", version="v2"):
    print (event)

# Few-shot prompt templates (示例选择器)

我们将学习如何创建一个简单的提示模板,该模板在生成时为模型提供示例输入和输出。为 LLM 提供一些这样的示例称为 few-shotting。

提示词中包含交互样本的作用是为了帮助模型更好地理解用户的意图,从而更好地回答问题或执行任务。小样本提示模板是指使用一组少量的示例来指导模型处理新的输入。这些示例可以用来训练模型,以便模型可以更好地理解和回答类似的问题。

例如:

Q: 什么是 802.1x 协议
A: 我不知道。
Q: 什么是 capwap 协议
A: 我不知道
Q: 什么是 sftp 协议
A: 我不知道

告诉大模型,Q 是问题,A 是答案,按照这种格式进行问答交互

# 示例集

使用实例集提供示例分为三步:

    1. 创建格式器 (formatter)
    1. 创建示例集 (example set)
  • example setformatter 传递给 FewShotPromptTemplate
  1. 创建格式器
    配置一个格式器 (formatter),将 few-shot 示例格式化为字符串。此格式化器应为 object.PromptTemplate

input_variables 是模板中要求必须在运行时动态传入的变量列表

from langchain_core.prompts import PromptTemplate
example_prompt = PromptTemplate.from_template ("Question: {question}\n {answer}")
from langchain_core.prompts import PromptTemplate
example_prompt = PromptTemplate (
    input_variables=["question", "answer"],
    template="Question: {question}\n {answer}"
    )
  1. 创建实例集:创建一个 few-shot 示例列表。每个示例都应该是一个字典,代表我们上面定义的格式化程序提示符的示例输入。
examples = [
    {
        "question": "现在是几点",
        "answer":
        """
        现在是 0:59 
        """
    },
        {
        "question": "今天是几号",
        "answer":
        """
        今天是 2025 年 3 月 15 号
        """
    }
]

用其中一个示例来测试格式设置提示: print (example_prompt.invoke (examples [0]).to_string ())

  1. 将实例和格式化程序传递给 FewShotPromptTemplate

创建一个 FewShotPromptTemplate 对象。此对象采用 few-shot 示例和 few-shot 示例的格式化程序。它会格式化传递示例,并将它们添加到最终提示符中: FewShotPromptTemplate example_prompt suffix

suffix 和 input_variables 用于在提示词模板最后追加内容,input_variables 用于定义 suffix 模板参数中必须传入的参数

from langchain_core.prompts import FewShotPromptTemplate
prompt = FewShotPromptTemplate (
    examples=examples,
    example_prompt=example_prompt,
    suffix="Question: {input}",
    input_variables=["input"],
)
print (
    prompt.invoke ({"input": "Who was the father of Mary Ball Washington?"}).to_string ()
)

完整示例:

from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate
examples = [
    {
        "question": "现在是几点",
        "answer":
        """
        现在是 0:59 
        """
    },
        {
        "question": "今天是几号",
        "answer":
        """
        今天是 2025 年 3 月 15 号
        """
    }
]
example_prompt = PromptTemplate (
    input_variables=["question", "answer"],
    template="Q: {question}\n {answer}"
    )
prompt_template = FewShotPromptTemplate (
    examples=examples,
    example_prompt=example_prompt,
    suffix="Q: {input}",
    input_variables=["input"]
)
chain = prompt_template | chat
response = chain.invoke ({"input": "明天是几号"})
print ("AI 回答:", response.content)

$ 传递单个样本示例 $

通过 PromptTemplate 对象,简单的在提示词模板中插入单个样例(解包)

example [0] = {"question":" 现在是几点 ","answer":" 现在是 0:59"}
**example [0] = Q: 现在是几点 现在是 0:59

example_prompt = PromptTemplate (
    input_variables=["question", "answer"],
    template="Q: {question}\n {answer}"
    )
print (example_prompt.format (**examples [0]))

# 示例选择器

这里重用前一部分中的示例集和提示词模板 (prompt template)。但是,不会将示例直接提供给 FewShotPrompt Template 对象,把全部示例插入到提示词中,而是将它们提供给一个 ExampleSelector 对象,插入部分示例。

这里我们使用 SemanticSimilarityExampleSelector 类。该类根据与输入的相似性选择小样本示例。它使用嵌入模型计算输入和小样本示例之间的相似性,然后使用向量数据库执行相似搜索,获取跟输入相似的示例。

在如下代码中,我们单独输出示例选择器的内容

执行代码前请安装依赖

  • pip install langchain-ollama
  • pip install -U langchain-chroma

代码使用本地 ollama 服务中的 Embeddings: mxbai-embed-large:latest
请在 ollama 中使用 ollama run mxbai-embed-large:latest 进行安装

examples = [
    {
        "question": "今天北京天气怎么样",
        "answer":
        """
        今天北京天气:晴,温度: 8℃
        """
    },
        {
        "question": "今天武汉天气怎么样",
        "answer":
        """
        今天武汉天气:晴,温度: 11℃
        """
    }
]
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_ollama import OllamaEmbeddings
ollama_embedding = OllamaEmbeddings (
    model="mxbai-embed-large:latest",
    base_url="http://127.0.0.1:11434/"
)
example_selector = SemanticSimilarityExampleSelector.from_examples (
    # 可供选择的示例列表
    examples,
    # 用于生成嵌入的嵌入类,该嵌入用于衡量语义相似性
    ollama_embedding,
    # 用于存储嵌入和执行相似搜索的 VectorStore 类
    Chroma,
    # 要生成的示例数
    k=1,
)
# 选择与输入最相似的示例
question = "今天武汉天气怎样"
selected_examples = example_selector.select_examples ({"question": question})
print (f"Examples most similar to the input: {question}")
for example in selected_examples:
    print ("\n")
    for k, v in example.items ():
        print (f"{k}: {v}")

如果代码报错,使用 pip check 检查兼容性问题
如果版本不兼容: langchain-chroma 0.2.2 has requirement numpy<2.0.0,>=1.22.4; python_version < "3.12", but you have numpy 2.2.3.
解决方案: pip uninstall numpy -y pip install numpy==1.26.4

我们沿用之前的 FewShotPromptTemplate 对象,完整代码如下

examples = [
    {
        "question": "今天北京天气怎么样",
        "answer":
        """
        今天北京天气:晴,温度: 8℃
        """
    },
        {
        "question": "今天武汉天气怎么样",
        "answer":
        """
        今天武汉天气:晴,温度: 11℃
        """
    }
]
from langchain_chroma import Chroma
from langchain_core.example_selectors import SemanticSimilarityExampleSelector
from langchain_openai import OpenAIEmbeddings
from langchain_ollama import OllamaEmbeddings
ollama_embedding = OllamaEmbeddings (
    model="mxbai-embed-large:latest",
    base_url="http://127.0.0.1:11434/"
)
example_selector = SemanticSimilarityExampleSelector.from_examples (
    examples=examples,
    embeddings=ollama_embedding,
    vectorstore_cls=Chroma,
    k=1,
)
example_prompt = PromptTemplate (
    input_variables=["question", "answer"],
    template="Q: {question}\n {answer}"
)
prompt_template = FewShotPromptTemplate (
    example_selector=example_selector,
    example_prompt=example_prompt,
    suffix="Question: {input}",
    input_variables=["input"],
)
chain = prompt_template | chat
response = chain.invoke ({"input": "武汉天气怎样"})
print ("AI 回答:", response.content)

# Chat Model (聊天模型)

# 返回结构化数据

通常,模型返回符合特定模式的输出是非常有用的。一个常见的用例是从文本中提取数据以插入数据库或与其他下游系统一起使用。本章节涵盖了从模型获取结构化输出的一些策略。

# .with_structured_output ()

您可以在这里找到 支持此方法的模型列表。

这是获取结构化输出最简单和最可靠的方法。 with_structured_output () 针对提供结构化输出的原生 API 的模型实现,例如工具 / 函数调用或 JSON 模式,并在底层利用这些功能。

此方法接受一个模式作为输入,该模式指定所需输出属性的名称、类型和描述。该方法返回一个类似模型的可运行对象,除了输出字符串或消息外,它输出与给定模式对应的对象。模式可以指定为 TypedDict 类、 JSON SchemaPydantic 类。如果使用 TypedDictJSON Schema ,则可运行对象将返回一个字典;如果使用 Pydantic 类,则将返回一个 Pydantic 对象。

作为一个例子,让我们让模型生成一个笑话,并将设置与笑点分开,首先我们初始化模型:

from langchain_ollama import ChatOllama
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)

# Pydantic

from typing import Optional
from pydantic import BaseModel, Field
# Pydantic
class Joke (BaseModel):
    """Joke to tell user."""
    setup: str = Field (description="The setup of the joke")
    punchline: str = Field (description="The punchline to the joke")
    rating: Optional [int] = Field (
        default=None, description="How funny the joke is, from 1 to 10"
    )
structured_llm = chat.with_structured_output (Joke)
>>> structured_llm.invoke ("Tell me a joke about cats")
setup="Why don't cats play poker in the jungle?" punchline='Because there are too many cheetahs!' rating=8

除了 Pydantic 类的结构,Pydantic 类的名称、文档字符串以及参数的名称和提供的描述也非常重要。大多数情况下,with_structured_output 使用的是模型的函数 / 工具调用 API,您可以有效地将所有这些信息视为添加到模型提示中。

# TypedDict

如果您不想使用 Pydantic,明确不想对参数进行验证,或者希望能够流式处理模型输出,您可以使用 TypedDict 类定义您的模式。我们可以选择性地使用 LangChain 支持的特殊 Annotated 语法,允许您指定字段的默认值和描述。请注意,如果模型没有生成默认值,则默认值不会自动填充,它仅用于定义传递给模型的模式。

强烈建议从 typing_extensions 导入 Annotated 和 TypedDict,而不是从 typing 导入,以确保在不同 Python 版本之间的一致行为。

from typing_extensions import Annotated, TypedDict, Optional
# TypedDict
class Joke (TypedDict):
    """Joke to tell user."""
    setup: Annotated [str, ..., "The setup of the joke"]
    # Alternatively, we could have specified setup as:
    # setup: str                    # no default, no description
    # setup: Annotated [str, ...]    # no default, no description
    # setup: Annotated [str, "foo"]  # default, no description
    punchline: Annotated [str, ..., "The punchline of the joke"]
    rating: Annotated [Optional [int], None, "How funny the joke is, from 1 to 10"]
structured_llm = chat.with_structured_output (Joke)
>>> response = structured_llm.invoke ("Tell me a joke about cats")
{'punchline': 'Because there are too many cheetahs!', 'rating': 8, 'setup': "Why don't cats play poker in the jungle?"}

# JSON Schema

同样,我们可以传入一个 JSON Schema 字典。这不需要任何导入或类,并且清楚地说明了每个参数的文档,代价是稍微冗长一些。

json_schema = {
    "title": "joke",
    "description": "Joke to tell user.",
    "type": "object",
    "properties": {
        "setup": {
            "type": "string",
            "description": "The setup of the joke",
        },
        "punchline": {
            "type": "string",
            "description": "The punchline to the joke",
        },
        "rating": {
            "type": "integer",
            "description": "How funny the joke is, from 1 to 10",
            "default": None,
        },
    },
    "required": ["setup", "punchline"],
}
structured_llm = chat.with_structured_output (json_schema)
>>> structured_llm.invoke ("Tell me a joke about cats")
{'punchline': 'Because there are too many cheetahs!', 'rating': 8, 'setup': "Why don't cats play poker in the jungle?"}

# 在模式之间选择

让模型从多个模式中选择的最简单方法是创建一个具有联合类型属性的父模式:

from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
from Config_Loader import ConfigLoader
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
config = ConfigLoader.get_instance ()
chat = ChatOpenAI (
    api_key=config.get ('LLMapis.api_key'), 
    base_url=config.get ('LLMapis.base_url'),
    model=config.get ('LLMapis.chat_model'),
    temperature=1.0
)
from typing import Union, Literal
from typing_extensions import Optional
from pydantic import BaseModel, Field
# Pydantic
class Joke (BaseModel):
    """Joke to tell user."""
    type: Literal ["joke"] = "joke"  # 固定值字段
    setup: str = Field (description="The setup of the joke")
    punchline: str = Field (description="The punchline to the joke")
    rating: Optional [int] = Field (
        default=None, description="How funny the joke is, from 1 to 10"
    )
class Trivia (BaseModel):
    """Trivia to tell user"""
    type: Literal ["trivia"] = "trivia"  # 固定值字段
    response: str = Field (description="Trivia content")
class FinalResponse (BaseModel):
    final_output: Union [Joke, Trivia]
structured_llm = chat.with_structured_output (FinalResponse)
structured_llm.invoke ("Tell me a joke about cats")
structured_llm.invoke ("Tell me a trivia about cats")

在实际运行中,我并未能复现输出,两个 llm 均使用了 class Trivia

# 流式处理

当输出类型为字典时(即,当模式被指定为 TypedDict 类或 JSON Schema 字典时),我们可以从我们的结构化模型中流式输出。

from typing_extensions import Annotated, TypedDict, Optional
# TypedDict
class Joke (TypedDict):
    """Joke to tell user."""
    setup: Annotated [str, ..., "The setup of the joke"]
    punchline: Annotated [str, ..., "The punchline of the joke"]
    rating: Annotated [Optional [int], None, "How funny the joke is, from 1 to 10"]
structured_llm = chat.with_structured_output (Joke)
for chunk in structured_llm.stream ("Tell me a joke about cats"):
    print (chunk)

# 指定结构化输出

对于支持多种结构化输出方式的模型(即,它们同时支持工具调用和 JSON 模式),您可以使用 method = 参数指定使用哪种方法。

structured_llm = chat.with_structured_output (None, method="json_mode")
structured_llm.invoke (
    "Tell me a joke about cats, respond in JSON with`setup`and`punchline`keys"
)

# 原始输出

大型语言模型在生成结构化输出方面并不完美,尤其是当模式变得复杂时。您可以通过传递 include_raw=True 来避免引发异常并自行处理原始输出。这会将输出格式更改为包含原始消息输出、parsed 值(如果成功)以及任何结果错误:

structured_llm = chat.with_structured_output (Joke, include_raw=True)
structured_llm.invoke ("Tell me a joke about cats")

# 直接提示和解析模型输出

并非所有模型都支持 .with_structured_output (),因为并非所有模型都具有工具调用或 JSON 模式支持。对于这些模型,您需要直接提示模型使用特定格式,并使用输出解析器从原始模型输出中提取结构化响应。

# PydanticOutputParser

以下示例使用内置的 PydanticOutputParser 来解析被提示以匹配给定 Pydantic 模式的聊天模型的输出。请注意,我们直接将 format_instructions 添加到解析器的方法中的提示中:

from typing import List
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
class Person (BaseModel):
    """Information about a person."""
    name: str = Field (..., description="The name of the person")
    height_in_meters: float = Field (
        ..., description="The height of the person expressed in meters."
    )
class People (BaseModel):
    """Identifying information about all people in a text."""
    people: List [Person]
# Set up a parser
parser = PydanticOutputParser (pydantic_object=People)
# Prompt
prompt = ChatPromptTemplate.from_messages (
    [
        (
            "system",
            "Answer the user query. Wrap the output in`json`tags\n {format_instructions}",
        ),
        ("human", "{query}"),
    ]
).partial (format_instructions=parser.get_format_instructions ())

让我们看看发送到模型的信息:

query = "Anna is 23 years old and she is 6 feet tall"
>>> print (prompt.invoke (query).to_string ())
System: Answer the user query. Wrap the output in `json` tags
The output should be formatted as a JSON instance that conforms to the JSON schema below.
As an example, for the schema {"properties": {"foo": {"title": "Foo", "description": "a list of strings", "type": "array", "items": {"type": "string"}}}, "required": ["foo"]}
the object {"foo": ["bar", "baz"]} is a well-formatted instance of the schema. The object {"properties": {"foo": ["bar", "baz"]}} is not well-formatted.
Here is the output schema:
\`\`\`
{"description": "Identifying information about all people in a text.", "properties": {"people": {"title": "People", "type": "array", "items": {"$ref": "#/definitions/Person"}}}, "required": ["people"], "definitions": {"Person": {"title": "Person", "description": "Information about a person.", "type": "object", "properties": {"name": {"title": "Name", "description": "The name of the person", "type": "string"}, "height_in_meters": {"title": "Height In Meters", "description": "The height of the person expressed in meters.", "type": "number"}}, "required": ["name", "height_in_meters"]}}}
\`\`\`
Human: Anna is 23 years old and she is 6 feet tall

现在让我们调用它:

chain = prompt | llm | parser
>>> chain.invoke ({"query": query})
People (people=[Person (name='Anna', height_in_meters=1.8288)])

# 自定义解析

您还可以使用 LangChain 表达式 (LCEL) 创建自定义提示和解析器,使用普通函数解析模型的输出:

import json
import re
from typing import List
from langchain_core.messages import AIMessage
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
class Person (BaseModel):
    """Information about a person."""
    name: str = Field (..., description="The name of the person")
    height_in_meters: float = Field (
        ..., description="The height of the person expressed in meters."
    )
class People (BaseModel):
    """Identifying information about all people in a text."""
    people: List [Person]
# Prompt
prompt = ChatPromptTemplate.from_messages (
    [
        (
            "system",
            "Answer the user query. Output your answer as JSON that"
            "matches the given schema: \`\`\`json\n {schema}\n\`\`\`."
            "Make sure to wrap the answer in \`\`\`json and \`\`\`tags",
        ),
        ("human", "{query}"),
    ]
).partial (schema=People.schema ())
# Custom parser
def extract_json (message: AIMessage) -> List [dict]:
    """Extracts JSON content from a string where JSON is embedded between \`\`\`json and \`\`\` tags.
    Parameters:
        text (str): The text containing the JSON content.
    Returns:
        list: A list of extracted JSON strings.
    """
    text = message.content
    # Define the regular expression pattern to match JSON blocks
    pattern = r"\`\`\`json (.*?)\`\`\`"
    # Find all non-overlapping matches of the pattern in the string
    matches = re.findall (pattern, text, re.DOTALL)
    # Return the list of matched JSON strings, stripping any leading or trailing whitespace
    try:
        return [json.loads (match.strip ()) for match in matches]
    except Exception:
        raise ValueError (f"Failed to parse: {message}")

这是发送给模型的提示:

query = "Anna is 23 years old and she is 6 feet tall"
>>> print (prompt.format_prompt (query=query).to_string ())
System: Answer the user query. Output your answer as JSON that  matches the given schema: \`\`\`json
{'title': 'People', 'description': 'Identifying information about all people in a text.', 'type': 'object', 'properties': {'people': {'title': 'People', 'type': 'array', 'items': {'$ref': '#/definitions/Person'}}}, 'required': ['people'], 'definitions': {'Person': {'title': 'Person', 'description': 'Information about a person.', 'type': 'object', 'properties': {'name': {'title': 'Name', 'description': 'The name of the person', 'type': 'string'}, 'height_in_meters': {'title': 'Height In Meters', 'description': 'The height of the person expressed in meters.', 'type': 'number'}}, 'required': ['name', 'height_in_meters']}}}
\`\`\`. Make sure to wrap the answer in \`\`\`json and \`\`\` tags
Human: Anna is 23 years old and she is 6 feet tall

当我们调用它时,它的样子是这样的:

chain = prompt | llm | extract_json
>>> chain.invoke ({"query": query})
[{'people': [{'name': 'Anna', 'height_in_meters': 1.8288}]}]

# 缓存聊天模型响应

LangChain 为聊天模型提供了一个可选的缓存层。这主要有两个好处:

  • 如果您经常请求相同的内容,这可以通过减少您对大模型供应商的 API 调用次数来节省您的费用。这在应用开发期间尤其有用。
  • 通过减少您对大模型供应商的 API 调用次数,它可以加快您的应用程序速度。

# 内存缓存

这是一个临时缓存,用于在内存中存储模型调用。当您的环境重启时,它将被清除,并且在进程之间不共享。

from langchain_community.cache import InMemoryCache
from langchain.globals import set_llm_cache
import time
set_llm_cache (InMemoryCache ())
# The first time, it is not yet in cache, so it should take longer
llm.invoke ("Tell me a joke")
time.sleep (5)
llm.invoke ("Tell me a joke")

# SQLite 缓存

此缓存实现使用 SQLite 数据库来存储响应,并且在进程重启时仍然有效。

创建 sqlite 数据库文件

sqlite3 .langchain.db
PRAGMA encoding = 'UTF-8';

实现使用 SQLite 数据库来存储响应

from langchain_community.cache import SQLiteCache
set_llm_cache (SQLiteCache (database_path=".langchain.db"))
chat.invoke ("Tell me a joke")

# 获取日志概率

某些聊天模型可以配置为返回表示给定令牌可能性的令牌级日志概率。

为了让 OpenAI API 返回日志概率,我们需要配置 logprobs=True 参数。然后,日志概率将作为 response_metadata 的一部分包含在每个输出的 AIMessage 中:

from langchain_openai import ChatOpenAI
llm = ChatOpenAI (model="gpt-4o-mini").bind (logprobs=True)
msg = llm.invoke (("human", "how are you today"))
msg.response_metadata ["logprobs"]["content"][:5]

并且也作为流式消息块的一部分:

ct = 0
full = None
for chunk in llm.stream (("human", "how are you today")):
    if ct < 5:
        full = chunk if full is None else full + chunk
        if "logprobs" in full.response_metadata:
            print (full.response_metadata ["logprobs"]["content"])
    else:
        break
    ct += 1

# 获取 token 用量

跟踪令牌使用情况以计算成本是将您的应用投入生产的重要部分。

# 使用 LangSmith

您可以使用 LangSmith 来帮助跟踪您 LLM 应用中的令牌使用情况。请参阅 LangSmith 快速入门指南。

# 使用 AIMessage.usage_metadata

一些大模型供应商在聊天生成响应中返回令牌使用信息。当可用时,这些信息将包含在相应模型生成的 AIMessage 对象中。

LangChain 的 AIMessage 对象包含一个 usage_metadata 属性。当填充时,该属性将是一个具有标准键(例如,"input_tokens" 和 "output_tokens")的 UsageMetadata 字典。

response = chat.invoke ("Tell me a joke")
>>> print (response.usage_metadata)
{'input_tokens': 33, 'output_tokens': 25, 'total_tokens': 58}

# 使用 AIMessage.response_metadata

模型响应中的元数据也包含在 AIMessage 的 response_metadata 属性中。这些数据通常不是标准化的。请注意,不同的大模型供应商采用不同的约定来表示令牌计数:

print (f'OpenAI: {openai_response.response_metadata ["token_usage"]}\n')
print (f'Anthropic: {anthropic_response.response_metadata ["usage"]}')

# 流式处理

某些大模型供应商在流式上下文中支持令牌计数元数据。

OpenAI
例如,OpenAI 将在流结束时返回一条消息 chunk,其中包含令牌使用信息。此行为由 langchain-openai >= 0.1.9 支持,并可以通过设置 stream_usage=True 来启用。此属性在实例化 ChatOpenAI 时也可以设置。

llm = ChatOpenAI (model="gpt-4o-mini")
aggregate = None
for chunk in llm.stream ("hello", stream_usage=True):
    print (chunk)
    aggregate = chunk if aggregate is None else aggregate + chunk
# 请注意,使用元数据将包含在各个消息块的总和中:
print (aggregate.content)
print (aggregate.usage_metadata)

要禁用 OpenAI 的流式令牌计数,请将 stream_usage 设置为 False,或从参数中省略它:

aggregate = None
for chunk in llm.stream ("hello"):
    print (chunk)

您还可以通过在实例化聊天模型时设置 stream_usage 来启用流式令牌使用。这在将聊天模型纳入 LangChain 链 时非常有用:可以在 流式中间步骤 或使用诸如 LangSmith 的跟踪软件时监控使用元数据。

请参见下面的示例,我们返回结构化为所需模式的输出,但仍然可以观察到从中间步骤流式传输的令牌使用情况。

from pydantic import BaseModel, Field
class Joke (BaseModel):
    """Joke to tell user."""
    setup: str = Field (description="question to set up a joke")
    punchline: str = Field (description="answer to resolve the joke")
llm = ChatOpenAI (
    model="gpt-4o-mini",
    stream_usage=True,
)
# Under the hood, .with_structured_output binds tools to the
# chat model and appends a parser.
structured_llm = llm.with_structured_output (Joke)
async for event in structured_llm.astream_events ("Tell me a joke", version="v2"):
    if event ["event"] == "on_chat_model_end":
        print (f'Token usage: {event ["data"]["output"].usage_metadata}\n')
    elif event ["event"] == "on_chain_end":
        print (event ["data"]["output"])
    else:
        pass

# 使用回调

还有一些特定于 API 的回调上下文管理器,可以让您跟踪多个调用中的令牌使用情况。目前仅在 OpenAI API 和 Bedrock Anthropic API 中实现。

让我们首先看一个极其简单的示例,跟踪单个聊天模型调用的令牌使用情况。

# !pip install -qU langchain-community wikipedia
from langchain_community.callbacks.manager import get_openai_callback
llm = ChatOpenAI (
    model="gpt-4o-mini",
    temperature=0,
    stream_usage=True,
)
with get_openai_callback () as cb:
    result = llm.invoke ("Tell me a joke")
    print (cb)

上下文管理器中的任何内容都会被跟踪。以下是使用它按顺序跟踪多个调用的示例。

with get_openai_callback () as cb:
    result = llm.invoke ("Tell me a joke")
    result2 = llm.invoke ("Tell me a joke")
    print (cb.total_tokens)
with get_openai_callback () as cb:
    for chunk in llm.stream ("Tell me a joke"):
        pass
    print (cb)

如果使用了包含多个步骤的链或代理,它将跟踪所有这些步骤。

from langchain.agents import AgentExecutor, create_tool_calling_agent, load_tools
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages (
    [
        ("system", "You're a helpful assistant"),
        ("human", "{input}"),
        ("placeholder", "{agent_scratchpad}"),
    ]
)
tools = load_tools (["wikipedia"])
agent = create_tool_calling_agent (llm, tools, prompt)
agent_executor = AgentExecutor (agent=agent, tools=tools, verbose=True)
with get_openai_callback () as cb:
    response = agent_executor.invoke (
        {
            "input": "What's a hummingbird's scientific name and what's the fastest bird species?"
        }
    )
    print (f"Total Tokens: {cb.total_tokens}")
    print (f"Prompt Tokens: {cb.prompt_tokens}")
    print (f"Completion Tokens: {cb.completion_tokens}")
    print (f"Total Cost (USD): ${cb.total_cost}")

# 多轮对话中查看 token 最新用量

在实现多轮对话的时候,我们会将历史记录一起传入,我们需要确保上下文 token 不要超过限制 ,我们可以通过以下方式查看历史 token

def process_message (self, user_input: str) -> str:
    """处理用户输入并返回助手回复"""
    # 添加用户消息
    self.chat_history.append (HumanMessage (content=user_input))

例如 多轮交互中, {'input_tokens': 20, 'output_tokens': 11, 'total_tokens': 31} ,确保 total_tokens 不超过模型的 总容量上限

示例代码使用的 Qwen2.5 全系列支持的最大上下文长度是 128K tokens(131,072 tokens)

from typing import List, Union, Optional, Dict
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
class ChatAssistant:
    def __init__(self):
        self.chat_history: List [Union [HumanMessage, AIMessage]] = []
        self.llm = ChatOllama (
            model="qwen2.5:7b",
            base_url="http://127.0.0.1:11434/",
        )
        self.prompt = ChatPromptTemplate.from_messages ([
            ("system", "你是一个人工智能助手"),
            # 定义模板中历史消息的插入位置
            MessagesPlaceholder (variable_name="chat_history"),
            ("human", "{input}"),
        ])
        self.chain = self.prompt | self.llm
    def _get_last_usage (self) -> Optional [Dict]:
        """获取最近一次 AI 回复的用量数据"""
        #  逆序遍历对话历史(从最新到最旧),reversed () 保证优先检查最新消息
        for msg in reversed (self.chat_history):
            # 确认消息类型是 AI 回复(AIMessage),存在 usage_metadata 属性(防止属性不存在时报错)
            if isinstance (msg, AIMessage) and hasattr (msg, 'usage_metadata'):
                return msg.usage_metadata
        return None
    def process_message (self, user_input: str) -> str:
        """处理用户输入并返回助手回复"""
        # 添加用户消息
        self.chat_history.append (HumanMessage (content=user_input))
        
        try:
            # 填充模板的具体历史内容
            response = self.chain.invoke ({
                "input": user_input,
                "chat_history": self.chat_history
            })
        except Exception as e:
            return f"生成回复时出错:{str (e)}"
        
        # 添加助手回复到历史,type (response)=<class 'langchain_core.messages.ai.AIMessage'>
        self.chat_history.append (response)
        return response.content
if __name__ == "__main__":
    assistant = ChatAssistant ()
    print ("输入 exit/e 退出 | ls 查看历史 | token 查看用量")
    while True:
        user_input = input ("\n 用户:").strip ().lower ()
        if user_input in ("e", "exit"):
            break
        if user_input == "ls":
            print ("对话历史:", assistant.chat_history)
            continue
        if user_input == "token":
            if usage := assistant._get_last_usage ():
                print (f"最近用量: {usage}")
            else:
                print ("暂无用量数据")
            continue
        if not user_input:
            print ("请输入有效内容")
            continue
        response = assistant.process_message (user_input)
        print (f"助手: {response}")

# 初始化模型

许多大型语言模型应用程序允许最终用户指定他们希望应用程序使用的大模型供应商和模型。这需要编写一些逻辑,根据用户配置初始化不同的聊天模型。init_chat_model () 辅助方法使得初始化多种不同模型集成变得简单,而无需担心导入路径和类名。

# 基本用法

from langchain.chat_models import init_chat_model
# Returns a langchain_openai.ChatOpenAI instance.
gpt_4o = init_chat_model ("gpt-4o", model_provider="openai", temperature=0)
# Returns a langchain_anthropic.ChatAnthropic instance.
claude_opus = init_chat_model (
    "claude-3-opus-20240229", model_provider="anthropic", temperature=0
)
# Returns a langchain_google_vertexai.ChatVertexAI instance.
gemini_15 = init_chat_model (
    "gemini-1.5-pro", model_provider="google_vertexai", temperature=0
)
# Since all model integrations implement the ChatModel interface, you can use them in the same way.
print ("GPT-4o:" + gpt_4o.invoke ("what's your name").content + "\n")
print ("Claude Opus:" + claude_opus.invoke ("what's your name").content + "\n")
print ("Gemini 1.5:" + gemini_15.invoke ("what's your name").content + "\n")

# 推断大模型供应商

对于常见和不同的模型名称,init_chat_model () 将尝试推断大模型供应商。有关推断行为的完整列表,请参见 API 参考。例如,任何以 gpt-3... 或 gpt-4... 开头的模型将被推断为使用大模型供应商 openai。

gpt_4o = init_chat_model ("gpt-4o", temperature=0)
claude_opus = init_chat_model ("claude-3-opus-20240229", temperature=0)
gemini_15 = init_chat_model ("gemini-1.5-pro", temperature=0)

# 创建可配置模型

您还可以通过指定 configurable_fields 来创建一个运行时可配置的模型。如果您不指定 model 值,则 “model” 和 “model_provider” 将默认可配置。

configurable_model = init_chat_model (temperature=0)
configurable_model.invoke (
    "what's your name", config={"configurable": {"model": "gpt-4o"}}
)
configurable_model.invoke (
    "what's your name", config={"configurable": {"model": "claude-3-5-sonnet-20240620"}}
)

# 带有默认值的可配置模型

我们可以创建一个带有默认模型值的可配置模型,指定哪些参数是可配置的,并为可配置参数添加前缀:

first_llm = init_chat_model (
    model="gpt-4o",
    temperature=0,
    configurable_fields=("model", "model_provider", "temperature", "max_tokens"),
    config_prefix="first",  # useful when you have a chain with multiple models
)
first_llm.invoke ("what's your name")
first_llm.invoke (
    "what's your name",
    config={
        "configurable": {
            "first_model": "claude-3-5-sonnet-20240620",
            "first_temperature": 0.5,
            "first_max_tokens": 100,
        }
    },
)

# 以声明方式使用可配置模型

我们可以在可配置模型上调用声明性操作,如 bind_tools、with_structured_output、with_configurable 等,并以与常规实例化的聊天模型对象相同的方式链接可配置模型。

from pydantic import BaseModel, Field
class GetWeather (BaseModel):
    """Get the current weather in a given location"""
    location: str = Field (..., description="The city and state, e.g. San Francisco, CA")
class GetPopulation (BaseModel):
    """Get the current population in a given location"""
    location: str = Field (..., description="The city and state, e.g. San Francisco, CA")
llm = init_chat_model (temperature=0)
llm_with_tools = llm.bind_tools ([GetWeather, GetPopulation])
llm_with_tools.invoke (
    "what's bigger in 2024 LA or NYC", config={"configurable": {"model": "gpt-4o"}}
).tool_calls
llm_with_tools.invoke (
    "what's bigger in 2024 LA or NYC",
    config={"configurable": {"model": "claude-3-5-sonnet-20240620"}},
).tool_calls

# Messages (消息)

消息 是聊天模型的输入和输出。它们具有一些 内容 和一个 角色,描述消息的来源。

# 修剪消息

所有模型都有有限的上下文窗口,这意味着它们可以作为输入的令牌数量是有限的。如果您有非常长的消息或一个累积了长消息历史的链 / 代理,您需要管理传递给模型的消息长度。

trim_messages 工具提供了一些基本策略,用于将消息列表修剪为特定的令牌长度。有关所有参数的完整描述,请访问 API 参考:

# 获取最后 max_tokens 个令牌

要获取消息列表中的最后一个 max_tokens,我们可以设置 strategy="last"。请注意,对于我们的 token_counter,我们可以传入一个函数(稍后会详细说明)或一个语言模型(因为语言模型有消息令牌计数方法)。当你在修剪消息以适应特定模型的上下文窗口时,传入一个模型是有意义的:

参数 token_counter: 用于对 BaseMessage 或 BaseMessage 列表中的令牌进行计数的函数或 llm。如果传入了 BaseLanguageModel,则将使用 BaseLanguageModel.get_num_tokens_from_message()。设置为 len 以统计聊天历史记录中的消息数。

from langchain_core.messages import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    trim_messages,
)
from langchain_openai import ChatOpenAI
messages = [
    SystemMessage ("you're a good assistant, you always respond with a joke."),
    HumanMessage ("i wonder why it's called langchain"),
    AIMessage (
        'Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!'
    ),
    HumanMessage ("and who is harrison chasing anyways"),
    AIMessage (
        "Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"
    ),
    HumanMessage ("what do you call a speechless parrot"),
]
trim_messages (
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=ChatOpenAI (model="gpt-4o"),
)
[AIMessage (content="Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"),
 HumanMessage (content='what do you call a speechless parrot')]

如果我们想始终保留初始系统消息,可以指定 include_system=True:

trim_messages (
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=ChatOpenAI (model="gpt-4o"),
    include_system=True,
)
[SystemMessage (content="you're a good assistant, you always respond with a joke."),
 HumanMessage (content='what do you call a speechless parrot')]

如果我们想允许拆分消息的内容,可以指定 allow_partial=True:

trim_messages (
    messages,
    max_tokens=56,
    strategy="last",
    token_counter=ChatOpenAI (model="gpt-4o"),
    include_system=True,
    allow_partial=True,
)
[SystemMessage (content="you're a good assistant, you always respond with a joke."),
 AIMessage (content="\nWhy, he's probably chasing after the last cup of coffee in the office!"),
 HumanMessage (content='what do you call a speechless parrot')]

如果我们需要确保我们的第一条消息(不包括系统消息)始终是特定类型,可以指定 start_on:

trim_messages (
    messages,
    max_tokens=60,
    strategy="last",
    token_counter=ChatOpenAI (model="gpt-4o"),
    include_system=True,
    start_on="human",
)
[SystemMessage (content="you're a good assistant, you always respond with a joke."),
 HumanMessage (content='what do you call a speechless parrot')]

# 获取前 max_tokens 个令牌

我们可以通过指定 strategy="first" 来执行获取 前 max_tokens 的反向操作:

trim_messages (
    messages,
    max_tokens=45,
    strategy="first",
    token_counter=ChatOpenAI (model="gpt-4o"),
)
[SystemMessage (content="you're a good assistant, you always respond with a joke."),
 HumanMessage (content="i wonder why it's called langchain")]

# 自定义令牌计数器

我们可以编写一个自定义令牌计数器函数,该函数接受消息列表并返回一个整数。

from typing import List
# pip install tiktoken
import tiktoken
from langchain_core.messages import BaseMessage, ToolMessage
def str_token_counter (text: str) -> int:
    enc = tiktoken.get_encoding ("o200k_base")
    return len (enc.encode (text))
def tiktoken_counter (messages: List [BaseMessage]) -> int:
    """Approximately reproduce https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
    For simplicity only supports str Message.contents.
    """
    num_tokens = 3  # every reply is primed with <|start|>assistant<|message|>
    tokens_per_message = 3
    tokens_per_name = 1
    for msg in messages:
        if isinstance (msg, HumanMessage):
            role = "user"
        elif isinstance (msg, AIMessage):
            role = "assistant"
        elif isinstance (msg, ToolMessage):
            role = "tool"
        elif isinstance (msg, SystemMessage):
            role = "system"
        else:
            raise ValueError (f"Unsupported messages type {msg.__class__}")
        num_tokens += (
            tokens_per_message
            + str_token_counter (role)
            + str_token_counter (msg.content)
        )
        if msg.name:
            num_tokens += tokens_per_name + str_token_counter (msg.name)
    return num_tokens
trim_messages (
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=tiktoken_counter,
)

# chain

trim_messages 可以以命令式(如上所示)或声明式使用,使其易于与链中的其他组件组合

llm = ChatOpenAI (model="gpt-4o")
# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
trimmer = trim_messages (
    max_tokens=45,
    strategy="last",
    token_counter=llm,
    include_system=True,
)
chain = trimmer | llm
chain.invoke (messages)

仅从修剪器来看,我们可以看到它是一个可运行的对象,可以像所有可运行对象一样被调用:

trimmer.invoke (messages)

# 聊天消息历史

修剪消息在处理聊天历史时特别有用,因为聊天历史可能会变得非常长:

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
chat_history = InMemoryChatMessageHistory (messages=messages [:-1])
def dummy_get_session_history (session_id):
    if session_id != "1":
        return InMemoryChatMessageHistory ()
    return chat_history
llm = ChatOpenAI (model="gpt-4o")
trimmer = trim_messages (
    max_tokens=45,
    strategy="last",
    token_counter=llm,
    include_system=True,
)
chain = trimmer | llm
chain_with_history = RunnableWithMessageHistory (chain, dummy_get_session_history)
chain_with_history.invoke (
    [HumanMessage ("what do you call a speechless parrot")],
    config={"configurable": {"session_id": "1"}},
)

# 过滤消息

在更复杂的链和代理中,我们可能会通过消息列表来跟踪状态。这个列表可能会开始积累来自多个不同模型、发言者、子链等的消息,我们可能只想将这个完整消息列表的子集传递给链 / 代理中的每个模型调用。

filter_messages 工具使按类型、ID 或名称过滤消息变得简单。

# 基本用法

from langchain_core.messages import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    filter_messages,
)
messages = [
    SystemMessage ("you are a good assistant", id="1"),
    HumanMessage ("example input", id="2", name="example_user"),
    AIMessage ("example output", id="3", name="example_assistant"),
    HumanMessage ("real input", id="4", name="bob"),
    AIMessage ("real output", id="5", name="alice"),
]
>>> filter_messages (messages, include_types="human")
[HumanMessage (content='example input', name='example_user', id='2'),
 HumanMessage (content='real input', name='bob', id='4')]
>>> filter_messages (messages, exclude_names=["example_user", "example_assistant"])
[SystemMessage (content='you are a good assistant', id='1'),
 HumanMessage (content='real input', name='bob', id='4'),
 AIMessage (content='real output', name='alice', id='5')]
>>> filter_messages (messages, include_types=[HumanMessage, AIMessage], exclude_ids=["3"])
[HumanMessage (content='example input', name='example_user', id='2'),
 HumanMessage (content='real input', name='bob', id='4'),
 AIMessage (content='real output', name='alice', id='5')]

# chain

filter_messages 可以以命令式(如上所示)或声明式使用,使其易于与链中的其他组件组合:

# pip install -U langchain-anthropic
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic (model="claude-3-sonnet-20240229", temperature=0)
# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
filter_ = filter_messages (exclude_names=["example_user", "example_assistant"])
chain = filter_ | llm
>>> chain.invoke (messages)
AIMessage (content=[], response_metadata={'id': 'msg_01Wz7gBHahAwkZ1KCBNtXmwA', 'model': 'claude-3-sonnet-20240229', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 16, 'output_tokens': 3}}, id='run-b5d8a3fe-004f-4502-a071-a6c025031827-0', usage_metadata={'input_tokens': 16, 'output_tokens': 3, 'total_tokens': 19})

仅查看 filter_,我们可以看到它是一个可运行对象,可以像所有可运行对象一样被调用:

>>> filter_.invoke (messages)
[HumanMessage (content='real input', name='bob', id='4'),
 AIMessage (content='real output', name='alice', id='5')]

# 合并相同类型的连续消息

某些模型不支持传递相同类型的连续消息(即相同消息类型的 “运行”)。

merge_message_runs

pip install -qU langchain-core langchain-anthropic

# 基本用法

from langchain_core.messages import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    merge_message_runs,
)
messages = [
    SystemMessage ("you're a good assistant."),
    SystemMessage ("you always respond with a joke."),
    HumanMessage ([{"type": "text", "text": "i wonder why it's called langchain"}]),
    HumanMessage ("and who is harrison chasing anyways"),
    AIMessage (
        'Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!'
    ),
    AIMessage ("Why, he's probably chasing after the last cup of coffee in the office!"),
]
merged = merge_message_runs (messages)
>>> print ("\n\n".join ([repr (x) for x in merged]))
SystemMessage (content="you're a good assistant.\nyou always respond with a joke.", additional_kwargs={}, response_metadata={})
HumanMessage (content=[{'type': 'text', 'text': "i wonder why it's called langchain"}, 'and who is harrison chasing anyways'], additional_kwargs={}, response_metadata={})
AIMessage (content='Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!\nWhy, he\'s probably chasing after the last cup of coffee in the office!', additional_kwargs={}, response_metadata={})

请注意,如果要合并的消息内容是一个内容块的列表,则合并后的消息将包含一个内容块的列表。如果要合并的两个消息都有字符串内容,则这些内容将用换行符连接。

# chain

merge_message_runs 可以以命令式(如上所示)或声明式使用,使其易于与链中的其他组件组合:

% pip install -qU langchain-anthropic
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic (model="claude-3-sonnet-20240229", temperature=0)
# Notice we don't pass in messages. This creates
# a RunnableLambda that takes messages as input
merger = merge_message_runs ()
chain = merger | llm
>>> chain.invoke (messages)
Note: you may need to restart the kernel to use updated packages.
AIMessage (content=[], additional_kwargs={}, response_metadata={'id': 'msg_01KNGUMTuzBVfwNouLDpUMwf', 'model': 'claude-3-sonnet-20240229', 'stop_reason': 'end_turn', 'stop_sequence': None, 'usage': {'input_tokens': 84, 'output_tokens': 3}}, id='run-b908b198-9c24-450b-9749-9d4a8182937b-0', usage_metadata={'input_tokens': 84, 'output_tokens': 3, 'total_tokens': 87})

仅查看合并器,我们可以看到它是一个可运行对象,可以像所有可运行对象一样被调用:

>>> merger.invoke (messages)
[SystemMessage (content="you're a good assistant.\nyou always respond with a joke.", additional_kwargs={}, response_metadata={}),
 HumanMessage (content=[{'type': 'text', 'text': "i wonder why it's called langchain"}, 'and who is harrison chasing anyways'], additional_kwargs={}, response_metadata={}),
 AIMessage (content='Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!\nWhy, he\'s probably chasing after the last cup of coffee in the office!', additional_kwargs={}, response_metadata={})]

merge_message_runs 也可以放在提示之后:

from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate (
    [
        ("system", "You're great a {skill}"),
        ("system", "You're also great at explaining things"),
        ("human", "{query}"),
    ]
)
chain = prompt | merger | llm
chain.invoke ({"skill": "math", "query": "what's the definition of a convergent series"})

# Output parsers (输出解释器)

# 简介

输出解析器负责获取模型的输出并将其转换为更适合下游任务的格式。 在使用大型语言模型生成结构化数据或规范化聊天模型和大型语言模型的输出时非常有用。

LangChain 有许多不同类型的输出解析器。这是 LangChain 支持的输出解析器列表。下表包含各种信息:

  • 名称:输出解析器的名称
  • 支持流式处理:输出解析器是否支持流式处理。
  • 有格式说明:输出解析器是否有格式说明。通常在以下情况下可用: (a) 所需的模式未在提示中指定,而是在其他参数中(如 OpenAI 函数调用),或 (b) 当 OutputParser 包装另一个 OutputParser 时。
  • 调用 LLM: 此输出解析器是否自己调用大型语言模型。通常只有那些试图纠正格式错误输出的输出解析器才会这样做。
  • 输入类型:预期的输入类型。大多数输出解析器适用于字符串和消息,但某些(如 OpenAI 函数)需要带有特定关键字参数的消息。
  • 输出类型:解析器返回的对象的输出类型。
  • 描述:输出解析器的描述以及何时使用它。
名称支持流式处理有格式说明调用大型语言模型输入类型输出类型描述
JSONstr消息JSON 对象返回指定的 JSON 对象。您可以指定一个 Pydantic 模型,它将返回该模型的 JSON。可能是获取不使用函数调用的结构化数据的最可靠输出解析器。
XMLstr消息dict返回标签的字典。当需要 XML 输出时使用。与擅长编写 XML 的模型(如 Anthropic 的模型)一起使用。
CSVstr消息List [str]返回以逗号分隔的值的列表。
OutputFixingstr消息包装另一个输出解析器。如果该输出解析器出错,则会将错误消息和错误输出传递给大型语言模型,并请求其修复输出。
RetryWithErrorstr消息包装另一个输出解析器。如果该输出解析器出错,则会将原始输入、错误输出和错误消息传递给大型语言模型,并请求其修复。与 OutputFixingParser 相比,这个还会发送原始说明。注意元素之间的空格,统一改为一个空格
Pydanticstr消息pydantic.BaseModel接受用户定义的 Pydantic 模型,并以该格式返回数据。
YAMLstr消息pydantic.BaseModel接受用户定义的 Pydantic 模型,并以该格式返回数据。使用 YAML 进行编码。
PandasDataFramestr消息dict对于使用 pandas DataFrame 进行操作非常有用。
枚举str消息Enum将响应解析为提供的枚举值之一。
日期时间str消息datetime.datetime将响应解析为日期时间字符串。
结构化str消息Dict [str, str]一种输出解析器,返回结构化信息。它的功能不如其他输出解析器强大,因为它只允许字段为字符串。当您使用较小的 LLM 时,这可能会很有用。

# 解析响应为结构化格式

语言模型输出文本。但有时您希望获得比仅仅文本更结构化的信息。虽然一些大模型供应商支持内置方式返回结构化输出,但并非所有都支持。

输出解析器是帮助结构化语言模型响应的类。输出解析器必须实现两个主要方法:

  • "获取格式说明": 一个返回字符串的方法,包含有关语言模型输出应如何格式化的说明。
  • "解析": 一个接受字符串(假定为来自语言模型的响应)并将其解析为某种结构的方法。

然后是一个可选的方法:

  • "使用提示解析": 一个接受字符串(假定为来自语言模型的响应)和一个提示(假定为生成该响应的提示)并将其解析为某种结构的方法。提示主要是在输出解析器希望以某种方式重试或修复输出时提供的,并需要提示中的信息来做到这一点。

# 开始使用

下面我们将介绍主要的输出解析器类型, PydanticOutputParser

from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
from pydantic import BaseModel, Field, model_validator
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
# Define your desired data structure.
class Joke (BaseModel):
    setup: str = Field (description="question to set up a joke")
    punchline: str = Field (description="answer to resolve the joke")
    # You can add custom validation logic easily with Pydantic.
    @model_validator (mode="before")
    @classmethod
    def question_ends_with_question_mark (cls, values: dict) -> dict:
        setup = values ["setup"]
        if setup [-1] != "?":
            raise ValueError ("Badly formed question!")
        return values
# Set up a parser + inject instructions into the prompt template.
parser = PydanticOutputParser (pydantic_object=Joke)
prompt = PromptTemplate (
    template="Answer the user query.\n {format_instructions}\n {query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
# And a query intended to prompt a language model to populate the data structure.
prompt_and_model = prompt | model
output = prompt_and_model.invoke ({"query": "Tell me a joke."})
>>> parser.invoke (output)
Joke (setup='Why did the tomato turn red?', punchline='Because it saw the salad dressing!')

# LCEL

输出解析器实现了运行接口,这是 LangChain 表达式 (LCEL) 的基本构建块。这意味着它们支持 invoke、ainvoke、stream、astream、batch、abatch、astream_log 调用。

输出解析器接受一个字符串或 BaseMessage 作为输入,并可以返回任意类型。

我们也可以将解析器直接添加到我们的 Runnable 序列中,而不是手动调用它:

chain = prompt | model | parser
chain.invoke ({"query": "Tell me a joke."})

虽然所有解析器都支持流式接口,但只有某些解析器可以通过部分解析的对象进行流式处理,因为这高度依赖于输出类型。无法构建部分对象的解析器将简单地返回完全解析的输出。

例如,SimpleJsonOutputParser 可以通过部分输出进行流式处理,但 PydanticOutputParser 则不能

from langchain.output_parsers.json import SimpleJsonOutputParser
json_prompt = PromptTemplate.from_template (
    "Return a JSON object with an`answer`key that answers the following question: {question}"
)
json_parser = SimpleJsonOutputParser ()
json_chain = json_prompt | model | json_parser
list (json_chain.stream ({"question": "Who invented the microscope?"}))

# 解析 JSON 输出

虽然一些大模型供应商支持 内置方式返回结构化输出,但并非所有都支持。我们可以使用输出解析器帮助用户通过提示指定任意 JSON 架构,查询模型以获取符合该架构的输出,最后将该架构解析为 JSON。

JsonOutputParser 是一个内置选项,用于提示和解析 JSON 输出。虽然它的功能与 PydanticOutputParser 类似,但它还支持流式返回部分 JSON 对象。

这是一个示例,展示了如何与 Pydantic 一起使用,以方便地声明预期的模式:

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
from pydantic import BaseModel, Field
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
# Define your desired data structure.
class Joke (BaseModel):
    setup: str = Field (description="question to set up a joke")
    punchline: str = Field (description="answer to resolve the joke")
# And a query intented to prompt a language model to populate the data structure.
joke_query = "Tell me a joke."
# Set up a parser + inject instructions into the prompt template.
parser = JsonOutputParser (pydantic_object=Joke)
prompt = PromptTemplate (
    template="Answer the user query.\n {format_instructions}\n {query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
chain = prompt | model | parser
>>> chain.invoke ({"query": joke_query})
{'setup': "Why couldn't the bicycle stand up by itself?",
 'punchline': 'Because it was two tired!'}

请注意,我们将 format_instructions 从解析器直接传递到提示中。您可以并且应该尝试在提示的其他部分添加自己的格式提示,以增强或替换默认指令:

parser.get_format_instructions ()

# 流式处理

如上所述,JsonOutputParser 和 PydanticOutputParser 之间的一个关键区别是 JsonOutputParser 输出解析器支持流式部分块:

for s in chain.stream ({"query": joke_query}):
    print (s)

# 无需 Pydantic

您也可以在不使用 Pydantic 的情况下使用 JsonOutputParser。这将提示模型返回 JSON,但不会提供关于模式应是什么的具体信息。

joke_query = "Tell me a joke."
parser = JsonOutputParser ()
prompt = PromptTemplate (
    template="Answer the user query.\n {format_instructions}\n {query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
chain = prompt | model | parser
chain.invoke ({"query": joke_query})

# 解析 XML 输出

来自不同提供商的大型语言模型通常在特定数据上训练时具有不同的优势。这也意味着某些模型在生成 JSON 以外格式的输出时可能 “更好” 且更可靠。

使用前安装 pip install -qU langchain langchain-anthropic pip install defusedxml

本指南向您展示如何使用 XMLOutputParser 提示模型生成 XML 输出,然后将该输出解析为可用格式。

from langchain_core.output_parsers import XMLOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
actor_query = "Generate the shortened filmography for Tom Hanks."
output = model.invoke (
    f"""{actor_query}
Please enclose the movies in <movie></movie> tags"""
)
print (output)

这实际上效果很好!但将 XML 解析为更易于使用的格式会更好。我们可以使用 XMLOutputParser 来为提示添加默认格式说明,并将输出的 XML 解析为字典:

parser = XMLOutputParser ()
# We will add these instructions to the prompt below
parser.get_format_instructions ()
prompt = PromptTemplate (
    template="""{query}\n {format_instructions}""",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
chain = prompt | model | parser
output = chain.invoke ({"query": actor_query})
print (output)

我们还可以添加一些标签,以便根据我们的需求定制输出。您可以并且应该在提示的其他部分尝试添加自己的格式提示,以增强或替换默认说明:

parser = XMLOutputParser (tags=["movies", "actor", "film", "name", "genre"])
# We will add these instructions to the prompt below
parser.get_format_instructions ()
prompt = PromptTemplate (
    template="""{query}\n {format_instructions}""",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
chain = prompt | model | parser
output = chain.invoke ({"query": actor_query})
print (output)

这个输出解析器还支持部分块的流式处理。以下是一个示例:

for s in chain.stream ({"query": actor_query}):
    print (s)

# 解析 YAML 输出

来自不同提供商的大型语言模型通常根据其训练的特定数据具有不同的优势。这也意味着某些模型在生成 JSON 以外格式的输出时可能 “更好” 且更可靠。

此输出解析器允许用户指定任意模式,并查询大型语言模型以获取符合该模式的输出,使用 YAML 格式化其响应。

我们使用 Pydantic 和 YamlOutputParser 来声明我们的数据模型,并为模型提供更多上下文,以便生成正确类型的 YAML:

from langchain.output_parsers import YamlOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_ollama import OllamaLLM
from pydantic import BaseModel, Field
# Define your desired data structure.
class Joke (BaseModel):
    setup: str = Field (description="question to set up a joke")
    punchline: str = Field (description="answer to resolve the joke")
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
# And a query intented to prompt a language model to populate the data structure.
joke_query = "Tell me a joke."
# Set up a parser + inject instructions into the prompt template.
parser = YamlOutputParser (pydantic_object=Joke)
prompt = PromptTemplate (
    template="Answer the user query.\n {format_instructions}\n {query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
chain = prompt | model | parser
chain.invoke ({"query": joke_query})

解析器将自动解析输出的 YAML,并使用数据创建 Pydantic 模型。我们可以看到解析器的 format_instructions,这些指令会被添加到提示中:

parser.get_format_instructions ()

# 解析错误发生时重试

虽然在某些情况下,仅通过查看输出就可以修复任何解析错误,但在其他情况下则不行。一个例子是当输出不仅格式不正确,而且部分完成时。考虑下面的例子。

from langchain.output_parsers import OutputFixingParser
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from pydantic import BaseModel, Field
from langchain_ollama import OllamaLLM
template = """Based on the user question, provide an Action and Action Input for what step should be taken.
{format_instructions}
Question: {query}
Response:"""
class Action (BaseModel):
    action: str = Field (description="action to take")
    action_input: str = Field (description="input to the action")
parser = PydanticOutputParser (pydantic_object=Action)
prompt = PromptTemplate (
    template="Answer the user query.\n {format_instructions}\n {query}\n",
    input_variables=["query"],
    partial_variables={"format_instructions": parser.get_format_instructions ()},
)
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
prompt_value = prompt.format_prompt (query="who is leo di caprios gf?")

如果我们尝试按原样解析此响应,将会出现错误:

bad_response = '{"action": "search"}'
parser.parse (bad_response)

如果我们尝试使用 OutputFixingParser 来修复这个错误,它会感到困惑 - 也就是说,它不知道实际应该为 action input 放什么。

fix_parser = OutputFixingParser.from_llm (parser=parser, llm=ChatOpenAI ())
>>> fix_parser.parse (bad_response)
Action (action='search', action_input='input')

我们可以使用 RetryOutputParser,它将提示(以及原始输出)传入,以再次尝试获得更好的响应。

from langchain.output_parsers import RetryOutputParser
retry_parser = RetryOutputParser.from_llm (parser=parser, llm=OpenAI (temperature=0))
retry_parser.parse_with_prompt (bad_response, prompt_value)

我们还可以通过自定义链轻松添加 RetryOutputParser,该链将原始 LLM/ChatModel 输出转换为更可用的格式。

from langchain_core.runnables import RunnableLambda, RunnableParallel
completion_chain = prompt | OpenAI (temperature=0)
main_chain = RunnableParallel (
    completion=completion_chain, prompt_value=prompt
) | RunnableLambda (lambda x: retry_parser.parse_with_prompt (**x))
main_chain.invoke ({"query": "who is leo di caprios gf?"})

# 输出修复解析器

这个输出解析器包装了另一个输出解析器,如果第一个解析器失败,它会调用另一个大型语言模型来修复任何错误。

但我们可以做其他事情,而不仅仅是抛出错误。具体来说,我们可以将格式错误的输出和格式化的指令一起传递给模型,并要求它进行修复。

在这个例子中,我们将使用上面的 Pydantic 输出解析器。如果我们传递一个不符合模式的结果,会发生以下情况:

from typing import List
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from langchain_ollama import OllamaLLM
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
class Actor (BaseModel):
    name: str = Field (description="name of an actor")
    film_names: List [str] = Field (description="list of names of films they starred in")
actor_query = "Generate the filmography for a random actor."
parser = PydanticOutputParser (pydantic_object=Actor)
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"
parser.parse (misformatted)

现在我们可以构建并使用 OutputFixingParser。这个输出解析器接受另一个输出解析器作为参数,同时也接受一个大型语言模型(LLM),用于尝试纠正任何格式错误。

from langchain.output_parsers import OutputFixingParser
new_parser = OutputFixingParser.from_llm (parser=parser, llm=model)
new_parser.parse (misformatted)

# 自定义输出解析器

在某些情况下,您可能希望实现一个自定义解析器,以将模型输出结构化为自定义格式。

实现自定义解析器有两种方法:

  • 使用 LCEL 中的 RunnableLambda 或 RunnableGenerator -- 我们强烈推荐这种方法用于大多数用例
  • 通过从一个基础类继承来进行输出解析 -- 这是一种较为复杂的方法

这两种方法之间的区别主要是表面的,主要体现在触发的回调(例如,on_chain_start 与 on_parser_start)以及在像 LangSmith 这样的追踪平台中,如何可视化可运行的 lambda 与解析器。

# 可运行的 Lambda 和生成器

推荐的解析方式是使用 可运行的 lambda 和 可运行的生成器!

在这里,我们将进行一个简单的解析,将模型输出的大小写反转。

例如,如果模型输出:"Meow",解析器将生成 "mEOW",反转大小写。

from typing import Iterable
from langchain_ollama import OllamaLLM
from langchain_core.messages import AIMessage, AIMessageChunk
model = OllamaLLM (model="qwen2.5:7b", base_url="http://localhost:11434/")
def parse (ai_message: AIMessage) -> str:
    """Parse the AI message."""
    # return ai_message.content.swapcase ()
    return ai_message.swapcase ()
chain = model | parse
chain.invoke ("hello")

当使用 | 语法组合时,LCEL 会自动将函数 parse 升级为 RunnableLambda (parse)。如果你不喜欢这样,你可以手动导入 RunnableLambda,然后运行 parse = RunnableLambda (parse)。

此方法不能用于流式处理

如果我们想实现一个流式解析器,我们可以让解析器接受一个可迭代的输入,并生成 结果,随着它们的可用性而生成。

from langchain_core.runnables import RunnableGenerator
def streaming_parse (chunks: Iterable [AIMessageChunk]) -> Iterable [str]:
    for chunk in chunks:
        # yield chunk.content.swapcase ()
        yield chunk.swapcase ()
streaming_parse = RunnableGenerator (streaming_parse)
chain = model | streaming_parse
for chunk in chain.stream ("tell me about yourself in one sentence"):
    print (chunk, end="|", flush=True)

# 从解析基类继承

实现解析器的另一种方法是从 BaseOutputParser、BaseGenerationOutputParser 或其他基解析器继承,具体取决于你需要做什么。

一般来说,我们不推荐这种方法用于大多数用例,因为这会导致需要编写更多代码而没有显著的好处。

最简单的输出解析器类型扩展了 BaseOutputParser 类,并且必须实现以下方法:

  • parse:接收模型的字符串输出并进行解析。
  • (可选)_type:标识解析器的名称。

当聊天模型或大型语言模型的输出格式不正确时,可以抛出 OutputParserException 来指示解析失败是由于输入不良。使用此异常允许使用解析器的代码以一致的方式处理异常。

因为 BaseOutputParser 实现了 Runnable 接口,所以您以这种方式创建的任何自定义解析器将成为有效的 LangChain 可运行对象,并将受益于自动异步支持、批处理接口、日志支持等。

# 简单解析器

这是一个简单的解析器,可以解析布尔值的字符串表示(例如,YES 或 NO),并将其转换为相应的 boolean 类型。

from langchain_core.exceptions import OutputParserException
from langchain_core.output_parsers import BaseOutputParser
# The [bool] desribes a parameterization of a generic.
# It's basically indicating what the return type of parse is
# in this case the return type is either True or False
class BooleanOutputParser (BaseOutputParser [bool]):
    """Custom boolean parser."""
    true_val: str = "YES"
    false_val: str = "NO"
    def parse (self, text: str) -> bool:
        cleaned_text = text.strip ().upper ()
        if cleaned_text not in (self.true_val.upper (), self.false_val.upper ()):
            raise OutputParserException (
                f"BooleanOutputParser expected output value to either be"
                f"{self.true_val} or {self.false_val} (case-insensitive)."
                f"Received {cleaned_text}."
            )
        return cleaned_text == self.true_val.upper ()
    @property
    def _type (self) -> str:
        return "boolean_output_parser"
parser = BooleanOutputParser ()
parser.invoke ("YES")
try:
    parser.invoke ("MEOW")
except Exception as e:
    print (f"Triggered an exception of type: {type (e)}")
# 测试更改参数化
parser = BooleanOutputParser (true_val="OKAY")
parser.invoke ("OKAY")
# 确认其他 LCEL 方法是否存在
parser.batch (["OKAY", "NO"])
await parser.abatch (["OKAY", "NO"])
from langchain_anthropic.chat_models import ChatAnthropic
anthropic = ChatAnthropic (model_name="claude-2.1")
anthropic.invoke ("say OKAY or NO")
# 测试解析器是否有效
chain = anthropic | parser
chain.invoke ("say OKAY or NO")

# 解析原始模型输出

有时模型输出中除了原始文本之外还有其他重要的元数据。一个例子是工具调用,其中传递给被调用函数的参数在一个单独的属性中返回。如果您需要更细粒度的控制,可以子类化 BaseGenerationOutputParser 类。

该类需要一个单一的方法 parse_result。该方法接受原始模型输出(例如,Generation 或 ChatGeneration 的列表)并返回解析后的输出。

支持 Generation 和 ChatGeneration 使得解析器能够处理常规大型语言模型以及聊天模型。

from typing import List
from langchain_core.exceptions import OutputParserException
from langchain_core.messages import AIMessage
from langchain_core.output_parsers import BaseGenerationOutputParser
from langchain_core.outputs import ChatGeneration, Generation
class StrInvertCase (BaseGenerationOutputParser [str]):
    """An example parser that inverts the case of the characters in the message.
    This is an example parse shown just for demonstration purposes and to keep
    the example as simple as possible.
    """
    def parse_result (self, result: List [Generation], *, partial: bool = False) -> str:
        """Parse a list of model Generations into a specific format.
        Args:
            result: A list of Generations to be parsed. The Generations are assumed
                to be different candidate outputs for a single model input.
                Many parsers assume that only a single generation is passed it in.
                We will assert for that
            partial: Whether to allow partial results. This is used for parsers
                     that support streaming
        """
        if len (result) != 1:
            raise NotImplementedError (
                "This output parser can only be used with a single generation."
            )
        generation = result [0]
        if not isinstance (generation, ChatGeneration):
            # Say that this one only works with chat generations
            raise OutputParserException (
                "This output parser can only be used with a chat generation."
            )
        return generation.message.content.swapcase ()
chain = anthropic | StrInvertCase ()
chain.invoke ("Tell me a short sentence about yourself")

# Document loaders (文档加载器)

# 简介

文档加载器 负责从各种来源加载文档。这些类加载文档对象。LangChain 与各种数据源有数百个集成,可以从中加载数据:Slack、Notion、Google Drive 等。

每个 DocumentLoader 都有其特定的参数,但它们都可以通过.load 方法以相同的方式调用。

该章节的示例中,涉及文件加载,我们都会使用:

本章示例使用的嵌入模型为: [bge-m3] https://ollama.com/library/bge-m3)

from pathlib import Path
current_path = Path (__file__).resolve ().parent/ 'file_name'

# 加载 PDF

可移植文档格式 (PDF),标准化为 ISO 32000,是由 Adobe 于 1992 年开发的一种文件格式,用于以独立于应用软件、硬件和操作系统的方式呈现文档,包括文本格式和图像。

本指南涵盖如何将 PDF 文档加载到我们下游使用的 LangChain 文档格式中。

PDF 中的文本通常通过文本框表示。它们也可能包含图像。PDF 解析器可能会执行以下某种组合:

  • 通过启发式或机器学习推断将文本框聚合成行、段落和其他结构;
  • 对图像运行光学字符识别 (OCR) 以检测其中的文本;
  • 将文本分类为段落、列表、表格或其他结构;
  • 将文本结构化为表格行和列,或键值对。

LangChain 与多种 PDF 解析器集成。一些解析器简单且相对低级;其他解析器将支持 OCR 和图像处理,或执行高级文档布局分析。正确的选择将取决于您的需求。

# 简单快速的文本提取

如果您正在寻找嵌入在 PDF 中的文本的简单字符串表示,下面的方法是合适的。它将返回一个 Document 对象的列表 —— 每页一个 —— 包含文档的 page_content 属性中的页面文本的单个字符串。它不会解析图像或扫描 PDF 页面中的文本。在底层,它使用 pydf Python 库: pip install -qU pypdf

LangChain 文档加载器实现了 lazy_load 及其异步变体 alazy_load,返回 Document 对象的迭代器。我们将在下面使用这些。

示例使用的 pdf 为 大模型关键技术与应用

我们在项目根目录下新建一个目录 file,用于存放文件

from pathlib import Path
file_name = "PHP 安全问题.pdf"
file_path = Path (__file__).resolve ().parent/ 'file' /file_name
from langchain_community.document_loaders import PyPDFLoader
import asyncio
loader = PyPDFLoader (file_path)
pages = []
async def pdf_loder ():
    async for page in loader.alazy_load ():
        pages.append (page)
asyncio.run (pdf_loder ())
print (f"{pages [0].metadata}\n")
print (pages [0].page_content)

# PDF 上的向量搜索

一旦我们将 PDF 加载到 LangChain Document 对象中,我们可以以通常的方式对其进行索引(例如,RAG 应用)。下面我们使用 ollama 嵌入,尽管任何 LangChain 嵌入模型都可以。

from langchain_core.vectorstores import InMemoryVectorStore
from langchain_ollama import OllamaEmbeddings
embedding = OllamaEmbeddings (model="bge-m3:latest", base_url="http://localhost:11434/")
# 将 PDF 文档转换为向量并存储在内存中
vector_store = InMemoryVectorStore.from_documents (pages, embedding=embedding)
# 相似性搜索查询 (基于余弦相似度的向量检索方法),返回最相关的 k=2 个结果
docs = vector_store.similarity_search ("大模型当下有哪些应用?", k=2)
# 遍历结果并格式化输出,显示页码(metadata 中的 page 信息), 截取每页前 100 个字符展示
for doc in docs:
    print (f'Page {doc.metadata ["page"]}: {doc.page_content [:100]}\n')

# 布局分析和从图像中提取文本

如果您需要对文本进行更细粒度的分割(例如,分成不同的段落、标题、表格或其他结构)或需要从图像中提取文本,下面的方法是合适的。它将返回一个 Document 对象的列表,其中每个对象表示页面上的一个结构。文档的元数据存储了页码和与对象相关的其他信息(例如,在表格对象的情况下,它可能存储表格的行和列)。

在底层,它使用 langchain-unstructured 库 pip install -qU langchain-unstructured ,Unstructured 支持多个参数用于 PDF 解析:

  • strategy(例如,"fast" 或 "hi-res")
  • API 或本地处理。您需要一个 API 密钥才能使用 API。

该 hi-res 策略提供文档布局分析和 OCR 的支持。我们将在下面通过 API 演示。有关本地运行时的注意事项,请参见下面的本地解析部分。

import getpass
import os
if "UNSTRUCTURED_API_KEY" not in os.environ:
    os.environ ["UNSTRUCTURED_API_KEY"] = getpass.getpass ("Unstructured API Key:")

与之前一样,我们初始化一个加载器并懒加载文档:

from langchain_unstructured import UnstructuredLoader
loader = UnstructuredLoader (
    file_path=file_path,
    strategy="hi_res",
    partition_via_api=True,
    coordinates=True,
)
docs = []
for doc in loader.lazy_load ():
    docs.append (doc)

我们可以使用文档元数据从单个页面恢复内容:

first_page_docs = [doc for doc in docs if doc.metadata.get ("page_number") == 1]
for doc in first_page_docs:
    print (doc.page_content)

# 本地解析

本地解析需要安装额外的依赖项。我们还需要安装 unstructured PDF 附加组件: pip install -qU"unstructured [pdf]"

Poppler(PDF 分析)

  • Linux: apt-get install poppler-utils
  • Mac: brew install poppler
  • Windows:
    1. 下载: https://github.com/oschwartz10612/poppler-windows
    1. 添加环境变量:在 "系统变量" 中找到 Path,点击编辑 → 新建,添加 Poppler 的 bin 目录路径
    1. 使用 conda 安装 conda install -c conda-forge poppler

安装完成后,通过以下代码测试可用性:

from pdf2image import convert_from_path
# PDF 文件路径(确保文件存在)
from pathlib import Path
file_name = "大模型关键技术与应用.pdf"
file_path = Path (__file__).resolve ().parent/ 'file' /file_name
# 测试可用性
images = convert_from_path (pdf_path=file_path)
print (f"成功转换 {len (images)} 页")

Tesseract(OCR)

  • Linux: apt-get install tesseract-ocr
  • Mac: brew install tesseract
  • Windows:
    1. 下载: pip install tesseract -i https://pypi.tuna.tsinghua.edu.cn/simple pip install pytesseract -i https://pypi.tuna.tsinghua.edu.cn/simple
    1. 下载 (安装时建议全勾选): https://github.com/UB-Mannheim/tesseract/wiki#tesseract-installer-for-windows
    1. 将下载的 Tesseract-OCR 添加环境变量 (此处以下载到 F 根目录为例): Path 中添加 F:\Tesseract-OCR\tessdata F:\Tesseract-OCR
    1. 新建系统变量:变量名: TESSDATA_PREFIX 变量值: F:\Tesseract-OCR\tessdata
    1. 语言仓库 下载中文包 chi_sim.traineddatachi_tra.traineddataeng.traineddata,粘贴到如下文件夹内 F:\Tesseract-OCR\tessdata。
    1. 打开 Lib 下 site-packages 下 pytesseract 下的 pytesseract.py 文件 (conda 环境中 Lib\site-packages\pytesseract.py Lib\site-packages\unstructured_pytesseract\pytesseract.py ),更改 tesseract_cmd=‘tesseract’tesseract_cmd = 'F:\\Tesseract-OCR\\tesseract.exe'
    1. 测试,cmd 输入 tesseract -v tesseract --list-langs

注意:如果报错 unstructured_pytesseract.pytesseract.TesseractNotFoundError: tesseract is not installed or it's not in your PATH. See README file for more information. 需要按照第五步操作,请查看报错显示的 pytesseract.py 路径

安装完成后,使用以下代码测试可用性

import pytesseract
from PIL import Image
import subprocess
# 1. 强制指定路径,在步骤 5 中已经修改,此处无需再次指定
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
# tessdata_dir_config = '--tessdata-dir "C:\\Program Files\\Tesseract-OCR\\tessdata"'
# 验证 Tesseract 路径
try:
    print ("Tesseract 路径:", pytesseract.pytesseract.tesseract_cmd)
    # 直接调用 Tesseract 命令行
    subprocess.run ([pytesseract.pytesseract.tesseract_cmd, '--version'], check=True)
except Exception as e:
    print ("Tesseract 路径验证失败:", e)
    exit ()
# 生成测试图片
image = Image.new ('RGB', (800, 600), color='white')
image.save ('test.png')
# OCR 测试
try:
    text = pytesseract.image_to_string ('test.png', lang='chi_sim')
    print ("OCR 测试成功!输出长度:", len (text))
except Exception as e:
    print ("OCR 测试失败:", e)

我们以类似的方式使用 UnstructuredLoader,不需要 API 密钥和 partition_via_api 设置,文档列表以类似于从 API 获取的文档的方式进行处理。

from langchain_unstructured import UnstructuredLoader
loader_local = UnstructuredLoader (
    file_path=file_path,
    strategy="hi_res",
)
docs_local = []
for doc in loader_local.lazy_load ():
    docs_local.append (doc)

# 提取表格和其他结构

我们加载的每个 Document 代表一个结构,如标题、段落或表格。

某些结构可能对索引或问答任务特别感兴趣。这些结构可能是:

  • 分类以便于识别;
  • 解析为更结构化的表示。

下面,我们识别并提取一个表格: pip install -qU matplotlib PyMuPDF pillow

import fitz
import matplotlib.patches as patches
import matplotlib.pyplot as plt
from PIL import Image
def plot_pdf_with_boxes (pdf_page, segments):
    pix = pdf_page.get_pixmap ()
    pil_image = Image.frombytes ("RGB", [pix.width, pix.height], pix.samples)
    fig, ax = plt.subplots (1, figsize=(10, 10))
    ax.imshow (pil_image)
    categories = set ()
    category_to_color = {
        "Title": "orchid",
        "Image": "forestgreen",
        "Table": "tomato",
    }
    for segment in segments:
        points = segment ["coordinates"]["points"]
        layout_width = segment ["coordinates"]["layout_width"]
        layout_height = segment ["coordinates"]["layout_height"]
        scaled_points = [
            (x * pix.width/layout_width, y * pix.height/layout_height)
            for x, y in points
        ]
        box_color = category_to_color.get (segment ["category"], "deepskyblue")
        categories.add (segment ["category"])
        rect = patches.Polygon (
            scaled_points, linewidth=1, edgecolor=box_color, facecolor="none"
        )
        ax.add_patch (rect)
    # Make legend
    legend_handles = [patches.Patch (color="deepskyblue", label="Text")]
    for category in ["Title", "Image", "Table"]:
        if category in categories:
            legend_handles.append (
                patches.Patch (color=category_to_color [category], label=category)
            )
    ax.axis ("off")
    ax.legend (handles=legend_handles, loc="upper right")
    plt.tight_layout ()
    plt.show ()
def render_page (doc_list: list, page_number: int, print_text=True) -> None:
    pdf_page = fitz.open (file_path).load_page (page_number - 1)
    page_docs = [
        doc for doc in doc_list if doc.metadata.get ("page_number") == page_number
    ]
    segments = [doc.metadata for doc in page_docs]
    plot_pdf_with_boxes (pdf_page, segments)
    if print_text:
        for doc in page_docs:
            print (f"{doc.page_content}\n")
render_page (docs, 5)

请注意,尽管表格文本在文档内容中被压缩为一个字符串,但元数据包含其行和列的表示:

from IPython.display import HTML, display
segments = [
    doc.metadata
    for doc in docs
    if doc.metadata.get ("page_number") == 5 and doc.metadata.get ("category") == "Table"
]
display (HTML (segments [0]["text_as_html"]))

# 从特定部分提取文本

结构可能具有父子关系 —— 例如,一个段落可能属于一个有标题的部分。如果某个部分特别重要(例如,用于索引),我们可以隔离相应的 Document 对象。

下面,我们提取与文档的 “结论” 部分相关的所有文本:

render_page (docs, 14, print_text=False)
conclusion_docs = []
parent_id = -1
for doc in docs:
    if doc.metadata ["category"] == "Title" and "Conclusion" in doc.page_content:
        parent_id = doc.metadata ["element_id"]
    if doc.metadata.get ("parent_id") == parent_id:
        conclusion_docs.append (doc)
for doc in conclusion_docs:
    print (doc.page_content)

# 从图像中提取文本

对图像运行 OCR,从中提取文本:

render_page (docs, 11)

# 多模态模型的使用

许多现代大型语言模型支持对多模态输入(例如,图像)的推理。在某些应用中 —— 例如对具有复杂布局、图表或扫描的 PDF 进行问答 —— 跳过 PDF 解析可能是有利的,而是将 PDF 页面转换为图像并直接传递给模型。这使得模型能够对页面上的二维内容进行推理,而不是对 “单维” 字符串表示进行推理。

原则上,我们可以使用任何支持多模态输入的 LangChain 聊天模型,首先,我们定义一个简短的工具函数,将 PDF 页面转换为 base64 编码的图像: pip install -qU PyMuPDF pillow langchain-openai

import base64
import io
import fitz
from PIL import Image
def pdf_page_to_base64 (pdf_path: str, page_number: int):
    pdf_document = fitz.open (pdf_path)
    page = pdf_document.load_page (page_number - 1)  # input is one-indexed
    pix = page.get_pixmap ()
    img = Image.frombytes ("RGB", [pix.width, pix.height], pix.samples)
    buffer = io.BytesIO ()
    img.save (buffer, format="PNG")
    return base64.b64encode (buffer.getvalue ()).decode ("utf-8")
from IPython.display import Image as IPImage
from IPython.display import display
base64_image = pdf_page_to_base64 (file_path, 11)
display (IPImage (data=base64.b64decode (base64_image)))

然后我们可以以 通常的方式 查询模型。下面我们向它询问与页面上的图表相关的问题。

from langchain_openai import ChatOpenAI
llm = ChatOpenAI (model="gpt-4o-mini")
from langchain_core.messages import HumanMessage
query = "What is the name of the first step in the pipeline?"
message = HumanMessage (
    content=[
        {"type": "text", "text": query},
        {
            "type": "image_url",
            "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
        },
    ],
)
response = llm.invoke ([message])
print (response.content)

# 加载网页

本指南涵盖如何将网页加载到我们下游使用的 LangChain 文档 格式。网页包含文本、图像和其他多媒体元素,通常以 HTML 表示。它们可能包含指向其他页面或资源的链接。

LangChain 集成了一系列适合网页的解析器。合适的解析器将取决于您的需求。下面我们演示两种可能性:

  • 简单快速 解析,我们为每个网页恢复一个 Document,其内容表示为 “扁平化” 字符串;
  • 高级 解析,我们为每个页面恢复多个 Document 对象,允许识别和遍历部分、链接、表格和其他结构。

# 设置

对于 “简单快速” 解析,我们需要 langchain-community 和 beautifulsoup4 库: pip install -qU langchain-community beautifulsoup4

对于高级解析,我们将使用 langchain-unstructured: pip install -qU langchain-unstructured

# 简单快速的文本提取

如果您正在寻找嵌入在网页中的文本的简单字符串表示,下面的方法是合适的。它将返回一个 Document 对象的列表 —— 每个页面一个 —— 包含页面文本的单个字符串。在底层,它使用 beautifulsoup4 Python 库。

LangChain 文档加载器实现了 lazy_load 及其异步变体 alazy_load,返回 Document 对象的迭代器。我们将在下面使用这些。

from langchain_community.document_loaders import WebBaseLoader
import bs4
import asyncio
page_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = WebBaseLoader (web_paths=[page_url])
docs = []
async def doc_loader ():
    async for doc in loader.alazy_load ():
        docs.append (doc)
asyncio.run (doc_loader ())
assert len (docs) == 1
doc = docs [0]
print (f"{doc.metadata}\n")
print (doc.page_content [:500].strip ())

这基本上是页面 HTML 中文本的转储。它可能包含多余的信息,如标题和导航栏。如果您熟悉预期的 HTML,您可以通过 BeautifulSoup 指定所需的 <div> 类和其他参数。下面我们仅解析文章的主体文本:

from langchain_community.document_loaders import WebBaseLoader
import bs4
import asyncio
page_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = WebBaseLoader (
    web_paths=[page_url],
    bs_kwargs={
        "parse_only": bs4.SoupStrainer (class_="theme-doc-markdown markdown"),
    },
    bs_get_text_kwargs={"separator": "|", "strip": True},
)
docs = []
async def doc_loader ():
    async for doc in loader.alazy_load ():
        docs.append (doc)
asyncio.run (doc_loader ())
assert len (docs) == 1
doc = docs [0]
print (f"{doc.metadata}\n")
print (doc.page_content [:500])
print (doc.page_content [-500:])

请注意,这需要对底层 HTML 中主体文本的表示有提前的技术知识。

我们可以使用各种设置对 WebBaseLoader 进行参数化,允许指定请求头、速率限制、解析器和其他 BeautifulSoup 的关键字参数。有关详细信息,请参见其 API 参考

# 高级解析

如果我们想对页面内容进行更细粒度的控制或处理,这种方法是合适的。下面,我们不是为每个页面生成一个 Document 并通过 BeautifulSoup 控制其内容,而是生成多个 Document 对象,表示页面上的不同结构。这些结构可以包括章节标题及其对应的主体文本、列表或枚举、表格等。

在底层,它使用 langchain-unstructured 库。有关如何将 UnstructuredLangChain 一起使用的更多信息,请参见 集成文档。

from langchain_community.document_loaders import WebBaseLoader
import asyncio
from langchain_unstructured import UnstructuredLoader
page_url = "https://python.langchain.com/docs/how_to/chatbots_memory/"
loader = UnstructuredLoader (web_url=page_url)
docs = []
async def doc_loader ():
    async for doc in loader.alazy_load ():
        docs.append (doc)
asyncio.run (doc_loader ())

请注意,在没有提前了解页面 HTML 结构的情况下,我们恢复了正文文本的自然组织:

for doc in docs [:5]:
    print (doc.page_content)

# 从特定部分提取内容

每个 Document 对象代表页面的一个元素。其元数据包含有用的信息,例如其类别:

for doc in docs [:5]:
    print (f'{doc.metadata ["category"]}: {doc.page_content}')

元素之间也可能存在父子关系 —— 例如,一个段落可能属于一个有标题的部分。如果某个部分特别重要(例如,用于索引),我们可以隔离相应的 Document 对象。

作为示例,下面我们加载两个网页的 “设置” 部分的内容:

from typing import List
import asyncio
from langchain_unstructured import UnstructuredLoader
from langchain_core.documents import Document
from collections import defaultdict
async def _get_setup_docs_from_url (url: str) -> List [Document]:
    loader = UnstructuredLoader (web_url=url)
    setup_docs = []
    parent_id = -1
    async for doc in loader.alazy_load ():
        if doc.metadata ["category"] == "Title" and doc.page_content.startswith ("Setup"):
            parent_id = doc.metadata ["element_id"]
        if doc.metadata.get ("parent_id") == parent_id:
            setup_docs.append (doc)
    return setup_docs
async def _get_setup_text_from_url (page_urls: str) -> None:
    setup_docs = []
    for url in page_urls:
        page_setup_docs = await _get_setup_docs_from_url (url)
        setup_docs.extend (page_setup_docs)
    setup_text = defaultdict (str)
    for doc in setup_docs:
        url = doc.metadata ["url"]
        setup_text [url] += f"{doc.page_content}\n"
    result = dict (setup_text)
    print (result)
page_urls = [
    "https://python.langchain.com/docs/how_to/chatbots_memory/",
    "https://python.langchain.com/docs/how_to/chatbots_tools/",
]
asyncio.run (_get_setup_text_from_url (page_urls))

# 对页面内容进行向量搜索

一旦我们将页面内容加载到 LangChain Document 对象中,我们可以以通常的方式对其进行索引(例如,用于 RAG 应用)。下面我们使用 OpenAI 嵌入,尽管任何 LangChain 嵌入模型都可以。

from typing import List
import asyncio
from langchain_unstructured import UnstructuredLoader
from langchain_core.documents import Document
from collections import defaultdict
# 从给定 URL 加载文档,并筛选出与 “Setup” 相关的内容。
async def _get_setup_docs_from_url (page_urls: str) -> List [Document]:
    loader = UnstructuredLoader (web_url=page_urls)
    setup_docs = []
    parent_id = -1
    async for doc in loader.alazy_load ():
        if doc.metadata ["category"] == "Title" and doc.page_content.startswith ("Setup"):
            parent_id = doc.metadata ["element_id"]
        if doc.metadata.get ("parent_id") == parent_id:
            setup_docs.append (doc)
    return setup_docs
# 处理多个 URL,调用_get_setup_docs_from_url 获取每个 URL 的设置文档。
async def _get_setup_text_from_url (page_urls: str) -> List [Document]:
    setup_docs = []
    for url in page_urls:
        page_setup_docs = await _get_setup_docs_from_url (url)
        setup_docs.extend (page_setup_docs)
    setup_text = defaultdict (str)
    for doc in setup_docs:
        url = doc.metadata ["url"]
        setup_text [url] += f"{doc.page_content}\n"
    result = dict (setup_text)
    return setup_docs
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_ollama import OllamaEmbeddings
async def main ():
    ollama_embedding = OllamaEmbeddings (
        model="bge-m3:latest",
        base_url="http://localhost:11434/"
    )
    page_urls = [
        "https://python.langchain.com/docs/how_to/chatbots_memory/",
        "https://python.langchain.com/docs/how_to/chatbots_tools/",
    ]
    setup_docs = await _get_setup_text_from_url (page_urls)
    vector_store = InMemoryVectorStore.from_documents (setup_docs, ollama_embedding)
    retrieved_docs = vector_store.similarity_search ("Install Tavily", k=2)
    for doc in retrieved_docs:
        print (f'Page {doc.metadata ["url"]}: {doc.page_content [:300]}\n')
asyncio.run (main ())

# 其他的网页加载器

有关可用的 LangChain 网页加载器的列表,请参见 此表

# 加载 CSV 文件

CSV 文件是一个使用逗号分隔值的定界文本文件。文件的每一行都是一个数据记录。每个记录由一个或多个字段组成,字段之间用逗号分隔。

LangChain 实现了一个 CSV 加载器,可以将 CSV 文件加载为一系列 文档 对象。CSV 文件的每一行被转换为一个文档。

from langchain_community.document_loaders.csv_loader import CSVLoader
file_path = (
    "../../../docs/integrations/document_loaders/example_data/mlb_teams_2012.csv"
)
loader = CSVLoader (file_path=file_path)
data = loader.load ()
for record in data [:2]:
    print (record)

# 自定义 CSV 解析和加载

CSVLoader 将接受一个 csv_args 关键字参数,支持自定义传递给 Python 的 csv.DictReader 的参数。有关支持的 csv 参数的更多信息,请参见 csv 模块 文档。

loader = CSVLoader (
    file_path=file_path,
    csv_args={
        "delimiter": ",",
        "quotechar": '"',
        "fieldnames": ["MLB Team", "Payroll in millions", "Wins"],
    },
)
data = loader.load ()
for record in data [:2]:
    print (record)

指定一个列以识别文档来源

可以使用 CSV 的一列设置 Document 元数据中的 "source" 键。使用 source_column 参数指定从每一行创建的文档的来源。否则,将使用 file_path 作为从 CSV 文件创建的所有文档的来源。

当使用从 CSV 文件加载的文档进行回答问题的链时,这非常有用。

loader = CSVLoader (file_path=file_path, source_column="Team")
data = loader.load ()
for record in data [:2]:
    print (record)

# 从字符串加载

在直接处理 CSV 字符串时,可以使用 Python 的 tempfile。

import tempfile
from io import StringIO
string_data = """
"Team", "Payroll (millions)", "Wins"
"Nationals",     81.34, 98
"Reds",          82.20, 97
"Yankees",      197.96, 95
"Giants",       117.62, 94
""".strip ()
with tempfile.NamedTemporaryFile (delete=False, mode="w+") as temp_file:
    temp_file.write (string_data)
    temp_file_path = temp_file.name
loader = CSVLoader (file_path=temp_file_path)
loader.load ()
for record in data [:2]:
    print (record)

# 从目录加载文档

在开始前,建议安装 Python-magic 库,该库用于检测和识别文件类型。它使用 libmagic 库,可以根据文件的内容确定文件的类型,而不是根据文件的扩展名来确定文件类型:

pip install python-magic-bin
pip install python-magic

LangChain 的 DirectoryLoader 实现了从磁盘读取文件到 LangChain Document 对象的功能。这里我们演示:

  • 如何从文件系统加载,包括使用通配符模式;
  • 如何使用多线程进行文件 I/O;
  • 如何使用自定义加载器类解析特定文件类型(例如,代码);
  • 如何处理错误,例如由于解码引起的错误。

DirectoryLoader 接受一个 loader_cls 关键字参数,默认为 UnstructuredLoaderUnstructured 支持解析多种格式,如 PDF 和 HTML。在这里我们用它来读取一个 markdown (.md) 文件。

DirectoryLoader:

  • path (str) – 目录的路径。
  • glob (List [str] | 元组 [str] | str)– 用于查找文件的 glob 模式或 glob 模式列表。 默认为 “**/[!.]*“ (除隐藏文件外的所有文件)。

我们可以使用 glob 参数来控制加载哪些文件。请注意,这里不会加载 .rst 文件或 .html 文件。

from langchain_community.document_loaders import DirectoryLoader
# '.' 当前目录  '../' 父目录
loader = DirectoryLoader (".", glob="**/*.md")
docs = loader.load ()
len (docs)
print (docs [0].page_content [:100])

# 指定加载器

使用 DirectoryLoader 从目录加载时,可以指定加载器,例如:加载目录下的 md 文件,指定使用 Markdown 加载器

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Document
from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader
from pathlib import Path
current_path = Path (__file__).resolve ().parent 
# 使用 DirectoryLoader 并指定 Markdown 加载器
loader = DirectoryLoader (
    path=current_path,  # 要扫描的根目录
    glob="**/*.md",    # 匹配所有子目录中的.md 文件
    loader_cls=UnstructuredMarkdownLoader,  # 指定使用 Markdown 加载器
    recursive=True      # 启用递归扫描(确保处理子目录)
)
# 加载所有匹配的文档
docs = loader.load ()
# 验证结果
print (f"共加载 {len (docs)} 个文档")
for doc in docs:
    assert isinstance (doc, Document) # 类型验证,断言 doc 为 Document 类型,否则报错,与下方注释代码意义一致
    # if not isinstance (doc, Document):
    #     raise ValueError (f"无效的文档类型: {type (doc)}")
    print (doc.page_content [:100] + "...")  # 打印每个文档的前 100 字符

# 显示进度条

默认情况下不会显示进度条。要显示进度条,请安装 tqdm 库(例如 pip install tqdm),并将 show_progress 参数设置为 True。

loader = DirectoryLoader ("../", glob="**/*.md", show_progress=True)
docs = loader.load ()

# 使用多线程

默认情况下加载在一个线程中进行。为了利用多个线程,请将 use_multithreading 标志设置为 true。

loader = DirectoryLoader ("../", glob="**/*.md", use_multithreading=True)
docs = loader.load ()

# 更改加载器类

默认情况下使用 UnstructuredLoader 类。要自定义加载器,请在 loader_cls 关键字参数中指定加载器类。下面我们展示一个使用 TextLoader 的示例:

from langchain_community.document_loaders import TextLoader
loader = DirectoryLoader ("../", glob="**/*.md", loader_cls=TextLoader)
docs = loader.load ()
print (docs [0].page_content [:100])

请注意,虽然 UnstructuredLoader 解析 Markdown 头部,TextLoader 则不解析。

如果您需要加载 Python 源代码文件,请使用 PythonLoader:

from langchain_community.document_loaders import PythonLoader
loader = DirectoryLoader ("../../../../../", glob="**/*.py", loader_cls=PythonLoader)

# 使用 TextLoader 自动检测文件编码

DirectoryLoader 可以帮助管理由于文件编码差异而导致的错误。下面我们将尝试加载一组文件,其中一个文件包含非 UTF8 编码。

path = "../../../../libs/langchain/tests/unit_tests/examples/"
loader = DirectoryLoader (path, glob="**/*.txt", loader_cls=TextLoader)

# A. 默认行为

默认情况下,我们会引发一个错误:

>>> loader.load ()
Error loading file ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt

文件 example-non-utf8.txt 使用了不同的编码,因此 load () 函数失败,并提供了一个有用的消息,指示哪个文件解码失败。

在 TextLoader 的默认行为下,任何文档加载失败都会导致整个加载过程失败,且没有文档被加载。

# B. 静默失败

我们可以将参数 silent_errors 传递给 DirectoryLoader,以跳过无法加载的文件并继续加载过程。

loader = DirectoryLoader (
    path, glob="**/*.txt", loader_cls=TextLoader, silent_errors=True
)
>>> docs = loader.load ()
Error loading file ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt: Error loading ../../../../libs/langchain/tests/unit_tests/examples/example-non-utf8.txt
doc_sources = [doc.metadata ["source"] for doc in docs]
doc_sources

# C. 自动检测编码

我们还可以要求 TextLoader 在失败之前自动检测文件编码,通过将 autodetect_encoding 传递给加载器类。

text_loader_kwargs = {"autodetect_encoding": True}
loader = DirectoryLoader (
    path, glob="**/*.txt", loader_cls=TextLoader, loader_kwargs=text_loader_kwargs
)
docs = loader.load ()
doc_sources = [doc.metadata ["source"] for doc in docs]
doc_sources

# 加载 HTML

超文本标记语言或 HTML 是为在网页浏览器中显示的文档设计的标准标记语言。

这部分介绍如何将 HTML 文档加载到 LangChain Document 对象中,以便我们在后续使用。

解析 HTML 文件通常需要专门的工具。在这里,我们演示了通过 UnstructuredBeautifulSoup4 进行解析,这些工具可以通过 pip 安装。

# 使用 Unstructured 加载 HTML

pip install unstructured

from langchain_community.document_loaders import UnstructuredHTMLLoader
file_path = "../../docs/integrations/document_loaders/example_data/fake-content.html"
loader = UnstructuredHTMLLoader (file_path)
data = loader.load ()
print (data)

# 使用 BeautifulSoup4 加载 HTML

我们还可以使用 BeautifulSoup4 通过 BSHTMLLoader 加载 HTML 文档。这将从 HTML 中提取文本到 page_content,并将页面标题作为 title 提取到 metadata。

pip install bs4

from langchain_community.document_loaders import BSHTMLLoader
loader = BSHTMLLoader (file_path)
data = loader.load ()
print (data)

# 加载 JSON

JSON (JavaScript 对象表示法) 是一种开放标准文件格式和数据交换格式,使用人类可读的文本来存储和传输由属性 - 值对和数组(或其他可序列化值)组成的数据对象。

JSON Lines 是一种文件格式,其中每一行都是一个有效的 JSON 值。

LangChain 实现了一个 JSONLoader 用于将 JSON 和 JSONL 数据转换为 LangChain 文档 对象。 它使用指定的 jq schem 来解析 JSON 文件,允许将特定字段提取到内容 和 LangChain 文档的元数据中。

它使用 jq python 包。查看这个 手册 以获取 jq 语法的详细文档。 pip install jq

在这里我们将演示:

如何将 JSON 和 JSONL 数据加载到 LangChain Document 的内容中;
如何将 JSON 和 JSONL 数据加载到与 Document 相关的元数据中。

from langchain_community.document_loaders import JSONLoader
import json
from pathlib import Path
from pprint import pprint
file_path='./example_data/facebook_chat.json'
data = json.loads (Path (file_path).read_text ())
print (data)

# 使用 JSONLoader

假设我们想提取 JSON 数据中 messages 键下 content 字段的值。这可以通过下面的 JSONLoader 轻松完成。

# JSON 文件

loader = JSONLoader (
    file_path='./example_data/facebook_chat.json',
    jq_schema='.messages [].content',
    text_content=False)
data = loader.load ()
pprint (data)

# JSON Lines 文件

如果您想从 JSON Lines 文件加载文档,请传递 json_lines=True 并指定 jq_schema 以从单个 JSON 对象中提取 page_content。

file_path = './example_data/facebook_chat_messages.jsonl'
pprint (Path (file_path).read_text ())
loader = JSONLoader (
    file_path='./example_data/facebook_chat_messages.jsonl',
    jq_schema='.content',
    text_content=False,
    json_lines=True)
data = loader.load ()
print (data)

另一个选项是设置 jq_schema='.' 并提供 content_key:

loader = JSONLoader (
    file_path='./example_data/facebook_chat_messages.jsonl',
    jq_schema='.',
    content_key='sender_name',
    json_lines=True)
data = loader.load ()
print (data)

# 带有 jq schema content_key 的 JSON 文件

要使用 jq schema 中的 content_key 从 JSON 文件加载文档,请设置 is_content_key_jq_parsable=True。 确保 content_key 兼容并可以使用 jq schema 进行解析。

file_path = './sample.json'
print (Path (file_path).read_text ())
loader = JSONLoader (
    file_path=file_path,
    jq_schema=".data []",
    content_key=".attributes.message",
    is_content_key_jq_parsable=True,
)
data = loader.load ()
print (data)

# 提取元数据

通常,我们希望将 JSON 文件中可用的元数据包含到我们从内容创建的文档中。

以下演示了如何使用 JSONLoader 提取元数据。

需要注意一些关键变化。在之前的示例中,我们没有收集元数据,我们直接在模式中指定了 page_content 的值可以从哪里提取。

.messages [].content

在当前示例中,我们必须告诉加载器遍历 messages 字段中的记录。jq_schema 必须是: .messages []

这允许我们将记录(字典)传递给必须实现的 metadata_func。metadata_func 负责识别记录中哪些信息应包含在最终 Document 对象中存储的元数据中。

此外,我们现在必须通过 content_key 参数在加载器中明确指定记录中提取 page_content 值的键。

def metadata_func (record: dict, metadata: dict) -> dict:
    metadata ["sender_name"] = record.get ("sender_name")
    metadata ["timestamp_ms"] = record.get ("timestamp_ms")
    return metadata
loader = JSONLoader (
    file_path='./example_data/facebook_chat.json',
    jq_schema='.messages []',
    content_key="content",
    metadata_func=metadata_func
)
data = loader.load ()

# metadata_func

如上所示,metadata_func 接受由 JSONLoader 生成的默认元数据。这使用户能够完全控制元数据的格式。

例如,默认元数据包含 source 和 seq_num 键。然而,JSON 数据中也可能包含这些键。用户可以利用 metadata_func 来重命名默认键,并使用 JSON 数据中的键。

下面的示例展示了我们如何修改 source 以仅包含相对于 langchain 目录的文件源信息。

def metadata_func (record: dict, metadata: dict) -> dict:
    metadata ["sender_name"] = record.get ("sender_name")
    metadata ["timestamp_ms"] = record.get ("timestamp_ms")
    if "source" in metadata:
        source = metadata ["source"].split ("/")
        source = source [source.index ("langchain"):]
        metadata ["source"] = "/".join (source)
    return metadata
loader = JSONLoader (
    file_path='./example_data/facebook_chat.json',
    jq_schema='.messages []',
    content_key="content",
    metadata_func=metadata_func
)
data = loader.load ()

# 常见的 JSON 结构与 jq 模式

下面的列表提供了用户可以使用的可能的 jq_schema 参考,以根据结构从 JSON 数据中提取内容。

JSON        -> [{"text": ...}, {"text": ...}, {"text": ...}]
jq_schema   -> ".[].text"
JSON        -> {"key": [{"text": ...}, {"text": ...}, {"text": ...}]}
jq_schema   -> ".key [].text"
JSON        -> ["...", "...", "..."]
jq_schema   -> ".[]"

# 加载 Markdown

Markdown 是一种轻量级标记语言,用于使用纯文本编辑器创建格式化文本。

在这里,我们介绍如何将 Markdown 文档加载到 LangChain 文档 对象中,以便我们可以在后续使用。

我们将涵盖:

基本用法;
将 Markdown 解析为标题、列表项和文本等元素。
LangChain 实现了一个 UnstructuredMarkdownLoader 对象,该对象需要 Unstructured 包。首先,我们安装它: pip install"unstructured [md]"nltk

基本用法将会把一个 Markdown 文件导入为一个单一文档。这里我们展示 LangChain 的自述文件:

from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_core.documents import Document
markdown_path = "../../../README.md"
loader = UnstructuredMarkdownLoader (markdown_path)
data = loader.load ()
assert len (data) == 1
assert isinstance (data [0], Document)
readme_content = data [0].page_content
print (readme_content [:250])

# 保留元素

在底层,Unstructured 为不同的文本块创建不同的 “元素”。默认情况下,我们将这些元素组合在一起,但您可以通过指定 mode="elements" 轻松保持这种分离。

loader = UnstructuredMarkdownLoader (markdown_path, mode="elements")
data = loader.load ()
print (f"Number of documents: {len (data)}\n")
for document in data [:2]:
    print (f"{document}\n")

请注意,在这种情况下,我们恢复了三种不同的元素类型:

print (set (document.metadata ["category"] for document in data))

# 加载 Microsoft Office 文件

Microsoft Office 办公软件套件包括 Microsoft Word、Microsoft Excel、Microsoft PowerPoint、Microsoft Outlook 和 Microsoft OneNote。它可用于 Microsoft Windows 和 macOS 操作系统,也可在 Android 和 iOS 上使用。

这涵盖了如何将常用文件格式,包括 DOCX、XLSX 和 PPTX 文档加载到 LangChain 中。 文档 对象,我们可以在下游使用。

# 使用 AzureAIDocumentIntelligenceLoader 加载 DOCX、XLSX、PPTX

Azure AI Document Intelligence(前称 Azure Form Recognizer)是基于机器学习的 服务,能够从数字或扫描的 PDF、图像、Office 和 HTML 文件中提取文本(包括手写)、表格、文档结构(例如标题、章节标题等)和键值对。 文档智能支持 PDF、JPEG/JPG、PNG、BMP、TIFF、HEIF、DOCX、XLSX、PPTX 和 HTML。

这个使用 Document Intelligence 的加载器的 当前实现 可以逐页整合内容并将其转换为 LangChain 文档。默认输出格式为 markdown,可以与 MarkdownHeaderTextSplitter 轻松链式处理以进行语义文档分块。您还可以使用 mode="single" 或 mode="page" 返回单页或按页分割的纯文本。

# 前提条件

在以下三个预览区域之一创建一个 Azure AI 文档智能资源:东部美国、西部美国 2、西欧 - 如果您没有,请按照 此文档 创建一个。您将把 和 作为参数传递给加载器。

from langchain_community.document_loaders import AzureAIDocumentIntelligenceLoader
file_path = "<filepath>"
endpoint = "<endpoint>"
key = "<key>"
loader = AzureAIDocumentIntelligenceLoader (
    api_endpoint=endpoint, api_key=key, file_path=file_path, api_model="prebuilt-layout"
)
documents = loader.load ()

# 自定义文档加载器

# 概述

基于大型语言模型(LLMs)的应用程序通常涉及从数据库或文件(如 PDF)中提取数据,并将其转换为 LLMs 可以使用的格式。在 LangChain 中,这通常涉及创建文档对象(Document),它封装了提取的文本(page_content)以及元数据 —— 一个包含有关文档的详细信息的字典,例如作者的姓名或出版日期。

Document 对象通常被格式化为提示词,输入到 LLM 中,使 LLM 能够使用 Document 中的信息生成所需的响应(例如,总结文档)。 Documents 可以立即使用,也可以索引到向量存储中以便将来检索和使用。

文档加载的主要抽象是:

组件描述
文档包含文本和元数据
基础加载器用于将原始数据转换为 Documents
Blob二进制数据的表示,位于文件或内存中
BaseBlobParser解析 Blob 的逻辑,以生成 Document 对象

本指南将演示如何编写自定义文档加载和文件解析逻辑;具体来说,我们将看到如何:

  • 通过从 BaseLoader 子类化来创建标准文档加载器。
  • 使用 BaseBlobParser 创建解析器,并将其与 Blob 和 BlobLoaders 一起使用。这在处理文件时特别有用。

# 标准文档加载器

文档加载器可以通过从 BaseLoader 子类化来实现,后者提供了加载文档的标准接口。

# 接口

方法名称说明
lazy_load用于懒加载文档,一次加载一个。用于生产代码。
alazy_loadlazy_load 的异步变体
load用于急加载所有文档到内存中。用于原型设计或交互式工作。
aload用于急加载所有文档到内存中。用于原型设计或交互式工作。
  • load 方法是一个便利方法,仅用于原型设计工作 -- 它只是调用 list (self.lazy_load ())。
  • alazy_load 有一个默认实现,将委托给 lazy_load。如果您使用异步,我们建议覆盖默认实现并提供原生异步实现。

实现文档加载器时不要通过 lazy_load 或 alazy_load 方法提供参数。
所有配置预计通过初始化器 (init) 传递。这是 LangChain 做出的设计选择,以确保一旦实例化文档加载器,它就拥有加载文档所需的所有信息。

# 实现

让我们创建一个标准文档加载器的示例,该加载器加载一个文件并从文件中的每一行创建一个文档。

from typing import AsyncIterator, Iterator
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
class CustomDocumentLoader (BaseLoader):
    """An example document loader that reads a file line by line."""
    def __init__(self, file_path: str) -> None:
        """Initialize the loader with a file path.
        Args:
            file_path: The path to the file to load.
        """
        self.file_path = file_path
    def lazy_load (self) -> Iterator [Document]:  # <-- Does not take any arguments
        """A lazy loader that reads a file line by line.
        When you're implementing lazy load methods, you should use a generator
        to yield documents one by one.
        """
        with open (self.file_path, encoding="utf-8") as f:
            line_number = 0
            for line in f:
                yield Document (
                    page_content=line,
                    metadata={"line_number": line_number, "source": self.file_path},
                )
                line_number += 1
    # alazy_load is OPTIONAL.
    # If you leave out the implementation, a default implementation which delegates to lazy_load will be used!
    async def alazy_load (
        self,
    ) -> AsyncIterator [Document]:  # <-- Does not take any arguments
        """An async lazy loader that reads a file line by line."""
        # Requires aiofiles
        # Install with `pip install aiofiles`
        # https://github.com/Tinche/aiofiles
        import aiofiles
        async with aiofiles.open (self.file_path, encoding="utf-8") as f:
            line_number = 0
            async for line in f:
                yield Document (
                    page_content=line,
                    metadata={"line_number": line_number, "source": self.file_path},
                )
                line_number += 1

# 测试

为了测试文档加载器,我们需要一个包含内容的文件。

with open ("./meow.txt", "w", encoding="utf-8") as f:
    quality_content = "meow meow🐱 \n meow meow🐱 \n meow😻😻"
    f.write (quality_content)
loader = CustomDocumentLoader ("./meow.txt")
## Test out the lazy load interface
for doc in loader.lazy_load ():
    print ()
    print (type (doc))
    print (doc)
## Test out the async implementation
async for doc in loader.alazy_load ():
    print ()
    print (type (doc))
    print (doc)
loader.load ()

# 处理文件

许多文档加载器涉及解析文件。这些加载器之间的区别通常源于文件的解析方式,而不是文件的加载方式。例如,您可以使用 open 来读取 PDF 或 markdown 文件的二进制内容,但您需要不同的解析逻辑将该二进制数据转换为文本。

因此,将解析逻辑与加载逻辑解耦可能会很有帮助,这使得无论数据是如何加载的,都更容易重用给定的解析器。

# BaseBlobParser

BaseBlobParser 是一个接口,接受一个 blob 并输出一个 Document 对象的列表。blob 是一种数据表示,存在于内存中或文件中。LangChain python 有一个 Blob 原语,详情请查看 Blob WebAPI 规范

from langchain_core.document_loaders import BaseBlobParser, Blob
class MyParser (BaseBlobParser):
    """A simple parser that creates a document from each line."""
    def lazy_parse (self, blob: Blob) -> Iterator [Document]:
        """Parse a blob into a document line by line."""
        line_number = 0
        with blob.as_bytes_io () as f:
            for line in f:
                line_number += 1
                yield Document (
                    page_content=line,
                    metadata={"line_number": line_number, "source": blob.source},
                )
blob = Blob.from_path ("./meow.txt")
parser = MyParser ()
list (parser.lazy_parse (blob))

使用 blob API 还允许直接从内存加载内容,而无需从文件中读取!

blob = Blob (data=b"some data from memory\nmeow")
list (parser.lazy_parse (blob))

# Blob

让我们快速浏览一下 Blob API。

blob = Blob.from_path ("./meow.txt", metadata={"foo": "bar"})
>>> blob.encoding
'utf-8'
>>> blob.as_bytes ()
b'meow meow\xf0\x9f\x90\xb1 \n meow meow\xf0\x9f\x90\xb1 \n meow\xf0\x9f\x98\xbb\xf0\x9f\x98\xbb'
>>> blob.as_string ()
'meow meow🐱 \n meow meow🐱 \n meow😻😻'
>>> blob.as_bytes_io ()
<contextlib._GeneratorContextManager at 0x743f34324450>
>>> blob.metadata
{'foo': 'bar'}
>>> blob.source
'./meow.txt'

# Blob 加载器

虽然解析器封装了将二进制数据解析为文档所需的逻辑,但 blob 加载器 封装了从给定存储位置加载 blobs 所需的逻辑。

目前,LangChain 仅支持 FileSystemBlobLoader。

您可以使用 FileSystemBlobLoader 加载 blobs,然后使用解析器对其进行解析。

from langchain_community.document_loaders.blob_loaders import FileSystemBlobLoader
blob_loader = FileSystemBlobLoader (path=".", glob="*.mdx", show_progress=True)
parser = MyParser ()
for blob in blob_loader.yield_blobs ():
    for doc in parser.lazy_parse (blob):
        print (doc)
        break

# 通用加载器

LangChain 具有一个 GenericLoader 抽象,它将 BlobLoader 与 BaseBlobParser 组合在一起。

GenericLoader 旨在提供标准化的类方法,使使用现有的 BlobLoader 实现变得简单。目前,仅支持 FileSystemBlobLoader。

from langchain_community.document_loaders.generic import GenericLoader
loader = GenericLoader.from_filesystem (
    path=".", glob="*.mdx", show_progress=True, parser=MyParser ()
)
for idx, doc in enumerate (loader.lazy_load ()):
    if idx < 5:
        print (doc)
print ("... output truncated for demo purposes")

自定义通用加载器:

如果你真的喜欢创建类,你可以继承并创建一个类来封装逻辑。

你可以从这个类继承,以使用现有的加载器加载内容。

from typing import Any
class MyCustomLoader (GenericLoader):
    @staticmethod
    def get_parser (**kwargs: Any) -> BaseBlobParser:
        """Override this method to associate a default parser with the class."""
        return MyParser ()
loader = MyCustomLoader.from_filesystem (path=".", glob="*.mdx", show_progress=True)
for idx, doc in enumerate (loader.lazy_load ()):
    if idx < 5:
        print (doc)
print ("... output truncated for demo purposes")

# Text splitters (文本分割器)

# 简介

一旦加载了文档,您通常会希望对其进行转换,以更好地适应您的应用程序。最简单的例子是,您可能希望将一份长文档拆分成可以适应模型上下文窗口的小块。LangChain 提供了许多内置的文档转换器,使得拆分、组合、过滤和其他操作文档变得简单。

当您想处理长文本时,有必要将文本拆分成块。尽管这听起来很简单,但这里有很多潜在的复杂性。理想情况下,您希望将语义相关的文本片段放在一起。“语义相关” 意味着什么可能取决于文本的类型。这个笔记本展示了几种实现方法。

从高层次来看,文本分割器的工作原理如下:

  1. 将文本拆分成小的、语义上有意义的块(通常是句子)。
  2. 开始将这些小块组合成一个更大的块,直到达到某个大小(通过某个函数来衡量)。
  3. 一旦达到该大小,将该块作为独立的文本片段,然后开始创建一个新的文本块,并保持一些重叠(以保持块之间的上下文)。

这意味着您可以在两个不同的轴上自定义您的文本分割器:

  1. 文本的拆分方式
  2. 块大小的测量方式

# 递归分割文本

这个文本分割器是推荐用于通用文本的。它通过字符列表进行参数化。它尝试按顺序在这些字符上进行分割,直到块的大小足够小。默认列表是 ['\n\n', '\n', ' ', '']。这会尽量保持所有段落(然后是句子,再然后是单词)在一起,因为这些通常被认为是语义上最相关的文本片段。

  1. 文本是如何分割的:按字符列表。
  2. 块大小是如何测量的:按字符数。

下面我们展示示例用法。

要直接获取字符串内容,请使用 .split_text。

要创建 LangChain 文档 对象(例如,用于下游任务),请使用 .create_documents。

pip install -qU langchain-text-splitters

from langchain_text_splitters import RecursiveCharacterTextSplitter
# Load example document
with open ("state_of_the_union.txt") as f:
    state_of_the_union = f.read ()
text_splitter = RecursiveCharacterTextSplitter (
    # Set a really small chunk size, just to show.
    chunk_size=100,
    chunk_overlap=20,
    length_function=len,
    is_separator_regex=False,
)
texts = text_splitter.create_documents ([state_of_the_union])
print (texts [0])
print (texts [1])
text_splitter.split_text (state_of_the_union)[:2]

让我们来看看上面为 RecursiveCharacterTextSplitter 设置的参数:

  • chunk_size: 每个块的最大大小,大小由 length_function 决定。
  • chunk_overlap: 块之间的目标重叠。重叠的块有助于减轻在块之间划分上下文时信息的丢失。
  • length_function: 确定块大小的函数。
  • is_separator_regex: 分隔符列表(默认为 ['\n\n', '\n', ' ', ''])是否应被解释为正则表达式。

# 从没有词边界的语言中分割文本

某些书写系统没有 词边界,例如中文、日文和泰文。使用默认的分隔符列表 ['\n\n', '\n', ' ', ''] 分割文本可能会导致单词在块之间被拆分。为了保持单词在一起,您可以覆盖分隔符列表以包含额外的标点符号:

  • 添加 ASCII 句号 "."、Unicode 全角 句号 "."(用于中文文本)和 表意全角句号 "。"(用于日文和中文)
  • 添加在泰文、缅甸文、柬文和日文中使用的 零宽空格。
  • 添加 ASCII 逗号 ","、Unicode 全角逗号 "," 和 Unicode 表意逗号 "、"
text_splitter = RecursiveCharacterTextSplitter (
    separators=[
        "\n\n",
        "\n",
        " ",
        ".",
        ",",
        "\u200b",  # Zero-width space
        "\uff0c",  # Fullwidth comma
        "\u3001",  # Ideographic comma
        "\uff0e",  # Fullwidth full stop
        "\u3002",  # Ideographic full stop
        "",
    ],
    # Existing args
)

# 分割 HTML

# 按 HTML 头部进行分割

HTMLHeaderTextSplitter 是一个 “结构感知” 的分块器,它在 HTML 元素级别分割文本,并为每个与给定块 “相关” 的头部添加元数据。它可以逐个元素返回块,或将具有相同元数据的元素组合在一起,目的是 (a) 保持相关文本在语义上(或多或少)分组,以及 (b) 保留文档结构中编码的丰富上下文信息。它可以与其他文本分割器一起使用,作为分块管道的一部分。

它类似于用于 markdown 文件的 MarkdownHeaderTextSplitter。

要指定要分割的头部,请在实例化 HTMLHeaderTextSplitter 时指定 headers_to_split_on,如下所示。

# 分割 HTML 字符串

pip install -qU langchain-text-splitters

from langchain_text_splitters import HTMLHeaderTextSplitter
html_string = """
<!DOCTYPE html>
<html>
<body>
    <div>
        <h1>Foo</h1>
        <p>Some intro text about Foo.</p>
        <div>
            <h2>Bar main section</h2>
            <p>Some intro text about Bar.</p>
            <h3>Bar subsection 1</h3>
            <p>Some text about the first subtopic of Bar.</p>
            <h3>Bar subsection 2</h3>
            <p>Some text about the second subtopic of Bar.</p>
        </div>
        <div>
            <h2>Baz</h2>
            <p>Some text about Baz</p>
        </div>
        <br>
        <p>Some concluding text about Foo</p>
    </div>
</body>
</html>
"""
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
]
html_splitter = HTMLHeaderTextSplitter (headers_to_split_on)
html_header_splits = html_splitter.split_text (html_string)
html_header_splits

要将每个元素与其相关的标题一起返回,请在实例化 HTMLHeaderTextSplitter 时指定 return_each_element=True:

html_splitter = HTMLHeaderTextSplitter (
    headers_to_split_on,
    return_each_element=True,
)
html_header_splits_elements = html_splitter.split_text (html_string)

与上述内容相比,元素按其标题聚合:

for element in html_header_splits [:2]:
    print (element)

现在每个元素作为一个独立的 Document 返回:

for element in html_header_splits_elements [:3]:
    print (element)

从 URL 或 HTML 文件中拆分:

要直接从 URL 读取,请将 URL 字符串传递给 split_text_from_url 方法。

同样,可以将本地 HTML 文件传递给 split_text_from_file 方法。

url = "https://plato.stanford.edu/entries/goedel/"
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
    ("h4", "Header 4"),
]
html_splitter = HTMLHeaderTextSplitter (headers_to_split_on)
# for local file use html_splitter.split_text_from_file (<path_to_file>)
html_header_splits = html_splitter.split_text_from_url (url)

# 限制块大小

HTMLHeaderTextSplitter 根据 HTML 标题进行拆分,可以与另一个根据字符长度限制拆分的拆分器组合,例如 RecursiveCharacterTextSplitter。

这可以通过第二个拆分器的 .split_documents 方法完成:

from langchain_text_splitters import RecursiveCharacterTextSplitter
chunk_size = 500
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter (
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)
# Split
splits = text_splitter.split_documents (html_header_splits)
splits [80:85]

# 限制

不同的 HTML 文档之间可能存在相当大的结构变化,虽然 HTMLHeaderTextSplitter 会尝试将所有 “相关” 的标题附加到任何给定的块上,但有时它可能会遗漏某些标题。例如,该算法假设存在一个信息层次结构,其中标题总是在与其相关文本 “上方” 的节点上,即前兄弟、祖先及其组合。在以下新闻文章中(截至本文撰写时),文档的结构使得顶级标题的文本虽然标记为 “h1”,但与我们期望它 “上方” 的文本元素处于不同的子树中 —— 因此我们可以观察到 “h1” 元素及其相关文本未出现在块元数据中(但在适用的情况下,我们确实看到了 “h2” 及其相关文本):

url = "https://www.cnn.com/2023/09/25/weather/el-nino-winter-us-climate/index.html"
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
]
html_splitter = HTMLHeaderTextSplitter (headers_to_split_on)
html_header_splits = html_splitter.split_text_from_url (url)
print (html_header_splits [1].page_content [:500])

# 按 HTML 部分进行分割

与 HTMLHeaderTextSplitter 的概念类似,HTMLSectionSplitter 是一个 “结构感知” 的分块器,它在元素级别分割文本,并为与任何给定块 “相关” 的每个标题添加元数据。

它可以逐个返回块,或将具有相同元数据的元素组合在一起,目的是 (a) 保持相关文本在语义上(或多或少)分组,以及 (b) 保留编码在文档结构中的丰富上下文信息。

使用 xslt_path 提供一个绝对路径来转换 HTML,以便它可以根据提供的标签检测部分。默认情况下使用 data_connection/document_transformers 目录中的 converting_to_header.xslt 文件。这是为了将 html 转换为更容易检测部分的格式 / 布局。例如,基于字体大小的 span 可以转换为标题标签,以便被检测为一个部分。

# 分割 HTML 字符串

from langchain_text_splitters import HTMLSectionSplitter
html_string = """
    <!DOCTYPE html>
    <html>
    <body>
        <div>
            <h1>Foo</h1>
            <p>Some intro text about Foo.</p>
            <div>
                <h2>Bar main section</h2>
                <p>Some intro text about Bar.</p>
                <h3>Bar subsection 1</h3>
                <p>Some text about the first subtopic of Bar.</p>
                <h3>Bar subsection 2</h3>
                <p>Some text about the second subtopic of Bar.</p>
            </div>
            <div>
                <h2>Baz</h2>
                <p>Some text about Baz</p>
            </div>
            <br>
            <p>Some concluding text about Foo</p>
        </div>
    </body>
    </html>
"""
headers_to_split_on = [("h1", "Header 1"), ("h2", "Header 2")]
html_splitter = HTMLSectionSplitter (headers_to_split_on)
html_header_splits = html_splitter.split_text (html_string)
html_header_splits

# 限制块大小

HTMLSectionSplitter 可以与其他文本分割器一起使用,作为分块管道的一部分。内部,当节的大小大于块的大小时,它使用 RecursiveCharacterTextSplitter。它还考虑文本的字体大小,以根据确定的字体大小阈值来判断它是否为一个节。

from langchain_text_splitters import RecursiveCharacterTextSplitter
html_string = """
    <!DOCTYPE html>
    <html>
    <body>
        <div>
            <h1>Foo</h1>
            <p>Some intro text about Foo.</p>
            <div>
                <h2>Bar main section</h2>
                <p>Some intro text about Bar.</p>
                <h3>Bar subsection 1</h3>
                <p>Some text about the first subtopic of Bar.</p>
                <h3>Bar subsection 2</h3>
                <p>Some text about the second subtopic of Bar.</p>
            </div>
            <div>
                <h2>Baz</h2>
                <p>Some text about Baz</p>
            </div>
            <br>
            <p>Some concluding text about Foo</p>
        </div>
    </body>
    </html>
"""
headers_to_split_on = [
    ("h1", "Header 1"),
    ("h2", "Header 2"),
    ("h3", "Header 3"),
    ("h4", "Header 4"),
]
html_splitter = HTMLSectionSplitter (headers_to_split_on)
html_header_splits = html_splitter.split_text (html_string)
chunk_size = 500
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter (
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)
# Split
splits = text_splitter.split_documents (html_header_splits)
splits

# 按字符分割

这是最简单的方法。它基于给定的字符序列进行分割,默认值为 "\n\n"。块长度以字符数来衡量。

  1. 文本是如何分割的:按单个字符分隔符。
  2. 块大小是如何衡量的:按字符数。

要直接获取字符串内容,请使用.split_text。

要创建 LangChain 文档 对象(例如,用于下游任务),请使用.create_documents。

pip install -qU langchain-text-splitters

from langchain_text_splitters import CharacterTextSplitter
# Load an example document
with open ("state_of_the_union.txt") as f:
    state_of_the_union = f.read ()
text_splitter = CharacterTextSplitter (
    separator="\n\n",
    chunk_size=1000,
    chunk_overlap=200,
    length_function=len,
    is_separator_regex=False,
)
texts = text_splitter.create_documents ([state_of_the_union])
print (texts [0])

使用 .create_documents 将与每个文档相关的元数据传播到输出块:

metadatas = [{"document": 1}, {"document": 2}]
documents = text_splitter.create_documents (
    [state_of_the_union, state_of_the_union], metadatas=metadatas
)
print (documents [0])

使用 .split_text 直接获取字符串内容: text_splitter.split_text (state_of_the_union)[0]

# 分割代码

递归字符文本分割器 包含用于在特定编程语言中分割文本的预构建分隔符列表。

支持的语言存储在 langchain_text_splitters.Language 枚举中。它们包括:

"cpp",
"go",
"java",
"kotlin",
"js",
"ts",
"php",
"proto",
"python",
"rst",
"ruby",
"rust",
"scala",
"swift",
"markdown",
"latex",
"html",
"sol",
"csharp",
"cobol",
"c",
"lua",
"perl",
"haskell"

要查看给定语言的分隔符列表,请将此枚举中的值传入: RecursiveCharacterTextSplitter.get_separators_for_language

要实例化一个针对特定语言的分割器,请将枚举中的值传入: RecursiveCharacterTextSplitter.from_language

下面我们展示了各种语言的示例。请确保已经安装了 langchain-text-splitters pip install -qU langchain-text-splitters

from langchain_text_splitters import (
    Language,
    RecursiveCharacterTextSplitter,
)

要查看支持的语言的完整列表: [e.value for e in Language]

您还可以查看给定语言使用的分隔符: RecursiveCharacterTextSplitter.get_separators_for_language (Language.PYTHON)

# Python

这是一个使用 PythonTextSplitter 的示例:

PYTHON_CODE = """
def hello_world ():
    print ("Hello, World!")
# Call the function
hello_world ()
"""
python_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
python_docs = python_splitter.create_documents ([PYTHON_CODE])
python_docs

# JS

JS_CODE = """
function helloWorld () {
  console.log ("Hello, World!");
}
// Call the function
helloWorld ();
"""
js_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.JS, chunk_size=60, chunk_overlap=0
)
js_docs = js_splitter.create_documents ([JS_CODE])
js_docs

# TS

TS_CODE = """
function helloWorld (): void {
  console.log ("Hello, World!");
}
// Call the function
helloWorld ();
"""
ts_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.TS, chunk_size=60, chunk_overlap=0
)
ts_docs = ts_splitter.create_documents ([TS_CODE])
ts_docs

# Markdown

markdown_text = """
# 🦜️🔗 LangChain
⚡ Building applications with LLMs through composability ⚡
## Quick Install
# Hopefully this code block isn't split
pip install langchain
As an open-source project in a rapidly developing field, we are extremely open to contributions.
"""
md_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.MARKDOWN, chunk_size=60, chunk_overlap=0
)
md_docs = md_splitter.create_documents ([markdown_text])
md_docs

# Latex

latex_text = """
\documentclass {article}
\begin {document}
\maketitle
\section {Introduction}
Large language models (LLMs) are a type of machine learning model that can be trained on vast amounts of text data to generate human-like language. In recent years, LLMs have made significant advances in a variety of natural language processing tasks, including language translation, text generation, and sentiment analysis.
\subsection {History of LLMs}
The earliest LLMs were developed in the 1980s and 1990s, but they were limited by the amount of data that could be processed and the computational power available at the time. In the past decade, however, advances in hardware and software have made it possible to train LLMs on massive datasets, leading to significant improvements in performance.
\subsection {Applications of LLMs}
LLMs have many applications in industry, including chatbots, content creation, and virtual assistants. They can also be used in academia for research in linguistics, psychology, and computational linguistics.
\end {document}
"""
latex_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.MARKDOWN, chunk_size=60, chunk_overlap=0
)
latex_docs = latex_splitter.create_documents ([latex_text])
latex_docs

# HTML

html_text = """
<!DOCTYPE html>
<html>
    <head>
        <title>🦜️🔗 LangChain</title>
        <style>
            body {
                font-family: Arial, sans-serif;
            }
            h1 {
                color: darkblue;
            }
        </style>
    </head>
    <body>
        <div>
            <h1>🦜️🔗 LangChain</h1>
            <p>⚡ Building applications with LLMs through composability ⚡</p>
        </div>
        <div>
            As an open-source project in a rapidly developing field, we are extremely open to contributions.
        </div>
    </body>
</html>
"""
html_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.HTML, chunk_size=60, chunk_overlap=0
)
html_docs = html_splitter.create_documents ([html_text])
html_docs

# Solidity

SOL_CODE = """
pragma solidity ^0.8.20;
contract HelloWorld {
   function add (uint a, uint b) pure public returns (uint) {
       return a + b;
   }
}
"""
sol_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.SOL, chunk_size=128, chunk_overlap=0
)
sol_docs = sol_splitter.create_documents ([SOL_CODE])
sol_docs

# C#

C_CODE = """
using System;
class Program
{
    static void Main ()
    {
        int age = 30; // Change the age value as needed
        // Categorize the age without any console output
        if (age < 18)
        {
            // Age is under 18
        }
        else if (age >= 18 && age < 65)
        {
            // Age is an adult
        }
        else
        {
            // Age is a senior citizen
        }
    }
}
"""
c_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.CSHARP, chunk_size=128, chunk_overlap=0
)
c_docs = c_splitter.create_documents ([C_CODE])
c_docs

# Haskell

HASKELL_CODE = """
main :: IO ()
main = do
    putStrLn "Hello, World!"
-- Some sample functions
add :: Int -> Int -> Int
add x y = x + y
"""
haskell_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.HASKELL, chunk_size=50, chunk_overlap=0
)
haskell_docs = haskell_splitter.create_documents ([HASKELL_CODE])
haskell_docs

# PHP

PHP_CODE = """<?php
namespace foo;
class Hello {
    public function __construct () { }
}
function hello () {
    echo "Hello World!";
}
interface Human {
    public function breath ();
}
trait Foo { }
enum Color
{
    case Red;
    case Blue;
}"""
php_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.PHP, chunk_size=50, chunk_overlap=0
)
php_docs = php_splitter.create_documents ([PHP_CODE])
php_docs

# PowerShell

POWERSHELL_CODE = """
$directoryPath = Get-Location
$items = Get-ChildItem -Path $directoryPath
$files = $items | Where-Object { -not $_.PSIsContainer }
$sortedFiles = $files | Sort-Object LastWriteTime
foreach ($file in $sortedFiles) {
    Write-Output ("Name:" + $file.Name + "| Last Write Time:" + $file.LastWriteTime)
}
"""
powershell_splitter = RecursiveCharacterTextSplitter.from_language (
    language=Language.POWERSHELL, chunk_size=100, chunk_overlap=0
)
powershell_docs = powershell_splitter.create_documents ([POWERSHELL_CODE])
powershell_docs

# 按标题分割 Markdown

许多聊天或问答应用在嵌入和向量存储之前涉及对输入文档进行分块。

这些笔记 来自 Pinecone,提供了一些有用的提示:

When a full paragraph or document is embedded, the embedding process considers both the overall context and the relationships between the sentences and phrases within the text. This can result in a more comprehensive vector representation that captures the broader meaning and themes of the text.

如前所述,分块通常旨在将具有共同上下文的文本保持在一起。考虑到这一点,我们可能希望特别尊重文档本身的结构。例如,markdown 文件是通过标题组织的。在特定标题组内创建块是一个直观的想法。为了解决这个挑战,我们可以使用 MarkdownHeaderTextSplitter。这将根据指定的标题集拆分 markdown 文件。

例如,如果我们想要拆分这个 markdown:

md = '# Foo\n\n ## Bar\n\nHi this is Jim  \nHi this is Joe\n\n ## Baz\n\n Hi this is Molly'

我们可以指定要拆分的标题:

[("#", "Header 1"),("##", "Header 2")]

内容按共同标题分组或拆分:

{'content': 'Hi this is Jim  \nHi this is Joe', 'metadata': {'Header 1': 'Foo', 'Header 2': 'Bar'}}
{'content': 'Hi this is Molly', 'metadata': {'Header 1': 'Foo', 'Header 2': 'Baz'}}

# 基本用法

开始之前,请确保你已经安装 pip install -qU langchain-text-splitters

from langchain_text_splitters import MarkdownHeaderTextSplitter
markdown_document = "# Foo\n\n    ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo \n\n Hi this is Lance \n\n ## Baz\n\n Hi this is Molly"
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
    ("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter (headers_to_split_on)
md_header_splits = markdown_splitter.split_text (markdown_document)
md_header_splits
type (md_header_splits [0])

默认情况下,MarkdownHeaderTextSplitter 会从输出块的内容中剥离正在拆分的标题。通过设置 strip_headers = False 可以禁用此功能。

markdown_splitter = MarkdownHeaderTextSplitter (headers_to_split_on, strip_headers=False)
md_header_splits = markdown_splitter.split_text (markdown_document)
md_header_splits

# 将 Markdown 行作为单独的文档返回

默认情况下,MarkdownHeaderTextSplitter 根据 headers_to_split_on 中指定的标题聚合行。我们可以通过指定 return_each_line 来禁用此功能:

markdown_splitter = MarkdownHeaderTextSplitter (
    headers_to_split_on,
    return_each_line=True,
)
md_header_splits = markdown_splitter.split_text (markdown_document)
md_header_splits

请注意,这里每个文档的 metadata 中保留了标题信息。

# 限制块大小

在每个 markdown 组内,我们可以应用任何我们想要的文本分割器,例如 RecursiveCharacterTextSplitter,它允许进一步控制块大小。

markdown_document = "# Intro \n\n    ## History \n\n Markdown [9] is a lightweight markup language for creating formatted text using a plain-text editor. John Gruber created Markdown in 2004 as a markup language that is appealing to human readers in its source code form.[9] \n\n Markdown is widely used in blogging, instant messaging, online forums, collaborative software, documentation pages, and readme files. \n\n ## Rise and divergence \n\n As Markdown popularity grew rapidly, many Markdown implementations appeared, driven mostly by the need for \n\n additional features such as tables, footnotes, definition lists,[note 1] and Markdown inside HTML blocks. \n\n #### Standardization \n\n From 2012, a group of people, including Jeff Atwood and John MacFarlane, launched what Atwood characterised as a standardisation effort. \n\n ## Implementations \n\n Implementations of Markdown are available for over a dozen programming languages."
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"),
]
# MD splits
markdown_splitter = MarkdownHeaderTextSplitter (
    headers_to_split_on=headers_to_split_on, strip_headers=False
)
md_header_splits = markdown_splitter.split_text (markdown_document)
# Char-level splits
from langchain_text_splitters import RecursiveCharacterTextSplitter
chunk_size = 250
chunk_overlap = 30
text_splitter = RecursiveCharacterTextSplitter (
    chunk_size=chunk_size, chunk_overlap=chunk_overlap
)
# Split
splits = text_splitter.split_documents (md_header_splits)
splits

# 分割 JSON 数据

这个 JSON 分割器在允许控制块大小的同时分割 JSON 数据。它深度优先遍历 JSON 数据并构建较小的 JSON 块。它尝试保持嵌套的 JSON 对象完整,但如果需要,会将其分割,以保持块在 min_chunk_size 和 max_chunk_size 之间。

如果值不是嵌套的 JSON,而是一个非常大的字符串,则该字符串不会被分割。如果您需要对块大小进行严格限制,可以考虑将其与递归文本分割器组合使用。还有一个可选的预处理步骤,可以通过先将列表转换为 JSON(字典)然后再进行分割来分割列表。

  1. 文本如何分割:JSON 值。
  2. 块大小如何测量:按字符数。

请确保你安装了 pip install -qU langchain-text-splitters ,首先我们加载一些 json 数据:

import json
import requests
# This is a large nested json object and will be loaded as a python dict
json_data = requests.get ("https://api.smith.langchain.com/openapi.json").json ()

# 基本用法

指定 max_chunk_size 来限制块大小:

from langchain_text_splitters import RecursiveJsonSplitter
splitter = RecursiveJsonSplitter (max_chunk_size=300)

要获取 json 块,请使用 .split_json 方法:

# Recursively split json data - If you need to access/manipulate the smaller json chunks
json_chunks = splitter.split_json (json_data=json_data)
for chunk in json_chunks [:3]:
    print (chunk)

要获取 LangChain 文档 对象,请使用 .create_documents 方法:

# The splitter can also output documents
docs = splitter.create_documents (texts=[json_data])
for doc in docs [:3]:
    print (doc)

或者使用 .split_text 直接获取字符串内容:

texts = splitter.split_text (json_data=json_data)
print (texts [0])
print (texts [1])

# 管理列表内容的块大小

请注意,这个示例中的一个块大于指定的 max_chunk_size 300。查看这个较大的块,我们看到里面有一个列表对象:

print ([len (text) for text in texts][:10])
print ()
print (texts [3])
[171, 231, 126, 469, 210, 213, 237, 271, 191, 232]
{"paths": {"/api/v1/sessions/{session_id}": {"get": {"parameters": [{"name": "session_id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid", "title": "Session Id"}}, {"name": "include_stats", "in": "query", "required": false, "schema": {"type": "boolean", "default": false, "title": "Include Stats"}}, {"name": "accept", "in": "header", "required": false, "schema": {"anyOf": [{"type": "string"}, {"type": "null"}], "title": "Accept"}}]}}}}

默认情况下,json 分割器不会分割列表。

指定 convert_lists=True 来预处理 json,将列表内容转换为 index:item 形式的字典,作为 key:val 对:

texts = splitter.split_text (json_data=json_data, convert_lists=True)

让我们看看块的大小。现在它们都在最大值之下

>>> print ([len (text) for text in texts][:10])
[176, 236, 141, 203, 212, 221, 210, 213, 242, 291]

列表已被转换为字典,但即使分成多个块,仍保留所有所需的上下文信息:

>>> print (texts [1])
{"paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": {"0": "tracer-sessions"}, "summary": "Read Tracer Session", "description": "Get a specific session.", "operationId": "read_tracer_session_api_v1_sessions__session_id__get"}}}}
# We can also look at the documents
>>> docs [1]
Document (page_content='{"paths": {"/api/v1/sessions/{session_id}": {"get": {"tags": ["tracer-sessions"], "summary": "Read Tracer Session", "description": "Get a specific session.", "operationId": "read_tracer_session_api_v1_sessions__session_id__get"}}}}')

# 根据语义相似性分割文本

摘自 Greg Kamradt 的精选笔记: 5_Levels_Of_Text_Splitting

本指南涵盖如何根据语义相似性分割块。如果嵌入足够远离,则会进行分割。

从高层次来看,这将文本分割为句子,然后分组为 3 个句子一组。 然后合并在嵌入空间中相似的句子。

# 安装依赖

使用前请安装依赖 pip install --quiet langchain_experimental langchain_openai

# 加载示例数据

# This is a long document we can split up.
with open ("state_of_the_union.txt") as f:
    state_of_the_union = f.read ()

# 创建文本分割器

要实例化一个 SemanticChunker,我们必须指定一个嵌入模型。下面我们将使用 OpenAIEmbeddings。

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
text_splitter = SemanticChunker (OpenAIEmbeddings ())

# 分割文本

我们以通常的方式分割文本,例如,通过调用 .create_documents 来创建 LangChain 文档 对象:

docs = text_splitter.create_documents ([state_of_the_union])
print (docs [0].page_content)

# 断点

这个分割器通过确定何时 “断开” 句子来工作。这是通过查看任何两个句子之间的嵌入差异来完成的。当该差异超过某个阈值时,它们就会被分割。

有几种方法可以确定该阈值,这些方法由 breakpoint_threshold_type 关键字参数控制。

# 百分位

默认的分割方式是基于百分位。在这种方法中,计算句子之间的所有差异,然后将任何大于 X 百分位的差异进行分割。

text_splitter = SemanticChunker (
    OpenAIEmbeddings (), breakpoint_threshold_type="percentile"
)
docs = text_splitter.create_documents ([state_of_the_union])
print (docs [0].page_content)
>>> print (len (docs))
26

# 标准差

在此方法中,任何大于 X 个标准差的差异都会被拆分。

text_splitter = SemanticChunker (
    OpenAIEmbeddings (), breakpoint_threshold_type="standard_deviation"
)
docs = text_splitter.create_documents ([state_of_the_union])
print (docs [0].page_content)
>>> print (len (docs))
4

# 四分位数

在此方法中,使用四分位距来拆分块。

text_splitter = SemanticChunker (
    OpenAIEmbeddings (), breakpoint_threshold_type="interquartile"
)
docs = text_splitter.create_documents ([state_of_the_union])
print (docs [0].page_content)
>>> print (len (docs))
25

# 梯度

在此方法中,使用距离的梯度与百分位法一起拆分块。 当块之间高度相关或特定于某个领域(例如法律或医学)时,此方法非常有用。其思想是对梯度数组应用异常检测,以便分布变得更宽,从而更容易识别高度语义数据中的边界。

text_splitter = SemanticChunker (
    OpenAIEmbeddings (), breakpoint_threshold_type="gradient"
)
docs = text_splitter.create_documents ([state_of_the_union])
print (docs [0].page_content)
>>> print (len (docs))
26

# Embedding models (嵌入模型)

嵌入模型创建文本片段的向量表示。您可以将向量视为一个数字数组,它捕捉了文本的语义含义。 通过这种方式表示文本,您可以执行数学运算,从而进行诸如搜索其他在意义上最相似的文本等操作。 这些自然语言搜索能力支撑着许多类型的上下文检索, 在这里,我们为大型语言模型提供其有效响应查询所需的相关数据。

Embeddings 类是一个用于与文本嵌入模型接口的类。存在许多不同的嵌入大模型供应商(OpenAI、Cohere、Hugging Face 等)和本地模型,此类旨在为它们提供标准接口。

LangChain 中的基础嵌入类提供了两种方法:一种用于嵌入文档,另一种用于嵌入查询。前者接受多个文本作为输入,而后者接受单个文本。将这两者作为两个单独的方法的原因是某些嵌入大模型供应商对文档(待搜索的内容)和查询(搜索查询本身)有不同的嵌入方法。

# 文本嵌入模型

Embeddings 类是一个用于与文本嵌入模型接口的类。有很多嵌入大模型供应商(OpenAI、Cohere、Hugging Face 等) - 这个类旨在为它们提供一个标准接口。

嵌入会创建一段文本的向量表示。这是有用的,因为这意味着我们可以在向量空间中思考文本,并进行语义搜索,寻找在向量空间中最相似的文本片段。

LangChain 中的基础 Embeddings 类提供了两个方法:一个用于嵌入文档,一个用于嵌入查询。前者,.embed_documents,接受多个文本作为输入,而后者,.embed_query,接受单个文本。将这两个方法分开是因为某些嵌入大模型供应商对文档(待搜索的内容)和查询(搜索查询本身)有不同的嵌入方法。 .embed_query 将返回一个浮点数列表,而 .embed_documents 返回一个浮点数列表的列表。

# 开始使用

要开始,我们需要安装 OpenAI 合作伙伴包: pip install langchain-openai

访问 API 需要一个 API 密钥,您可以通过创建一个帐户并前往这里来获取。一旦我们有了密钥,我们将通过运行将其设置为环境变量: export OPENAI_API_KEY="..."

如果您不想设置环境变量,可以在初始化 OpenAI LLM 类时通过 api_key 命名参数直接传递密钥:

from langchain_openai import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings (api_key="...")

否则,您可以在没有任何参数的情况下进行初始化:

from langchain_openai import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings ()

# embed_documents

嵌入文本列表,使用 .embed_documents 嵌入字符串列表,返回嵌入列表:

embeddings = embeddings_model.embed_documents (
    [
        "Hi there!",
        "Oh, hello!",
        "What's your name?",
        "My friends call me World",
        "Hello World!"
    ]
)
len (embeddings), len (embeddings [0])

# embed_query

嵌入单个查询,使用 .embed_query 嵌入单个文本(例如,用于与其他嵌入文本进行比较)。

embedded_query = embeddings_model.embed_query ("What was the name mentioned in the conversation?")
embedded_query [:5]

# 缓存嵌入结果

嵌入可以存储或临时缓存,以避免需要重新计算它们。

缓存嵌入可以使用 CacheBackedEmbeddings 来完成。缓存支持的嵌入器是一个包装器,它在一个键值存储中缓存嵌入。 文本被哈希处理,哈希值用作缓存中的键。

初始化 CacheBackedEmbeddings 的主要支持方式是 from_bytes_store。它接受以下参数:

  • underlying_embedder: 用于嵌入的嵌入器。
  • document_embedding_cache: 用于缓存文档嵌入的任何 ByteStore。
    batch_size: (可选,默认为 None)在存储更新之间要嵌入的文档数量。
    namespace: (可选,默认为 ``)用于文档缓存的命名空间。此命名空间用于避免与其他缓存的冲突。例如,将其设置为所使用的嵌入模型的名称。
    query_embedding_cache: (可选,默认为 None 或不缓存)用于缓存查询嵌入的 ByteStore,或 True 以使用与 document_embedding_cache 相同的存储。

注意:

  • 确保设置 namespace 参数,以避免使用不同嵌入模型嵌入相同文本时发生冲突。
  • CacheBackedEmbeddings 默认不缓存查询嵌入。要启用查询缓存,需要指定 query_embedding_cache。 from langchain.embeddings import CacheBackedEmbeddings

# 与向量存储一起使用

首先,让我们看一个使用本地文件系统存储嵌入并使用 FAISS 向量存储进行检索的示例。 pip install --upgrade --quiet langchain-openai faiss-cpu

from langchain.storage import LocalFileStore
from langchain_community.document_loaders import TextLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import CharacterTextSplitter
underlying_embeddings = OpenAIEmbeddings ()
store = LocalFileStore ("./cache/")
cached_embedder = CacheBackedEmbeddings.from_bytes_store (
    underlying_embeddings, store, namespace=underlying_embeddings.model
)

在嵌入之前缓存是空的: list (store.yield_keys ())

加载文档,将其拆分为块,嵌入每个块并将其加载到向量存储中。

raw_documents = TextLoader ("state_of_the_union.txt").load ()
text_splitter = CharacterTextSplitter (chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents (raw_documents)

创建向量存储:

%% time
db = FAISS.from_documents (documents, cached_embedder)

如果我们尝试再次创建向量存储,它会快得多,因为不需要重新计算任何嵌入。

%% time
db2 = FAISS.from_documents (documents, cached_embedder)

这里是一些创建的嵌入: list (store.yield_keys ())[:5]

# 交换 ByteStore

为了使用不同的 ByteStore,只需在创建 CacheBackedEmbeddings 时使用它。下面,我们创建一个等效的缓存嵌入对象,但使用非持久的 InMemoryByteStore:

from langchain.embeddings import CacheBackedEmbeddings
from langchain.storage import InMemoryByteStore
store = InMemoryByteStore ()
cached_embedder = CacheBackedEmbeddings.from_bytes_store (
    underlying_embeddings, store, namespace=underlying_embeddings.model
)

# Vector stores (向量存储)

存储和搜索非结构化数据的最常见方法之一是将其嵌入并存储生成的嵌入向量, 然后在查询时嵌入非结构化查询并检索与嵌入查询 ' 最相似 ' 的嵌入向量。 向量存储负责为您存储嵌入数据并执行向量搜索。

大多数向量存储还可以存储有关嵌入向量的元数据,并支持在相似性搜索之前对该元数据进行过滤, 让您对返回的文档有更多控制。

向量存储可以通过以下方式转换为检索器接口:

vectorstore = MyVectorStore ()
retriever = vectorstore.as_retriever ()

存储和搜索非结构化数据的最常见方法之一是将其嵌入并存储生成的嵌入向量, 然后在查询时嵌入非结构化查询并检索与嵌入查询 “最相似” 的嵌入向量。 向量存储负责存储嵌入数据并执行向量搜索, 为您处理这些。

# 初始化

在使用向量存储之前,我们需要加载一些数据并进行文档分割

from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from pathlib import Path
# 获取当前文件所在目录路径
current_path = Path (__file__).resolve ().parent 
# 1. 加载 Markdown 文档,使用 DirectoryLoader 并指定 Markdown 加载器
loader = DirectoryLoader (
    path=current_path,  # 要扫描的根目录
    glob="**/*.md",    # 匹配所有子目录中的.md 文件
    loader_cls=UnstructuredMarkdownLoader,  # 指定使用 Markdown 加载器
    recursive=True,      # 启用递归扫描(确保处理子目录)
    show_progress=True  # 添加加载进度显示
)
raw_documents = loader.load ()
# 2. 配置文本分割器(推荐调整参数优化 Markdown 处理)
text_splitter = CharacterTextSplitter (
    chunk_size=1000,     # 每个分块的最大字符数
    chunk_overlap=200,   # 推荐设置重叠(保留上下文)
    separator="\n\n##", # 优先按二级标题分割
)
# 3. 执行文档分割
documents = text_splitter.split_documents (raw_documents)

# 设置向量数据库

有许多优秀的向量存储选项,以下是一些免费的、开源的,并且完全在本地机器上运行的选项。

# Chroma

使用 chroma 向量数据库,它作为库在本地机器上运行: pip install langchain-chroma

from langchain_chroma import Chroma
db = Chroma.from_documents (documents, OpenAIEmbeddings ())

# FAISS

使用 FAISS 向量数据库,它利用了 Facebook AI 相似性搜索 (FAISS) 库: pip install faiss-cpu

from langchain_community.vectorstores import FAISS
db = FAISS.from_documents (documents, OpenAIEmbeddings ())

# Lance

使用与基于 Lance 数据格式的 LanceDB 向量数据库相关的功能: pip install lancedb

from langchain_community.vectorstores import LanceDB
import lancedb
db = lancedb.connect ("/tmp/lancedb")
table = db.create_table (
    "my_table",
    data=[
        {
            "vector": embeddings.embed_query ("Hello World"),
            "text": "Hello World",
            "id": "1",
        }
    ],
    mode="overwrite",
)
db = LanceDB.from_documents (documents, OpenAIEmbeddings ())

# Milvus

使用 Milvus 向量数据库,它作为库在本地机器上运行: pip install pymilvus milvus

兼容性问题:降低版本: pip install grpcio-status==1.67.1 pip install grpcio==1.67.1
使用 pipdeptree ( pip install pipdeptree ) 查看完整的依赖树,windows: pipdeptree | findstr grpcio

要创建本地的 Milvus 向量数据库,只需实例化一个 MilvusClient ,指定一个存储所有数据的文件名,如 "milvus_demo.db"。

from pymilvus import MilvusClient
client = MilvusClient ("milvus_demo.db")

milvus_lite 目前支持以下环境:

  • Ubuntu>=20.04
  • MacOS>=11.0
  • 如果是 Windows,需要尝试使用在线 milvus 或通过 docker 部署完整的 milvus

详情可参考 官方文档

# 相似性搜索

所有向量存储都暴露了 similarity_search 方法。 这将接收传入的文档,创建它们的嵌入,然后找到所有具有最相似嵌入的文档。

from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_ollama import OllamaEmbeddings
from pathlib import Path
# 获取当前文件所在目录路径
current_path = Path (__file__).resolve ().parent 
# 1. 加载 Markdown 文档,使用 DirectoryLoader 并指定 Markdown 加载器
loader = DirectoryLoader (
    path=current_path,  # 要扫描的根目录
    glob="**/*.md",    # 匹配所有子目录中的.md 文件
    loader_cls=UnstructuredMarkdownLoader,  # 指定使用 Markdown 加载器
    recursive=True,      # 启用递归扫描(确保处理子目录)
    show_progress=True  # 添加加载进度显示
)
raw_documents = loader.load ()
# print (raw_documents [1].page_content)
# 2. 文本分割策略:两阶段分割
# 阶段一:按 Markdown 标题结构粗分
headers_to_split_on = [
    ("#", "Header 1"),
    ("##", "Header 2"), 
    ("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter (
    headers_to_split_on=headers_to_split_on,
    strip_headers=False  # 保留标题文本
)
# stage1_docs = markdown_splitter.split_text (raw_documents [0].page_content)  # 处理单个文件
stage1_docs = []    # 处理多个文件
for doc in raw_documents:
    stage1_docs.extend (markdown_splitter.split_text (doc.page_content))
# 阶段二:精细分割(处理长段落)
text_splitter = RecursiveCharacterTextSplitter (
    chunk_size=800,          # 块大小
    chunk_overlap=150,       # 块与块之间的重叠字符数
    separators=["\n\n", "\n", "。", "!", "?", "\.\s+", " "],  # 增加自然断句符
)
final_documents = text_splitter.split_documents (stage1_docs)
print (f"总分割块数: {len (final_documents)}")
# print ("示例块内容:", final_documents [0].page_content [:200] + "...")
# 3. 初始化 OllamaEmbeddings 和 FAISS 向量数据库
ollama_embedding = OllamaEmbeddings (model="bge-m3:latest", base_url="http://localhost:11434/")
db = FAISS.from_documents (final_documents, ollama_embedding)
# 4. 执行相似性搜索
query = "LineText"
docs = db.similarity_search (query)
# print (len (docs))
print (docs [0].page_content)

# 按向量进行相似性搜索

也可以使用 similarity_search_by_vector 搜索与给定嵌入向量相似的文档,该方法接受嵌入向量作为参数,而不是字符串。

# 4. 按向量进行相似性搜索
query = "LineText"
embedding_vector = ollama_embedding.embed_query (query)
docs = db.similarity_search_by_vector (embedding_vector)
print (docs [0].page_content)

# 异步操作

向量存储通常作为一个单独的服务运行,需要一些 IO 操作,因此它们可能会被异步调用。这带来了性能上的好处,因为你不会浪费时间等待外部服务的响应。如果你使用异步框架,例如 FastAPI,这也可能很重要。

LangChain 支持在向量存储上进行异步操作。所有方法都可以使用其异步对应方法调用,前缀为 a,表示 async。

docs = await db.asimilarity_search (query)
docs

# FAISS 持久化存储

在示例中,我们在内存中实例化 FAISS 向量数据库,下面,我们将实现一个类,用于持久化存储该向量数据库,该类实现简单的存储管理

from pathlib import Path
import json
from typing import List, Optional, Dict, Any
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_core.embeddings import Embeddings
import shutil
import os
class SimpleFAISS:
    """FAISS 向量存储类"""
    
    def __init__(
        self,
        storage_path: str | Path,
        embedding_model: Embeddings,
        create_if_not_exist: bool = True
    ):
        """ 初始化 FAISS 向量存储
        
        参数:
            storage_path: 存储目录路径
            embedding_model: 嵌入模型
            create_if_not_exist: 不存在时是否自动创建
        """
        self.storage_path = Path (storage_path)
        self.embedding_model = embedding_model
        self.index_path = self.storage_path/ "index"
        self.vector_db = None
        
        # 确保存储目录存在
        self.storage_path.mkdir (parents=True, exist_ok=True)
        
        # 检测并加载或创建向量库
        if self._index_exists ():
            self._load_existing ()
        elif create_if_not_exist:
            # 创建一个空的向量库
            self._create_empty ()
        
    def _index_exists (self) -> bool:
        """检查索引文件是否存在"""
        return (self.index_path/ "index.faiss").exists () and (self.index_path/ "index.pkl").exists ()
    
    def _load_existing (self):
        """加载现有的向量库"""
        try:
            self.vector_db = FAISS.load_local (
                str (self.index_path),
                self.embedding_model,
                allow_dangerous_deserialization=True
            )
            print (f"成功加载现有向量库: {self.index_path}")
        except Exception as e:
            print (f"加载向量库失败: {str (e)}")
            self.vector_db = None
    
    def _create_empty (self):
        """创建空的向量库"""
        try:
            # 创建一个临时目录
            self.index_path.mkdir (exist_ok=True)
            
            # 创建一个含有占位符文档的向量库
            placeholder_doc = Document (
                page_content="初始化占位符",
                metadata={"type": "placeholder"}
            )
            self.vector_db = FAISS.from_documents ([placeholder_doc], self.embedding_model)
            
            # 保存向量库
            self.vector_db.save_local (str (self.index_path))
            print (f"创建了新的向量库: {self.index_path}")
        except Exception as e:
            print (f"创建向量库失败: {str (e)}")
            self.vector_db = None
    
    def add_documents (self, documents: List [Document]) -> bool:
        """ 添加文档到向量库
        
        参数:
            documents: 要添加的文档列表
            
        返回:
            是否添加成功
        """
        if not documents:
            return True
            
        if not self.vector_db:
            # 如果没有向量库,直接创建
            try:
                self.vector_db = FAISS.from_documents (documents, self.embedding_model)
                self.vector_db.save_local (str (self.index_path))
                return True
            except Exception as e:
                print (f"创建向量库失败: {str (e)}")
                return False
        else:
            # 增量添加文档
            try:
                # 创建临时备份
                backup_path = self.storage_path/ "backup_index"
                if backup_path.exists ():
                    shutil.rmtree (backup_path)
                shutil.copytree (self.index_path, backup_path)
                
                # 添加文档
                self.vector_db.add_documents (documents)
                
                # 保存更新后的向量库
                self.vector_db.save_local (str (self.index_path))
                
                # 删除临时备份
                shutil.rmtree (backup_path)
                return True
            except Exception as e:
                print (f"添加文档失败: {str (e)}")
                
                # 恢复备份
                if backup_path.exists ():
                    if self.index_path.exists ():
                        shutil.rmtree (self.index_path)
                    shutil.copytree (backup_path, self.index_path)
                    shutil.rmtree (backup_path)
                    self._load_existing ()
                
                return False
    
    def similarity_search (
        self,
        query: str,
        k: int = 4,
        filter: Optional [Dict [str, Any]] = None
    ) -> List [Document]:
        """ 执行相似度搜索
        
        参数:
            query: 查询文本
            k: 返回结果数量
            filter: 元数据过滤条件
            
        返回:
            相似文档列表
        """
        if not self.vector_db:
            print ("向量库未初始化,无法执行搜索")
            return []
        
        try:
            return self.vector_db.similarity_search (query=query, k=k, filter=filter)
        except Exception as e:
            print (f"搜索失败: {str (e)}")
            return []
    
    def delete_all (self) -> bool:
        """删除所有内容并重新初始化"""
        try:
            if self.index_path.exists ():
                shutil.rmtree (self.index_path)
            
            self.index_path.mkdir (exist_ok=True)
            self._create_empty ()
            return True
        except Exception as e:
            print (f"删除失败: {str (e)}")
            return False
# 使用示例
if __name__ == "__main__":
    from langchain_openai import OpenAIEmbeddings
    import tempfile
    
    # 创建临时目录作为存储路径
    temp_dir = os.path.join (tempfile.gettempdir (), "simple_faiss_test")
    
    # 初始化嵌入模型和向量存储
    embeddings = OpenAIEmbeddings ()
    vector_store = SimpleFAISS (temp_dir, embeddings)
    
    # 添加一些测试文档
    docs = [
        Document (page_content="北京是中国的首都", metadata={"city": "北京", "country": "中国"}),
        Document (page_content="上海是中国最大的城市", metadata={"city": "上海", "country": "中国"}),
        Document (page_content="东京是日本的首都", metadata={"city": "东京", "country": "日本"}),
        Document (page_content="纽约是美国最大的城市", metadata={"city": "纽约", "country": "美国"})
    ]
    
    print ("添加文档...")
    vector_store.add_documents (docs)
    
    # 执行搜索测试
    print ("\n 搜索测试 1: 中国的城市")
    results = vector_store.similarity_search ("中国的城市", k=2)
    for i, doc in enumerate (results):
        print (f"结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
        print ()
    
    # 使用过滤器搜索
    print ("\n 搜索测试 2: 首都 (带过滤)")
    results = vector_store.similarity_search ("首都", filter={"country": "中国"})
    for i, doc in enumerate (results):
        print (f"结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
        print ()
    
    # 重新加载测试
    print ("\n 重新加载测试")
    vector_store = None  # 释放当前实例
    vector_store = SimpleFAISS (temp_dir, embeddings)
    
    # 验证数据是否正确加载
    results = vector_store.similarity_search ("城市", k=1)
    if results:
        print (f"重新加载成功:找到文档 '{results [0].page_content}'")
    else:
        print ("重新加载失败:未找到文档")
    
    # 清理临时目录
    # 注释掉以便检查生成的文件
    # shutil.rmtree (temp_dir)

创建完成后,在上述示例代码中,只需要将第三步更改为

# 3. 初始化 OllamaEmbeddings 和 FAISS 向量数据库,执行相似性搜索
from FAISS.E_FAISS import SimpleFAISS   # 根据实际路径,导入 SimpleFAISS 类
temp_dir = current_path / "faiss_temp"
simple_fiss = SimpleFAISS (temp_dir, ollama_embedding)
simple_fiss.add_documents (final_documents)
results = simple_fiss.similarity_search ("qfluence", k=2)
for i, doc in enumerate (results):
    print (f"结果 {i+1}:")
    print (f"内容: {doc.page_content}")
    print (f"元数据: {doc.metadata}")
    print ()

当然,我们可以继续优化这个类,实现更多的内容:

  • 持久化存储:文件系统基于分层目录结构
  • 原子性操作:使用临时文件和目录确保写入安全
  • 增量更新:避免重复处理已索引的文档
  • 兼容性检查:确保模型和配置一致性
  • 性能监控:捕获操作统计和系统指标
from pathlib import Path
import json
from datetime import datetime
from typing import List, Optional, Dict, Any, Set, Tuple
import faiss
from langchain_core.documents import Document
from langchain_community.vectorstores import FAISS
from langchain_core.embeddings import Embeddings
import hashlib
import shutil
from uuid import uuid4
try:
    import psutil
except ImportError:
    psutil = None
class VectorStoreManager:
    """向量存储管理类,支持通用嵌入模型和外部文档处理"""
    
    def __init__(
        self,
        storage_path: str | Path,
        embedding_model: Embeddings,
        config_signature: str = "default",
    ):
        self.storage_path = Path (storage_path)
        self.embedding_model = embedding_model
        self.config_signature = config_signature
        self._current_hashes: Optional [Set [str]] = None  # 哈希缓存
        
        self._init_storage ()
        self._vector_db: Optional [FAISS] = None
        self._load_existing ()
    def _init_storage (self):
        """初始化分层存储结构"""
        self.storage_path.mkdir (parents=True, exist_ok=True)
        (self.storage_path/ "index").mkdir (exist_ok=True)
        (self.storage_path/ "meta").mkdir (exist_ok=True)
        (self.storage_path/ "hashes").mkdir (exist_ok=True)
    @property
    def index_exists (self) -> bool:
        """检查是否存在有效索引"""
        return (self.storage_path/ "index" / "index.faiss").exists ()
    def process_documents (
        self,
        processed_chunks: List [Document],
        force_rebuild: bool = False
    ) -> None:
        current_hashes = self._generate_hashes (processed_chunks)
        
        if force_rebuild or not self.index_exists:
            self._full_update (processed_chunks, current_hashes)
        else:
            self._incremental_update (processed_chunks, current_hashes)
    def _full_update (self, chunks: List [Document], hashes: List [str]):
        """全量更新索引(原子化操作)"""
        # 使用临时目录防止写入中断
        temp_index_dir = self.storage_path/f"tmp_index_{uuid4 ().hex}"
        temp_index_dir.mkdir ()
        
        try:
            self._vector_db = FAISS.from_documents (chunks, self.embedding_model)
            self._vector_db.save_local (str (temp_index_dir))
            self._save_metadata (hashes, is_full=True, temp_index=temp_index_dir)
            self._current_hashes = set (hashes)  # 更新哈希缓存
        finally:
            if temp_index_dir.exists ():
                shutil.rmtree (temp_index_dir)
        self._log_operation ("full_update", len (chunks))
    def _incremental_update (self, chunks: List [Document], new_hashes: List [str]):
        """增量更新索引(带缓存优化)"""
        update_set = {
            (h, doc) for h, doc in zip (new_hashes, chunks)
            if h not in self.current_hashes
        }
        
        if not update_set:
            return
        update_hashes, update_docs = zip (*update_set)
        
        # 原子化增量更新
        temp_index_dir = self.storage_path/f"tmp_index_{uuid4 ().hex}"
        try:
            self._vector_db.save_local (str (temp_index_dir))
            self._vector_db.add_documents (update_docs)
            self._save_metadata (update_hashes, is_full=False)
            self._current_hashes.update (update_hashes)  # 更新缓存
        finally:
            if temp_index_dir.exists ():
                shutil.rmtree (temp_index_dir)
        self._log_operation ("incremental_update", len (update_docs))
    def _save_metadata (
        self,
        hashes: List [str],
        is_full: bool,
        temp_index: Optional [Path] = None
    ):
        """原子化元数据保存"""
        # 保存哈希记录(带时间戳)
        hash_file = self.storage_path/ "hashes" /f"{datetime.now ().strftime ('% Y% m% d% H% M% S')}.hash"
        with open (hash_file, "w") as f:
            f.write ("\n".join (hashes))
            
        # 原子化保存主元数据
        temp_manifest = self.storage_path/ "meta" /f".tmp_{uuid4 ().hex}.json"
        metadata = {
            "timestamp": datetime.utcnow ().isoformat (),
            "config_signature": self.config_signature,
            "embedding_model": self._get_model_signature (),
            "total_chunks": len (hashes) if is_full else self._get_total_chunks () + len (hashes),
            "operation_type": "full" if is_full else "incremental"
        }
        try:
            with open (temp_manifest, "w") as f:
                json.dump (metadata, f, indent=2)
            temp_manifest.replace (self.storage_path/ "meta" / "manifest.json")
        finally:
            if temp_manifest.exists ():
                temp_manifest.unlink ()
        # 原子化移动索引文件
        if temp_index:
            index_dir = self.storage_path/ "index"
            for f in temp_index.glob ("*"):
                f.replace (index_dir /f.name)
    def _load_existing (self):
        """安全加载现有索引"""
        if self.index_exists:
            try:
                # 先校验再加载
                self._validate_compatibility ()
                self._vector_db = FAISS.load_local (
                    str (self.storage_path/ "index"),
                    self.embedding_model,
                    allow_dangerous_deserialization=True
                )
            except Exception as e:
                raise VectorStoreError (f"索引加载失败: {str (e)}") from e
    def _validate_compatibility (self):
        """增强型兼容性验证"""
        try:
            with open (self.storage_path/ "meta" / "manifest.json") as f:
                saved_meta = json.load (f)
        except FileNotFoundError as e:
            raise VectorStoreError ("元数据文件缺失,请使用 force_rebuild=True 重建索引") from e
            
        current_model_sig = self._get_model_signature ()
        
        if saved_meta ["config_signature"] != self.config_signature:
            raise ConfigMismatchError (
                f"配置签名不匹配:保存值 ={saved_meta ['config_signature']}, 当前值 ={self.config_signature}"
            )
            
        if saved_meta ["embedding_model"] != current_model_sig:
            raise EmbeddingModelMismatchError (
                f"嵌入模型不兼容:保存签名 ={saved_meta ['embedding_model']}, 当前签名 ={current_model_sig}"
            )
    def _get_model_signature (self) -> str:
        """增强型维度检测"""
        model_name = getattr (self.embedding_model, "model", "unknown_model")
        
        try:
            # 使用标准化测试文本
            test_text = "嵌入维度检测文本 -" * 10
            emb1 = self.embedding_model.embed_query (test_text)
            emb2 = self.embedding_model.embed_query ("二次验证文本")
            dim = len (emb1)
            
            # 维度一致性验证
            if dim != len (emb2):
                raise ValueError (f"维度不一致: {dim} vs {len (emb2)}")
                
            # 值范围验证
            if max (emb1) > 100 or min (emb1) < -100:
                print (f"警告:嵌入值范围异常 ({min (emb1):.2f}, {max (emb1):.2f})")
                
        except Exception as e:
            print (f"维度检测失败: {str (e)}, 使用默认值 512")
            dim = 512
        
        return hashlib.sha256 (
            f"{model_name}-{dim}-{self.config_signature}".encode ()
        ).hexdigest ()
    @property
    def current_hashes (self) -> Set [str]:
        """带缓存的当前哈希集合"""
        if self._current_hashes is None:
            self._current_hashes = self._load_existing_hashes ()
        return self._current_hashes
    def _load_existing_hashes (self) -> Set [str]:
        """优化哈希加载(仅读取最新文件)"""
        hash_files = list ((self.storage_path/ "hashes").glob ("*.hash"))
        if not hash_files:
            return set ()
        
        latest_file = max (hash_files, key=lambda f: f.stat ().st_mtime)
        return set (latest_file.read_text ().splitlines ())
    def _generate_hashes (self, documents: List [Document]) -> List [str]:
        """带元数据的哈希生成"""
        return [
            hashlib.sha256 (
                f"{doc.page_content}{json.dumps (doc.metadata, sort_keys=True)}".encode ()
            ).hexdigest ()
            for doc in documents
        ]
    def _log_operation (self, op_type: str, doc_count: int):
        """增强型审计日志"""
        log_entry = {
            "timestamp": datetime.now ().isoformat (),
            "operation": op_type,
            "documents_processed": doc_count,
            "system_stats": self.get_operational_stats (),
            "performance": self._get_performance_metrics ()
        }
        
        audit_log = self.storage_path/ "meta" / "audit.log"
        with open (audit_log, "a") as f:
            f.write (json.dumps (log_entry) + "\n")
    def _get_performance_metrics (self) -> Dict [str, Any]:
        """获取系统性能指标"""
        metrics = {}
        if psutil:
            process = psutil.Process ()
            metrics.update ({
                "memory_mb": process.memory_info ().rss// 1024 // 1024,
                "cpu_percent": psutil.cpu_percent (interval=0.1),
                "load_avg": psutil.getloadavg () if hasattr (psutil, "getloadavg") else None
            })
        return metrics
    def get_operational_stats (self) -> Dict [str, Any]:
        """运营统计信息"""
        if not self.index_exists:
            return {"status": "uninitialized"}
            
        with open (self.storage_path/ "meta" / "manifest.json") as f:
            meta = json.load (f)
            
        return {
            "last_updated": meta ["timestamp"],
            "total_chunks": meta ["total_chunks"],
            "embedding_model": meta ["embedding_model"],
            "storage_usage": self._calculate_storage_usage ()
        }
    def _calculate_storage_usage (self) -> Dict [str, float]:
        return {
            "index": self._dir_size (self.storage_path/ "index") / 1024**2,
            "meta": self._dir_size (self.storage_path/ "meta") / 1024**2,
            "hashes": self._dir_size (self.storage_path/ "hashes") / 1024**2
        }
    def _dir_size (self, path: Path) -> int:
        return sum (f.stat ().st_size for f in path.glob ("**/*") if f.is_file ())
    
    def similarity_search (
        self,
        query: str,
        k: int = 4,
        filter: Optional [Dict [str, Any]] = None,
        **kwargs: Any
    ) -> List [Document]:
        """相似性搜索(自动处理嵌入)"""
        if not self._vector_db:
            raise VectorStoreError ("向量库未初始化")
        return self._vector_db.similarity_search (
            query=query,
            k=k,
            filter=filter,
            **kwargs
        )
    def similarity_search_by_vector (
        self,
        embedding: List [float],
        k: int = 4,
        filter: Optional [Dict [str, Any]] = None,
        **kwargs: Any
    ) -> List [Document]:
        """通过向量进行相似性搜索"""
        if not self._vector_db:
            raise VectorStoreError ("向量库未初始化")
        return self._vector_db.similarity_search_by_vector (
            embedding=embedding,
            k=k,
            filter=filter,
            **kwargs
        )
class VectorStoreError (Exception):
    """向量存储异常基类"""
class ConfigMismatchError (VectorStoreError):
    """配置不匹配异常"""
class EmbeddingModelMismatchError (VectorStoreError):
    """嵌入模型不匹配异常"""

以下是使用示例

from pathlib import Path
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document
from langchain.text_splitter import CharacterTextSplitter
from Vector_StoreManager import VectorStoreManager
import os
import json
from langchain_community.document_loaders import DirectoryLoader, UnstructuredMarkdownLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter, MarkdownHeaderTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_ollama import OllamaEmbeddings
from pathlib import Path
# 1. 设置 OpenAI API 密钥(如已设置环境变量则可省略)
# os.environ ["OPENAI_API_KEY"] = "你的 OpenAI API 密钥"
def main ():
    """VectorStoreManager 使用示例"""
    # 获取当前文件所在目录路径
    current_path = Path (__file__).resolve ().parent 
    
    # 2. 创建存储目录
    storage_path = current_path / "vector_store_demo"
    
    # 3. 初始化嵌入模型
    print ("初始化嵌入模型...")
    embedding_model = OpenAIEmbeddings ()
    
    # 4. 初始化向量存储管理器
    print ("创建向量存储管理器...")
    vector_manager = VectorStoreManager (
        storage_path=storage_path,
        embedding_model=embedding_model,
        config_signature="demo_config"
    )
    # 5. 加载 Markdown 文档,使用 DirectoryLoader 并指定 Markdown 加载器
    loader = DirectoryLoader (
        path=current_path,  # 要扫描的根目录
        glob="**/*.md",    # 匹配所有子目录中的.md 文件
        loader_cls=UnstructuredMarkdownLoader,  # 指定使用 Markdown 加载器
        recursive=True,      # 启用递归扫描(确保处理子目录)
        show_progress=True  # 添加加载进度显示
    )
    raw_documents = loader.load ()
    # print (raw_documents [1].page_content)
    # 6. 文本分割策略:两阶段分割
    # 阶段一:按 Markdown 标题结构粗分,定义分割器
    headers_to_split_on = [
        ("#", "Header 1"),
        ("##", "Header 2"), 
        ("###", "Header 3"),
    ]
    markdown_splitter = MarkdownHeaderTextSplitter (
        headers_to_split_on=headers_to_split_on,
        strip_headers=False  # 保留标题文本
    )
    # stage1_docs = markdown_splitter.split_text (raw_documents [0].page_content)  # 处理单个文件
    stage1_docs = []    # 处理多个文件
    for doc in raw_documents:
        stage1_docs.extend (markdown_splitter.split_text (doc.page_content))
    # 7. 阶段二:精细分割(处理长段落)
    text_splitter = RecursiveCharacterTextSplitter (
        chunk_size=800,          # 块大小
        chunk_overlap=150,       # 块与块之间的重叠字符数
        separators=["\n\n", "\n", "。", "!", "?", "\.\s+", " "],  # 增加自然断句符
    )
    final_documents = text_splitter.split_documents (stage1_docs)
    print (f"总分割块数: {len (final_documents)}")
    # print ("示例块内容:", final_documents [0].page_content [:200] + "...")
    # 8. 处理文档(添加到向量存储)
    documents = final_documents
    print (f"添加 {len (documents)} 个文档到向量存储...")
    vector_manager.process_documents (documents)
    
    # 9. 显示运营统计信息
    print ("\n 向量库统计信息:")
    stats = vector_manager.get_operational_stats ()
    print (json.dumps (stats, indent=2, ensure_ascii=False))
    
    # 10. 执行基本相似性搜索
    print ("\n 基本相似性搜索 (查询: ' 城市景点 '):")
    results = vector_manager.similarity_search ("城市景点", k=3)
    for i, doc in enumerate (results):
        print (f"\n 结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
    
    # 11. 使用过滤器执行搜索
    print ("\n 过滤搜索 (查询: ' 著名 ', 过滤:华东地区):")
    filter_results = vector_manager.similarity_search (
        query="著名",
        k=3,
        filter={"region": "华东"}
    )
    for i, doc in enumerate (filter_results):
        print (f"\n 结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
    
    # 12. 增量更新示例
    print ("\n 增量添加新文档...")
    new_documents = [
        Document (
            page_content="广州是广东省省会,历史悠久,是中国重要的经济、文化中心。",
            metadata={
                "source": "text_6",
                "city": "广州",
                "chunk": 0,
                "region": "华南"
            }
        ),
        Document (
            page_content="西安是陕西省省会,是中国古都之一,拥有兵马俑等世界闻名的文化遗产。",
            metadata={
                "source": "text_7",
                "city": "西安",
                "chunk": 0,
                "region": "西北"
            }
        )
    ]
    vector_manager.process_documents (new_documents)
    
    # 13. 验证增量更新
    print ("\n 验证增量更新 (查询: ' 历史文化 '):")
    after_update = vector_manager.similarity_search ("历史文化", k=2)
    for i, doc in enumerate (after_update):
        print (f"\n 结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
    
    # 14. 测试强制重建
    print ("\n 测试强制重建向量库...")
    subset_docs = documents [:3] + new_documents  # 仅使用部分文档重建
    vector_manager.process_documents (subset_docs, force_rebuild=True)
    
    # 15. 验证重建结果
    print ("\n 验证重建结果 (查询: ' 中国 '):")
    rebuild_results = vector_manager.similarity_search ("中国", k=3)
    for i, doc in enumerate (rebuild_results):
        print (f"\n 结果 {i+1}:")
        print (f"内容: {doc.page_content}")
        print (f"元数据: {doc.metadata}")
    
    # 16. 展示存储使用情况
    print ("\n 存储使用情况:")
    storage_usage = vector_manager._calculate_storage_usage ()
    for category, size_mb in storage_usage.items ():
        print (f"{category}: {size_mb:.2f} MB")
if __name__ == "__main__":
    main ()

# tools (工具)

LangChain 工具 包含工具的描述(传递给语言模型)以及调用的函数的实现。有关构建工具的列表,请参见 这里

# 创建工具

在构建 agent 时,您需要提供一个它可以使用的 Tool 列表。除了被调用的实际函数外,Tool 由几个组件组成:

AttributeTypeDescription
namestr在提供给 LLM 或代理的一组工具中必须是唯一的。
descriptionstr描述工具的功能。被 LLM 或代理用作上下文。
args_schemapydantic.BaseModel可选但推荐,如果使用回调处理程序则是必需的。可以用来提供更多信息(例如,少量示例)或验证预期参数。
return_directboolean仅与 agent 相关。当为 True 时,在调用给定工具后,agent 将停止并直接将结果返回给用户。

LangChain 支持从以下内容创建工具:

  • 函数;
  • LangChain 运行接口;
  • 通过从 BaseTool 子类化 (这是最灵活的方法,它提供了最大的控制程度,但需要更多的代码量)。

从函数创建工具可能足以满足大多数用例,可以通过简单的 @tool 装饰器 来完成。如果需要更多配置,例如同时指定同步和异步实现,也可以使用 StructuredTool.from_function 类方法。

# 从函数创建工具

# @tool 装饰器

@tool 装饰器是定义自定义工具的最简单方法。默认情况下,装饰器使用函数名称作为工具名称,但可以通过将字符串作为第一个参数传递来覆盖。此外,装饰器将使用函数的文档字符串作为工具的描述,因此必须提供文档字符串。

from langchain_core.tools import tool
@tool
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
# Let's inspect some of the attributes associated with the tool.
>>> print (multiply.name)
multiply
>>> print (multiply.description)
Multiply two numbers.
>>> print (multiply.args)
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

或者创建一个 异步 实现:

from langchain_core.tools import tool
@tool
async def amultiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b

请注意, @tool 支持解析注释、嵌套模式和其他特性:

from typing import Annotated, List
@tool
def multiply_by_max (
    a: Annotated [str, "scale factor"],
    b: Annotated [List [int], "list of ints over which to take maximum"],
) -> int:
    """Multiply a by the maximum of b."""
    return a * max (b)
>>> multiply_by_max.args_schema.schema ()
{'description': 'Multiply a by the maximum of b.',
 'properties': {'a': {'description': 'scale factor',
   'title': 'A',
   'type': 'string'},
  'b': {'description': 'list of ints over which to take maximum',
   'items': {'type': 'integer'},
   'title': 'B',
   'type': 'array'}},
 'required': ['a', 'b'],
 'title': 'multiply_by_maxSchema',
 'type': 'object'}

您还可以通过将工具名称和 JSON 参数传递给工具装饰器来自定义它们。

from pydantic import BaseModel, Field
class CalculatorInput (BaseModel):
    a: int = Field (description="first number")
    b: int = Field (description="second number")
@tool ("multiplication-tool", args_schema=CalculatorInput, return_direct=True)
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
# Let's inspect some of the attributes associated with the tool.
>>> print (multiply.name)
multiplication-tool
>>> print (multiply.description)
Multiply two numbers.
>>> print (multiply.args)
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
>>> print (multiply.return_direct)
True

@tool 可以选择性地解析 Google 风格文档 字符串,并将文档字符串组件(如参数描述)与工具模式的相关部分关联起来。要切换此行为,请指定 parse_docstring

@tool (parse_docstring=True)
def foo (bar: str, baz: int) -> str:
    """The foo.
    Args:
        bar: The bar.
        baz: The baz.
    """
    return bar
>>> foo.args_schema.schema ()
{'description': 'The foo.',
 'properties': {'bar': {'description': 'The bar.',
   'title': 'Bar',
   'type': 'string'},
  'baz': {'description': 'The baz.', 'title': 'Baz', 'type': 'integer'}},
 'required': ['bar', 'baz'],
 'title': 'fooSchema',
 'type': 'object'}

默认情况下,如果文档字符串无法正确解析, @tool (parse_docstring=True) 将引发 ValueError。有关详细信息和示例,请参见 API 参考。

# 结构化工具

StructuredTool.from_function 类方法提供比 @tool 装饰器更多的可配置性,而无需太多额外代码。

from langchain_core.tools import StructuredTool
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
async def amultiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
calculator = StructuredTool.from_function (func=multiply, coroutine=amultiply)
print (calculator.invoke ({"a": 2, "b": 3}))
print (await calculator.ainvoke ({"a": 2, "b": 5}))

要进行配置

class CalculatorInput (BaseModel):
    a: int = Field (description="first number")
    b: int = Field (description="second number")
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
calculator = StructuredTool.from_function (
    func=multiply,
    name="Calculator",
    description="multiply numbers",
    args_schema=CalculatorInput,
    return_direct=True,
    # coroutine= ... <- you can specify an async method if desired as well
)
>>> print (calculator.invoke ({"a": 2, "b": 3}))
6
>>> print (calculator.name)
Calculator
>>> print (calculator.description)
multiply numbers
>>> print (calculator.args)
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}

# 从可运行对象创建工具

接受字符串或 dict 输入的 LangChain Runnables 可以使用 as_tool 方法转换为工具,该方法允许为参数指定名称、描述和其他模式信息。

from langchain_core.language_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages (
    [("human", "Hello. Please respond in the style of {answer_style}.")]
)
# Placeholder LLM
llm = GenericFakeChatModel (messages=iter (["hello matey"]))
chain = prompt | llm | StrOutputParser ()
as_tool = chain.as_tool (
    name="Style responder", description="Description of when to use tool."
)
>>> as_tool.args
{'answer_style': {'title': 'Answer Style', 'type': 'string'}}

# 子类化 BaseTool

您可以通过从 BaseTool 子类化来定义自定义工具。这提供了对工具定义的最大控制,但需要编写更多代码。

from typing import Optional, Type
from langchain_core.callbacks import (
    AsyncCallbackManagerForToolRun,
    CallbackManagerForToolRun,
)
from langchain_core.tools import BaseTool
from pydantic import BaseModel
class CalculatorInput (BaseModel):
    a: int = Field (description="first number")
    b: int = Field (description="second number")
# Note: It's important that every field has type hints. BaseTool is a
# Pydantic class and not having type hints can lead to unexpected behavior.
class CustomCalculatorTool (BaseTool):
    name: str = "Calculator"
    description: str = "useful for when you need to answer questions about math"
    args_schema: Type [BaseModel] = CalculatorInput
    return_direct: bool = True
    def _run (
        self, a: int, b: int, run_manager: Optional [CallbackManagerForToolRun] = None
    ) -> str:
        """Use the tool."""
        return a * b
    async def _arun (
        self,
        a: int,
        b: int,
        run_manager: Optional [AsyncCallbackManagerForToolRun] = None,
    ) -> str:
        """Use the tool asynchronously."""
        # If the calculation is cheap, you can just delegate to the sync implementation
        # as shown below.
        # If the sync calculation is expensive, you should delete the entire _arun method.
        # LangChain will automatically provide a better implementation that will
        # kick off the task in a thread to make sure it doesn't block other async code.
        return self._run (a, b, run_manager=run_manager.get_sync ())
multiply = CustomCalculatorTool ()
>>> print (multiply.name)
Calculator
>>> print (multiply.description)
useful for when you need to answer questions about math
>>> print (multiply.args)
{'a': {'description': 'first number', 'title': 'A', 'type': 'integer'}, 'b': {'description': 'second number', 'title': 'B', 'type': 'integer'}}
>>> print (multiply.return_direct)
True
>>> print (multiply.invoke ({"a": 2, "b": 3}))
6
>>> print (await multiply.ainvoke ({"a": 2, "b": 3}))
6

# 创建异步工具

LangChain 工具实现了 运行接口

所有 Runnables 都具有 invokeainvoke 方法(以及其他方法,如 batch、abatch、astream 等)。

因此,即使您只提供工具的 sync 实现,您仍然可以使用 ainvoke 接口,但有一些重要事项需要了解: 需要了解的一些重要事项:

  • LangChain 默认提供异步实现,假设该函数计算开销较大,因此会将执行委托给另一个线程。
  • 如果您在异步代码库中工作,应该创建异步工具而不是同步工具,以避免因该线程而产生小的开销。
  • 如果您需要同步和异步实现,请使用 StructuredTool.from_function 或从 BaseTool 子类化。
  • 如果同时实现同步和异步,并且同步代码运行速度较快,请覆盖默认的 LangChain 异步实现并直接调用同步代码。
  • 不能将同步 invoke 与异步工具一起使用。
from langchain_core.tools import StructuredTool
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
calculator = StructuredTool.from_function (func=multiply)
print (calculator.invoke ({"a": 2, "b": 3}))
print (
    await calculator.ainvoke ({"a": 2, "b": 5})
)  # Uses default LangChain async implementation incurs small overhead
from langchain_core.tools import StructuredTool
def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
async def amultiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
calculator = StructuredTool.from_function (func=multiply, coroutine=amultiply)
print (calculator.invoke ({"a": 2, "b": 3}))
print (
    await calculator.ainvoke ({"a": 2, "b": 5})
)  # Uses use provided amultiply without additional overhead

当仅提供异步定义时,不能使用 .invoke

@tool
async def multiply (a: int, b: int) -> int:
    """Multiply two numbers."""
    return a * b
try:
    # 该段代码报错
    multiply.invoke ({"a": 2, "b": 3})
except NotImplementedError:
    print ("Raised not implemented error. You should not be doing this.")

# 处理工具错误

如果您在使用带有代理的工具,您可能需要一个错误处理策略,以便代理能够从错误中恢复并继续执行。

一个简单的策略是在工具内部抛出 ToolException ,并使用 handle_tool_error 指定错误处理程序。

当指定错误处理程序时,异常将被捕获,错误处理程序将决定从工具返回哪个输出。

您可以将 handle_tool_error 设置为 True 、字符串值或函数。如果是函数,该函数应接受一个 ToolException 作为参数并返回一个值。

请注意,仅仅抛出 ToolException 是无效的。您需要首先设置工具的 handle_tool_error ,因为它的默认值是 False

from langchain_core.tools import ToolException
def get_weather (city: str) -> int:
    """Get weather for the given city."""
    raise ToolException (f"Error: There is no city by the name of {city}.")

这是一个默认 handle_tool_error=True 行为的示例。

get_weather_tool = StructuredTool.from_function (
    func=get_weather,
    handle_tool_error=True,
)
get_weather_tool.invoke ({"city": "foobar"})
'Error: There is no city by the name of foobar.'

我们可以将 handle_tool_error 设置为一个始终返回的字符串。

get_weather_tool = StructuredTool.from_function (
    func=get_weather,
    handle_tool_error="There is no such city, but it's probably above 0K there!",
)
get_weather_tool.invoke ({"city": "foobar"})
"There is no such city, but it's probably above 0K there!"

使用函数处理错误:

def _handle_error (error: ToolException) -> str:
    return f"The following errors occurred during tool execution:`{error.args [0]}`"
get_weather_tool = StructuredTool.from_function (
    func=get_weather,
    handle_tool_error=_handle_error,
)
get_weather_tool.invoke ({"city": "foobar"})
'The following errors occurred during tool execution: `Error: There is no city by the name of foobar.`'

# 返回工具执行的工件

有时工具执行的工件我们希望能够让下游组件在我们的链或代理中访问,但我们不想将其暴露给模型本身。例如,如果工具返回自定义对象如文档,我们可能希望将一些视图或元数据传递给模型,而不将原始输出传递给模型。同时,我们可能希望能够在其他地方访问这个完整的输出,例如在下游工具中。

工具和 ToolMessage 接口使得能够区分工具输出中用于模型的部分(这是 ToolMessage.content)和用于模型外部使用的部分(ToolMessage.artifact)。

如果我们希望我们的工具区分消息内容和其他工件,我们需要在定义工具时指定 response_format="content_and_artifact",并确保返回一个元组 (content, artifact):

import random
from typing import List, Tuple
from langchain_core.tools import tool
@tool (response_format="content_and_artifact")
def generate_random_ints (min: int, max: int, size: int) -> Tuple [str, List [int]]:
    """Generate size random ints in the range [min, max]."""
    array = [random.randint (min, max) for _ in range (size)]
    content = f"Successfully generated array of {size} random ints in [{min}, {max}]."
    return content, array

如果我们直接使用工具参数调用我们的工具,我们将只得到输出的内容部分:

generate_random_ints.invoke ({"min": 0, "max": 9, "size": 10})
'Successfully generated array of 10 random ints in [0, 9].'

如果我们使用 ToolCall(如工具调用模型生成的那样)调用我们的工具,我们将得到一个 ToolMessage,其中包含内容和工具生成的工件:

generate_random_ints.invoke (
    {
        "name": "generate_random_ints",
        "args": {"min": 0, "max": 9, "size": 10},
        "id": "123",  # required
        "type": "tool_call",  # required
    }
)

在子类化 BaseTool 时,我们也可以这样做:

from langchain_core.tools import BaseTool
class GenerateRandomFloats (BaseTool):
    name: str = "generate_random_floats"
    description: str = "Generate size random floats in the range [min, max]."
    response_format: str = "content_and_artifact"
    ndigits: int = 2
    def _run (self, min: float, max: float, size: int) -> Tuple [str, List [float]]:
        range_ = max - min
        array = [
            round (min + (range_ * random.random ()), ndigits=self.ndigits)
            for _ in range (size)
        ]
        content = f"Generated {size} floats in [{min}, {max}], rounded to {self.ndigits} decimals."
        return content, array
    # Optionally define an equivalent async method
    # async def _arun (self, min: float, max: float, size: int) -> Tuple [str, List [float]]:
    #     ...
rand_gen = GenerateRandomFloats (ndigits=4)
rand_gen.invoke (
    {
        "name": "generate_random_floats",
        "args": {"min": 0.1, "max": 3.3333, "size": 3},
        "id": "123",
        "type": "tool_call",
    }
)
ToolMessage (content='Generated 3 floats in [0.1, 3.3333], rounded to 4 decimals.', name='generate_random_floats', tool_call_id='123', artifact=[1.5566, 0.5134, 2.7914])

# 内置工具和工具包

# 工具集成

LangChain 拥有大量第三方工具。请访问 工具集成 查看可用工具的列表。

以维基百科继承作为示例:

!pip install -qU langchain-community wikipedia
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
api_wrapper = WikipediaAPIWrapper (top_k_results=1, doc_content_chars_max=100)
tool = WikipediaQueryRun (api_wrapper=api_wrapper)
>>> print (tool.invoke ({"query": "langchain"}))
Page: LangChain
Summary: LangChain is a framework designed to simplify the creation of applications
>>> print (f"Name: {tool.name}")
Name: wikipedia
>>> print (f"Description: {tool.description}")
Description: A wrapper around Wikipedia. Useful for when you need to answer general questions about people, places, companies, facts, historical events, or other subjects. Input should be a search query.
>>> print (f"args schema: {tool.args}")
args schema: {'query': {'description': 'query to look up on wikipedia', 'title': 'Query', 'type': 'string'}}
>>> print (f"returns directly?: {tool.return_direct}")
returns directly?: False

# 自定义默认工具

我们还可以修改内置的名称、描述和参数的 JSON 模式。

在定义参数的 JSON 模式时,输入必须与函数保持一致,因此不应更改。但您可以轻松为每个输入定义自定义描述。

from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from pydantic import BaseModel, Field
class WikiInputs (BaseModel):
    """Inputs to the wikipedia tool."""
    query: str = Field (
        description="query to look up in Wikipedia, should be 3 or less words"
    )
tool = WikipediaQueryRun (
    name="wiki-tool",
    description="look up things in wikipedia",
    args_schema=WikiInputs,
    api_wrapper=api_wrapper,
    return_direct=True,
)
print (tool.run ("langchain"))

# 内置工具包

工具包是为特定任务设计的工具集合。它们具有方便的加载方法。

所有工具包都具有一个 get_tools 方法,该方法返回工具列表。

您通常应该这样使用它们:

# Initialize a toolkit
toolkit = ExampleTookit (...)
# Get list of tools
tools = toolkit.get_tools ()

# chat model 调用工具

工具调用 允许聊天模型通过 “调用工具” 来响应给定的提示。

请记住,虽然 “工具调用” 这个名称暗示模型直接执行某些操作,但实际上并非如此!模型仅生成工具的参数,实际运行工具(或不运行)取决于用户。

工具调用是一种通用技术,可以从模型生成结构化输出,即使您不打算调用任何工具也可以使用它。一个示例用例是 从非结构化文本中提取。

工具调用并不是通用的,但许多流行的 LLM 提供商支持它。你可以 在这里找到支持工具调用的所有模型列表。

LangChain 实现了定义工具、将其传递给 LLM 以及表示工具调用的标准接口。 本指南将介绍如何将工具绑定到 LLM,然后调用 LLM 生成这些参数。

# 定义工具模式

为了使模型能够调用工具,我们需要传入描述工具功能及其参数的工具模式。支持工具调用功能的聊天模型实现了一个.bind_tools () 方法,用于将工具模式传递给模型。工具模式可以作为 Python 函数(带有类型提示和文档字符串)、Pydantic 模型、TypedDict 类或 LangChain 工具对象传入。模型的后续调用将与提示一起传入这些工具模式。

# Python 函数

我们的工具模式可以是 Python 函数:

def add (a: int, b: int) -> int:
    """Add two integers.
    Args:
        a: First integer
        b: Second integer
    """
    return a + b
def multiply (a: int, b: int) -> int:
    """Multiply two integers.
    Args:
        a: First integer
        b: Second integer
    """
    return a * b

# LangChain 工具

LangChain 还实现了一个 @tool 装饰器,允许进一步控制工具模式,例如工具名称和参数描述。有关详细信息,请参阅 这里 的使用指南。

# Pydantic

您可以等效地使用 Pydantic 定义没有附带函数的模式。

请注意,除非提供默认值,否则所有字段都是 必需 的。

from pydantic import BaseModel, Field
class add (BaseModel):
    """Add two integers."""
    a: int = Field (..., description="First integer")
    b: int = Field (..., description="Second integer")
class multiply (BaseModel):
    """Multiply two integers."""
    a: int = Field (..., description="First integer")
    b: int = Field (..., description="Second integer")

# TypedDict 类

或者使用 TypedDict 和注解:

from typing_extensions import Annotated, TypedDict
class add (TypedDict):
    """Add two integers."""
    # Annotations must have the type and can optionally include a default value and description (in that order).
    a: Annotated [int, ..., "First integer"]
    b: Annotated [int, ..., "Second integer"]
class multiply (TypedDict):
    """Multiply two integers."""
    a: Annotated [int, ..., "First integer"]
    b: Annotated [int, ..., "Second integer"]
tools = [add, multiply]

# 绑定工具到 chat model

要将这些模式实际绑定到聊天模型,我们将使用 .bind_tools () 方法。这将处理将 add 和 multiply 模式转换为模型所需的格式。工具模式将在每次调用模型时传递。

注意:如果你在 chat 中使用了 format="json" 添加格式约束,可能导致调用工具异常

import datetime
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
def add (a: int, b: int) -> int:
    """Add two integers.
    Args:
        a: First integer
        b: Second integer
    """
    return a + b
def multiply (a: int, b: int) -> int:
    """Multiply two integers.
    Args:
        a: First integer
        b: Second integer
    """
    return a * b
@tool
def get_current_date_time () -> str:
    """
    Get the current date and time.
    Args:
        No Args
    Returns:
        str: "YYYY-MM-DD HH:MM:SS"
    """
    # 获取当前日期和时间
    now = datetime.datetime.now ()
    # 格式化日期和时间
    formatted_date_time = now.strftime ("% Y-% m-% d % H:% M:% S")
    return formatted_date_time
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
tools = [add, multiply, get_current_date_time]
llm_with_tools = chat.bind_tools (tools=tools)
query = "现在几点了"
llm_with_tools.invoke (query)
>>> llm_with_tools.invoke (query)
content='' additional_kwargs={} response_metadata={'model': 'qwen2.5:7b', 'created_at': '2025-03-17T08:25:00.0475282Z', 'done': True, 'done_reason': 'stop', 'total_duration': 4076887400, 'load_duration': 18971000, 'prompt_eval_count': 294, 'prompt_eval_duration': 371000000, 'eval_count': 48, 'eval_duration': 3685000000, 'message': Message (role='assistant', content='', images=None, tool_calls=None)} id='run-b9d2824f-6453-47c6-96c3-55a91fc246f9-0' tool_calls=[{'name': 'get_current_date_time', 'args': {}, 'id': 'a31cca26-4768-4a15-858e-b3cf18eb0d26', 'type': 'tool_call'}] usage_metadata={'input_tokens': 294, 'output_tokens': 48, 'total_tokens': 342}

正如我们所看到的,我们的 LLM 生成了工具的参数!您可以查看 文档 中 bind_tools () 的相关内容,了解自定义 LLM 选择工具的所有方法,以及如何 强制 LLM 调用工具 的指南,而不是让它自行决定。

# 工具调用

如果工具调用包含在 LLM 响应中,它们将附加到相应的消息或消息块作为工具调用对象的列表,位于 .tool_calls 属性中。 请注意,聊天模型可以同时调用多个工具。

一个 ToolCall 是一个包含 工具名称、参数值字典和(可选)标识符的类型字典。没有工具调用的消息 默认将此属性设置为空列表。

>>> llm_with_tools.invoke ("现在是几点?").tool_calls
[{'name': 'get_current_date_time', 'args': {}, 'id': 'bca172f5-782a-48c8-9ba9-3719e24bb22e', 'type': 'tool_call'}]
>>> llm_with_tools.invoke ("What is 3 * 12? Also, what is 11 + 49?").tool_calls
[{'name': 'multiply', 'args': {'a': 3, 'b': 12}, 'id': '21b538e0-f201-439b-8cab-0daf737c3e55', 'type': 'tool_call'}, {'name': 'add', 'args': {'a': 11, 'b': 49}, 'id': 'd8261d43-06bd-4ebb-b1de-a84b0673185e', 'type': 'tool_call'}]

.tool_calls 属性应包含有效的工具调用。注意:有时 LLMs 可能会输出格式错误的工具调用(例如,参数不是 有效的 JSON)。在这些情况下解析失败时, InvalidToolCall 的实例 会填充在 .invalid_tool_calls 属性中。一个 InvalidToolCall 可以具有 名称、字符串参数、标识符和错误消息。

# 解析

如果需要,输出解析器可以进一步处理输出。例如,我们可以使用将.tool_calls 中填充的现有值转换为 Pydantic 对象的 PydanticToolsParser:

from langchain_core.output_parsers import PydanticToolsParser
from pydantic import BaseModel, Field
class add (BaseModel):
    """Add two integers."""
    a: int = Field (..., description="First integer")
    b: int = Field (..., description="Second integer")
class multiply (BaseModel):
    """Multiply two integers."""
    a: int = Field (..., description="First integer")
    b: int = Field (..., description="Second integer")
chain = llm_with_tools | PydanticToolsParser (tools=[add, multiply])
chain.invoke (query)

# 工具输出传递给 chat model

Agent 调用工具时的标准逻辑流程:

  1. 首次请求获取工具调用指令

  2. 执行工具并将结果作为新的消息存入历史

  3. 携带完整上下文进行第二次请求生成最终回答

这种模式(工具调用 -> 结果注入 -> 二次推理)是 ReAct 等主流 Agent 框架的设计范式。传递 messages 能够保持对话历史的完整性,使得 LLM 能基于完整上下文进行推理。OpenAI Assistant API、LangChain Agent 等主流实现都采用这种模式。

首先,让我们定义我们的工具和模型,将模型调用工具视为对话历史并添加到消息列表

import datetime
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage
@tool
def get_current_date_time () -> str:
    """
    Get the current date and time.
    Args:
        No Args
    Returns:
        str: "YYYY-MM-DD HH:MM:SS"
    """
    now = datetime.datetime.now ()
    formatted_date_time = now.strftime ("% Y-% m-% d % H:% M:% S")
    return formatted_date_time
tools = [get_current_date_time]
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
llm_with_tools = chat.bind_tools (tools=tools)
query = "现在几点了"
# 定西对话历史消息列表
messages = [HumanMessage (query)]
# ai 回复
ai_msg = llm_with_tools.invoke (query)
print (ai_msg.tool_calls)
messages.append (ai_msg)

接下来,让我们使用模型填充的参数调用工具函数

方便的是,如果我们使用 ToolCall 调用 LangChain Tool,我们将自动获得一个可以反馈给模型的 ToolMessage:

for tool_call in ai_msg.tool_calls:
    selected_tool = {"get_current_date_time": get_current_date_time}[tool_call ["name"].lower ()]
    tool_msg = selected_tool.invoke (tool_call)
    messages.append (tool_msg)

此时,messages 中将会 调用 tools 后返回内容的消息记录,再将 message 传给 chat model,模型将使用此信息生成对我们原始查询的最终答案

llm_with_tools.invoke (messages)

请注意,每个 ToolMessage 必须包含一个与模型生成的原始工具调用中的 id 匹配的 tool_call_id。这有助于模型将工具响应与工具调用匹配。

# 运行时传递值给工具

您可能需要将仅在运行时已知的值绑定到工具。例如,工具逻辑可能需要使用发出请求的用户的 ID。

大多数情况下,这些值不应由大型语言模型(LLM)控制。实际上,允许 LLM 控制用户 ID 可能会导致安全风险。

相反,LLM 应仅控制工具中应由 LLM 控制的参数,而其他参数(如用户 ID)应由应用程序逻辑固定。

本章节演示如何防止模型生成某些工具参数并在运行时直接注入它们。

# 隐藏模型参数

我们可以使用 InjectedToolArg 注解来标记我们工具的某些参数,例如 user_id ,表示它们在运行时被注入,意味着它们不应该由模型生成

from typing import List
from langchain_core.tools import InjectedToolArg, tool
from typing_extensions import Annotated
user_to_pets = {}
@tool (parse_docstring=True)
def update_favorite_pets (
    pets: List [str], user_id: Annotated [str, InjectedToolArg]
) -> None:
    """Add the list of favorite pets.
    Args:
        pets: List of favorite pets to set.
        user_id: User's ID.
    """
    user_to_pets [user_id] = pets
@tool (parse_docstring=True)
def delete_favorite_pets (user_id: Annotated [str, InjectedToolArg]) -> None:
    """Delete the list of favorite pets.
    Args:
        user_id: User's ID.
    """
    if user_id in user_to_pets:
        del user_to_pets [user_id]
@tool (parse_docstring=True)
def list_favorite_pets (user_id: Annotated [str, InjectedToolArg]) -> None:
    """List favorite pets if any.
    Args:
        user_id: User's ID.
    """
    return user_to_pets.get (user_id, [])
from langchain_ollama import ChatOllama
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
tools = [update_favorite_pets, delete_favorite_pets, list_favorite_pets]

如果我们查看这些工具的输入模式,我们会看到 user_id 仍然被列出:

>>> update_favorite_pets.get_input_schema ().schema ()
{'description': 'Add the list of favorite pets.',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'},
  'user_id': {'description': "User's ID.",
   'title': 'User Id',
   'type': 'string'}},
 'required': ['pets', 'user_id'],
 'title': 'update_favorite_petsSchema',
 'type': 'object'}

但是如果我们查看工具调用模式,也就是传递给模型进行工具调用的内容,user_id 已被移除:

>>> update_favorite_pets.tool_call_schema.schema ()
{'description': 'Add the list of favorite pets.',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'}},
 'required': ['pets'],
 'title': 'update_favorite_pets',
 'type': 'object'}

所以当我们调用我们的工具时,我们需要传入 user_id:

user_id = "123"
update_favorite_pets.invoke ({"pets": ["lizard", "dog"], "user_id": user_id})
>>> print (user_to_pets)
{'123': ['lizard', 'dog']}
>>> print (list_favorite_pets.invoke ({"user_id": user_id}))
['lizard', 'dog']

但是当模型调用工具时,不会生成 user_id 参数:

tools = [
    update_favorite_pets,
    delete_favorite_pets,
    list_favorite_pets,
]
llm_with_tools = llm.bind_tools (tools)
ai_msg = llm_with_tools.invoke ("my favorite animals are cats and parrots")
ai_msg.tool_calls
[{'name': 'update_favorite_pets',
  'args': {'pets': ['cats', 'parrots']},
  'id': 'call_pZ6XVREGh1L0BBSsiGIf1xVm',
  'type': 'tool_call'}]

# 在运行时注入参数

如果我们想要实际使用模型生成的工具调用来执行我们的工具,我们需要自己注入 user_id:

from copy import deepcopy
from langchain_core.runnables import chain
@chain
def inject_user_id (ai_msg):
    tool_calls = []
    for tool_call in ai_msg.tool_calls:
        tool_call_copy = deepcopy (tool_call)
        tool_call_copy ["args"]["user_id"] = user_id
        tool_calls.append (tool_call_copy)
    return tool_calls
>>> inject_user_id.invoke (ai_msg)
[{'name': 'update_favorite_pets',
  'args': {'pets': ['cats', 'parrots'], 'user_id': '123'},
  'id': 'call_pZ6XVREGh1L0BBSsiGIf1xVm',
  'type': 'tool_call'}]

现在我们可以将我们的模型、注入代码和实际工具链在一起,创建一个工具执行链:

tool_map = {tool.name: tool for tool in tools}
@chain
def tool_router (tool_call):
    return tool_map [tool_call ["name"]]
chain = llm_with_tools | inject_user_id | tool_router.map ()
chain.invoke ("my favorite animals are cats and parrots")

完整代码如下

from typing import List
from langchain_ollama import ChatOllama
from langchain_core.tools import InjectedToolArg, tool
from typing_extensions import Annotated
from copy import deepcopy
from langchain_core.runnables import chain
@tool (parse_docstring=True)
def update_favorite_pets (
    pets: List [str], user_id: Annotated [str, InjectedToolArg]
) -> None:
    """Add the list of favorite pets.
    Args:
        pets: List of favorite pets to set.
        user_id: User's ID.
    """
    user_to_pets [user_id] = pets
@tool (parse_docstring=True)
def delete_favorite_pets (user_id: Annotated [str, InjectedToolArg]) -> None:
    """Delete the list of favorite pets.
    Args:
        user_id: User's ID.
    """
    if user_id in user_to_pets:
        del user_to_pets [user_id]
@tool (parse_docstring=True)
def list_favorite_pets (user_id: Annotated [str, InjectedToolArg]) -> None:
    """List favorite pets if any.
    Args:
        user_id: User's ID.
    """
    return user_to_pets.get (user_id, [])
@chain
def inject_user_id (ai_msg):
    tool_calls = []
    for tool_call in ai_msg.tool_calls:
        tool_call_copy = deepcopy (tool_call)
        tool_call_copy ["args"]["user_id"] = user_id
        tool_calls.append (tool_call_copy)
    return tool_calls
@chain
def tool_router (tool_call):
    return tool_map [tool_call ["name"]]
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
user_to_pets = {}
user_id = "123"
tools = [update_favorite_pets, delete_favorite_pets, list_favorite_pets]
tool_map = {tool.name: tool for tool in tools}
llm_with_tools = chat.bind_tools (tools=tools)
query = "my favorite animals are cats and parrots"
chain = llm_with_tools | inject_user_id | tool_router.map ()
response = chain.invoke ("my favorite animals are cats and parrots")
print (user_to_pets)

# 注释参数的其他方法

以下是注释我们工具参数的几种其他方法

from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field
class UpdateFavoritePetsSchema (BaseModel):
    """Update list of favorite pets"""
    pets: List [str] = Field (..., description="List of favorite pets to set.")
    user_id: Annotated [str, InjectedToolArg] = Field (..., description="User's ID.")
@tool (args_schema=UpdateFavoritePetsSchema)
def update_favorite_pets (pets, user_id):
    user_to_pets [user_id] = pets
>>> update_favorite_pets.get_input_schema ().schema ()
{'description': 'Update list of favorite pets',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'},
  'user_id': {'description': "User's ID.",
   'title': 'User Id',
   'type': 'string'}},
 'required': ['pets', 'user_id'],
 'title': 'UpdateFavoritePetsSchema',
 'type': 'object'}
>>> update_favorite_pets.tool_call_schema.schema ()
{'description': 'Update list of favorite pets',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'}},
 'required': ['pets'],
 'title': 'update_favorite_pets',
 'type': 'object'}
from typing import Optional, Type
class UpdateFavoritePets (BaseTool):
    name: str = "update_favorite_pets"
    description: str = "Update list of favorite pets"
    args_schema: Optional [Type [BaseModel]] = UpdateFavoritePetsSchema
    def _run (self, pets, user_id):
        user_to_pets [user_id] = pets
>>> UpdateFavoritePets ().get_input_schema ().schema ()
{'description': 'Update list of favorite pets',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'},
  'user_id': {'description': "User's ID.",
   'title': 'User Id',
   'type': 'string'}},
 'required': ['pets', 'user_id'],
 'title': 'UpdateFavoritePetsSchema',
 'type': 'object'}
>>> UpdateFavoritePets ().tool_call_schema.schema ()
{'description': 'Update list of favorite pets',
 'properties': {'pets': {'description': 'List of favorite pets to set.',
   'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'}},
 'required': ['pets'],
 'title': 'update_favorite_pets',
 'type': 'object'}
class UpdateFavoritePets2 (BaseTool):
    name: str = "update_favorite_pets"
    description: str = "Update list of favorite pets"
    def _run (self, pets: List [str], user_id: Annotated [str, InjectedToolArg]) -> None:
        user_to_pets [user_id] = pets
>>> UpdateFavoritePets2 ().get_input_schema ().schema ()
{'description': 'Use the tool.\n\nAdd run_manager: Optional [CallbackManagerForToolRun] = None\nto child implementations to enable tracing.',
 'properties': {'pets': {'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'},
  'user_id': {'title': 'User Id', 'type': 'string'}},
 'required': ['pets', 'user_id'],
 'title': 'update_favorite_petsSchema',
 'type': 'object'}
>>> UpdateFavoritePets2 ().tool_call_schema.schema ()
{'description': 'Update list of favorite pets',
 'properties': {'pets': {'items': {'type': 'string'},
   'title': 'Pets',
   'type': 'array'}},
 'required': ['pets'],
 'title': 'update_favorite_pets',
 'type': 'object'}

# 为工具调用添加人工审批

有些工具我们不信任模型单独执行。在这种情况下,我们可以在调用工具之前要求人类批准。

要构建生产应用程序,您需要做更多工作以适当地跟踪应用程序状态。我们建议使用 langgraph 来支持此功能。

# 设置

安装软件包

% pip install --upgrade --quiet langchain

设置环境变量

import getpass
import os
# If you'd like to use LangSmith, uncomment the below:
# os.environ ["LANGCHAIN_TRACING_V2"] = "true"
# os.environ ["LANGCHAIN_API_KEY"] = getpass.getpass ()

现在,让我们先写一个简单的工具和工具调用链

from typing import Dict, List
from langchain_ollama import ChatOllama
from langchain_core.messages import AIMessage
from langchain_core.runnables import Runnable, RunnablePassthrough
from langchain_core.tools import tool
@tool
def count_emails (last_n_days: int) -> int:
    """Dummy function to count number of e-mails. Returns 2 * last_n_days."""
    return last_n_days * 2
@tool
def send_email (message: str, recipient: str) -> str:
    """Dummy function for sending an e-mail."""
    return f"Successfully sent email to {recipient}."
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
tools = [count_emails, send_email]
llm_with_tools = chat.bind_tools (tools)
def call_tools (msg: AIMessage) -> List [Dict]:
    """Simple sequential tool calling helper."""
    tool_map = {tool.name: tool for tool in tools}
    tool_calls = msg.tool_calls.copy ()
    for tool_call in tool_calls:
        tool_call ["output"] = tool_map [tool_call ["name"]].invoke (tool_call ["args"])
    return tool_calls
chain = llm_with_tools | call_tools
response = chain.invoke ("how many emails did i get in the last 5 days?")
>>> print (response)
[{'name': 'count_emails', 'args': {'last_n_days': 5}, 'id': '4f47387b-56ff-4c7e-90e8-511724f81ea6', 'type': 'tool_call', 'output': 10}]

# 添加人工审批

让我们在链中添加一个步骤,询问一个人是否批准或拒绝该调用请求。

在拒绝时,该步骤将引发异常,停止执行链的其余部分。

import json
class NotApproved (Exception):
    """Custom exception."""
def human_approval (msg: AIMessage) -> AIMessage:
    """Responsible for passing through its input or raising an exception.
    Args:
        msg: output from the chat model
    Returns:
        msg: original output from the msg
    """
    tool_strs = "\n\n".join (
        json.dumps (tool_call, indent=2) for tool_call in msg.tool_calls
    )
    input_msg = (
        f"Do you approve of the following tool invocations\n\n {tool_strs}\n\n"
        "Anything except 'Y'/'Yes' (case-insensitive) will be treated as a no.\n >>>"
    )
    resp = input (input_msg)
    if resp.lower () not in ("yes", "y"):
        raise NotApproved (f"Tool invocations not approved:\n\n {tool_strs}")
    return msg
chain = llm_with_tools | human_approval | call_tools
try:
    response = chain.invoke ("Send sally@gmail.com an email saying 'What's up homie'")
    print (response)
except NotApproved as e:
    print ()
    print (e)
Do you approve of the following tool invocations
{
  "name": "count_emails",
  "args": {
    "last_n_days": 5
  },
  "id": "toolu_01WbD8XeMoQaRFtsZezfsHor"
}
Anything except 'Y'/'Yes' (case-insensitive) will be treated as a no.
 >>>

# 处理工具错误

使用大型语言模型调用工具通常比纯提示更可靠,但并不完美。模型可能会尝试调用不存在的工具,或者未能返回与请求的模式匹配的参数。保持模式简单、减少一次传递的工具数量,以及使用良好的名称和描述等策略可以帮助降低这种风险,但并非万无一失。

本章涵盖了一些将错误处理构建到您的链中的方法,以减少这些失败。

首先,依然定义一个 chain ,我们将故意使我们的工具复杂,以试图让模型出错。

from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage
@tool
def complex_tool (int_arg: int, float_arg: float, dict_arg: dict) -> int:
    """Do something complex with a complex tool."""
    return int_arg * float_arg
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
llm_with_tools = chat.bind_tools (
    [complex_tool],
)
chain = llm_with_tools | (lambda msg: msg.tool_calls [0]["args"]) | complex_tool
response = chain.invoke ("use complex tool. the args are 5, 2.1, empty dictionary.") # don't forget dict_arg
print (response)

# try/Except 工具调用

处理错误的最简单方法是对工具调用步骤进行 try/except,并在出现错误时返回有帮助的消息:

from typing import Any
from langchain_core.runnables import Runnable, RunnableConfig
def try_except_tool (tool_args: dict, config: RunnableConfig) -> Runnable:
    try:
        complex_tool.invoke (tool_args, config=config)
    except Exception as e:
        return f"Calling tool with arguments:\n\n {tool_args}\n\nraised the following error:\n\n {type (e)}: {e}"
chain = llm_with_tools | (lambda msg: msg.tool_calls [0]["args"]) | try_except_tool
print (
    chain.invoke (
        "use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg"
    )
)

# 回退

在工具调用错误的情况下,我们也可以尝试回退到更好的模型。在这种情况下,我们将回退到一个使用 gpt-4-1106-preview 的相同链,而不是 gpt-3.5-turbo。

chain = llm_with_tools | (lambda msg: msg.tool_calls [0]["args"]) | complex_tool
better_model = ChatOpenAI (model="gpt-4-1106-preview", temperature=0).bind_tools (
    [complex_tool], tool_choice="complex_tool"
)
better_chain = better_model | (lambda msg: msg.tool_calls [0]["args"]) | complex_tool
chain_with_fallback = chain.with_fallbacks ([better_chain])
chain_with_fallback.invoke (
    "use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg"
)

# 带异常重试

进一步说,我们可以尝试自动重新运行链,并传入异常,以便模型能够纠正其行为:

from langchain_core.messages import AIMessage, HumanMessage, ToolCall, ToolMessage
from langchain_core.prompts import ChatPromptTemplate
class CustomToolException (Exception):
    """Custom LangChain tool exception."""
    def __init__(self, tool_call: ToolCall, exception: Exception) -> None:
        super ().__init__()
        self.tool_call = tool_call
        self.exception = exception
def tool_custom_exception (msg: AIMessage, config: RunnableConfig) -> Runnable:
    try:
        return complex_tool.invoke (msg.tool_calls [0]["args"], config=config)
    except Exception as e:
        raise CustomToolException (msg.tool_calls [0], e)
def exception_to_messages (inputs: dict) -> dict:
    exception = inputs.pop ("exception")
    # Add historical messages to the original input, so the model knows that it made a mistake with the last tool call.
    messages = [
        AIMessage (content="", tool_calls=[exception.tool_call]),
        ToolMessage (
            tool_call_id=exception.tool_call ["id"], content=str (exception.exception)
        ),
        HumanMessage (
            content="The last tool call raised an exception. Try calling the tool again with corrected arguments. Do not repeat mistakes."
        ),
    ]
    inputs ["last_output"] = messages
    return inputs
# We add a last_output MessagesPlaceholder to our prompt which if not passed in doesn't
# affect the prompt at all, but gives us the option to insert an arbitrary list of Messages
# into the prompt if needed. We'll use this on retries to insert the error message.
prompt = ChatPromptTemplate.from_messages (
    [("human", "{input}"), ("placeholder", "{last_output}")]
)
chain = prompt | llm_with_tools | tool_custom_exception
# If the initial chain call fails, we rerun it withe the exception passed in as a message.
self_correcting_chain = chain.with_fallbacks (
    [exception_to_messages | chain], exception_key="exception"
)
self_correcting_chain.invoke (
    {
        "input": "use complex tool. the args are 5, 2.1, empty dictionary. don't forget dict_arg"
    }
)

# 强制模型调用工具

为了强制我们的 LLM 选择特定工具,我们可以使用 tool_choice 参数来确保某种行为。首先,让我们定义我们的模型和工具:

from langchain_core.tools import tool
@tool
def add (a: int, b: int) -> int:
    """Adds a and b."""
    return a + b
@tool
def multiply (a: int, b: int) -> int:
    """Multiplies a and b."""
    return a * b
tools = [add, multiply]

例如,我们可以通过使用以下代码强制我们的工具调用乘法工具:

llm_forced_to_multiply = llm.bind_tools (tools, tool_choice="Multiply")
llm_forced_to_multiply.invoke ("what is 2 + 4")

即使我们传递给它的内容不需要乘法 - 它仍然会调用该工具!

我们还可以通过将 “any”(或 “required”,这是 OpenAI 特定的)关键字传递给 tool_choice 参数,强制我们的工具选择至少一个工具。

llm_forced_to_use_tool = llm.bind_tools (tools, tool_choice="any")
llm_forced_to_use_tool.invoke ("What day is today?")

# 禁用并行工具调用

OpenAI 工具调用默认以并行方式执行工具调用。这意味着如果我们问一个问题,比如 “东京、纽约和芝加哥的天气如何?”,并且我们有一个获取天气的工具,它将并行调用该工具 3 次。我们可以通过使用 parallel_tool_call 参数强制它仅调用一个工具一次。

首先,让我们设置我们的工具和模型:

from langchain_core.tools import tool
@tool
def add (a: int, b: int) -> int:
    """Adds a and b."""
    return a + b
@tool
def multiply (a: int, b: int) -> int:
    """Multiplies a and b."""
    return a * b
tools = [add, multiply]
import os
from getpass import getpass
from langchain_openai import ChatOpenAI
if "OPENAI_API_KEY" not in os.environ:
    os.environ ["OPENAI_API_KEY"] = getpass ()
llm = ChatOpenAI (model="gpt-4o-mini", temperature=0)

现在让我们快速展示一下如何禁用并行工具调用的示例:

llm_with_tools = llm.bind_tools (tools, parallel_tool_calls=False)
llm_with_tools.invoke ("Please call the first tool two times").tool_calls

正如我们所看到的,尽管我们明确告诉模型调用工具两次,但通过禁用并行工具调用,模型被限制为只能调用一次。

# 从工具访问 RunnableConfig

如果您有一个调用聊天模型、检索器或其他可运行的工具,您可能希望访问这些可运行的内部事件或使用附加属性进行配置。本指南将向您展示如何手动正确传递参数,以便您可以使用 astream_events () 方法做到这一点。

工具是可运行的,您可以在接口级别将它们视为任何其他可运行的工具 - 您可以像往常一样调用 invoke ()、batch () 和 stream ()。然而,在编写自定义工具时,您可能希望调用其他可运行的工具,如聊天模型或检索器。为了正确追踪和配置这些子调用,您需要手动访问并传递工具当前的 RunnableConfig 对象。本指南将向您展示一些如何做到这一点的示例。

# 按参数类型推断

要从您的自定义工具访问活动配置对象,您需要在工具的签名中添加一个参数,类型为 RunnableConfig。当您调用工具时,LangChain 将检查工具的签名,查找类型为 RunnableConfig 的参数,如果存在,将用正确的值填充该参数。

注意: 参数的实际名称无关紧要,只有类型才重要。

为了说明这一点,定义一个自定义工具,该工具接受两个参数 - 一个类型为字符串,另一个类型为 RunnableConfig:

from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
@tool
async def reverse_tool (text: str, special_config_param: RunnableConfig) -> str:
    """A test tool that combines input text with a configurable parameter."""
    return (text + special_config_param ["configurable"]["additional_field"])[::-1]

然后,如果我们调用一个包含 configurable 字段的 config 的工具,我们可以看到 additional_field 被正确传递:

await reverse_tool.ainvoke (
    {"text": "abc"}, config={"configurable": {"additional_field": "123"}}
)

# 从工具中流式传输事件

如果您有调用聊天模型、检索器或其他可运行对象的工具,您可能希望访问这些可运行对象的内部事件或使用附加属性对其进行配置。本指南将向您展示如何正确手动传递参数,以便您可以使用 astream_events () 方法来实现这一点。

如果您在 python <= 3.10 中运行 async 代码,LangChain 无法自动传播配置,包括 astream_events () 所需的回调到子可运行对象。这是您可能无法看到自定义可运行对象或工具发出事件的常见原因,您需要手动将 RunnableConfig 对象传播到异步环境中的子可运行对象。
如果您在 python>=3.11 中运行,RunnableConfig 将自动传播到异步环境中的子可运行对象。然而,如果您的代码可能在较旧的 Python 版本中运行,手动传播 RunnableConfig 仍然是个好主意。

假设你有一个自定义工具,它调用一个链,通过提示聊天模型仅返回 10 个单词来压缩其输入,然后反转输出。首先,以一种简单的方式定义它:

from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
import asyncio
@tool
async def special_summarization_tool (long_text: str) -> str:
    """A tool that summarizes input text using advanced techniques."""
    prompt = ChatPromptTemplate.from_template (
        "You are an expert writer. Summarize the following text in 10 words or less:\n\n {long_text}"
    )
    def reverse (x: str):
        return x [::-1]
    chain = prompt | chat | StrOutputParser () | reverse
    summary = await chain.ainvoke ({"long_text": long_text})
    return summary
async def main ():
    LONG_TEXT = """
    NARRATOR:
    (Black screen with text; The sound of buzzing bees can be heard)
    According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible.
    BARRY BENSON:
    (Barry is picking out a shirt)
    Yellow, black. Yellow, black. Yellow, black. Yellow, black. Ooh, black and yellow! Let's shake it up a little.
    JANET BENSON:
    Barry! Breakfast is ready!
    BARRY:
    Coming! Hang on a second.
    """
    response = await special_summarization_tool.ainvoke ({"long_text": LONG_TEXT})
    print (response)
# 初始化模型,ollama 接口格式
chat = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)
asyncio.run (main ())

但是如果你想访问聊天模型的原始输出而不是完整的工具,你可以尝试使用 astream_events () 方法并寻找 on_chat_model_end 事件

async def main ():
    LONG_TEXT = """
    NARRATOR:
    (Black screen with text; The sound of buzzing bees can be heard)
    According to all known laws of aviation, there is no way a bee should be able to fly. Its wings are too small to get its fat little body off the ground. The bee, of course, flies anyway because bees don't care what humans think is impossible.
    BARRY BENSON:
    (Barry is picking out a shirt)
    Yellow, black. Yellow, black. Yellow, black. Yellow, black. Ooh, black and yellow! Let's shake it up a little.
    JANET BENSON:
    Barry! Breakfast is ready!
    BARRY:
    Coming! Hang on a second.
    """
    stream = special_summarization_tool.astream_events (
    {"long_text": LONG_TEXT}, version="v2"
    )
    async for event in stream:
        if event ["event"] == "on_chat_model_end":
            # Never triggers in python<=3.10!
            print (event)
    response = await special_summarization_tool.ainvoke ({"long_text": LONG_TEXT})

你会注意到(除非你在 python>=3.11 中运行此指南),子运行没有发出聊天模型事件!

这是因为上面的示例没有将工具的配置对象传递到内部链中。要解决此问题,请重新定义你的工具,使其接受一个特殊参数,类型为 RunnableConfig。在执行内部链时,你还需要将该参数传递进去:

from langchain_core.runnables import RunnableConfig
@tool
async def special_summarization_tool_with_config (
    long_text: str, config: RunnableConfig
) -> str:
    """A tool that summarizes input text using advanced techniques."""
    prompt = ChatPromptTemplate.from_template (
        "You are an expert writer. Summarize the following text in 10 words or less:\n\n {long_text}"
    )
    def reverse (x: str):
        return x [::-1]
    chain = prompt | chat | StrOutputParser () | reverse
    # Pass the "config" object as an argument to any executed runnables
    summary = await chain.ainvoke ({"long_text": long_text}, config=config)
    return summary

现在尝试使用你的新工具进行相同的 astream_events () 调用,会触发事件

对于流式处理,astream_events () 会自动在启用流式处理的情况下调用链中的内部可运行对象,因此如果你想在聊天模型生成时流式传输令牌,你可以简单地过滤以查找 on_chat_model_stream 事件,而无需其他更改:

stream = special_summarization_tool_with_config.astream_events (
    {"long_text": LONG_TEXT}, version="v2"
)
async for event in stream:
    if event ["event"] == "on_chat_model_stream":
        print (event)
response = await special_summarization_tool_with_config.ainvoke ({"long_text": LONG_TEXT})
print (response)

# 从工具返回工件

工具是可以被模型调用的实用程序,其输出旨在反馈给模型。然而,有时工具执行的工件我们希望能够让下游组件访问,但又不想将其暴露给模型本身。例如,如果一个工具返回一个自定义对象、数据框或图像,我们可能希望将一些关于该输出的元数据传递给模型,而不传递实际的输出。同时,我们可能希望能够在其他地方访问这个完整的输出,例如在下游工具中。

Tool 和 ToolMessage 接口使得能够区分工具输出中用于模型的部分(这是 ToolMessage.content)和用于模型外部的部分(ToolMessage.artifact)。

# 定义工具

如果我们希望我们的工具区分消息内容和其他工件,我们需要在定义工具时指定 response_format="content_and_artifact",并确保返回一个元组 (content, artifact):

import random
from typing import List, Tuple
from langchain_core.tools import tool
@tool (response_format="content_and_artifact")
def generate_random_ints (min: int, max: int, size: int) -> Tuple [str, List [int]]:
    """Generate size random ints in the range [min, max]."""
    array = [random.randint (min, max) for _ in range (size)]
    content = f"Successfully generated array of {size} random ints in [{min}, {max}]."
    return content, array

# 使用 ToolCall 调用工具

如果我们直接使用工具参数调用工具,您会注意到我们只返回了工具输出的内容部分:

>>> generate_random_ints.invoke ({"min": 0, "max": 9, "size": 10})
'Successfully generated array of 10 random ints in [0, 9].'

为了同时获取内容和工件,我们需要使用 ToolCall 调用我们的模型(这只是一个包含 "name"、"args"、"id" 和 "type" 键的字典),它包含生成 ToolMessage 所需的额外信息,例如工具调用 ID:

generate_random_ints.invoke (
    {
        "name": "generate_random_ints",
        "args": {"min": 0, "max": 9, "size": 10},
        "id": "123",  # required
        "type": "tool_call",  # required
    }
)

# 与模型一起使用

使用 工具调用模型,我们可以轻松使用模型调用我们的工具并生成 ToolMessages:

import getpass
import os
os.environ ["OPENAI_API_KEY"] = getpass.getpass ()
from langchain_openai import ChatOpenAI
llm = ChatOpenAI (model="gpt-4o-mini")
llm_with_tools = llm.bind_tools ([generate_random_ints])
ai_msg = llm_with_tools.invoke ("generate 6 positive ints less than 25")
ai_msg.tool_calls
generate_random_ints.invoke (ai_msg.tool_calls [0])

如果我们只传入工具调用参数,我们只会得到内容:

generate_random_ints.invoke (ai_msg.tool_calls [0]["args"])
'Successfully generated array of 6 random ints in [1, 24].'

如果我们想要声明性地创建一个链,我们可以这样做:

from operator import attrgetter
chain = llm_with_tools | attrgetter ("tool_calls") | generate_random_ints.map ()
chain.invoke ("give me a random number between 1 and 5")

# 从 BaseTool 类创建

如果您想直接创建一个 BaseTool 对象,而不是使用 @tool 装饰一个函数,可以这样做:

from langchain_core.tools import BaseTool
class GenerateRandomFloats (BaseTool):
    name: str = "generate_random_floats"
    description: str = "Generate size random floats in the range [min, max]."
    response_format: str = "content_and_artifact"
    ndigits: int = 2
    def _run (self, min: float, max: float, size: int) -> Tuple [str, List [float]]:
        range_ = max - min
        array = [
            round (min + (range_ * random.random ()), ndigits=self.ndigits)
            for _ in range (size)
        ]
        content = f"Generated {size} floats in [{min}, {max}], rounded to {self.ndigits} decimals."
        return content, array
    # Optionally define an equivalent async method
    # async def _arun (self, min: float, max: float, size: int) -> Tuple [str, List [float]]:
    #     ...
rand_gen = GenerateRandomFloats (ndigits=4)
rand_gen.invoke ({"min": 0.1, "max": 3.3333, "size": 3})
rand_gen.invoke (
    {
        "name": "generate_random_floats",
        "args": {"min": 0.1, "max": 3.3333, "size": 3},
        "id": "123",
        "type": "tool_call",
    }
)

# 将可运行对象转换为工具

在这里,我们将演示如何将一个 LangChain 可运行对象 转换为可以被 agent、链或聊天模型使用的工具。

注意:本指南需要 langchain-core >= 0.2.13。我们还将使用 OpenAI 进行嵌入,但任何 LangChain 嵌入都应该足够。我们将使用一个简单的 LangGraph 代理进行演示。

LangChain 工具 ——BaseTool 的实例 —— 是具有额外约束的 可运行对象,使其能够被语言模型有效调用:

  • 它们的输入被限制为可序列化,特别是字符串和 Python dict 对象;
  • 它们包含名称和描述,指示如何以及何时使用;
  • 它们可能包含详细的 args_schema 以定义其参数。也就是说,虽然一个工具(作为 Runnable)可能接受单个 dict 输入,但填充 dict 所需的特定键和类型信息应在 args_schema 中指定。

接受字符串或 dict 输入的可运行对象可以使用 as_tool 方法转换为工具,该方法允许为参数指定名称、描述和额外的模式信息。

# 基本用法

使用类型化的 dict 输入:

from typing import List
from langchain_core.runnables import RunnableLambda
from typing_extensions import TypedDict
class Args (TypedDict):
    a: int
    b: List [int]
def f (x: Args) -> str:
    return str (x ["a"] * max (x ["b"]))
runnable = RunnableLambda (f)
as_tool = runnable.as_tool (
    name="My tool",
    description="Explanation of when to use tool.",
)
print (as_tool.description)
as_tool.args_schema.schema ()
as_tool.invoke ({"a": 3, "b": [1, 2]})

在没有类型信息的情况下,可以通过 arg_types 指定参数类型:

from typing import Any, Dict
def g (x: Dict [str, Any]) -> str:
    return str (x ["a"] * max (x ["b"]))
runnable = RunnableLambda (g)
as_tool = runnable.as_tool (
    name="My tool",
    description="Explanation of when to use tool.",
    arg_types={"a": int, "b": List [int]},
)

或者,可以通过直接传递所需的 args_schema 完全指定模式:

from pydantic import BaseModel, Field
class GSchema (BaseModel):
    """Apply a function to an integer and list of integers."""
    a: int = Field (..., description="Integer")
    b: List [int] = Field (..., description="List of ints")
runnable = RunnableLambda (g)
as_tool = runnable.as_tool (GSchema)

字符串输入也被支持:

def f (x: str) -> str:
    return x + "a"
def g (x: str) -> str:
    return x + "z"
runnable = RunnableLambda (f) | g
as_tool = runnable.as_tool ()
as_tool.invoke ("b")

# 在 Agent 中使用

下面我们将把 LangChain 可运行组件作为工具整合到 Agent 应用中。我们将通过以下内容进行演示:

  • 一个文档 检索器;
  • 一个简单的 RAG 链,允许 Agent 将相关查询委托给它。

我们首先实例化一个支持 工具调用 的聊天模型:

from langchain_ollama import ChatOllama
llm = ChatOllama (
    model="qwen2.5:7b",
    base_url="http://127.0.0.1:11434/",
)

根据 RAG 教程,我们首先构建一个检索器:

from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_openai import OpenAIEmbeddings
documents = [
    Document (
        page_content="Dogs are great companions, known for their loyalty and friendliness.",
    ),
    Document (
        page_content="Cats are independent pets that often enjoy their own space.",
    ),
]
vectorstore = InMemoryVectorStore.from_documents (
    documents, embedding=OpenAIEmbeddings ()
)
retriever = vectorstore.as_retriever (
    search_type="similarity",
    search_kwargs={"k": 1},
)

接下来我们使用一个简单的预构建 LangGraph 代理 并为其提供工具:

from langgraph.prebuilt import create_react_agent
tools = [
    retriever.as_tool (
        name="pet_info_retriever",
        description="Get information about pets.",
    )
]
agent = create_react_agent (llm, tools)
for chunk in agent.stream ({"messages": [("human", "What are dogs known for?")]}):
    print (chunk)
    print ("----")

进一步,我们可以创建一个简单的 RAG 链,它接受一个额外的参数 —— 在这里是答案的 “风格”。

from operator import itemgetter
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
system_prompt = """
You are an assistant for question-answering tasks.
Use the below context to answer the question. If
you don't know the answer, say you don't know.
Use three sentences maximum and keep the answer
concise.
Answer in the style of {answer_style}.
Question: {question}
Context: {context}
"""
prompt = ChatPromptTemplate.from_messages ([("system", system_prompt)])
rag_chain = (
    {
        "context": itemgetter ("question") | retriever,
        "question": itemgetter ("question"),
        "answer_style": itemgetter ("answer_style"),
    }
    | prompt
    | llm
    | StrOutputParser ()
)

请注意,我们的链的输入模式包含所需的参数,因此它转换为一个工具而无需进一步说明:

rag_chain.input_schema.schema ()
rag_tool = rag_chain.as_tool (
    name="pet_expert",
    description="Get information about pets.",
)

下面我们再次调用代理。请注意,代理在其 tool_calls 中填充所需的参数:

agent = create_react_agent (llm, [rag_tool])
for chunk in agent.stream (
    {"messages": [("human", "What would a pirate say dogs are known for?")]}
):
    print (chunk)
    print ("----")

# 临时工具调用

对于不原生支持工具调用的模型,可以为聊天模型添加临时工具调用支持,这是一种调用工具的替代方法,其原理为 编写一个 prompt,指定模型可以访问的工具、这些工具的参数以及模型的期望输出格式。

目前大多数 LLMs 都支持工具调用,如果你需要对不支持原生工具调用的 LLMs 进行该操作,请查看 文档

# 传递不被追踪的秘密对象

我们可以使用 RunnableConfig 在运行时将机密传递给我们的可运行对象。具体来说,我们可以将带有 __ 前缀的机密传递给 configurable 字段。这将确保这些机密不会作为调用的一部分被追踪:

from langchain_core.runnables import RunnableConfig
from langchain_core.tools import tool
@tool
def foo (x: int, config: RunnableConfig) -> int:
    """Sum x and a secret int"""
    return x + config ["configurable"]["__top_secret_int"]
foo.invoke ({"x": 5}, {"configurable": {"__top_secret_int": 2, "traced_key": "bar"}})

# Use cases (案例)

# 简单的 LLM 应用

在这个快速入门中,我们将向您展示如何使用 LangChain 构建一个简单的 LLM 应用。这个应用将把文本从英语翻译成另一种语言。这是一个相对简单的 LLM 应用 - 只需一次 LLM 调用加上一些提示。尽管如此,这是一个很好的开始使用 LangChain 的方式 - 许多功能只需一些提示和一次 LLM 调用就可以构建!

案例中涉及到的知识点包含

  • 聊天模型
  • 提示词模板 和 输出解析器
  • LangChain 表达式 (LCEL) 将组件串联在一起
  • LangServe 部署应用

# 使用语言模型

首先,初始化一个模型,本次示例中我们使用 ollama 接口下的模型

from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage, SystemMessage
model = ChatOllama (model="qwen2.5:7b", base_url="http://localhost:11434")
messages = [
    SystemMessage (content="Translate the following from English into Italian"),
    HumanMessage (content="hi!"),
]
result = model.invoke (messages)

# 输出解析器

请注意,模型的响应是一个 AIMessage 。这包含一个字符串响应以及关于响应的其他元数据。我们通常可能只想处理字符串响应。我们可以通过使用简单的输出解析器来解析出这个响应。

首先导入简单的输出解析器,我们可以单独使用它。例如,我们可以保存语言模型调用的结果,然后将其传递给解析器。

from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser ()
result = model.invoke (messages)
parser.invoke (result)

更常见的是,我们可以将模型与此输出解析器 “链式” 连接。这意味着在此链中,每次都会调用此输出解析器。此链采用语言模型的输入类型(字符串或消息列表)并返回输出解析器的输出类型(字符串)。

我们可以使用 | 运算符轻松创建链。| 运算符在 LangChain 中用于将两个元素组合在一起。

chain = model | parser
chain.invoke (messages)

# 提示词模板

现在我们直接将消息列表传递给语言模型。这些消息由用户输入和应用逻辑的组合构建而成的。这个应用逻辑通常会将原始用户输入转换为准备传递给语言模型的消息列表。常见的转换包括添加系统消息或使用用户输入格式化模板。

PromptTemplate 是 LangChain 中的一个概念,旨在帮助进行这种转换。它们接收原始用户输入并返回准备传递给语言模型的数据(提示)。

让我们在这里创建一个 PromptTemplate 。它将接收两个用户变量:

  • language: 要翻译成的语言
  • text: 要翻译的文本

首先,让我们创建一个字符串,我们将格式化为系统消息,接下来,我们可以创建 PromptTemplate。这将是 system_template 和一个更简单的模板的组合,用于放置要翻译的文本,此模板的输入是一个字典。

from langchain_core.prompts import ChatPromptTemplate
system_template = "Translate the following into {language}:"
prompt_template = ChatPromptTemplate.from_messages (
    [("system", system_template), ("user", "{text}")]
)
result = prompt_template.invoke ({"language": "chinese", "text": "hi"})

prompt_template.invoke 返回一个 ChatPromptValue ,由两个消息组成。如果我们想直接访问这些消息,可以通过 result.to_messages ()

# 使用 LCEL 连接组件

我们现在可以使用管道 (|) 操作符将其与上面的模型和输出解析器结合起来:

chain = prompt_template | model | parser
chain.invoke ({"language": "chinese", "text": "hi"})

# 使用 LangServe 提供服务

现在我们已经构建了一个应用程序,我们需要提供服务。这需要用到 LangServe ,安装方式: pip install"langserve [all]"

LangServe 帮助开发者将 LangChain 链部署为 REST API,我们可以使用 LangServe 部署应用。

我们将制作一个 serve.py 文件,为我们的应用创建一个服务器,这个文件将包含我们服务应用的逻辑。它由三部分组成:

  • 构建的链的定义
  • FastAPI 应用
  • 一个定义用于服务链的路由,通过 langserve.add_routes 完成
#!/usr/bin/env python
from fastapi import FastAPI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import ChatOllama
from langserve import add_routes
# 1. 创建提示模板
system_template = "Translate the following into {language}:"
prompt_template = ChatPromptTemplate.from_messages ([
    ("system", system_template),
    ("user", "{text}")
])
# 2. 创建模型
model = ChatOllama (model="qwen2.5:7b", base_url="http://localhost:11434")
# 3. 创建解析器
parser = StrOutputParser ()
# 4. 创建链
chain = prompt_template | model | parser
# 5. 创建 FastAPI 应用
app = FastAPI (
  title="LangChain Server",
  version="1.0",
  description="A simple API server using LangChain's Runnable interfaces",
)
# 6. 添加链路由
add_routes (
    app,
    chain,
    path="/chain",
)
# 7. 运行 FastAPI 应用
if __name__ == "__main__":
    import uvicorn
    uvicorn.run (app, host="localhost", port=8000)

每个 LangServe 服务都配有一个简单的 内置用户界面,用于配置和调用应用,支持流式输出并可查看中间步骤。

前往 http://localhost:8000/chain/playground/ 试用一下!输入与之前相同的内容 - {"language": "chinese", "text": "hi"} - 它应该会像之前一样响应。

# 设置客户端

现在让我们设置一个客户端,以便以编程方式与我们的服务进行交互。我们可以使用 langserve.RemoteRunnable 轻松做到这一点。 使用这个,我们可以像在客户端运行一样与服务的链进行交互。

from langserve import RemoteRunnable
remote_chain = RemoteRunnable ("http://localhost:8000/chain/")
response = remote_chain.invoke ({"language": "chinese", "text": "hi"})
print (response)

# 聊天机器人

我们将通过一个示例来设计和实现一个基于大型语言模型的聊天机器人。 这个聊天机器人将能够进行对话并记住之前的互动。

请注意,我们构建的这个聊天机器人将仅使用语言模型进行对话。 后续可在以下方面拓展:

  • 对话式 RAG: 在外部数据源上启用聊天机器人体验
  • Agent: 构建一个可以采取行动的聊天机器人

本教程将涵盖基础知识,这对这两个更高级的主题将有所帮助。

# 使用语言模型

首先,初始化一个模型,本次示例中我们使用 openai 接口下的模型,我们将 apikey 等信息存储在了 _config.yml 中,并且封装了一个读取类 ConfigLoader ,你可以在 读取_config.yml 的 [相关章节](# 读取 -_configyml) 了解详情。

# 初始化模型,openai 接口格式,从 _config.yml 中读取 api_key
from Config_Loader import ConfigLoader
config = ConfigLoader ()
model = ChatOpenAI (
    api_key=config ['LLMapis.api_key'], 
    base_url=config ['LLMapis.base_url'],
    model=config ['LLMapis.chat_model'],
    temperature=0.7
)

模型本身没有任何状态概念。例如,如果您问一个后续问题: model.invoke ([HumanMessage (content="What's my name?")]) ,由于模型没有将之前的对话轮次作为上下文,因此无法回答问题。

为了解决这个问题,我们需要将整个对话历史传递给模型:

from langchain_core.messages import AIMessage
model.invoke (
    [
        HumanMessage (content="Hi! I'm Bob"),
        AIMessage (content="Hello Bob! How can I assist you today?"),
        HumanMessage (content="What's my name?"),
    ]
)

# 消息历史

我们可以使用消息历史类来包装我们的模型,使其具有状态。 这将跟踪模型的输入和输出,并将其存储在某个数据存储中。 未来的交互将加载这些消息,并将其作为输入的一部分传递给链。

首先,确保安装 langchain-community ,因为我们将使用其中的集成来存储消息历史: pip install langchain_community

之后,我们可以导入相关类并设置我们的链,该链包装模型并添加此消息历史。

我们会使用到 get_session_history 函数。这个函数预计接受一个 session_id 并返回一个消息历史对象。 session_id 用于区分不同的对话,并应作为配置的一部分在调用新链时传入。

from langchain_core.chat_history import (
    BaseChatMessageHistory,
    InMemoryChatMessageHistory,
)
from langchain_core.runnables.history import RunnableWithMessageHistory
store = {}
def get_session_history (session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store [session_id] = InMemoryChatMessageHistory ()
    return store [session_id]
with_message_history = RunnableWithMessageHistory (model, get_session_history)

我们现在需要创建一个 config ,每次都传递给可运行的部分。这个配置包含的信息并不是直接作为输入的一部分,但仍然是有用的。在这种情况下,我们想要包含一个 session_id。这应该看起来像:

config = {"configurable": {"session_id": "abc2"}}
response = with_message_history.invoke (
    [HumanMessage (content="Hi! I'm Bob")],
    config=config,
)
response.content
response = with_message_history.invoke (
    [HumanMessage (content="What's my name?")],
    config=config,
)
response.content

我们的聊天机器人现在记住了关于我们的事情。如果我们更改配置以引用不同的 session_id ,我们可以看到它开始新的对话,并且我们也可以回到原来的对话

config = {"configurable": {"session_id": "b"}}
response = with_message_history.invoke (
    [HumanMessage (content="What's my name?")],
    config=config,
)
print (response.content)
config = {"configurable": {"session_id": "a"}}
response = with_message_history.invoke (
    [HumanMessage (content="What's my name?")],
    config=config,
)
print (response.content)

这样我们就实现了与多个用户进行对话,现在,我们所做的只是为模型添加了一个简单的持久化层。我们可以通过添加提示词模板来使其变得更加复杂和个性化。

# 提示词模板

提示词模板帮助将原始用户信息转换为大型语言模型可以处理的格式。我们创建一个 ChatPromptTemplate 并以元组格式添加了系统消息,并且定义了一个变量的默认值

我们改变改变了输入类型,现在传递的是一个包含 messages 键的字典,其中包含一系列消息,而不是传递消息列表。

使用元组方便了进行格式替换,因此我们能使用 .partial (language="Chinese") 定义变量的默认值

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages ([
    # SystemMessage (content="You are a helpful assistant. Answer all questions in {language}."),
    ("system", "You are a helpful assistant. Answer all questions in {language}."),
    MessagesPlaceholder (variable_name="messages"),
]).partial (language="Chinese")
chain = prompt | model
response = chain.invoke ({
    "messages": [HumanMessage (content="hi! I'm bob")],
})
print (response.content)

现在,我们将这个链封装在一个消息历史类中。这次,由于输入中有多个键,我们需要指定正确的键来保存聊天历史。

with_message_history = RunnableWithMessageHistory (
    chain,
    get_session_history,
    input_messages_key="messages",
)
config = {"configurable": {"session_id": "abc11"}}
response = with_message_history.invoke (
    {"messages": [HumanMessage (content="hi! I'm todd")], "language": "English"},
    config=config,
)

# 管理对话历史

构建聊天机器人时,我们需要考虑如何管理对话历史。如果不加以管理,消息列表将无限增长,并可能溢出大型语言模型的上下文窗口。因此,需要添加一个限制传入消息大小的步骤。

我们可以通过在提示前添加一个简单的步骤,适当地修改 messages 键,然后将该新链封装在消息历史类中来实现。

LangChain 提供了一些内置的方法来 管理消息列表。在这种情况下,我们将使用 trim_messages 方法来减少我们发送给模型的消息数量。修剪器允许我们指定希望保留的令牌数量,以及其他参数,例如是否希望始终保留系统消息以及是否允许部分消息:

from langchain_core.messages import SystemMessage, trim_messages
# 使用 tiktoken 实现 token 计数
import tiktoken
def count_tokens (messages):
    """使用 tiktoken 计算消息的 token 数量"""
    encoding = tiktoken.get_encoding ("cl100k_base")  # 适用于 gpt-3.5/4
    num_tokens = 0
    for msg in messages:
        # 为每条消息添加基础 token 数
        num_tokens += 4  # 每条消息有固定开销
        
        # 计算内容的 token 数
        content = msg.content if hasattr (msg, 'content') else ""
        num_tokens += len (encoding.encode (content))
        
        # 计算角色的 token 数(role 取决于消息类型)
        if isinstance (msg, SystemMessage):
            role = "system"
        elif isinstance (msg, HumanMessage):
            role = "user"
        elif isinstance (msg, AIMessage):
            role = "assistant"
        else:
            role = "unknown"
        num_tokens += len (encoding.encode (role))
    
    # 每次对话的额外开销
    num_tokens += 2
    return num_tokens
trimmer = trim_messages (
    max_tokens=65,
    strategy="last",
    token_counter=count_tokens,
    include_system=True,
    allow_partial=False,
    start_on="human",
)
messages = [
    SystemMessage (content="you're a good assistant"),
    HumanMessage (content="hi! I'm bob"),
    AIMessage (content="hi!"),
    HumanMessage (content="I like vanilla ice cream"),
    AIMessage (content="nice"),
    HumanMessage (content="whats 2 + 2"),
    AIMessage (content="4"),
    HumanMessage (content="thanks"),
    AIMessage (content="no problem!"),
    HumanMessage (content="having fun?"),
    AIMessage (content="yes!"),
]
trimmer.invoke (messages)

要在我们的链中使用它,我们只需在将 messages 输入传递给提示之前运行修剪器。

from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
chain = (
    RunnablePassthrough.assign (messages=itemgetter ("messages") | trimmer)
    | prompt
    | model
)
response = chain.invoke ({
    "messages": messages + [HumanMessage (content="what's my name?")],
    "language": "English",
})
response.content

现在如果我们尝试询问模型我们的名字,由于我们修剪了聊天历史的那部分,model 将不会给出正确答案。

现在让我们将其包装在消息历史中

with_message_history = RunnableWithMessageHistory (
    chain,
    get_session_history,
    input_messages_key="messages",
)
config = {"configurable": {"session_id": "abc20"}}
response = with_message_history.invoke (
    {
        "messages": messages + [HumanMessage (content="whats my name?")],
        "language": "English",
    },
    config=config,
)
response.content

正如预期的那样,我们声明姓名的第一条消息已被删除。此外,聊天历史中现在有两条新消息(我们最新的问题和最新的回答)。这意味着以前可以在我们的对话历史中访问的更多信息现在不再可用。

response = with_message_history.invoke (
    {
        "messages": [HumanMessage (content="what math problem did i ask?")],
        "language": "English",
    },
    config=config,
)
response.content

# 流式处理

现在我们有了一个功能齐全的聊天机器人。然而,对于聊天机器人应用程序来说,流式处理也是提升用户体验的一个重要选项。大型语言模型有时可能需要一段时间才能响应,因此为了改善用户体验,大多数应用程序所做的一件事是随着每个令牌的生成流回。这样用户就可以看到进度。

所有链都暴露一个.stream 方法,使用消息历史的链也不例外。我们可以简单地使用该方法获取流式响应。

config = {"configurable": {"session_id": "abc15"}}
for r in with_message_history.stream (
    {
        "messages": [HumanMessage (content="hi! I'm todd. tell me a joke")],
        "language": "English",
    },
    config=config,
):
    print (r.content, end="")