大模型学习20 大模型学习 - LangChain3 - Agents

1 Agent

"Agents"(在AI语境下通常指 AI Agent,即"人工智能体")并非一个单一的技术,而是一种以目标为导向、能够自主规划并执行多步骤任务的AI系统。它标志着AI从"被动回答问题的工具"向"主动完成任务的伙伴"的范式转变。

1.1🧠 核心能力:它如何"自主"工作?

一个现代AI Agent的"自主性",建立在一套协同工作的核心能力之上,通常被概括为"感知-决策-行动-记忆"的闭环。

推理与规划:面对一个模糊的最终目标(如"策划一场产品发布会"),Agent能将其拆解为市场调研、场地预订、物料设计等一系列有序的子任务,并制定执行计划。

工具调用:这是Agent与真实世界交互的"手脚"。它能调用API、数据库、软件等外部工具来执行具体操作,比如查询库存、发送邮件或预订会议室。

记忆与情境:Agent拥有短期记忆(记住当前任务的中间结果和对话上下文)和长期记忆(存储用户偏好和历史经验),确保行动连贯,并能从过往中学习优化。

迭代执行:Agent在一个 "推理→行动→观察结果→再推理"的循环中工作。它会根据每一步的实际反馈动态调整后续步骤,而不是死板地执行预设脚本,直到目标达成。

1.2 关键区别:Agent vs. 聊天机器人

理解Agent的关键,在于看清它与我们熟悉的聊天机器人(如传统ChatGPT)的本质不同。

维度 传统聊天机器人 AI Agent
交互模式 被动响应:根据单次指令生成回复,需要用户一步步引导。 主动执行:接收一个最终目标,自主完成全过程,无需逐步干预。
任务范围 单点问答:擅长回答孤立的问题。 多步骤工作流:能处理"查询余额→发现异常→提醒用户"这样的连贯任务链。
工作逻辑 遵循预设的对话路径或规则。 基于AI进行动态规划与决策,能处理开放式请求和突发情况。
类比 像需要详细指导的实习生。 像能独当一面的专业负责人,你只需告知目标。

通用人工智能(AGI)将是 AI 的终极形态,几乎已成为业界共识。同样,构建 Agent则是 AI 工程应用当下的"终极形态"。

1.3 它被用来做什么?------从"对话"到"实干"

AI Agent的价值在于闭环执行能力,即不只是"说",而是能"做完"一件事。其应用正从实验性工具快速渗透到核心工作流中。

主要应用领域包括:

  • 企业与生产力:自动处理客服工单、IT运维、代码生成。例如,在软件研发中,Agent辅助生成的代码占比可超过30%;在客服场景,标准咨询的自动处理率能超过70%。

  • 个人事务管理:作为个人助理,自动规划行程、预订机票酒店、管理财务账单,甚至发现并提醒用户遗忘的自动续费订阅。

  • 垂直行业深化:在工业制造领域,用于预测性维护和生成式设计;在金融合规领域,辅助进行材料预审和风险监控。

  • 多智能体协作(MAS):面对复杂任务,由多个具备不同专长的Agent分工协作,例如一个负责市场分析,另一个负责撰写报告,共同完成一个商业项目。

1.4 Agent

将 AI 和人类协作的程度类比自动驾驶的不同阶段:

语言模型本身无法采取行动------它们只是输出文本。LangChain 的一个重要功能是创建Agent。Agent 是一种使用 LLM 作为推理引擎的系统,它决定要采取哪些行动以及这些行动的输入应该是什么。这些行动的结果可以反馈给 Agent,由 Agent 决定是否需要采取更多行动,或者是否可以完成。

与传统的固定流程链不同,Agent 具备一定的自主决策能力,更适合处理开放式、多步骤的问题。它可以拆解任务,根据任务动态决定调用哪些工具,并利用中间结果推进任务。

Agent 的核心能力/组件:

  • 大模型(LLM):作为大脑,提供推理、规划和知识理解能力。
  • 记忆(Memory):具备短期记忆和长期记忆,支持快速知识检索。
  • 工具(Tools):调用外部工具(如API、数据库)的执行单元。
  • 规划(Planning):任务分解、反思与自省框架实现复杂任务处理。
  • 行动(Action):实际执行决策的能力。
  • 协作:通过与其他 Agent 交互合作,完成更复杂的任务目标。

2 Agent 构建

LangChain 当中提供了一个功能函数:create_agent,可以快速构建一个Agent。create_agent函数所需要传递的参数如下所示:

名称 是否必传 描述
model 是 模型实例
system_prompt 否 系统提示词
tools 否 工具列表
response_format 否 结构化输出的schema信息
checkpointer 否 用以保存状态的实例,可用以实现短期记忆
middleware 否 要应用于代理的一系列中间件实例。中间件可以在各个阶段拦截并修改代理的行为

通过传递相关参数,即可构建一个复杂的Agent实例,接下来将详细讲解其中的tools, checkpointer和middleware参数。

3 Agent 添加本地工具

Agent 添加工具,本质上是使用大模型服务的 function call 能力,将所有的工具封装成 json schema 信息,在调用大模型时,传入所有的 json schema,大模型在回答时,能够给出所需要调用的参数名字和入参信息。

