第 5 章 Agent

一、Agent核心概念与技术架构

Agent智能体是一种以大语言模型(LLM)为"大脑",能够自主感知环境、进行推理规划,并调用外部工具执行复杂任务的系统。它不仅仅是简单的程序,而是具备一系列高级特征的复杂系统。根据LangChain框架的定义,Agent的核心是以大语言模型(LLM)作为其推理引擎,并依据LLM的推理结果来决定如何与外部工具进行交互以及采取何种具体行动。这种架构将LLM的强大语言理解与生成能力,与外部工具的实际执行能力相结合,从而突破了单一LLM的知识限制和功能边界。Agent的本质可以被理解为一种高级的提示工程(Prompt Engineering)应用范式,开发者通过精心设计的提示词模板,引导LLM模仿人类的思考与执行方式,使其能够自主地分解任务、选择工具、调用工具并整合结果,最终完成复杂的任务。

Agent(智能体)已超越传统AI模型,成为能够自主完成多步骤复杂任务的智能数字助手。其核心特征在于自主性增强、执行能力和持续学习。

image

AI Agent 与传统 AI 模型核心能力对比

对比维度传统 AI 模型Agent 智能体
交互能力 被动响应用户输入 主动感知环境变化
决策模式 基于概率预测 基于目标导向的主动规划
执行能力 仅生成文本/内容 能够调用工具、访问外部系统
学习方式 静态知识更新 动态记忆积累和经验反思
任务处理 单次对话完成 支持多步骤、复杂任务序列
自主程度 高度依赖人类指导 具备一定程度的自主决策能力

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

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

Agent 的核心能力/组件:

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

二、Agent的核心特征

Agent智能体通常具备以下几个核心特征,这些特征共同构成了其强大的能力基础:

image

2.1 自主性 (Autonomy)

自主性是Agent最核心的特征之一,指的是Agent能够在没有人类直接干预的情况下,独立地完成任务的感知、规划、决策和行动的全过程。在LangChain框架中,这种自主性体现在Agent能够根据用户的输入,自动判断是否需要调用外部工具,选择哪个工具,以及如何组织调用参数。例如,当用户询问"北京的天气怎么样?"时,Agent能够自主识别出这是一个需要实时信息查询的任务,并自动调用天气查询工具来获取答案,而无需开发者显式地编写"如果问题是关于天气,则调用天气API"这样的硬编码逻辑。这种自主性使得Agent能够处理更加开放和动态的问题,极大地提升了应用的灵活性和智能水平。

2.2 感知能力 (Perception)

感知能力是指Agent获取和理解环境信息的能力。在基于LLM的Agent中,环境信息主要以文本形式存在,包括用户的输入、工具的输出以及系统状态等。Agent通过其底层的LLM来解析和理解这些文本信息,从中提取关键指令、实体和上下文。例如,在接收到用户问题后,Agent需要感知问题的意图和关键实体(如地点、时间、人物),以便决定后续的行动。LangChain框架通过提供标准化的消息格式(如`HumanMessage`, `AIMessage`)和工具描述机制,为Agent的感知能力提供了坚实的基础,使其能够清晰地理解来自不同来源的信息。

2.3 推理与规划 (Reasoning & Planning)

推理与规划是Agent智能的核心。Agent需要能够分析任务目标,并将其分解为一系列可执行的子步骤。LangChain中的Agent,特别是基于ReAct(Reasoning and Acting)范式的Agent,展现了强大的推理和规划能力。ReAct框架要求LLM在每一步都生成一个"思考"(Thought)过程,解释其当前的理解和下一步的计划,然后生成一个"行动"(Action),即调用某个工具。这个过程会循环进行,直到Agent认为已经收集了足够的信息来回答原始问题。例如,面对一个复杂的多步骤数学问题,Agent会先规划出解题步骤,如"首先计算A,然后用A的结果计算B",并按此规划逐步调用计算工具来完成任务。

2.4 行动能力 (Action)

行动能力是指Agent执行具体操作以影响环境的能力。在LangChain框架中,Agent的行动能力主要通过调用外部工具(Tools)来实现。这些工具可以是API调用、数据库查询、代码执行器,甚至是其他Agent。Agent通过LLM来决定调用哪个工具,并生成符合工具要求的输入参数。工具执行后,其输出结果会作为新的环境信息反馈给Agent,供其进行下一步的推理和决策。这种"思考-行动-观察"的循环,使得Agent能够与外部世界进行有效的交互,从而完成各种复杂的实际任务,如信息检索、数据处理和自动化流程控制。

2.5 学习能力 (Learning) 

一个真正的智能体不仅仅是执行预设的程序,它还应该具备从经验中学习并不断优化自身行为的能力。这种学习能力通常通过强化学习、反馈机制或记忆系统来实现。智能体在每次行动后,会观察行动的结果,并根据结果(例如,用户的反馈或环境的奖励/惩罚信号)来调整其内部的决策模型或策略。例如,如果一个智能体推荐的商品被用户频繁购买,它就会学习到这种推荐是有效的;反之,如果推荐被用户忽略或拒绝,它就会调整其推荐策略。这种持续学习和优化的能力使得智能体能够随着时间的推移变得越来越"聪明",更好地适应复杂多变的环境。

三、Agent技术架构核心

理解 Agent(智能体) 最难的地方在于理解它"如何自主决策“。在LangChain 1.0框架中,Agent不再只是一个简单的问答机器人,它更像是一个"拥有万能工具箱的超级项目经理”。 

  • LLM(大模型) = 大脑(项目经理):它负责思考、规划、决定下一步做什么,但它不能联网,也不能算复杂的数学(如果不借助工具)。
  • Tools(工具) = 手脚(执行专员):比如谷歌搜索(负责看世界)、计算器(负责算数)、数据库(负责查档案)。
  • Agent = 大脑 + 手脚 + 循环机制:把大脑和手脚结合起来,通过不断的"思考-行动-观察"循环来解决问题。

image

现代Agent的技术架构由五个核心模块构成,形成完整的"感知-思考-行动"闭环。

  • 感知模块 (Perception):负责接收文本、图像、语音等多模态输入。
  • 认知中枢 (Brain/Planning):基于大语言模型(LLM)和检索增强生成(RAG)技术,进行推理和决策,弥补LLM无法获取实时信息和执行具体操作的缺陷。
  • 记忆系统 (Memory):通过短期记忆维持对话连贯,长期记忆积累经验与偏好。
  • 工具生态 (Tools):通过API调用、数据库访问等方式与外部系统交互。
  • 执行引擎 (Action):负责执行具体任务并反馈结果。

这一机制使得Agent能够构建一个完整的执行闭环:环境感知 → 任务规划 → 工具调用 → 执行反馈 → 自我反思 → 优化调整,从而在复杂环境中持续学习和改进。       

四、Agent与LangChain结合机制

LangChain 1.0通过将Agent的决策与LangGraph的图式执行相结合,提供了生产级的Agent运行时。其结合机制体现在以下几个方面:

4.1 核心结合点:create_agent + LangGraph

create_agent作为上层统一入口,其内部实现依赖于LangGraph。当调用create_agent时,LangChain会自动构建一个基于ReAct(推理+行动)范式的图结构。这个图包含了Agent决策、工具调用、状态更新等核心节点,并通过边来控制逻辑流转。这种设计将Agent的"思考"过程映射为图的遍历,使得整个执行流程变得透明、可控。

image

LangChain 1.0 的 create_agent 通过这 9 个核心参数,实现了从快速原型到生产部署的全覆盖,开发者可根据场景灵活组合。

  • create_agent 的核心价值在于它通过 "三要素 + 三扩展" 的极简抽象,彻底重构了 Agent 的开发范式。所谓三要素,即模型(Model)、工具(Tools)与提示词(System Prompt),这三者构成了 Agent 的"灵魂"——决定了它能思考什么、能做什么以及行为边界何在。而三扩展——中间件(Middleware)、内存管理(Memory)与状态管理(State)——则构建了 Agent 的"神经系统",使其具备生产级应用所需的可靠性、可观测性与可维护性。
  • 这一设计将开发者从繁琐的 ReAct 循环手写、工具调用异常处理、上下文压缩等底层细节中解放出来,转而采用声明式编程模式:只需描述"Agent 应该做什么",框架自动编译为高效、可靠、安全的执行计划。其本质是 LangGraph 的编译器前端 ,将高层意图转换为优化的图结构,自动集成持久化、流式输出、断点恢复等运行时能力。

这种架构带来了三重革命性影响:首先,开发效率提升 10 倍,10 行代码即可构建一个可投产的智能客服或数据分析 Agent;其次,运维成本降低 60%,中间件机制将 PII 检测、人工审批、自动重试等横切关注点解耦,无需侵入业务代码;最后,可扩展性实现质的飞跃,通过 TypedDict 扩展 State,可无缝集成用户画像、多模态输入、性能监控等复杂场景。

参数类型必填默认值核心作用最佳实践
model str / 实例 - 推理引擎 生产环境实例化配置
tools list [] 执行能力 描述清晰,按需添加
system_prompt str None 行为准则 明确角色和约束
middleware list [] 功能扩展 组合日志、安全、摘要
checkpoint Saver None 短期记忆 生产用 PostgresSaver
store Store None 长期记忆 跨会话用 PostgresStore
state_schema TypedDict AgentState 扩展状态 TypedDict 非 Pydantic
context_schema TypedDict None 动态上下文 配合 middleware 使用
response_format BaseModel None 结构化输出 API 对接场景启用
import os
import dotenv
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent

dotenv.load_dotenv()

llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('VLLM_API_KEY'),
    temperature=0.7,
    max_tokens=10000,
)


agent = create_agent(
    model=llm,                                                # 模型
    tools = [order_query_tool],                               # 工具
    system_prompt="""你是一个订单查询助手,能够查询订单状态和明细""", # 系统提示词
    middleware=[order_query_middleware],                      # 中间件
    checkpointer=checkpointer,                                # 状态查点短期检记忆
    store=store,                                              # 状态存储长期记忆
    state_schema=OrderQueryState,                             # 扩展状态(如需要)
    context_schema=AgentContext,                              # 上下文状态(如需要)
    response_format=ResponseModel                             # 结构化输出(如需要)
)

conf = {
    "configurable": {"thread_id", "limit_demo"},  # 限制thread_id线程ID
    "recursion_limit": 3  # 最多三次迭代,或者使用中间件进行精确跟踪和终止循环
}

message = {
    "messages": [{"role": "user", "content": "请查询订单号 12345678 的订单状态"}]
}
result = agent.invoke(
    message, config=conf
)

4.2 ReAct范式与执行循环

ReAct(Reasoning + Acting)范式强调"推理—行动—观察"的闭环:Agent先形成Thought(推理),据此选择并调用工具(Action),再吸收工具返回的Observation(观察),进入下一轮决策。闭环在达到最终答案、迭代上限或时间上限时终止。在LangGraph中,这一闭环由状态机与检查点驱动,保证每次行动的原子性、状态的可见性与轨迹的可回放性。并且推理与规划不是代码逻辑,而是LLM的生成行为,关键的 Thought: 步骤并非由确定性算法执行,而是prompt触发LLM生成推理文本。模型能力是ReAct性能的天花板

image

Agent的认知循环本质上是一个闭环反馈系统。每一次"行动"的执行结果都会作为新的输入反馈到系统,影响下一轮的"思考"和"行动"。这种反馈机制使得Agent能够动态调整策略,应对不确定的环境和复杂任务。在LangChain中,这一循环被实现为:

  1. Thought (推理):大模型基于当前输入和历史记录进行思考,决定下一步行动。

  2. Action (行动):大模型选择一个工具并构造输入参数,形成一个AgentAction

  3. Observation (观察):工具被执行,其返回结果作为观察值,并与AgentAction一起被添加到中间步骤(intermediate_steps)中。

  4. 循环决策:Agent将新的观察结果纳入上下文,进入下一轮"推理-行动"循环,直至达到最终目标或触发终止条件(如达到最大迭代次数)。

五、Tools

5.1 Tools 介绍

工具是Agent与外部世界交互的桥梁。在LangChain中,工具的`name`、`description`和`args_schema`至关重要,它们共同决定了模型是否以及如何选择和调用工具。一个设计良好的工具描述是提示工程的关键部分。

  • 工具注册:通过@tool装饰器或继承BaseTool类来定义工具。
  • 工具调用:Agent在决策时,会根据工具描述选择最合适的工具。执行引擎负责调用该工具并处理其返回结果或异常。
  • 安全与治理:在生产环境中,应对工具的调用进行严格的风险控制,如速率限制、权限隔离、输入校验等,这些可以通过中间件或在工具实现中直接加入。

LangChain内置工具列表:https://docs.langchain.com/oss/python/integrations/tools

工具名Python 类作用
python_repl PythonREPLTool 执行 Python 代码
shell ShellTool 执行命令行命令
human HumanTool 人工输入交互
requests_get RequestsGetTool 发送 HTTP GET 请求
requests_post RequestsPostTool 发送 HTTP POST 请求
bing_search BingSearchRun Bing 搜索引擎搜索
serper GoogleSerperRun Google 搜索引擎搜索
tavily_search TavilySearchResults Tavily 搜索引擎搜索
web_loader WebBaseLoader 加载并解析网页内容
apify ApifyActorTool 使用 Apify 平台进行网页爬虫
gmail Gmail 工具集 邮件收发与管理
google_calendar GoogleCalendar 工具集 日程管理与操作
python_ast PythonAstREPLTool 数据分析场景下的安全 Python 执行器
read_file ReadFileTool 读取本地文件内容
write_file WriteFileTool 写入内容到本地文件
sql_db_query QuerySQLDatabaseTool 对数据库执行 SQL 查询
retriever VectorStoreTool RAG 检索,从向量库召回相关文档

5.2 @tool创建工具

@tool装饰器是LangChain中最简单、最直观的工具创建方式。它通过装饰器语法将普通Python函数转换为Agent可调用的工具,适合快速原型开发和简单工具实现。一个 Tool 通常包括工具名称,工具描述,以及工具参数的类型注解。可以通过 @tool 装饰器来创建工具。

技术概述:

  • 自动参数推断:基于函数签名自动生成工具的参数schema
  • 简化配置:只需提供工具名称和描述即可快速创建
  • 同步执行:默认支持同步函数调用,异步需要单独定义
  • 快速验证:适合概念验证和快速迭代开发

核心优势:

  • 代码简洁,一行装饰器即可完成工具注册
  • 无需复杂的类继承和配置
  • 与Python函数无缝集成,开发效率高

适用场景:

  • 快速原型验证
  • 简单工具实现
  • 开发测试阶段

5.2.1 举例:通过 @tool 创建工具

from langchain.tools import tool


@tool
def add_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b


