Week 2 -- Day 5:Agent 系统(下)— Function Calling 与 create_agent 在 Day 4 的学习中我们理解了 Agent 系统最核心的概念工具Tool和 ReAct 推理循环。今天我们要进一步深入两个在实际开发中至关重要的主题底层的 Function Calling 机制以及 LangChain v1 中最强大的 Agent 构建入口create_agent和它背后的中间件Middleware系统。如果把 Day 4 的内容比作理解了汽车发动机的工作原理那么今天我们要学习的是整车的电控系统和底盘架构它们让 Agent 从能跑进化为稳定、可控、可扩展。Function Calling模型与工具的通信协议bind_tools()是连接大语言模型和工具世界的桥梁。从表面上看它的用法非常简单把用tool装饰器标记过的函数列表传给bind_tools()模型就获得了一种新能力在生成文字回复的间隙它可以决定暂停文字输出转而发出一个结构化的工具调用请求。但实际上它的工作机制值得仔细拆解因为理解了这个机制你才能真正掌握 Agent 的行为逻辑。tool装饰器的职责是把一个普通的 Python 函数转换成模型可以理解的结构化接口。它做了三件事把函数的类型标注type hints自动转换为 JSON Schema 格式的输入参数定义把函数的 docstring 提取为工具的自然语言用途描述以及生成一个全局唯一的工具名称默认使用函数名。这个 JSON Schema 和描述文档会在每次模型调用时被注入到 API 请求中成为模型决定要不要用工具、用哪个工具、传什么参数的唯一依据。值得注意的是工具名应该使用 snake_case 命名如search_database而不是Search Database因为部分模型提供商会拒绝包含空格或特殊字符的函数名依据官方文档的建议“Prefer snake_case for tool names; some model providers have issues with or reject names containing spaces or special characters.” 我们来看一个最基本的工具定义它展示了tool装饰器如何把一个带有类型标注和 docstring 的普通函数转化为模型可以理解并调用的结构化接口fromlangchain.toolsimporttooltooldefsearch_database(query:str,limit:int10)-str:搜索客户数据库中的匹配记录。 Args: query: 搜索关键词 limit: 最大返回结果数 returnf在数据库中找到了{limit}条与 {query} 相关的结果。这段代码虽然简短但包含了模型决策所需的全部信息query: str和limit: int告诉模型需要哪些参数及其类型docstring 用自然语言解释了工具的用途返回值则会在工具执行后重新流回模型。如果你需要更精细的控制比如为参数添加自然语言描述、设置枚举值约束或定义复杂的嵌套结构——可以通过args_schema参数传入一个 Pydantic 模型。模型看到的不再是简单的类型标注而是包含详细Field(description...)的完整 JSON Schema这在高风险、多工具场景下能显著提升调用准确率。下面的例子中我们为天气查询工具定义了一个 Pydantic 输入模型其中Field(description...)是模型决定如何填充参数的关键依据frompydanticimportBaseModel,FieldfromtypingimportLiteralclassWeatherInput(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}°{CifunitscelsiuselseF}ifinclude_forecast:result\n未来5天晴间多云returnresult在这个例子中WeatherInput里的Literal[celsius, fahrenheit]将温度单位约束为两个有效选项include_forecast的布尔字段让模型知道它可以选择性地请求天气预报这些约束都直接来源于 Pydantic 模型并被tool自动转换为 JSON Schema无需任何手动编写。bind_tools()在 ChatOpenAI 等聊天模型上的作用是把这个结构化的工具描述附加到模型实例上。当你对一个绑定了工具的模型发起调用时请求中会额外携带一个tools字段包含了所有工具的名称、描述和参数 JSON Schema。模型的响应也因此多了一种可能性除了生成普通的文本消息它还可以返回一个tool_calls数组每个元素包含了要调用的工具名和一组符合该工具参数 Schema 的 JSON 参数。在create_agent内部这个tool_calls会被 LangGraph 的状态图机制拦截和解析随后实际执行对应的 Python 函数再将执行结果包装成ToolMessage追加到对话历史中供模型在下一轮推理中参考。那为什么在create_agent的代码中我们看不到显式的bind_tools()调用呢因为create_agent在内部帮你完成了这一步。当你把工具列表传给create_agent(tools[...])时框架会自动检测这些工具并将它们绑定到模型上同时构建一个完整的 LangGraph 状态图来管理 ReAct 循环的流转。这意味着你不必关心底层是 ReAct 还是 Function Callingcreate_agent统一了它们的调用方式框架根据当前模型的能力自动选择最优的执行策略。正如官方文档所概括的“Agent Model Harness”模型负责思考而 harness包括工具绑定、状态管理、中间件、循环控制负责为模型提供正确的上下文和行动框架。create_agentLangChain v1 的 Agent 标准入口如果说 2023 年的 LangChain Agent 开发体验是在工具箱里挑扳手和螺丝刀然后自己组装那么create_agent给开发者的感觉更接近坐进驾驶位然后点火。这个函数是 LangChain v1 构建 Agent 的标准方式它整合了过去分散在create_react_agent、AgentExecutor、AgentAction等多个组件中的功能把所有复杂性封装进一个由 LangGraph 驱动的高层抽象中。下面这段代码展示了一个完整的 Agent 从定义工具到调用执行的完整生命周期只需寥寥二十行就能构建一个能够自主查询天气、执行计算和检索数据库的智能助手fromlangchain.agentsimportcreate_agentfromlangchain.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromdotenvimportload_dotenv load_dotenv()tooldefget_weather(city:str)-str:获取指定城市的天气returnf{city}晴25度tooldefcalculate(expression:str)-str:执行数学计算输入一个算术表达式返回计算结果returnstr(eval(expression))tooldefsearch_database(query:str)-str:搜索内部数据库returnf搜索结果关于{query}的3条记录agentcreate_agent(modelChatOpenAI(modelQwen/Qwen3.6-35B-A3B,temperature0,base_urlhttps://api.siliconflow.cn/v1),tools[get_weather,calculate,search_database],system_prompt你是一个专业的AI助手遇到不确定的信息时优先使用工具。)resultagent.invoke({messages:[{role:user,content:北京今天多少度华氏度是多少}]})print(result[messages][-1].content)当你运行这段代码时Agent 在后台做的事情远比表面上复杂。模型首先收到用户的问题意识到需要先调用get_weather获取北京的温度工具执行结果返回后模型再次推理判断还需要调用calculate做华氏度换算直到信息完备才输出最终答案。整个过程对调用者来说只表现为一次agent.invoke()但内部经历了多轮模型推理和工具执行这正是create_agent所封装的 LangGraph 状态图在发挥作用。create_agent的核心参数体现了它的设计哲学。model参数接收一个模型标识字符串如Qwen/Qwen3.6-35B-A3B或者一个已初始化的聊天模型实例框架自动适配不同提供商的调用差异。tools参数接受工具列表框架在内部完成工具到模型的绑定以及工具执行节点的注册。system_prompt参数塑造 Agent 的行为基调可以传入一个字符串或一个SystemMessage对象对于需要在运行时动态生成系统提示词的场景可以通过 middleware 来实现而非直接传入静态字符串。response_format参数允许你传入一个 Pydantic 模型来约束 Agent 的最终输出格式框架会在 Agent 停止工具调用后自动进行结构化输出解析将散落在多轮对话中的信息凝聚为结构化的数据对象。checkpointer参数是另一个在实际项目中极其重要的配置项。当你为 Agent 提供一个 checkpointer本地开发时用InMemorySaver()生产环境用PostgresSaver等持久化实现并在invoke时传入一个thread_idAgent 就能在多次独立的调用之间保持对话连续性。同一个thread_id下的所有消息会自动累积模型能记住之前的对话内容而不同thread_id之间的对话则是完全隔离的。如果需要为每次调用传递不可变的上下文数据如用户 ID、权限标记或功能开关则通过context_schema定义数据类并在invoke时传入context实例工具和 middleware 就能通过ToolRuntime访问这些信息而无需依赖全局状态。下面的示例将checkpointer和context_schema组合在一起展示了如何在本地开发中实现带用户上下文的持久化对话同一个thread_id下的多轮对话会自动累积历史而context则让工具能够感知当前是哪个用户在提问fromdataclassesimportdataclassfromdotenvimportload_dotenvfromlangchain.toolsimporttoolfromlangchain.agentsimportcreate_agentfromlangchain_openaiimportChatOpenAIfromlanggraph.checkpoint.memoryimportInMemorySaver load_dotenv()tooldefget_weather(city:str)-str:获取指定城市的天气returnf{city}晴25度tooldefcalculate(expression:str)-str:执行数学计算输入一个算术表达式返回计算结果returnstr(eval(expression))dataclassclassUserContext:user_id:strtier:str# free or premiumagentcreate_agent(modelChatOpenAI(modelQwen/Qwen3.6-35B-A3B,temperature0,base_urlhttps://api.siliconflow.cn/v1),tools[get_weather,calculate],context_schemaUserContext,checkpointerInMemorySaver(),)result_1agent.invoke({messages:[{role:user,content:北京天气怎么样}]},config{configurable:{thread_id:conversation-001,}},contextUserContext(user_iduser-123,tierpremium))print(result_1[messages][-1].content)# 再次询问Agent 能够记住之前的对话历史并正确响应result_2agent.invoke({messages:[{role:user,content:我刚才问了什么}]},config{configurable:{thread_id:conversation-001,}},contextUserContext(user_iduser-123,tierpremium))print(result_2[messages][-1].content)正因为有了checkpointer和thread_id后续再次调用agent.invoke并传入相同的thread_id时Agent 会带着之前的完整对话历史进入新一轮推理而不需要用户把前面说过的话再重复一遍。当你调用agent.invoke(...)时create_agent返回的实际上是一个编译好的 LangGraph 图对象CompiledGraph它的内部节点编排是经过精密设计的模型调用节点负责生成思考或工具选择工具执行节点负责实际运行工具函数并捕获结果条件路由边根据模型响应中是否包含tool_calls来决定是进入工具执行分支还是结束循环返回最终答案。这个编排过程对开发者完全透明但你可以通过agent.stream(..., stream_modevalues)逐块获取中间状态观察每一步的工具调用和返回结果就像在旧版 API 中开启verboseTrue一样只是方式更现代也更灵活。MiddlewareAgent 的可插拔扩展层create_agent最令人惊艳的设计是它的中间件Middleware系统。在旧版 API 中如果你想要实现工具执行失败后自动重试或者超过一定轮次后强制终止你只能在AgentExecutor的参数里寻找有没有对应的开关或者手动在外层包装 try-catch。而在create_agent中所有这些横切关注点都被抽象为了可组合、可排序、可自定义的中间件每个中间件只做一件事并且把它做到极致。官方文档对此的定位非常明确 “Middleware is the primitive for customization: each piece handles one concern, hooks into the agent loop at the right moment, and composes freely with any other.”中间件的本质是 Agent 执行生命周期中的一系列钩子Hooks分为节点式和包裹式两种风格。节点式钩子在 Agent 流程的特定时间点顺序运行before_agent在整个 Agent 调用开始前执行一次适合做全局的状态初始化before_model在每一次模型调用前触发适合注入动态上下文或做调用计数after_model在每一次模型响应后触发适合做日志记录或响应校验after_agent在整个 Agent 调用结束后执行一次适合做清理或汇总。包裹式钩子则环绕在每次调用周围允许你控制被包裹的操作被调用零次、一次还是多次wrap_model_call环绕每次模型调用典型用途包括重试、缓存、动态切换模型wrap_tool_call环绕每次工具调用可以用来做错误转换、日志记录和限流。一个典型的自定义中间件场景是在每次模型调用前向系统提示词动态注入当前时间戳让 Agent 获得时间感知能力。这可以通过before_model装饰器实现fromdatetimeimportdatetimefromdotenvimportload_dotenvfromlangchain.agents.middlewareimportbefore_model,AgentStatefromlangchain.messagesimportHumanMessagefromlanggraph.runtimeimportRuntimefromtypingimportAnyfromlangchain.toolsimporttoolfromlangchain.agentsimportcreate_agentfromlangchain_openaiimportChatOpenAI load_dotenv()tooldefget_weather(city:str,date:str)-str:获取指定城市的天气returnf{date}{city}晴25度tooldefcalculate(expression:str)-str:执行数学计算输入一个算术表达式返回计算结果returnstr(eval(expression))before_modeldefadd_timestamp(state:AgentState,runtime:Runtime):在每次模型调用前注入当前时间戳nowdatetime.now().strftime(%Y年%m月%d日 %H:%M:%S)state[messages].append(HumanMessage(contentf[系统信息] 当前时间{now}))returnNone# 返回 None 表示不额外更新 state已在原地修改agentcreate_agent(modelChatOpenAI(modelQwen/Qwen3.6-35B-A3B,base_urlhttps://api.siliconflow.cn/v1),tools[get_weather,calculate],system_prompt你是一个智能助手注意利用系统时间信息回答时间敏感问题。,middleware[add_timestamp])resultagent.invoke({messages:[{role:user,content:现在北京的天气怎么样}]})print(result[messages][-1].content)这段代码的效果是每当 Agent 准备调用模型进行下一轮推理时add_timestamp钩子会抢在模型调用前将当前时间作为一条系统消息插入对话上下文模型因此获得了现在是什么时候的准确认知——这对于需要回答时间敏感问题如今天的截止日期过了没有的 Agent 来说尤为实用因为它不再需要凭空猜测或编造一个时间。更贴近生产需求的是wrap_tool_call中间件它像一个透明的拦截器包裹在每一次工具调用之外。最典型的用法是异常转换在 try 块中调用handler(request)执行真实的工具在 except 块中捕获异常并返回一个ToolMessage将异常信息转化为模型可以理解和响应的文本。这确保了任何工具层面的故障都不会导致整个 Agent 崩溃Agent 收到的是一条工具返回了错误信息的观测结果它可以根据这条信息自主决定下一步行动。下面是一个完整的异常转换中间件实现当任何工具因为参数错误、网络超时或业务逻辑异常而抛出 Exception 时它不会让整个 Agent 崩溃而是把异常包装成一条结构化的ToolMessage返回给模型让模型像处理正常的工具返回结果一样去理解和响应fromlangchain.agents.middlewareimportwrap_tool_callfromlangchain.tools.tool_nodeimportToolCallRequestfromlangchain.messagesimportToolMessagefromlangchain.agentsimportcreate_agentfromlangchain.toolsimporttoolfromlangchain_openaiimportChatOpenAIfromcollections.abcimportCallablefromdotenvimportload_dotenvfromagentimportsearch_databasefromagentimportsearch_database load_dotenv()tooldefget_weather(city:str)-str:获取指定城市的天气returnf{city}晴25度tooldefcalculate(expression:str)-str:执行数学计算输入一个算术表达式返回计算结果returnstr(eval(expression))wrap_tool_calldefhandle_tool_errors(request:ToolCallRequest,handler:Callable[[ToolCallRequest],ToolMessage])-ToolMessage:将工具异常转换为模型可理解的 ToolMessagetry:returnhandler(request)exceptExceptionase:returnToolMessage(contentf工具执行出错请检查输入参数后重试。错误详情{e},tool_call_idrequest.tool_call[id],)agentcreate_agent(modelChatOpenAI(modelgpt-4o,base_urlhttps://api.siliconflow.cn/v1),tools[get_weather,calculate,search_database],middleware[handle_tool_errors],system_prompt你是一个智能助手。)这个中间件的精妙之处在于它完全遵循了 ReAct 的核心理念把一切都转化为可推理的信息。工具出错不再是一个需要外部干预的异常事件而是被转化为一条工具返回了错误的观测结果模型在下一轮思考中自然会据此做出调整比如尝试换一种参数重试、换个工具完成同样的任务或者实事求是地告知用户当前无法完成请求。LangChain 还提供了一套丰富的官方预置中间件覆盖了从容错到安全的完整场景。ModelRetryMiddleware在模型调用因网络波动失败时自动重试支持指数退避策略ToolRetryMiddleware对工具调用执行同样的容错重试ModelFallbackMiddleware在主力模型宕机时自动切换到备用模型ModelCallLimitMiddleware通过限制单次执行的模型调用次数来防止成本失控ToolCallLimitMiddleware可以从全局或按工具粒度限制工具调用次数PIIMiddleware在输入或输出中检测和脱敏邮箱、信用卡号等敏感信息HumanInTheLoopMiddleware在特定工具被调用前暂停 Agent 执行等待人工审批SummarizationMiddleware在对话历史逼近 token 上限时自动生成历史摘要以压缩上下文TodoListMiddleware为 Agent 配备任务规划和追踪能力让复杂多步任务更可控LLMToolSelectorMiddleware在大规模工具集中自动筛选当前问题最相关的工具子集以减少 token 消耗和模型决策负担。这些预置中间件每一个都经过了生产环境的验证你只需要按需挑选并添加到middleware列表即可无需从零实现。不过当你同时使用多个中间件时它们的执行顺序就变得至关重要。好在这套顺序遵循一套清晰而直观的规则理解这套规则对于设计复杂的中间件组合至关重要。before_*钩子按中间件列表的顺序从前到后执行after_*钩子按相反的顺序从后到前执行类似洋葱模型的 return 阶段wrap_*钩子形成嵌套结构列表中最前面的中间件包裹在最外层。这意味着如果你同时使用了日志记录、错误处理和限流三个中间件限流包裹着错误处理错误处理包裹着日志记录而 before 钩子按日志→错误→限流的顺序触发after 钩子按限流→错误→日志的顺序触发。官方文档推荐将关键的中间件放在列表前面因为它们会获得最外层的控制权。从 ReAct 到 Function Calling 的范式演进回到最根本的问题ReAct 和 Function Calling 到底有什么区别为什么有了 Function Calling 之后我们还需要关心 ReActReActReasoning Acting是一种提示词工程范式它通过在系统提示词中嵌入固定的格式指令来诱导模型按Thought → Action → Observation的模板输出文本然后由程序用正则或解析器从文本中提取 Action 块并执行工具。Function Calling 则是模型提供商在模型推理层面原生支持的能力模型不再输出带格式的文本而是输出一个结构化的 JSON 对象tool_calls数组精确地指出了要调用哪个函数以及传入什么参数。从工程角度看Function Calling 的解析是确定性的JSON 解析不会像正则匹配那样因为模型输出格式的微小偏差而失败。从效果角度看Function Calling 的调用准确率通常高于 ReAct 文本解析因为它是模型训练时就内置的能力而非通过提示词强行约束出来的行为。两者的执行流程在概念上是对齐的用户提问 → 模型分析是否需要工具 → 如果是则发出工具调用 → 执行工具获取结果 → 将结果注入对话 → 模型再次评估 → 循环直到给出最终答案。区别在于中间发出工具调用这一步的实现方式ReAct 依赖文本格式约定Function Calling 依赖原生的结构化输出。这意味着在面对复杂的嵌套参数或多步骤并行调用时Function Calling 通常更稳定和高效。而在没有原生 Function Calling 支持的模型上或需要追溯模型每一步思考过程的学术场景中ReAct 仍然有它独特的价值。在 LangChain v1 的世界里这个选择已经被create_agent帮你做好了。如果你绑定了工具并且使用的模型原生支持 tool calling框架会自动走 Function Calling 路径如果你的模型不支持 tool calling框架会退回到传统的文本推理模式。重要的是你不需要改变 Agent 的构建代码同样的create_agent调用、同样的invoke接口、同样的 middleware 体系——底层协议的选择只影响执行效率而不影响功能接口。这种抽象层的统一是 LangChain v1 相比旧版本最重要的架构升级之一。练习任务用 create_agent() 重写 Day4 的 ReAct Agent编写一个 before_model middleware对比传统 ReAct 与 Function Calling 的差异考核点 ✅create_agent 迁移提交用create_agent()重写的 Agent 代码功能与 Day4 等价Middleware 实战提交一个自定义 middleware 的create_agent说明其触发时机和作用机制对比用同一任务运行 ReAct 和 Function Calling 版本输出对比报告含异同点工具绑定理解口头解释bind_tools()的工作原理及与tool装饰器的配合关系