OpenAI中关于function call的文档链接:https://platform.openai.com/docs/guides/function-calling?api-mode=chat。

3.1 OpenAI SDK调用添加工具 schema

以下代码演示了如何通过 OpenAI 的原生 SDK 来进行工具调用:

python 复制代码
from openai import OpenAI
import json

# 初始化OpenAI客户端
client = OpenAI()

# ========== 1. 通过JSON结构定义工具(Function Calling) ==========
# tools数组:告诉大模型它可以调用哪些外部工具、工具是干嘛的、需要什么入参
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",       # 工具函数名称,模型要靠这个名字匹配本地函数
            "description": "Get today's weather for a location.", # 给模型看的工具描述,让模型知道什么时候调用它
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称, e.g. San Francisco",
                    },
                    "date" :{
                        "type":"string",
                        "description":"想要查询的天气的日期, e.g. 2023-12-25"
                    }
                },
                "required": ["city","date"], # 标记必填参数,模型必须输出这两个字段
                "additionalProperties": False, # 不允许模型额外生成不在定义里的参数
            },
            "strict": True, # 严格模式:强制输出JSON完全符合schema,减少格式错乱
        },
    },
]

# 本地实现的天气查询工具函数(真实场景这里对接天气API)
def get_weather(city,date):
    return f"{city} on {date} is cloudy with a chance of rain."

# 消息列表:整个对话上下文,会不断追加消息
messages = [
    {"role": "user", "content": "What is the weather like in 北京 on 2024-12-25?"}
]

# ========== 2. 第一次请求模型:带上工具定义,让模型判断是否需要调用工具 ==========
response = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,  # 传入工具清单
)

# 把模型返回的这条消息(tool_call)加入对话上下文,保证上下文连贯
messages.append(response.choices[0].message)

# 遍历模型发起的工具调用请求(一次返回可能多个tool_call)
for tool_call in response.choices[0].message.tool_calls or []:
    if tool_call.function.name == "get_weather":
        # ========== 3. 解析模型输出的参数,执行本地工具逻辑 ==========
        args = json.loads(tool_call.function.arguments) # 把模型输出的参数字符串转字典
        weather = get_weather(args["city"],args["date"])

        # ========== 4. 将工具返回结果包装成tool消息,追加进消息列表 ==========
        # role="tool" 是固定角色;tool_call_id用来和前面模型的调用请求一一对应
        messages.append(
            {
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps({"weather": weather}),
            }
        )

# ========== 第二次请求模型:此时上下文已经包含工具返回结果 ==========
# 模型拿到工具结果后,整理信息,生成自然语言最终回答
response = client.chat.completions.create(
    model="gpt-4.1",
    messages=messages,
    tools=tools,
)

# ========== 5. 输出模型整理后的最终自然语言回复 ==========
print(response.choices[0].message.content)

3.2 通过 LangChain 定义工具

-- 通过原生的SDK定义工具时需要使用 json 来描述工具,比较繁琐。langchain.tools模块下提供了 tool 装饰器,可以通过 在函数上使用 @tool 的方式,就可以将一个函数创建成工具。

-- 需要注意的是:定义函数时,需要通过使用 @tool 装饰器内传参:description 或者是定义函数的文档字符串的方式,来添加函数的解释说明。

-- 而对于参数的说明,没有对于参数描述的强制限制,不过也可以通过 pydantic 的 BaseModel 来定义一个入参的具体描述信息,传递到 tool 装饰内。

-- 通过 LangChain 定义好工具之后,调用 llm.bind_tools() 方法,得到一个新的 llm 对象,后续在调用新的 llm 对象时,就可以使用相关的工具

示例代码如下:

python 复制代码
from langchain.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage
import logging
# 开启DEBUG日志,可以看到LangChain内部详细执行过程
logging.basicConfig(level=logging.DEBUG)

# 可选:通过BaseModel详细定义工具的参数(Pydantic做参数校验、自动生成schema)
from pydantic import BaseModel, Field
class GetWeatherArgs(BaseModel):
    city: str = Field(description="城市名称")
    date: str = Field(description="日期,格式为YYYY-MM-DD")


@tool
def get_weather(city, date) -> str:
    """获取指定城市在指定日期的天气"""
    # 模拟天气接口返回,真实场景对接第三方天气API
    return f"{city} 在 {date} 天气多云,有下雨的可能性"


from langchain_openai import ChatOpenAI
# 初始化OpenAI大模型
llm = ChatOpenAI(model="gpt-4.1")

# 将工具绑定到LLM:告诉模型可用的工具、自动生成Function Calling的schema
llm = llm.bind_tools([get_weather])

# 消息列表:LangChain消息对象,区分HumanMessage/AIMessage/ToolMessage
message_list = [
    HumanMessage(content="北京2024-12-25的天气")
]

# 第一轮调用模型:模型判断需要调用工具,返回tool_calls,不直接给答案
res = llm.invoke(message_list)
message_list.append(res)

# 当前结果当中包含调用工具的出参
print('LLM的首次回复:', res)

