03 LangChain

本文记录的是我用 Vibe Coding 方式学习 LangChain 相关技术的 AI (Gemini-3.1-Pro)生成对话。
为什么学:2026 年最通用 LLM 应用框架,生态最完善。
核心内容:Chains / LCEL / Memory / Tools / Agents 基础 / Indexes。
项目:用 LangChain 搭建一个带记忆 + 工具调用的客服 Bot。

第一课:LangChain 基础入门与 DeepSeek LLM 调用

🗺️ 我们的下一步计划

  1. 今天的第一课:基础入门(用 LangChain 成功调通你的 DeepSeek API)
  2. 后续:学习链(Chains)、记忆(Memory)、检索增强(接入你的本地 bge-large-zh 模型),最后组装成客服 Bot。

📖 概念小讲堂:LangChain 与 LLM 调用

什么是 LangChain?
简单来说,LangChain 就是一个胶水框架(Glue Framework)。大模型(LLM)本身只是个“聪明的大脑”,但它没有手脚(不能查资料、不能记仇、不会用计算器)。LangChain 提供了一套标准化的管道,把“大脑”和“手脚”拼装在一起。

如何调用 DeepSeek?
DeepSeek 非常贴心,它的 API 接口完全兼容了 OpenAI 的格式。因此在 LangChain 里,我们不需要找专门的 DeepSeek 库,而是直接使用 ChatOpenAI 这个类,只需要把它的“请求地址(Base URL)”和“钥匙(API Key)”替换成 DeepSeek 的即可。这就是业界常说的接口兼容

核心概念:消息类型 (Message Types)
LangChain 把对话标准化为了几种角色:

  • SystemMessage(系统消息):给大模型定人设的(比如:你是一个客服)。
  • HumanMessage(人类消息):用户说的话。
  • AIMessage(AI消息):大模型回复的话。

💻 你的第一次挖空练习

我已经为你准备好了第一份练习代码:01_basic_llm.py。

请打开这个文件,你会看到里面有几个标记了 # TODO 的地方。你的任务是:

  1. 找到所有的 # TODO
  2. 根据注释里的提示,把代码补全(主要是把大模型实例化,并让它跑起来)。
  3. 补充完成后,在终端运行它:
    pip install langchain langchain-openai langchain-core  # 如果你还没安装依赖的话,先执行这句
    python 01_basic_llm.py
    

等你填完代码并成功跑出结果后,把你的代码发给我看看,或者告诉我你遇到了什么报错,我们一起来 Review!

准备好了吗?开始动手吧!如果哪里卡住了,随时问我。

点击查看代码
import os
# 从 LangChain 导入兼容 OpenAI 接口的模型类,因为 DeepSeek API 完全兼容 OpenAI
from langchain_openai import ChatOpenAI
# 从 LangChain 核心库导入“人类消息”类
from langchain_core.messages import HumanMessage

def main():
    """
    第一课:LangChain 基础调用
    目标:使用 LangChain 成功调用 DeepSeek API 并获取回复。
    """
    print("🤖 正在初始化大模型...")
    
    # 1. 获取系统环境变量中的 API Key
    api_key = os.getenv("DEEPSEEK_API_KEY")
    if not api_key:
        print("❌ 找不到 DEEPSEEK_API_KEY,请检查环境变量设置!")
        return

    # 2. 初始化大模型对象 (LLM)
    # TODO: 请在这里实例化 ChatOpenAI。
    # 提示:你需要传入 api_key,并且指定 model="deepseek-chat"
    # 注意:因为我们用的是 DeepSeek 的服务,所以需要将 base_url 设置为 "https://api.deepseek.com"
    llm = ChatOpenAI(
        api_key=None,       # TODO: 填入正确的变量
        model=None,         # TODO: 填入 DeepSeek 的模型名称字符串
        base_url=None,      # TODO: 填入 DeepSeek 的 API 地址字符串
        max_tokens=100      # 限制一下输出长度,节约时间
    )

    # 3. 构造给大模型的消息
    # 在 LangChain 中,用户的输入通常被封装为 HumanMessage (人类消息)
    messages = [
        HumanMessage(content="你好,DeepSeek!请用一句话做个自我介绍。")
    ]

    print(f"发送消息:{messages[0].content}")
    
    # 4. 调用模型获取回复
    # TODO: 使用 llm 对象的方法来处理 messages 并获取结果
    # 提示:LangChain 推荐使用 invoke() 方法
    response = None # TODO: 请替换为调用代码
    
    if response:
        print("\n✅ 收到回复:")
        # TODO: 打印出模型回复的纯文本内容。
        # 提示:response 是一个 AIMessage 对象,它的文本内容存储在一个名为 content 的属性中
        print(f"模型说:") # TODO: 替换这里的打印逻辑

if __name__ == "__main__":
    main()

第二课:LCEL 语法与 Chains(构建基础问答链)