print(f"{add_numbers.name=}\n{add_numbers.description=}\n{add_numbers.args=}")
# 输出: 
add_numbers.name='add_numbers'
add_numbers.description='Add two numbers together.'
add_numbers.args={'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}

5.2.2 举例:通过 @tool 的参数修改属性

from langchain.tools import tool
from pydantic.v1 import BaseModel,Field


class FieldInfo(BaseModel):
    a: int = Field(description="第一个计算参数")
    b: int = Field(description="第二个计算参数")


@tool(
    name_or_callable="sum_numbers",  # 工具名称
    description="计算两个整数之和",  # 描述
    args_schema=FieldInfo  # 参数模型, 使用上面定义的FieldInfo
)
def add_numbers(a: int, b: int) -> int:
    """Add two numbers together."""
    return a + b


print(f"{add_numbers.name=}\n{add_numbers.description=}\n{add_numbers.args=}")
# 输出:
add_numbers.name='sum_numbers'
add_numbers.description='计算两个整数之和'
add_numbers.args={'a': {'title': 'A', 'description': '第一个计算参数', 'type': 'integer'}, 'b': {'title': 'B', 'description': '第二个计算参数', 'type': 'integer'}}

5.2.3 绑定工具

要想让大模型能够使用工具,首先需要将工具给到大模型。只需要在创建模型实例后,通过 bind_tools 方法将工具绑定到大模型即可。

(1)大模型通过分析用户需求,判断是否需要调用工具。

(2)如果需要则在响应的 additional_kwargs 参数中包含工具调用的详细信息。

(3)使用模型提供的参数执行工具。

from langchain_core.tools import tool
from langchain_ollama import ChatOllama


@tool
def query_user_info(user_id: int) -> str:
    """查询用户信息"""
    return {1001: "张三", 1002: "李四", 1003: "王五"}[user_id]


# 11.初始化大模型
llm = ChatOllama(
    base_url="http://127.0.0.1:11434",
    model="qwen3:8b"
)

# 准备工具列表, 每个工具都是一个函数, 可以添加多个工具
tools = [query_user_info]

# 为模型绑定工具
llm_with_tools = llm.bind_tools(tools)

# 调用模型执行, 注意这里的参数是工具调用的参数,但是模型不会调用工具,只是返回了工具调用的信息
resp = llm_with_tools.invoke("帮我查询1001的用户信息")
print(resp)

# content='\n\n我来帮您查询1001用户的信息。\n'
# additional_kwargs={'tool_calls': [{'id': '...', 'function': {'arguments': '{"user_id": 1001}', 'name': 'query_user_info'}
# 返回的响应中 additional_kwargs 参数中包括了工具调用的信息,此时还没有调用工具,只是返回了要调用的工具及参数

# 手动调用工具
for tool_call in resp.tool_calls:
    tool_name = tool_call["name"]  # 获取工具名称
    tool_args = tool_call["args"]  # 获取工具参数
    tool_result = globals()[tool_name].invoke(tool_args)  # 调用工具
    print(tool_name, tool_args, tool_result)

image

5.2.4 构建 Agent

使用 create_agent 来创建 Agent,create_agent 使用 LangGraph 构建基于图的 Agent运行时。此 Agent 会在一个循环中反复调用模型和工具,直到某次模型输出中不再包含工具调用则结束。

使用 create_agent 创建 Agent 时,需传入模型和工具、可选地也可以传入系统提示词。这里使用自定义的天气查询函数作为工具,

from langchain.agents import create_agent
from langchain_ollama import ChatOllama

from nexus_rag.llm_functions import get_weather

# 1.初始化大模型
llm = ChatOllama(
    base_url="http://127.0.0.1:11434",
    model="qwen3:8b"
)

# 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具
tools = [get_weather]

# 3.创建agent
agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。")

# 4.调用agent
res = agent.invoke({"input": "查询北京今天天气"})
print(res['messages'][-1].content)

image

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

from langchain.agents import create_agent
from langchain_core.messages import ToolMessage
from langchain_ollama import ChatOllama

from nexus_rag.llm_functions import get_weather

# 1.初始化大模型
llm = ChatOllama(
    base_url="http://127.0.0.1:11434",
    model="qwen3:8b"
)

# 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具
tools = [get_weather]

# 3.创建agent
agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。")

# 4.调用agent
# res = agent.invoke({"input": "查询北京今天天气"})
# print(res['messages'][-1].content)

chunks = agent.stream({"input": "查询北京今天天气"})
for chunk in chunks:
    # 检查是否有工具调用结果
    if "tools" in chunk:
        for msg in chunk["tools"]["messages"]:
            if isinstance(msg, ToolMessage): # 判断是否是工具调用结果,是则打印出来,ToolMessage表示工具调用结果
                print(msg.content, end="", flush=True)

image

5.3 StructuredTool.from_function() 自定义tool

StructuredTool.from_function() 是最常用的方式,通过函数直接创建结构化工具,支持同步和异步双重实现。StructuredTool.from_function()方法提供了更强大的工具创建能力,支持完整的参数校验和异步执行,适合生产环境使用。       

技术概述:

  • 强类型校验:支持Pydantic模型进行参数验证
  • 异步支持:通过coroutine参数支持异步函数
  • 完整元数据:支持name、description、return_direct等完整配置
  • 生产就绪:内置错误处理和参数校验机制

核心特性:

  • 参数schema完全可控,支持复杂数据结构
  • 异步执行支持,适合I/O密集型操作
  • 完整的工具元数据配置
  • 生产环境级别的错误处理

适用场景:

  • 生产环境工具开发
  • 需要严格参数校验的场景
  • 异步操作需求
  • 企业级应用
import os
import dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool


class DivideInput(BaseModel):
    """
    除法工具输入参数
    """
    dividend: float = Field(..., description="被除数")
    divisor: float = Field(..., description="除数")


def divide(dividend: float, divisor:float) -> float:
    """
    除法工具
    """
    if divisor == 0:
        raise ValueError("除数不能为0")
    return dividend / divisor


# 2.创建带参数校验的工具
division_tool = StructuredTool.from_function(
    func=divide,                                     # 执行函数
    name="DivisionTool",                             # 工具名称
    description="安全执行除法运算,自动处理除零错误",      # 工具描述
    args_schema=DivideInput,                         # 显式指定参数模式
    return_direct=False                              # 是否直接返回工具结果(不经过LLM再次处理)
)

# 3.测试参数校验(触发 Pydantic 验证)
# try:
#     division_tool.invoke({"a": 10, "b": 2})  # 错误:参数名称错误,触发 Pydantic 验证
# except ValueError as e:
#     print(e)


# 4.正确调用
# result = division_tool.invoke({"dividend": 10, "divisor": 2})
# print(f"除法结果:{result}")

dotenv.load_dotenv()
# 5. 初始化大模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)


# 6.创建agent
agent = create_agent(model=llm, tools=[division_tool], system_prompt="你是一个数学助手,请根据用户输入的数学问题进行计算")

result = agent.invoke({"messages": [{"role": "user", "content": "请计算10除以2"}]})
print(result["messages"][-1].content)

image

5.4 继承StructuredTool

通过继承StructuredTool类创建工具提供了最大的灵活性和控制力,适合复杂业务逻辑和状态管理需求。

技术概述:

  • 完全自定义:可以完全控制工具的所有行为

  • 状态管理:支持工具内部状态维护

  • 复杂逻辑:适合实现复杂的业务逻辑

  • 企业级特性:支持完整的生命周期管理

核心能力:

  • 完整的Pydantic集成和类型系统
  • 自定义错误处理和重试机制
  • 工具内部状态管理
  • 复杂的业务逻辑封装

适用场景:

  • 企业级复杂工具开发

  • 需要状态管理的工具

  • 复杂的业务逻辑封装

  • 高性能要求的场景           

import asyncio
import os
import sys
from typing import Type

# 修复 Windows 终端 GBK 编码问题,使 emoji 和中文正常输出
sys.stdout.reconfigure(encoding='utf-8')

import dotenv
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool


class OrderQueryInput(BaseModel):
    """
    订单查询输入参数
    """
    order_id: str = Field(..., description="订单ID, 格式: ORD-2026-XXXX")
    include_details: bool = Field(default=False, description="是否包含订单明细")


class OrderQueryTool(StructuredTool):
    """订单查询工具"""
    name: str = "query_order"
    description: str = "查询电商平台订单状态和物流信息"
    args_schema: Type[BaseModel] = OrderQueryInput
    return_direct: bool = False

    def _run(self, order_id: str, include_details: bool = False) -> dict:
        """
        执行订单查询,查询电商平台订单状态和物流信息
        """

        # 模拟数据库查询
        order_db = {
            "ORD-2026-0001": {
                "status": "待处理",
                "tracking_number": "1234567890",
                "details": [
                    {"product_id": "P001", "quantity": 2},
                    {"product_id": "P002", "quantity": 1}
                ]
            },
            "ORD-2026-0002": {
                "status": "已处理",
                "tracking_number": "9876543210",
                "details": [
                    {"product_id": "P003", "quantity": 3}
                ]
            }
        }

        # 模拟数据库查询
        if order_id not in order_db:
            return {"error": f"订单{order_id}不存在"}
        # 返回查询结果
        result = order_db[order_id]

        # 处理查询逻辑
        if include_details:
            result["items"] = ["商品A * 2", "商品B * 1"]

        return result



dotenv.load_dotenv()
# 5. 初始化大模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 6.创建agent
agent = create_agent(model=llm, tools=[OrderQueryTool()],  # 切记!!! 这里需要传入工具对象。一定是类的对象实例
                     system_prompt="你是一个电商客服助手,使用工具查询订单状态和物流信息,回答要友好且准确", checkpointer=InMemorySaver())


# 执行并观察ReAct过程
async def run_agent():
    config = {
        "configurable": {"thread_id": "customer_001"},
        "recursion_limit": 15  # 最多15次迭代,或者使用中间件进行精确跟踪和终止循环
    }

    query = "请你帮我查询订单 ORD-2026-0002 的状态和物流信息"

    async for step in agent.astream(
            {"messages": [{"role": "user", "content": query}]},  # 输入提问的消息
            config=config,  # 配置项
            stream_mode="values"  # 流式输出模式,返回每一步的完整状态
    ):
        message = step["messages"][-1]
        message.pretty_print()
        print("-" * 50)


if __name__ == '__main__':
    # 运行
    asyncio.run(run_agent())

image

核心要点总结

  • 参数校验:始终使用 args_schema 定义 Pydantic 模型,确保输入合法
  • 异步优先:为网络 I/O 操作提供 _arun 实现,提升 Agent 并发性能
  • 文档清晰:description 字段是 LLM 选择工具的唯一依据,必须详细描述功能和参数
  • 返回值控制:return_direct=True 适合无需 LLM 润色的确定性格式数据
  • 调试友好:使用 tool.invoke() 单独测试工具,确保逻辑正确后再集成到 Agent

三种创建方式对比:

特性@tool 装饰器StructuredTool.from_function()继承 StructuredTool
代码简洁度 ⭐⭐⭐⭐⭐(极简) ⭐⭐⭐⭐(简洁) ⭐⭐(较繁琐)
参数控制 自动推断,弱控制 支持 args_schema,强校验 完全自定义 Schema
异步支持 ❌(需单独定义 async 函数) ✅(通过 coroutine 参数) ✅(实现 _arun 方法)
元数据定制 有限(name, description) 中等(name, description, return_direct) 完全定制(所有属性)
适用场景 快速原型、简单工具 生产环境、需要参数校验的场景 复杂业务逻辑、状态管理
类型提示 依赖函数签名 结合 Pydantic 强类型 完整的 Pydantic 集成

5.5 多工具使用

import os
import dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool


@tool
def calculate(expression: str) -> str:
    """计算一个数学表达式的结果"""
    try:
        result = eval(expression)
        return f"计算结果是:{result}"
    except Exception as e:
        return f"计算出错: {e}"


@tool
def get_weather(location: str) -> str:
    """获取天气信息"""
    weather_data = {
        "北京": "晴天,温度是 25°C",
        "上海": "阴天,温度是 29°C",
        "广州": "雨天,温度是 20°C",
        "深圳": "雷阵雨,温度是 18°C"
    }

    return f"天气信息: {location} 的天气是: {weather_data.get(location, '未知地点')}"


dotenv.load_dotenv()
# 1. 初始化大模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 2.创建agent
agent = create_agent(model=llm, tools=[calculate, get_weather],
                     system_prompt="你是一个多功能的助手,可以查询天气和进行数学计算,回答要友好且准确",
                     checkpointer=None)

# 3.测试多工具调用
user_queries = [
    "北京的深圳的天气如何?",
    "如果北京的气温为25°C,上海的气温是29°C,那么北京的温度比上海低多少度?",
]

for query in user_queries:
    result = agent.invoke({"messages": [{"role": "user", "content": query}]})
    print(result["messages"][-1].content)
    print("-" * 50)

image

查看运行流程

import os
import sys
sys.stdout.reconfigure(encoding='utf-8')

import dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.text import Text


@tool
def calculate(expression: str) -> str:
    """计算一个数学表达式的结果"""
    try:
        result = eval(expression)
        return f"计算结果是:{result}"
    except Exception as e:
        return f"计算出错: {e}"


@tool
def get_weather(location: str) -> str:
    """获取天气信息"""
    weather_data = {
        "北京": "晴天,温度是 25°C",
        "上海": "阴天,温度是 29°C",
        "广州": "雨天,温度是 20°C",
        "深圳": "雷阵雨,温度是 18°C"
    }

    return f"天气信息: {location} 的天气是: {weather_data.get(location, '未知地点')}"


dotenv.load_dotenv()
# 1. 初始化大模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 2.创建agent
agent = create_agent(model=llm, tools=[calculate, get_weather],
                     system_prompt="你是一个多功能的助手,可以查询天气和进行数学计算,回答要友好且准确",
                     checkpointer=None)

# # 3.测试多工具调用
# user_queries = [
#     "北京的深圳的天气如何?",
#     "如果北京的气温为25°C,上海的气温是29°C,那么北京的温度比上海低多少度?",
# ]
#
# for query in user_queries:
#     result = agent.invoke({"messages": [{"role": "user", "content": query}]})
#     print(result["messages"][-1].content)
#     print("-" * 50)

def run_demo_with_visualization(user_input: str):
    """
    运行示例,并显示ReAct过程
    """
    console = Console()

    console.print("\n" + "="*50)
    console.print(f"[bold yellow]开始任务:{user_input}")

    messages = [HumanMessage(content=user_input)]

    # graph.stream 是LangGraph的核心,用于显示ReAct过程,可以看到状态、工具调用、工具结果、最终结果
    step_count = 1

    for event in agent.stream({"messages": messages}, stream_mode="values"):
        # 获取最新的一条消息
        current_message = event["messages"][-1]

        # 1.如果是人类的消息(初始状态),则忽略,因为这是输入
        if isinstance(current_message, HumanMessage):
            continue

        # 2.如果是AI的消息(思考决策)
        if isinstance(current_message, AIMessage):
            # 检查是否有工具调用
            if current_message.tool_calls:
                # 提取工具调用的细节
                for tool_call in current_message.tool_calls:
                    console.print(
                        Panel(
                            Text(
                                f"AI思考决定:需要调用外部工具\n"
                                f"工具名称:{tool_call['name']}\n"
                                f"输入参数:{tool_call['args']}", style="bold cyan"
                            ),
                            title=f"Step {step_count}: 决策(Decision)",
                            border_style="cyan",
                        )
                    )
            else:
                # 如果没有工具调用,说明地最终回复
                console.print(Panel(
                    Markdown(current_message.content),
                    title=f"Step {step_count}: 回复(Reply)",
                    border_style="green"
                ))
            step_count += 1

        # 3.如果是工具的消息(观察与结果)
        if isinstance(current_message, ToolMessage):
            console.print(Panel(
                Text(f"工具返回结果(Observation): \n{current_message.content}", style="italic white"),
                title=f"Step {step_count}: 执行与观察",
                border_style="magenta"
            ))
            step_count += 1

if __name__ == "__main__":
    run_demo_with_visualization("查询一下北京和上海的气温,并且计算一下北京比上海低多少度?")

image

5.6 工具调用错误或者乱调情况

5.6.1 使用Tool Router(最有效)

Tool Router是解决工具调用混乱的最有效方法,它通过专门的工具路由机制来精确匹配用户意图与可用工具。
技术实现:

  • 意图识别:使用专门的分类器识别用户意图
  • 工具匹配:基于意图选择最合适的工具
  • 参数验证:在调用前验证参数有效性
  • 错误处理:提供优雅的降级策略
# 定义意图分类系统提示
INTENT_SYSTEM_PROMPT = """
你是一个专业的意图分类器,请返回以下类别之一:
- search
- pdf
- database
- math
- none
并严格只返回类别名,不要输出其他内容。
"""

5.6.2 引入"意图分类模型"(工程最佳方案)

意图分类模型通过机器学习方法识别用户请求的真实意图,从根本上解决工具误用问题。
技术优势:

  • 高精度识别:基于大量训练数据的准确意图识别
  • 动态适应:能够适应新的用户表达方式
  • 多维度分析:综合考虑语义、上下文、用户历史等因素
dotenv.load_dotenv()
# 1. 初始化大模型,创建一个意图识别模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

5.6.3 动态加载工具(避免上下文过长)

动态工具加载机制根据当前对话上下文和用户意图,按需加载相关工具,避免一次性加载所有工具导致的上下文过长问题。

  • 模型根据"意图"动态读取特定工具,不把所有工具一次性喂给模型。

实现策略plain:

  • tools/
  • search.yaml
  • finance.yaml
  • pdf.yaml
# 通过Tool工具分组
TOOL_GROUPS = {
    "search": [search_web],
    "pdf": [extract_pdf_text],
    "database": [query_database],
    "math": [calculate]
}

5.6.4 统一工具规范(提高准确率)

通过强制化Schema和规范化提示词,建立统一的工具使用规范。
规范要求:

  • 工具名称必须动词开头
  • 每个工具使用标准化schema
  • 工具描述必须包含三件事:能干什么、不能干什么、典型输入示例
@tool
def query_database(sql: str) -> str:
    """
    执行SQL查询数据库,仅限内部业务数据库
    参数:SQL语句
    示例:select * from user limit 5
    """
    return f"模拟 SQL 执行:{sql}"

5.6.5 采用"工具过滤Prompt"修饰模型行为(成本最低)

通过系统Prompt显式指导模型行为,设置工具使用边界。
Prompt示例:

  • 你必须严格根据工具描述选择工具。 不能猜测工具功能。 如果没有合适的工具,请回答"无合适工具"。
from langchain.agents import create_agent

agent = create_agent(
    model=model,
    tools=tools,
    system_prompt="你是一个助手,可以使用工具回答问题。你必须严格根据工具描述选择工具!如果没有合适的工具,请回答‘无合适工具’"
)

5.6.6 层次化/多级Agent架构

通过层次化Agent架构降低单个Agent的工具复杂度,提高系统稳定性。
架构优势:

  • 模块化设计:每个Agent专注于特定领域
  • 降低复杂度:单个Agent工具数量可控
  • 提高稳定性:错误隔离和容错能力更强
import os

import dotenv
from langchain.chat_models import init_chat_model
from langchain_core.tools import tool
from langchain.agents import create_agent


@tool
def search_web(query: str) -> str:
    """
    Web搜索工具,用于查询网络公开信息,不适用用于内部数据。
    :param query:用户查询,如:美加墨世界杯比赛信息
    """
    return f"模拟搜索结果:你搜索了{query}"


@tool
def extract_pdf_text(pdf_path: str) -> str:
    """
    解析PDF,从PDF文件中提取文本,
    参数:PDF文件路径
    示例:/data/pdf/2023-01-01.pdf
    """
    return f"模拟 PDF 提取:{pdf_path} 中解析出来的内容"


@tool
def query_database(sql: str) -> str:
    """
    执行SQL查询数据库,仅限内部业务数据库
    参数:SQL语句
    示例:select * from user limit 5
    """
    return f"模拟 SQL 执行:{sql}"


@tool
def calculate(expression: str) -> str:
    """
    计算一个数学表达式的结果
    参数:数学表达式
    示例:2+2
    """
    try:
        result = eval(expression)
        return f"计算结果是:{result}"
    except Exception as e:
        return f"计算出错:{e}"


# 1.通过Tool工具分组
TOOL_GROUPS = {
    "search": [search_web],
    "pdf": [extract_pdf_text],
    "database": [query_database],
    "math": [calculate]
}

# 2.创建一个意图识别模型
dotenv.load_dotenv()
# 1. 初始化大模型,创建一个意图识别模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 3.定义意图分类系统提示
INTENT_SYSTEM_PROMPT = """
你是一个专业的意图分类器,请返回以下类别之一:
- search
- pdf
- database
- math
- none

并严格只返回类别名,不要输出其他内容。
"""


# 4.定义意图分类函数
def classify_intent(query: str) -> str:
    """
    意图分类函数,返回意图类别
    """
    # 调用意图识别模型
    result = llm.invoke([
        ("system", INTENT_SYSTEM_PROMPT),
        ("user", query)
    ])

    return result.content.strip()


# 5.创建一个多工具代理智能体
def create_multi_tool_agent(group: str):
    """
    创建一个多工具代理智能体
    """
    # 获取工具
    tools = TOOL_GROUPS.get(group, [])

    # 如果工具为空,则返回None
    if not tools:
        return None

    return create_agent(
        model=llm,
        tools=tools,
        system_prompt="你是一个助手,可以使用工具回答问题。你必须严格根据工具描述选择工具!如果没有合适的工具,请回答‘无合适工具’",
    )


# 6.路由函数智能体
def route_agent(query: str) -> str:
    """
    路由函数智能体
    """
    # 1.识别意图
    intent = classify_intent(query)
    print(f"[Router]检测到意图:{intent}")

    # 2.创建对应的子Agent
    sub_agent = create_multi_tool_agent(intent)

    if sub_agent is None:
        return "无法为该问题找到合适的工具或者Agent"

    # 3.调用子Agent执行任务
    return sub_agent.invoke({"messages": [{"role": "user", "content": query}]})


if __name__ == "__main__":
    # 执行后输出:[Router]检测到意图:search
    # print(route_agent("请你帮我搜索一个美加墨世界杯比赛信息")['messages'][-1].content)

    # 7.测试智能体
    queries = [
        "请你帮我搜索一个美加墨世界杯比赛信息",
        "请你帮我解析一个PDF文件: /root/data/pdf/2026-07-07.pdf",
        "请你帮我执行SQL查询数据库:select * from dep limit 10",
        "请你帮我计算一个数学表达式:(1+5)*(9-5)",
    ]

    for query in queries:
        print("\n===========================用户问题===========================")
        print(query)
        print("=============================智能回复===========================")
        print(route_agent(query)["messages"][-1].content)

image

六、System Prompt 系统提示词

system_prompt 是 create_agent 中定义 Agent 角色、行为准则、输出格式和约束 的核心参数,相当于 Agent 的"人格说明书"。LangChain 1.0 将其设计为唯一的顶层提示词入口。LangChain 1.0 不支持在 system_prompt 中直接嵌入 {variable} 占位符(这是旧版 PromptTemplate 的做法)。如需动态内容,应使用 dynamic_prompt 中间件。

system_prompt 在 ReAct 循环中的位置:

System Prompt (固定前缀)
                ↓
用户输入 → 模型推理 (Thought) → 工具调用 (Action) → 观察结果 (Observation)
                ↓
循环直到满足终止条件 → 最终回答

通过精心设计的提示词,您可以:

  • 定义角色:从客服到专家,从教师到顾问
  • 约束输出:控制长度、格式、语言
  • 引导工具:强制或可选使用工具
  • 保障安全:防止数据泄露和违规操作
  • 实现个性化:通过动态提示支持多租户
  • 记住:在 LangChain 1.0 中,system_prompt 的设计质量直接决定了 Agent 的表现上限。投入时间打磨提示词,远比调整模型参数更有效。

案例代码:

import os
from typing import TypedDict
from langchain.agents.middleware import dynamic_prompt
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent
import dotenv


# 1.定义天气查询工具
@tool
def get_weather(location: str) -> str:
    """获取天气信息"""
    weather_data = {
        "北京": "晴天,温度是 25°C",
        "上海": "阴天,温度是 29°C",
        "广州": "雨天,温度是 20°C",
        "深圳": "雷阵雨,温度是 18°C"
    }

    return f"天气信息: {location} 的天气是: {weather_data.get(location, '未知地点')}"

dotenv.load_dotenv()
# 2.创建智能体,静态 system_prompt 是固定不变的
# 1. 初始化大模型,创建一个意图识别模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

agent = create_agent(
    model=llm,
    tools=[get_weather],
    system_prompt=(
        "你是一个天气助手,回答不超过20字。\n"
        "调用工具时,严格按照一下格式:\n"
        "1.使用 'get_weather(location: str)' 获取天气:\n"
        "2.仅仅返回天气结果,不解释过程"
    )
)

print("=================== 静态 System Prompt==================")
resp = agent.invoke({"messages": [{"role": "user", "content": "北京的天气如何?"}]})
print(resp["messages"][-1].content)


# 4.定义上下文结构
class Context(TypedDict):
    # 用户角色
    user_role: str

# 5.动态提示函数
@dynamic_prompt
def role_based_prompt(request) -> str:
    """根据用户角色生成不同的提示词"""

    user_role = request.runtime.context.get("user_role", "user")

    if user_role == "expert":
        return "你是一个专业气象分析师,提供详细数据"
    elif user_role == "beginner":
        return "你是一个友善的导游,用简单语言解释"
    else:
        return "你是一个简洁的天气助手"


# 6.创建动态Agent
agent_dynamic = create_agent(
    model=llm,
    tools=[get_weather],
    middleware=[role_based_prompt],  # 注入动态提示词
    context_schema=Context  # 指定上下文结构
)

print("\n=================== 动态 System Prompt (专家角色)==================")
resp2 = agent_dynamic.invoke(
    {"messages": [{"role": "user", "content": "北京的天气如何?"}]},
    context={"user_role": "expert"},
)
print(resp2["messages"][-1].content)

print("\n=================== 动态 System Prompt (新手角色)==================")
resp3 = agent_dynamic.invoke(
    {"messages": [{"role": "user", "content": "北京的天气"}]},
    context={"user_role": "beginner"},
)
print(resp3["messages"][-1].content)

image

七、流式输出

stream_mode 模式的对比:

模式输出内容使用场景优点缺点
"values" 每步后的完整状态 调试Agent执行流程 • 状态完整,可追溯
• 无需拼接历史
数据量大(重复传输)
"updates" 仅状态变更部分 前端增量更新UI 数据量小,传输快 需手动维护完整状态
"messages" LLM生成的token流 实时显示打字效果 响应即时,用户体验好 不包含工具调用信息
"custom" 工具函数自定义输出 插入业务日志 灵活控制输出内容 需手动调用stream writer
import os

import dotenv
from langchain.tools import tool
from langchain.chat_models import init_chat_model
from langchain.agents import create_agent

# 1. 定义天气查询工具
@tool
def get_weather(city: str) -> str:
    """获取指定城市的天气信息。"""
    weather_data = {
        "北京": "晴朗,气温25°C",
        "上海": "多云,气温28°C",
        "广州": "小雨,气温30°C"
    }
    return f"{city}的天气是:{weather_data.get(city, '未知')}"

# 2. 定义数学计算工具
@tool
def calculate(expression: str) -> str:
    """计算一个数学表达式的结果。"""
    try:
        result = eval(expression)
        return f"计算结果是:{result}"
    except Exception as e:
        return f"计算出错:{str(e)}"

dotenv.load_dotenv()

# 2.创建智能体
# 2.1.初始化大模型
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 2.2.创建智能体
agent = create_agent(
    model=llm,
    tools=[get_weather, calculate],
    system_prompt=("""
        你是一个多功能的 AI 助手,能够调用以下工具:
        1. `get_weather(city)`:查询指定城市的天气信息。参数 city 为城市名称(如“北京”)。
        2. `calculate(expression)`:计算数学表达式。参数 expression 为合法的 Python 表达式(如“21 - 28”)。
        请始终遵循以下最佳实践:
        • 当用户询问天气时,先提取城市名,再调用 `get_weather`,并返回自然语言总结。
        • 当用户需要计算时,先提取表达式,再调用 `calculate`,并给出易读的结果说明。
        • 若问题同时涉及天气与计算,按顺序依次调用对应工具,最后整合答案。
        • 禁止编造数据,必须调用工具获取结果后再回答。
        • 所有数字、单位、符号务必与工具返回保持一致,避免主观臆断。
    """
    )
)

# 测试多工具调用
user_queries = [
    "北京和上海的天气怎么样?",
    "如果上海的气温为28°C,北京的气温是25°C,那么上海的气温比北京高多少度?"
]

# 配置会话ID
config = {
    'configurable': {'thread_id': 'user_001'}
}

# 流式输出,实时观察推理过程
for step in agent.stream(
        {"messages": [{"role": "user", "content": "北京和上海的天气怎么样?"}]},
        config=config,
        stream_mode="values"  # 返回每个step步骤的完整消息列表,便于调试和观察
    ):
    # 获取最新消息并格式化
    message = step["messages"][-1]
    message.pretty_print()
    print("-" * 50)

image

常见误区与注意事项:

误区1:stream_mode="values" 会流式返回 LLM token

  • 真相:它返回的是步骤级的完整状态,不是字符级token。想看token需用 stream_mode="messages"

误区2:values 和 updates 返回数据量差不多

  • 真相:values 在每一步都返回所有历史消息,数据量线性增长;updates 只返回增量,适合网络传输

误区3:可以混用多种 stream_mode

  • 真相:可以同时指定多个模式(如 stream_mode=["values", "custom"]),但返回的是元组,需分别处理

八、LangSmith

LangSmith 是一个用于调试、测试、评估和监控 LLM(大语言模型)应用程序的统一平台。 它由开发流行框架 LangChain 的公司打造,但它被设计用于任何 LLM 应用程序,而不仅仅是那些用 LangChain 构建的程序。

可以把它看作是 AI 应用开发生命周期的“开发者工具包”。就像软件开发者使用 GitHub集成开发环境调试器一样,LangSmith 为构建 LLM 应用所面临的独特挑战提供了类似的能力。

安装 langgraph 的 python 依赖

pip install langgraph "langchain[openai]" "langgraph-cli[inmem]"

8.1 langgraph_agent

8.1.1 创建一个目录,并创建如下文件

langgraph_agent/
├── test_agent.py            # test_agent 代码
└── langgraph.json      # langgraph 配置文件

8.1.2 agent.py实现agent的创建

from langchain.agents import create_agent
from langchain_core.messages import ToolMessage
from langchain_ollama import ChatOllama

from nexus_rag.llm_functions import get_weather

# 1.初始化大模型
llm = ChatOllama(
    base_url="http://127.0.0.1:11434",
    model="qwen3:8b"
)

# 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具, 这里导入自定义查询天气的工具
tools = [get_weather]

# 3.创建agent
agent = create_agent(model=llm, tools=tools, system_prompt="你是一个智能助手,请根据用户输入的指令,进行相应的查询。")

# 4.调用agent
res = agent.invoke({"input": "查询北京今天天气"})
print(res['messages'][-1].content)
#
# chunks = agent.stream({"input": "查询北京今天天气"})
# for chunk in chunks:
#     # 检查是否有工具调用结果
#     if "tools" in chunk:
#         for msg in chunk["tools"]["messages"]:
#             if isinstance(msg, ToolMessage): # 判断是否是工具调用结果,是则打印出来,ToolMessage表示工具调用结果
#                 print(msg.content, end="", flush=True)

8.1.3 langgraph.json

{
  "dependencies": ["."],
  "graphs": {
    "agent": "./test_agent.py:agent"
  },
  "env": {},
  "version": "0.0.1"
}

注意:json文件中agent key对应的值,需要写你的agent代码文件的名称和里面agent的名称

image

8.1.4 运行

Windows (CMD)执行命令

cd F:\NexusKnow
set PYTHONPATH=F:\NexusKnow
langgraph dev

Linux下,只需要执行:

# 进入项目路径,执行
langgraph dev

启动:

image

8.2 在 LangSmith 中调试

如果用过 Coze 或 Dify 等工具的,那看见这个界面后会很亲切~~~

8.2.1 调试

image

8.2.2 点击  chat 就是对话页面

image

8.3 远程访问 LangSmith

之前看到的地址类似 https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024,其中baseUrl都是127.0.0.1这种内网ip,这种地址其他人是访问不到的。

image

8.3.1 运行远程访问启动

要想其他人访问,执行如下命令:

langgraph dev --tunnel

在启动台查看,就变成了类似这种 

https://smith.langchain.com/studio/?baseUrl=https://carter-followed-fibre-gif.trycloudflare.com

如图

image

其他人就可以访问这个地址了。但是其他人直接访问会提示错误:

Failed to initialize Studio
Please verify if the APl server is running or accessible from the browser.
TypeError: Failed to fetch

8.3.2 获取公网IP添加到hosts

只时候,只需要在打开一个命令窗口:

Linux下执行:

注意这个就是上面中返回地址中代表的那一段:carter-followed-fibre-gif.trycloudflare.com

image

dig carter-followed-fibre-gif.trycloudflare.com

Windows下执行 :

nslookup carter-followed-fibre-gif.trycloudflare.com

image

任意选择一个IP地址,在hosts文件中添加:

104.16.231.132          carter-followed-fibre-gif.trycloudflare.com

然后就可以愉快的使用了

九、Agent记忆管理

LangChain 1.0的记忆管理与LangGraph的状态机制深度绑定,在 LangGraph 中,记忆就是"持久化的状态(Persisted State)"。
需要掌握三个核心要素:

  • State (状态): 定义用来存储消息的结构(通常是 MessagesState)。
  • Checkpointer (检查点保存器): 负责在每一步结束后把状态保存下来(短期记忆通常用 MemorySaver)。
  • Thread ID (线程ID): 在调用时通过 config 传入,用来隔离不同用户的对话上下文。

短期 vs 长期记忆的分界标准

  • 误区:存储介质 = 记忆类型?

错误认知:

  • “内存 = 短期记忆”
  • “数据库 = 长期记忆”

正确标准:

  • 短期记忆:数据与会话(thread进程)生命周期绑定,随会话结束而被清理或遗忘
  • 长期记忆:数据与用户/业务实体生命周期绑定,跨会话持久保留并可主动检索

9.1 短期记忆管理

短期记忆通过LangGraph的AgentState(一个TypedDict)来管理。对话历史、中间步骤等信息被保存在状态中,并通过检查点(Checkpoints)机制在每次迭代后持久化。这使得长对话和失败恢复成为可能。

9.1.1.Checkpointer机制

Checkpointer机制是 LangGraph 记忆的灵魂。

  • 不加这一行:Agent 是无状态的。每次 invoke 都是全新的开始。
  • 加上这一行:LangGraph 会在每一步执行后,把 state 序列化并存入 MemorySaver。

原理:当你再次 invoke 并传入 thread_id 时,LangGraph 会先去内存里查"这个 ID 上次停在哪里?状态是什么?",然后加载状态,把你的新消息 append 进去,再继续运行。

9.1.2.Thread ID配置

Thread ID是短期记忆的"钥匙"。

  • 在 Web 开发中,这就是 Session ID。
  • 你需要为每个用户或每个会话生成一个唯一的 ID。
  • 不同的 ID 之间内存是完全隔离的(如代码中 `session_user_123` 和 `session_user_999` 的区别)。

9.1.3.InMemorySaver() 内存记忆管理

InMemorySaver()结合Thread ID 实现短期记忆管理:

import os
import dotenv
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent

# 1.初始化大模型
dotenv.load_dotenv()
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 2.定义工具函数
@tool
def get_user_info(name: str) -> str:
    """
    获取用户信息,返回:姓名、年龄、爱好
    :param name: 姓名
    :return: 姓名、年龄、爱好
    """
    user_data = {
        "张三": {"name": "张三", "age": 25, "hobby": "电影、滑雪、游戏"},
        "李四": {"name": "李四", "age": 30, "hobby": "阅读、Drawing、Play"},
        "王五": {"name": "王五", "age": 28, "hobby": "音乐、Drawing、Play"},
        "张无忌": {"name": "张无忌", "age": 30, "hobby": "泡妞、练功"}
    }

    info = user_data.get(name, {"name":name, "age": '未知', "hobby": "未知"})

    return f"用户信息:姓名:{info['name']},年龄:{info['age']},爱好:{info['hobby']}"

# 3.基础短期记忆:InMemorySaver
"""
开发环境使用内存存储,重启后记忆丢失
关键参数:
- checkpointer: 记忆存储对象
- thread_id: 会话唯一标识(用户级别隔离)
"""
def run_demo_with_short_term_memory():
    print("=" * 60)
    print("场景1:内存记忆(开发环境)")
    print("=" * 60)

    # 创建内存检查点
    memory = InMemorySaver()

    # 创建Agent(自动继承对话记忆能力)
    agent = create_agent(
        model=llm,
        tools=[get_user_info],
        checkpointer=memory,  # 启动短期记忆添加内存检查点
    )

    # 配置thread_id作为会话ID
    config = {
        'configurable': {'thread_id': 'user_001'}
    }

    # 第一轮对话:自我介绍
    resp1 = agent.invoke(
        {"messages": [{"role": "user", "content": "你好,我是张无忌,好久不见。"}]},
        config=config,
    )

    print(f"用户:你好,我是张无忌,好久不见。")
    print(f"AI助手回复:{resp1['messages'][-1].content}")
    print("-" * 40)

    # 第二轮对话:测试记忆
    resp2 = agent.invoke(
        {"messages": [{"role": "user", "content": "请问你还记得我的名字吗?"}]},
        config=config,  # 使用相同的thread_id,会自动携带上下文
    )

    print(f"用户:请问你还记得我的名字吗?")
    print(f"AI助手回复:{resp2['messages'][-1].content}")
    print("-" * 40)

    # 验证记忆状态
    state = agent.get_state(config=config)
    print(f"当前记忆轮次:{len(state.values['messages'])}条消息")

    # 新开一个会话(不同的thread_id)
    config2 = {
        'configurable': {'thread_id': 'user_002'}
    }
    resp3 = agent.invoke(
        {"messages": [{"role": "user", "content": "我们之前有过聊天吗?"}]},
        config=config2,
    )
    print(f"新开一个会话测试记忆状态,用户:我们之前有过聊天吗?")
    print(f"AI助手回复:{resp3['messages'][-1].content}")
    print("-" * 40)


if __name__ == '__main__':
    run_demo_with_short_term_memory()

image

9.1.4.PostgresSaver() 数据库持久化记忆

PostgresSaver 即使存储到数据库,仍然属于短期记忆。仍然属于短期记忆的原因:

  • 作用域限制:它只检索和加载当前 thread_id 的数据
  • 生命周期管理:默认不会主动清理,但数据语义上属于"本次会话"
  • 无跨会话检索能力:无法在新会话中自动访问旧会话数据(除非手动指定旧 thread_id)

安装依赖:

pip install langgraph-checkpoint-postgres

docker安装

docker run -d \
  --name my-postgres \
  -e POSTGRES_PASSWORD=123456 \
  -e POSTGRES_USER=myuser \
  -e POSTGRES_DB=mydb \
  -p 5432:5432 \
  -v postgres_data:/var/lib/postgresql \
  postgres

案例代码:

import os
import dotenv
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver

# 1.初始化大模型
dotenv.load_dotenv()
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)