# 解析模型输出的工具调用信息,手动执行工具
tool_call = res.tool_calls[0]
args = tool_call['args']       # 模型提取出来的入参 {"city":"北京","date":"2024-12-25"}
call_tool_res = get_weather.invoke(args) # 使用langchain tool自带的invoke方法执行函数
id = tool_call['id']           # 工具调用唯一id,用来关联本次tool call

# 构造ToolMessage,这是LangChain规定的工具返回消息类型
tool_message = ToolMessage(tool_call_id=id, content=call_tool_res)
message_list.append(tool_message)

# 第二轮调用模型:传入包含工具结果的完整上下文,模型整理结果生成自然语言回答
res = llm.invoke(message_list)

print('基于工具调用返回信息后,LLM的回复:', res)

3.3构建agent时添加本地工具

了解了如何定义tool之后,就可以使用 create_agent来创建 一个可以调用工具的Agent了。

这里使用 Tavily (搜索引擎)作为工具,需要先获取它的 API-Key 并添加到环境变量。

python 复制代码
# pip install langchain-tavily
import os
from langchain_tavily import TavilySearch
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

# 定义大语言模型
llm = init_chat_model(
    model="gpt-4o-mini",
    model_provider="openai",
)

# 定义 Tavily 联网搜索工具
# Tavily是专门给AI Agent使用的搜索引擎,可以实时联网获取互联网信息
search = TavilySearch(max_results=5)  # max_results:每次搜索最多返回5条搜索结果
tools = [search]  # 把工具放进工具列表,供给Agent使用

# 创建Agent
agent = create_agent(
    model=llm,        # 绑定使用的大模型
    tools=tools,      # 绑定可用工具(这里只有联网搜索)
    system_prompt="你是位助手,需要调用工具来帮助用户。",  # 给Agent的系统提示,规定Agent行为
)

# 调用Agent执行任务
res = agent.invoke(
    {"messages": [{"role": "user", "content": "今天北京的天气怎么样?"}]}
)
print(res)

如果 Agent 执行多个步骤,这可能需要一些时间。为了显示中间进度,我们可以使用 stream 流式返回消息。

python 复制代码
# pip install langchain-tavily
import os
from langchain_tavily import TavilySearch
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

# 初始化大模型,gpt-4o-mini,使用OpenAI接口
llm = init_chat_model(
    model="gpt-4o-mini",
    model_provider="openai",
)

# 实例化 Tavily 联网搜索工具(专为Agent设计的AI搜索引擎)
# max_results=5:每次搜索最多返回5条摘要结果
search = TavilySearch(max_results=5)
tools = [search]

# 创建Agent,内置自动工具调用循环逻辑,无需手动处理tool_call
agent = create_agent(model=llm, tools=tools)

# 使用 stream() 流式执行Agent,逐步返回过程块(chunk)
# stream 可以看到Agent完整思考链路:思考→调用工具→拿到结果→生成回答
for chunk in agent.stream(
    {
        "messages": [
            {"role": "system", "content": "你是位助手,需要调用工具来帮助用户。"},
            {"role": "user", "content": "今天北京的天气怎么样?"},
        ]
    }
):
    # 逐块打印每一步输出,查看Agent执行中间过程
    print(chunk, end="\n\n")

-- 要想更加精确地看到每个步骤的耗时,以及输入输出等信息,可以通过 LangSmith 来实现。LangSmith 是一个用于调试、测试、评估和监控 LLM 应用程序(特别是基于 LangChain 和 LangGraph 构建的应用)的全流程开发平台,旨在帮助开发者将原型转化为生产级应用。

-- 使用 LangSmith 非常简单,只需要配置如下两个环境变量,不需要改动任何代码,即可将整个调用过程在 LangSmit 当中展示,其中 API_KEY 可在 langsmith 官网进行配置:

python 复制代码
LANGSMITH_TRACING="true"
LANGSMITH_API_KEY="..."

-- 配置好环境变量之后,可在 LangSmith 的 Tracing Projects 中查看跟踪记录。

-- LangSmith 默认将跟踪记录到 default 项目,可通过 LANGSMITH_PROJECT 环境变量设置 LangSmith 跟踪记录保存到哪个项目,如果该项目不存在则会创建。

4 Agent 添加 MCP 工具

4.1 MCP 介绍

Model Context Protocol(MCP,模型上下文协议)是一个开源协议,它标准化了大语言模型与外部工具和数据源通信的方式,允许开发者和工具提供商只需集成一次,就能与任何兼容 MCP 的系统交互。MCP 就像 USB-C 标准:不需要为每个设备使用不同的连接器,而是使用一个端口来处理多种类型的连接。

4.2 MCP 架构

MCP 遵循客户端-服务器架构,架构中包括:

MCP 主机 协调和管理一个或多个 MCP 客户端的 AI 应用
MCP 客户端 一个保持与 MCP 服务器连接的组件,通过 MCP 定义的消息处理通信,从服务器查找并请求资源和工具,并管理与服务器的连接生命周期
MCP 服务器 一个向 MCP 客户端提供服务的程序,通过协议暴露工具、资源和提示模板功能

4.3 MCP 数据传输协议

MCP 客户端和服务端之间的数据传输机制,包括 Stdio、Streamable HTTP、SSE。

