现代 LangChain 开发指南:从 LCEL 原理到企业级 RAG 与 Agent 实战

LangChain 是一个开源框架,专门用于帮助开发者更轻松、高效地构建基于大型语言模型(LLMs)(如 ChatGPT、Gemini、Claude 等)的应用程序。

如果把大语言模型比作一个非常聪明但被关在房间里的大脑(它拥有海量的静态知识,但无法获取实时信息,也不能直接操作电脑),那么 LangChain 就是为这个大脑装上"眼睛"、"手脚"和"记忆"的工具箱。

LangChain 的核心组件

LangChain 主要是通过提供一系列标准化的模块来简化开发流程的,它的核心概念包括:

  • 模型 (Models): 提供统一的 API 接口来调用各种不同的底层大模型。这意味着如果你想把应用底层的 AI 从 OpenAI 切换到某个开源模型,只需改动极少的代码。

  • 提示词 (Prompts): 用于管理、格式化和优化提示词(Prompt Templates),将用户的简单输入自动拼接成能让 AI 给出最佳回答的复杂指令。

  • 链 (Chains): 这是 LangChain 的名字由来。它允许你将多个组件"链接"成一个工作流。例如:接收用户问题 -> 检索相关文档 -> 拼接提示词 -> 发送给 LLM -> 解析最终输出,这整个过程就是一个"Chain"。

  • 数据检索 (Retrieval): 解决 LLM 知识停留在训练阶段的问题。LangChain 提供了极其丰富的工具来加载外部数据(PDF、网页、数据库)、切割文本,并结合向量数据库(Vector Databases)实现检索增强生成(RAG)。这让你可以基于自己的私有数据来问答。

  • 记忆 (Memory): 原生的 LLM 接口是无状态的(记不住上一句话)。LangChain 提供了记忆组件,让应用能够记住上下文,从而实现连贯的上下文对话。

  • 代理 (Agents): 这是最强大的功能之一。它赋予了 AI "使用工具"的能力。你可以给 AI 提供搜索引擎、计算器、API 接口等工具,当用户提出复杂问题时,Agent 会自主思考并决定调用哪些工具来一步步获取答案。

在使用 LangChain 之前,开发者如果想让 AI 结合公司内部的 PDF 文档回答问题,需要自己写大量的代码去处理文件读取、文本切分、向量化、计算相似度、构建 API 等等。而 LangChain 预先封装好了所有这些流程,开发者只需要几行代码就能把这些模块拼装在一起。

常见的应用场景包括:

  • 专属知识库问答: 上传公司的规章制度或产品手册,打造一个"百事通"智能客服。

  • 智能长文总结: 快速提取长篇 PDF 或网页的核心观点。

  • 全能个人助理: 结合 Agents 功能,打造不仅能聊天,还能帮你查天气、写邮件、跑代码的助手。

为了保证学习效果,本教程将基于最新的 LangChain 生态(重点关注 LCEL - LangChain 表达式语言) 以及 Python 语言进行讲解。

第 1 课:环境搭建与 LangChain 架构全貌

1. 环境搭建与依赖安装

首先,确保你的环境中安装了 Python(建议版本 >= 3.9)。

核心包安装

在一个全新的 Python 虚拟环境中,执行以下命令:

复制代码
# 安装 LangChain 主包
pip install langchain

# 安装 OpenAI 的特定集成包(后续示例我们将使用 OpenAI)
pip install langchain-openai 

# 安装环境变量管理工具
pip install python-dotenv

环境变量配置

LangChain 绝大多数组件在底层都需要与外部 API(如大模型 API、数据库 API)进行交互。最标准的做法是将 API 密钥通过环境变量注入,而不是硬编码在代码里。

在你的项目根目录下创建一个名为 .env 的文件,写入:

复制代码
OPENAI_API_KEY="sk-你的真实API密钥"
# 如果你在国内使用代理,可能还需要配置 BASE_URL
# OPENAI_BASE_URL="你的代理地址/v1"

在 Python 代码中加载这些环境变量:

复制代码
import os
from dotenv import load_dotenv

# 加载 .env 文件中的环境变量
load_dotenv()

# 验证是否加载成功
print("API Key loaded:", os.environ.get("OPENAI_API_KEY") is not None)

2. 深入理解 LangChain 的模块化架构

早期版本的 LangChain 把所有的功能都塞在一个包里,导致安装极其臃肿。现在的 LangChain 被拆分成了多个层级,了解这些层级对于你后续查找文档和导入类库至关重要。

以下是现代 LangChain 的包结构关系(自底向上):

langchain-core (核心基础层): 这是整个生态的最底层基石。

  • 它定义了所有基础的接口抽象 (例如大语言模型应该有什么方法,输出解析器应该长什么样,消息格式 SystemMessage/HumanMessage 的定义)。

  • 特点: 不包含任何第三方 API 依赖,极其轻量。

langchain-[provider] (官方合作方集成层):

  • 专门为特定的大型厂商独立拆分的包。

  • 例如 langchain-openai, langchain-anthropic, langchain-google-genai

  • 特点: 如果你要用 OpenAI,就导入这个独立的包。这样做的好处是 OpenAI API 更新时,只有这个包需要更新,不影响其他组件。

langchain-community (社区集成层):

  • 包含所有由社区维护的第三方集成库。

  • 比如各种小众向量数据库的接入代码、各类文件(PDF、Word)的加载器。

  • 特点: 这是一个大杂烩,如果你用的不是主流头部厂商的模型或工具,通常需要从这里导入。

langchain (认知架构层):

  • 这是主包,它依赖于 langchain-core

  • 它包含了用于构建具体应用的链(Chains) 、代理(Agents)检索策略(Retrieval Strategies)的组装逻辑。

导入代码实例对比

了解了架构后,你就明白为什么要这样导入代码了

复制代码
# 1. 导入基础数据结构(来自 core)
from langchain_core.messages import HumanMessage, SystemMessage

# 2. 导入具体的模型实现(来自独立的 provider 包)
from langchain_openai import ChatOpenAI

# 3. 导入某个社区维护的第三方 PDF 解析器(来自 community)
from langchain_community.document_loaders import PyPDFLoader

# 4. 导入用于组装代理的高级工具(来自主包)
from langchain.agents import create_tool_calling_agent

随堂测试(第 1 课)

请回答以下两道题目:

1. 单选题: 如果你想在你的应用中自定义一个新的数据类型,用于规范输入给大模型的数据格式,你应该去阅读哪一个包的源码以确保你的实现符合 LangChain 的底层标准?

A. langchain

B. langchain-core

C. langchain-community

D. langchain-openai

2. 简答题: 假设你在 GitHub 上看到一段旧代码,里面写着 from langchain.chat_models import ChatOpenAI。根据现代 LangChain 的架构,这段代码在最新的版本中应该被替换成什么导入语句?为什么 LangChain 要做这种架构上的拆分?

第 2 课:模型接口 (LLMs vs. Chat Models)

在 LangChain 中,与大模型交互的接口主要分为两类:LLMsChat Models。理解它们的区别,是构建现代 AI 应用的基础。

1. 核心概念:Text in vs. Messages in

  • LLMs (纯文本模型):

    • 这是早期的模型形态(比如 OpenAI 的 text-davinci-003)。

    • 接口逻辑: 纯字符串输入 -> 纯字符串输出

    • 你给它一段未完成的文本,它帮你续写。

  • Chat Models (聊天模型):

    • 这是目前的主流形态(比如 gpt-3.5-turbo, gpt-4o, claude-3)。

    • 它们底层虽然也是 LLM,但经过了专门的指令微调(Instruction Tuning),习惯于以对话的形式进行交互。

    • 接口逻辑: 消息列表 (List of Messages) 输入 -> 单条消息 (Message) 输出

注意:在现代应用开发中,我们几乎 99% 的时间都在使用 Chat Models。接下来的教程我们将重点围绕它展开。如果对这个有兴趣可看我上一个博客的关于这个的深入解析。

2. LangChain 的三大基础消息类型 (Message Types)

因为 Chat Models 需要"消息列表"作为输入,LangChain 在 langchain-core 中定义了三种标准的消息类型来统一格式:

  1. SystemMessage (系统消息): 用于给 AI 设定人设、背景或全局指令。通常放在消息列表的第一位。

  2. HumanMessage (人类消息): 代表用户的输入或提问。

  3. AIMessage (AI 消息): 代表大模型返回的回答。我们在记录历史对话时会用到它。

3. 代码实例:调用 Chat Model

我们来看一段完整的、标准的调用 OpenAI 聊天模型的代码:

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage

# 1. 加载环境变量 (确保你的 .env 文件中有 OPENAI_API_KEY)
load_dotenv()

# 2. 实例化 Chat Model
# temperature 控制随机性 (0: 最严谨/固定, 1: 最具创造性/随机)
chat_model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)

# 3. 构建消息列表
messages = [
    SystemMessage(content="你是一个资深的 Python 程序员。请用严谨、简洁的代码风格回答问题。"),
    HumanMessage(content="请写一个计算斐波那契数列的 Python 函数。")
]

# 4. 调用模型 (使用 invoke 方法)
response = chat_model.invoke(messages)

# 5. 查看输出结果
print(type(response)) 
# 输出: <class 'langchain_core.messages.ai.AIMessage'>

print(response.content) 
# 输出生成的代码文本

4. 底层逻辑分析:LangChain 替你做了什么?

你可能会问:"我直接用 OpenAI 的官方 Python SDK 也能发消息,为什么要用 LangChain 包一层?"

答案是:标准化与解耦。

如果你用 OpenAI 的官方 SDK,你需要构建这样的字典: {"role": "system", "content": "..."}。 如果你明天老板要求换成 Anthropic 的 Claude 模型,或者国内的文心一言,它们的 API 数据结构、字段名(比如不叫 role,叫 message_type)完全不同,你的代码必须重写。

