LangChain官方教程

前言

什么是langchain

LangChain作为目前最流行的大模型应用开发框架,可以简单快速地构建由 LLM 驱动的 Agent 和应用程序的方式。

在今年的十月份,langchain也是发布了他的1.0版本,再次掀起了一波热潮。目前市面上类似的大模型开发框架可以说是百花齐放,比如LlamaIndex,AutoGen等,还有各家的adk(Agent Development Kit),而langchain在其中可以算的上最热门的,社区最活跃的框架。所以,对于所有后续有进行大模型应用开发工作的同学们,学习langchain框架是一个稳赚不赔的买卖

为什么要学习langchain

我个人觉得学习任何东西之前,都要先清楚我们学习这个东西的目的,这样才会更有动力去学习他。对于这个问题我想拆分成两个:一个是为什么我要学习这样一个大模型开发框架;另一个是,相对于其他框架,我们为什么要学习langchain

对于第一个问题,我的理解是:

  1. 与模型对话,或者说是调用api,不是像我们平时对话那样,只要把文字传过去,厂商模型就把答案传回来了,其背后有的请求响应是有一定格式的,常见的有openai和anthropic,而框架做的事情就帮我封装对这些复杂请求响应的处理,让我们几行代码就可以实现模型的调用
  2. 如果想搭建agent,光依靠调用模型是不够的,我们需要增加更多的功能,常见的如提示词,记忆功能,检索等,而langchain就帮我们做了这件事
image-20251202163703272

对于第二个问题,我们可以了解一下langchain的优势,如下

LangChain 有四个核心优势:

1. 标准化的模型接口 - 不同提供商有独特的 API,但 LangChain 标准化了交互方式,让你无缝切换提供商,避免被锁定。

2. 易用且高度灵活的 Agent - 简单的 Agent 可以 10 行代码搞定,但也提供足够的灵活性让你进行所有的上下文工程优化。

3. 建立在 LangGraph 之上 - LangChain Agent 使用 LangGraph 构建,自动获得持久化执行、人工介入、流式传输和对话记忆等能力。

4. 用 LangSmith 调试 - 获得深度可观测性,追踪执行路径、捕获状态转换,提供详细的运行时指标。

langchain和langgraph的区别

关键点: LangChain Agents 实际上是建立在 LangGraph 之上的。这意味着:

  • 如果你只是想快速构建 Agent,用 LangChain 就够了
  • 如果你需要复杂的工作流、需要对执行流程进行精细控制,才需要使用 LangGraph
  • 使用 LangChain 时,你不需要了解 LangGraph,但你自动获得了 LangGraph 的所有底层能力(持久化、流式处理、中断等)

资料

langchain的官方文档LangChain overview - Docs by LangChain

langchain与langgraph的官方教程LangChain Academy

大家如果想学习langchain或者langgraph的话,我只推荐官方教程

课前准备

1
git clone https://github.com/zxj-2023/academy-langchain.git

请大家克隆我为大家准备的课件

Lesson 1: Create Agent

image-20251202233412878

这一部分我会带大家构建一个ReAct架构agent,可以通过调用tool,进行sql的查询

初始化

加载并所需的环境变量

1
2
3
4
from dotenv import load_dotenv

# 从 .env 加载环境变量
load_dotenv()

导入实例数据库

1
2
3
from langchain_community.utilities import SQLDatabase

db = SQLDatabase.from_uri("sqlite:///Chinook.db")

Chinook.db 是数据库和 SQL 学习领域最著名的示例数据库(Sample Database)之一。

定义上下文信息,工具与系统提示词

定义运行时上下文,为代理和工具提供指定数据库访问。

1
2
3
4
5
6
7
from dataclasses import dataclass
from langchain_community.utilities import SQLDatabase

# 定义上下文结构以支持依赖注入
@dataclass
class RuntimeContext:
db: SQLDatabase #方便后续传入对应的数据库

Context(上下文)是为 Agent 提供正确信息和工具的方式

该工具将连接数据库,注意使用 get_runtime 访问图的运行时上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain_core.tools import tool
from langgraph.runtime import get_runtime

@tool
def execute_sql(query: str) -> str:
"""Execute a SQLite command and return results."""
runtime = get_runtime(RuntimeContext) # 取出运行时上下文
db = runtime.context.db # 获取数据库连接

try:
return db.run(query)#进行数据库查询
except Exception as e:
return f"Error: {e}"

添加系统提示语以定义代理的行为。

1
2
3
4
5
6
7
8
9
10
SYSTEM_PROMPT = """You are a careful SQLite analyst.

Rules:
- Think step-by-step.
- When you need data, call the tool `execute_sql` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows of output unless the user explicitly asks otherwise.
- If the tool returns 'Error:', revise the SQL and try again.
- Prefer explicit column lists; avoid SELECT *.
"""

你是一名谨慎的 SQLite 分析员。

规则:

按步骤思考。

需要数据时,使用工具 execute_sql 发起「单条」SELECT 查询。

只读:不允许 INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE。

输出默认限制 5 行,除非用户明确要求更多。

如果工具返回 “Error:”,请修改 SQL 再试。

优先写明列名,避免使用 SELECT *。