Stdio 使用标准输入和输出流,与在终端输入命令并看到响应时使用的机制相同。适用于本地开发,例如本地文件服务器等
Streamable HTTP 该传输使用 HTTP POST 和 GET 请求,服务器可以选择使用SSE来流式传输多个服务器消息。支持流式传输和服务器到客户端通知,并支持标准 HTTP 身份验证方法,包括授权令牌、API 密钥和自定义头信息
SSE 带有 SSE(Server-Sent Events 服务器发送事件)的 HTTP,MCP早期传输机制,现逐渐被 Streamable HTTP 取代

4.4 MCP工作流程

MCP的工作流程如下所示:

  • (1)握手与能力声明(Handshake & Discovery):当AI应用程序(Host)启动时,它会根据配置启动 MCP Server(通常是一个子进程)。Host 记住了这些工具的名字和用法说明,但此时 并没有执行任何代码 。
  • (2)用户提问与上下文注入(Context Injection):User提出问题后,Host 把用户的这个问题,加上刚才 Server 汇报的 工具说明书 ,一起打包发给 LLM。
  • (3)模型决策(LLM Reasoning):LLM决定是否需要去调用相关工具。
  • (4)路由与执行(Routing & Execution):Host 收到指令,发现是要调用 MCP 工具,Host 通过 MCP 协议(JSON-RPC)给 MCP Server 发消息,Server 收到请求, 在它自己的进程(如果是StreamableHttp方式,则是在远端执行)里执行相关操作。 所有的驱动依赖、复杂逻辑都在 Server 端。
  • (5)结果回传(Result Feedback):Server 拿到结果,打包成 MCP 响应发回给 Host,Host再将相关结果,传递给大模型,让大模型做下一步的输出。

4.5 MCP SDK

4.5.1 Stdio

以下是使用MCP进行stdio模式下的服务端编码过程:

python 复制代码
# pip install mcp
from mcp.server.fastmcp import FastMCP

# 创建 FastMCP 服务实例,服务名称为 Demo
# MCP:Model Context Protocol,模型上下文协议,用来标准化给大模型提供工具、资源、提示词
mcp = FastMCP("Demo")


# ========== 注册 MCP 工具(Tool) ==========
# @mcp.tool() 装饰器:把这个函数暴露成MCP工具,供支持MCP的客户端(Claude Desktop等)调用
@mcp.tool()
def add(a: int, b: int) -> int:
    """加法工具:接收两个整数,返回相加结果"""
    return a + b


# ========== 注册 MCP 资源(Resource) ==========
# @mcp.resource() 装饰器:注册资源,使用URI标识 greeting://default
# 资源是可供模型读取的静态/动态内容(文档、配置、常量文本等,只读)
@mcp.resource("greeting://default")
def get_greeting() -> str:
    """获取一条问候资源文本"""
    return "Hello from static resource!"


# ========== 注册 MCP 提示词模板(Prompt) ==========
# @mcp.prompt() 装饰器:注册可复用的Prompt模板,客户端可以传参渲染提示词
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    """生成问候提示词模板,支持不同风格"""
    styles = {
        "friendly": "写一句友善的问候",
        "formal": "写一句正式的问候",
        "casual": "写一句轻松的问候",
    }
    return f"为{name}{styles.get(style, styles['friendly'])}"


if __name__ == "__main__":
    # 启动MCP服务
    # transport="stdio":使用标准输入输出通信(最常用,适合Claude Desktop本地对接)
    # 还有其他传输方式:http、websocket等
    mcp.run(transport="stdio")

stdio客户端使用MCP的流程为:

  • 通过stdio_client启动进程:构建stdio启动参数
  • 建立会话:通过ClientSession构建会话session
  • 握手初始化:调用session.initialize()
  • 获取能力: 通过session.list_tools() / session.call_tools() 等获取或者使用MCP能力

示例代码如下所示:

python 复制代码
# pip install mcp
import asyncio
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters


async def stdio_run():
    """
    MCP 客户端示例
    通过 stdio 方式启动并连接上面我们写的 MCP Server(mcp_server_stdio.py)
    依次演示:查看工具列表、调用工具、读取资源、加载提示词模板
    """
    # 服务端进程启动参数
    server_params = StdioServerParameters(
        # python解释器路径,使用虚拟环境内的python
        command=r"D:\PycharmProjects\LangGraph_demo\.venv\Scripts\python.exe",
        # 启动服务端脚本:就是刚才写的MCP服务文件
        args=[r"./mcp_server_stdio.py"],
    )

    # 启动子进程,建立stdio管道通信,得到读写流 read / write
    async with stdio_client(server_params) as (read, write):
        # 创建MCP会话,会话是客户端和服务端交互的载体
        async with ClientSession(read, write) as session:
            # 和MCP服务端完成握手、初始化连接,必须先执行
            await session.initialize()

            # ========== 查询服务端暴露的所有工具 ==========
            tools = await session.list_tools()
            print("=== 可用工具列表 ===")
            print(tools)
            print()

            # ========== 调用 add 加法工具,传入参数 a=1,b=2 ==========
            call_res = await session.call_tool("add", {"a": 1, "b": 2})
            print("=== 调用 add 工具返回结果 ===")
            print(call_res)
            print()

            # ========== 查询服务端暴露的所有资源 ==========
            resources = await session.list_resources()
            print("=== 可用资源列表 ===")
            print(resources)
            print()

            # ========== 读取指定URI的资源 greeting://default ==========
            read_res = await session.read_resource("greeting://default")
            print("=== 读取 greeting 资源结果 ===")
            print(read_res)
            print()

            # ========== 查询服务端暴露的所有提示词模板 ==========
            prompts = await session.list_prompts()
            print("=== 可用Prompt模板列表 ===")
            print(prompts)
            print()

            # ========== 获取渲染后的提示词模板,传入参数 name=Jack ==========
            get_res = await session.get_prompt("greet_user", {"name": "Jack"})
            print("=== 获取 greet_user 提示模板结果 ===")
            print(get_res)
            print()