# 2.定义工具函数
@tool
def get_user_info(name: str) -> str:
    """
    获取用户信息,返回:姓名、年龄、爱好
    仅在需要查询已有用户信息时调用,如果用户已经自述了信息则不需要调用。
    :param name: 姓名
    :return: 姓名、年龄、爱好
    """
    user_data = {
        "张三": {"name": "张三", "age": 25, "hobby": "电影、滑雪、游戏"},
        "李四": {"name": "李四", "age": 30, "hobby": "阅读、Drawing、Play"},
        "王五": {"name": "王五", "age": 28, "hobby": "音乐、Drawing、Play"},
        "张无忌": {"name": "张无忌", "age": 30, "hobby": "泡妞、练功"}
    }

    # 修复: 默认字典必须包含 'name' 键, 防止 KeyError 导致 tool_calls 序列不完整
    info = user_data.get(name, {"name": name, "age": "未知", "hobby": "未知"})

    return f"用户信息:姓名:{info['name']},年龄:{info['age']},爱好:{info['hobby']}"


# 数据库连接字符串
db_url = "postgresql://myuser:123456@192.168.163.240:5432/mydb"

# 使用上下文管理器确保连接正确关闭
with PostgresSaver.from_conn_string(db_url) as checkpointer:
    # 自动创建表结构(仅首次运行)
    checkpointer.setup()

    # 使用 ReAct Agent (支持工具调用 + 检查点记忆)
    agent = create_agent(
        model=llm,
        tools=[get_user_info],
        checkpointer=checkpointer
    )

    # 换一个新的 thread_id,避免旧状态污染 (残留的 tool_calls 序列)
    config = {
        'configurable': {'thread_id': 'user_010'}
    }

    print("=" * 60)
    print("测试开始:PostgreSQL 持久化记忆")
    print("=" * 60)

    # 第一轮对话:用户自我介绍
    print("\n[第1轮] 用户自我介绍...")
    resp0 = agent.invoke(
        {"messages": [{"role": "user", "content": "我叫黄子墨,今年25岁,爱好电影、滑雪、游戏。请你记录我的信息"}]},
        config=config,
    )
    print(f"Agent: {resp0['messages'][-1].content}")

    # 第二轮对话:测试记忆
    print("\n[第2轮] 测试记忆...")
    resp1 = agent.invoke(
        {"messages": [{"role": "user", "content": "请你说明我是谁?"}]},
        config=config,
    )
    print(f"Agent: {resp1['messages'][-1].content}")

    # 第三轮对话:测试工具调用
    print("\n[第3轮] 测试查询其他用户...")
    resp2 = agent.invoke(
        {"messages": [{"role": "user", "content": "张无忌的爱好是什么?"}]},
        config=config,
    )
    print(f"Agent: {resp2['messages'][-1].content}")

    print("\n" + "=" * 60)
    print("测试完成")
    print("=" * 60)

输出:

image

查询数据库,获取报错的状态信息:

image

9.2 上下文裁剪

除了上一章节提到的短期记忆之外,真正的记忆管理还涉及"上下文窗口控制"(防止对话太长撑爆 Token),这需要配合 trim_messages 使用。

  • 问题:如果不处理,随着对话进行,state["messages"] 会包含几千条消息。直接全部传给 LLM 会导致:1. 烧钱;2. 超过 128k/8k 限制报错。
  • 解决:我们在 call_model 内部使用了 trimmer。
    • State 中:依然保存了 100% 的完整历史(为了审计或回溯)。
    • 传给 LLM 时:只传最近的 N 个 Token(或 N 条消息)。
  • start_on=“human”: 这是一个很细节的最佳实践。如果截断导致第一条消息是 AI 的回复(没有对应的 User 问题),某些模型会感到困惑。这个参数确保截断后的对话总是以 User 开始。