第一课里我们直接写死了一句话发给大模型,但实际开发中,我们不可能每次都手敲完整的对话。这就引出了我们第二课的主角——LCEL 语法与 Chains(链)


🗺️ 下一步计划:掌握 LangChain 最核心的 LCEL 语法

我们的目标是:把“模板填空”、“调用大模型”和“提取纯文本”这三个步骤,像工厂流水线一样串联起来。这就是 Chain(链)。


📖 概念小讲堂:LCEL 与流水线作业

  1. 什么是 PromptTemplate(提示词模板)?
    想象一下“请假条模板”:“尊敬的__老师,我是__,因为__原因请假__天。”。我们只需要把空格填上就能生成一张完整的请假条。在代码里,我们用大括号 {} 来表示填空处。

  2. 什么是 OutputParser(输出解析器)?
    在第一课里,你发现大模型返回的是一个 AIMessage 对象,你要手动写 response.content 才能把文字拿出来。这很繁琐。StrOutputParser 就像一个翻译官,它自动把复杂的对象剥开,只留下纯文本字符串。

  3. 什么是 LCEL (LangChain Expression Language)?
    这是 LangChain 的灵魂!它允许你用管道符 | (就像水管一样)把各个组件串起来。
    比如:面粉(输入变量) | 压面机(Prompt模板) | 烤箱(大模型) | 切片机(解析器) = 面包(纯文本结果)
    对应的代码就是:chain = prompt | llm | parser

为什么要这么学?
在 2026 年的 LangChain 生态里,老的 LLMChain 写法已经逐渐被淘汰,现在官方强烈推荐且所有高级功能(比如并发、流式输出、Agent 工具调用)都依赖于 LCEL 管道语法。掌握它,你就掌握了 LangChain 的命脉。


💻 你的第二次挖空练习

我已经为你准备好了第二份练习代码:02_lcel_chain.py。

这次我们要构建一个简易的翻译小助手流水线。请打开这个文件,你的任务依然是:

  1. 找到所有的 # TODO
  2. 自己设计一个带有占位符的 PromptTemplate 字符串。
  3. | 符号把 promptllmparser 组装成一个 chain
  4. 构造一个包含占位符具体内容的字典 input_data,然后调用 chain.invoke()

写完之后,在终端运行:

python 02_lcel_chain.py

如果你遇到任何报错,或者对模板怎么写有疑问,随时把代码发给我。

现在的概念和第一课比起来,是不是觉得流水线的思想更优雅了?理解这个 | 的概念了吗?准备好就开始填空吧!

点击查看代码
import os
from langchain_openai import ChatOpenAI
# 从 LangChain 核心库导入 PromptTemplate (提示词模板)
from langchain_core.prompts import PromptTemplate
# 从 LangChain 核心库导入 StrOutputParser (字符串输出解析器)
from langchain_core.output_parsers import StrOutputParser

def main():
    """
    第二课:LCEL 语法与基础链 (Chain)
    目标:使用 PromptTemplate + LLM + StrOutputParser 构建一个简易翻译小助手。
    """
    print("🔗 正在构建翻译 Chain...")
    
    # 1. 初始化 LLM (复用第一课的知识)
    api_key = os.getenv("DEEPSEEK_API_KEY")
    llm = ChatOpenAI(
        api_key=api_key,
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        max_tokens=200
    )

    # 2. 创建 Prompt 模板 (PromptTemplate)
    # 作用:我们不希望每次都手写完整的句子,而是像填空题一样,只需要填入核心内容。
    # 提示:使用大括号 {variable_name} 来占位
    # TODO: 创建一个翻译模板,比如 "请将以下{source_lang}文本翻译成{target_lang}:\n{text}"
    template_str = "" # TODO: 填入带有至少两个占位符的字符串模板
    prompt = PromptTemplate.from_template(template_str)

    # 3. 创建输出解析器 (OutputParser)
    # 作用:在第一课中,我们需要手动写 response.content 来获取纯文本。
    # StrOutputParser 可以自动把 LLM 返回的 AIMessage 对象转换成普通的纯文本字符串。
    parser = StrOutputParser()

    # 4. 使用 LCEL (LangChain Expression Language) 组装 Chain
    # 提示:在 LangChain 中,我们用管道符 `|` 把各个组件串联起来,就像流水线一样
    # TODO: 按照 prompt -> llm -> parser 的顺序组装 chain
    chain = None # TODO: 填入组装代码,例如 a | b | c

    # 5. 运行 Chain
    # 提示:传入一个字典,字典的键必须和你在模板里定义的占位符变量名完全一致
    print("🚀 运行 Chain 中...")
    input_data = {
        # TODO: 根据你上面 template_str 定义的占位符变量名,填入对应的键值对
        # 例如如果你的模板有 {text},这里就要写 "text": "LangChain 是一个非常强大的大模型开发框架!"
    }
    
    # TODO: 调用 chain.invoke() 并传入 input_data 字典
    result = None # TODO: 请替换为实际的调用代码

    if result:
        print("\n✅ 翻译结果:")
        # 因为我们使用了 StrOutputParser,现在的 result 直接就是纯文本字符串啦!
        print(result)