# 运行异步主函数
asyncio.run(stdio_run())

4.5.2 StreamableHttp

构建 StreamableHttp 服务端 和 stdio服务端类似,只是需要将服务端运行起来。运行的方式同样是通过 mcp.run,并设置 transport 为 streamable-http 即可:

python 复制代码
# 安装依赖命令:pip install mcp
# mcp 是 Model Context Protocol 官方SDK,FastMCP 是快速开发服务端的封装
from mcp.server.fastmcp import FastMCP

# 创建一个 MCP 服务实例,名字叫 Demo(用于客户端识别这个服务)
mcp = FastMCP("Demo")

# -------------------------- 工具 Tool:可供大模型调用的函数(可执行逻辑) --------------------------
# 使用 @mcp.tool() 装饰器,把函数注册成 MCP 工具,大模型可以调用这个函数、传参、拿到返回结果
@mcp.tool()
def add(a: int, b: int) -> int:
    """加法工具:接收两个整数,返回相加结果"""
    return a + b

# -------------------------- 资源 Resource:只读数据源(不是执行函数,读取静态/动态内容) --------------------------
# @mcp.resource(uri) 注册资源,uri 是资源唯一标识,格式自定义 greeting://xxx
# 客户端可以通过这个 URI 读取资源内容,适合读取配置、文档、静态文本
@mcp.resource("greeting://default")
def get_greeting() -> str:
    """获取默认问候资源"""
    return "Hello from static resource!"

# -------------------------- 提示模板 Prompt:预定义提示词模板,支持参数渲染 --------------------------
# @mcp.prompt() 注册提示模板,客户端可以传入参数渲染出完整 prompt 交给大模型
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    """生成问候提示词模板
    :param name: 要问候的人名
    :param style: 风格,可选 friendly / formal / casual
    """
    styles = {
        "friendly": "写一句友善的问候",
        "formal": "写一句正式的问候",
        "casual": "写一句轻松的问候",
    }
    # 根据传入的 style 匹配文案,拼接成最终提示词返回
    return f"为{name}{styles.get(style, styles['friendly'])}"

# 程序入口
if __name__ == "__main__":
    # mcp.settings.host = "0.0.0.0"  # 放开注释则监听所有网卡,允许局域网其他机器访问
    # mcp.settings.port = 8888        # 放开注释可以自定义端口
    # 启动服务,transport 选择 streamable-http(HTTP流式传输,适合远程客户端连接)
    # 默认地址:http://127.0.0.1:8000
    mcp.run(transport="streamable-http")
简单总结三者区别
  1. @tool :动作、计算、操作,会执行代码,比如加法、查数据库、调用 API
  2. @resource :读取数据,只读,类似文件 / 文档,通过 URI 寻址
  3. @prompt:提示词模板,负责拼接 prompt 文本,交给 LLM 去生成回答,本身不执行业务逻辑

StreamableHttp 和 Stdio 客户端编码方式类似,构建 client 的方式,从 stdio_client 的方式换成了 streamable_http_client,配置服务端所在的地址。

python 复制代码
# 安装依赖:pip install mcp
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client


async def streamablehttp_run():
    # MCP Streamable HTTP 服务端地址,就是上面Demo服务的接入端点
    url = "http://127.0.0.1:8000/mcp"

    # 创建 streamable-http 客户端连接,返回 (read 读流, write 写流, 其他上下文)
    async with streamable_http_client(url=url) as (read, write, _):
        # 使用读写流创建 MCP ClientSession,所有交互都通过 session
        async with ClientSession(read, write) as session:
            # 和MCP服务端握手初始化,协商协议版本、能力
            await session.initialize()

            # 查询服务端暴露的所有工具列表
            tools = await session.list_tools()
            print(tools)
            print()

            # 调用名为 add 的工具,传入参数 a=1, b=2
            call_res = await session.call_tool("add", {"a": 1, "b": 2})
            print(call_res)
            print()

            # 查询服务端暴露的所有资源列表
            resources = await session.list_resources()
            print(resources)
            print()

            # 根据资源URI读取资源内容(只读资源)
            read_res = await session.read_resource("greeting://default")
            print(read_res)
            print()

            # 查询服务端暴露的所有提示词模板
            prompts = await session.list_prompts()
            print(prompts)
            print()

            # 获取提示词模板,传入参数 name="Jack" 渲染prompt
            get_res = await session.get_prompt("greet_user", {"name": "Jack"})
            print(get_res)
            print()