定义模型与智能体

这里我使用阿里百炼平台的apikey进行测试,对于不同家的模型langchain提供了不同的集成方式,对于qwen模型,我一般会选择langchain_qwq这个包

ChatQwen - Docs by LangChain

langchain-qwq · PyPI — langchain-qwq · PyPI

image-20251203084019522

对于兼容openai格式的模型调用,通常也都可以使用langchain-openai这个包通过修改base_url进行模型的初始化

1
2
3
4
5
6
7
8
from langchain_qwq import ChatQwen
import os
llm=ChatQwen(
model="qwen3-max",
base_url=os.getenv("DASHSCOPE_BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY")
)
llm.invoke("你好")

接下来我们使用langchain快速搭建一个ReAct智能体

1
2
3
4
5
6
7
8
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[execute_sql],#工具
system_prompt=SYSTEM_PROMPT,#系统提示词
context_schema=RuntimeContext,#上下文依赖
)
image-20251203084918070

调用智能体

1
2
3
4
5
6
7
8
question = "Which table has the largest number of entries?"
#哪张表的条目数量最多?
for step in agent.stream(#流式调用
{"messages": question},
context=RuntimeContext(db=db),#上下文依赖
stream_mode="values",#流式调用的模式,langchain提供了多种流式调用的模式
):
step["messages"][-1].pretty_print()

ReAct就是Reasoning + Acting,推理+行动pretty_print 可以更优雅地展示模型与工具之间传递的消息。

运行结果如下

image-20251203091300736

使用langsmith观察过程 https://smith.langchain.com/public/114e9325-12c2-4a6f-a0f1-25087b66278c/r

Lesson 2: Models and Messages

image-20251203094311795

在 LangChain 中,消息是模型上下文的基本单位。它们代表模型的输入和输出,承载与 LLM 交互时表示对话状态所需的内容和元数据。

在langchain中,存在以下几种消息类型

消息类型 角色 用途 需要回应 包含工具 说明
SystemMessage system 系统指示 背景规则
HumanMessage user 用户问题 需要模型回应
AIMessage assistant 模型输出 可包含工具调用
ToolMessage tool 工具结果 必须关联工具调用

为什么我们需要消息类型

如果只有一种消息类型

1
2
3
4
5
6
# 使用纯文本字符串列表 - 模型会困惑!
messages = [
"You are a helpful assistant", # ← 规则?还是问题?
"What is the weather?", # ← 用户问题
"It's sunny", # ← 工具结果?还是用户补充?
]

模型不知道:

  • 哪个是指示,哪个是输入
  • 哪个是工具结果,哪个是用户补充
  • 应该回应什么,什么是背景

使用多种消息类型

1
2
3
4
5
6
7
8
from langchain.messages import SystemMessage, HumanMessage, AIMessage, ToolMessage

messages = [
SystemMessage("You are a helpful assistant"), # ← 清晰的指示
HumanMessage("What is the weather?"), # ← 清晰的用户问题
AIMessage(tool_calls=[{"name": "get_weather"}]), # ← 清晰的 Agent 决策
ToolMessage("It's sunny"), # ← 清晰的工具结果
]

模型立即知道:

  • 系统消息 = 不需要回应,只是规则
  • 人类消息 = 需要处理的问题
  • AI 消息 = 我之前的决策
  • 工具消息 = 工具执行的结果

定义模型

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_qwq import ChatQwen
import os
llm=ChatQwen(
model="qwen3-max",
base_url=os.getenv("DASHSCOPE_BASE_URL"),
api_key=os.getenv("DASHSCOPE_API_KEY")
)
agent = create_agent(
model=llm,
system_prompt="You are a full-stack comedian"
)

调用模型

1
2
3
human_msg = HumanMessage("Hello, how are you?")#定义询问的问题

result = agent.invoke({"messages": [human_msg]})

虽然langchain支持字符串的自动转化,但我还是推荐大家,使用消息列表的形式进行模型的调用,具体示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ✅ 标准方式 1:消息对象列表
from langchain.messages import HumanMessage, SystemMessage
messages = [
SystemMessage("You are helpful"),
HumanMessage("Hi")
]
response = model.invoke(messages)

# ✅ 标准方式 2:字典列表(LangChain 自动转换)
messages = [
{"role": "system", "content": "You are helpful"},
{"role": "user", "content": "Hi"}
]
response = model.invoke(messages)

# ✅ 标准方式 3:字符串(自动转换为 HumanMessage)
response = model.invoke("Hi")

查看消息内容

使用pretty_print()优雅地查看消息信息

1
2
for i, msg in enumerate(result["messages"]):
msg.pretty_print()
1
2
3
4
5
6
7
8
9
================================ Human Message =================================

Hello, how are you?
================================== Ai Message ==================================

Oh, you know—just over here living my best life! 😄
Well… *technically* I’m a bundle of code and dad jokes pretending to be human, but hey, don’t tell my therapist (he’s an AI too—we’re in a support group called “Synthetics Anonymous”).

But seriously—how are *you*? Crushing it? Barely surviving? Or just here for the free emotional support and terrible puns? 😏

消息的字段信息

