Featured image of post 基于Langchain搭建 Agent 流程解析与实战

基于Langchain搭建 Agent 流程解析与实战

基于Langchain搭建 Agent 流程解析与实战

欢迎来到这一期博客!最近我对agent比较感兴趣,想搭建一个全能的 AI Agent。不仅限于聊天,我希望它能帮我干活儿,比如查天气、订机票、分析数据……总之,就是一个真正的“AI助理”。

要实现这个目标,光靠一个LLM是不行的,它还得有“手”和“脚”——也就是连接现实世界、执行具体任务的能力。

在看完一系列教程之后之后,我决定继续使用LangChain 框架。所以,这篇博客就是我的学习笔记,记录一下如何使用 LangChain 来搭建一个初级的 Agent,以及它背后最核心的“工具调用”(Function Calling)逻辑。

LangChain的优势

在“万物皆可 LLM”的时代,我们为什么还需要一个像 LangChain 这样的框架?因为它精准地解决了大模型应用开发中的两大核心痛点:

  1. 解决了“整合”的痛点: 当前,大模型“百花齐放”(OpenAI, Gemini, Claude, Llama…),外部工具(API、数据库、搜索引擎)也五花八门。如果没有统一的框架,开发者每次想要切换模型或增加一个工具,都不得不重写大量的“胶水代码”。LangChain 提供了标准化的接口抽象,让我们摆脱了这种重复劳动。

  2. 提供了“组件化”的便利: LangChain 提供了大量标准化的组件(Components),如 Prompt 模板、模型 I/O、输出解析器(Output Parsers)、工具(Tools)等。这使得开发过程像“搭积木”一样,开发者可以专注于业务逻辑的编排,快速组合出一个完整的应用,极大提升了研发效率。

如何使用 LangChain 搭建 Agent

1. 到底什么是 Agent?

首先,Agent 不是 LLM,LLM 只是 Agent 的“大脑”。

一个 Agent 是一个能够自主决策和行动的系统。它会观察环境、思考下一步该做什么,并利用可用的工具去执行动作,直到完成目标。

目前,驱动 Agent 的核心逻辑大多基于 ReAct 模式,这是“Reason + Act”的缩写。

简单来说,ReAct 的流程就是:

  1. Reason (思考):Agent(背后是 LLM)根据你的指令和当前情况,思考:“我该怎么做?我需要什么信息?我该用哪个工具?”

  2. Act (行动):Agent 决定调用一个工具(比如搜索工具、计算器工具)。

  3. Observation (观察):Agent 拿到工具返回的结果。

  4. Repeat (循环):Agent 带着新拿到的结果,回到第1步,再次思考,直到任务完成。

2. 构建 Agent 的四个核心步骤

要让一个 Agent 跑起来,至少需要三个核心组件:

  • 模型 (Model):Agent 的“大脑”,负责思考(Reason)。

  • 工具 (Tools):Agent 的“手脚”,负责执行(Act)。

  • 代理执行器 (Agent Executor):负责管理 ReAct 循环,把“大脑”和“手脚”调度起来。

下面,介绍一下具体的agent构建流程。


Step 1: 自定义函数,也就是可用的工具 (Define Custom Functions)

Agent 的能力上限,取决于你给它多少好用的工具。LLM 本身不会看天气、不会执行代码,这些都需要我们通过“工具”赋予它。

在 LangChain 中,定义一个工具(Tool)非常简单,本质上就是封装一个 Python 函数。但有几个关键点必须包含:

  1. 清晰的函数定义:包括函数名和必要的传入参数(带类型注解)。

  2. 详细的用途描述:这是最关键的一步!你需要用自然语言(通常在 docstring 里)清楚地描述这个工具是干什么用的什么时候该用它参数代表什么。Agent 的“大脑”(LLM)会_阅读_这个描述,来决定是否以及如何调用这个工具。

  3. 具体的运行流程和返回结果:函数体本身。