在 LangChain 中,SystemMessageHumanMessage抽象层 。 当调用 chat_model.invoke(messages) 时:

  1. LangChain 识别到当前的底层实现是 ChatOpenAI

  2. 它在底层自动将 SystemMessage 翻译成了 OpenAI 认识的 {"role": "system", ...} 格式并发送 HTTP 请求。

  3. 收到 OpenAI 的原始 JSON 响应后,LangChain 提取其中的内容,并将其封装成标准的 AIMessage 对象返回给你。

这意味着,如果要把上面的代码切换到 Claude 模型,你只需要改动第2步 (将 ChatOpenAI 换成 ChatAnthropic),其他构建 Message 和处理返回结果的代码一行都不用改

随堂测试(第 2 课)

我们使用 response = chat_model.invoke(messages) 调用模型后,response 变量直接就是一个字符串格式的回答吗?如果不是,它是什么类型,我们应该如何获取真正的文本回答?

不是纯字符串,而是包含了各种格式和元数据(比如消耗了多少 Token、模型 ID 是什么)。在 LangChain 中,这个返回值的严格类型是 AIMessage 对象。不过我们不需要手动写代码去"清洗"它,只需要通过 response.content 就可以直接提取出纯文本的回答了。

第 3 课:提示词模板 (Prompt Templates)

在上一课中,我们将用户的输入和系统指令硬编码(Hardcode)在了一起。但在实际开发中,系统指令通常是固定的,而用户的输入是动态变化的。

如果只用 Python 自带的字符串拼接(比如 f-string),当应用变复杂时,代码会变得极难维护。LangChain 提供了 Prompt Templates(提示词模板) 来优雅地解决这个问题。

1. 核心概念:为什么要用模板?

提示词模板的作用,就像是 Web 开发中的 HTML 模板。它允许你预先挖好"坑位"(占位符变量),然后在运行时将用户的数据精准地"填"进去,最终生成可以被模型接收的消息列表。

针对上一课学的 Chat Models,我们主要使用 ChatPromptTemplate

2. 代码实例:使用 ChatPromptTemplate

构建聊天提示词模板最常见、最清晰的方式是使用 from_messages 方法。它允许你用元组 (角色, 内容模板) 的形式快速定义。

python 复制代码
from langchain_core.prompts import ChatPromptTemplate

# 1. 定义模板:用大括号 {} 作为变量占位符
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个资深的{domain}专家。请用{style}的语气回答问题。"),
    ("human", "请解释一下什么是 {concept}?")
])

# 2. 注入变量(格式化模板)
# 使用 invoke 方法,传入一个字典来替换变量
formatted_messages = prompt_template.invoke({
    "domain": "天体物理学",
    "style": "幽默且通俗易懂",
    "concept": "黑洞"
})

# 3. 打印结果,看看 LangChain 在底层做了什么
print(type(formatted_messages))
# 输出: <class 'langchain_core.prompt_values.ChatPromptValue'>

print(formatted_messages.to_messages())
# 输出: 
# [SystemMessage(content='你是一个资深的天体物理学专家。请用幽默且通俗易懂的语气回答问题。'), 
#  HumanMessage(content='请解释一下什么是 黑洞?')]

你传入的是一个包含三个字符串变量的字典。

ChatPromptTemplate.invoke() 在底层不仅完成了字符串的替换,更重要的是,它自动帮你实例化了 SystemMessageHumanMessage 对象

这意味着你可以把生成的 formatted_messages 直接无缝丢给上节课的 chat_model 去执行。

3. 进阶技巧:部分格式化 (Partial Formatting)

有时,你不能一次性拿到所有的变量。比如,应用的"风格(style)"在程序启动时就确定了,但"概念(concept)"需要等用户在网页上输入。

这时可以使用 partial 方法先填入一部分变量:

python 复制代码
# 假设我们只知道 style,不知道 concept
partial_template = prompt_template.partial(domain="计算机", style="极客")

# 等用户输入后,再注入剩下的变量
final_messages = partial_template.invoke({"concept": "内存泄漏"})

随堂测试(第 3 课)

1. 思考题: 既然 Python 自带的 f"{user_input}" 也可以替换字符串,为什么 LangChain 还要专门设计一个 ChatPromptTemplate 类?(结合上面的代码实例和底层逻辑剖析来思考)。

2. 代码填空题: 假设你有以下模板:

python 复制代码
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "将以下{source_lang}翻译成{target_lang}。"),
    ("human", "{text}")
])

请写出一行代码,使用 invoke 方法,将 source_lang 设为 "中文",target_lang 设为 "法文",text 设为 "你好",并将生成的结果赋值给变量 messages

现在,我们有了大模型,也学会了用模板动态拼接输入。但目前还有一个大问题:大模型返回给我们的永远是一段自然语言文本(字符串)

在实际开发中,如果你想把大模型的提取结果存入数据库,或者传给前端渲染界面的卡片,你需要的是 JSON 或特定的数据结构,而不是一堆啰嗦的自然语言。

第 4 课:输出解析器 (Output Parsers)

输出解析器(Output Parsers)的作用就是把大模型输出的"非结构化文本",转换成程序可以直接使用的"结构化对象"(如 Python 字典、列表或 Pydantic 对象)。

1. 核心概念:解析器在干什么?

一个标准的 LangChain 输出解析器在底层其实做了两件事

  1. 提供格式化指令 (Format Instructions): 它会生成一段文字,明确告诉大模型:"请你严格按照以下 JSON 格式输出,不要说废话"。我们会把这段指令自动塞进 Prompt 里。

  2. 执行解析 (Parse): 拿到大模型返回的文本后,它会使用 JSON 解析库或正则表达式,把文本转换成 Python 对象。

2. 代码实例:使用 PydanticOutputParser

在现代 Python 开发中,Pydantic 是用来定义数据结构最流行的库。LangChain 完美融合了它。

假设我们要让 AI 从一段杂乱的文本中提取人物信息:

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

load_dotenv()
chat_model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 提取任务通常将温度设为0,保证输出稳定

# 1. 定义你期望的输出结构 (使用 Pydantic)
class PersonInfo(BaseModel):
    name: str = Field(description="人物的名字")
    age: int = Field(description="人物的年龄,如果未提及请设为 0")
    hobbies: list[str] = Field(description="人物的爱好列表")

# 2. 实例化解析器
parser = PydanticOutputParser(pydantic_object=PersonInfo)

# 3. 获取解析器生成的"底层指令"
format_instructions = parser.get_format_instructions()
# 此时如果你打印 format_instructions,会看到一大段英文,大意是:
# "The output should be formatted as a JSON instance that conforms to the JSON schema below..."

# 4. 构建提示词模板 (将指令注入进去)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个信息提取助手。\n{format_instructions}"),
    ("human", "从以下文本中提取信息:{text}")
])

# 5. 执行流程
# 组装变量
messages = prompt.invoke({
    "format_instructions": format_instructions,
    "text": "今天我遇到了张三,他今年28岁了,平时特别喜欢打篮球,周末经常去游泳。"
})

# 调用模型
response = chat_model.invoke(messages)

# 解析结果
# 注意:response.content 是纯文本字符串 (如 '{"name": "张三", "age": 28, "hobbies": ["打篮球", "游泳"]}')
parsed_result = parser.invoke(response)

# 6. 查看最终的魔法
print(type(parsed_result))
# 输出: <class '__main__.PersonInfo'> (不再是字符串,而是真正的 Python 对象)

print(parsed_result.name)    # 输出: 张三
print(parsed_result.hobbies) # 输出: ['打篮球', '游泳']
3. 常见问题:如果模型没有严格输出 JSON 怎么办?

早期的模型经常会在 JSON 外面包一些废话,比如:"好的,这是你要的 JSON:json { ... } " 。 LangChain 的 Parser 底层非常健壮,它内置了正则表达式 ,会自动去寻找文本中的 { }[ ] 块,把外围的废话剥离掉,再进行解析。

随堂测试

1. 判断题: 输出解析器(Output Parser)能够改变大模型底层的运行机制,让它在生成文本之前就直接在内存里构建一个 JSON 对象。 A. 对 B. 错

2. 场景题: 假设你想让 AI 根据用户的需求生成一段 Markdown 格式的代码(不需要 JSON)。根据我们刚才学的底层逻辑,你觉得你一定需要 用到 PydanticOutputParser 吗?如果不用,通常怎么做?

3. 概念连线题(复习巩固): 请用自己的话,把以下三个概念和它们在一条"流水线"上扮演的角色对应起来:

  • ChatPromptTemplate 扮演的角色是:?

  • ChatOpenAI 扮演的角色是:?

  • OutputParser 扮演的角色是:?

第 5 课:LangChain 表达式语言 (LCEL) 基础

LCEL(LangChain Expression Language) 并不是一门新的编程语言,而是 LangChain 独创的一种代码编写风格(语法糖) 。它的核心目标只有一个:让你能用极简的代码,把零散的组件拼接成强大的流水线(Chain)。

1. 核心概念:Linux 管道符 | 与 Runnable 协议

在 Linux 终端里,你可以用 |(管道符)把上一个命令的输出作为下一个命令的输入。LangChain 借用了这个极其优雅的概念。

为了让不同组件能够通过 | 连接,LangChain 底层重写了所有的核心类(Prompt, Model, Parser 等),让它们都继承自一个叫做 Runnable 的基础类。

只要是 Runnable 对象,就都拥有统一的标准方法,最常用的三个是:

  • invoke(): 传入单条输入,同步获取结果。

  • stream(): 传入单条输入,流式返回结果(打字机效果)。

  • batch(): 传入一个列表,并发处理多条输入。

2. 代码实例:传统写法 vs. LCEL 写法