message的一些字段信息,我们可以做一些了解,还是以这个案例为例,AIMessage的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
AIMessage(
content=(
"Oh, you know—just over here living my best life! 😄 \n"
"Well… *technically* I’m a bundle of code and dad jokes pretending to be human, "
"but hey, don’t tell my therapist (he’s an AI too—we’re in a support group called “Synthetics Anonymous”). \n\n"
"But seriously—how are *you*? Crushing it? Barely surviving? Or just here for the free emotional support and terrible puns? 😏"
),
additional_kwargs={"refusal": None},
response_metadata={
"token_usage": {
"completion_tokens": 95,
"prompt_tokens": 25,
"total_tokens": 120,
"completion_tokens_details": None,
"prompt_tokens_details": {
"audio_tokens": None,
"cached_tokens": 0
}
},
"model_provider": "dashscope",
"model_name": "qwen3-max",
"system_fingerprint": None,
"id": "chatcmpl-d64206cf-e53e-4608-8880-53c014c55f97",
"finish_reason": "stop",
"logprobs": None
},
id="lc_run--ad9df25c-ce4a-4e0d-8a7c-d140a84174e7-0",
usage_metadata={
"input_tokens": 25,
"output_tokens": 95,
"total_tokens": 120,
"input_token_details": {"cache_read": 0},
"output_token_details": {}
}
)
  • content: AI 的完整回复文本。
  • additional_kwargs: 包含 refusal 字段(此处为 None,表示未拒绝回答)。
  • response_metadata: 模型原始响应元数据(含 token 用量、模型名、完成原因等)。
  • usage_metadata: LangChain 标准化的 token 使用统计(新版推荐使用此字段)。
  • id: LangChain 运行时生成的消息 ID(含 lc_run-- 前缀)。

response_metadata 来自底层 LLM API(如 DashScope/Qwen)。

usage_metadata 是 LangChain 对不同模型 token 信息的统一抽象,便于跨模型使用。

完整的 Message 字段清单

字段 类型 必需 说明 示例
type/role str 消息角色 “human”, “ai”, “system”, “tool”
content str | list 消息内容 “Hello”, [{“type”: “text”, “text”: “…”}]
name str 消息发送者名称 “alice”, “assistant”
id str 唯一消息ID “msg_123”
tool_calls list 工具调用(AIMessage) [{“name”: “search”, “args”: {…}}]
tool_call_id str 关联的工具调用ID(ToolMessage) “call_abc123”
response_metadata dict 响应元数据 {“finish_reason”: “tool_calls”}
usage_metadata dict Token 使用统计 {“input_tokens”: 10, “output_tokens”: 5}

Lesson 3: Streaming

流式调用(Streaming)允许你逐块接收 Agent 和模型的输出,而不是等待完整结果,提升用户体验和实时性。invoke() 等待完整响应不同,stream() 会实时返回执行过程中的中间步骤。

langchain提供了几种不同的流式模式

values 模式

1
2
3
4
5
6
7
8
# 流式模式 = values
for step in agent.stream(
{"messages": [{"role": "user", "content": "Tell me a Dad joke"}]},
stream_mode="values",
):
step["messages"][-1].pretty_print()
#print(step)
#print("-----")

这里我觉得要先说一下langgraph才更好理解,由于langchain的底层是由langgraph实现的,所以他的agent同样也是图结构,而values模式的流式可以理解为图的流式,每次agent到一个新的节点,便输出当前的状态(state),这里的话,状态只维护了消息队列