举个例子,我们用 LangChain 提供的 @tool 装饰器来快速定义一个“查询天气”的工具:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain_core.tools import tool

@tool
def get_current_weather(location: str) -> str:
    """
    当需要查询某个地点的当前天气时,请使用此工具。
    参数 'location' 必须是一个城市名称,例如 '北京' 或 'San Francisco'。
    """
    # 这里的代码会真实地去调用一个天气 API
    if "北京" in location:
        return "北京今天晴,25摄氏度。"
    elif "东京" in location:
        return "东京现在在下雨,18摄氏度。"
    else:
        return f"抱歉,我查询不到 {location} 的天气。"

# 你可以定义任意多个工具
@tool
def calculator(a: int, b: int) -> int:
    """
    当需要进行简单的加法计算时,使用此工具。
    """
    return a + b

Step 2: 创建 Tool_List 列表,注册工具

你已经打造好了工具(calculator)(get_current_weather),现在得把它们放进一个“工具箱”,交给 Agent。

这一步很简单,就是把所有创建好的工具函数放进一个列表(List)里:

1
tools = [get_current_weather, calculator]

这个 tools 列表会被传递给 Agent。Agent 在思考时,就会“浏览”这个列表里的所有工具描述,智能决策

Step 3: 使用 LCEL 构建 Agent 的核心推理组件

这一步是 Agent 的关键所在。我们要把模型(LLM)工具(Tools)、**提示词(Prompt)解析器(Parser)**组装起来,构建一个“推理链”。

在 LangChain 中,我们使用 LCEL (LangChain Expression Language) 这种“管道符 |”风格来组装链。

虽然你可以手动组装(如下面注释掉的 LCEL 结构),但 LangChain 提供了一个非常方便的快捷方式:create_tool_calling_agent

它封装了以下所有逻辑:

  • prompt: 一个内置的、高质量的提示词模板(当然你也可以自定义),它会告诉 LLM 如何扮演一个 Agent 并如何使用工具。

  • llm.bind_tools(tools): 这是关键一步,它把你的工具列表“绑定”到 LLM 上,让 LLM 知道这些工具的存在,并能在它的输出中生成特定的“工具调用”请求。

  • OutputParser: 一个输出解析器,用于解析 LLM 返回的“思考过程”或“工具调用请求”。

Python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from langchain_openai import ChatOpenAI
from langchain.agents import create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate

# 假设你已经设置好了 OpenAI 的 API Key
# 1. 选择一个支持工具调用的模型
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# 2. 定义一个基础的 Prompt 模板 (这里用一个极简的,实际中 create_tool_calling_agent 会用更复杂的)
# 注意:必须包含 'agent_scratchpad' 和 'input' 变量
prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "你是一个强大的助手,你可以使用你拥有的工具来回答问题。"),
        ("user", "{input}"),
        ("placeholder", "{agent_scratchpad}"), # 这是 Agent 思考和行动的“草稿纸”
    ]
)

# 3. 使用快捷函数创建 Agent 

agent_chain = create_tool_calling_agent(llm, tools, prompt)
# (它等价于: prompt | llm.bind_tools(tools) | OpenAIToolCallingOutputParser())

现在,agent_chain 就成了 Agent 的核心推理组件。你给它 inputagent_scratchpad(历史记录),它就会返回下一步的决策(是回答用户,还是调用工具)。

Step 4: 创建 AgentExecutor,管理循环和执行

AgentExecutor 负责管理 ReAct 循环:

  1. 它把用户的输入(Input)丢给 agent_chain

  2. agent_chain返回一个决策(比如:Action: call get_current_weather(location='北京'))。

  3. AgentExecutor捕获这个决策,暂停 LLM,然后真正地去 Python 环境中执行 get_current_weather('北京') 函数。

  4. 执行器拿到返回结果(Observation: "北京今天晴,25摄氏度。")。

  5. 它把这个结果塞进 agent_scratchpad,再次调用 agent_chain

  6. agent_chain看到新的观察结果,再次思考:“OK,拿到天气了,现在我可以回答用户了。”

  7. agent_chain 返回最终答案。

  8. AgentExecutor 看到是大脑的最终答案(Finish),于是将结果返回给用户。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from langchain.agents import AgentExecutor