# 启动异步主函数
asyncio.run(streamablehttp_run())

5 Agent 添加记忆

-- 前面的 agent 在多次 invoke 过程中,并不会将会话历史保存下来,也就是没有短期记忆。要想 Agent 拥有短期记忆,就需要在 create_agent 当中为 checkpointer 参数赋值,该参数其实是底层 LangGraph 用以保存多次调用记录的检查点配置。

-- Agent 拥有短期记忆能力之后,会将会话历史按照 thread_id 作为粒度进行隔离的。Thread_id 是来自于 agent 的底层运行时---LangGraph---当中的概念。不同的 thread_id,对应着不同的消息列表

如下图所示:

在具体调用时,只需要指定一个 thread_id,即可保证将所有短期记忆,存储到一个特定的消息列表当中;LLM在回复时,只会以当前 thread_id 所对应的消息列表作为上下文进行回复。

下面构造一个实际的 WeatherAgent, 来演示记忆功能:

python 复制代码
import os
import datetime
from langchain_tavily import TavilySearch
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import InMemorySaver

# ---------------------- 初始化搜索工具 ----------------------
# TavilySearch 是专门面向大模型的搜索工具,会返回结构化搜索结果
# max_results=5:每次搜索最多返回5条结果
search = TavilySearch(max_results=5)
# 把工具放进列表,传给agent,agent可以自主选择调用
tools = [search]

# ---------------------- 初始化大模型 ----------------------
# init_chat_model 是 langchain 统一的模型加载入口,适配多家厂商
llm = init_chat_model(
    model="gpt-4o-mini",
    model_provider="openai",
)

# ---------------------- 关键点1:定义 checkpointer 存储实例 ----------------------
# InMemorySaver:内存级别的存储,用来保存对话状态、历史消息
# 只存在程序内存里,程序重启后记忆全部丢失,生产环境要用 RedisSaver / PostgresSaver
checkpointer = InMemorySaver()

# ---------------------- 创建支持记忆的 Agent ----------------------
agent = create_agent(
    model=llm,
    tools=tools,
    checkpointer=checkpointer, # 关键点2:把存储实例注入agent,开启记忆持久化能力
)

# ====================== 第一次对话调用 ======================
print("=== 第一次调用 ===")
# agent.stream() 流式输出,一块一块返回结果,适合实时打印
for chunk in agent.stream(
    input={
        "messages": [
            {
                "role": "system",
                "content": f"当前时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            },
            {"role": "user", "content": "今天北京天气怎么样?"},
        ]
    },
    # 关键点3:thread_id 会话ID,同一个thread_id代表同一段对话
    config={"configurable": {"thread_id": "abc123"}},
):
    print(chunk, end="\n\n")

# ====================== 第二次对话调用 ======================
print("=== 第二次调用 ===")
for chunk in agent.stream(
    input={
        "messages": [
            {"role": "user", "content": "我刚才问你什么了"},
        ]
    },
   
    # 这里写的是 abcedf 和上面 abc123 不一样!
    # 两个不同thread_id = 两个独立会话,agent 回忆不到上一轮问题
    # 想要记住,必须改成同样的 thread_id="abc123"
    config={"configurable": {"thread_id": "abcedf"}},
):
    print(chunk, end="\n\n")

总结

  1. checkpointer + thread_id = LangGraph 会话记忆方案
    • checkpointer:仓库,用来存状态
    • thread_id:会话钥匙;钥匙相同,读取同一份历史;钥匙不同,全新对话
  2. InMemorySaver 局限:进程退出记忆清空,不能多实例共享
  3. create_agent 是 LangGraph 新版预制 Agent,自带工具调用、状态管理,比旧版 AgentExecutor 简洁
  4. stream():流式返回,适合做聊天界面;如果要一次性拿结果,用 .invoke()

修复后的第二次调用配置(能正常记住上下文)

复制代码
config={"configurable": {"thread_id": "abc123"}}

6 Agent 执行添加中间件

-- 在 create_agent 方法当中,还有一个 middleware 参数,可以传入中间件列表,即可在调用过程中使用中间件。

-- 在 LangChain 的 Agent 当中,中间件用于更加方便地控制Agent在执行过程当中的一些行为,例如,总结中间件可以在调用大模型前对历史消息列表进行总结,人在循环(Human-In-the-Loop)中间件,可以在调用工具过程中引入人类审核机制,人类审核通过后,才继续调用工具。

-- 在没有中间件时,整个 Agent 的执行过程如下所示:

-- 用户先输入请求,会直接进入到model节点,model节点判断,如果需要调用工具,则进入到tools节点,调用完工具之后,再回到model节点,model节点内,大模型继续判断,是否需要进一步调用工具,如果不需要直接走到END节点即可。

-- 而在引入了中间件之后,整个调用过程如下图所示:

整个调用Agent的过程,可以在不改变model和tools的相关代码前提下,实现多处调整:

  • before/after_agent:在agent调用的起始输入和终点输出,进行相关处理(切片编程思想);
  • before/after_model: 在model调用的前后,进行相关处理(切片编程思想);
  • wrap_tool/model_call: 通过handler回调的方式,拦截工具/模型执行,可以为工具执行/模型执行,添加重试,缓存,多次调用等相关逻辑(代理思想)。

注意:不同的中间件,可选择性地仅对以上的一个或者多个节点定义处理逻辑。

以下是两种中间件,及其分别实现的逻辑:

1)SummarizationMiddleware