1
2
3
4
{'messages': [HumanMessage(content='Tell me a Dad joke', additional_kwargs={}, response_metadata={}, id='ccc6edf4-aac0-494f-a0cc-bf733e609a59')]}
-----
{'messages': [HumanMessage(content='Tell me a Dad joke', additional_kwargs={}, response_metadata={}, id='ccc6edf4-aac0-494f-a0cc-bf733e609a59'), AIMessage(content="Why don't skeletons fight each other?\n\nBecause they don’t have the guts! 💀\n\n*(leans in with a cheesy grin, then mimes pulling out imaginary guts like spaghetti)* \n...Get it? *Guts?* 😏", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 49, 'prompt_tokens': 24, 'total_tokens': 73, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-6b72b4da-4d7c-4cf0-bc2a-68eadd9da296', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--82c793a0-594d-4f7e-a3a5-ba2144a41797-0', usage_metadata={'input_tokens': 24, 'output_tokens': 49, 'total_tokens': 73, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}
-----

看这个更清晰些,每次流式输出状态,在这里也就是消息队列,第二次流式输出,由于ai回复,增加了AIMessage,但还是把更新后的状态完整地输出出来

updates 模式

updates与values不同的是,只有状态出现更新时,才会流式输出

例如,如果你有一个调用一次工具的代理,你应该看到以下更新:

  • LLM 节点AIMessage]带有工具调用请求
  • 工具节点ToolMessage带有执行结果
  • LLM 节点 :最终 AI 响应
1
2
3
4
5
6
7
# 流式模式 = updates
for step in agent.stream(
{"messages": [{"role": "user", "content": "Tell me a Dad joke"}]},
stream_mode="updates",
):
print(step)
print("-----")
1
2
{'model': {'messages': [AIMessage(content="Why don't skeletons fight each other?\n\nBecause they don’t have the guts! 💀\n\n*(leans in with a cheesy grin, then mimes pulling out imaginary intestines like party streamers)*  \n...Get it? *Guts?* 😏", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 52, 'prompt_tokens': 24, 'total_tokens': 76, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-b18ac1df-986c-4ddb-a6a1-8fbc6f191484', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--be9f295b-3202-47e2-8d79-ef34fe1d8bbe-0', usage_metadata={'input_tokens': 24, 'output_tokens': 52, 'total_tokens': 76, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})]}}
-----

messages 模式

使用 messages 流式模式从你的图中的任何部分(包括节点、工具、子图或任务)流式传输大型语言模型(LLM)的输出, 逐个 token。来自 messages 模式的流式输出是一个元组 (message_chunk, metadata),其中:

  • message_chunk:来自 LLM 的 token 或消息片段。
  • metadata:一个包含有关图节点和 LLM 调用详情的字典。
1
2
3
4
5
for token, metadata in agent.stream(
{"messages": [{"role": "user", "content": "Write me a family friendly poem."}]},
stream_mode="messages",
):
print(f"{token.content}", end="")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
content='' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='哎' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='哟!' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='可' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='算等到' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='你了!我刚刚' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----
content='还在后台急得直跺' additional_kwargs={} response_metadata={'model_provider': 'dashscope'} id='lc_run--1b5367ee-8576-4b09-841b-4b97528f04cf'
-----

流式传输多种模式

你可以将一个列表作为 stream_mode 参数,一次性流式传输多种模式。

流式输出将是 (mode, chunk) 元组,其中mode是流式模式的名字,chunk是该模式流式传输的数据。

1
2
for mode, chunk in graph.stream(inputs, stream_mode=["updates", "custom"]):
print(chunk)

(mode, chunk) 元组的示例结构如下

1
2
('values', {'messages': [HumanMessage(content='What is the weather in SF?', additional_kwargs={}, response_metadata={}, id='0f0b79db-5e22-419f-ab04-128853a7e84d')]})
('custom', 'Looking up data for city: SF')

custom 模式

要在 LangGraph 节点或工具内部发送 自定义用户定义数据 ,请按照以下步骤操作:

  1. 使用 get_stream_writer 访问流写入器并发出自定义数据。
  2. 在调用 .stream().astream() 时设置 stream_mode="custom",以便在流中获取自定义数据。您可以组合多种模式(例如,["updates", "custom"]),但至少必须有一个是 "custom"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from langchain.agents import create_agent
from langgraph.config import get_stream_writer

def get_weather(city: str) -> str:
"""获取指定城市的天气。"""
writer = get_stream_writer()
# 可流式输出任意自定义数据
writer(f"Looking up data for city: {city}") # 推送实时更新(会出现在 stream 的 custom channel)
# ...执行耗时操作(HTTP 请求、爬取等)
writer(f"Acquired data for city: {city}") # 再次推送进度
return f"It's always sunny in {city}!" # 常规返回(会作为 ToolMessage / Tool 的输出被捕获)


agent = create_agent(
model=llm,
tools=[get_weather],
)

# 调用端:同时订阅 values(完整状态)和 custom(工具 writer 输出)
for mode, chunk in agent.stream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode=["values", "custom"], # 同时请求两类流
):
if mode == "values":
# chunk 是当前完整 state 快照(通常包含 messages 列表)
print("LATEST:", chunk["messages"][-1])
print(type(chunk["messages"][-1]))
else:
# mode == "custom": chunk 是工具 writer 推送的文本(或自定义结构)
print("TOOL STREAM:", chunk)
print("-----")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
LATEST: content='What is the weather in SF?' additional_kwargs={} response_metadata={} id='a30e826d-8ab9-4287-a28a-d5ce79617bcd'
<class 'langchain_core.messages.human.HumanMessage'>
-----
LATEST: content='' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 266, 'total_tokens': 287, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-23cb42ff-3d21-4a81-8ec5-44e0650307a6', 'finish_reason': 'tool_calls', 'logprobs': None} id='lc_run--b44564e9-3368-46ae-9e5d-46773a801329-0' tool_calls=[{'name': 'get_weather', 'args': {'city': 'SF'}, 'id': 'call_70a8894b68e744fca508db9c', 'type': 'tool_call'}] usage_metadata={'input_tokens': 266, 'output_tokens': 21, 'total_tokens': 287, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
<class 'langchain_core.messages.ai.AIMessage'>
-----
TOOL STREAM: Looking up data for city: SF
-----
TOOL STREAM: Acquired data for city: SF
-----
LATEST: content="It's always sunny in SF!" name='get_weather' id='d43e7aac-d1e1-4432-8709-666e1647c412' tool_call_id='call_70a8894b68e744fca508db9c'
<class 'langchain_core.messages.tool.ToolMessage'>
-----
LATEST: content="The weather in San Francisco (SF) is always sunny! Let me know if you'd like more details or updates. 😊" additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 26, 'prompt_tokens': 308, 'total_tokens': 334, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_provider': 'dashscope', 'model_name': 'qwen3-max', 'system_fingerprint': None, 'id': 'chatcmpl-5b146ff6-cd99-493d-83d4-ac84a456c678', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--0daa8b02-2207-4acd-abc5-79c7cc79882f-0' usage_metadata={'input_tokens': 308, 'output_tokens': 26, 'total_tokens': 334, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}}
<class 'langchain_core.messages.ai.AIMessage'>
-----