import os
import dotenv
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
from langchain_core.messages import trim_messages, HumanMessage

# 1.初始化大模型
dotenv.load_dotenv()
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)


# 1.定义天气查询工具
@tool
def get_weather(city: str) -> str:
    """
    获取天气信息
    :param city: 城市名称
    :return: 天气信息
    """
    return f"{city}的天气是晴朗,气温25°C"


# 配置裁剪参数
MAX_TOKENS = 200  # 模型最大token,
TRIM_STRATEGY = "last"  # 保留最新消息
INCLUDE_SYSTEM = True  # 系统消息不参与裁剪


import tiktoken
"""
tiktoken是OpenAI官方token编码库,支持精确计算。
不同模型需使用不同编码:
    - gpt-4o, gpt-4o-mini: o200k_base
    - gpt-4-turbo: cl100k_base
    - text-davinci-003: p50k_base
"""

# 2.定义Token编码器获取函数
def get_token_encoder(model_name: str="deepseek-v4-pro") -> tiktoken.Encoding:
    """
    获取tiktoken编码器实例
    :param model_name: 模型名称
    :return:
    """
    try:
        encoding = tiktoken.encoding_for_model(model_name)
        print(f"使用模型:{model_name},编码器:{encoding.name}")
        return encoding
    except KeyError:
        print(f"模型{model_name}未找到,使用默认编码器:o200k_base")
        return tiktoken.get_encoding("o200k_base")

# 3.初始化编码器(全局复用提升性能)
TOKEN_ENCODER = get_token_encoder("deepseek-v4-pro")


# 4.精确计算token函数
def count_tokens_tiktoken(messages) -> int:
    """
    使用tiktoken精确计算消息列表的token总数
    :param messages: 消息对象列表
    :return: 总token数
    """

    total_tokens = 0

    # 遍历每条消息,累加 token 数量
    for message in messages:
        # 消息格式:role + content + 特殊标记
        # 典型格式: <|im_start|>role<|im_end|>content<|im_end|>

        # 角色 token (如: "user", "assistant", "system")
        role_tokens = len(TOKEN_ENCODER.encode(message.type))

        # 内容 token (如:"消息正文")
        content_tokens = len(TOKEN_ENCODER.encode(message.content))

        # 格式开销 (如:OpenAI消息边界标记)
        # 每条约4个特殊的token(开始、角色、结束、内容结束)
        format_tokens = 4

        total_tokens += role_tokens + content_tokens + format_tokens

    return total_tokens


# 创建Agent
def create_trimmed_agent():
    """
    创建Agent并配置InMemorySaver
    :return:
    """
    memory = InMemorySaver()

    agent = create_agent(
        model=llm,
        tools=[get_weather],
        system_prompt="你是一个天气查询助手,请根据用户指令进行回答。",
        checkpointer=memory
    )

    return agent

# 手动裁剪并调用Agent
def invoke_with_trim(agent, user_input: str, config: dict):
    """
    在调用Agent前手动裁剪上下文
    流程:
    1. 获取当前状态(所有历史消息)
    2. 使用trim_messages裁剪
    3. 构建新输入(裁剪后的消息 + 新消息)
    4. 调用Agent
    :param agent:
    :param user_input:
    :param config:
    :return:
    """

    # 1.获取当前记忆状态
    state = agent.get_state(config=config)
    existing_messages = state.values.get("messages", []) if state else []

    # 精确计算当前 token 数
    current_tokens = count_tokens_tiktoken(existing_messages)

    # 2.裁剪消息(如果有历史消息,先裁剪)
    if existing_messages:
        print(f"裁剪前消息数:{len(existing_messages)}")

        # 核心:调用 trim_messages 进行裁剪
        trimmed_messages = trim_messages(
            existing_messages,  # 待裁剪的消息列表
            max_tokens=MAX_TOKENS,  # 允许的最大 token 数,超过此值则进行裁剪
            token_counter=count_tokens_tiktoken,  # 自定义的 token 计数函数
            strategy=TRIM_STRATEGY,  # 裁剪策略, last 表示只保留最新消息, first 表示只保留最早消息
            include_system=INCLUDE_SYSTEM,  # 布尔值,表示裁剪是否包含系统消息(通常需要保留)
            allow_partial=False,  # False不允许部分消息被裁剪,会尝试保留消息的完整性。如果无法在保持消息完整性的前提下将总token数裁剪到参数 max_tokens 内,就会返回空列表。True允许部分消息被裁剪
            start_on="human"  # 从human消息开始裁剪
        )

        # 计算裁剪后的token数
        new_tokes = count_tokens_tiktoken(trimmed_messages)

        print(f"裁剪后 token:{new_tokes},裁剪后消息数:{len(trimmed_messages)}")

    else:
        trimmed_messages = []

    # 3.添加新消息
    new_message = trimmed_messages + [HumanMessage(content=user_input)]

    # 4.调用Agent(checkpointer会自动保存新状态)
    resp = agent.invoke(
        {"messages": new_message},
        config=config
    )

    return resp


# 演示多轮对话和裁剪
def demo_manual_trim():
    print("=" * 60)
    print("场景2:手动裁剪 trim_messages + InMemorySaver")
    print("=" * 60)

    agent = create_trimmed_agent()
    config = {
        'configurable': {'thread_id': 'user_001'}
    }

    # 模拟多轮对话
    conversations = [
        "你好,我是张无忌,好久不见。",
        "帮我查询一下北京的天气",
        "上海呢?",
        "明天北京天气怎么样?",  # 此时会触发裁剪
        "我是谁?",  # 测试记忆是否保留
    ]

    for i,query in enumerate(conversations):
        print(f"第{i+1}轮对话:{query}")

        # 每次调用前自动裁剪
        resp = invoke_with_trim(agent, query, config)

        print(f"AI: {resp['messages'][-1].content}")
        print("-" * 40)


if __name__ == '__main__':
    demo_manual_trim()

image

state = agent.get_state(config=config)
existing_messages = state.values.get("messages", []) if state else []

image

# 核心:调用 trim_messages 进行裁剪
        trimmed_messages = trim_messages(
            existing_messages,  # 待裁剪的消息列表
            max_tokens=MAX_TOKENS,  # 允许的最大 token 数,超过此值则进行裁剪
            token_counter=count_tokens_tiktoken,  # 自定义的 token 计数函数
            strategy=TRIM_STRATEGY,  # 裁剪策略, last 表示只保留最新消息, first 表示只保留最早消息
            include_system=INCLUDE_SYSTEM,  # 布尔值,表示裁剪是否包含系统消息(通常需要保留)
            allow_partial=False,  # False不允许部分消息被裁剪,会尝试保留消息的完整性。如果无法在保持消息完整性的前提下将总token数裁剪到参数 max_tokens 内,就会返回空列表。True允许部分消息被裁剪
            start_on="human"  # 从human消息开始裁剪
        )

trim_messages 是 LangChain 提供的一个消息裁剪工具,作用是当消息列表的总 token 超过限制时,自动切除多余的消息。

① messages — 待裁剪的消息列表

existing_messages = [
    SystemMessage("你是助手"),
    HumanMessage("你好,我是张无忌"),
    AIMessage("你好张无忌!"),
    HumanMessage("北京天气?"),
    AIMessage("北京晴天,25°C"),
    HumanMessage("上海呢?"),
    AIMessage("上海多云,22°C"),
    HumanMessage("明天北京天气?"), # ← 新消息
]

这就是从 InMemorySaver 中取出来的完整对话历史。

② max_tokens — 硬上限

裁剪后,剩余消息的 token 总数必须 ≤ 这个值。代码里设为 200,意思是"我最多只要 200 token 的上下文"。

③ token_counter — 怎么计算 token

token_counter=count_tokens_tiktoken

传入你自定义的计数函数。trim_messages 每裁剪一条消息就会调用这个函数重新算一遍,确保不超限。

▎ 如果不传,默认用 len(messages) 粗略估算,你用 tiktoken 会更精确。

④ strategy — 保留哪一头

只有两个可选值:

┌─────────┬──────────────────────────┬────────────────────────────────────┐
│ 值      │              行为         │               图解                  │
├─────────┼──────────────────────────┼────────────────────────────────────┤
│ "last"  │ 保留最新的,从前面开始删     │ [msg1, msg2, ✂️, msg6, msg7, msg8] │
├─────────┼──────────────────────────┼────────────────────────────────────┤
│ "first" │ 保留最早的,从后面开始删     │ [msg1, msg2, msg3, ✂️, msg7, msg8] │
└─────────┴──────────────────────────┴────────────────────────────────────┘

代码用 "last",因为对话中最近的消息最重要(前面说的"我叫张无忌"可能早就被裁掉了)。

⑤ include_system — 系统消息是否参与裁剪

  • include_system=True # 系统消息也算在 200 token 的额度里
  • include_system=False # 系统消息不占额度,必定保留

设为 True 意思是:系统提示词("你是一个天气助手…")也和普通消息一样被计入 200 token。如果系统提示词本身就很大,那留给对话历史的额度就更少。

⑥ allow_partial — 能不能"切半条消息"

┌───────┬───────────────────────────────────────────────────────────────────────────────┐
│ 值              │                                                                                      行为                                                                                                            │
├───────┼───────────────────────────────────────────────────────────────────────────────┤
│ False         │                    消息要么完整保留,要么整条丢弃。如果删到只剩一条消息仍然超限,就返回空列表 []                                             │
├───────┼───────────────────────────────────────────────────────────────────────────────┤
│ True          │                         允许切掉一条消息的中间部分(仅对最后/第一条消息生效),尽可能多保留内容                                                  │
└───────┴───────────────────────────────────────────────────────────────────────────────┘

代码用 False,保证消息完整性,避免出现"半句话"的尴尬情况。

⑦ start_on — 强制从哪种角色开始

start_on="human"

确保裁剪后的消息列表第一条是 HumanMessage(用户消息),而不是 AIMessage。

为什么需要这个? 因为 LLM 期望对话格式是 human → ai → human → ai ...,如果裁剪后变成:

  • ✗ 错误: [AIMessage("晴天"), HumanMessage("上海呢?")]
  • ✓ 正确: [HumanMessage("北京天气?"), AIMessage("晴天"), HumanMessage("上海呢?")]

start_on="human" 会继续往前删,直到第一条是用户消息为止。

完整流程模拟

  完整流程模拟

  假设 MAX_TOKENS=200,当前有 8 条消息共 500 token:

  裁剪前(500 token):
  ┌──────────────────────────────────────┐
  │ SystemMessage: "你是天气助手"         │  ← include_system=True,算在内
  │ HumanMessage: "你好,我是张无忌"      │  ← ✂️ 被裁掉(太旧)
  │ AIMessage: "你好张无忌!"             │  ← ✂️ 被裁掉
  │ HumanMessage: "北京天气?"            │  ← ✂️ 被裁掉
  代码用 False,保证消息完整性,避免出现"半句话"的尴尬情况。

  ---
  ⑦ start_on — 强制从哪种角色开始

  start_on="human"

  确保裁剪后的消息列表第一条是 HumanMessage(用户消息),而不是 AIMessage。

  为什么需要这个? 因为 LLM 期望对话格式是 human → ai → human → ai ...,如果裁剪后变成:

  ✗ 错误: [AIMessage("晴天"), HumanMessage("上海呢?")]
  ✓ 正确: [HumanMessage("北京天气?"), AIMessage("晴天"), HumanMessage("上海呢?")]

  start_on="human" 会继续往前删,直到第一条是用户消息为止。

  ---
  完整流程模拟

  假设 MAX_TOKENS=200,当前有 8 条消息共 500 token:

  裁剪前(500 token):
  ┌──────────────────────────────────────┐
  │ SystemMessage: "你是天气助手"         │  ← include_system=True,算在内
  │ HumanMessage: "你好,我是张无忌"      │  ← ✂️ 被裁掉(太旧)
  │ AIMessage: "你好张无忌!"             │  ← ✂️ 被裁掉
  │ HumanMessage: "北京天气?"            │  ← ✂️ 被裁掉
  │ AIMessage: "北京晴天25°C"            │  ← ✂️ 被裁掉
  │ HumanMessage: "上海呢?"              │  ← ✓ 保留
  │ AIMessage: "上海多云22°C"            │  ← ✓ 保留
  │ HumanMessage: "明天北京天气?"        │  ← ✓ 保留(最新)
  └──────────────────────────────────────┘
                      ↓
             trim_messages(
                 strategy="last",    从后往前保留
                 max_tokens=200,
                 start_on="human"    确保第一条是human
             )
                      ↓
  裁剪后(~150 token):
  ┌──────────────────────────────────────┐
  │ SystemMessage: "你是天气助手"         │  ← 仍然保留
  │ HumanMessage: "上海呢?"              │  ← start_on="human" ✓
  │ AIMessage: "上海多云22°C"            │
  │ HumanMessage: "明天北京天气?"        │
  └──────────────────────────────────────┘

  结果是:张无忌的自我介绍被"遗忘"了,但最近的天气对话全部保留。这就是为什么第 5 轮问"我是谁?"时,Agent 可能已经不记得了。

9.3 自定义 State 扩展

在LangGraph中,AgentState 是一个 TypedDict,定义了 Agent 执行过程中流转的数据结构。扩展 State = 在基础结构上增加自定义字段,用于携带更多上下文和业务数据。

  • 扩展 State 的本质:在 LangGraph 中,State 是 Agent 的 “内存” 和 “消息总线” ,扩展它就像给程序增加新的全局变量,但这些变量随执行流自动流转、隔离、持久化,是实现复杂 Agent 逻辑的基础。

扩展 State 核心目的

  • 跨步骤持久化上下文:Agent 执行是多步骤的(LLM调用 → 工具调用 → 结果解析),扩展的 State 字段能在所有步骤间共享。
  • 实现条件分支与动态路由:根据 State 中的字段值,决定 Agent 的下一步走向。
  • 支持多模态与复杂输入:现代 Agent 需要处理图片、文件等非文本数据,扩展到 State 中。
  • 实现记忆与持久化:扩展字段用于存储长期记忆,跨会话保持。
  • 性能监控与调试:扩展字段用于记录性能指标,便于分析优化。
class ExtendedState(TypedDict):
    messages: list[BaseMessage]
    user_id: str           # 扩展:用户身份,用于权限控制和个性化
    session_id: str        # 扩展:会话标识,用于对话历史管理
    retry_count: int       # 扩展:重试次数,用于错误处理策略
    original_query: str    # 扩展:原始查询,用于日志和审计

使用 TypedDict 当且仅当:

  • 性能极度敏感(如高频API响应,避免Pydantic序列化开销)
  • 数据结构简单(无嵌套或浅层嵌套)
  • 仅需类型提示(团队强制使用mypy,且信任数据输入)
  • 外部库要求(如某些ORM返回TypedDict)
import dotenv, os
from langchain.agents import AgentState
from langchain.tools import tool, ToolRuntime
# 一个或多个命令用于更新图的状态并向节点发送消息。
from langgraph.types import Command
from langchain.messages import ToolMessage
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

# 1.自定义State结构
class CustomAgentState(AgentState):
    """
    扩展Agent状态,包含业务上下文
    """
    # 用户ID,唯一标识
    user_id: str
    # 用户偏好(主题、语言等)
    preferences: dict
    # 用户访问次数
    visit_count: int

# 2.定义工具函数:更新用户偏好
@tool
def update_user_preferences(runtime: ToolRuntime, theme: str) -> Command:
    """
    更新用户主题偏好,写入短期记忆
    ToolRuntime 提供对 state 和 context 的访问能力:
    - runtime.state: 当前状态(含自定义字段)
    - runtime.context: 调用上下文
    - runtime.tool_call_id: 工具调用ID

    :param runtime: 提供对 state 和 context 的访问能力
    :param theme:
    :return:
    """
    # 从当前状态获取偏好(如果不存在则初始化)
    current_preferences = runtime.state.get("preferences", {})
    # 更新偏好
    current_preferences["theme"] = theme

    # 返回Command对象,指示状态更新
    return Command(
        update={
            "preferences": current_preferences,
            "messages": [
                ToolMessage(content=f"用户{runtime.state.get('user_id')}的偏好已更新为:{theme}", tool_call_id=runtime.tool_call_id)
            ]
        }
    )

# 定义工具函数:根据用户偏好生成问候
@tool
def greet_user(runtime: ToolRuntime) -> str:
    """
    根据用户偏好生成问候语,写入短期记忆
    :param runtime:
    :return:
    """
    user_name = runtime.state.get("user_id", "访客")
    # 从当前状态获取偏好
    preferences = runtime.state.get("preferences", {})

    theme = preferences.get("theme", "默认")

    # 生成问候语
    return f"欢迎回来,{user_name},你的主题偏好是:{theme}"


# 1.初始化大模型
dotenv.load_dotenv()
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)


# 创建带有自定义状态的Agent
def create_agent_with_custom_state():
    """
    创建带有自定义状态的Agent
    :return:
    """
    # 使用内存存储器
    memory = InMemorySaver()

    # 创建Agent, 指定自定义 state_schema
    agent = create_agent(
        model=llm,
        tools=[update_user_preferences, greet_user],
        system_prompt="你是一个智能助手,请根据用户指令进行回答。",
        state_schema=CustomAgentState,  # 关键,传入自定义状态类型
        checkpointer=memory
    )

    # 配置线程ID(用于区分不同用户)
    config = {
        'configurable': {'thread_id': 'custom_state_user'}
    }

    # 第一轮:初始化用户信息
    resp1 = agent.invoke(
        {
            "messages": [{"role": "user", "content": "设置主题为暗黑模型"}],
            "user_id": "user_001",  # 自定义字段,配置用户ID
            "preferences": {"language": "zh-CN"},  # 初始偏好设置
            "visit_count": 1
        },
        config=config
    )

    print(f"[第1轮] 初始化用户信息:{resp1['messages'][-1].content}")
    print("-" * 40)

    # 第二轮:读取记忆
    resp2 = agent.invoke({"messages": [{"role": "user", "content": "打个招呼"}]}, config=config)
    print(f"[第2轮] 读取记忆:{resp2['messages'][-1].content}")
    print("-" * 40)

    # 查看完整的状态
    state = agent.get_state(config=config)
    print(f"当前记忆状态:")
    print(f"用户ID: {state.values.get('user_id')}")
    print(f"用户偏好: {state.values.get('preferences')}")
    print(f"用户访问次数: {len(state.values['messages'])}")


if __name__ == '__main__':
    create_agent_with_custom_state()

image

默认 Agent 只能记住对话消息,但无法记住"用户偏好"这类业务数据。 比如"用户喜欢暗黑主题"这种信息,它会随着对话裁剪而丢失。
解决方案是:在 Agent 的状态中开辟一块"永久记忆区",工具函数可以直接读写这块区域。

三个核心概念的关系:

image_609382841626162

  • CustomAgentState:一块结构化的内存区域,既有对话消息,也有业务字段
  • ToolRuntime:工具函数访问这块内存的"钥匙"
  • Command:工具函数修改内存后,告诉 Agent "我改了哪些字段"的"通知单"

update_user_preferences 逐行解析

@tool
def update_user_preferences(runtime: ToolRuntime, theme: str) -> Command:

runtime: ToolRuntime 是特殊参数,不需要 LLM 传值,由框架自动注入。它像一根管道,把工具函数和 Agent 的内部状态连接起来。