总结历史消息的中间件,可以通过多种不同的策略,使用一个单独的大模型总结历史消息。该中间件实现了before_model的处理逻辑:在调用模型前,先按照配置的策略对消息进行压缩,再传递给model节点进行调用。

实例化方式如下所示:

python 复制代码
# 导入 LangGraph Agent 的中间件:人类介入/摘要中间件
# SummarizationMiddleware:自动对超长对话历史做摘要,压缩消息上下文,防止消息列表无限膨胀、token超限
from langchain.agents.middleware.human_in_the_loop import SummarizationMiddleware

# 实例化一个专门负责做摘要的 LLM
# 摘要任务不需要很强的模型,用 gpt-4o-mini 性价比高
summarize_llm = ChatOpenAI(model="gpt-4o-mini")

# 实例化摘要中间件
# trigger=("messages", 10) 触发规则:
# 监控状态里的 messages 消息列表,当消息数量达到 10 条时,自动触发摘要
# 触发后:把前面历史消息压缩成一段摘要文本,保留最新消息,减少上下文占用
summary_middleware = SummarizationMiddleware(model=summarize_llm, trigger=("messages", 10))

trigger表示所使用的策略,传入方式为元组,可以传入多种不同的策略:

  • ("messages", 100):agent消息列表的长度达到100以后,即进行总结压缩;
  • ("fraction",0.5): agent消息列表当中的token总长度(langchain内部提供了token计算函数,也可以在该函数当中自定义token_counter)达到了model最大输入token数的0.5时,进行总结压缩;
  • ("tokens",3000): agent消息列表当中的token总长度达到了一个绝对值之后(3000),进行总结压缩。

2)HumanInTheLoopMiddleware

调用工具前实现人工审核功能的中间件,可以对不同的工具,定义不同的审核策略(是否需要审核)。该中间件实现了after_model的处理逻辑。在模型回复后,该中间件判断模型的回复结果当中是否有工具调用。如果有,判断所需要调用的模型当中,是否有需要人工审核的工具。

具体实例化方式如下所示:

python 复制代码
# 导入 LangGraph Agent 的人工介入中间件(HITL,Human-in-the-Loop)
# 作用:指定某些工具调用前自动暂停 Agent,交给人工审核确认,通过之后才真正执行工具
from langchain.agents.middleware.human_in_the_loop import HumanInTheLoopMiddleware

# 配置规则字典:key = 工具名称,value = 是否需要人工中断审核
# send_email: True → 调用 send_email 发邮件工具时,先暂停,等待人类确认
# get-current-date: False → 调用该工具不需要人工确认,Agent直接执行
tool_name_to_interrupt = {'send_email':True,'get-current-date':False}

# 实例化人工介入中间件
# interrupt_on:传入上面的规则表,定义哪些工具触发人工拦截
human_in_the_loop_middleware = HumanInTheLoopMiddleware(interrupt_on=tool_name_to_interrupt)

示例

python 复制代码
import asyncio
from typing import Literal

from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command


# ====================== 1. 定义Agent可用工具 ======================
@tool
def get_weather(city: str) -> str:
    """查询天气
    Args:
        city: 城市名称
    Returns:
        天气结果字符串
    """
    return f"{city}的天气晴朗,气温25度。"


@tool
def transfer_money(amount: int, to_account: str) -> str:
    """转账工具 (敏感高危操作)
    Args:
        amount: 转账金额
        to_account: 收款账户
    Returns:
        转账结果信息
    """
    print(f"!!! 正在执行转账: {amount} -> {to_account} !!!")
    return f"成功转账 {amount} 元给 {to_account}。"


# ====================== 2. 初始化大模型 ======================
llm = init_chat_model("gpt-4o-mini", model_provider="openai")


# ====================== 3. 配置人工介入中间件 HITL ======================
# interrupt_config:配置哪些工具调用需要人工拦截审核
# key:工具名称;value=True → 开启拦截,等待人工确认;False → 直接放行自动执行
interrupt_config = {
    "transfer_money": True,   # 转账属于高危操作,调用前中断,等待人审批
    "get_weather": False      # 查询天气是只读查询,无需人工审核,直接执行
}
hitl_middleware = HumanInTheLoopMiddleware(interrupt_on=interrupt_config)


# ====================== 4. 创建Agent ======================
# ⚠️ 重要:中断功能必须搭配 checkpointer
# 中断时需要保存当前图的运行状态,等人工确认后恢复继续跑
checkpointer = InMemorySaver()
agent = create_agent(
    model=llm,
    tools=[get_weather, transfer_money],
    middleware=[hitl_middleware],  # 挂载人工审核中间件
    checkpointer=checkpointer,
)


