Chapter 04

ReAct Agent 实战:工具调用的标准范式

ReAct = Reason + Act。LLM 先"想"再"干",干完再"想"。这章从手搓版本开始,过渡到 LangGraph prebuilt 一行搞定。

先搞清楚"工具调用"到底发生了什么

现代 LLM(GPT-4o、Claude、Gemini)内置了 tool_calls 协议。你在请求里附带工具定义,模型输出就能包含"我要调哪个工具、传什么参数":

from langchain_core.tools import tool

@tool
def search_order(order_id: str) -> str:
    """查询订单状态,传入订单号。"""
    return f"订单 {order_id}: 已发货"

@tool
def refund(order_id: str, reason: str) -> str:
    """发起退款。"""
    return f"已为 {order_id} 创建退款单 (理由: {reason})"

TOOLS = [search_order, refund]
tool 三大要素 函数名 + docstring + 参数类型注解。LLM 全靠这三样决定什么时候调、调哪个、传什么。 写清楚 docstring 是 Agent 工具质量的 80%。

手搓 ReAct Agent(理解原理)

from typing import TypedDict, Annotated, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from langchain_core.messages import ToolMessage

llm = ChatOpenAI(model="gpt-4o-mini").bind_tools(TOOLS)
tool_map = {t.name: t for t in TOOLS}

class State(TypedDict):
    messages: Annotated[list, add_messages]

def think(s):
    return {"messages": [llm.invoke(s["messages"])]}

def act(s):
    last = s["messages"][-1]
    out = []
    for call in last.tool_calls:
        result = tool_map[call["name"]].invoke(call["args"])
        out.append(ToolMessage(content=str(result), tool_call_id=call["id"]))
    return {"messages": out}

def should_continue(s) -> Literal["act", END]:
    return "act" if s["messages"][-1].tool_calls else END

g = StateGraph(State)
g.add_node("think", think)
g.add_node("act", act)
g.add_edge(START, "think")
g.add_conditional_edges("think", should_continue)
g.add_edge("act", "think")

app = g.compile()
result = app.invoke({"messages": [{"role": "user", "content": "查一下订单 A123"}]})
print(result["messages"][-1].content)

一次完整执行的 trace

[HumanMessage]    "查一下订单 A123"
[AIMessage]       tool_calls=[{name="search_order", args={"order_id": "A123"}}]
[ToolMessage]     "订单 A123: 已发货"
[AIMessage]       "您的订单 A123 已发货。"

用 prebuilt:一行 ReAct

手搓版本写熟了就不用再写,LangGraph 提供 create_react_agent:

from langgraph.prebuilt import create_react_agent

app = create_react_agent(
    model="openai:gpt-4o-mini",
    tools=TOOLS,
    prompt="你是一个客服助手,简洁回答。",
)

result = app.invoke({"messages": [("user", "帮我退 B456,质量问题")]})

create_react_agent 返回的就是一个 compiled graph,等价于手搓版本。

拆开看:ToolNode + tools_condition

from langgraph.prebuilt import ToolNode, tools_condition

g = StateGraph(State)
g.add_node("llm", lambda s: {"messages": [llm.invoke(s["messages"])]})
g.add_node("tools", ToolNode(TOOLS))    # 内置的 ToolNode 自动跑所有 tool_calls
g.add_edge(START, "llm")
g.add_conditional_edges("llm", tools_condition)  # 有 tool_calls → "tools", 否则 END
g.add_edge("tools", "llm")
app = g.compile()

工具的 6 个编写原则

  1. 名字要准确:search_orderget_data 好 100 倍
  2. docstring 要给 LLM 看的:说清"做什么、参数含义、什么时候用"
  3. 参数用 Pydantic / 类型注解:LLM 会按 schema 严格输出
  4. 返回值要可读:返回 str / JSON / markdown,别返回复杂对象
  5. 错误要捕获:raise 会让 Agent 崩;返回 "错误: ..." 能让 LLM 重试
  6. 副作用要幂等:LLM 会重试,写操作最好带去重 key
# 好的工具示例
from pydantic import BaseModel, Field

class RefundInput(BaseModel):
    order_id: str = Field(description="订单号,格式 X##### 例如 A12345")
    reason: str = Field(description="退款原因,简述")
    amount: float | None = Field(None, description="部分退款金额,留空则全额")

@tool(args_schema=RefundInput)
def create_refund(order_id: str, reason: str, amount: float | None = None) -> str:
    """为指定订单创建退款工单。仅在用户明确要求退款时调用。"""
    try:
        rid = api.create_refund(order_id, reason, amount)
        return f"退款单已创建: {rid}"
    except ApiError as e:
        return f"创建失败: {e}。请检查订单号是否正确。"

state 增加自定义字段

create_react_agent 允许扩展 state,跨节点带业务数据:

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    user_id: str
    lang: str

app = create_react_agent(
    model="openai:gpt-4o-mini",
    tools=TOOLS,
    state_schema=AgentState,
)

result = app.invoke({
    "messages": [("user", "帮我退款")],
    "user_id": "u_42",
    "lang": "zh",
})

动态 Prompt / 动态工具列表

真实场景中,系统提示词和可用工具常要按用户/租户变化:

def prompt_fn(state, config):
    lang = state.get("lang", "zh")
    return [
        ("system", f"你是 {lang} 客服。"),
        *state["messages"],
    ]

app = create_react_agent(
    model="openai:gpt-4o-mini",
    tools=TOOLS,
    prompt=prompt_fn,
)

可视化 + 验证你的图

print(app.get_graph().draw_mermaid())
# 输出:
# graph TD;
#   __start__ --> llm;
#   llm -- tool_calls --> tools;
#   llm -- else --> __end__;
#   tools --> llm;

何时要换成自定义图

需求prebuilt 够?原因
基础 ReActprebuilt 就是为此设计
工具调用前审批需要 interrupt_before=["tools"],自己搭图
检索-重写-检索-回答多个非工具节点,要画图
Planner + Executor 分层两段式规划,自搭图或 subgraph
Multi-Agent handoff部分 ✅简单场景 prebuilt 够,复杂见 Ch8

本章小结