流式消息在时间线上的典型顺序(示例):

  1. 模型生成 AIMessage 并发出工具调用(含 tool_call id)
  2. 流中先返回 values 快照(显示模型决定调用工具)
  3. 工具开始运行并用 stream_writer 推送若干 custom 更新(这些会以 mode="custom" 到达)
  4. 工具完成并返回 ToolMessage(最终的工具结果)
  5. 模型看到 ToolMessage 后生成最终 AIMessage(最终回答),最新 values 会包含这个结果

在 LangChain 的 AIMessage 对象中,只要 tool_calls 字段非空(即 len(message.tool_calls) > 0,就表示模型决定调用一个或多个工具。

Lesson 4: Tools

工具是代理调用来执行操作的组件。它们通过允许模型通过定义良好的输入和输出来与世界交互,从而扩展模型的功能。

工具封装了一个可调用函数及其输入模式。这些可以传递给兼容的聊天模型,允许模型决定是否调用工具以及使用什么参数。在这些场景中,工具调用使模型能够生成符合指定输入模式的请求。

工具的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from typing import Literal

from langchain.tools import tool


@tool(
"calculator",
parse_docstring=True,
description=(
"对两个实数执行基础算术运算。"
"当你需要对任何数字进行运算时都可以使用,即便是整数。"
),
)
def real_number_calculator(
a: float, b: float, operation: Literal["add", "subtract", "multiply", "divide"]
) -> float:
"""对两个实数执行基础算术运算。

Args:
a (float): 第一个操作数。
b (float): 第二个操作数。
operation (Literal['add','subtract','multiply','divide']): 要执行的操作。

Returns:
float: 计算结果。

Raises:
ValueError: 当操作无效或尝试被零除时抛出。
"""
print("🧮 调用计算器工具")
# 执行指定的运算
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
if b == 0:
raise ValueError("不允许被零除。")
return a / b
else:
raise ValueError(f"无效的操作: {operation}")

创建工具最简单的方法是使用 @tool 装饰器。默认情况下,函数的文档字符串成为工具的描述,帮助模型理解何时使用它

类型提示是必需的 ,因为它们定义了工具的输入模式。文档字符串应具有信息量且简洁,以帮助模型理解工具的用途。

LangChain 也支持更丰富的描述,下面的示例使用了一种方式:Google 风格的参数描述。配合 parse_docstring=True 使用时,会解析并将参数描述传递给模型。你可以重命名工具并修改其描述。

工具访问上下文

参数名称 用途
config 保留用于内部传递 RunnableConfig
runtime 保留用于 ToolRuntime 参数(访问状态、上下文、存储)

工具可以通过 ToolRuntime 参数访问运行时信息,该参数提供:

  • 状态State - 在执行过程中流动的可变数据(例如,消息、计数器、自定义字段)
  • 上下文Context - 不可变的配置,如用户 ID、会话详情或特定应用的配置
  • 存储Store - 跨对话的持久长期记忆
  • 流式写入器Stream Writer- 在工具执行时流式传输自定义更新
  • 配置Config - RunnableConfig 用于执行
  • 工具调用 ID Tool Call ID - 当前工具调用的 ID
image-20251203175228797

详细查看文档Tools - Docs by LangChain

调用工具运算

1
2
3
4
5
6
7
8
9
10
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[real_number_calculator],
system_prompt="You are a helpful assistant",
)

result = agent.invoke({"messages": [{"role": "user", "content": "what is 3.0 * 4.0"}]})
print(result["messages"][-1].content)
1
2
result = agent.invoke({"messages": [{"role": "user", "content": "what is 3.0 * 4.0"}]})
print(result["messages"][-1].content)

https://smith.langchain.com/public/d5162eac-47d1-421e-adb9-bf6b223b5618/r

Lesson 5: Tools with MCP

模型上下文协议(MCP)为 AI 代理连接外部工具和数据源提供了标准化方式。让我们用 langchain-mcp-adapters 连接一个 MCP 服务器。

mcp的相关知识这里就不介绍了

mcp工具的获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain_mcp_adapters.client import MultiServerMCPClient
import nest_asyncio

nest_asyncio.apply()

# 连接 mcp-time 服务器以进行时区相关操作
# 该 Go 服务器提供当前时间、相对时间解析、
# 时区转换、时长计算与时间比较等工具
mcp_client = MultiServerMCPClient(
{
"time": {
"transport": "streamable_http",
"url": "https://mcp.api-inference.modelscope.net/adcc1ca6e6d642/mcp"
}
},
)

# 从 MCP 服务器加载工具
mcp_tools = await mcp_client.get_tools()
print(f"Loaded {len(mcp_tools)} MCP tools: {[t.name for t in mcp_tools]}")
1
Loaded 2 MCP tools: ['get_current_time', 'convert_time']

这里看到可以获取两个mcp工具