# 4. 创建 Agent 执行器
agent_executor = AgentExecutor(agent=agent_chain, tools=tools, verbose=True)
# verbose=True 让我们能看到 Agent 完整的“思考-行动”链条,强烈推荐在调试时开启

# 案例1: 需要使用工具
response = agent_executor.invoke({"input": "北京今天天气怎么样?"})
print(response["output"])

# 案例2: 结合多个工具
response = agent_executor.invoke({"input": "东京今天多少度?然后计算一下 123 + 456 等于多少?"})
print(response["output"])

当你运行 agent_executor.invoke 时,如果你设置了 verbose=True,你将在控制台看到 Agent 完整的 ReAct 思考过程。

就这样,我们从零到一,使用 LangChain 成功构建并运行了一个具备“思考”和“行动”能力的智能 Agent。

3. Agent 运行的主要流程

在上一节,我们创建并运行了 AgentExecutor。我们只管 invoke,它就自动完成了一系列复杂的交互。这种便捷的背后,是一套被精确定义的、基于“工具调用”(Tool Calling)的循环机制。

AgentExecutor 扮演的,是一个精密的**调度器(Orchestrator)**角色。它负责管理整个工作流,确保 LLM 的决策 Reason 和工具的 Act 能够正确衔接。

我们来拆解一下这个 AgentExecutor 运行时的 ReAct Loop ,看看它在技术上是如何实现的。


步骤一:用户请求与首次模型调用

  1. 用户发起请求:用户输入 “北京今天天气怎么样?”

  2. AgentExecutor 接收任务:它获取这个 input

  3. 调用核心推理链AgentExecutor 将用户的输入,连同我们定义的 tools 列表,一起传递给 agent_chain

  4. agent_chain 内部prompt 模板被填充,然后 LLM 被调用。最关键的是,在这次 API 调用中(例如调用 OpenAI API),tools 参数被传入。这等于告诉 LLM:“你具备了调用这些工具的能力。”


步骤二:模型决策(是否使用工具?)

LLM 在分析了用户的输入和可用的工具描述后,会返回一个决策。这里会产生两个分支:

分支 A:不需要工具(直接回答)

  • 场景:例如用户输入“给我讲个笑话”。

  • LLM 决策:LLM 判断仅凭自身知识库即可回答。

  • LLM 输出:模型返回一个标准的 Assistant 消息。其 content 字段包含笑话内容,而 tool_calls 字段为 None

  • AgentExecutor 动作AgentExecutor 检查到这是一个最终答案(Finish 状态),于是将 content 作为 output 返回给用户。流程结束。

分支 B:需要工具(请求调用)

  • 场景:例如用户输入“北京今天天气?”

  • LLM 决策:LLM 识别到这需要外部信息,并匹配到了 get_current_weather 工具。

  • LLM 输出:这是关键。模型返回的 Assistant 消息中,content 字段是 None。取而代之的是,tool_calls 字段会包含一个(或多个)结构化对象,这本质上是一个函数调用请求

  • “函数调用请求”的结构

    • function.name: 要调用的函数名,如 get_current_weather

    • function.arguments: 一个包含参数的 JSON 字符串,如 {"location": "北京"}

    • id: 一个唯一的调用 ID,如 tool_call_id_abc123。这个 ID 用于后续追踪。


步骤三:工具的本地执行

AgentExecutor 作为调度器,当它收到 LLM 返回的 tool_calls 请求时,它知道现在轮到它在本地执行代码了。

  1. 解析请求AgentExecutor 遍历 tool_calls 列表。

  2. 匹配与执行:它根据请求中的 function.nameget_current_weather),从我们提供的 tools 列表中找到对应的 Python 函数。

  3. 传递参数:它解析 function.arguments{"location": "北京"}),并将这些参数传递给该 Python 函数并执行它。

  4. 获取结果:函数在本地运行,并返回一个结果,例如:"北京今天晴,25摄氏度。"


