如果你是在 LangChain 早期版本开始学习 Agent 的,那么你大概率写过类似这样的代码:
py
model_with_tools = model.bind_tools(tools)
messages = [
HumanMessage("帮我查一下上海天气")
]
while True:
response = model_with_tools.invoke(messages)
messages.append(response)
if not response.tool_calls:
break
for tool_call in response.tool_calls:
tool = tools_by_name[tool_call["name"]]
result = tool.invoke(
tool_call["args"]
)
messages.append(
ToolMessage(
content=str(result),
tool_call_id=tool_call["id"],
)
)
这其实已经是一个完整的 Agent Loop。
它的核心逻辑非常简单:
markdown
用户
↓
LLM
↓
是否需要 Tool?
├── 不需要 → 最终回答
└── 需要
↓
Tool
↓
ToolMessage
↓
LLM
↓
再次判断
但是到了新版 LangChain,这套写法发生了非常明显的变化。
现在推荐使用:
py
from langchain.agents import create_agent
例如:
py
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from app.tools import tools
model = ChatOpenAI(
model="gpt-5"
)
agent = create_agent(
model=model,
tools=tools,
system_prompt="""
你是一名专业的软件开发助手。
你可以自主调用工具完成任务。
"""
)
调用:
py
result = agent.invoke(
{
"messages": [
{
"role": "user",
"content": "上海天气怎么样?"
}
]
}
)
表面上看,我们之前写的一大堆代码突然消失了:
python
bind_tools
while True
tool_calls
ToolMessage
但这些东西真的没了吗?
并没有。
它们只是被放进了 create_agent 底层的 Agent Runtime。
一、create_agent 到底替我们做了什么?
可以把它近似理解成:
py
while True:
response = model.invoke(messages)
messages.append(response)
if not response.tool_calls:
break
tool_results = execute_tools(
response.tool_calls
)
messages.extend(tool_results)
也就是说:
create_agent
解决的不是一个新的 Agent 原理,而是:
把以前手写的 Agent Loop 标准化了。
底层仍然是:
Model
↓
Tool
↓
Model
↓
Tool
↓
Model
只不过现在这个循环是由 LangGraph Runtime 驱动的。
你可以把它理解成:
ini
以前:
Agent = while 循环
现在:
Agent = LangGraph 状态图
大致对应:
yaml
Model
│
▼
有 tool_calls?
/ \
yes no
↓ ↓
Tools END
│
└────────→ Model
所以,如果你之前学习过 bind_tools + while,那部分知识不仅没有过时,反而是理解新版 Agent 最重要的基础。
二、第一个真正的 Tool
新版 LangChain 仍然使用:
css
@tool
定义工具。
例如计算器:
py
from typing import Literal
from langchain.tools import tool
@tool
def calculator(
a: float,
b: float,
operator: Literal[
"add",
"subtract",
"multiply",
"divide",
],
) -> float:
"""
执行两个数字之间的数学运算。
"""
match operator:
case "add":
return a + b
case "subtract":
return a - b
case "multiply":
return a * b
case "divide":
if b == 0:
raise ValueError(
"除数不能为 0"
)
return a / b
raise ValueError(
f"不支持的运算:{operator}"
)
然后交给 Agent:
py
agent = create_agent(
model=model,
tools=[
calculator
]
)
用户:
123 * 456 等于多少?
模型可能不会直接回答,而是生成:
py
AIMessage
tool_calls:
[
{
"name": "calculator",
"args": {
"a": 123,
"b": 456,
"operator": "multiply"
}
}
]
Agent Runtime 执行:
py
calculator(
a=123,
b=456,
operator="multiply",
)
得到:
56088
然后形成:
ToolMessage
再次送给模型。
最终:
ini
123 × 456 = 56088
三、为什么 @tool 很重要?
你写:
python
@tool
def calculator(
a: float,
b: float,
operator: str
):
"""
执行数学运算
"""
LangChain 会把 Python Function 转成模型可以理解的工具 Schema。
大概类似:
py
{
"name": "calculator",
"description": "执行数学运算",
"parameters": {
"type": "object",
"properties": {
"a": {
"type": "number"
},
"b": {
"type": "number"
},
"operator": {
"type": "string"
}
}
}
}
然后一起发给模型。
所以:
typescript
Python Function
↓
@tool
↓
JSON Schema
↓
LLM
模型不是通过:
arduino
if "计算" in message:
来决定调用工具。
而是根据:
diff
Tool Name
+
Tool Description
+
Tool Schema
+
用户上下文
自主判断。
因此:
Tool Description 本质上也是 Prompt Engineering。
例如:
py
@tool
def query_order(order_id: str):
"""处理订单"""
描述很差。
更好的写法:
py
@tool
def query_order(order_id: str):
"""
根据订单 ID 查询订单状态。
当用户询问订单是否发货、
当前订单状态或物流状态时使用。
"""
模型选择工具的准确率通常会更高。
四、为什么推荐使用 Literal 和 Pydantic Schema?
假设:
arduino
operator: str
模型理论上可能传:
json
{
"operator": "*"
}
但你的程序只支持:
multiply
所以最好写:
py
operator: Literal[
"add",
"subtract",
"multiply",
"divide",
]
这样生成的 Schema 会告诉模型:
你只能从这几个值中选择
对于复杂 Tool,还可以使用 Pydantic:
py
from pydantic import BaseModel, Field
class CalculatorInput(BaseModel):
a: float = Field(
description="第一个数字"
)
b: float = Field(
description="第二个数字"
)
然后:
py
@tool(
args_schema=CalculatorInput
)
def calculator(...):
...
所以 Tool 参数定义最好尽量:
diff
明确类型
+
明确范围
+
明确描述
五、一次 agent.invoke() 不等于一次 LLM 请求
这是非常重要的一点。
例如:
arduino
agent.invoke(...)
看起来只是调用了一次函数。
实际上可能发生:
sql
LLM 请求 1
↓
Tool Call
↓
Tool 执行
↓
LLM 请求 2
↓
最终答案
如果任务复杂:
sql
LLM
↓
Search Tool
↓
LLM
↓
Database Tool
↓
LLM
↓
Calculator
↓
LLM
所以:
一次 Agent Invocation
可能包含:
diff
N 次 LLM
+
N 次 Tool
这也解释了为什么 Agent:
成本更高
延迟更高
行为更复杂
相比普通聊天模型,需要更多工程治理。
六、多个 Tool 可以同时调用
假设用户:
查询上海天气,
告诉我北京时间,
再计算 123 * 456。
模型可能一次生成:
css
tool_calls:
get_weather(...)
get_current_time(...)
calculator(...)
大致形成:
css
Model
│
┌────────┼────────┐
↓ ↓ ↓
Weather Time Calculator
│ │ │
└────────┼────────┘
↓
ToolMessage
↓
Model
也就是说:
Tool Calling 并不意味着一次只能执行一个 Tool。
这也是后面为什么 Agent State 需要考虑:
并行更新
Reducer
状态冲突
七、FastAPI 中怎么调用 Agent?
我们可以把 Agent 放进 FastAPI。
请求模型:
py
from pydantic import BaseModel
class ChatRequest(BaseModel):
message: str
Router:
py
@router.post("/chat")
async def chat(
request: ChatRequest
):
result = await agent.ainvoke(
{
"messages": [
{
"role": "user",
"content": request.message,
}
]
}
)
return {
"answer":
result["messages"][-1].content
}
这里推荐:
csharp
await agent.ainvoke()
而不是:
arduino
agent.invoke()
因为 FastAPI Router 是:
python
async def
Agent 又会调用外部 LLM API。
所以:
csharp
FastAPI async
↓
Agent async
↓
HTTP async
整个链路更加合理。
八、新版 Agent 最重要的认知
学到这里,最好形成一个新的知识框架。
第一层:
LLM Tool Calling
对应:
bind_tools
tool_calls
ToolMessage
第二层:
LangGraph Runtime
对应:
State
Node
Edge
Conditional Edge
ToolNode
第三层:
LangChain create_agent
对应:
create_agent
Middleware
Runtime Context
Structured Output
Memory
也就是说:
create_agent
↓
LangGraph
↓
Tool Calling
新版 LangChain 并没有把旧知识推翻。
它只是把底层能力:
Tool Calling
逐渐升级成了一个:
可状态化
可扩展
可持久化
可中断
可流式
的 Agent Runtime。
九、总结
如果你以前写的是:
diff
bind_tools
+
while
+
ToolMessage
现在可以升级成:
ini
agent = create_agent(
model=model,
tools=tools,
)
但千万不要把 create_agent 当成黑盒。
你脑子里应该始终能把它展开成:
yaml
用户
↓
Model
↓
tool_calls?
├── no → END
│
└── yes
↓
Tool
↓
ToolMessage
↓
Model
这就是新版 LangChain Agent 最底层、也最稳定的一层认知。
下一篇我们继续进入真正发生巨大变化的部分:
State、Context、ToolRuntime 和 Middleware。