先搞清楚"工具调用"到底发生了什么
现代 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 个编写原则
- 名字要准确:
search_order比get_data好 100 倍 - docstring 要给 LLM 看的:说清"做什么、参数含义、什么时候用"
- 参数用 Pydantic / 类型注解:LLM 会按 schema 严格输出
- 返回值要可读:返回 str / JSON / markdown,别返回复杂对象
- 错误要捕获:raise 会让 Agent 崩;返回
"错误: ..."能让 LLM 重试 - 副作用要幂等: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 够? | 原因 |
|---|---|---|
| 基础 ReAct | ✅ | prebuilt 就是为此设计 |
| 工具调用前审批 | ❌ | 需要 interrupt_before=["tools"],自己搭图 |
| 检索-重写-检索-回答 | ❌ | 多个非工具节点,要画图 |
| Planner + Executor 分层 | ❌ | 两段式规划,自搭图或 subgraph |
| Multi-Agent handoff | 部分 ✅ | 简单场景 prebuilt 够,复杂见 Ch8 |
本章小结
- ReAct = think → act → think 循环,LangGraph 天然契合
- 手搓三件套:
think/act/should_continue,理解底层 - 生产一行:
create_react_agent(model, tools, prompt) - 工具要 6 原则:好名字、好 docstring、类型注解、可读返回、捕获错误、幂等
- 需要审批 / 多阶段流程 / Multi-Agent → 放弃 prebuilt,自己画图