我们来实现一个简单的"专业名词解释器",对比一下新旧写法。

先准备好三大组件:

python 复制代码
import os
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser # 最简单的字符串解析器

load_dotenv()

# 1. 加工员 (Prompt)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个用大白话解释复杂概念的专家。"),
    ("human", "请解释一下什么是 {concept}?")
])

# 2. 大脑 (Model)
model = ChatOpenAI(model="gpt-3.5-turbo")

# 3. 包装员 (Parser)
parser = StrOutputParser()

以前的繁琐写法(手动传递):

python 复制代码
# 每次都要调用 invoke,还需要引入中间变量
messages = prompt.invoke({"concept": "量子纠缠"})
response = model.invoke(messages)
final_result = parser.invoke(response)

print(final_result)

现在的 LCEL 写法:

python 复制代码
# 使用 | 符号,将三个组件串联成一个全新的 Runnable 对象:chain
chain = prompt | model | parser

# 只需要对组装好的 chain 调用一次 invoke
# 输入的字典会先给 prompt,prompt 的输出自动给 model,model 的输出自动给 parser
final_result = chain.invoke({"concept": "量子纠缠"})

print(final_result)
3. LCEL 的降维打击:原生支持流式输出 (Streaming)

如果在以前,你想让大模型像 ChatGPT 那样一个字一个字地把结果弹出来,你需要写非常复杂的异步代码和回调函数。

但有了 LCEL,因为底层的 Runnable 协议统一了接口,你只需要把 invoke 换成 stream,流式输出就自动生效了!

python 复制代码
# 直接调用 stream 方法,它会返回一个生成器
for chunk in chain.stream({"concept": "API 接口"}):
    # chunk 是实时返回的字符串片段
    print(chunk, end="", flush=True)

随堂测试(第 5 课)

LCEL 的语法是整个现代 LangChain 的灵魂,理解它至关重要。

1. 代码阅读题: 假设我定义了这样一个链: my_chain = prompt | model (注意,这里没有接 OutputParser)。 如果我执行 result = my_chain.invoke({"topic": "AI"}),请问 result 变量的数据类型是什么?(提示:回忆一下模型直接返回的是什么)。

  • 如果是 prompt | model,流水线到 model 就结束了,输出的就是未经处理的 AIMessage

  • 只有加上了解析器:prompt | model | parser,最后的 parser(包装员)才会把 AIMessage 拆开,提取出里面的字符串给你。

2. 简答题: 结合你的编程经验,Python 中默认的 | 符号通常用于按位或运算(Bitwise OR)或者集合的并集。LangChain 是使用了 Python 的什么高级特性(语法机制),才让 prompt | model 这种写法没有报错,反而实现了数据的传递?

在 Python 中,| 符号原本是用来做数学运算(按位或)的。但是,Python 允许程序员重新定义这个符号的功能。LangChain 的开发者在底层的代码里写了一个特殊的方法叫做 __or__。当你敲下 A | B 时,Python 在底层其实是悄悄执行了类似 A.__or__(B) 的函数代码,从而实现了把 A 和 B 连接起来的功能。

第 6 课:LCEL 进阶操作 (数据的传递与拼装)

在上一课的 prompt | model | parser 中,我们给流水线的输入是一个字典 (比如 {"concept": "量子纠缠"})。 但在实际开发中,用户在网页输入框里输入的通常只是一个单纯的字符串(比如 "量子纠缠")。

那么,我们怎么把一个单纯的字符串,转换成 Prompt 模板需要的字典格式呢?这就需要用到 LCEL 的进阶工具。

1. 核心概念:RunnablePassthrough (数据透传)

RunnablePassthrough 翻译过来就是"让数据直接穿过去"。你可以把它想象成一根空心水管。当数据流经它时,它什么都不做,直接把原数据原封不动地交到下一步。

它最常见的用法,是和字典配合使用,把用户的单个输入组装成复杂的格式。

2. 代码实例

我们来看一个场景:我们要写一个 AI 翻译器,固定把用户输入的话翻译成英文。

python 复制代码
# 导入操作系统相关库,用于与你的电脑系统打交道(比如读取隐藏的环境变量)
import os
# 导入 dotenv 库。它的作用是帮你悄悄加载 .env 文件里存着的密码或秘钥(比如 OpenAI API Key),这样你就不用把密码明文写在代码里了
from dotenv import load_dotenv

# 从 LangChain 工具箱里导入我们需要的三大核心零件:
# 1. 提示词模板(用来设计我们跟 AI 说话的格式/剧本)
from langchain_core.prompts import ChatPromptTemplate
# 2. OpenAI 的聊天模型(这就是那个负责思考的"AI大脑")
from langchain_openai import ChatOpenAI
# 3. 字符串输出解析器(用来把 AI 回复的复杂代码结构剥开,只留下纯人类语言文字)
from langchain_core.output_parsers import StrOutputParser

# 【重点引入】导入 RunnablePassthrough,你可以把它想象成一截"空心水管"或者"万能接球手"
from langchain_core.runnables import RunnablePassthrough

# 运行这个函数,它会去读取你藏好的 API Key,让后面的大模型能顺利连上网
load_dotenv()

# ==========================================
# 1. 创建模型和解析器(准备好干活的工具)
# ==========================================
# 雇佣一个 OpenAI 的聊天机器人,并指定让他用 "gpt-3.5-turbo" 这个脑子(模型版本)
model = ChatOpenAI(model="gpt-3.5-turbo")

# 准备一个"翻译官",它的唯一工作就是把机器人回复的复杂数据包,变成干净的字符串
parser = StrOutputParser()

# ==========================================
# 2. 创建 Prompt 模板(写好给机器人的剧本)
# ==========================================
# 我们在这里定下规矩:
# 第一句告诉 AI 它的身份(翻译官),第二句是留给用户输入内容的"填空题"。
# 注意:这里的 {text} 就像一张试卷上的"横线(坑位)",等着后面把真正要翻译的句子填进来。
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个翻译官,请将用户的输入翻译成英文。"),
    ("human", "{text}") # {text} 就是挖好的坑
])

# ==========================================
# 3. 组装 LCEL 链条(把工具连成一条自动化流水线)
# ==========================================
# 这里用到了"管道"符号 `|`,作用是把上一步的结果直接扔给下一步。
chain = (
    # 第一步:处理用户的原始输入。
    # 为什么要把 RunnablePassthrough() 放在 {"text": ...} 这个字典里?
    # 因为上面的 prompt 模板需要一个包裹着 "text" 标签的数据去填坑。
    # 用户最后只会传进来一句干巴巴的话(比如"你好"),RunnablePassthrough 的作用就是一把接住这句话,原封不动地贴上 "text" 的标签,变成 {"text": "你好"}。
    {"text": RunnablePassthrough()} 
    | prompt  # 第二步:把刚才贴好标签的数据 {"text": "用户的输入"} 扔给剧本,把 {text} 那个坑填上。
    | model   # 第三步:剧本拼接完整了,直接扔给 AI 大脑去思考、翻译。
    | parser  # 第四步:AI 思考完吐出了一堆包含很多附加信息的数据,交给解析器,把外壳脱掉,只留下纯文字的翻译结果。
)

# ==========================================
# 4. 运行链条(按下流水线的启动按钮)
# ==========================================
# invoke 的意思是"调用/启动"。我们按下启动键,并往流水线里扔进一句话:"今天天气真不错"。
# 这句话会瞬间经历上面的四个步骤:
# 1. 被接住并包装成 {"text": "今天天气真不错"}
# 2. 填入模板变成完整的对话提示
# 3. 交给模型翻译
# 4. 提取出纯文字
result = chain.invoke("今天天气真不错")

# 把最后流水线吐出来的结果打印在屏幕上
print(result) # 输出结果应该是类似: "The weather is really nice today."

RunnablePassthrough() (到底透传了个啥?)

字面意思: 可运行的透传工具(Pass-through 意味着直接穿过去,不改变内容)。

为什么需要它? 因为我们的 prompt(剧本)是个傲娇的组件,它不接受一句话,它只接受一个"字典"(比如 {"text": "今天天气真不错"})。

但是!作为用户,我们调用 chain.invoke() 时,又只想简单粗暴地传一句话进去("今天天气真不错")。

所以,我们需要一个中间人来包装一下:{"text": RunnablePassthrough()}。当那个干巴巴的句子传进来时,RunnablePassthrough() 张开双手接住它,啥也不改,直接把它塞进字典里对应的位置。

3. 补充概念:字典会自动变成"并行处理"

在上面的代码中,流水线的最开头是一个字典 {"text": RunnablePassthrough()}。 在 LCEL 的规则中,如果在一个流水线步骤中出现了一个字典,LangChain 会同时并行执行字典里的所有的值

虽然上面的例子只有一个键值对,体现不出并行的威力。但在未来的"检索增强生成 (RAG)"中,我们会经常看到类似这样的伪代码:

python 复制代码
# 这里的字典有两个键值对,LangChain 会同时去执行它们
setup_and_retrieval = {
    # 去数据库里搜索相关文档
    "context": 数据库检索器, 
    # 原样保留用户的原始问题
    "question": RunnablePassthrough() 
}

# 真正的 RAG 流水线:先准备数据,再拼接提示词,再提问,再解析
rag_chain = setup_and_retrieval | prompt | model | parser

随堂测试(第 6 课)

为了确保你理解了今天这两段代码(特别是第一段详细注释的代码),请试着回答以下两道题:

1. 基础理解题: 在第一段代码的第 3 步中,如果我把字典写成 {"user_input": RunnablePassthrough()},运行代码会报错吗?为什么?(提示:回头看一下第 2 步定义的 Prompt 模板里挖的坑叫什么名字)。