调用

1
2
3
4
5
result = await agent_with_mcp.ainvoke(
{"messages": [{"role": "user", "content": "What's the time in SF right now?"}]}
)
for msg in result["messages"]:
msg.pretty_print()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
================================ Human Message =================================

What's the time in SF right now?
================================== Ai Message ==================================
Tool Calls:
get_current_time (call_719ae124b2b242fd8648fb27)
Call ID: call_719ae124b2b242fd8648fb27
Args:
timezone: America/Los_Angeles
================================= Tool Message =================================
Name: get_current_time

{
"timezone": "America/Los_Angeles",
"datetime": "2025-12-03T02:27:23-08:00",
"day_of_week": "Wednesday",
"is_dst": false
}
================================== Ai Message ==================================

The current time in San Francisco is 2:27 AM on Wednesday, December 3, 2025.

Lesson 6: Memory

Memory 是让 Agent 记住先前交互信息的系统,分为 短时记忆(Short-term Memory)长时记忆(Long-term Memory) 两种。 它让 Agent 能记住用户偏好、对话历史,实现个性化交互。

LangGraph支持两种对于构建对话代理至关重要的内存类型:

  • 短期内存:通过在会话中维护消息历史来跟踪正在进行的对话。
  • 长期内存:在不同会话之间存储用户特定或应用程序级别的数据。
特性 Checkpointer Store
范围 单线程完整状态 跨线程 key-value
自动保存 每个 super-step 手动调用
用途 对话历史、时间旅行 用户偏好、配置
访问 thread_id namespace + key
image-20250715094855646

在LangGraph中

  • 短期内存也称为线程级内存
  • 长期内存也称为跨线程内存

添加记忆

1
2
3
4
5
6
7
8
9
10
11
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent
from langchain_core.messages import SystemMessage

agent = create_agent(
model=llm,
tools=[execute_sql],
system_prompt=SYSTEM_PROMPT,
context_schema=RuntimeContext,
checkpointer=InMemorySaver(),
)

在langchain中,短期记忆的机制我们称之为checkpointer,以上代码中,我们添加了InMemorySaver作为checkpointer,其作用机制为将 checkpoint 数据保存在进程内存中(字典结构),进程结束即丢失。

如果想上生产环境,推荐使用如from langgraph.checkpoint.postgres import PostgresSaver 进行持久化存储

1
2
3
4
5
6
7
8
9
10
question = "This is Frank Harris, What was the total on my last invoice?"
steps = []

for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
stream_mode="values",
context=RuntimeContext(db=db),
):
step["messages"][-1].pretty_print()
steps.append(step)

在这个例子中,在添加checkpointer后,第一次调用时模型会进行tool call进行sql查询,第二次调用时,模型有了记忆,便直接进行回复

1
2
3
4
5
6
================================ Human Message =================================

我是弗兰克·哈里斯,我上一张发票的总金额是多少?
================================== Ai Message ==================================

弗兰克,您上一张发票的总金额是 **$5.94**。

Lesson 7: Structured Output

image-20251204090512625

结构化输出允许代理以特定的、可预测的格式返回数据。你无需解析自然语言响应,而是直接获得应用程序可以直接使用的 JSON 对象、Pydantic 模型或数据类等结构化数据。

LangChain 的 create_agent 会自动处理结构化输出。用户设置所需的结构化输出模式,当模型生成结构化数据时,它会被捕获、验证,并以代理状态中的 'structured_response' 键返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from langchain.agents import create_agent
from pydantic import BaseModel, Field


class ContactInfo(BaseModel):
"""用户信息结构"""
name: str = Field(..., description="姓名")
email: str = Field(..., description="电子邮件")
phone: str = Field(..., description="电话号码")


agent = create_agent(model=llm, response_format=ContactInfo)

recorded_conversation = """ We talked with John Doe. He works over at Example. His number is, let's see,
five, five, five, one two three, four, five, six seven. Did you get that?
And, his email was john at example.com. He wanted to order 50 boxes of Captain Crunch."""

result = agent.invoke(
{"messages": [{"role": "user", "content": recorded_conversation}]}
)

# 访问结构化响应
result["structured_response"]

Pydantic 的 Field(..., description="...") 主要好处是 自动生成高质量的工具描述、API 文档和模型提示 ,让 LLM 更好地理解字段含义,提高工具调用准确率。

虽然langchain支持大家使用其他方式进行结构化输出,我还是最推荐使用pydantic,原因如下

特性 TypedDict Pydantic BaseModel
类型检查 运行时无验证 运行时验证 + IDE 提示
验证 ❌ 无 ✅ 内置验证(正则、长度、格式)
序列化 手动 model_dump() / model_dump_json()
文档生成 ❌ 手动 ✅ 自动(Field description)
错误处理 静默失败 ✅ 抛出 ValidationError

Lesson 8: Middleware

Middleware是插入 Agent 执行流程的 可插拔组件 ,用于在关键节点拦截、修改或增强数据流,实现 动态提示 状态管理 错误处理 工具控制 等功能。

LangChain 中间件的设计就是 “即插即用” 开发者在使用中间件的过程中,只需要关注中间件的功能与需求是否匹配,不需要关心中间件调用的位置。