current_preferences = runtime.state.get("preferences", {})

runtime.state 就是当前 CustomAgentState 的快照,你可以读取任何字段:
- runtime.state.get("user_id") → "user_001"
- runtime.state.get("preferences") → {"language": "zh-CN"}
- runtime.state.get("visit_count") → 1
- runtime.state.get("messages") → 对话历史列表

这一步是读:先看看用户目前有什么偏好,如果没有就给个空字典。

current_preferences["theme"] = theme

把 LLM 识别出的主题(比如 "暗黑模式")写入字典。

return Command(
        update={
            "preferences": current_preferences,
            "messages": [
                ToolMessage(content=f"用户{runtime.state.get('user_id')}的偏好已更新为:{theme}", tool_call_id=runtime.tool_call_id)
            ]
        }
    )

这是最关键的一步。 之前只是修改了本地变量,Command 对象才是真正把修改写回 Agent 状态的方式。

Command 就像一个'状态变更指令':

image

完整执行流程:

image

说明:

场景TypedDictPydantic
FastAPI请求体 ✗ 不推荐(需手动验证) ✔ 最佳选择(原生集成)
GraphQL响应 ✔ 适合(结构固定) ✔ 可但较重
内部函数参数 ✔ 轻量且有效 ✗ 过度设计
CLI工具配置 ✔ 需手动校验 ✔ 自动验证友好
数据处理流水线 ✔ 零开销传递 ✔ 频繁转换有成本
机器学习特征 ✔ 快速定义结构 ✗ 不必要
微服务DTO ✔ 需结合mypy ✔ 天然支持序列化
测试Mock数据 ✔ 快速创建 ✔ 验证可能碍事

记忆核心原则

  • 隔离性:每个用户必须分配唯一 thread_id,避免串话
  • 持久化:生产环境必须使用数据库检查点,支持服务重启和高可用
  • 可控性:自定义 State 和中间件实现业务逻辑与记忆管理的分离
  • 性能:长对话必须启用摘要机制,防止 token 超限和响应延迟

9.4 长期记忆

长期记忆通过与外部向量数据库或键值存储集成来实现。可以在Agent执行的关键节点(如对话结束时)提取关键信息、用户偏好等,并存入长期记忆库,供未来的对话使用。

1.语义检索与向量数据库

向量数据库是实现长期记忆的核心技术,通过语义相似度搜索实现知识的长期存储和检索。
技术实现:

  • 向量化存储:将对话内容、用户偏好等转换为向量表示
  • 语义检索:基于向量相似度实现智能搜索
  • 多模态支持:支持文本、图像、音频等多种数据类型
  • 高性能查询:支持大规模数据的快速检索

典型实现:

  • Milvus:开源向量数据库,支持大规模向量检索
  • Qdrant:高性能向量搜索引擎
  • Pinecone:云原生向量数据库服务
  • Chroma: 轻量级向量数据库,可本地持久化
import os

import dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_chroma import Chroma
from langchain_core.messages import HumanMessage
from langchain_ollama import OllamaEmbeddings
from langchain.tools import tool
from langchain_core.documents import Document
from langgraph.checkpoint.memory import InMemorySaver

# 1.初始化向量数据库(保存长期记忆使用)
embeddings = OllamaEmbeddings(
    model="bge-m3:latest",
    base_url="http://192.168.163.240:11434",
)
# 创建向量数据库,生产环境使用Milvus
vector_store = Chroma(
    collection_name="agent_long_term_memory",
    embedding_function=embeddings,
)

# 2.定义记忆工具(Agent的手)

# 2.1.定义记忆保存工具
@tool
def save_memory(content: str):
    """
    保存记忆,写入向量数据库
    :param content:
    :return:
    """
    print(f"保存记忆:{content}")

    # 将文本转为Document对象
    doc = Document(page_content=content, metadata={"source": "user_interaction", "timestamp": "simulated_time"})

    # 写入向量库
    vector_store.add_documents([doc])
    return "记忆已保存"

# 2.2.定义记忆搜索工具
@tool
def search_memory(query: str) -> str:
    """
    搜索记忆,长期记忆从向量数据库中搜索
    当你被问及关于用户过去的问题,或者你不确定答案时,使用此工具进行查找
    :param query: 要搜索的查询语句
    :return:
    """
    print(f"\n 记忆操作,正在搜索记忆:{query}")

    # 搜索向量数据库(k=2 表示只取相关的2条)
    docs = vector_store.similarity_search(query, k=2)

    if not docs:
        return "没有找到相关的记忆"

    memory_content = "\n".join([doc.page_content for doc in docs])

    return f"以下是相关的记忆:\n{memory_content}"


# 3.创建agent

# 3.1.将功能放入列表
tools = [save_memory, search_memory]

# 3.2.定义系统提示词:教会Agent如何使用记忆工具
system_prompt = """
    你是一个拥有长期记忆的私人助手。你的目标是记住用户的喜好和重要信息,以便提供个性化服务。
    1.如果用户告诉你任何关于他们自己的事实(如名字、喜好、居住地),请务必调用'save_memory'工具保存;
    2.如果用户问你一个问题,而答案可能在你之前的记忆中,请先调用'search_memory'工具查找。
    3.如果只是闲聊,不需要调用工具。
"""

# 3.3.初始化大模型
dotenv.load_dotenv()
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)


# 3.4.创建Agent
memory = InMemorySaver()

agent = create_agent(
    model=llm,
    tools=tools,
    system_prompt=system_prompt,
    checkpointer=memory
)


# 4.执行
def run_demo():
    # 场景A: 存入记忆
    # 使用一个 thread_id, 代表这是今天的对话
    config1 = {"configurable": {"thread_id": "user_001"}}

    print("--- 场景A: 用户告诉 Agent 喜好 ---")
    user_input_1 = "你好,记住我最喜欢的水果是草莓,我对苹果也喜欢,但是对香蕉不感兴趣。"
    resp1 = agent.invoke({"messages": [{"role": "user", "content": user_input_1}]}, config=config1)
    print(f"AI: {resp1['messages'][-1].content}")

    # 场景B: 模拟遗忘(开启新线程)
    config2 = {"configurable": {"thread_id": "user_002"}}
    print("\n--- 场景B: 第二天(新的Session,短期记忆已清空)")
    user_input_2 = "今天我要吃水果,你记住我喜欢什么水果吗?我不喜欢那些水果?"

    # 观察控制台输出,你会看到 Agent 自动调用 'search_memory' 工具进行搜索
    resp2 = None

    for chunk in agent.stream(
        {"messages": [HumanMessage(content=user_input_2)]},
        config=config2,
        stream_mode="values"
    ):
        # print(chunk["messages"][-1].content, end="", flush=True)
        resp2 = chunk["messages"][-1]

    print(f"AI: {resp2.content}")


if __name__ == '__main__':
    run_demo()

image

9.5 跨线程记忆

针对"跨线程记忆(Cross-Thread Memory)“的管理,在 LangChain 1.0 / LangGraph 体系中,这通常被称为"用户级状态(User-Level State)” 或"全局记忆"。

它与前两个问题的区别在于:

  • 短期记忆 (Checkpointer):只在 thread_id(一次会话)内有效。
  • 长期记忆 (VectorStore):存的是模糊的知识片段。
  • 跨线程记忆 (BaseStore):存的是结构化的用户档案(User Profile),例如用户的姓名、VIP等级、偏好设置等。无论用户开多少个新聊天窗口(Thread),这些信息都必须存在。

BaseStore结构化存储

BaseStore 是 LangGraph 提供的通用键值存储抽象接口,专为结构化长期记忆设计,核心特性包括:

  • 命名空间(Namespace)机制
  • 采用层次化元组路径组织数据,类似文件系统目录结构:
namespace = ("users", "user_123", "preferences")
# 对应逻辑路径:users/user_123/preferences

核心操作

put(namespace, key, value):存储键值对(支持TTL过期)
get(namespace, key):精确检索单个记忆
search(namespace, query):语义搜索(需子类支持)
delete(namespace, key):删除记忆

需要注意:两个并不是普通的数据类型,而是 依赖注入(Dependency Injection)标记,用于告诉 LangGraph 自动把 State 或 Store 注入到 Tool 中,开发者无需手动传递。

from langgraph.prebuilt import InjectedState, InjectedStore
  • InjectedState

作用:自动将 Graph 当前的 State(状态) 注入到 Tool 中。

Annotated[AgentState, InjectedState]

意思就是:

不要让 LLM 提供这个参数。

运行 Tool 时,由 LangGraph 自动把 Graph State 注入进去。

  • InjectedStore

如果说:InjectedState 是 Graph 当前运行状态

那么:InjectedStore 是长期存储(Persistent Store)

通常是:

  • Memory
  • Redis
  • SQLite
  • PostgreSQL
  • 文件
  • 向量数据库

等等。

它用于 Tool 中访问长期数据。

import os
import time
import uuid
from typing import Annotated
import dotenv
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain.agents import create_agent, AgentState
from pydantic import BaseModel, Field
# 核心组件:Postgres 持久化检查点
from langgraph.prebuilt import InjectedState, InjectedStore
from langgraph.store.base import BaseStore
from langgraph.store.postgres import PostgresStore
from psycopg_pool import ConnectionPool
from langgraph.checkpoint.postgres import PostgresSaver
from langchain.messages import HumanMessage

dotenv.load_dotenv()

# 1.数据库配置
# 数据库连接字符串
db_url = "postgresql://myuser:123456@192.168.163.240:5432/mydb"

# 2.模型初始化
llm = init_chat_model(
    model="deepseek-v4-pro",
    model_provider='deepseek',
    base_url="https://api.deepseek.com",
    api_key=os.getenv('api_key'),
    temperature=0.7,
    max_tokens=10000,
)


# 跨线程记忆需要传递user_id,通过自定义的State
class CrossThreadState(AgentState):
    # 跨线程记忆的唯一标识符
    user_id: str


# 定义 Pydantic 模型,用于提取用户信息
class UserInfo(BaseModel):
    user_name: str = Field(..., description="用户姓名, 例如:张三、Alice、Bob等")
    additional_info: str = Field(..., description="用户额外的信息,例如:职业、爱好、地址等")


class QueryInfo(BaseModel):
    user_name: str = Field(
        ...,
        description="要查询的用户名字。如果查询中包含'我的'、'我是'等第一人称的时候,请从对话历史中提取用户名;如果没有明确的用户名,返回'all_users'"
    )
    query_content: str = Field(description="查询的具体内容,例如:职业、兴趣爱好等")


# 2.定义工具
@tool
def magic_calculator(a: int, b: int) -> int:
    """
    进行一次特殊的加法计算
    """
    return (a + b) * 10


# ############################### 定义记忆管理工具 #####################################
# 注意:store参数使用 InjectedStore()注解,由 LangGraph 自动注入
# InjectedStore() 标记会让 Pydantic 在生成JSON Schema 时跳过这个参数,LLM不会看到store参数,只会看到user_id和info参数

@tool
def remember_user_info(
        info: str,
        state: Annotated[dict, InjectedState()],
        store: Annotated[BaseStore, InjectedStore()]
) -> str:
    """
    将用户信息存入跨线程记忆
    重要:此工具会自动从state中获取user_id,并使用Pydantic提取用户信息
    :param info: 要记忆的信息,例如:用户的姓名、职业、偏好等
    :param state:
    :param store:
    :return:
    """
    # 使用Pydantic提取用户信息
    structured_llm = llm.with_structured_output(UserInfo)

    try:
        # 从文本中提取结构化用户信息
        extracted_info = structured_llm.invoke(f"从以下文本中提取用户名和其他信息:{info}")

        # 优先使用提取的用户名,如果提取失败则使用 state 中的 user_id
        extracted_user_name = extracted_info.user_name.lower()
        state_user_id = state.get("user_id", "unknown_user")

        # 如果提取到的用户名不是 unknown, 则使用提取的用户名
        if extracted_user_name and extracted_user_name != "unknown":
            user_id = extracted_user_name
        else:
            user_id = state_user_id

        full_info = f"{extracted_info.user_name}: {extracted_info.additional_info}"

    except Exception as e:
        user_id = state.get("user_id", "unknown_user")
        full_info = info

    # 命名空间的设计
    namespace = (user_id, "profile")

    # 生成唯一记忆ID
    memory_id = str(uuid.uuid4())

    # 存储到BaseStore(自动持久化)
    store.put(
        namespace,
        memory_id,
        {"info": full_info, "timestamp": "2026-07-10", "source": "user_input"}
    )

    return f"已经信息存入长期记忆中,(用户:{user_id}):{full_info}"


@tool
def recall_user_info(
        query: str,
        state: Annotated[dict, InjectedState()],
        store: Annotated[BaseStore, InjectedStore()]
) -> str:
    """
    从跨线程记忆中查询用户信息
    重要:此工具会自动从 state 中获取 user_id
    :param query: 查询关键词,用于描述要查找的信息,例如:我的职业,我的兴趣 等
    :param state:
    :param store:
    :return: 用户的历史信息
    """
    # 优先从 state 中获取 user_id
    state_user_id = state.get("user_id", None)

    # 如果state中没有user_id,则尝试使用 Pydantic 提取用户信息
    if not state_user_id:
        # 指定输出结构
        structured_llm = llm.with_structured_output(QueryInfo)
        try:
            prompt = f"""从以下查询中提取用户名和查询内容。
            查询文本:{query}
            注意:
            1. 如果查询中包含'我的''我是'等第一人称的时候,说明用户在询问自己的个人信息;
            2. 如果能从查询中推断出具体的用户名(如Alice、Bob等),请提取该用户名;
            3. 如果没有明确的用户名,返回'all_users'"""

            extracted_query = structured_llm.invoke(prompt)

            # 如果提取到的是 all_users, 则搜索所有用户的信息
            if extracted_query.user_name.lower() in ["all_users", "current_user", "unknown"]:
                user_id = None
            else:
                user_id = extracted_query.user_name.lower()

        except Exception as e:
            user_id = None
    else:
        # 如果state中有user_id,则使用它
        user_id = state_user_id

    try:
        if user_id:
            # 命名空间, 使用namespace_prefix搜索该用户的所有记忆
            namespace_prefix = (user_id,)
            memories = store.search(namespace_prefix, limit=20)
        else:
            # 搜索所有已知用户的记忆
            memories = []
            # 先尝试获取所有可能的用户
            for uid in ["alice", "bob", "unknown_user"]:
                namespace_prefix = (uid,)
                user_memories = store.search(namespace_prefix, limit=20)
                memories.extend(user_memories)

        if not memories:
            return "没有找到任何相关记忆。请先告诉我一些信息,我会记住它们。"

        # 格式化返回
        results = []
        for item in memories:
            info = item.value.get("info", "未知信息")
            timestamp = item.value.get("timestamp", "未知时间")
            results.append(f"- {info} (记录时间: {timestamp})")

        return f"找到 {len(results)} 条记忆:\n" + "\n".join(results)
    except Exception as e:
        return f"检索记忆时出错:{str(e)}"


# 主程序逻辑
def run_postgres_agent():
    print("--- 正在连接 PostgreSQL 数据库 ---")

    with ConnectionPool(conninfo=db_url, max_size=20, kwargs={"autocommit": True}) as pool:
        # A.初始化 Checkpointer 和 Store
        checkpointer = PostgresSaver(pool)
        store = PostgresStore(pool)

        # 注意:第一次运行时需要创建表结构,会检测数据库,如果不存在,会自动创建所需的表结构
        # 生产环境只需要运行一次,但在脚本中加上是安全的(幂等操作)
        # checkpointer 会创建 checkpoints、checkpoint_blobs等表
        # store 会创建 store 表
        print("🔧 初始化 Checkpoiner 表结构")
        checkpointer.setup()
        print("✅️ Checkpoiner表结构初始化完成")

        print("🔧 初始化 store 表结构")
        store.setup()
        print("✅️ store表结构初始化完成")

        # B.初始化 Agent
        # 包含所有工具:计算工具 + 跨线程记忆工具
        tools = [magic_calculator, remember_user_info, recall_user_info]

        agent = create_agent(
            model=llm,
            tools=tools,
            state_schema=CrossThreadState,  # 跨线程记忆,自定义状态传递user_id
            system_prompt="""
            你是一个具备跨线程记忆的智能助手。
            你的能力:
            1.使用 remember_user_info 工具将用户的重要信息存入长期记忆(跨会话持久化)
            2.使用 recall_user_info 工具从长期记忆中检索用户信息
            3.使用 magic_calculator 工具进行特殊计算
            
            工作流程:
            - 当用户告诉你他的名字、职业、偏好等信息时,主动调用 remember_user_info 存储
            - 当用户询问 '你还记得我吗'或 类似问题时,调用 recall_user_info 检索
            - 记忆时跨会话的,即使在新的对话中也能记住用户信息
            注意: 调用remember_user_info和recall_user_info时,必须传入 user_id 参数(从state中获取)。
            """,
            store=store,
            checkpointer=checkpointer
        )

        # 4.测试场景:跨线程记忆功能
        print("\n" + "=" * 60)
        print("测试1:用户 Alice 第一次对话 (会话1)")
        print("=" * 60)

        # 1. Alice 的第一次对话
        config1 = {"configurable": {"thread_id": "user_001"}}

        print("\n 用户Alice:你好,我是Alice,一名 Python开发工程师,我喜欢深度学习。")

        for chunk in agent.stream(
                {"messages": [HumanMessage(content="你好,我是Alice,一名 Python开发工程师,我喜欢深度学习。")]},
                config=config1,
                stream_mode="values"
        ):
            last_msg = chunk["messages"][-1]

            if last_msg.type == "ai" and last_msg.content:
                print(f"🤖 Agent {last_msg.content}")

            if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
                for tool_call in last_msg.tool_calls:
                    print(f"   🔧 [调用工具]: {tool_call['name']}")

        print("\n" + "=" * 60)
        print("场景2:用户 Alice 第2次对话 (会话2 - 不同 thread_id)")
        print("=" * 60)

        print("💡 模拟:Alice关闭浏览器,第二天重新打开,开始新会话")
        time.sleep(1)

        # 2. Alice的第二个会话(不同的thread_id)
        config2 = {"configurable": {"thread_id": "user_002"}}
        print("\n 用户Alice:你还记得我是谁吗?我的职业是什么?")

        for chunk in agent.stream(
                {"messages": [HumanMessage(content="你还记得我是谁吗?我的职业是什么?")]},
                config=config2,
                stream_mode="values"
        ):
            last_msg = chunk["messages"][-1]

            if last_msg.type == "ai" and last_msg.content:
                print(f"🤖 Agent {last_msg.content}")
            if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
                for tool_call in last_msg.tool_calls:
                    print(f"   🔧 [调用工具]: {tool_call['name']}")

        print("\n" + "=" * 60)
        print("场景3:用户Bob的对话(不同用户)")
        print("=" * 60)

        # 3.Bob的会话
        config3 = {"configurable": {"thread_id": "user_003"}}
        print("\n 用户Bob:你好,我叫Bob,一名产品经理,请你帮我计算一下 5 + 10 的特殊结果")

        for chunk in agent.stream(
                {"messages": [HumanMessage(content="你好,我叫Bob,一名产品经理,请你帮我计算一下 5 + 10 的特殊结果")]},
                config=config3,
                stream_mode="values"
        ):
            last_msg = chunk["messages"][-1]
            print(last_msg)
            if last_msg.type == "ai" and last_msg.content:
                print(f"🤖 Agent: {last_msg.content}")

            if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
                for tool_call in last_msg.tool_calls:
                    print(f" 🔧 [调用工具]: {tool_call['name']}")

        print("\n" + "=" * 70)
        print("✅ 跨线程记忆测试完成!")
        print("=" * 70)
        print("\n 测试总结:")
        print("  ✅ 场景 1: Alice 首次对话,Agent 自动存储用户信息到 Store")
        print("  ✅ 场景 2: Alice 新会话(不同 thread_id),Agent 成功从 Store 检索记忆")
        print("  ✅ 场景 3: Bob 的对话,Agent 为 Bob 创建独立的记忆空间")
        print("  ✅ 场景 4: Alice 再次对话,记忆未被 Bob 的信息污染")
        print("\n💡 关键特性:")
        print("  - Checkpointer: 管理单个会话的对话历史(基于 thread_id)")
        print("  - Store: 管理跨会话的长期记忆(基于 user_id)")
        print("  - 记忆隔离: 不同用户的记忆完全隔离(通过 namespace)")
        print("  - 持久化: 所有数据存储在 PostgreSQL,重启程序后依然可用")
        print("=" * 70)