2. 代码填空题: 假设我有一个计算器 Prompt 模板,它需要两个变量:{num1}{num2}

复制代码
prompt = ChatPromptTemplate.from_messages([
    ("human", "请计算 {num1} 加上 {num2} 等于多少?")
])

如果我希望流水线的开头,能够接收一个单纯的数字(比如 5)作为 num1,而 num2 我想永远固定写死为 10。 请你模仿第一段代码的第 3 步,帮我补全下面的字典(只需写出字典部分即可)

复制代码
chain = (
    {
        # 请在这里填写代码
    }
    | prompt
    | model
    | parser
)

第 7 课:文档加载与文本切割 (Loaders & Splitters)

RAG(Retrieval-Augmented Generation,检索增强生成) 是解决大模型"胡说八道"和"不知道最新/私有知识"的终极方案。

它的核心思想是:在向大模型提问之前,先去我们自己的知识库(比如公司文档、PDF)里搜索相关的段落,然后把这些段落和问题一起发给大模型,让它"开卷考试"。

为了实现 RAG,第一步就是把我们现有的文件变成程序能认识的数据。这就是 Loaders(加载器)Splitters(切割器) 的工作。

1. 核心概念:为什么要加载和切割?
  • Document Loaders (加载器): 你的硬盘里有 PDF、Word、TXT、CSV,格式千奇百怪。加载器的作用是把这些不同格式的文件,统统转换成 LangChain 统一的标准对象:Document 对象

  • Text Splitters (切割器): 大模型单次能阅读的字数是有限的(上下文窗口限制)。如果你把一本 10 万字的书直接塞给它,不仅会报错,而且非常费钱。因此,我们需要把长文档切成一段段几百字的小块(Chunks),方便后续搜索和发给模型。

2. 代码实例:

这次我们将演示如何读取一个普通的 TXT 文本,并把它切割成小块。

python 复制代码
# ==========================================
# 准备工作:导入需要的工具包
# ==========================================

# 1. 导入文档加载器(这里以加载纯文本 .txt 文件为例)
# 为什么它在 community (社区包) 里?因为读取各种各样的文件(PDF、Word、TXT、网页)属于"杂活",
# 官方把这些千奇百怪的第三方读取工具都放在了社区包里。
from langchain_community.document_loaders import TextLoader

# 2. 导入文本切割器
# RecursiveCharacterTextSplitter 是目前最常用、最"通人性的"文本切割工具(切片机)
from langchain_text_splitters import RecursiveCharacterTextSplitter

# ---------------- 第一步:加载文档(把书搬进内存) ----------------

# 假设我们在电脑桌面上有一个叫做 "company_rules.txt" 的文件(比如公司的规章制度长文)
# 我们请来一个专属的"文本搬运工"(TextLoader),告诉他文件在哪,
# 并特别叮嘱 encoding="utf-8"(这是告诉搬运工:请用标准的中文方式去读,千万别读出乱码来)。
loader = TextLoader("company_rules.txt", encoding="utf-8")

# 让搬运工正式开始干活:调用 load() 方法,真正把硬盘里的文件内容读取到电脑内存里。
# docs 是一个列表(你可以理解为一个大箱子),里面装的是被打包好的标准包裹(LangChain 特有的 Document 对象)。
docs = loader.load()

# 打印出来看看我们搬了几份文件进箱子
# (如果你只加载了一个 txt,那这里的长度就是 1)
print("一共加载了", len(docs), "个文档")

# 每一份 Document 包裹其实分两层:
# 第一层叫 page_content(里面装的纯纯的文字内容)
# 第二层叫 metadata(贴在包裹外面的标签,记录了这堆文字是从哪个文件来的、第几页等信息)
# print(docs[0].page_content) # 如果把这行代码前面的 # 删掉,就会把文件的所有字都打印在屏幕上

# ---------------- 第二步:切割文档(把长文切成小块,方便喂给 AI) ----------------

# 买一台"智能切片机",并设置好切片的规则:
# Python 语法小贴士:把参数名写出来再赋值(比如 chunk_size=100),这叫"关键字参数",能让你以后看代码时一秒懂它是什么意思。
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 100,      # 【最大块头】规定每块"肉"最多只能包含 100 个字符,防止 AI 一次吃太多"噎住"
    chunk_overlap = 20,    # 【首尾相连】切下一块时,要倒退 20 个字符。也就是让相邻的两块有 20 个字的重合部分(防止一句话刚好被一刀两断,导致 AI 读不懂上下文意思)
    separators = ["\n\n", "\n", "。", ",", " "] # 【下刀的优先顺序】告诉机器:先试着按段落(双换行)切;如果一段还是太长(超过100字),再按单换行切;还不行,就按句号切;再不行按逗号切......
)

# 启动切片机:调用 split_documents 方法,把刚才搬运进来的那个长篇大论的大箱子 (docs) 倒进机器里切碎
# 返回的 splits 也是一个箱子,但里面装的已经是切得大小匀称的"小块 Document 包裹"了
splits = text_splitter.split_documents(docs)

# 打印看看这台机器一共切出了多少个小块
print("长文档被切成了", len(splits), "个小块。")

# 我们可以随便拿第一小块出来,看看里面切出了什么字
print("第一块的内容是:")
print(splits[0].page_content)
3. 底层逻辑:为什么有 chunk_overlap (重叠)?

假设有一句话:"LangChain / 是一个非常强大的 / 开发框架"。 如果没有重叠,恰好切在了斜杠处。 第一块是:"LangChain " 第二块是:"是一个非常强大的" 当用户搜索"LangChain的评价"时,可能只能搜出第一块,大模型拿到"LangChain"这个词,根本不知道后面其实有一句"非常强大",从而给出错误的回答。 设置了 chunk_overlap=20 后,第二块可能会变成"LangChain 是一个非常强大的",这样大模型就能联系上下文了。

随堂测试(第 7 课)

这节课没有过于复杂的语法,主要是理解 RAG 的第一步逻辑。请回答:

1. 简答题: 在上述代码中,最终切分出来的 splits 变量是一个列表。请问这个列表里装的元素,它的数据类型是纯粹的 Python 字符串(String),还是 LangChain 的某种特定对象?如果是特定对象,它通常包含哪两个重要属性?(提示:回顾一下代码注释)

  • LangChain 的 Document 对象,而不是纯字符串。

  • 为什么不直接用字符串? 假设我们在 500 页的 PDF 里切出了一句话:"公司规定迟到罚款 50 元"。如果这只是个纯字符串,大模型读到它时,完全不知道这句话是出自哪份文件、第几页。

  • 因此,LangChain 用 Document 对象把文本包装了起来。它包含两个核心属性:

    1. page_content: 真正的文字内容("公司规定迟到...")。

    2. metadata (元数据) : 附加信息的字典,比如 {"source": "员工手册.pdf", "page": 42}。有了这个,AI 甚至可以告诉你:"根据员工手册第42页的规定..."。

2. 场景选择题: 如果公司给你一份长达 500 页的《员工手册 PDF》,并让你做一个基于这份手册的问答机器人。在写代码时,为了保证 AI 每次回答既准确又不超出字数限制,你最需要 调整 RecursiveCharacterTextSplitter 中的哪一个参数? A. chunk_size (每块的字符数) B. chunk_overlap (重叠的字符数) C. separators (切割符)

第 8 课:向量化与向量数据库 (Embeddings & Vectorstores)

传统的数据库(比如 MySQL)搜索靠的是"关键字完全匹配"。比如你搜"苹果手机",如果文章里写的是"iPhone",传统数据库是搜不出来的,因为字完全不一样。

但在 AI 时代,我们希望实现"语义搜索"(根据意思来搜)。为了让计算机理解"意思",我们需要把文字变成计算机能懂的语言------数字(更准确地说是:多维数组 / 向量)

1. 核心概念:Embeddings 与 Vectorstores
  • Embeddings (向量化/嵌入模型): 这是一种特殊的人工智能模型(OpenAI 也有专门的 Embedding 模型,极其便宜)。它的唯一工作就是接收一段文字,然后吐出一长串浮点数(比如 [0.012, -0.045, 0.887, ...],通常有上千个数字)。

    • 核心逻辑: 意思越相近的句子,它们变成数字后的坐标在空间中离得就越近。"苹果手机"和"iPhone"算出来的数字列表是非常相似的。
  • Vectorstores (向量数据库): 专门用来存这些长串数字的数据库。当你提出一个问题时,向量数据库会计算你问题的向量与库里所有文档向量的"距离",找出距离最近(最相关)的那几块文本给你。

2. 代码实例:极其详细的注释版

在这个例子中,我们将使用一款非常流行的、可以跑在本地内存里的开源向量数据库:Chroma

(注意:运行此代码前,你需要在终端里安装 chroma 包:pip install langchain-chroma)

python 复制代码
import os
from dotenv import load_dotenv

# 1. 导入 OpenAI 的向量化模型 (注意,它不是普通的聊天模型,而是专用的 Embeddings 模型)
from langchain_openai import OpenAIEmbeddings

# 2. 导入 Chroma 向量数据库集成包
from langchain_chroma import Chroma

# 3. 导入上一节课学过的 Document 对象,为了方便演示,我们手动创建几个文档片段
from langchain_core.documents import Document

load_dotenv()

# ================= 第一步:准备好切割后的小块文本 =================
# 在实际开发中,这里的数据应该是上一节课 text_splitter 切出来的结果 (splits)
# 这里我们手动写 3 个文档对象来模拟
documents = [
    Document(page_content="iPhone 15 Pro 采用了钛金属边框,非常轻便。"),
    Document(page_content="今天中午食堂的红烧肉很好吃。"),
    Document(page_content="苹果公司最新款的智能手机在重量上做了很大的优化。")
]