步骤四:结果回传与第二次模型调用

拿到工具的执行结果后,AgentExecutor 并不会直接将其返回给用户。它需要将这个新信息反馈给 LLM,让 LLM 来决定下一步做什么(是继续调用其他工具,还是总结答案)。

  1. 封装“工具消息” (Tool Message)AgentExecutor 将刚才的函数执行结果,封装成一个符合规范的新消息。这个消息必须包含:

    • role: 'tool' (表明这是一个工具的返回结果)

    • content: 函数的返回值 (e.g., "北京今天晴,25摄氏度。")

    • tool_call_id: 必须是步骤二中 LLM 返回的那个 idtool_call_id_abc123)。这至关重要,它让 LLM 能够将结果与它之前的调用请求对应起来。

  2. 追加历史并再次调用AgentExecutor 将这个 Tool Message 添加到“草稿纸”(agent_scratchpad)中,这维护了完整的上下文。 然后,它再次调用 agent_chain,将包含原始问题、LLM 首次调用请求、以及 Tool Message全部历史消息再次发送给 LLM。


步骤五:生成最终答案

  1. LLM 综合信息: LLM 这一次接收到了所有上下文:用户的原始问题(“北京天气如何?”)和工具的执行结果(“北京…25度”)。

  2. 生成最终答案: 基于这些完整的信息,LLM 会用自然语言生成一个对用户友好的、综合性的回答。这次它返回的 Assistant 消息中,content 字段将包含这个最终答案(例如:“北京今天天气晴朗,气温是25摄氏度。”),而 tool_calls 字段为 None

  3. AgentExecutor 输出AgentExecutor 收到这个响应,判断这是一个 Finish 状态,于是将 content 作为最终 output 返回给用户。

至此,一次完整的 Tool Calling 循环结束。AgentExecutor 通过这个严谨的“调用-执行-回传-再调用”流程,实现了 LLM 与外部世界的可靠交互。

另一种方法:基于原生工具调用

除了上面介绍的基于 ReAct 的推理过程,其实还有另一种更先进的agent模式,不过本博客中的实战项目并没有使用这种方式,所以先暂时介绍一下:

对于 GPT-4o、Gemini 等支持原生“Tool Calling”功能的先进 LLM,决策更高效:

  • 结构化绑定: 工具通过 Python 函数签名或 JSON Schema 的形式,直接“绑定”到 LLM 上。

  • 模型原生判断: LLM 利用其内置能力,直接根据用户意图,生成一个结构化的 JSON 对象,清晰指明要调用的工具名称和参数。

  • 精确执行: 这消除了文本解析的错误风险,提高了决策的准确性和速度。

五、项目实战:构建一个“网站总结 PDF 报告” Agent

我们现在就把前面学到的知识组合起来,构建一个有实际价值的自动化流程。

1. 项目的意义

这个项目的目标很明确:你给它一个 URL,它自动访问该网站,阅读并总结内容,最后生成一份排版精美的 PDF 总结报告。

这绝对是一个“懒人福音”。想象一下,你需要调研 10 个竞品网站,手动操作就是:打开、阅读、复制、粘贴、排版… 而现在,你只需要一个 URL 列表。

并且这个项目也有很大的 学习价值:这个项目能帮我们完美串联起前面介绍的 Agent 的核心概念。

2. 总体构建思路

我们的策略不是构建一个庞大的“超级 Agent”。我们将遵循“Unix 哲学”:创建小而精的工具,然后用“链”将它们连接起来。

