前四篇走完了 Model I/O、Chains 和 RAG,所有流程都有一个共同点:执行路径是开发者预先编排好的。这一篇进入四大模块的最后一个——Agents,核心转变只有一句话:把"下一步做什么"的决定权交给模型。本篇讲清楚工具调用的底层机制,以及如何用
create_agent搭出第一个能自主行动的智能体;MCP、记忆与中间件放在下一篇。
一、从 Chain 到 Agent:为什么链不够用
Chain 的工作方式是固定流程:输入按照 prompt | model | parser 的顺序流过每一步,路径在写代码时就定死了。这在很多场景下运行得很好,但有一类问题它天然处理不了——你事先不知道该走哪条路。
举个例子,用户说:"帮我查查北京明天会不会下雨,如果下雨,取消我明天的户外预约。"完成这个任务需要先调天气接口,再根据查到的结果做判断,下雨才调日程接口。问题在于,"是否取消预约"这个分支取决于运行时才知道的天气结果,每次用户输入不同,要走的路径也不同——你没法把它硬编码成一条固定的链。
Agent 的思路是:把模型当作"大脑",让它自己决定下一步做什么。
Chain:开发者编排流程,模型负责执行 → "你告诉它怎么做"
Agent:模型自主编排流程,工具负责执行 → "你告诉它要做什么,它自己想怎么做"
一个完整的 Agent 通常由几个部件协作运转:大模型(大脑,负责推理和规划)、工具(手脚,负责和外部世界交互)、记忆(记住之前发生了什么)、规划与行动(把任务拆解并逐步执行)。其中最基础、也最值得先搞懂的是工具——没有工具的 Agent 就像一个只会说话但没有手脚的人,什么实际操作也完成不了。
二、工具调用的本质:Function Calling
在写第一行 Agent 代码之前,必须先破除一个误解:模型自身并不能执行任何工具。所谓"工具调用",底层是大模型的 Function Calling 能力,完整流程是这样的:
- 你定义工具(函数名 + 参数说明 + 功能描述);
- 框架把工具信息转成 JSON Schema,随提示词一起发给模型;
- 模型阅读工具描述,根据用户问题决定要调用哪个工具、传什么参数;
- 模型返回的不是答案,而是一条**"工具调用指令"**;
- 你的代码在本地执行对应的函数;
- 执行结果拼回消息列表再发给模型,模型继续推理或给出最终回复。
模型的角色是"调度员"而不是"执行者"。这也直接推出一个重要结论:工具的描述写得好不好,决定了模型能不能正确地选择和调用它——描述就是你和模型之间的说明书。
为了体感这个机制,先看不用 LangChain、直接用 OpenAI SDK 的原生写法(理解原理用,实际开发不需要这样写):
from openai import OpenAI
import json
client = OpenAI()
# 第1步:手写 JSON Schema 描述工具
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市在指定日期的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "description": "日期,格式为YYYY-MM-DD"},
},
"required": ["city", "date"],
},
},
}]
# 第2步:工具的实际执行逻辑
def get_weather(city, date):
return f"{city} 在 {date} 天气多云,有下雨的可能。" # 实际项目中调用真实天气API
# 第3步:用户消息和工具描述一起发给模型
messages = [{"role": "user", "content": "北京2026-07-25的天气怎么样?"}]
response = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=tools)
# 第4、5步:模型返回"工具调用指令",我们在本地执行,把结果喂回去
messages.append(response.choices[0].message)
for tool_call in response.choices[0].message.tool_calls []:
args = json.loads(tool_call.function.arguments)
result = get_weather(**args)
messages.append({
: ,
: tool_call.,
: json.dumps({: result}),
})
final = client.chat.completions.create(model=, messages=messages, tools=tools)
(final.choices[].message.content)
能跑,但繁琐得很:手写 Schema、手动解析指令、手动喂回结果、手动管理消息列表。工具一多、调用轮次一多,代码量爆炸式增长。这就是 LangChain 存在的意义——把这些脏活全部封装掉。
三、用 @tool 定义工具
LangChain 的 @tool 装饰器让工具定义回归到"写一个普通 Python 函数":
from langchain.tools import tool
@tool
def get_weather(city: str, date: str) -> str:
"""获取指定城市在指定日期的天气。
:param city: 城市名称,如"北京"、"上海"
:param date: 日期,格式为YYYY-MM-DD
:return: 天气信息
"""
return f"{city} 在 {date} 天气多云,有下雨的可能。"
# 工具本身也是 Runnable,可以直接 invoke 测试
print(get_weather.invoke({"city": "北京", "date": "2026-07-25"}))
装饰器会自动根据函数签名生成 JSON Schema、根据 docstring 生成工具描述——上一节手写的十几行 Schema 就这样被一个装饰器替代了。也可以显式传 @tool(description="..."),显式的 description 优先于 docstring。
影响模型调用准确性的三个关键点:
| 要素 | 作用 | 写法建议 |
|---|---|---|
| 函数名 | 模型据此初步判断工具用途 | 清晰的动词+名词,如 get_weather、search_documents |
| docstring | 模型据此理解工具的具体功能 | 写清楚"这个工具做什么、什么时候用",越具体越好 |
| 参数类型注解 | 模型据此决定传什么值 | 每个参数都要有类型注解和说明 |
最常见的错误就是 docstring 写得太简略(比如只写"查天气"),模型不确定什么时候该用、该传什么,调用准确率随之下降。
参数结构复杂时,可以用 Pydantic 模型来定义参数,约束更精确:
from pydantic import BaseModel, Field
class GetWeatherArgs(BaseModel):
"""天气查询参数"""
city: str = Field(description="城市名称,如'北京'、'上海'")
date: str = Field(description="查询日期,格式为YYYY-MM-DD")
@tool(description="获取指定城市在指定日期的天气", args_schema=GetWeatherArgs)
def get_weather(city: str, date: str) -> str:
return f"{city} 在 {date} 天气多云,有下雨的可能。"
四、bind_tools:手动走一遍调用闭环
在直接上 Agent 之前,值得先用 bind_tools 手动把"模型选工具 → 执行 → 喂回结果"的闭环走一遍——Agent 内部自动做的正是这件事,亲手做一次,后面遇到问题才知道去哪排查:
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, ToolMessage
model = init_chat_model("openai:gpt-4o-mini").bind_tools([get_weather])
messages = [HumanMessage("获取北京2026-07-25的天气情况")]
# 第一轮:模型返回工具调用指令(AIMessage.tool_calls)
result = model.invoke(messages)
messages.append(result)
tool_call = result.tool_calls[0]
# tool_call 形如 {"name": "get_weather", "args": {"city": "北京", ...}, "id": "..."}
# 执行工具,把结果包成 ToolMessage 喂回去
tool_result = get_weather.invoke(tool_call["args"])
messages.append(ToolMessage(content=tool_result, tool_call_id=tool_call["id"]))
# 第二轮:模型基于工具结果生成最终回复
final = model.invoke(messages)
print(final.content)
注意 ToolMessage 必须带上 tool_call_id——一轮里可能有多个工具调用,模型靠这个 id 把结果和指令对上号。这段代码依然是"手动挡":如果模型看完工具结果决定还要再调一次工具呢?你就得把这个循环写成 while。而这个"推理 → 调用 → 观察 → 再推理"的循环,正是 Agent 框架要替你管理的东西。
五、create_agent:三行搭出一个 Agent
LangChain 1.x 用 create_agent 构建 Agent(底层基于 LangGraph,跑的是经典的 ReAct 循环——推理与行动交替,直到得出最终答案)。消息管理、指令解析、结果回传、循环控制,全部由它接管:
from langchain.agents import create_agent
from langchain_tavily import TavilySearch # pip install langchain-tavily
search = TavilySearch(max_results=5) # 第三方联网搜索工具
agent = create_agent(
model="openai:gpt-4o-mini", # Agent 的大脑(也可传模型实例)
tools=[get_weather, search], # 自定义工具和第三方工具混着放
system_prompt="你是一个智能助手,请根据用户的需求调用合适的工具来帮助他们。",
)
核心参数一览:
| 参数 | 作用 | 是否必填 |
|---|---|---|
model | 模型标识字符串或模型实例,Agent 的大脑 | 必填 |
tools | 工具列表,Agent 的手脚 | 必填 |
system_prompt | 系统提示词,指导 Agent 的行为风格 | 可选 |
checkpointer | 记忆存储(下一篇详讲) | 可选 |
middleware | 中间件列表(下一篇详讲) | 可选 |
调用方式有两种。invoke 等 Agent 完成所有推理和工具调用后一次性返回;注意输入输出都围绕一个 messages 状态——这是 LangGraph 架构下 Agent 管理对话状态的方式:
result = agent.invoke(
{"messages": [{"role": "user", "content": "北京2026-07-25的天气怎么样?"}]}
)
print(result["messages"][-1].content) # 最后一条消息即最终回复
stream 则实时吐出每一步的执行状态,能清楚看到"模型决定调工具 → 工具返回结果 → 模型给出回复"的全过程:
for step in agent.stream(
{"messages": [{"role": "user", "content": "北京2026-07-25的天气怎么样?"}]}
):
print(step, end="\n\n")
开发调试阶段用 stream 观察 Agent 每一步在干什么,方便排查;生产环境按产品形态选——聊天界面适合流式,后台任务适合一次性调用。
六、完整示例:让 Agent 自己规划多步任务
把本篇的知识点串成一个可直接运行的例子——三个工具,一个需要多步推理的问题:
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_tavily import TavilySearch
@tool
def calculate(expression: str) -> str:
"""计算数学表达式的结果。
:param expression: 数学表达式,如 "2 + 3 * 4"、"100 / 7"
:return: 计算结果
"""
try:
return f"计算结果:{expression} = {eval(expression)}"
except Exception as e:
return f"计算出错:{e}"
@tool
def get_current_date() -> str:
"""获取当前日期和时间,不需要任何参数。"""
from datetime import datetime
return datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
agent = create_agent(
model=init_chat_model("openai:gpt-4o-mini"),
tools=[calculate, get_current_date, TavilySearch(max_results=3)],
system_prompt="""你是一个智能助手,拥有以下能力:
- 计算数学表达式
- 查询当前日期时间
- 搜索网络信息
请根据用户的问题选择合适的工具。不需要工具就能回答的,直接回答即可。""",
)
for i, step in enumerate(agent.stream(
{"messages": [{"role": "user", "content": "今天是几号?距离明年五一还有多少天?"}]}
), start=):
()
(step, end=)
运行时你会看到,Agent 在没有任何流程编码的情况下自己完成了规划:先调 get_current_date 拿到今天的日期,再(视模型习惯)调 calculate 算天数差,最后组织语言回复。哪一步调哪个工具、调几轮,全是模型在运行时决定的——这正是第一节里 Chain 做不到的事。
顺便提一个安全常识:示例里的 eval 只是教学演示,生产代码里应换成安全的表达式求值方案,永远不要让模型生成的字符串直接进 eval。
七、调试:LangSmith
Agent 的执行路径是动态的,出了问题(选错工具、传错参数、陷入循环)光看最终输出很难排查。LangChain 官方的 LangSmith 平台可以记录每次运行的完整轨迹——每轮推理、每次工具调用、每个参数和返回值。启用只需要设置环境变量:
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "你的API Key"
os.environ["LANGSMITH_PROJECT"] = "my-agent-project"
配好之后代码零改动,每次运行的轨迹自动上报到平台,可视化回溯。写 Agent 强烈建议一开始就把它挂上——它解决的正是"Agent 为什么这么做"这个最难回答的问题。
八、小结与下一步
本篇完成了从 Chain 到 Agent 的观念转变和第一个能跑的智能体:
- Chain vs Agent:链是开发者定路径,Agent 是模型定路径,适用于运行时才能确定分支的任务;
- Function Calling 本质:模型只下达"调用指令",执行永远在你的代码里,工具描述是模型选对工具的关键;
@tool:函数签名生成 Schema、docstring 生成描述,复杂参数用 Pydanticargs_schema;bind_tools:手动闭环一次,理解 Agent 内部循环在做什么;create_agent:模型 + 工具 + 系统提示词三要素,invoke拿结果、stream看过程,状态围绕messages管理。
但现在这个 Agent 还有三个很现实的问题:想接别人做好的工具(比如查火车票)得自己从头封装;每次调用都是失忆的,上一轮说过的话下一轮全忘;敏感操作(转账、删数据)模型说执行就执行,没有人工把关。这三个问题分别对应 MCP 协议、记忆管理和中间件——下一篇《Agents 下篇》一次讲完,也为整个系列收尾。
老规矩,以官方文档为准,把每段代码亲手跑一遍。







还没有公开留言,成为第一个写下回声的人。