工具是写给模型读的,不是写给人读的。函数名、docstring、参数 schema 三者共同构成模型的「使用说明书」。模型靠这些文本决定要不要调、调哪个、传什么参数。一个含糊的 docstring 会让最强的模型也调错工具。

一、定义工具:@tool 与 args_schema

最快的方式是给普通函数加 @tool 装饰器。LangChain 会自动把函数名变成工具名,把 docstring 变成工具描述,把类型注解推断成参数 schema。

# 安装核心包 + OpenAI 集成(截至 2026 的稳定方式)
pip install -U langchain langchain-core langchain-openai pydantic

# 配置 API key(模型调用需要)
export OPENAI_API_KEY="sk-..."
from langchain_core.tools import tool

@tool
def multiply(a: int, b: int) -> int:
    """计算两个整数相乘的结果。当用户需要做乘法时使用。"""
    return a * b

# @tool 把函数变成 StructuredTool,自带这些属性,全是给模型读的:
print(multiply.name)         # -> multiply
print(multiply.description)  # -> 计算两个整数相乘的结果。当用户需要做乘法时使用。
print(multiply.args)         # -> {'a': {'title': 'A', 'type': 'integer'}, 'b': {...}}

# 工具本身也是 Runnable,可以直接 invoke(用于本地测试):
print(multiply.invoke({"a": 6, "b": 7}))  # -> 42

当参数需要更严格的约束(取值范围、字段说明、必填校验)时,用 args_schema 挂一个 Pydantic 模型。字段的 description 同样会被模型读到,是提升传参准确率的关键。

from langchain_core.tools import tool
from pydantic import BaseModel, Field

class SearchInput(BaseModel):
    query: str = Field(description="要搜索的关键词,使用自然语言,不要带引号")
    top_k: int = Field(default=3, ge=1, le=10, description="返回结果数量,1 到 10 之间")

@tool(args_schema=SearchInput)
def web_search(query: str, top_k: int = 3) -> str:
    """在互联网上搜索实时信息。当问题涉及新闻、天气、最新数据等模型不知道的内容时使用。"""
    # 这里用假数据演示;真实场景接 Tavily / SerpAPI 等
    return f"[模拟搜索] 关于 '{query}' 的前 {top_k} 条结果:LangChain 是一个 LLM 应用框架..."

print(web_search.args_schema.model_json_schema())
# Pydantic 的 ge/le 约束和 description 都会进入 schema,模型据此传参

二、Tool Calling 手动循环:理解 Agent 的底层

在用高层 Agent 之前,必须先看清底层循环。bind_tools 把工具的 schema 绑到模型上,模型在需要时不直接回答,而是返回一个 tool_calls 列表,告诉你「我想调 multiply,参数是 a=6, b=7」。模型不会自己执行,执行是你的活。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 1) 把工具 schema 绑到模型上(模型只是「知道」有这些工具)
tools = [multiply, web_search]
llm_with_tools = llm.bind_tools(tools)

# 工具名 -> 工具对象,方便后面按名分发
tool_map = {t.name: t for t in tools}

messages = [HumanMessage(content="6 乘以 7 等于多少?")]

# 2) 第一次调用:模型决定调工具,content 通常为空,tool_calls 有值
ai_msg = llm_with_tools.invoke(messages)
print(ai_msg.tool_calls)
# -> [{'name': 'multiply', 'args': {'a': 6, 'b': 7}, 'id': 'call_abc', 'type': 'tool_call'}]

messages.append(ai_msg)  # AIMessage 必须先入历史,否则后面 ToolMessage 无处对应

# 3) 执行模型选中的每个工具,把结果包成 ToolMessage 回填
for call in ai_msg.tool_calls:
    selected = tool_map[call["name"]]
    result = selected.invoke(call["args"])           # 真正执行
    messages.append(ToolMessage(content=str(result),
                                tool_call_id=call["id"]))  # id 必须对应

# 4) 把含工具结果的完整历史再喂回模型,得到自然语言终答
final = llm_with_tools.invoke(messages)
print(final.content)  # -> 6 乘以 7 等于 42。

这四步就是 Agent 的全部秘密:模型出 tool_calls → 你执行 → 回填 ToolMessage(靠 tool_call_id 对齐)→ 模型继续。如果模型这一轮又返回新的 tool_calls,就再循环一次;直到 tool_calls 为空、只剩 content,循环结束。AgentExecutor 做的就是把这段 while 自动跑起来。

三、Agent:把循环交给 AgentExecutor

手动循环看清原理后,生产中用封装好的 Agent。create_tool_calling_agent 适配所有支持原生工具调用的现代模型(OpenAI、Anthropic、通义等),是首选。它产出一个负责「决策下一步」的 Runnable,再交给 AgentExecutor 反复执行直到任务完成。

from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate
from langchain.agents import create_tool_calling_agent, AgentExecutor
from pydantic import BaseModel, Field

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# --- 工具 1:计算器 ---
@tool
def calculator(expression: str) -> str:
    """计算一个数学表达式的值,支持 + - * / 和括号。例如 '(3+5)*2'。"""
    try:
        # 演示用,仅允许数字和运算符;生产环境请用安全的表达式求值库
        allowed = set("0123456789+-*/(). ")
        if not set(expression) <= allowed:
            return "错误:表达式含非法字符"
        return str(eval(expression))
    except Exception as e:
        return f"计算出错:{e}"