# ================= 第二步:初始化向量化模型 =================
# 实例化一个 OpenAI 提供的嵌入模型
# "text-embedding-3-small" 是目前 OpenAI 最新、性价比最高的向量化模型
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")

# ================= 第三步:将文本存入向量数据库 =================
print("正在将文字转换成数字并存入数据库,请稍候...")

# 调用 Chroma 的 from_documents 方法
# 这个方法会在底层自动做两件事:
# 1. 拿着 documents 里的文字去请求 OpenAI 的 Embeddings API,把文字变成数字。
# 2. 把生成的数字和原本的文字一起存到 Chroma 数据库里。
vectorstore = Chroma.from_documents(
    documents=documents,      # 要存入的文档列表
    embedding=embeddings_model # 指定使用哪个模型把文字变成数字
)

print("数据库构建完成!\n")

# ================= 第四步:执行语义搜索 =================
# 假设用户提出了一个问题,他并没有直接说 "iPhone" 也没有说 "苹果手机"
user_query = "哪款通讯设备材质变轻了?"

print(f"用户问题: '{user_query}'")
print("正在数据库中寻找最相关的文档...\n")

# 调用 similarity_search (相似度搜索) 方法
# k=2 表示我们只想要最相关的前 2 条结果
# 底层逻辑:它会先把你提问的 user_query 变成向量,然后在数据库里找跟它距离最近的 2 个向量
results = vectorstore.similarity_search(user_query, k=2)

# 打印找出来的结果
print("搜索到的结果:")
for i, doc in enumerate(results):
    print(f"第 {i+1} 条: {doc.page_content}")

# 运行结果应该会精准地把第1条(iPhone)和第3条(苹果公司)找出来,
# 而把不相关的第2条(红烧肉)过滤掉。
3. 为什么 RAG 是目前最火的技术?

如果没有向量数据库,你每次问大模型问题,都要把 500 页的 PDF 原封不动塞进 Prompt 里,这可能要花几十块钱的 Token 费。 有了向量数据库,你可以免费地在本地(比如上面的 Chroma)以毫秒级的速度找出最相关的 2-3 个段落,然后只把这几百个字发给大模型。既准,又省钱。

随堂测试(第 8 课)

这节课的代码逻辑非常顺畅:准备文档 -> 初始化模型 -> 存入数据库 -> 搜索。请回答以下问题:

1. 概念判断题: 我们在构建 RAG 应用时,处理用户的提问(user_query)需要用到大模型。那么在上面的代码流程中,当执行 vectorstore.similarity_search(user_query, k=2) 这行代码时,LangChain 在底层调用的是用于聊天的 ChatOpenAI 还是用于向量化的 OpenAIEmbeddings 模型?

2. 场景应用题: 小明用上面的代码做了一个系统,把公司的1000条报销规定存进了 Chroma 数据库。当用户输入"我不小心把发票弄丢了,还能报销打车费吗?"这段话时,Chroma 数据库返回的 results 变量,它的数据类型是什么?(提示:回忆一下它存进去的是什么格式,拿出来的就是什么格式)。

第 9 课:检索器与完整的 RAG 链组装 (Retrievers & RAG Chain)

现在,我们的桌面上放着四样东西:

  1. 向量数据库 (Vectorstore):装满了公司规定的知识库。

  2. 提示词模板 (Prompt):挖了坑的系统指令。

  3. 聊天模型 (Model):GPT-3.5 的聪明大脑。

  4. 输出解析器 (Parser):负责提取纯文字的质检员。

今天,我们要用第二阶段学过的 LCEL (管道符 |),把它们串联成一个真正能用的智能客服。

1. 核心概念:检索器 (Retriever)

在上一课中,我们用的是 vectorstore.similarity_search("问题")。这只是一个普通的函数调用,它不是 Runnable 协议,所以不能直接放进 | 流水线里。

为了让向量数据库能接入 LCEL 流水线,LangChain 提供了一个极其简单的方法:as_retriever() 。 调用这个方法后,数据库就会摇身一变,成为一个符合 Runnable 标准的组件:输入是一个字符串问题,输出是一个 Document 列表。

2. 代码实例

这段代码将展示现代 LangChain 中最标准、最优雅的 RAG 实现方式:

python 复制代码
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 引入 RunnablePassthrough,我们马上要用它接住用户的问题
from langchain_core.runnables import RunnablePassthrough
from langchain_core.documents import Document

load_dotenv()

# ================= 准备工作 (回顾前两课的内容) =================
documents = [
    Document(page_content="公司规定:员工迟到一次扣款 50 元。"),
    Document(page_content="公司规定:晚上加班超过 9 点,可以报销打车费,上限 100 元。"),
    Document(page_content="公司规定:年假每年 5 天,不可跨年累积。")
]
embeddings_model = OpenAIEmbeddings(model="text-embedding-3-small")
# 把文档存入 Chroma 向量数据库
vectorstore = Chroma.from_documents(documents, embeddings_model)

# ================= 核心步骤 1:创建检索器 =================
# 把普通的向量数据库,转化为 LCEL 兼容的 Retriever (检索器)
# 并且通过 search_kwargs={"k": 2} 告诉它:每次只帮我找出最相关的 2 句话即可
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})

# ================= 核心步骤 2:创建组件 =================
# 大脑
model = ChatOpenAI(model="gpt-3.5-turbo")
# 解析器
parser = StrOutputParser()

# Prompt 模板:非常关键!注意看我们挖了哪两个坑
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个公司 HR 助手。请严格根据以下提供的参考资料回答问题,不知道就说不知道。\n\n参考资料:\n{context}"),
    ("human", "{question}")
])

# ================= 核心步骤 3:用字典组装并行的开头 =================
# 这是一个非常经典的用法!当我们向流水线丢入一个字符串时,字典会并行处理它。
setup_and_retrieval = {
    # 动作1:把用户输入传给 retriever,去数据库搜出 2 条 Document 列表。
    # 搜出来的 Document 列表,会自动填入到 prompt 的 {context} 坑位中。
    "context": retriever, 
    
    # 动作2:用 RunnablePassthrough() 把用户的原始输入原封不动地接住。
    # 这个原始输入,会自动填入到 prompt 的 {question} 坑位中。
    "question": RunnablePassthrough() 
}

# ================= 核心步骤 4:LCEL 一键组装 =================
# 完美的 RAG 链诞生了!
rag_chain = setup_and_retrieval | prompt | model | parser

# ================= 执行提问 =================
# 用户在网页端输入了这句话:
user_input = "我昨天晚上加班到晚上 10 点,打车花了 120 块钱,能报销吗?"

print("AI 正在思考...")
# 一次 invoke,完成:搜索 -> 填坑 -> 思考 -> 输出字符串
result = rag_chain.invoke(user_input)

print("\n最终回答:")
print(result) 
# 预期输出类似:"根据参考资料,加班超过 9 点可以报销打车费,但上限是 100 元。您打车花了 120 元,最多只能报销 100 元。"
3. 底层流程回放(数据在管道里是怎么流动的?)

当你执行 rag_chain.invoke("我昨天...") 时,底层发生了这几件事:

  1. 字符串 "我昨天..." 进入字典。

  2. "context" 拿到字符串,触发检索,得到:[Document("加班超过9点...")]

  3. "question" 拿到字符串,什么都不做,直接传递。

  4. 现在数据变成了一个组装好的大字典:{"context": [Document(...)], "question": "我昨天..."}

  5. 这个大字典被传给 promptprompt 把这两个值塞进模板里,变成了一段完整的给 OpenAI 的话。

  6. OpenAI 接收到长长的话,根据"参考资料"进行阅读理解。

  7. 返回结果给 parser,输出纯净的回答。

随堂测试

1. 代码阅读题: 如果在上面的代码中,我把 Prompt 模板改写成这样:

复制代码
prompt = ChatPromptTemplate.from_messages([
    ("system", "根据资料:{info} 回答问题。"),
    ("human", "{user_q}")
])

那么为了让程序不报错,我们在第 3 步构建 setup_and_retrieval 字典时,里面的两个键(Key)分别应该被改成什么名字?

2. 逻辑填空题: 大语言模型在做 RAG 回答时,它之所以能够给出准确的私有知识,是因为知识是由( A )提供的,而大语言模型本质上扮演的是一个拥有极强( B )能力的角色,它本身并不记忆这些私有知识。 请从以下选项中选出 A 和 B 对应的词语: 选项:向量数据库、文本生成与阅读理解、提示词模板、输出解析器。 A 是:___ B 是:___

请给出你的答案。答对后,我们将进入第四阶段:如何让 AI 拥有记忆 (Memory)!

第 10 课:对话历史的管理

如果你自己写过调用 OpenAI API 的代码,你会发现一个让人崩溃的事情:大模型本质上是"失忆"的(无状态的)。 你对它说:"我叫张三。" 它回答:"你好张三。" 紧接着你问:"我叫什么名字?" 它会回答:"抱歉,我不知道你的名字。"

为了让 AI 拥有记忆,我们必须在每次提问时,把之前聊过的所有话,连同最新的问题一起打包发给它。LangChain 提供了非常优雅的工具来自动完成这种"打包"工作。

2. 代码实例:

这段代码将展示如何给 AI 装上记忆,并且同时和"张三"、"李四"两个人聊天而不串号。

python 复制代码
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 【新引入 1】这是一个专门用来占位的符,用于在 Prompt 中给"历史记录"留位置
from langchain_core.prompts import MessagesPlaceholder 
# 【新引入 2】存在内存里的聊天记录本
from langchain_core.chat_history import InMemoryChatMessageHistory
# 【新引入 3】这就是我们说的"记忆外套"
from langchain_core.runnables.history import RunnableWithMessageHistory

load_dotenv()

