在 LangChain/LangGraph 生态里Function Calling 和 Tool Calling 指的是同一能力——都是让 LLM 输出结构化请求、由后端执行后再把结果喂回模型的一段协议。差别主要在历史演进和工程封装层级上OpenAI 在 2023 年 6 月先用functions/function_call参数推出这个功能同年 11 月升级为tools/tool_choice/tool_calls旧参数已废弃现代代码统一用tools。“Tool Calling” 是更通用的演进形态它不仅涵盖函数还包括 Web 搜索、代码解释器、MCP 服务等更广泛的工具类型并支持并行调用。一、核心概念Function Calling 与 Tool Calling 的真实关系在 LangChain 语境里Function Calling是 OpenAI 2023 年 6 月首次推出时的原始 API 形态参数为functionsfunction_call。Tool Calling是 2023 年 11 月之后的统一演进形态参数为toolstool_choicetool_calls字段支持一次返回多个调用并行。两者在 2026 年的现代用法中完全同义OpenAI 官方文档明确写了 “Function calling (also known as tool calling)”。各家的对应叫法平台术语控制参数OpenAIFunction calling / Tool callingtool_choiceAnthropic ClaudeTool usetool_choiceGoogle GeminiFunction callingtool_configAzure OpenAITool callingtool_choice一次 Tool Call 的完整生命周期5 步循环开发者用 JSON Schema 定义工具放进tools参数用户提问模型根据上下文决定要不要调工具模型返回tool_calls:[{id, type:function, function:{name, arguments}}]finish_reason为tool_calls后端真实执行函数拿到结果把结果包装成role:tool、tool_call_id对应的消息回传模型模型生成最终回复 关键点模型本身不执行函数它只产生结构化调用请求。真正的执行、副作用控制、错误处理都在后端。在 LangChain 里工具被抽象成BaseTool包含可调用函数 输入 schema 描述元数据。模型靠 description 选择工具靠 schema 约束参数。二、LangChain 侧的 Tool 抽象LangChain 把工具统一封装为BaseTool核心能力由tool装饰器提供fromlangchain_core.toolsimporttoolfrompydanticimportBaseModel,FieldfromtypingimportLiteral# 简单工具docstring 自动成为 description类型提示推断 schematooldefsearch_database(query:str,limit:int10)-str:在客户数据库中搜索匹配查询的记录。 Args: query: 要查找的搜索词 limit: 返回结果的最大数量 returnf找到{limit}个关于 {query} 的结果更复杂的场景用 Pydantic 模型定义args_schemaclassWeatherInput(BaseModel):天气查询的输入。location:strField(description城市名称或坐标)units:Literal[celsius,fahrenheit]Field(defaultcelsius,description温度单位偏好)include_forecast:boolField(defaultFalse,description是否包含5天预报)tool(args_schemaWeatherInput)defget_weather(location:str,units:strcelsius,include_forecast:boolFalse)-str:获取当前天气及可选预报。temp22ifunitscelsiuselse72resultf{location}当前天气:{temp}度{units[0].upper()}ifinclude_forecast:result\n未来5天: 晴天returnresulttool装饰器的关键参数description覆盖 docstring作为给模型的工具说明args_schema用 Pydantic 或 JSON Schema 精确控制输入return_directTrue短路 Agent 循环工具输出直接作为最终答案返回不再过一遍 LLMparse_docstringTrue从 docstring 的Args:段解析字段描述InjectedToolCallId用于在工具内拿到本次调用的 ID方便返回ToolMessagefromtypingimportAnnotatedfromlangchain_core.messagesimportToolMessagefromlangchain_core.toolsimporttool,InjectedToolCallIdtooldeffoo(x:int,tool_call_id:Annotated[str,InjectedToolCallId])-ToolMessage:Return x.returnToolMessage(str(x),artifactx,namefoo,tool_call_idtool_call_id)抛ToolException可以让工具有控制地把错误回传给 Agent而不是中断流程fromlangchain_core.toolsimportToolExceptiontooldefrisky_tool(param:str)-str:raiseToolException(服务暂时不可用请稍后重试)三、用 LangChain 快速搭一个 Tool-Calling Agent最简单的做法是用 LangGraph 预构建的create_react_agentfromlangchain_openaiimportChatOpenAIfromlanggraph.prebuiltimportcreate_react_agentfromlangchain_core.toolsimporttooltooldefget_weather(city:str)-str:获取指定城市的当前天气。# 实际应用中调用天气 APIreturnf{city}: 22°C, 晴tooldefsend_email(to:str,subject:str,body:str)-str:发送邮件。# 实际应用中调用邮件服务returnf邮件已发送至{to}# 1. 初始化支持 tool calling 的模型modelChatOpenAI(modelgpt-4o,temperature0)# 2. 把工具交给 Agent —— 内部就是 LangGraph 图tools[get_weather,send_email]agentcreate_react_agent(model,tools)# 3. 调用resultagent.invoke({messages:[{role:user,content:北京天气怎么样如果低于 25 度就提醒我穿外套}]})print(result[messages][-1].content)create_react_agent在底层构建的就是一个 LangGraph 图LLM 节点 → 条件路由 → ToolNode → 回到 LLM形成 ReAct 循环。四、用 LangGraph 显式编排生产级当工具调用涉及重试、分支、人工确认、长任务恢复时应该显式写图fromtypingimportTypedDict,Annotatedimportoperatorfromlanggraph.graphimportStateGraph,START,ENDfromlanggraph.prebuiltimportToolNodefromlangchain_openaiimportChatOpenAIfromlangchain_core.messagesimportToolMessagefromlangchain_core.toolsimporttool# ---------- 1. 定义状态 ----------classAgentState(TypedDict):messages:Annotated[list,operator.add]# 消息累积error:Annotated[list,operator.add]# 错误累积# ---------- 2. 定义工具 ----------tooldefget_weather(city:str)-str:获取天气。returnf{city}: 22°Ctools[get_weather]tool_nodeToolNode(tools,handle_tool_errorsTrue)# ---------- 3. 定义 LLM 节点 ----------modelChatOpenAI(modelgpt-4o,temperature0).bind_tools(tools)defcall_model(state:AgentState):msgsstate[messages]responsemodel.invoke(msgs)return{messages:[response]}# ---------- 4. 路由函数判断是否继续调工具 ----------defshould_continue(state:AgentState)-Literal[tools,END]:laststate[messages][-1]# 如果模型产生了 tool_calls且不是错误兜底则去执行工具ifgetattr(last,tool_calls,None):returntoolsreturnEND# ---------- 5. 组装图 ----------builderStateGraph(AgentState)builder.add_node(agent,call_model)builder.add_node(tools,tool_node)builder.add_edge(START,agent)builder.add_conditional_edges(agent,should_continue,{tools:tools,END:END})builder.add_edge(tools,agent)# 工具结果回传模型形成循环# 6. 编译时挂载 checkpointer支持断点恢复fromlanggraph.checkpoint.memoryimportMemorySaver checkpointerMemorySaver()graphbuilder.compile(checkpointercheckpointer)# 7. 调用带 thread_id 以支持恢复config{configurable:{thread_id:session-001}}resultgraph.invoke({messages:[{role:user,content:北京天气怎么样}]},configconfig)这张图和create_react_agent的本质区别状态、路由、循环完全在掌控中。你可以插入重试逻辑在should_continue里检查state[error]长度超过阈值走兜底人工确认在tools节点前插一个human_approval节点敏感操作暂停等人类输入LangGraph 的interrupt分支不同工具结果路由到不同下游节点Checkpointgraph.get_state(config)可以取回状态graph.invoke(None, config)可以从断点恢复五、并行工具调用现代模型支持一次返回多个tool_calls。在 LangGraph 里ToolNode会自动并行执行这些调用。如果要在自己的图里手动并行fromlanggraph.graphimportStateGraph,START,END# 让 plan 节点分叉到多个独立检索节点builder.add_edge(plan,search_A)builder.add_edge(plan,search_B)# 汇合节点前State 里要用 reducer 合并classState(TypedDict):results:Annotated[list,operator.add]# 多个分支 append 到这里⚠️ 并行陷阱结果顺序并行节点的返回顺序不确定不要在汇合节点依赖下标要用带标识的数据结构部分失败一个分支失败可能导致整个 fan-in 卡住要做超时和降级状态合并共享字段必须有 reducer 函数否则后面的写入覆盖前面的六、生产环境的关键坑点坑 1工具名和 schema 的跨模型兼容性不同模型对工具名、参数格式的要求不同。一些模型提供商对包含空格或特殊字符的名称会有问题或拒绝。# ✅ 推荐snake_case字母数字下划线/连字符tool(web_search)defsearch(query:str)-str:...# ❌ 避免Web Search、 get-weather! 等坑 2参数校验必须由你来做模型可能产出不符合 schema 的参数甚至 JSON 解析失败。务必在工具入口做防御tooldeftransfer_money(from_account:str,to_account:str,amount:float)-str:# 1. 类型与范围校验ifamount0:raiseToolException(转账金额必须大于 0)# 2. 业务校验ifnotis_valid_account(from_account):raiseToolException(f账户{from_account}不存在)# 3. 权限校验ifnothas_permission(context.user_id,transfer):raiseToolException(无转账权限)# 4. 执行带幂等键returnexecute_transfer(from_account,to_account,amount,idempotency_key...)坑 3副作用工具必须幂等 人工确认发邮件、删数据、下订单、退款——这些动作不能让模型随意触发。模式defshould_continue(state):laststate[messages][-1]ifnotlast.tool_calls:returnEND# 敏感工具走人工确认节点sensitive{send_email,delete_record,refund}ifany(tc[name]insensitivefortcinlast.tool_calls):returnhuman_approvalreturntools配合 LangGraph 的interruptfromlanggraph.typesimportinterruptdefhuman_approval(state):decisioninterrupt({question:确认执行敏感操作,tool_calls:state[messages][-1].tool_calls})ifdecisionapprove:return{messages:[],approved:True}else:return{messages:[ToolMessage(content用户拒绝了操作,tool_call_id...)]}坑 4错误处理的三个层级层级策略实现瞬时错误超时/429指数退避重试retry(stop_after_attempt(3), waitwait_exponential(...))工具失败Fallback 到备用数据源在 tool_node 外包一层 try/except调用备用工具不可恢复人工介入 / 明确报错LangGraph 的interrupt或返回ToolExceptionfromtenacityimportretry,stop_after_attempt,wait_exponentialretry(stopstop_after_attempt(3),waitwait_exponential(multiplier1,min4,max10))defcall_unreliable_api(ticker:str):responserequests.get(fhttps://api.example.com/quote/{ticker})response.raise_for_status()returnresponse.json()deftool_node_with_fallback(state):try:datacall_unreliable_api(state[ticker])exceptExceptionase:# 降级用缓存价格dataget_cached_price(state[ticker])ifnotdata:# 升级人工介入send_slack_alert(fPrice check failed for{state[ticker]})state[needs_human]Truereturnstate坑 5空结果导致幻觉工具返回空字符串时模型可能脑补数据。解决方案是返回显式的NO_RESULTS_FOUND信号tooldefsearch_kb(query:str)-str:resultskb.search(query)ifnotresults:returnNO_RESULTS_FOUND: 知识库中未检索到相关内容return\n.join(results)坑 6流式输出的解析LangGraph 支持多种 stream modeupdates、values、messages、custom、checkpoints等。# 推荐新应用使用 event streamingLangGraph v1.2forchunkingraph.stream({messages:[...]},stream_mode[updates,messages],versionv2):ifchunk[type]messages:# 处理 token-by-token 的 LLM 输出print(chunk[data].content,end)elifchunk[type]updates:# 节点更新fornode,state_updateinchunk[data].items():print(fNode{node}updated)⚠️ 流式下解析tool_calls要特别小心模型可能分多个 chunk 返回一个 tool_call需要累积拼接。ToolCallChunk的合并要求index相等且非 None。坑 7tool_call_id必须正确回传模型返回的每一个tool_call都有一个唯一id。你的ToolMessage必须带上对应的tool_call_id否则模型无法把结果和调用对应起来# ❌ 错误漏了 tool_call_idToolMessage(content22°C,nameget_weather)# ✅ 正确ToolMessage(content22°C,nameget_weather,tool_call_idcall_abc123)坑 8工具数量爆炸给模型的工具越多选择错误的几率越高。建议控制在 10 个以内。工具多时用tool_search动态加载仅 gpt-5.4 支持或者按业务域拆分多个 Agent。坑 9Checkpoint 与长期记忆生产环境的长任务必须持久化状态。开发用MemorySaver生产要实现BaseCheckpointSaver写到 PostgreSQL / Redis / S3fromlanggraph.checkpoint.postgresimportPostgresSaverwithPostgresSaver.from_conn_string(conn_string)ascheckpointer:graphbuilder.compile(checkpointercheckpointer)# 即使容器重启也能从 checkpoint 恢复stategraph.get_state(config)ifstate.next:# 还有后续节点要执行graph.invoke(None,config)# 从断点继续坑 10可观测性复杂 Agent 必须接入 tracing如 LangSmith记录每个节点的输入输出、状态变化、工具返回、失败点。没有 tracing 的 Agent 等于盲人摸象。七、LangChain 还是 LangGraph维度LangChainLCEL/AgentExecutorLangGraph控制流线性 DAG拓扑固定循环状态机运行时路由状态管理基础上下文传递显式 State Checkpoint重试/分支难实现条件边天然支持人工介入不支持interrupt原生支持适用场景RAG、简单链、快速原型ReAct Agent、多 Agent、长任务、生产系统判断标准线性流程检索→提示→回答→ LangChain 足够需要思考→行动→观察循环、分支、重试、人工确认 → 必须用 LangGraph工具调用有副作用发邮件、删库、下订单→ 强烈建议 LangGraph 注意坊间有说法称LangChain 1.0 统一为create_agent这是不准确的。正确的预构建入口是langgraph.prebuilt.create_react_agentLangGraph 并非退居幕后而是核心显式依赖。八、最终总结Function Calling 与 Tool Calling 本质是同一样东西——都是模型输出结构化调用请求、后端执行的协议。前者是 OpenAI 2023 年 6 月的原始叫法functions后者是同年 11 月后的统一演进tools支持并行调用和更多工具类型。现代代码一律用tools。LangChain 负责工具抽象tool装饰器 BaseTool把 Python 函数封装成模型可理解的、带 schema 和描述的工具。ToolNode提供高级工具执行控制。LangGraph 负责流程编排用 State Node Edge 条件路由把工具调用组织成可循环、可分支、可恢复的图。create_react_agent是封装好的 ReAct 图显式写图则获得完全控制。生产落地的 10 个核心坑工具名兼容、参数校验、副作用幂等、三层错误处理、空结果防幻觉、tool_call_id回传、流式解析、工具数量控制、Checkpoint 持久化、可观测性接入。架构选型简单工具调用 LangChain 足够工具调用一旦有副作用或需要重试/分支/人工确认必须用 LangGraph。两者不是替代关系——LangGraph 是架在 LangChain 组件之上的流程管理层。构建一个生产级 Agent 的正确姿势是用 LangChain 的tool把每个能力封装好用 LangGraph 的图把这些工具编排成可控、可恢复、有状态的系统并全程接入 tracing、retry、fallback、human-in-the-loop。