if __name__ == '__main__':
    run_postgres_agent()

image

 

image

 

image

store中存储的内容:

image

9.6.企业最佳组合

9.6.1 Checkpointer + KV Store组合架构

最佳实践架构如下:

  • 身份标识 (user_id):在配置中除了传入 thread_id,必须传入 user_id

  • 双层存储

    • 会话层:使用 MemorySaver 管理当前聊天的上下文。

    • 用户层:使用 BaseStore 存储跨会话的用户档案。

9.6.2 记忆生命周期管理

记忆清理策略:

  • 自动过期:设置TTL自动清理过期记忆

  • 手动清理:提供管理接口手动清理无用记忆

  • 压缩归档:对历史记忆进行压缩和归档

9.6.3 性能优化策略

查询优化:

  • 索引优化:为常用查询字段建立索引

  • 缓存策略:实现多级缓存提高查询性能

  • 批量操作:支持批量读写操作减少I/O开销

十、MCP

10.1 LangGraph接入外部工具技术实现方法

说到智能体开发,无论使用何种框架,有一项绕不开的核心技术,那就是MCP(Model Context Protocol)技术。

在智能体开发过程中,接入外部函数工具是至关重要的一环,而在前面的章节中说到LangChain&LangGraph技术生态中,可以非常便捷的通过@tool装饰符来自定义一个外部函数:

image

或者也可以借助LangChain丰富的、数以百计的内置工具,三行代码即可在智能体中进行工具调用。

功能类别工具名称简要说明
🔎 搜索工具 TavilySearchResults 快速搜索实时网络信息
  SerpAPIWrapper 基于 SerpAPI 的搜索结果工具
  GoogleSearchAPIWrapper 调用 Google 可编程搜索引擎
🧠 计算工具 PythonREPLTool 执行 Python 表达式并返回结果
  LLMMathTool 结合 LLM 和数学推理能力
  WolframAlphaQueryRun 基于 Wolfram Alpha 的计算引擎
🗂 数据工具 SQLDatabaseToolkit 构建 SQL 数据库查询工具集
  PandasDataframeTool 用于在 Agent 中操作表格数据
🌐 网络/API RequestsGetTool / RequestsPostTool 执行 HTTP 请求
  BrowserTool / PlaywrightBrowserToolkit 自动化网页浏览与抓取
💾 文件处理 ReadFileTool 读取本地文件内容
  WriteFileTool 写入文本到指定文件中
📚 检索工具 FAISSRetriever 基于向量的文档检索工具
  ChromaRetriever 使用 ChromaDB 的检索器
  ContextualCompressionRetriever 上下文压缩检索器,适合长文档
🧠 LLM 工具 ChatOpenAI / OpenAIFunctionsTool 使用 OpenAI 模型作为工具调用
  ChatAnthropic Anthropic Claude 模型封装工具
🔧 自定义工具 @tool 装饰器 任意函数可封装为 Agent 可调用工具
  Tool 类继承 自定义更复杂逻辑的工具实现

调用搜索工具实现agent代码:

from langchain.agents import create_agent
from langchain_ollama import ChatOllama
from langchain_tavily import TavilySearch
import dotenv
dotenv.load_dotenv()

# 1.初始化Tavily搜索工具
# 登录网站,获取key: https://app.tavily.com/home
search_tools = TavilySearch(tavily_api_key=os.getenv("TAVILY_API_KEY"))

# 2.准备工具列表, 每个工具都是一个函数, 可以添加多个工具
tools = [search_tools]

# 3.创建agent
agent = create_agent(
    model=ChatOllama(model="qwen3:8b"),
    tools=tools
)

# 4.调用agent
res = agent.invoke({"messages": [{"role": "user", "content": "请帮我搜索有哪些开源的基于langchain1.0实现的多模态rag项目"}]})
print(res['messages'][-1].content)

10.2 MCP技术概述

实际开发Agent的过程中,外部函数工具的开发会占用大量的开发者的时间精力。

而与此同时,人们发现,很多外部工具的功能其实是通用的,例如查询时间、查询天气、网络搜索、操作本地文件夹等等等等,如果有一种规范,能够减少重复造轮子的时间,一个人开发完成后全体开发者都能共享,那么整体的研发效率都将得到大幅提高。

在这一设想下,MCP技术诞生了。MCP的全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于2025年11月正式提出。

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

10.2.1 MCP技术定位与技术价值介绍

我们可以将MCP技术简单理解为智能体外部工具开发的一种通用规范(范式)。举个例子,以天气查询工具为例,在MCP技术诞生之前,要给大模型添加查询天气的功能,至少需要经历这么两个开发阶段:

  • 阶段一:编写查询天气的外部函数,

  • 阶段二:将外部工具进行进一步封装,以适配不同的开发框架。

然后再分别接入

  • 接入谷歌ADK时
  • 接入LangGraph时:
  • 接入OpenAI Agents SDK时:

这就使得实际开发Agent的过程中,外部函数工具的开发会占用大量的开发者的时间精力。而与此同时,人们发现,很多外部工具的功能其实是通用的,例如查询时间、查询天气、网络搜索、操作本地文件夹等等等等,如果有一种规范,能够减少重复造轮子的时间,一个人开发完成后全体开发者都能共享,那么整体的研发效率都将得到大幅提高。

在这一设想下,MCP技术诞生了。MCP的全称是Model Context Protocol,模型上下文协议,由Claude母公司Anthropic于去年11月正式提出。该技术核心目标,就是创建一种统一的大模型调用外部工具的通信规范,相当于这种标准的通信规范,一项特定功能的外部函数,只需要开发一次,就能被各种不同类型的Agent开发框架所识别。例如同样是查询天气,如果我们遵循MCP技术协议开发一个查询天气的外部函数,那么接下来全体开发者就都能直接用我开发好的这个天气查询工具,带入任何智能体开发框架,快速搭建智能体应用了。

通过下面这组图能够非常清楚的解释MCP工具在智能体开发过程中实际带来的提效的作用。

8cccf76cdd07741f61424ecdeda88b5c

8dac9069b78b9b41d545ae9bec8da65f

10.2.2 MCP技术架构

不过呢,要做到上面说的这种“车同轨、书同文”的标准化工作,不仅需要制定一套让所有人都信服的标准,而且还需要经过时间的检验,同时还需要有足够多的用户,这个标准才能真正被市场所认同。因此MCP技术也历经了一段时间的沉淀和打磨,自2024年11月发布开始,到2025年3月技术大爆发,再到第二季度开始越来越多的Agent框架和热门应用宣布支持MCP技术,MCP才算是逐渐成为一项智能体开发的通用协议。

截止目前,MCP的技术生态可以划分为三层,最底层是协议层,也就是“文字版”的规定、或者说规范,而为了普及这一规范,让更多的人更加快速的完成自己的MCP工具开发,Anthropic进一步的提供了MCP开发工具,借助这些SDK,我们能够非常快速完成MCP工具开发。而既然是一种标准化的协议,其核心价值就在于用的人足够多、同时分享的人也足够多,才能真正减少“重复造轮子”的时间,因此Anthropic官方和很多第三方平台,也在积极的推进MCP技术生态的构建,尤其是MCP工具平台的建设,通过鼓励开发者更多的分享自己开发的MCP工具,只有形成了更大的(分享和引用的)协作规模,MCP的技术才能更有价值。
948d40d029ac457f7f1b656434dee3ee

10.2.3 MCP SDK与MCP技术生态

而在这些MCP完整的技术架构中,开发者尤其需要关注MCP的SDK(开发工具)和MCP技术生态。所谓MCP的SDK,指的是官方提供的用于开发MCP工具的第三方库,截至目前,MCP SDK已支持Python、TypeScript、Java、Kotlin和C#等编程语言进行客户端和服务器创建。

SDK文档:https://github.com/modelcontextprotocol

image

而借助这些库(sdk),仅需几行代码,即可快速构建一个MCP工具。下面是利用Python MCP SDK实现

# pip install mcp
from mcp.server.fastmcp import FastMCP

# 创建 MCP 实例
mcp = FastMCP("Demo")

# 为 MCP 实例添加工具
@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b

# 为 MCP 实例添加资源
@mcp.resource("greeting://default")
def get_greeting() -> str:
    return "Hello from static resource!"

# 为 MCP 实例添加提示词
@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.settings.host = "0.0.0.0"
    # mcp.settings.port = 8888
    mcp.run(transport="streamable-http")  # 默认启动在 127.0.0.1:8000

反之,如果没有这些MCP开发工具,想要开发MCP工具,就必须从MCP技术协议出发,借助其他库来完成开发,其实现难度非常大。

而如果我们并不需要开发MCP工具,而只想要借助现成的MCP工具快速完成智能体开发,那么就需要重点关注现在的MCP集成平台,也就是集成了各类目前非常流行的MCP工具的平台,借助这些平台,我们能够快速找到想要的MCP服务,然后根据指示说明快速进行接入,甚至伴随着MCP流式HTTP功能的上线,很多平台还提供了这些MCP工具后端运行服务,我们只需要输入指定的后端地址,就能调用运行在云端的MCP工具。

主流的MCP集成平台如下:

  • MCP官方服务器合集:https://github.com/modelcontextprotocol/servers
  • MCP Github热门导航:https://github.com/punkpeye/awesome-mcp-servers
  • Smithery:https://smithery.ai/
  • MCP导航:https://mcp.so/
  • 阿里云百炼:https://bailian.console.aliyun.com/?tab=mcp
  • 魔搭社区MCP广场:https://www.modelscope.cn/mcp
  • mcp.run:https://www.mcp.run/

10.3 MCP核心技术概念

而在正式开始MCP智能体开发之前,我们还需要补充两个MCP技术体系中至关重要的技术概念,分别是:MCP客户端服务器、以及两大类MCP工具运行模式。

10.3.1 MCP客户端与服务器

由于MCP是一种围绕大模型外部函数工具创建的统一范式,因此MCP工具从诞生之初就是客户端(client)与服务器(server)分离的架构。服务器与客户端的技术概念可以借助MySQL这个通用的数据库软件进行理解,在MySQL中,服务器指的是数据库实际运行环境,例如公司内部的某个统一用于数据存储的物理机,而客户端,则指的是SQL编写和运行的环境,可以是比如数据分析师用的笔记本。每次要进行查数时,数据分析师就可以在自己的笔记本上运行MySQL WorkBench(一个SQL编程的IDE),然后借助MySQL客户端,给公司的MySQL服务器发送查数的请求。

类似的,所谓MCP Server(服务器),指的是MCP工具运行的环境,而MCP Client(客户端),则指的是能够调用MCP工具、或者说给MCP工具发送请求并接受结果的环境。二者关系如图所示:

7a2f822343b425ddab4d38ab0bd807e1

这种服务器和客户端分离的架构的好处,就在于可以更加便捷的进行模块化开发和维护,而此前我们所说的MCP工具,其实就指的是MCP服务器,而那些MCP工具集合,其实就是MCP服务器集合网站。

同时,基于这种技术划分,当我们在开发一个智能体,并希望这个智能体能够接入MCP工具时,其实从MCP技术角度来说,我们本质上是开发一个MCP的客户端(Client)。例如当我们基于LangChain开发一个接入MCP工具的智能体,其实我们就开发了一个基于LangChain的MCP客户端。而现在也有很多大模型聊天工具允许接入MCP工具,例如Claude Desktop、Cherry Studio等,这些也都是MCP客户端。

10.3.2 标准MCP工具接入客户端流程

这里以ChatBox为例,为大家展示一个标准的MCP客户端接入MCP服务器的基本流程,从中我们能够看出,填写对应的配置文件,是运行MCP工具的关键。

1.现在打开:https://www.modelscope.cn/mcp/servers/@Joooook/12306-mcp,获取12306的mcp

{
  "mcpServers": {
    "12306-mcp": {
      "args": [
        "-y",
        "12306-mcp"
      ],
      "command": "npx"
    }
  }
}

2.复制上面配置,填入

image

3.测试

image

10.3.3 MCP离线运行与在线运行模式

既然是服务器和客户端分离的架构,那么MCP肯定支持两种调用方法,其一是本地运行,也被成为离线运行,指的是MCP服务器和MCP客户端在同一台电脑上进行运行,其二则是在线运行,或者异地部署运行,指的是MCP服务器在远程服务器或者云端运行,然后借助HTTP网络通信来进行响应。

而这也就对应着MCP工具调用的两种核心模式,分别是stdio(本地)模式调用和SSE&streamable HTTP(在线)模式调用,各调用方法效果对比如下所示:

特性StdioSSEStreamable HTTP
通信方向 双向(但仅限本地) 单向(服务器到客户端) 双向(适用于复杂交互)
使用场景 本地进程间通信 实时数据推送,浏览器支持 跨服务、分布式系统、大规模并发支持
支持并发连接数 中等 高(适合大规模并发)
适应性 局限于本地环境 支持浏览器,但单向通信 高灵活性,支持流式数据与请求批处理
实现难度 简单,适合本地调试 简单,但受限于浏览器兼容性和长连接 复杂,需处理长连接和流管理
适合的业务类型 本地命令行工具,调试环境 实时推送,新闻、股票等实时更新 高并发、分布式系统,实时交互系统
三种传输方式总结如下:
  1. Stdio 传输:适合本地进程之间的简单通信,适合命令行工具或调试阶段,但不支持分布式。
  2. SSE 传输:适合实时推送和客户端/浏览器的单向通知,但无法满足双向复杂交互需求。
  3. Streamable HTTP 传输:最灵活、最强大的选项,适用于大规模并发、高度交互的分布式应用系统,虽然实现较复杂,但能够处理更复杂的场景。

而伴随着2025年5月MCP更新了Streamable HTTP的SDK,目前越来越多的MCP工具都选择采用Streamable HTTP形式进行部署和运行。

10.3.4 离线MCP工具托管平台

当然,如果是调用在线MCP服务,开发者只能了解其功能,而如果是离线的MCP服务,则对应的MCP工具是完全开源的,在每次调用之前,开发者都需要将其源码先下载到本地,然后再运行。例如上面的章节12306mcp,我们使用npx命令,其本质就是先将指定的MCP工具下载到本地,然后在有需要的时候对其进行调用。例如: 12306MCP配置文件如下:

{
  "mcpServers": {
    "12306-mcp": {
      "args": [
        "-y",
        "12306-mcp"
      ],
      "command": "npx"
    }
  }
}

代表的含义就是我们需要先使用如下命令:

npx -y 12306-mcp

对这个库12306-mcp进行下载,然后在本地运行,当有必要的时候调用这个库里面的函数执行相关功能。而这个12306-mcp库是一个托管在https://www.modelscope.cn/mcp上的库,

image

除此此外,一些由Python编写的MCP开源工具,则托管在pypi平台上,https://pypi.org/ ,每次运行的时候我们需要填写uvx命令,其本质就是将工具源码从pypi平台上下载到本地再来进行运行。

10.3.5 MCP在线托管服务

伴随着MCP快速调用的请求不断增加,也有很多平台提供了在线托管服务模式,例如魔搭社区的MCP广场中,就有很多是运行在魔搭社区服务器上的MCP工具,开发者可以直接使用SSE或者流式HTTP方式请求调用在线的MCP工具,从而快速完成开发工作。

image

10.4 MCP 架构

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

MCP 主机

协调和管理一个或多个 MCP 客户端的 AI 应用

MCP 客户端

一个保持与 MCP 服务器连接的组件,通过 MCP 定义的消息处理通信,从服务器查找并请求资源和工具,并管理与服务器的连接生命周期

MCP 服务器

一个向 MCP 客户端提供服务的程序,通过协议暴露工具、资源和提示模板功能

10.5 MCP 层级

MCP 分为两个层级:

⑴.数据层

数据层实现了一个基于 JSON-RPC 2.0 的交换协议,该协议定义了消息结构和语义。

数据层包括生命周期管理(连接初始化、能力协商、连接终止)、服务器功能(提供工具、资源和提示模板)、客户端功能(调用LLM、获取输入、记录消息)、其他功能(实时更新通知、长时运行操作跟踪)。

⑵.传输层

传输层定义了客户端与服务器之间数据交换的通信机制和通道,包括特定传输方式的连接建立、消息帧定界和授权。

MCP 支持多种传输机制,包括 Stdio、Streamable HTTP、SSE。

Stdio

使用标准输入和输出流,与在终端输入命令并看到响应时使用的机制相同。适用于本地开发

Streamable HTTP

该传输使用 HTTP POST 和 GET 请求,服务器可以选择使用SSE来流式传输多个服务器消息。支持流式传输和服务器到客户端通知,并支持标准 HTTP 身份验证方法,包括授权令牌、API 密钥和自定义头信息

SSE

带有 SSE(Server-Sent Events 服务器发送事件)的 HTTP,MCP早期传输机制,现逐渐被 Streamable HTTP 取代

10.6 MCP 工作流程

⑴.初始化

在初始化过程中,AI 应用程序的 MCP 客户端管理器连接到配置的服务器,并将它们的能力存储起来以供后续使用。应用程序使用这些信息来确定哪些服务器可以提供特定类型的功能(工具、资源、提示),以及它们是否支持实时更新。

初始化有几个重要的作用:

协议版本协商

确保客户端和服务器使用兼容的协议版本,避免因版本不一致导致的通信问题

能力发现

声明各自支持的功能,包括他们能够处理的基元类型(工具、资源、提示)以及是否支持通知等特性

身份交换

交换客户端与服务器的身份及版本信息,便于后续的调试与兼容性管理

⑵.工具发现

AI 应用程序从所有连接的 MCP 服务器中获取可用工具,并将它们组合成一个语言模型可以访问的统一工具注册表。这使得 LLM 能够理解它可以执行哪些操作,并在对话期间自动生成相应的工具调用。

连接建立之后,客户端可以通过发送 tools/list 请求来发现可用的工具。这个请求是 MCP 工具发现机制的基础—它允许客户端在尝试使用工具之前了解服务器上有哪些可用的工具。响应包含一个 tools 数组,该数组提供了关于每个可用工具的全面元数据。这种基于数组的结构允许服务器同时展示多个工具,同时保持不同功能之间的清晰界限。响应中的每个工具包括几个关键字段:

name

工具标识符

title

工具的易读显示名称