# ================= 第一步:创建基础链条 =================
model = ChatOpenAI(model="gpt-3.5-turbo")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个友好的 AI 助手。"),
    
    # 核心魔法:我们在系统提示词和用户当前问题之间,插一个占位符。
    # 它的名字叫 "history"。一会 LangChain 会自动把过去所有的对话列表塞进这里。
    MessagesPlaceholder(variable_name="history"), 
    
    ("human", "{question}") # 用户当前最新的问题
])

# 这是一个没有记忆的基础链条
chain = prompt | model 

# ================= 第二步:创建一个大账本,用来管理不同人的聊天记录 =================
# 这是一个普通的 Python 字典,用来存不同 Session ID 对应的历史记录
store = {} 

# 定义一个提取历史记录的函数。LangChain 每次运行前都会自动调用它。
def get_session_history(session_id: str):
    # 如果这个 session_id 之前没聊过天(不在账本里)
    if session_id not in store:
        # 就给它新建一个空白的聊天记录本
        store[session_id] = InMemoryChatMessageHistory()
    # 返回对应的聊天记录本
    return store[session_id]

# ================= 第三步:给链条穿上"记忆外套" =================
# 将普通链条升级为带记忆的链条
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,                       # 要穿外套的基础链条
    get_session_history=get_session_history, # 获取账本的函数
    input_messages_key="question",        # 告诉它:用户输入的新问题叫什么名字 (对应 prompt 里的 {question})
    history_messages_key="history"        # 告诉它:历史记录应该填到 prompt 里的哪个坑位 (对应 MessagesPlaceholder)
)

# ================= 测试记忆功能 =================

# 1. 模拟张三 (session_id = "zhangsan_chat") 来聊天
print("--- 张三的聊天 ---")
response1 = chain_with_history.invoke(
    {"question": "你好,我叫张三,我最喜欢吃红烧肉。"},
    # 注意:我们在这里通过 config 传入 session_id
    config={"configurable": {"session_id": "zhangsan_chat"}} 
)
print("AI回答:", response1.content)

response2 = chain_with_history.invoke(
    {"question": "我刚刚说我叫什么名字?我最爱吃什么?"},
    config={"configurable": {"session_id": "zhangsan_chat"}} # 还是张三的 ID
)
print("AI回答:", response2.content) 
# AI 会成功回答:"你叫张三,最喜欢吃红烧肉。" (它记住了!)

# 2. 模拟李四 (session_id = "lisi_chat") 来聊天
print("\n--- 李四的聊天 ---")
response3 = chain_with_history.invoke(
    {"question": "嗨,请问你知道我是谁吗?"},
    config={"configurable": {"session_id": "lisi_chat"}} # 换了一个新的 ID
)
print("AI回答:", response3.content)
# AI 会回答类似:"抱歉,我不知道你是谁,我们好像是第一次聊天。" (因为不同 session_id 记录是隔离的)
3. 底层机制揭秘

当你第二次调用 invoke 问"我叫什么名字"时:

  1. LangChain 看到你传了 session_id="zhangsan_chat"

  2. 它去 store 字典里找,拿出了张三之前的聊天记录([HumanMessage("你好..."), AIMessage("你好张三...")])。

  3. 它把这些记录塞进 Prompt 的 MessagesPlaceholder(variable_name="history") 坑位里。

  4. 它把你最新的问题塞进 {question} 里。

  5. 整个一大段文字打包发给 OpenAI,所以 OpenAI 显得"有记忆"了。

随堂测试(第 10 课)

这节课的代码稍微多了一点,但逻辑非常清晰。请思考并回答以下问题:

1. 代码逻辑题: 如果在上面代码的"测试记忆功能"环节中,我在运行第二句"我刚刚说我叫什么名字?"时,忘记写 config={"configurable": {"session_id": "zhangsan_chat"}} 这个配置项,直接执行了 chain_with_history.invoke({"question": "我刚刚说我叫什么名字?"})。 你认为程序会发生什么? A. AI 也能记住张三的名字,因为默认只有一个用户。 B. 程序会直接报错,因为必须提供 session_id 来找聊天记录本。

2. 原理简答题: 我们在第 7 课讲 RAG 的文本切割时,提到过大模型是有"上下文窗口(字数)限制"的。那么根据这节课讲的记忆原理(把历史记录都打包发给模型),如果用户和一个 AI 聊了整整一天,说了几万句话,会遇到什么致命问题?

第 11 课:高级记忆策略 (防止爆显存与破产)

为了解决"历史记录无限变长"的问题,业界有两种最主流的解决方案:

  1. 滑动窗口记忆 (Sliding Window): 简单粗暴,永远只记住最近聊的 N 句话,太久远的话直接丢弃。

  2. 对话摘要记忆 (Summary): 让 AI 每隔一段时间,就把前面的废话总结成一段精简的"背景提要"。

由于我们目前使用的是最新的 LCEL 架构,处理这两种策略最优雅的方式,就是在流水线中间加一个"拦截器",把过长的消息列表修剪掉。

1. 核心概念:消息修剪 (Message Trimming)

今天我们重点演示最常用、最好懂的"滑动窗口记忆"。 在 Python 中,我们要实现"只保留最后几句话",不需要用什么高深的技术,只需要用到 Python 的基础语法:列表切片 (List Slicing)

2. 代码实例:极其详细的注释版

这段代码将展示,如何在 Prompt 填完坑之后,传给 Model 之前,把消息列表强行"剪短"。

python 复制代码
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import InMemoryChatMessageHistory

load_dotenv()

# ================= 准备基础组件 =================
model = ChatOpenAI(model="gpt-3.5-turbo")
store = {}

def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# ================= 第一步:定义 Prompt =================
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个聪明的助手。"), # 注意:这是整个列表的第 0 条消息
    MessagesPlaceholder(variable_name="history"), 
    ("human", "{question}")
])

# ================= 第二步:写一个"修剪器"函数 (核心重点!) =================
# 这个函数的作用是:接收一长串消息列表,然后只返回其中一部分
def trim_messages(messages):
    # 假设我们现在 messages 里面有 10 条消息。
    
    # Python 列表基础:len(messages) 可以获取列表里有几条消息
    if len(messages) <= 5:
        # 如果总数不到 5 条,不用修剪,直接原样返回
        return messages 
    
    # 如果超过了 5 条,我们就需要修剪了!
    # 但是注意:我们【绝不能】把第 0 条删掉,因为第 0 条通常是 SystemMessage(你是一个聪明的助手...)
    # 如果把人设删了,AI 就会"精神分裂"。
    
    # Python 列表切片语法:
    # messages[0] 取出第 0 条 (系统提示词)
    # messages[-4:] 取出最后 4 条 (也就是最近的两轮对话)
    # 把它们拼成一个新的列表返回
    trimmed = [messages[0]] + messages[-4:]
    
    return trimmed

# ================= 第三步:组装 LCEL 链条 =================
# 见证奇迹的时刻:我们把 trim_messages 这个函数,直接塞进流水线里!
chain = prompt | trim_messages | model

# ================= 第四步:穿上记忆外套 =================
chain_with_history = RunnableWithMessageHistory(
    runnable=chain,
    get_session_history=get_session_history,
    input_messages_key="question",
    history_messages_key="history"
)

# ================= 运行测试 =================
# 我们给它塞入大量历史对话来测试
history = get_session_history("my_chat")
# 手动往账本里塞入 6 条历史记录 (凑够长度)
history.add_user_message("我叫张三。")
history.add_ai_message("你好张三。")
history.add_user_message("我今年 25 岁。")
history.add_ai_message("好的,25 岁。")
history.add_user_message("我住在北京。")
history.add_ai_message("北京是个好地方。")

# 现在历史账本里已经有 6 句话了。
# 如果我们现在问问题,总消息数 = 1(系统) + 6(历史) + 1(新问题) = 8 条。
# 但是经过流水线中间的 trim_messages 拦截,最终发给大模型的只有 5 条!

print("AI 思考中...")
response = chain_with_history.invoke(
    {"question": "请问我叫什么名字?"}, # 注意:名字是在最早的第一句话里说的
    config={"configurable": {"session_id": "my_chat"}}
)

print("\n最终回答:")
print(response.content) 
# 预期输出:AI 会说"抱歉,我不知道你的名字"。
# 为什么?因为包含名字的最早那轮对话,已经被我们的 trim_messages 扔进垃圾桶啦!
# 这就成功实现了"滑动窗口",只记最新的话,永远不会爆显存!
3. 底层机制回放:数据在管道中如何变形?
  1. Prompt 填坑前:只有系统指令和新问题。

  2. Prompt 填坑后 :变成 [System, 历史1, 历史2, 历史3, 历史4, 历史5, 历史6, 新问题] (共 8 条)。

  3. 经过 trim_messages :被强行切断,变成 [System, 历史5, 历史6, 新问题] (这只保留了最后的话题)。

  4. 传给 Model:大模型收到的是被修剪过的超短消息,处理飞快,极其省钱。

第 12 课:函数调用与工具 (Function Calling & Tools)

在默认情况下,大模型只能"说"不能"做"。如果你问它"今天北京的天气怎么样?",它只能瞎猜或者告诉你它没有实时联网能力。

工具(Tools) 的出现打破了这个限制。我们可以把 Python 函数(比如查天气、搜网页、操作数据库)封装成工具,交给大模型使用。

1. 核心底层逻辑:大模型真的能运行 Python 代码吗?

绝不!这是一个极其常见的误区。 OpenAI 的服务器绝对不会、也不可能直接运行你电脑上的 Python 代码。