# --- 工具 2:搜索(带 Pydantic schema) ---
class SearchArgs(BaseModel):
    query: str = Field(description="搜索关键词")

@tool(args_schema=SearchArgs)
def search(query: str) -> str:
    """搜索实时信息,用于回答模型不知道的最新事实,如天气、新闻、汇率。"""
    fake_db = {
        "北京天气": "北京今天晴,气温 24 摄氏度。",
        "langchain 最新版本": "LangChain 当前稳定版为 0.3.x 系列。",
    }
    for k, v in fake_db.items():
        if k in query:
            return v
    return f"未找到关于 '{query}' 的结果。"

tools = [calculator, search]

# Prompt 必须含 agent_scratchpad 占位,用来回填中间工具调用步骤
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个善用工具的助手。需要计算就用 calculator,需要实时信息就用 search。"),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),  # 关键:Agent 在此回填 tool_calls 与结果
])

agent = create_tool_calling_agent(llm, tools, prompt)

executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,        # 打印每一步推理与工具调用,调试必开
    max_iterations=5,    # 兜底:最多循环 5 轮,防止死循环烧 token
    return_intermediate_steps=True,  # 返回中间步骤,便于审计
)

# 一个需要两次工具调用的复合问题
result = executor.invoke({
    "input": "北京今天多少度?把这个温度乘以 3 是多少?"
})
print("\n最终答案:", result["output"])
# 中间步骤里能看到先调 search 拿到 24,再调 calculator 算 24*3=72
场景选哪个原因
想彻底理解原理 / 需完全自定义控制流手动 bind_tools 循环最透明,无封装黑盒,可插任意逻辑
模型支持原生 tool calling(绝大多数现代模型)create_tool_calling_agent准确率高、并行工具、结构化参数
模型无原生工具调用能力(老/小模型)create_react_agent靠文本协议模拟,是唯一退路
需要复杂状态机 / 多 Agent / 循环图升级到 LangGraphAgentExecutor 表达力不够时的方向
口诀懂原理用手动,现代模型用 tool_calling,老模型用 ReAct,复杂编排上 LangGraph
推荐做法
  • 给每个工具写清晰的 docstring:说明「做什么」+「什么时候用」
  • 用 Pydantic args_schema 约束入参,给每个字段写 description
  • Agent 永远设 max_iterations 兜底,调试期开 verbose=True
  • 工具函数内部 try/except,把错误作为字符串返回给模型,让它自我修正
不推荐
  • 不要在 docstring 里只写函数名同义反复(如 multiply 写「相乘函数」却不说何时用)
  • 不要让工具抛裸异常中断整个 Agent,模型看不到错误就无法纠偏
  • 不要在生产里直接 eval 用户输入,演示可以,上线必须换安全求值
  • 不要给 Agent 塞 20 个功能重叠的工具,选择空间过大会降低调用准确率
常见误区
  • Prompt 漏掉 agent_scratchpad 占位符,Agent 会丢失中间步骤直接报错
  • 把 react 的 prompt 用到 tool_calling_agent 上(或反之),变量契约不匹配
  • max_iterations 设太小,复杂任务还没跑完就被截断返回半成品

给一个需要两步工具调用的复合问题,verbose 日志里能看到模型按序调用正确的工具、参数无误,最终在 max_iterations 内给出整合答案。

模型该调工具却直接瞎答

典型表现
明明问了实时天气,模型不调 search 而是凭记忆编一个
判断标准
verbose 日志里压根没有 Invoking search 这一步
解决方向
强化工具 docstring 的「何时使用」描述;system prompt 明确指示「涉及实时信息必须用 search 工具」;确认确实调了 bind_tools / 把 tools 传给了 agent

Agent 反复调同一工具死循环

典型表现
日志里同一个工具被调十几次,token 飞涨
判断标准
中间步骤出现重复的相同 args 调用,无收敛迹象
解决方向
设 max_iterations(如 5)硬兜底;检查工具返回是否对模型有意义(返回空或报错会让它重试);让工具错误以可读字符串返回而非抛异常

ToolMessage 回填报错

典型表现
手动循环时报消息序列非法或 tool_call_id 不匹配
判断标准
异常信息指向 messages 顺序或 id 对应关系
解决方向
确保带 tool_calls 的 AIMessage 先 append;每个 ToolMessage 的 tool_call_id 与对应 tool_call 的 id 一致;一轮内所有 tool_calls 全部执行并回填

Agent 不是魔法,它只是一个会自己决定何时停下的 while 循环。看不懂它的人是没把手动 tool calling 跑通过一遍。

— 本页核心结论

下一章进入 Memory 与对话历史,让 Agent 记住多轮上下文,并补齐 stream / ainvoke / batch 三种执行模式——这是把单次问答升级为可用产品的最后一块拼图。