if __name__ == "__main__":
    main()

第三课:记忆机制 (Memory)(让模型记住上下文)

现在你已经能让大模型根据模板干活了,但你会发现一个问题:如果你问它“我叫什么名字”,它肯定会说不知道。因为大模型本质上是一条“金鱼”,它的记忆只有 7 秒——每次调用它都是独立且无状态(Stateless)的

这就引出了我们今天的重点:第三课。


🗺️ 下一步计划:赋予大模型“记忆”(Memory)

要搭建一个客服 Bot,它必须能记住顾客前面说过的话,否则顾客会疯掉的(“我都说了我订单号是1234,你怎么又问我?”)。我们要学习如何给这条流水线加上记忆机制


📖 概念小讲堂:大模型是怎么“记住”事情的?

  1. 记忆的本质(History)
    大模型其实并没有真正的“脑容量”去记住你。所谓的“记忆”,就是每次提问时,我们都在后台偷偷把你之前所有的聊天记录(历史消息)打包,一起塞进 Prompt 里发给它。它看到了历史,就“显得”它记住了。

  2. MessagesPlaceholder(消息占位符)
    在第二课里,你用 {text} 留了一个填字符串的空。现在我们需要留一个能填入一长串历史消息的空,这就是 MessagesPlaceholder 的作用。你可以把它理解为 Prompt 模板里的一个“抽屉”,专门用来放聊天记录。

  3. RunnableWithMessageHistory(历史消息包装器)
    这是 LangChain 里(特别是 2026 年现代写法中)最核心的记忆组件。
    你可以把它想象成一个自动打字员:它站在你组装好的基础 Chain 外面。每次你输入新话,它就自动打开历史记录本,把旧对话塞进“抽屉”里让 Chain 执行;等大模型回复后,它又自动把你们的一问一答抄到本子上,留着下次用。


💻 你的第三次挖空练习