所谓的"大模型调用工具",底层的真实流程是这样的(请务必理解这个流程):

  1. 开发者:写好一个 Python 函数,并且写一段说明书(告诉模型这个函数叫什么名字,能干什么,需要什么参数)。

  2. 大模型:你提问"北京天气怎样?"。模型看了看说明书,发现自己解决不了,但有个查天气的工具能解决。

  3. 大模型(发号施令) :它停止生成普通文本,转而输出一段特殊的 JSON 指令(类似:"喂,人类,请帮我执行 get_weather 函数,参数是 {"city": "北京"}")。

  4. LangChain(跑腿小弟):收到指令,在你的本地电脑上真正执行这个 Python 函数,拿到结果("晴,25度")。

  5. 大模型(总结汇报):LangChain 把结果送回给模型,模型看了之后,最终对用户说:"今天北京的天气是晴天,25度。"

今天这节课,我们先学如何完成前面的 第 1 步到第 3 步(也就是定义工具,并让模型学会"发号施令")。

2. 代码实例:

这段代码将展示如何用最优雅的 @tool 装饰器定义工具,并用 bind_tools 把工具绑定到模型上。

python 复制代码
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
# 【新引入】导入 tool 装饰器,这是把普通函数变成 LangChain 工具的魔法
from langchain_core.tools import tool

load_dotenv()

# ================= 第一步:定义工具 =================

# 使用 @tool 装饰器把普通的 Python 函数变成 LangChain 认识的工具
@tool
def get_weather(location: str) -> str:
    """
    获取指定城市的当前天气情况。
    
    参数:
    location: 城市的名称,例如 "北京" 或 "上海"
    """
    # 这里我们不用真实的 API,直接用假数据模拟一下运行逻辑
    print(f"\n[本地代码执行中...] 正在查询 {location} 的天气...")
    if "北京" in location:
        return "晴天,气温 25度,适合微服私访。"
    elif "上海" in location:
        return "阴有小雨,气温 20度,记得带伞。"
    else:
        return "未知城市,查不到天气。"

# 我们来看看 LangChain 到底把这个函数变成了什么样
# print(get_weather.name)        # 输出: get_weather
# print(get_weather.description) # 输出的就是你刚才写的多行注释!

# ================= 第二步:绑定工具到模型 =================

# 1. 实例化聪明的大脑
model = ChatOpenAI(model="gpt-3.5-turbo")

# 2. 准备一个工具箱 (列表)
tools = [get_weather]

# 3. 核心魔法:bind_tools()
# 这个方法并不会把代码传给 OpenAI。
# 它在底层做的事是:把你写的注释(说明书)翻译成 OpenAI 认识的 JSON 格式,
# 然后悄悄塞进提示词里告诉模型:"嘿,你现在拥有这些超能力了!"
model_with_tools = model.bind_tools(tools)

# ================= 第三步:测试模型反应 =================

# 场景 A:问一个普通问题,不需要工具
print("--- 场景 A:普通提问 ---")
response_normal = model_with_tools.invoke("你好,请问 1+1 等于几?")
print("AI的回答是字符串吗?:", response_normal.content != "") 
print("回答内容:", response_normal.content)
# 结果:模型觉得不需要工具,直接像平时一样回答了。

# 场景 B:问一个需要工具的问题
print("\n--- 场景 B:触发工具的提问 ---")
response_tool = model_with_tools.invoke("请问今天北京的天气怎么样?")

# 注意看这里的魔法!
print("AI 有直接回答文本吗?:", response_tool.content == "") # 返回 True,因为模型没有输出普通文字!

# 当模型决定调用工具时,它会把指令存在 `tool_calls` 这个特殊属性里
print("模型想要调用的工具列表:", response_tool.tool_calls)

# 打印出来大概长这样:
# [{'name': 'get_weather', 'args': {'location': '北京'}, 'id': 'call_xxx'}]
3. 重点避坑指南:Python 的"文档字符串 (Docstring)"就是 Prompt!

在普通的 Python 开发中,函数下面的三引号注释 """...""" 只是写给其他程序员看的,你可以随便写甚至不写。 但在 LangChain 开发中,这段注释就是大模型的 Prompt!是它认识这个工具的唯一途径!

如果你不写注释,或者写得极其模糊(比如:"""这是一个好用的工具"""),大模型就完全不知道什么时候该用它,或者会乱传参数导致程序报错。你必须像教三岁小孩一样,把工具的用途和参数格式写得清清楚楚。

第 13 课:Agent 架构与 ReAct 原理

1. 核心概念:什么是 ReAct 原理?

如果我问你:"我买了两部单价 5999 元的手机,外加一个 199 元的耳机,总共多少钱?"

你大脑的思考过程是:

  1. 思考 (Thought): 我需要先算手机的钱,也就是

  2. 行动 (Action): 拿出计算器,输入

  3. 观察 (Observation): 计算器显示 11998。

  4. 思考 (Thought): 手机花了 11998,现在还要加上耳机的 199,也就是

  5. 行动 (Action): 再次按计算器,输入

  6. 观察 (Observation): 计算器显示 12197。

  7. 最终回答 (Final Answer): 总共是 12197 元。

这就是著名的 ReAct (Reason + Act,推理+行动) 框架!

Agent 的本质,就是一个写满死循环(while True)的 Python 代码:它让大模型不断地重复"思考 -> 调工具 -> 看结果 -> 再思考"的过程,直到它觉得找出了最终答案,循环才停止。

2. 代码实例:

今天我们要打造一个"数学天才 Agent"。大语言模型本身的数学计算极差,但只要给它配备了计算器工具,它就能算对一切!

python 复制代码
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate
# 【新引入 1】MessagesPlaceholder 用于给 agent 留一张"草稿纸"
from langchain_core.prompts import MessagesPlaceholder
# 【新引入 2】专门用来创建工具调用 Agent 的快捷函数
from langchain.agents import create_tool_calling_agent
# 【新引入 3】AgentExecutor 是真正帮你跑死循环的"执行器"
from langchain.agents import AgentExecutor

load_dotenv()

# ================= 第一步:准备工具箱 =================
# 我们给 Agent 提供加法和乘法两个工具
@tool
def add(a: int, b: int) -> int:
    """计算两个数字的加法。"""
    return a + b

@tool
def multiply(a: int, b: int) -> int:
    """计算两个数字的乘法。"""
    return a * b

tools = [add, multiply]

# ================= 第二步:准备大脑和提示词 =================
model = ChatOpenAI(model="gpt-3.5-turbo")

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个非常有用的数学助手。请一定要使用工具来计算,不要自己瞎猜。"),
    ("human", "{input}"), # 用户的提问填在这里
    
    # 核心重点:【Agent 的草稿纸】
    # 当 Agent 第一次调用工具拿到结果后,它需要记住这个中间结果,才能进行下一步思考。
    # "agent_scratchpad" (草稿纸) 就是用来记录它"每次调用的历史和结果"的。
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

# ================= 第三步:组装 Agent 与执行器 =================
# 1. 创建 Agent 逻辑 (告诉大模型:这是你的提示词,这是你的工具箱)
agent = create_tool_calling_agent(model, tools, prompt)

# 2. 创建 Agent 执行器 (也就是那个 while True 死循环的引擎)
# verbose=True 是一个非常棒的参数,它会在控制台把 Agent 每一步在想什么、做了什么全部打印出来!
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

# ================= 第四步:见证奇迹的时刻 =================
print("=== 开始执行 Agent ===")

# 我们问一个必须分两步计算的复杂问题
user_question = "我买了两部单价 5999 元的手机,外加一个 199 元的耳机,总共多少钱?"

# 直接调用 invoke 即可,执行器会自动帮你搞定所有循环!
result = agent_executor.invoke({"input": user_question})

print("\n=== 最终结果 ===")
print(result["output"])
3. 底层机制回放:如果我们在控制台看着它运行,会发生什么?

因为开启了 verbose=True,你会看到控制台输出类似这样充满魔力的绿色文字:

> Entering new AgentExecutor chain...

(第一轮思考) Invoking: multiply with {'a': 5999, 'b': 2} (大模型自己决定调用乘法工具) 11998 (这是你在本地 Python 代码里 5999 * 2 返回的真实数字)

(第二轮思考:它看了看 11998) Invoking: add with {'a': 11998, 'b': 199} (它把上一步的结果带入到了第二步,决定调用加法工具) 12197 (本地 Python 返回结果)

(第三轮思考:大模型觉得答案齐了,输出最终文本) 总共是 12197 元。

> Finished chain.

这一切都是全自动的!在 AgentExecutor.invoke 结束前,这段代码在底层偷偷和 OpenAI 进行了足足 3次 的网络通信交互(大模型 -> 你的电脑算乘法 -> 大模型 -> 你的电脑算加法 -> 大模型得出结论)。

第 14 课:回调系统 (Callbacks) 与运行监控

当你把应用部署到生产环境(让真实用户去用)时,你会面临两个极其现实的问题:

  1. 用户体验: AI 思考太慢了,网页一直转圈圈。我要怎么让它像 ChatGPT 那样一个字一个字地往外蹦(流式输出)

  2. 老板算账: API 是按字数(Token)收费的。我怎么才能在后台偷偷记录下每次对话究竟花了多少钱

这就需要用到 LangChain 底层极其强大的回调系统(Callback System)

1. 核心概念:什么是 Callback(回调)?

你可以把大模型的运行过程想象成一趟行驶的绿皮火车。 火车会经过不同的站点(比如:开始思考、生成一个字、调用工具、遇到报错、结束运行)。 Callback 就像是你派去驻扎在各个站点的"侦察兵"。 每当火车经过某个站点,侦察兵就会立刻给你发一条微信报告情况。

LangChain 提供了一个基类 BaseCallbackHandler,里面定义了各种"站点"的方法,比如:

  • on_llm_start: 大脑开始思考时触发

  • on_llm_new_token: 每生成一个新字时触发 (极其重要,用于流式打字效果)

  • on_tool_start: 开始跑腿调用工具时触发

  • on_chain_end: 整个流水线全部跑完时触发