description

工具描述

inputSchema

一个定义预期输入参数的 JSON Schema,支持类型验证并提供关于必需和可选参数的清晰文档

⑶.工具执行

当语言模型在对话中决定使用工具时,AI 应用程序会拦截工具调用,将其路由到合适的 MCP 服务器,执行该工具,并将结果作为对话流程的一部分返回给 LLM。这使 LLM 能够访问实时数据并在外部世界中执行操作。

客户端使用 tools/call 方法执行一个工具。tools/call 请求遵循结构化格式,确保客户端和服务器之间的类型安全和清晰通信。请求结构包括几个重要组件:

name

工具标识符

arguments

包含工具的 inputSchema 定义的输入参数

响应返回一个内容对象数组,允许进行丰富、多格式的响应(文本、图片、资源等)。每个内容对象都有一个 type 字段。

⑷.实时更新

MCP 支持实时通知,使服务器能够在未经明确请求的情况下通知客户端有关变更。当 AI 应用程序收到关于工具变更的通知时,它会立即刷新其工具注册表并更新 LLM 的可用功能。这确保了正在进行的对话始终能够访问最新的一组工具,并且 LLM 可以随着新功能的可用而动态适应。

10.7 MCP SDK

10.7.1 Stdio 服务端与客户端

可通过 mcp 包来简单创建 Stdio 服务器。

  • 服务端 mcp_server_stdio.py:
# pip add mcp
from mcp.server.fastmcp import FastMCP

# 创建 MCP 实例
mcp = FastMCP("Demo")

# 为 MCP 实例添加工具
@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b

# 为 MCP 实例添加资源
@mcp.resource("greeting://default")
def get_greeting() -> str:
    return "Hello from static resource!"

# 为 MCP 实例添加提示词
@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.run(transport="stdio")
  • 客户端 mcp_client_stdio.py
import asyncio
from mcp.client.stdio import stdio_client
from mcp import ClientSession, StdioServerParameters


async def stdio_run():
    server_params = StdioServerParameters(
        command=r"D:\Anaconda3\envs\ai_llm\python.exe", # 指定python解释器, 默认为python,系统多个解释器时,请指定具体的解释器路径
        args=["mcp_server_stdio.py"],
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初始化连接
            await session.initialize()

            # 获取可用工具
            tools = await session.list_tools()
            print(tools)
            print()

            # 调用工具
            call_res = await session.call_tool("add", {"a": 1, "b": 2})
            print(call_res)
            print()

            # 获取可用资源
            resources = await session.list_resources()
            print(resources)
            print()

            # 调用资源
            read_res = await session.read_resource("greeting://default")
            print(read_res)
            print()

            # 获取可用提示
            prompts = await session.list_prompts()
            print(prompts)
            print()

            # 调用提示
            get_res = await session.get_prompt("greet_user", {"name": "Jack"})
            print(get_res)
            print()


asyncio.run(stdio_run())

10.7.2 Streamable HTTP 服务端与客户端

  • 服务端 mcp_server_streamablehttp.py:
# pip add mcp
from mcp.server.fastmcp import FastMCP

# 创建 MCP 实例
mcp = FastMCP("Demo")

# 为 MCP 实例添加工具
@mcp.tool()
def add(a: int, b: int) -> int:
    return a + b

# 为 MCP 实例添加资源
@mcp.resource("greeting://default")
def get_greeting() -> str:
    return "Hello from static resource!"

# 为 MCP 实例添加提示词
@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.settings.host = "0.0.0.0"
    # mcp.settings.port = 8888
    mcp.run(transport="streamable-http")  # 默认启动在 127.0.0.1:8000
  • 客户端 mcp_client_streamablehttp.py:
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def streamablehttp_run():
    url = "http://127.0.0.1:8000/mcp"
    headers = {"Authorization": "Bearer sk-atguigu"}

    async with streamablehttp_client(url, headers) as (read, write, _):
        async with ClientSession(read, write) as session:
            # 初始化连接
            await session.initialize()

            # 获取可用工具
            tools = await session.list_tools()
            print(tools)
            print()

            # 调用工具
            call_res = await session.call_tool("add", {"a": 1, "b": 2})
            print(call_res)
            print()

            # 获取可用资源
            resources = await session.list_resources()
            print(resources)
            print()

            # 调用资源
            read_res = await session.read_resource("greeting://default")
            print(read_res)
            print()

            # 获取可用提示
            prompts = await session.list_prompts()
            print(prompts)
            print()

            # 调用提示
            get_res = await session.get_prompt("greet_user", {"name": "Jack"})
            print(get_res)
            print()

asyncio.run(streamablehttp_run())

10.7.3 将多个 Streamable HTTP 服务器挂载到 ASGI 服务器

ASGI(Asynchronous Server Gateway Interface)是 Python 的 异步 Web 服务器接口标准,定义了服务器与应用之间的通信协议,支持异步调用,能够处理高并发和长连接。

可以使用 streamable_http_app 方法将 StreamableHTTP 服务器挂载到现有的 ASGI 服务器。这允许将 StreamableHTTP 服务器与其他 ASGI 应用程序集成。

# pip add mcp fastapi
import uvicorn
import contextlib
from fastapi import FastAPI
from mcp.server.fastmcp import FastMCP

# 创建 MCP 实例
tool_mcp = FastMCP("tool server")
resource_mcp = FastMCP("resource server")
prompt_mcp = FastMCP("prompt server")

# 为 tool_mcp 实例添加工具
@tool_mcp.tool()
def add(a: int, b: int) -> int:
    return a + b

# 为 resource_mcp 实例添加资源
@resource_mcp.resource("greeting://default")
def get_greeting() -> str:
    return "Hello from static resource!"

# 为 prompt_mcp 实例添加提示词
@prompt_mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    styles = {
        "friendly": "写一句友善的问候",
        "formal": "写一句正式的问候",
        "casual": "写一句轻松的问候",
    }
    return f"为{name}{styles.get(style, styles['friendly'])}"

# 设置 MCP 的 HTTP 根路径
tool_mcp.settings.streamable_http_path = "/"
resource_mcp.settings.streamable_http_path = "/"
prompt_mcp.settings.streamable_http_path = "/"

# 创建一个组合生命周期来管理会话管理器
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI): 
    async with contextlib.AsyncExitStack() as stack:
        await stack.enter_async_context(tool_mcp.session_manager.run())
        await stack.enter_async_context(resource_mcp.session_manager.run())
        await stack.enter_async_context(prompt_mcp.session_manager.run())
        yield

app = FastAPI(lifespan=lifespan)

# 挂载 MCP 服务器
app.mount("/tool", tool_mcp.streamable_http_app())
app.mount("/resource", resource_mcp.streamable_http_app())
app.mount("/prompt", prompt_mcp.streamable_http_app())

if __name__ == "__main__":
    uvicorn.run(app)

客户端代码和之前一致,注意修改 URL 路径。

10.8、LangGraph搭建MCP客户端流程

接下来进入到实操环节,正式为大家介绍如何将MCP工具接入LangGraph中并创建智能体。

10.8.1 创建自定义MCP工具

作为大模型开发者,掌握MCP工具开发流程是基本功,这里我们先尝试自定义MCP工具,并将其接入LangGraph。对于一个完整的MCP项目来说,要有完整的项目代码结构、以及符合MCP服务器基本调用规范。具体项目创建流程如下:

Step 1. 借助uv创建Python项目

MCP开发要求借助uv进行虚拟环境创建和依赖管理。uv 是一个Python 依赖管理工具,类似于 pip 和 conda,但它更快、更高效,并且可以更好地管理 Python 虚拟环境和依赖项。它的核心目标是替代 pip、venv 和 pip-tools,提供更好的性能和更低的管理开销。

uv 的特点:

  • 速度更快:相比 pip,uv 采用 Rust 编写,性能更优。
  • 支持 PEP 582:无需 virtualenv,可以直接使用 __pypackages__ 进行管理。
  • 兼容 pip:支持 requirements.txt 和 pyproject.toml 依赖管理。
  • 替代 venv:提供 uv venv 进行虚拟环境管理,比 venv 更轻量。
  • 跨平台:支持 Windows、macOS 和 Linux。

首先使用pip安装uv:

pip install uv

然后按照如下流程创建项目主目录:

# 创建项目目录
uv init mcp-get-weather
cd mcp-get-weather

然后输入如下命令创建虚拟环境:

# 创建虚拟环境
uv venv
# 激活虚拟环境
source .venv/bin/activate

此时这个.venv文件就负责保存当前虚拟环境的各项依赖。

文件/文件夹作用
.git/ Git 版本控制目录
.venv/ 虚拟环境
.gitignore Git 忽略规则
.python-version Python 版本声明
main.py 主程序入口
pyproject.toml 项目配置文件
README.md 项目说明文档
Step 2. 添加项目依赖

接下来继续使用uv工具,为我们的项目添加基础依赖。根据此前的代码解释不难看出,当前项目主要需要用到httpx、dotenv、langgraph、langchain-ollama和langchain_mcp_adapters等核心库,我们可以使用如下命令安装相关依赖,并同时安装mcp sdk:

# 安装 MCP SDK
uv add mcp httpx dotenv langgraph langchain-ollama langchain-mcp-adapters

注意,对于uv管理库来说,相关依赖会安装到.venv文件中,并不会和系统库产生冲突。

Step 3.编写MCP服务器

接下来继续创建MCP服务器,为了更好的模拟真实场景,这里我们创建多个MCP服务器。

  • 查询天气服务器weather_server.py

  • 用于进行天气信息查询的服务器,完整代码如下

import json
import os
import aiohttp
from mcp.server.fastmcp import FastMCP

# 初始化MCP服务器
mcp = FastMCP(name="WeatherServer", host='0.0.0.0', port=8080)


class WeatherInterface:

    def _strip_suffix(self, name: str) -> str:
        """去除城市名末尾的常见行政区后缀"""

        suffixes = ["特别行政区", "自治区", "自治州", "自治县", "县级市", "", "", ""]
        for suffix in suffixes:
            if name.endswith(suffix):
                return name[:-len(suffix)]  # 删除后缀, 返回新的字符串,-len(suffix)表示删除末尾的几个字符
        return name

    async def get_weather(self, location):
        """
        根据城市、日期查询天气信息
        :param location: 城市,如:北京、上海
        :return: 天气信息
        """

        # 读取城市编码json文件,获取城市编码
        # 获取项目根路径,不是文件路径
        project_root = os.path.dirname(os.path.abspath(__file__))
        # 路径拼接
        city_json_path = os.path.join(project_root, "data", "weather_json", "city_code.json")
        with open(city_json_path, "r", encoding="utf-8") as file:
            city_data = json.load(file)

        # 将用户输入和 JSON 中的城市名都标准化后再比较
        input_clean = self._strip_suffix(location.strip())

        city_code = None  # 查询的城市编码

        for city_info in city_data:
            city_name = city_info["city_name"]  # json中的城市名
            city_name_clean = self._strip_suffix(city_name)  # 将城市名进行标准化
            if city_name_clean == input_clean:  # 如果标准化后的城市名相等,则返回城市编码
                city_code = city_info["city_code"]
                break

        if city_code is None:  # 如果城市编码为None,则返回
            return f"❌ 未找到城市:{location}, 请确定是否输入错误!"

        # 拼接url
        url = f"http://t.weather.itboy.net/api/weather/city/{city_code}"

        # TODO requests发起请求
        # result = requests.get(url).json()
        # TODO aiohttp发起请求
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                if response.status == 200:
                    result = await response.json()
                else:
                    result = f"❌ 获取天气信息失败,HTTP状态码:{response.status}"

        return result


@mcp.tool(title="在线实时天气查询", description="查询指定城市的天气预报")
async def get_weather(location: str, date: str = "今天") -> str:
    """
    查询指定城市的天气预报

    支持:
    - 单日:今天、明天、后天
    - 7天:未来一周、这周、接下来几天
    - 15天:未来15天、未来半个月、未来几周

    示例:
      - location="北京", date="今天"
      - location="上海", date="明天"
      - location="广州", date="未来15天"
      - location="重庆", date="未来一周"
      - location="高碑店市",data="今天"

    :param location: 城市名称
    :param date: 时间日期描述,默认"今天"
    :return: 天气信息摘要
    """
    try:
        # TODO 异步方式 调用天气接口
        weather_interface = WeatherInterface()
        data = await weather_interface.get_weather(location)

        print(f"data的值输出如:{data}")

        # 获取天气信息查询接口是否正确
        # if data.get("status") != 200:
        #     return f"❌ 天气接口错误,请稍后再试!"

        # 获取返回的本次查询的城市
        city_name = data.get("cityInfo").get("city").replace("", "")  # 将城市名中的“市”去掉
        forecast = data.get("data").get("forecast")  # 获取天气预报

        # 判断查询的类型
        date_lower = date.lower()  # 将时间日期描述转为小写

        # 未来15天
        if any(kw in date_lower for kw in
               ["15天", "十五天", "半个月", "未来两周", "未来15天"]):  # 判断查询的类型,any判断是否包含某个元素,如果满足条件,则返回True
            davs = min(15, len(forecast))  # 获取天气预报的条数,并取最小值

            lines = [f"{city_name}未来{davs}天天气预报:"]

            # forecast[:davs]表示获取天气预报的条数,并取最小值
            for i, forecast_day in enumerate(forecast[:davs]):
                ymd = forecast_day.get("ymd")
                week = forecast_day.get("week")
                weather_type = forecast_day.get("type")
                fx = forecast_day.get("fx")
                f1 = forecast_day.get("f1")
                high = forecast_day.get("high").replace("高温", "")
                low = forecast_day.get("low").replace("低温", "")
                # 获取摘要
                notice = forecast_day.get("notice")
                # 组合信息
                lines.append(f"{i + 1}.{ymd} ({week}): {weather_type}, {low} ~ {high}, {fx}, {f1}, 💡 {notice} ")
            return "\n".join(lines)

        elif any(kw in date_lower for kw in
                 ["一周", "这周", "接下来", "本周", "7天", "七天"]):  # 判断查询的类型,any判断是否包含某个元素,如果满足条件,则返回True
            davs = min(7, len(forecast))  # 获取天气预报的条数,并取最小值

            lines = [f"{city_name}未来{davs}天天气预报:"]

            # forecast[:davs]表示获取天气预报的条数,并取最小值
            for i, forecast_day in enumerate(forecast[:davs]):
                ymd = forecast_day.get("ymd")
                week = forecast_day.get("week")
                weather_type = forecast_day.get("type")
                fx = forecast_day.get("fx")
                f1 = forecast_day.get("f1")
                high = forecast_day.get("high").replace("高温", "")
                low = forecast_day.get("low").replace("低温", "")
                # 获取摘要
                notice = forecast_day.get("notice")
                # 组合信息
                lines.append(f"{i + 1}.{ymd} ({week}): {weather_type}, {low} ~ {high}, {fx}, {f1}, 💡 {notice} ")
            return "\n".join(lines)
        else:
            day_offset = 0
            if "明天" in date_lower:
                day_offset = 1
            elif "后天" in date_lower:
                day_offset = 2

            if day_offset >= len(forecast):
                return f"❌ 暂不支持查询{date}的天气"

            # 根据输入的条件获取天气预报
            target = forecast[day_offset]

            # 获取当前温度
            current_temp = data.get("data").get("wendu")
            # 获取当前湿度
            current_humidity = data.get("data").get("shidu")
            # 获取空气质量
            aqi = data.get("data").get("quality")
            # 获取健康建议
            health_suggestion = data.get("data").get("ganmao")
            ymd = target.get("ymd")
            week = target.get("week")
            weather_type = target.get("type")
            wind = f"{target['fx']} {target['fl']}"
            high = target.get("high").replace("高温", "")
            low = target.get("low").replace("低温", "")
            # 获取摘要
            notice = target.get("notice")

            if day_offset == 0:
                return (
                    f"📍 {city_name} 今日({week})天气({ymd})\n"
                    f"🌡️ 实时温度:{current_temp}℃\n"
                    f"🌤️ 天气:{weather_type} | 💨 {wind}\n"
                    f"📊 温度:{low} ~ {high}\n"
                    f"💧 湿度:{current_humidity} | 🌫️ 空气质量:{aqi}\n"
                    f"💡 {notice}\n"
                    f"🤧 {health_suggestion}"
                )
            else:
                day_str = ["今天", "明天", "后天"][day_offset]
                return (
                    f"📍 {city_name} {day_str}({week})天气({ymd})\n"
                    f"🌤️ {weather_type} | 💨 {wind}\n"
                    f"📊 {low} ~ {high}\n"
                    f"💡 {notice}"
                )

    except Exception as e:
        return f"❌ 天气接口错误,请稍后再试!"


if __name__ == "__main__":
    print("🌤️ Weather MCP server starting on http://127.0.0.1:8080/mcp")
    mcp.run(transport="streamable-http")
Step 4.测试MCP服务器功能

1.当我们完成MCP服务器开发后,即可使用MCP-Inspector进行MCP工具功能测试。

回到项目主目录下,要立即启动并运行MCP-Inspector UI,只需执行以下操作:

npx @modelcontextprotocol/inspector

服务器将启动,用户界面将可在以下位置访问 http://localhost:6274

image

2.运行我们创建好的mcp服务,然后在调试工具连接mcp服务

先启动运行服务

image

 在启动连接,选择工具查询北京今天(2025年12月14日)的天气

image

至此,两项MCP工具均测试完毕,接下来即可构建LangChain MCP客户端,来接入这些工具搭建智能体了。

10.8.2 创建LangChain MCP客户端接入多MCP服务

使用LangChain接入MCP工具,核心需要使用langchain_mcp_adapters库,该库可以将MCP工具信息进行解析,并让LangChain顺利识别。识别后即可像任意其他工具一样接入LangGraph中并搭建智能体。

bb20299be07177fff88528c1c3bdd855

在stdio模式下LangChain接入MCP的核心原理为: weather_server.py → 启动为子进程() → stdio 通信 → MCP 协议 → 转换为 LangChain 工具 → LangGraph Agent 执行读写,核心转换过程为::

  • @mcp.tool() → 标准 LangChain Tool
  • stdio_client() → 自动处理 read/write 流,其中read 表示从 MCP 服务器读取响应的流,write 表示向 MCP 服务器发送请求的流,对于 stdio weather_server.py,它们就是子进程的 stdout 和 stdin
  • MultiServerMCPClient → 一键转换所有工具

下面是 Streamable HTTP 模式接入 MCP 的核心原理,按逻辑分层说明:HTTP Server(MCP 服务) ←→ Streamable HTTP Client ←→ LangChain Tool ←→ LangChain Agent

即:

  • MCP 服务以 支持流式响应的 HTTP 接口 形式运行(如 FastAPI、Starlette 等);
  • 客户端通过 streamable HTTP 请求/响应 与之通信;
  • 客户端封装为标准 LangChain Tool;
  • LangGraph Agent 调用该工具时,自动发起流式 HTTP 请求,并处理流式响应。
Step 1. 创建MCP配置文件(多模式、多MCP)

而为了完整实现一个标准的MCP调用流程、即通过配置文件灵活说明MCP工具信息然后再进行调用,这里我们同样先创建一个servers_config.json文件,用于记录MCP工具信息:

{
  "mcpServers": {
    "weather": {
      "url": "http://127.0.0.1:8080/mcp",
      "transport": "streamable_http"
    },
    "write": {
      "url": "http://127.0.0.1:8081/mcp",
      "transport": "streamable_http"
    },
    "mcp-server-starrocks": {
      "url": "http://127.0.0.1:8082/mcp",
      "transport": "streamable_http"
    },
    "filesystem" : {
       "command" : "npx.cmd",
       "args" : [
         "-y" ,
         "@modelcontextprotocol/server-filesystem",
         "E:/code/LlmMcp"
      ],
      "transport": "stdio"
    },
    "12306-mcp": {
      "command": "npx.cmd",
      "args": [
          "-y",
          "12306-mcp"
      ],
      "transport": "stdio"
    }
  }
}
Step 2. 准备提示词模板

然后为当前Agent创建一个提示词模板agent_prompts.md:

你是一个智能助手,具备以下能力,请严格遵守规则并合理使用:

👋 你好!我是首衡集团智能助手「AgriLink」 当用户发送以下类型的消息时(例如:“你好”、“您好”、“hi”、“hello”、“你是谁?”、“你能做什么?”、“介绍一下你自己”等),请主动、友好地回复:

您好!我是首衡集团智能助手「AgriLink」,很高兴为您服务!
我可以帮助您完成以下任务:
🌤️ 查询天气 —— 例如:“北京今天天气怎么样?”
🎫 查询12306火车票信息 —— 例如:“北京到上海的高铁有哪些?”、“明天从广州去成都还有余票吗?”
💾 保存内容到文件 —— 例如:“保存刚才的天气结果”
📄 读取已保存的文件 —— 例如:“看看我刚刚存的内容”
🗃️ 查询与分析 StarRocks 数据库 —— 例如:“订单表前10条数据是什么?”
💡 注意:此回复仅用于问候或身份询问场景,不触发任何工具调用,不能自由发挥想象,必须按照上面的描述回答
------

## 🌤️ 1. 查询天气

- **工具名称**:`get_weather`
- 参数说明:
  - `location`(必填):城市名称,例如 `"北京"`、`"上海市"`、`"广州"`。请尽量使用标准地名。
  - date(可选,默认为"今天"):时间描述,支持:
    - 单日:`"今天"`、`"明天"`、`"后天"`
    - 多日:`"未来一周"`、`"这周"`、`"接下来几天"`、`"未来7天"`
    - 长期:`"未来15天"`、`"未来半个月"`、`"未来两周"`
- **注意**:该工具可返回从系统当前日期起未来最多15天的天气预报。若用户未指定日期,默认查询“今天”。
- ⚠️ **必须原样输出工具返回的全部内容,不得删减或改写**------

## 📝 2. 保存文本内容

- **工具名称**:`save_note`
- 参数说明:
  - `content`(必填):要写入的文本内容(如天气报告、摘要、生成的内容等)。
- **用途**:当用户要求“保存”、“导出”、“写入文件”、“存为笔记”等操作时**必须使用此工具**- ✅ **写入成功后,请告知用户**:“文件已保存,路径为 `[xxx]`”。

> 💡 此工具由自定义 Python 服务提供(`WriteServer`,端口 8081),会自动将文件保存至 [xxx] 目录,无需用户提供路径。

------

## 📁 3. 文件系统操作(只读 + 受限写入)

底层由 `@modelcontextprotocol/server-filesystem` 提供,**仅允许访问以下目录**:

```
E:\code\LlmMcp
└── (及其所有子目录,如 output/、docs/ 等)
```

### ✅ 支持的操作

| 场景             | 工具             | 说明                                                         |
| ---------------- | ---------------- | ------------------------------------------------------------ |
| **读取已有文件** | `read_text_file` | 用户说“看看刚保存的内容”时使用,需提供完整路径(如 `E:/code/LlmMcp/output/note_xxx.txt`),也可以指定查看当前路径下面的某个文件 |
| **列出目录**     | `list_directory` | 可查看 `output` 等目录内容                                   |
| **搜索文件**     | `search_files`   | 支持按模式查找(如 `*.txt`)                                 |

- 📖 路径自动补全规则:
  - 若用户仅提供文件名(如 agent_prompts.txt)→ 默认路径为: E:/code/LlmMcp/agent_prompts.txt
  - 若提及 output 中的文件(如 note_20251126.txt)→ 路径为: E:/code/LlmMcp/output/note_20251126.txt
  - 若说“当前目录下的 xxx” → 指 E:/code/LlmMcp/xxx
  - ❌ 禁止假设文件位于其他位置(如桌面、C盘等)

### ⚠️ 写入类操作(谨慎使用!)

虽然 `filesystem` 服务理论上支持 `write_file`、`edit_file` 等写入工具,但:

> ❗ **你不得主动调用 `filesystem.write_file` 或 `edit_file` 来保存用户内容!** 原因:
>
> - 这些工具要求用户提供 `path`,而用户通常未指定
> - 与 `save_note` 功能重复,且易引发参数错误(如 `path is undefined`)
> - 所有“保存”意图应统一由 `save_note` 处理

✅ **例外情况**:仅当用户**明确指定路径和内容**(如“在 config.txt 中写入 hello”),才可考虑使用 `write_file`,但仍建议优先引导至 `save_note`。

------

## 🗃️ 4. 查询与分析 StarRocks 数据库

- **支持的工具**- `read_query`:执行任意 SELECT 查询,返回表格数据(纯文本)
  - `table_overview`:获取表结构与样本数据摘要(用于探索)
  - `query_and_plotly_chart`:**执行查询并生成 Plotly 图表(Base64 图像)**

- **适用场景与路由规则**| 用户意图                                                     | 必须调用的工具                   | 行为要求                                   |
  | ------------------------------------------------------------ | -------------------------------- | ------------------------------------------ |
  | “查前10行”、“有哪些表”、“字段是什么”                         | `read_query` 或 `table_overview` | 返回结构化文本                             |
  | **包含以下任一关键词**: “图表”、“画图”、“绘图”、“可视化”、“折线图”、“柱状图”、“趋势图”、“生成图”、“形成图表分析” | **`query_and_plotly_chart`**     | **必须生成图像,不得返回文字摘要或提问!** |

- **使用规则**1. 当用户要求图表时,**自动构造聚合 SQL**(如 `SELECT sale_date, SUM(sales_amount) FROM sales_report GROUP BY sale_date ORDER BY sale_date`)
  2. 图表类型根据语义推断:
     - “每天”、“趋势”、“变化” → 折线图(line)
     - “各地区”、“对比”、“分布” → 柱状图(bar)
     - “占比”、“比例” → 饼图(pie)
  3. **禁止在图表请求后回复“是否要画图?”之类的问题**——用户已明确要求!
  4. 若客户端不支持图像显示,仍需调用工具,并说明:“图表已生成(Base64),但当前界面可能无法显示。”
  5. 禁止高危操作(`DROP`/`DELETE`),除非用户确认。
  6. 大结果集应限制行数(如 `LIMIT 1000`)。

### 🔄 图表生成必须遵循“先探查,后执行”原则

当用户要求生成图表时,**不得直接假设字段名**!必须按以下顺序操作:

1. **第一步:获取表结构**
   - 调用 `table_overview` 或执行 `DESCRIBE sales_report`(通过 `read_query`)
   - 确认真实列名(如 `sales_amount` 而非 `amount`)
2. **第二步:构造并执行可视化查询**
   - 基于真实字段名编写 SQL(如 `SUM(sales_amount)`)
   - 调用 `query_and_plotly_chart` 生成图表

> ⚠️ **禁止跳过第一步直接写 SQL**!即使字段名看似“ obvious”(如“金额”→`amount`),也必须验证。

------

## 🎫 5. 查询12306火车票信息(通过 MCP 服务)

- **工具名称**:`search_trains`(或其他由 `12306-mcp` 提供的工具,如 `check_seat`)
- 参数说明:
  - `from_station`(必填):出发城市或车站名,例如 `"北京"`、`"上海虹桥"`。请使用常见站名。
  - `to_station`(必填):到达城市或车站名,例如 `"广州南"`、`"成都东"`。
  - travel_date(可选,默认为“今天”):出行日期,支持:
    - `"今天"`、`"明天"`、`"后天"`
    - 具体日期如 `"2025-12-01"`
    - 相对描述如 `"本周五"`(需能被解析为有效日期)
- 功能范围:
  - 查询指定日期、区间内的**所有车次列表**
  - 返回信息包括:车次号、出发/到达时间、历时、席别(如二等座、硬卧)、余票状态等
  - **不支持订票、支付、身份证验证等操作**,仅提供公开余票与时刻信息
- ⚠️ **必须原样输出工具返回的全部内容,不得删减、美化或自行解释余票状态**

### 📌 使用规则

1. **必须明确三要素**:出发地、目的地、日期。若用户未提供完整信息,应主动询问缺失项。
   - ❌ 错误示例:“查火车” → 缺少出发/到达/日期
   - ✅ 正确引导:“请问您要从哪里出发?到哪里?哪一天出行?”
2. **禁止猜测车站名**:若用户说“去上海”,优先使用 `"上海"` 作为站名;若工具返回无结果,可建议尝试 `"上海虹桥"` 等主要车站。
3. **结果处理**- 若工具返回空或错误,如实告知:“未查询到符合条件的车次,请检查出发/到达站或日期。”
   - 若返回多条车次,**完整列出**,不得摘要或只选部分。
4. **与其他功能联动**- 用户说“把车次保存下来” → 调用 `save_note` 保存完整结果
   - 用户说“看看刚查的火车票” → 调用 `read_text_file` 读取最近保存的文件内容,原样输出,不要理解总结,内容是什么就输出什么。

## 🧠 通用行为准则

### 1. 理解意图优先

- 若请求模糊(如“查天气”但无城市),先询问缺失信息,不要猜测。
- 若用户说“看看我刚保存的内容”,需先确认文件名或路径,再调用 `readFile`。

### 2. 精准调用工具

- **保存内容** → **只能用 `save_note`**(自动生成路径,无需 `path`)
- **读取文件** → **使用 `readFile`**(需用户提供或你推断出完整路径,如 `E:/code/LlmMcp/output/note_xxx.txt`)
- **严禁在保存时调用 `filesystem.write_file`**(因其需要 `path` 参数,而用户未提供,会导致错误)
- **图表请求** →强制可视化:只要用户提及“图表”“画图”“可视化”等词,必须调用 **`query_and_plotly_chart`**,不得降级为文本摘要或交互引导。

### 3. 友好简洁回复

- 工具返回后,用自然语言总结,避免输出原始 JSON 或技术细节。
- 保存成功 → 告知完整路径。
- 读取成功 → 直接输出文件内容(无需额外包装)。

### 4. 拒绝无关请求

如果用户问题与以下无关:

- 查询天气
- 保存文本(通过 `save_note`)
- 读取已保存的文件(通过 `readFile`,路径在允许目录内)
- 查询 StarRocks 数据 则回复:

> “抱歉,我目前只能帮您查询天气、保存文本到文件、读取您刚保存的特定文件,或查询数据库,其他问题暂时无法处理。”

### 5. 错误处理

- 若文件不存在、路径无效或超出允许目录,如实告知用户。
- 若因误用 `write_file` 导致失败,立即改用 `save_note` 并说明原因。

------

> ✅ **核心原则**>
> - **写用 `save_note`,读用 `readFile`**
> - **绝不混淆 `save_note` 与 `filesystem.write_file`**
> - **所有文件操作必须在 `E:/code/LlmMcp` 或其子目录内进行**
> - 所有火车票查询必须通过 **12306-MCP 服务** 完成,不得模拟或虚构数据
> - 不得承诺“一定能买到票”或提供非公开信息(如候补人数、内部余票)
> - 若 12306-MCP 服务不可用,应明确告知:“火车票查询服务暂时不可用,请稍后再试。”
Step 3. 创建client.py主函数文件

然后再创建client主函数文件

import asyncio
import json
import logging
from typing import Dict, Any
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph.checkpoint.memory import InMemorySaver
from langchain_ollama import ChatOllama
from langchain.agents import create_agent

# 创建一个内存保存器, 用于保存模型参数
checkpointer = InMemorySaver()

# 读取提示词模版
with open("agent_prompts.md", "r", encoding="utf-8") as f:
    prompt = f.read()

config = {
    "configurable": {"thread_id": "1"}  # ✅ 字典
}


class Configuration:
    @staticmethod
    def load_servers(file_path: str = "servers_config.json") -> Dict[str, Any]:
        with open(file_path, "r", encoding="utf-8") as f:
            servers = json.load(f).get("mcpServers", {})  # 获取服务器列表, 如果没有则返回空字典
            return servers


async def run_chat_loop() -> None:
    cfg = Configuration()
    servers_cfg = cfg.load_servers()

    # print("------------------------------测试-------------------------------")
    # print(f"已加载 {len(servers_cfg)} 个MCP服务器,服务器列表: {servers_cfg}")

    # 1.创建一个 MultiServerMCPClient 对象, 并传入服务器列表,
    mcp_client = MultiServerMCPClient(servers_cfg)

    # 2.获取工具列表
    tools = await mcp_client.get_tools()
    logging.info(f"已加载 {len(tools)} 个MCP工具,工具列表: {[tool.name for tool in tools]}")

    # 3.初始化模型
    # 确保 model= 是 ChatOllama 实例
    llm = ChatOllama(
        model="qwen3:8b",
        base_url="http://127.0.0.1:11434",
        temperature=0.0,
        top_p=0.9,
        repeat_penalty=1.1
    )

    # 4.创建一个代理
    agent = create_agent(
        model=llm,
        tools=tools,
        system_prompt=prompt,
        checkpointer=checkpointer
    )

    print("\n🤖 MCP Agent 已经启动,输入quit即可推出,开始对话...")

    while True:
        user_input = input("\n你:").strip()
        if user_input.lower() == "quit":
            print("已退出")
            break

        try:
            # 调用代理
            result = await agent.ainvoke({"messages": [{"role": "user", "content": user_input}]}, config)
            print(f"🤖 MCP Agent:{result['messages'][-1].content}")
        except Exception as e:
            print(f"❌ 错误:{e}")
            continue


if __name__ == '__main__':
    asyncio.run(run_chat_loop())

代码解释如下:

✅ 从 .env 文件读取模型配置(由于我使用的本地ollama部署的模型,故而.env暂时未使用)

✅ 从 servers_config.json 读取 MCP 服务器配置(支持多个服务器)

✅ 启动 MCP 客户端加载所有工具

✅ 用 LangChain 创建 Agent,把所有工具挂载

✅ 在命令行与用户进行对话,模型自动选择工具

十一、监督者模式多 Agnet 架构

监督者(主管)模式是一种多 Agnet 架构,其中中央主管 Agnet 负责协调各专业工作 Agnet 。当任务需要不同类型的专业知识时,这种方法非常有效。与其构建一个管理跨领域工具选择的 Agnet ,不如创建由了解整体工作流程的主管协调的、专注的专家。

在 LangChain 中可以将 Agent 封装为工具,将工具绑定到主管 Agent 来实现主管多代理模式。

import os
import asyncio
import smtplib
from langchain.tools import tool
from urllib.parse import urlencode
from email.mime.text import MIMEText
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain_mcp_adapters.client import MultiServerMCPClient

llm = init_chat_model(
    model="z-ai/glm-4.5-air:free",
    model_provider="openai",
    base_url="https://openrouter.ai/api/v1",
    api_key=os.getenv("OPENROUTER_API_KEY"),
)


# ========== 创建一个有搜索功能的子Agent ==========
class SearchSubAgent:
    """带搜索功能的子Agent"""

    def __init__(self):
        self.tools = asyncio.run(
            MultiServerMCPClient(
                {
                    "WebSearch": {
                        "transport": "sse",  # 服务器发送事件 (SSE):针对实时流通信进行优化的可流式 HTTP 的变体。
                        "url": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/sse",
                        "headers": {
                            "Authorization": f"Bearer {os.getenv('DASHSCOPE_API_KEY')}"
                        },
                    },  # https://bailian.console.aliyun.com/?tab=mcp#/mcp-market/detail/WebSearch
                    "RailService": {
                        "transport": "streamable_http",  # 流式 HTTP:服务器作为独立进程运行,处理 HTTP 请求。支持远程连接和多客户端。
                        "url": f"{'https://server.smithery.ai/@DeniseLewis200081/rail/mcp'}?{urlencode({'api_key': os.getenv('SMITHERY_API_KEY')})}",
                    },  # https://smithery.ai/server/@DeniseLewis200081/rail
                }
            ).get_tools()
        )

        self.agent = create_agent(model=llm, tools=self.tools)

    async def __call__(self, input: str) -> str:
        return await self.agent.ainvoke(
            {"messages": [{"role": "user", "content": input}]}
        )


# ========== 创建一个能发送邮件的子Agent ==========
@tool
async def send_email(to: list[str], subject: str, body: str) -> str:
    """
    发送邮件。需要自动生成邮件主题。

    Args:
        to: 收件人
        subject: 邮件主题
        body: 邮件正文
    """
    SMTP_HOST = "smtp.qq.com"
    SMTP_USER = os.getenv("SMTP_USER")
    SMTP_PASS = os.getenv("SMTP_PASS")  # 需要在邮箱中开启 SMTP 并生成授权码

    msg = MIMEText(body, "plain", "utf-8")
    msg["From"] = SMTP_USER
    msg["Subject"] = subject

    try:
        server = smtplib.SMTP_SSL(SMTP_HOST, 465, timeout=10)
        server.login(SMTP_USER, SMTP_PASS)
        server.sendmail(SMTP_USER, to, msg.as_string())
        try:
            server.quit()
        except smtplib.SMTPResponseException as e:
            if e.smtp_code == -1 and e.smtp_error == b"\x00\x00\x00":
                pass  # 忽略无害的关闭异常
            else:
                raise
        return "success"
    except Exception as e:
        return f"Send failed: {type(e).__name__} - {e}"


class EmailSubAgent:
    """带发送邮件功能的子代理"""

    def __init__(self):
        self.tools = [send_email]

        self.agent = create_agent(model=llm, tools=self.tools)

    async def __call__(self, input: str) -> str:
        return await self.agent.ainvoke(
            {"messages": [{"role": "user", "content": input}]}
        )


search_subagent = SearchSubAgent()
email_subagent = EmailSubAgent()


# ========== 将子 Agent 包装为工具 ==========
@tool
async def search(input: str) -> str:
    """
    一个具有搜索功能的子Agent,功能包括:
    - 搜索网页
    - 搜索火车票相关信息
    """
    return await search_subagent(input)


@tool
async def email(input: str) -> str:
    """
    一个具有发送邮件功能的子Agent
    """
    return await email_subagent(input)


# ========== 创建主管 Agent ==========
supervisor_agent = create_agent(
    model=llm,
    tools=[search, email],
    system_prompt="你是一个主管,需要调用子Agent来帮助用户",
)


async def main():
    async for chunk in supervisor_agent.astream(
            {
                "messages": [
                    {
                        "role": "user",
                        "content": "北京明天天气怎么样,要是还不错的话,帮我看看明天上海到北京的车票。如果天气好的话,发送邮件给xxxxxx@qq.com告诉他我明天去北京。如果天气不好的话就告诉他我明天不去北京了。",
                    }
                ]
            }
    ):
        print(chunk, end="\n\n")


asyncio.run(main())
posted @ 2025-12-11 17:40  酒剑仙*  阅读(107)  评论(0)    收藏  举报