我已经为你准备好了第三份练习代码:[03_memory.py](file:///e:/file/大模型技术学习/03-LangChain/03_memory.py)。

请打开这个文件,我们要实现一个能记住你名字的连续对话链

  1. 找到 # TODO
  2. 在 Prompt 模板里,用 MessagesPlaceholder(variable_name="history") 留出一个给历史消息的占位符。
  3. promptllm 和解析器组装成基础的 chain
  4. 在下面的两回合测试中,分别填入“告诉它名字”和“考考它名字”的 input

写完之后,在终端运行:

python 03_memory.py

提示:注意观察 chain_with_history.invoke 里的 config={"configurable": {"session_id": "user_123"}},这就是告诉“自动打字员”去翻哪一个用户的本子。

快去试试吧!如果你在填 MessagesPlaceholder 的时候觉得有点绕,或者跑出来报错了,随时把代码发给我!我们一起看。

点击查看代码
import os
from langchain_openai import ChatOpenAI
# 从核心库导入对话模板和消息占位符
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
# 导入基于 LCEL 的消息历史包装器 (2026 年现代写法)
from langchain_core.runnables.history import RunnableWithMessageHistory
# 导入内存中的聊天历史记录类
from langchain_community.chat_message_histories import ChatMessageHistory

def main():
    """
    第三课:记忆机制 (Memory)
    目标:让大模型记住你说过的话,实现连续对话。
    """
    print("🧠 正在为大模型装载记忆模块...")

    # 1. 初始化 LLM
    api_key = os.getenv("DEEPSEEK_API_KEY")
    llm = ChatOpenAI(
        api_key=api_key,
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        max_tokens=200
    )

    # 2. 创建带有“记忆占位符”的 Prompt 模板
    # 注意:我们这里使用 ChatPromptTemplate.from_messages,它允许我们按列表传入多种角色的消息
    prompt = ChatPromptTemplate.from_messages([
        ("system", "你是一个记性很好的智能助手。"),
        # TODO: 使用 MessagesPlaceholder 留出一个名为 "history" 的位置,专门用来塞入历史对话记录
        # 提示:MessagesPlaceholder(variable_name="你的变量名")
        None, # TODO: 替换为 MessagesPlaceholder
        ("human", "{input}") # 这是当前用户的最新输入
    ])

    # 3. 组装基础 Chain
    # TODO: 用 LCEL 管道符将 prompt, llm, StrOutputParser() 串联起来
    chain = None # TODO: 替换为组装代码

    # 4. 创建一个用来存放历史记录的“小本本”
    # 在实际应用中,这里可能会连接数据库(比如 Redis),但今天我们在内存里模拟即可
    demo_ephemeral_chat_history = ChatMessageHistory()

    # 这是一个辅助函数,告诉 LangChain 去哪里拿历史记录(通过 session_id 区分不同用户的对话)
    def get_session_history(session_id: str):
        return demo_ephemeral_chat_history

    # 5. 用 RunnableWithMessageHistory 包装我们的基础 Chain
    # 作用:它会在每次执行前,自动把 get_session_history() 里的内容塞进 prompt 的 history 占位符里
    # 并在执行后,自动把一问一答追加进历史记录里
    chain_with_history = RunnableWithMessageHistory(
        chain,
        get_session_history,
        input_messages_key="input",    # 告诉包装器,哪个变量是用户当前的新输入
        history_messages_key="history" # 告诉包装器,刚才在 prompt 里留的占位符叫什么名字
    )

    # 6. 开始测试!
    print("\n[第一回合] 告诉大模型一个秘密...")
    # 注意:使用带有历史记录的 Chain 时,必须在 config 里传入 session_id
    config = {"configurable": {"session_id": "user_123"}}
    
    # TODO: 向模型发送一句话,比如 "你好,我的名字叫小明!"
    res1 = chain_with_history.invoke(
        {"input": "..." }, # TODO: 填入你想告诉模型的话
        config=config
    )
    print(f"🤖 模型回答: {res1}")

    print("\n[第二回合] 考考大模型记不记得...")
    # TODO: 问模型刚才告诉它的名字是什么
    res2 = chain_with_history.invoke(
        {"input": "..." }, # TODO: 填入你的提问,比如 "你还记得我叫什么名字吗?"
        config=config
    )
    print(f"🤖 模型回答: {res2}")

if __name__ == "__main__":
    main()

第四课:检索增强 (Indexes/RAG)(接入本地 bge-large-zh 向量模型)

现在大模型能“按要求说话”(Chain),也能“记住你刚说过的话”(Memory),但是——如果用户问:“你们店普通会员多少钱免邮?”
DeepSeek 肯定不知道,因为它没看过你们店的规矩。

这就来到了 2026 年最火的、也是做 AI 客服最核心的技术:第四课:检索增强生成 (RAG - Retrieval-Augmented Generation)。这也是为什么要用到你本地下载好的 bge-large-zh-v1__5 模型的原因。


🗺️ 下一步计划:让大模型查阅“私有知识库”

我们要做的事其实非常符合人类的直觉:

  1. 先把一大堆说明书(私有知识)放进书架。
  2. 当用户提问时,我们先不去问大模型,而是先去书架里翻出相关的几句话
  3. 把这几句话连同用户的问题,一起扔给大模型说:“你看这段资料,再回答用户的问题”。

📖 概念小讲堂:向量化 (Embeddings) 与向量数据库

  1. 怎么“翻书架”找相关内容?
    在代码里,我们不能靠简单的“关键词匹配”(比如用户搜“退钱”,书里写的却是“退款”,就会搜不到)。
    我们需要用 Embedding(嵌入模型) 把文字变成一长串数字(向量)。如果两段话的意思相近,它们的数字就会很相似。
    你本地的 bge-large-zh 就是一个专门用来把中文变成数字的优秀模型!

  2. 向量数据库 (VectorStore)
    把文字变成数字后,我们需要一个地方把它们存起来。这节课我们使用 FAISS,这是 Facebook 开源的一个极其轻量且速度飞快的本地向量库。它专门用来在一大堆数字里,找出最相近的那几个。

  3. RunnablePassthrough(透传组件)
    这是 LCEL 流水线里的一个新朋友。它的作用是当个“传球手”——把流水线开头输入的东西,原封不动地传给后面的占位符(比如 Prompt 里的 {question})。


💻 你的第四次挖空练习

我已经为你准备好了第四份代码:[04_rag_basic.py](file:///e:/file/大模型技术学习/03-LangChain/04_rag_basic.py)。

请打开这个文件。在运行之前,因为我们要用到本地模型和 FAISS 数据库,你需要先在终端安装两个库

pip install faiss-cpu sentence-transformers langchain-community

安装完成后,你的任务是:

  1. 找到所有的 # TODO
  2. 实例化 HuggingFaceBgeEmbeddings,记得传入你的本地模型路径变量 model_path
  3. 调用 FAISS.from_texts(texts, embeddings) 把我们模拟的知识库灌进数据库。
  4. 调用 chain.invoke(question) 并打印出结果。

写完之后,在终端运行:

python 04_rag_basic.py

第一次加载本地的 bge 模型可能需要稍微等几秒钟。如果你遇到了缺少依赖的报错,或者有哪里看不懂,随时发出来,我们一起解决!准备好了吗?开始!

点击查看代码
import os
# 导入 LangChain 核心组件
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 导入一个特殊的 LCEL 组件:透传工具(原封不动地传递输入)
from langchain_core.runnables import RunnablePassthrough

# 导入 HuggingFace 嵌入模型类(用来把文本变成向量)
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
# 导入 FAISS,这是一个非常轻量且强大的本地向量数据库
from langchain_community.vectorstores import FAISS

def main():
    """
    第四课:检索增强生成 (RAG)
    目标:让大模型读取外部的“私有知识库”,根据检索到的内容回答问题。
    """
    print("🔍 正在初始化本地向量模型 (可能需要几秒钟)...")
    
    # 1. 初始化本地 Embedding 模型
    # 注意:这里使用的是你本地已经下载好的 bge-large-zh 模型路径
    model_path = r"E:\file\毕业设计\实战\智能客服系统\Backend\_models\bge-large-zh-v1__5"
    
    # TODO: 实例化 HuggingFaceBgeEmbeddings
    # 提示:传入 model_name=model_path 即可
    embeddings = HuggingFaceBgeEmbeddings(model_name=None) # TODO: 填入正确的变量
    
    # 2. 准备一些“私有知识”(模拟客服的内部规定)
    # 大模型本来是不知道这些“你们店里的规矩”的
    texts = [
        "我们店的退换货政策是:支持7天无理由退换货,但商品必须不影响二次销售。",
        "VIP会员满200元包邮,普通会员满500元包邮。",
        "客服工作时间为每天早9点到晚10点,节假日不休息。"
    ]
    
    print("📚 正在将知识库灌入向量数据库...")
    # 3. 创建向量数据库并灌入数据
    # TODO: 使用 FAISS.from_texts 将 texts 和 embeddings 结合起来,存入数据库
    vectorstore = None # TODO: 替换为调用 FAISS 的 from_texts 方法
    
    # 将向量数据库转换为“检索器” (Retriever)
    # search_kwargs={"k": 1} 表示每次检索只找出最相关的 1 条知识
    retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) 
    
    # 4. 初始化大模型 LLM (DeepSeek)
    llm = ChatOpenAI(
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        max_tokens=200
    )
    
    # 5. 构建 RAG 专用的 Prompt
    # 核心思想:把找出来的 context 和用户的 question 拼在一起给大模型看
    prompt = ChatPromptTemplate.from_template("""
你是一个严谨的客服助手。请**仅根据**以下提供的上下文信息来回答用户的问题。
如果上下文中没有提到,请诚实地回答“我不知道”,千万不要自己瞎编。

上下文信息:
{context}

用户问题:
{question}
""")
    
    print("🔗 组装 RAG 检索链...")
    # 6. 组装 RAG 链 (LCEL 的高阶用法)
    # 解析:
    # 字典的第一步是准备 Prompt 需要的两个变量:context 和 question
    # "context": retriever 会根据用户的输入,自动去向量库里找出相关的句子
    # "question": RunnablePassthrough() 会把用户的输入原封不动地传给 question
    chain = (
        {"context": retriever, "question": RunnablePassthrough()} 
        | prompt 
        | llm 
        | StrOutputParser()
    )
    
    # 7. 开始测试!
    # 让我们问一个关于“你们店规定”的问题
    question = "普通会员买多少钱可以包邮?"
    print(f"\n🙋 用户提问: {question}")
    
    # TODO: 调用 chain.invoke(),传入 question 字符串
    # 注意:和前面传字典不同,这里的 RunnablePassthrough() 允许我们直接传一个字符串进去!
    res = None # TODO: 替换为实际调用代码
    print(f"🤖 模型回答: {res}")

if __name__ == "__main__":
    main()

第五课:工具调用 (Tools) 与 Agents 基础

但是,如果用户说:“帮我退款”,或者“帮我查一下我昨天的订单物流到哪了”,光靠查资料是不够的,大模型需要去执行动作(比如去调用物流 API、去操作数据库)。

这就是我们今天的第五课,也是走向高级 AI 应用(Agent 智能体)的必经之路:工具调用 (Tools)。我们要给大模型装上“手”。


🗺️ 下一步计划:让大模型学会使用工具

我们的目标是:定义一个 Python 函数(比如查订单),然后告诉大模型:“当用户问订单时,你就用这个函数去查。”大模型会自己判断什么时候该聊天,什么时候该用工具


📖 概念小讲堂:大模型怎么知道该用什么工具?

  1. @tool 装饰器
    在 LangChain 里,我们只需要在一个普通的 Python 函数头上加一个 @tool,它就变成了大模型可以识别的工具。
    核心秘诀:函数里的中文注释(Docstring)!大模型其实是读了你的注释,才明白这个工具是干嘛用的。如果注释写得不好,大模型就会乱用或者不用。

  2. 绑定工具 (bind_tools)
    大模型对象(llm)本来是没有工具的。我们通过 llm.bind_tools([工具1, 工具2]),就像是把一个工具箱递给了大模型。

  3. 模型是如何“使用”工具的?
    这是一个常见的误区:很多人以为调用了 invoke,大模型就会自动帮你把代码执行了。
    其实不是的! 大模型的本质只是文本生成。当它发现需要用工具时,它会返回一个特殊的 JSON 格式(包含“我想调用的工具名字”和“我想传的参数”),这个返回结果在 LangChain 里被存放在 response.tool_calls 里。
    至于真正去执行那个 Python 函数并拿到结果,那是你的代码(或者更高阶的 Agent 框架)要干的活。今天我们先学习它如何“做出决定”。


💻 你的第五次挖空练习

我已经为你准备好了第五份练习代码:[05_tools_basic.py](file:///e:/file/大模型技术学习/03-LangChain/05_tools_basic.py)。

请打开这个文件,我们要实现一个会判断是否需要查订单的模型:

  1. 找到所有的 # TODO
  2. 使用 llm.bind_tools([check_order_status]) 把工具绑定给大模型。
  3. 在测试 1 和测试 2 中,使用这个绑定了工具的模型去 invoke 消息。

写完之后,在终端运行:

python 05_tools_basic.py

重点观察终端的输出:
在测试 1(闲聊)中,tool_calls 应该是一个空列表 []
在测试 2(查订单)中,tool_calls 里应该包含了大模型提取出来的订单号 ORD123

准备好了吗?开始填空吧!如果遇到问题随时喊它用工具它却不用,或者报了错,马上告诉我!

点击查看代码
import os
from langchain_openai import ChatOpenAI
# 从核心库导入 tool 装饰器,用来把普通 Python 函数变成大模型可以调用的工具
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

def main():
    """
    第五课:工具调用 (Tools)
    目标:让大模型知道有外部工具可用,并能根据用户需求决定是否调用。
    """
    print("🛠️ 正在给大模型装配工具箱...")

    # 1. 初始化大模型
    # 注意:不是所有模型都支持工具调用,但 DeepSeek 是支持的!
    llm = ChatOpenAI(
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        max_tokens=200
    )

    # 2. 定义一个工具函数
    # 使用 @tool 装饰器。注意:函数的 docstring (文档字符串) 非常重要!
    # 大模型就是通过阅读这段中文注释,来决定要不要调用这个工具的。
    @tool
    def check_order_status(order_id: str) -> str:
        """
        当你需要查询用户的订单状态时,调用这个工具。
        参数 order_id 是用户提供的订单号,比如 'ORD837'。
        """
        print(f"\n[系统日志] 🔔 大模型正在调用工具: 查询订单号 {order_id}")
        
        # 模拟查数据库的逻辑
        mock_db = {
            "ORD123": "已发货,顺丰快递,预计明天送达",
            "ORD456": "待付款,请提醒用户尽快支付"
        }
        return mock_db.get(order_id, "找不到该订单,请核对订单号")

    # 3. 将工具绑定到大模型上
    # 告诉大模型:“这是你的工具箱,里面有 check_order_status,需要的时候你可以用。”
    # TODO: 使用 llm.bind_tools() 方法把我们的工具放进去,传一个列表
    llm_with_tools = None # TODO: 替换为绑定代码,比如 llm.bind_tools([check_order_status])

    # 4. 测试场景 1:不需要工具的情况
    print("\n🙋 测试 1:闲聊 (不应触发工具)")
    msg1 = HumanMessage(content="你好呀,今天天气真不错。")
    # TODO: 使用绑定了工具的 llm_with_tools 调用 invoke
    res1 = None # TODO: 调用 invoke 传入 [msg1]
    
    # 我们打印出 res1.tool_calls 看看大模型有没有决定用工具
    print(f"🤖 模型回复: {res1.content}")
    print(f"🔧 工具调用记录: {res1.tool_calls}")

    # 5. 测试场景 2:需要工具的情况
    print("\n🙋 测试 2:查订单 (应该触发工具)")
    msg2 = HumanMessage(content="帮我查一下我的订单,单号是 ORD123")
    # TODO: 同样使用 llm_with_tools 调用 invoke
    res2 = None # TODO: 调用 invoke 传入 [msg2]
    
    print(f"🤖 模型回复文本 (通常为空,因为它的主要决定是去调工具): {res2.content}")
    print(f"🔧 工具调用记录: {res2.tool_calls}")
    
    # 🚨 注意:这里大模型只是“决定”要调用工具,并给出了要传给工具的参数 (order_id="ORD123")。
    # 实际上,在这一步工具并没有被真正执行(你看不到上面的 [系统日志] 打印出来)。
    # 真正的 Agent 框架会自动帮你执行工具,并把结果再塞回给大模型。
    # 今天我们只学大模型是如何“决定”调用的。

if __name__ == "__main__":
    main()

踩坑

恭喜你踩到了大模型应用开发中最常见的“模型拒绝使用工具”的问题。

其实你的代码逻辑完全没有错,工具也成功绑定了。之所以会出现 工具调用记录: [],并且大模型回复你“我目前无法直接查询订单状态...”,是因为大模型自身的安全机制/人设在作祟

📖 为什么会这样?(原理解析)

DeepSeek(包括 ChatGPT 等模型)在出厂时,被训练成了“诚实且守规矩”的 AI。当它听到“帮我查订单”时,它的第一反应是:“我只是个语言模型,我没有查你们公司数据库的权限,所以我不能骗用户,我得告诉他我查不了”。
虽然我们给它塞了工具,但如果我们的 Prompt(提示词)不够强硬,它还是会习惯性地拒绝。

🛠️ 怎么解决?(加系统提示词)

我们需要给它加上一段系统消息(SystemMessage),强行给它洗脑:“你现在不是普通的聊天机器人了,你有工具!你必须用工具!”

我已经帮你修改了代码 [05_tools_basic.py](file:///e:/file/大模型技术学习/03-LangChain/05_tools_basic.py#L58-L62),主要是增加了这一段:

    # 增加一个 SystemMessage 明确告诉它可以使用工具
    from langchain_core.messages import SystemMessage
    messages2 = [
        SystemMessage(content="你是一个得力的助手。当用户提出需要查询数据等请求时,请务必使用你手头的工具来完成,不要自己回答无法查询。"),
        HumanMessage(content="帮我查一下我的订单,单号是 ORD123")
    ]
    res2 = llm_with_tools.invoke(messages2)

你可以重新在终端运行一次 python 05_tools_basic.py 试试看!

这次你应该能看到类似这样的输出:

🔧 工具调用记录: [{'name': 'check_order_status', 'args': {'order_id': 'ORD123'}, 'id': '...', 'type': 'tool_call'}]

理解了吗?在真正做 Agent 开发时,我们通常都会在开头写一段很长的 System Prompt,告诉大模型它的身份、它能用什么工具、以及遇到不知道的问题该怎么办。

你先跑一下看看能不能拿到有内容的 tool_calls!如果成功了,我们就可以开始终极挑战最终的大项目啦我们就可以开始终极实战项目了!

实战项目:带记忆 + 工具调用的智能客服 Bot

哇!你真的太棒了!遇到问题能主动思考,而且你问到了最核心的关键点:“是不是要用 Agent 框架才能让工具调用结果再返回给大模型继续生成回答?”

答案是:完全正确! 💯

在第五课里,我们只学了“大脑如何下达指令(生成 JSON)”,但要让手脚真正动起来,并把看到的东西反馈给大脑,我们需要一个“循环控制系统”。这就是 Agent 框架干的活!

既然你希望多写代码挑战一下,我们就直接进入激动人心的最终实战项目!


🗺️ 下一步计划:使用 LangGraph 搭建真正的 Agent

在 2026 年的 LangChain 生态中,构建 Agent 的最强利器是官方推出的 LangGraph
如果你觉得 LCEL 是一条单向的流水线(从左到右跑完就结束),那么 LangGraph 就是一个可以画圈圈的流程图

它的核心逻辑就是你刚才说的:

  1. 用户提问 \(\rightarrow\) 大模型。
  2. 大模型说:“我要用工具”。
  3. 走到“工具节点”,真正执行 Python 代码
  4. (最关键的一步)将执行结果塞回给大模型,大模型根据结果生成人类看得懂的话。
  5. 返回给用户。

📖 概念小讲堂:LangGraph 的核心组件

  1. 状态 (State):就像大家一起编辑的一个云文档(在代码里是一个包含了 messages 列表的字典)。每个人(节点)都可以往里面加东西。
  2. 节点 (Nodes):干活的工人。我们今天主要有两个工人:
    • agent 节点:专门调用大模型。
    • tools 节点:专门执行工具(LangGraph 甚至贴心地提供了一个预置的 ToolNode,连代码都不用我们自己写,它会自动解析并执行!)。
  3. 边 (Edges):决定工人之间的流转顺序。特别是条件边 (Conditional Edges),它可以根据大模型有没有要求用工具,来决定是走到 tools 节点,还是直接走到终点(END)。

💻 你的终极实战挖空练习!

我已经为你准备了最后一份“大Boss”代码:[06_final_agent.py](file:///e:/file/大模型技术学习/03-LangChain/06_final_agent.py)。

请打开它。由于我们要用到 LangGraph,请先在终端安装它

pip install langgraph

然后,这可能是你目前面临的最具挑战性的填空题:

  1. 找到所有的 # TODO
  2. 绑定工具 llm_with_tools
  3. 实例化 ToolNode(tools)
  4. call_model 中调用绑定了工具的 llm_with_tools,传入拼接好的 messages
  5. 最关键的编排逻辑:用 workflow.add_conditional_edgesworkflow.add_edgeagenttools 这两个节点连成一个带循环的图!
  6. 在底部的 while 循环中,调用编译好的图 app.invoke()

完成之后,在终端运行:

python 06_final_agent.py

你会进入一个交互式的聊天界面。你可以先跟它闲聊,然后问它“帮我查一下订单 ORD123”,看看是不是真的打印出了 [系统日志],并且大模型根据“顺丰快递”给你生成了非常自然的回答!

放手去写吧!这一关如果能打通,你就真正掌握了目前最前沿的 Agent 开发技术!遇到不会填的随时发给我!随时求助我!

点击查看代码
import os
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import SystemMessage, HumanMessage

# 导入 LangGraph 相关组件
from langgraph.graph import MessagesState, StateGraph, START, END
from langgraph.prebuilt import ToolNode

def main():
    """
    实战项目:用 LangGraph 搭建一个带工具调用的智能客服 Bot
    目标:真正执行工具,并将结果返回给大模型继续生成回答。
    """
    print("🚀 正在初始化智能客服 Agent...")

    # 1. 定义我们的工具 (和第五课一样)
    @tool
    def check_order_status(order_id: str) -> str:
        """
        当你需要查询用户的订单状态时,调用这个工具。
        参数 order_id 是用户提供的订单号,比如 'ORD123'。
        """
        print(f"\n[系统日志] 🔔 正在执行工具: 查询订单号 {order_id}")
        mock_db = {
            "ORD123": "已发货,顺丰快递,预计明天送达",
            "ORD456": "待付款,请提醒用户尽快支付"
        }
        return mock_db.get(order_id, "找不到该订单,请核对订单号")

    # TODO: 把工具放进一个列表里
    tools = [check_order_status]

    # 2. 初始化大模型并绑定工具
    llm = ChatOpenAI(
        api_key=os.getenv("DEEPSEEK_API_KEY"),
        model="deepseek-chat",
        base_url="https://api.deepseek.com",
        max_tokens=200
    )
    # TODO: 绑定工具
    llm_with_tools = None # TODO: 绑定工具

    # 3. 定义图的节点函数 (Node Functions)

    # 节点 A: 调用大模型
    def call_model(state: MessagesState):
        """
        这个节点负责把当前所有的消息历史扔给大模型,获取回复。
        """
        # 我们在这里给大模型注入一个强硬的系统人设
        sys_msg = SystemMessage(content="你是一个得力的客服助手。当用户提出查询请求时,务必使用工具,不要自己瞎编。")
        messages = [sys_msg] + state["messages"]
        
        # TODO: 调用绑定了工具的 llm_with_tools
        response = None # TODO: 调用大模型
        return {"messages": [response]}

    # 节点 B: 工具执行节点 (ToolNode)
    # LangGraph 提供了一个预置的节点 ToolNode,它会自动从 state 中提取工具调用请求并执行!
    # TODO: 实例化 ToolNode,传入你的 tools 列表
    tool_node = None # TODO: 实例化 ToolNode

    # 4. 定义图的路由条件 (Conditional Edge)
    def should_continue(state: MessagesState):
        """
        这个函数决定大模型生成完后,下一步去哪里。
        """
        last_message = state["messages"][-1]
        # 如果大模型决定调用工具,就去 'tools' 节点
        if last_message.tool_calls:
            return "tools"
        # 否则(也就是生成了普通文字回复),就结束
        return END

    # 5. 构建状态图 (StateGraph)
    print("🕸️ 正在编织 Agent 逻辑图...")
    workflow = StateGraph(MessagesState)

    # TODO: 将刚刚定义的节点添加到图中
    # 提示: workflow.add_node("节点名称", 节点函数)

    # TODO: 设置起点
    workflow.add_edge(START, "agent")

    # TODO: 添加条件边 (根据 should_continue 的返回值决定下一步)
    # 提示: workflow.add_conditional_edges("agent", should_continue)
    # TODO: 填入条件边代码

    # TODO: 添加从 tools 节点回到 agent 节点的边 (这样工具执行完,结果才能给回大模型!)
    # 提示: workflow.add_edge("tools", "agent")
    # TODO: 填入返回边代码

    # 编译图,生成可以执行的 Agent
    app = workflow.compile()

    # 6. 开始多轮对话测试!
    print("\n✅ 客服 Bot 已上线!输入 'quit' 或 'exit' 退出。")
    
    # 用一个简单的列表来模拟记忆
    chat_history = []
    
    while True:
        user_input = input("\n🙋 用户: ")
        if user_input.lower() in ['quit', 'exit']:
            break
            
        # 把用户的新话加入记忆
        chat_history.append(HumanMessage(content=user_input))
        
        # 运行图!
        print("🤔 Bot 思考中...")
        # TODO: 调用 app.invoke,传入包含历史记录的字典
        # 提示: {"messages": chat_history}
        final_state = None # TODO: 运行图
        
        # 获取最新的消息(最后一条就是 Bot 结合了工具结果给出的最终回答)
        final_message = final_state["messages"][-1]
        print(f"🤖 Bot: {final_message.content}")
        
        # 更新记忆,把这次图运行产生的所有新消息(包括AI回复、工具调用记录、最终回复)都保存下来
        # 这样下一次提问时,Bot 才知道之前的上下文
        chat_history = final_state["messages"]

if __name__ == "__main__":
    main()
posted @ 2026-03-23 19:56  鼓舞人心的杰克森  阅读(0)  评论(0)    收藏  举报