2. 代码实例:极其详细的注释版

今天我们要自己写一个"侦察兵",它可以同时做到两件事:在控制台实现打字机效果 ,并且在最后统计总共消耗了多少 Token

python 复制代码
import os
from dotenv import load_dotenv

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 【新引入】导入基础的回调处理器
from langchain_core.callbacks import BaseCallbackHandler

load_dotenv()

# ================= 第一步:自定义我们的侦察兵 (Callback) =================
# 我们写一个类,继承自 BaseCallbackHandler
class MyCustomHandler(BaseCallbackHandler):
    
    def __init__(self):
        # 准备一个小本本,用来记总共收到了多少个字
        self.token_count = 0 
        
    # 重写 on_llm_new_token 方法。
    # 每当 OpenAI 的服务器返回一个字的包裹,LangChain 就会自动调用这个方法一次!
    def on_llm_new_token(self, token: str, **kwargs) -> None:
        self.token_count += 1 # 计数器加 1
        
        # print 默认是会换行的,加上 end="" 和 flush=True,就能实现一个字一个字往后拼的打字效果
        print(token, end="", flush=True)

    # 重写 on_llm_end 方法。
    # 当整段话全部生成完毕时,LangChain 会自动调用它。
    def on_llm_end(self, response, **kwargs) -> None:
        # 打字结束了,我们换个行,然后汇报一下计费情况
        print(f"\n\n[后台监控] 报告老板!本次回答一共生成了 {self.token_count} 个 Token。")


# ================= 第二步:组装流水线 =================
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个讲故事的专家。请写一段 50 字左右的微小说。"),
    ("human", "{topic}")
])

# 注意看!
# 要想让模型一个字一个字地吐出来,必须在实例化模型时开启 streaming=True
model = ChatOpenAI(model="gpt-3.5-turbo", streaming=True)

chain = prompt | model

# ================= 第三步:带上侦察兵去执行 =================
print("AI 开始创作:\n")

# 我们在 invoke 的时候,通过 config 里的 callbacks 参数,把我们的侦察兵派出去
# 注意它是一个列表,意味着你可以同时派好几个不同的侦察兵去干不同的事
chain.invoke(
    {"topic": "赛博朋克与流浪猫"},
    config={"callbacks": [MyCustomHandler()]}
)
3. 进阶玩法:LangSmith 一键监控

在实际的公司项目里,大家通常不会自己写那么复杂的 Print 回调代码。LangChain 官方做了一个极其强大的平台叫 LangSmith

只要你在 .env 文件里配好三个变量:

复制代码
LANGCHAIN_TRACING_V2=true
LANGCHAIN_ENDPOINT="https://api.smith.langchain.com"
LANGCHAIN_API_KEY="你的_LangSmith_密钥"

你的代码连一行都不用改! LangChain 会在底层自动安插侦察兵,把你整个代码的耗时、花了多少钱、Prompt 是怎么拼接的、甚至报了什么错,全部以极漂亮的图表展示在他们的网页端后台上。

(由于这是外部服务,我们这里仅作概念介绍,无需强制运行)

第 15 课:自定义组件 (接入公司私有模型与搜索)

在前面的课程里,我们用的都是 ChatOpenAI 这样的官方现成组件。 但在真实的企业级开发中,你极大概率会遇到以下情况:

  1. 私有模型: 你们公司为了保密,花重金在内网部署了一个自己微调过的大模型(比如基于 Llama 3 或 Qwen),没有任何现成的 LangChain 包可以直接用。

  2. 私有搜索: 你们公司的文档不存在 Chroma 向量数据库里,而是存在公司自己研发的老旧搜索引擎里。

这个时候,你就必须学会"手搓"组件。只要你搓出来的组件符合 LangChain 的底层协议(Runnable),你就能把它无缝接入到之前学过的所有 LCEL 流水线里!

1. 核心概念:继承与重写接口

在 Python 面向对象编程中,LangChain 规定好了"模具"(基类)。你只需要拿一个模具过来,按它的要求把核心业务逻辑填进去即可。

  • 自定义纯文本模型: 继承 LLM 基类,重写 _call 方法。

  • 自定义检索器: 继承 BaseRetriever 基类,重写 _get_relevant_documents 方法。

2. 代码实例:极其详细的注释版

这段代码将演示:如何把一个完全虚构的"公司内部烂接口",包装成高级的 LangChain 组件,并用 LCEL | 连起来。

python 复制代码
import time
from typing import Any, List, Optional
from langchain_core.callbacks.manager import CallbackManagerForLLMRun
from langchain_core.language_models.llms import LLM
from langchain_core.retrievers import BaseRetriever
from langchain_core.documents import Document
from langchain_core.prompts import PromptTemplate

# ================= 第一步:自定义公司内部的大模型 =================

# 继承 LangChain 底层的 LLM 基类
class MyCompanyInternalLLM(LLM):
    # 你可以定义一些自定义参数,比如控制回答字数的限制
    max_words: int = 100 

    # 【必须重写 1】告诉 LangChain 你这个模型叫什么类型 (用于后台日志记录)
    @property
    def _llm_type(self) -> str:
        return "my_company_internal_model"

    # 【必须重写 2】这是核心运行逻辑!当调用 invoke 时,其实底层就是跑这个函数
    def _call(
        self,
        prompt: str, # 这是处理好之后传进来的纯文本提示词
        stop: Optional[List[str]] = None,
        run_manager: Optional[CallbackManagerForLLMRun] = None,
        **kwargs: Any,
    ) -> str:
        
        # ---------------- 模拟调用公司内网 API 的过程 ----------------
        print(f"\n[内网通信] 正在将 Prompt 发送给公司机房...\nPrompt内容: {prompt}")
        time.sleep(1) # 假装网络延迟
        
        # 模拟模型瞎编的一个回答
        fake_response = f"这是来自公司内网模型的绝密回答。你问的问题太深奥了,我只能说:无可奉告!(限制字数:{self.max_words})"
        # -----------------------------------------------------------
        
        # 返回纯字符串即可,LangChain 会自动帮你把它包装好
        return fake_response


# ================= 第二步:自定义公司内部的搜索引擎 =================

# 继承 LangChain 的 BaseRetriever 基类
class MyCompanyRetriever(BaseRetriever):

    # 【必须重写】获取相关文档的核心逻辑
    def _get_relevant_documents(
        self, query: str, *, run_manager
    ) -> List[Document]:
        
        # ---------------- 模拟调用公司老旧数据库 ----------------
        print(f"\n[内部搜索] 正在公司古老的数据库里搜索关键字: '{query}'...")
        time.sleep(1) # 假装搜索很慢
        
        # 假设我们查到了两条数据,我们必须把它包装成 LangChain 标准的 Document 对象!
        doc1 = Document(page_content="内部机密文件 001:公司禁止带宠物上班。")
        doc2 = Document(page_content="内部机密文件 002:食堂周五免费加鸡腿。")
        # ---------------------------------------------------------
        
        return [doc1, doc2]


# ================= 第三步:见证奇迹!用 LCEL 无缝组装自定义组件 =================

print("开始组装全自研 RAG 系统...")

# 1. 实例化我们手搓的组件
custom_retriever = MyCompanyRetriever()
custom_llm = MyCompanyInternalLLM(max_words=50)

# 2. 准备提示词模板
# 这里为了简便,我们用基础的纯文本 PromptTemplate
prompt = PromptTemplate.from_template(
    "请根据以下参考资料回答问题:\n{context}\n\n问题:{question}"
)

# 3. 经典的并行字典开头
setup_dict = {
    "context": custom_retriever, # 使用手搓的检索器
    "question": lambda x: x      # 这是一个 Python 小技巧,等同于 RunnablePassthrough(),原样传递
}

# 4. LCEL 组装链条!注意看,用法和之前连接 OpenAI 时一模一样!
custom_rag_chain = setup_dict | prompt | custom_llm

# ================= 第四步:执行 =================

user_input = "明天周五食堂有什么好吃的?"
print(f"\n用户提问: {user_input}")

# invoke 触发整个全自研流水线
result = custom_rag_chain.invoke(user_input)

print("\n最终拿到结果:")
print(result)
3. 为什么掌握这一点才算真正入门了 LangChain?

很多初学者抱怨:"LangChain 太臃肿了,我的业务特殊,它现成的工具根本满足不了我,我还不如不用它。" 这其实是因为没有理解 LangChain 的接口解耦设计 。 LangChain 从来不是要限制你用什么工具,而是为你提供了一个统一的插座板(Runnable 协议)。只要你按照上面的方法,把你们公司的系统插头稍微改造一下,插到这个插座板上,你就能瞬间白嫖 LangChain 所有的生态能力(比如自动拥有流式输出、自动拥有重试机制、自动接入 LangSmith 监控、能接进 Agent 框架)。

相关推荐
百度Geek说1 小时前
Browser Use:为 Agent 构建 Runtime Harness
人工智能
Michelle80231 小时前
25大数据 11-1 函数
开发语言·python
aini_lovee1 小时前
C#与倍福PLC(通过ADS协议)通信上位机源程序实现
开发语言·c#
用户4330514143811 小时前
流程控制与并行工作
人工智能
云天AI实战派1 小时前
ChatGPT/API 调用故障排查指南:Realtime 音频、智能体浏览器操作与 AI 编码代理全流程修复手册
人工智能·chatgpt·音视频
fie88891 小时前
基于 MATLAB 的前景背景分割系统
开发语言·matlab
水上冰石1 小时前
怎么查看olama是否用到了显卡加速
人工智能·显卡
码点滴1 小时前
用自然语言指挥 K8s 集群:AI 运维 Agent 的架构原理与可运行原型
运维·人工智能·kubernetes
Wanderer X1 小时前
【LLM】PPO
人工智能