# ====================== 异步主演示函数 ======================
async def run_demo():
    print("=== HumanInTheLoopMiddleware 演示 ===")
    print("场景:用户让 Agent 转账,Agent 在执行前会暂停等待批准。")

    thread_id = "thread-1"
    # 会话配置,同一个thread_id绑定同一份状态(中断信息、对话历史都存在这里)
    config = {"configurable": {"thread_id": thread_id}}

    # 用户原始提问
    print("\n[User]: 请帮我转账 100 元给 Alice")

    # 初始输入:用户消息
    current_input = {"messages": [{"role": "user", "content": "请帮我转账 100 元给 Alice"}]}
    # Command 用来存放恢复中断时的指令,初始为空
    current_command = None

    while True:
        try:
            # 分支:如果有恢复指令,就用Command恢复图;否则发送新用户输入
            if current_command:
                result = await agent.ainvoke(current_command, config=config)
                current_command = None  # 清空恢复指令,避免重复使用
            else:
                if not current_input:
                    break
                result = await agent.ainvoke(current_input, config=config)
                current_input = None  # 本轮用户输入已经处理完毕

            # 打印返回消息,区分不同消息类型
            if "messages" in result:
                for msg in result["messages"]:
                    # AI消息:想要调用工具
                    if hasattr(msg, "tool_calls") and msg.tool_calls:
                        print(f"[Agent]: 我想调用工具: {msg.tool_calls}")
                    # tool类型消息:工具执行完成后的返回结果
                    if msg.type == "tool":
                        print(f"[Tool Output]: {msg.content}")
                    # AI普通回复,没有工具调用,代表任务结束
                    if msg.type == "ai" and not msg.tool_calls:
                        print(f"[Agent]: {msg.content}")

        except Exception as e:
            print(f"发生错误: {e}")
            pass

        # ========== 判断是否触发人工中断 ==========
        # 当中间件拦截成功,返回结果里会带上 "__interrupt__" 标记
        if "__interrupt__" in result:
            interrupt_value = result["__interrupt__"][0].value
            print(f"\n!!! 检测到中断 (Middleware 拦截) !!!")
            print(f"中断详情: {interrupt_value}")

            # 模拟人工审核交互界面
            print("\n[System]: 请审核上述操作 (approve/reject/edit):")
            decision_type = "approve"  # 这里硬编码模拟用户选择:approve 同意;也可以改成 reject拒绝
            print(f"[User]: {decision_type}")

            decisions = []
            # 取出本次被拦截等待审批的操作列表
            action_requests = interrupt_value.get("action_requests", [])

            for req in action_requests:
                print(f"  - 批准操作: {req['name']}")
                decisions.append({"type": "approve"})

            # ✅ 核心:构造 Command(resume=xxx) 给LangGraph,恢复被暂停的流程
            # resume里面携带人工审批结果,交给HITL中间件处理
            current_command = Command(resume={"decisions": decisions})
            print("[System]: 恢复执行...")
            continue  # 回到循环头部,用current_command恢复运行

        # 没有中断标记,说明流程正常跑完,退出循环
        break


if __name__ == "__main__":
    asyncio.run(run_demo())

核心要点整理

  1. HumanInTheLoopMiddleware 工作链路
    Agent 推理 → 决定调用 transfer_money → 中间件匹配到该工具需要拦截 → 抛出中断、保存状态到 checkpointer → 外部接收中断,人工选择 approve/reject/edit → 组装 Command(resume=...) → 恢复执行
  2. Command 的作用
    LangGraph 标准类型,专门用来恢复中断的流程,不是普通用户消息。
  3. 三种审批动作
    • approve:放行,正常执行工具
    • reject:拒绝,不执行工具,告诉 Agent 该操作被禁止
    • edit:修改工具入参之后再批准执行
  4. 局限性
    InMemorySaver 内存存储,程序重启中断状态丢失;生产环境替换为 RedisSaver / PostgresSaver。
相关推荐
zhangfeng11331 小时前
2026 书生国智科探挑战赛暨飞翔杯 AI Agent/Skills 开发大赛全景介绍
人工智能
zroooo1 小时前
计算机学习-知识庞大而繁杂
笔记·学习
DevMemo1 小时前
2026 AI外呼系统技术选型:全链路延迟预算与集成成本实测,10个品牌的架构真相
人工智能·架构
坤坤子吖1 小时前
算法学习——单调队列
开发语言·c++·笔记·学习·算法
ZhuoAIStage1 小时前
契约与闸门打架该改谁?用 AI 智能体 preflight 把配置漂移挡在发版前——以舞台工艺审图专家包闸门冲突为例
人工智能·闸门·舞台工艺·配置漂移·专家包·preflight
Flynn Liang1 小时前
大模型怎么自己调函数?Spring AI 2.0 Tool Calling 自动循环原理 + 完整时序图
java·人工智能·spring·agent·函数调用·springai·toolcalling
深频率1 小时前
苹果AI入华:11月4日开测,千问只是个选项
人工智能
一枕槐安4872 小时前
大模型 API 调用封装:统一适配 GPT、文心一言、通义千问
人工智能·gpt·文心一言·大模型api·流式输出·统一封装·多模态路由