middleware 参数是专为 langchain.agents.create_agent() 设计的LangGraph 用节点+边替代了中间件,提供更细粒度控制和更好的性能

image-20251204135208941

LangChain 中间件有 6种类型钩子 ,分为 节点式(Node-style) 包装式(Wrap-style) 两大类。

1️⃣ 节点式钩子(Node-style Hooks)

顺序执行,用于日志、验证、状态更新。

钩子名 执行时机 用途
beforeAgent Agent 开始前(一次) 加载状态、验证输入
beforeModel 模型调用前(每次) 修改提示、裁剪消息
afterModel 模型调用后(每次) 验证输出、防护栏
afterAgent Agent 结束后(一次) 保存结果、清理

2️⃣ 包装式钩子(Wrap-style Hooks)

嵌套执行,控制 handler 调用次数(0/1/多次)。

钩子名 执行时机 用途
wrapModelCall 包装模型调用 重试、缓存、转换
wrapToolCall 包装工具调用 错误处理、权限

wrapModelCall vs beforeModel 区别

特性 beforeModel wrapModelCall
控制粒度 只读:只能修改输入 完全控制:可修改输入输出
返回值 dict | None(状态更新) AIMessage(完整响应)
调用模型 必须调用 ✅ 可选调用
执行时机 模型前 模型前后(包装)
用途 预处理(裁剪、日志) 控制流(重试、缓存)

Dynamic Prompt 动态提示词

静态提示词无法适应不同的用户需求和运行时上下文,而动态提示词让你根据实际情况实时调整LLM的行为。

想象你有一个客服助手。如果你用静态提示词,每个用户都会得到完全相同的回复风格。但实际上:

  • 专家用户想要深入的技术细节
  • 新手用户需要简化的解释
  • VIP用户可能需要更高的优先级处理

动态提示词就是在这一刻决定如何指导LLM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SYSTEM_PROMPT_TEMPLATE = """You are a careful SQLite analyst.

Rules:
- Think step-by-step.
- When you need data, call the tool `execute_sql` with ONE SELECT query.
- Read-only only; no INSERT/UPDATE/DELETE/ALTER/DROP/CREATE/REPLACE/TRUNCATE.
- Limit to 5 rows unless the user explicitly asks otherwise.
{table_limits}
- If the tool returns 'Error:', revise the SQL and try again.
- Prefer explicit column lists; avoid SELECT *.
"""

from langchain.agents.middleware.types import ModelRequest, dynamic_prompt

@dynamic_prompt
def dynamic_system_prompt(request: ModelRequest) -> str:
if not request.runtime.context.is_employee:
table_limits = "- Limit access to these tables: Album, Artist, Genre, Playlist, PlaylistTrack, Track."
#仅限访问这些表:Album、Artist、Genre、Playlist、PlaylistTrack、Track。
else:
table_limits = ""

return SYSTEM_PROMPT_TEMPLATE.format(table_limits=table_limits)

构建动态提示词,根据is_employee的不同,构建不同的提示词

1
2
3
4
5
6
7
8
from langchain.agents import create_agent

agent = create_agent(
model=llm,
tools=[execute_sql],
middleware=[dynamic_system_prompt],
context_schema=RuntimeContext,
)

作为中间件加入create_agent

1
2
3
4
5
6
7
8
question = "Frank Harris 最昂贵的一次购买是什么?"

for step in agent.stream(
{"messages": [{"role": "user", "content": question}]},
context=RuntimeContext(is_employee=False, db=db),
stream_mode="values",
):
step["messages"][-1].pretty_print()

is_employee=False的回复

1
2
3
4
5
6
7
8
================================ Human Message =================================

Frank Harris 最昂贵的一次购买是什么?
================================== Ai Message ==================================

要确定 Frank Harris 最昂贵的一次购买,我们需要查看与他相关的购买记录。然而,在当前可访问的表(Album、Artist、Genre、Playlist、PlaylistTrack、Track)中,并没有包含客户信息或购买记录的表(如 Customer 或 Invoice)。因此,无法直接查询 Frank Harris 的购买信息。

如果您有其他相关表的信息或更多上下文,请提供进一步的细节!

is_employee=True的回复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
================================ Human Message =================================

Frank Harris 最昂贵的一次购买是什么?
================================== Ai Message ==================================

我需要找出 Frank Harris 最昂贵的一次购买。首先,我需要确认数据库中是否有包含客户姓名和购买信息的表。
Tool Calls:
execute_sql (call_9d0bb26b2b5b4d5ca2a24584)
Call ID: call_9d0bb26b2b5b4d5ca2a24584
Args:
query: SELECT name FROM sqlite_master WHERE type='table';
================================= Tool Message =================================
Name: execute_sql

[('Album',), ('Artist',), ('Customer',), ('Employee',), ('Genre',), ('Invoice',), ('InvoiceLine',), ('MediaType',), ('Playlist',), ('PlaylistTrack',), ('Track',)]
================================== Ai Message ==================================

