新版 LangChain Agent 入门:从 `bind_tools + while` 到 `create_agent`

8 阅读5分钟

如果你是在 LangChain 早期版本开始学习 Agent 的,那么你大概率写过类似这样的代码:

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。

它的核心逻辑非常简单:

用户
 ↓
LLM
 ↓
是否需要 Tool?
 ├── 不需要 → 最终回答
 └── 需要
       ↓
      Tool
       ↓
  ToolMessage
       ↓
      LLM
       ↓
  再次判断

但是到了新版 LangChain,这套写法发生了非常明显的变化。

现在推荐使用:

from langchain.agents import create_agent

例如:

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="""
你是一名专业的软件开发助手。

你可以自主调用工具完成任务。
"""
)

调用:

result = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "上海天气怎么样?"
            }
        ]
    }
)

表面上看,我们之前写的一大堆代码突然消失了:

bind_tools
while True
tool_calls
ToolMessage

但这些东西真的没了吗?

并没有。

它们只是被放进了 create_agent 底层的 Agent Runtime。


一、create_agent 到底替我们做了什么?

可以把它近似理解成:

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 驱动的。

你可以把它理解成:

以前:

Agent = while 循环


现在:

Agent = LangGraph 状态图

大致对应:

        Model
          
          
     tool_calls?
      /        \
    yes        no
               
   Tools       END
     
     └────────→ Model

所以,如果你之前学习过 bind_tools + while,那部分知识不仅没有过时,反而是理解新版 Agent 最重要的基础。


二、第一个真正的 Tool

新版 LangChain 仍然使用:

@tool

定义工具。

例如计算器:

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:

agent = create_agent(
    model=model,
    tools=[
        calculator
    ]
)

用户:

123 * 456 等于多少?

模型可能不会直接回答,而是生成:

AIMessage

tool_calls:
[
    {
        "name": "calculator",
        "args": {
            "a": 123,
            "b": 456,
            "operator": "multiply"
        }
    }
]

Agent Runtime 执行:

calculator(
    a=123,
    b=456,
    operator="multiply",
)

得到:

56088

然后形成:

ToolMessage

再次送给模型。

最终:

123 × 456 = 56088

三、为什么 @tool 很重要?

你写:

@tool
def calculator(
    a: float,
    b: float,
    operator: str
):
    """
    执行数学运算
    """

LangChain 会把 Python Function 转成模型可以理解的工具 Schema。

大概类似:

{
  "name": "calculator",
  "description": "执行数学运算",
  "parameters": {
    "type": "object",
    "properties": {
      "a": {
        "type": "number"
      },
      "b": {
        "type": "number"
      },
      "operator": {
        "type": "string"
      }
    }
  }
}

然后一起发给模型。

所以:

Python Function@toolJSON SchemaLLM

模型不是通过:

if "计算" in message:

来决定调用工具。

而是根据:

Tool Name
+
Tool Description
+
Tool Schema
+
用户上下文

自主判断。

因此:

Tool Description 本质上也是 Prompt Engineering。

例如:

@tool
def query_order(order_id: str):
    """处理订单"""

描述很差。

更好的写法:

@tool
def query_order(order_id: str):
    """
    根据订单 ID 查询订单状态。

    当用户询问订单是否发货、
    当前订单状态或物流状态时使用。
    """

模型选择工具的准确率通常会更高。


四、为什么推荐使用 Literal 和 Pydantic Schema?

假设:

operator: str

模型理论上可能传:

{
  "operator": "*"
}

但你的程序只支持:

multiply

所以最好写:

operator: Literal[
    "add",
    "subtract",
    "multiply",
    "divide",
]

这样生成的 Schema 会告诉模型:

你只能从这几个值中选择

对于复杂 Tool,还可以使用 Pydantic:

from pydantic import BaseModel, Field


class CalculatorInput(BaseModel):
    a: float = Field(
        description="第一个数字"
    )

    b: float = Field(
        description="第二个数字"
    )

然后:

@tool(
    args_schema=CalculatorInput
)
def calculator(...):
    ...

所以 Tool 参数定义最好尽量:

明确类型
+
明确范围
+
明确描述

五、一次 agent.invoke() 不等于一次 LLM 请求

这是非常重要的一点。

例如:

agent.invoke(...)

看起来只是调用了一次函数。

实际上可能发生:

LLM 请求 1
 ↓
Tool Call
 ↓
Tool 执行
 ↓
LLM 请求 2
 ↓
最终答案

如果任务复杂:

LLM
 ↓
Search Tool
 ↓
LLM
 ↓
Database Tool
 ↓
LLM
 ↓
Calculator
 ↓
LLM

所以:

一次 Agent Invocation

可能包含:

N 次 LLM
+
N 次 Tool

这也解释了为什么 Agent:

成本更高
延迟更高
行为更复杂

相比普通聊天模型,需要更多工程治理。


六、多个 Tool 可以同时调用

假设用户:

查询上海天气,
告诉我北京时间,
再计算 123 * 456。

模型可能一次生成:

tool_calls:

get_weather(...)
get_current_time(...)
calculator(...)

大致形成:

               Model
                 │
        ┌────────┼────────┐
        ↓        ↓        ↓
    Weather    Time   Calculator
        │        │        │
        └────────┼────────┘
                 ↓
            ToolMessage
                 ↓
               Model

也就是说:

Tool Calling 并不意味着一次只能执行一个 Tool。

这也是后面为什么 Agent State 需要考虑:

并行更新
Reducer
状态冲突

七、FastAPI 中怎么调用 Agent?

我们可以把 Agent 放进 FastAPI。

请求模型:

from pydantic import BaseModel


class ChatRequest(BaseModel):
    message: str

Router:

@router.post("/chat")
async def chat(
    request: ChatRequest
):
    result = await agent.ainvoke(
        {
            "messages": [
                {
                    "role": "user",
                    "content": request.message,
                }
            ]
        }
    )

    return {
        "answer":
            result["messages"][-1].content
    }

这里推荐:

await agent.ainvoke()

而不是:

agent.invoke()

因为 FastAPI Router 是:

async def

Agent 又会调用外部 LLM API。

所以:

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。


九、总结

如果你以前写的是:

bind_tools
+
while
+
ToolMessage

现在可以升级成:

agent = create_agent(
    model=model,
    tools=tools,
)

但千万不要把 create_agent 当成黑盒。

你脑子里应该始终能把它展开成:

用户
 
Model
 
tool_calls?
 ├── no  END
 
 └── yes
      
     Tool
      
 ToolMessage
      
     Model

这就是新版 LangChain Agent 最底层、也最稳定的一层认知。

下一篇我们继续进入真正发生巨大变化的部分:

State、Context、ToolRuntime 和 Middleware。