本实战中的工具 (Tools):

  1. 工具 A (summarize_website):一个“网站总结工具”。它接收一个 url,然后打开一个浏览器(我们使用 Playwright)来访问它(这能访问那些 JS 渲染的“现代”网页),提取文本,并调用 LLM 对文本进行总结。最后,它返回总结好的字符串

  2. 工具 B (generate_pdf):一个“PDF 生成工具”。它接收一段文本内容 (content),使用 reportlab 库将其排版(包括处理中文字体),并保存为一个带时间戳的 PDF 文件。最后,它返回生成的文件路径字符串

本项目中构建的Chains:

我们将构建两条Chains来对比效果:

  1. 简单功能Chainssummarize_website | generate_pdf

    • 流程:网站总结工具的输出(总结文本)被直接用作 PDF 生成工具的输入。

    • 优点:简单、快速。

    • 缺点:总结的原始文本可能格式比较乱,直接塞进 PDF 不够美观。

  2. 优化的Chainssummarize_website | LLM 优化 | generate_pdf

    • 流程:在总结和生成 PDF 之间,我们插入一个额外的 LLM 调用。这个 LLM 的任务是把原始总结润色和排版成更适合报告的格式(比如加上标题、要点)。

    • 优点:生成的 PDF 报告质量更高,更专业。

3. 核心代码解析

这里仅有代码的关键片段用于介绍,完整代码我后续会放到 文末GitHub 地址中供大家参考。

第 1 部分:summarize_website

这是我们第一个工具,也是最“秀”的一个。我们用 @tool 装饰器来定义它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from langchain_community.agent_toolkits import PlayWrightBrowserToolkit
from langchain_community.tools.playwright.utils import create_sync_playwright_browser
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.tools import tool
from langchain import hub

@tool
def summarize_website(url: str) -> str:
    """访问指定网站并返回内容总结"""
    try:
        # 1. 创建一个真实的浏览器实例
        sync_browser = create_sync_playwright_browser()
        toolkit = PlayWrightBrowserToolkit.from_browser(sync_browser=sync_browser)
        tools = toolkit.get_tools() # 这会返回 [navigate, extract_text, ...] 等工具

        # 2. 在工具内部,我们再创建一个Agent,专门负责浏览
        model = init_chat_model("deepseek-chat", model_provider="deepseek")
        prompt = hub.pull("hwchase17/openai-tools-agent") # 使用 LangChain Hub 上的标准 prompt
        agent = create_openai_tools_agent(model, tools, prompt)
        
        # 3. 这个Agent的执行器
        agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False)
        
        # 4. 给这个Agent下达一个指令
        command = {
            "input": f"访问这个网站 {url} 并帮我详细总结一下这个网站的内容..."
        }
        
        result = agent_executor.invoke(command)
        return result.get("output", "无法获取网站内容总结")
        
    except Exception as e:
        return f"网站访问失败: {str(e)}"

介绍:我们没有自己去写复杂的 Playwright 爬虫代码。我们利用了 LangChain 提供的 PlayWrightBrowserToolkit,它已经把“访问页面”、“提取文本”等操作封装成了 Agent 可用的 tools

我们在这个 @tool 函数内部,又创建并运行了一个 Agent。这就是或“Agent-as-a-Tool”模式:从我们主流程的视角看,summarize_website 只是一个工具;但在这个工具内部,其实是一个完整的 Agent 。

第 2 部分:generate_pdf

这个工具相对简单,它就是一个 Python 函数,负责调用 reportlab 库生成 PDF。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os

@tool  
def generate_pdf(content: str) -> str:
    """将文本内容生成为PDF文件"""
    try:
        filename = f"website_summary_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
        doc = SimpleDocTemplate(filename, pagesize=A4)
        
        # ... (省略了大量注册中文字体和定义样式的代码) ...
        # 关键:尝试注册中文字体,否则PDF中的中文会是“天书”
        try:
            font_path = "C:/Windows/Fonts/simhei.ttf" # 示例
            if os.path.exists(font_path):
                pdfmetrics.registerFont(TTFont('ChineseFont', font_path))
                # ... 定义使用 'ChineseFont' 的样式 ...
        except:
            print("⚠️ 未找到中文字体,使用默认字体")
            
        # ... (省略了构建 story[] 列表的代码) ...
        
        # 将内容段落添加到PDF
        paragraphs = content.split('\n')
        for para in paragraphs:
            if para.strip():
                story.append(Paragraph(para.strip(), content_style))
                story.append(Spacer(1, 8))
                
        doc.build(story)
        abs_path = os.path.abspath(filename)
        return f"PDF文件已成功生成: {abs_path}"
        
    except Exception as e:
        return f"PDF生成失败: {str(e)}"