我看到有 `Customer` 表和 `Invoice` 表,可能还有 `InvoiceLine` 表包含购买详情。首先,我会查看 `Customer` 表的结构,以确认如何找到 Frank Harris。
Tool Calls:
execute_sql (call_7bfbcb4928be435ab7bda4c0)
Call ID: call_7bfbcb4928be435ab7bda4c0
Args:
query: PRAGMA table_info(Customer);
================================= Tool Message =================================
Name: execute_sql
...
[(145, '2010-09-23 00:00:00', 13.86)]
================================== Ai Message ==================================

Frank Harris 最昂贵的一次购买是发票号 145,日期为 2010-09-23,总金额为 13.86。

Lesson 9: Human in the Loop

人机回路(HITL)中间件允许您为代理工具调用添加人工监督。当模型提议一个可能需要审核的操作时——例如,写入文件或执行 SQL——中间件可以暂停执行并等待决策。

它通过将每个工具调用与可配置的策略进行比对来实现这一点。如果需要干预,中间件会发出一个中断信号来停止执行。图状态会使用 LangGraph 的持久化层进行保存,因此执行可以安全地暂停并在稍后继续。

然后由人工决策决定下一步的操作:操作可以原样批准(approve)、在运行前进行修改(edit),或附带反馈被拒绝(reject)。

Interrupt 决策类型

中间件定义了三种人类可以响应中断的方式:

决策类型 描述 示例用例
approve 操作按原样批准并执行,无需更改。 按原文发送邮件草稿
✏️ edit 工具调用已修改后执行。 发送邮件前更改收件人
reject 工具调用被拒绝,并在对话中添加了说明。 拒绝邮件草稿并解释如何重写它

每种工具可用的决策类型取决于你在 interrupt_on 中配置的策略。当多个工具调用同时被暂停时,每个操作都需要单独的决策。决策必须按照中断请求中操作出现的顺序提供。

配置Interrupt

要使用 HITL,在创建代理时,将 中间件添加到代理的 middleware 列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver

agent = create_agent(
model=llm,
tools=[execute_sql],
system_prompt=SYSTEM_PROMPT,
checkpointer=InMemorySaver(),
context_schema=RuntimeContext,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"execute_sql": {#工具的名称。当agent尝试调用这个工具时,中间件会拦截
"allowed_decisions": ["approve", "reject"]#这个工具允许的人工决定类型
}
}
),
],
)

产生中断并恢复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from langgraph.types import Command

question = "What are the names of all the employees?"
#所有员工的姓名是什么?
config = {"configurable": {"thread_id": "1"}}

result = agent.invoke(
{"messages": [{"role": "user", "content": question}]},
config=config,
context=RuntimeContext(db=db)
)

if "__interrupt__" in result:# 检查执行结果中是否存在中断
# 获取最后一个中断对象的值
# result['__interrupt__'] 是一个列表,包含所有中断
# ['action_requests'] 获取需要审核的操作列表
description = result['__interrupt__'][-1].value['action_requests'][-1]['description']
print(f"\033[1;3;31m{80 * '-'}\033[0m")
print(
f"\033[1;3;31m Interrupt:{description}\033[0m"
)
print(result['__interrupt__'][-1])
# 调用agent继续执行,传入人工的审核决定
result = agent.invoke(
Command(# 使用Command对象恢复暂停的对话
resume={# resume字段包含人工的决定列表
"decisions": [
{
# 决定类型:拒绝该操作
"type": "reject",
# 拒绝的原因(会作为反馈发给agent)
"message": "the database is offline."#数据库已离线。
}
]
}
),
config=config, # 使用相同的线程 ID 以恢复暂停的对话
context=RuntimeContext(db=db),
)
print(f"\033[1;3;31m{80 * '-'}\033[0m")

print(result["messages"][-1].content)

输出

1
2
3
4
5
6
7
--------------------------------------------------------------------------------
Interrupt:Tool execution requires approval

Tool: execute_sql
Args: {'query': 'SELECT name FROM employees;'}
--------------------------------------------------------------------------------
The database is currently offline. Please try again later.

重要概念:__interrupt__ 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 中断对象 - 表示agent执行暂停,需要人工审核
Interrupt(
# value 字段包含中断的详细信息
value={
# action_requests 列表 - 包含所有需要人工审核的工具调用
'action_requests': [
{
# 工具的名称 - agent想要执行的工具
'name': 'execute_sql',

# args 字典 - 传递给工具的参数
'args': {
# SQL查询语句 - 具体要执行的数据库操作
'query': 'SELECT name FROM employees;'
},

# description 字符串 - 人类可读的完整描述(用于展示给审核员)
'description': "Tool execution requires approval\n\nTool: execute_sql\nArgs: {'query': 'SELECT name FROM employees;'}"
}
],

# review_configs 列表 - 包含审核的策略配置
'review_configs': [
{
# action_name 字符串 - 这个审核策略应用于哪个工具
'action_name': 'execute_sql',

# allowed_decisions 列表 - 人工审核员允许做出的决定类型,这里只允许两种决定:
'allowed_decisions': ['approve', 'reject']
}
]
},

# id 字符串 - 中断的唯一标识符(UUID)
# 用于追踪和关联这个特定的中断请求
# 在日志、审计等场景中用于识别具体是哪次中断
id='eee3e43d71fb8f3747759c8cbe9b5499'
)