介绍:在 Agent 开发中,经常需要写这类“胶水”工具,把 Python 的生态(如 PDF、Excel、API 调用)封装起来,供 LLM 使用。处理中文字体是这类任务中一个非常经典的“坑”。

第 3 部分:LCEL 组装chains

这是最能体现 LangChain 特点的地方。我们用 | 管道符来连接我们的各个模块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# --- 简单chains ---
# summarize_website 的输出 (str),直接流入 generate_pdf 的输入 (content)
simple_chain = summarize_website | generate_pdf


# --- 优化chains ---
optimization_prompt = ChatPromptTemplate.from_template(
    "请优化以下网站总结内容,使其更适合PDF报告格式:\n\n{summary}\n\n优化后的内容:"
)
model = init_chat_model("deepseek-chat", model_provider="deepseek")

optimized_chain = (
    summarize_website  
    | (lambda summary: {"summary": summary}) # 1. 将字符串转为字典以匹配 prompt
    | optimization_prompt                     # 2. 填入优化 prompt
    | model                                   # 3. LLM(DeepSeek)进行润色
    | StrOutputParser()                       # 4. 从 LLM 的回复中提取字符串
    | generate_pdf                            # 5. 将优化后的字符串送入PDF工具
)

4. 运行结果与分析

当我们使用一个 GitHub README 的 URL (https://github.com/fufankeji/MateGen/blob/main/README_zh.md) 来运行这个项目时:

AgentExecutor 开始工作。

如果选择“简单chains”:

  1. 控制台打印 📝 步骤1: 网站总结...

  2. summarize_website 工具被调用。其内部的“浏览器 Agent”开始访问 URL、提取文本、并调用 DeepSeek 总结。

  3. 它返回了总结的原始文本。

  4. 控制台打印 📄 步骤2: 生成PDF...

  5. generate_pdf 工具被调用,它把这段原始文本写进了 PDF。

  6. 结果:我们得到了一个 普通的总结PDF。

如果选择 “优化chains"

  1. 控制台打印 📝 步骤1: 网站总结...(同上)

  2. summarize_website 返回原始总结。

  3. 控制台打印 🎨 步骤2: 内容优化...

  4. LCEL 链自动将原始总结(通过 lambdaoptimization_prompt)发送给 DeepSeek 模型。

  5. DeepSeek 扮演“编辑”角色,返回了结构化、排版优美的文本(例如:“\n## 1. 主要功能\n- 功能A…\n- 功能B…\n## 2. 特点\n…”)。

  6. 控制台打印 📄 步骤3: 生成PDF...

  7. generate_pdf 工具接收到这个优化后的文本,并生成 PDF。

  8. 结果:我们得到了一个排版清晰、带有标题和分点的专业 PDF 报告。

如下图所示

###项目小结

这次关于LangChain和agent的学习与实战 ,让我彻底搞懂了 Agent 这个是怎么指挥 LLM和 Tools干活的。通过 ReAct 和 Tool Calling 机制,设计出严谨的工作流。从理论到实战,当我们把浏览器 Agent、LLM 润色和 PDF 工具用 LCEL 串联起来,看着 URL 自动变成一份精美的网页 PDF 报告时,非常直观地感受到了agent的巨大潜力。之后也会继续学习agent的相关内容并继续发布博客的。

( ๑╹ ꇴ╹) グッ!

💻项目源码

💻 项目源码可以访问我的Github:Langchain_Agent

使用 Hugo 构建
主题 StackJimmy 设计