系列文章目录
【 LangChain v1.2 入门系列教程】【一】开篇入门 | 从零开始,跑通你的第一个 AI Agent
【 LangChain v1.2 入门系列教程】【二】消息类型与提示词工程
【 LangChain v1.2 入门系列教程】【三】工具(Tools)开发,让 Agent 连接外部世界
【 LangChain v1.2 入门系列教程】【四】结构化输出,让 Agent 返回可预测的结构
【 LangChain v1.2 入门系列教程】【五】记忆管理,让 Agent 记住对话
【 LangChain v1.2 入门系列教程】【六】流式输出, 让 Agent 告别“想好了再说”
文章目录
- 系列文章目录
- 前言
- 一、 什么是Tools(工具)?
- 二 、Agent 什么时候调用工具?
- 三、 @tool 装饰器
- 1. 参数类型注解
- (1) 基础类型注解
- (2) 复杂参数:用 Pydantic 模型做强校验
- 2. 工具重命名
- 3. 覆盖描述
- 四、ToolRuntime 运行环境注入
- 1. 访问会话(State)
- 2. 获取用户上下文(Context)
- 五 、 多工具协同:让 Agent 完成复杂任务
- 六 、 工具调用调试
- 七、总结
前言
在前两篇中,我们已经学会了搭建基础的 Agent,而 Agent 之所以能从 “只会聊天的大模型” 变成 “能解决实际问题的智能体”,核心就在于Tools(工具)。本篇文章我们将循序渐进,用最简洁的代码和概念,带你掌握LangChain v1.2中工具开发的核心技巧。
一、 什么是Tools(工具)?
工具就是 Agent 可以使用的“外部能力”。比如:查询数据库、发送邮件、搜索网页、计算数学表达式……任何一个具体的、可重复执行的动作,都可以封装成一个工具,其本质是一个函数。
二 、Agent 什么时候调用工具?
- 当用户的问题无法仅靠模型自身的知识回答时(例如:实时天气、库存数量、用户等级)。
- 当模型判断需要执行某个动作才能完成用户指令时(例如:发送邮件、保存文件、查询 API)。
- 调用时机完全由 Agent 内部的模型自主决定。你只需要把工具注册给 Agent,它会像人类一样思考:“这个问题我能- 不能直接回答?不能的话,该用哪个工具?”
三、 @tool 装饰器
在LangChain v1.2中,使用 @tool 装饰器创建一个工具。它可以把一个普通的Python函数瞬间变成Agent可用的工具。
示例:
fromlangchain.toolsimporttool........#定义工具,从数据库查询商品库存@tooldefcheck_stock(product_id:str)->str:"""查询商品库存数量"""# 模拟库存数据stock_data={"A1001":15,"B2002":0,"C3003":3,}stock=stock_data.get(product_id,0)ifstock>0:returnf"商品{product_id}库存充足,剩余{stock}件"else:returnf"商品{product_id}已售罄,暂时无货"#创建agentagent=create_agent(model=llm,tools=[check_stock]#注册工具)#调用模型response=agent.invoke({"messages":[HumanMessage('A1001商品还有货吗?')]})print(response['messages'][-1].content)#商品 A1001 当前库存充足,还有 15 件在售说明:
- 参数类型注解是必需的 —— 它们定义了工具的输入参数模式(Schema),大模型据此知道该传什么参数
- 函数名即工具名 —— 建议用动词+名词具有语义化命名,帮助模型理解函数作用,如 get_weather、send_email
- Docstring (示例中的"““查询商品库存数量””")被自动提取为工具描述 —— 这是模型决定是否调用该工具的重要依据,描述越清晰,Agent调用工具的时机和方式就越准确。
1. 参数类型注解
LangChain v1.2 中,参数的类型注解是强制要求的—— 没有类型注解的参数,无法生成正确的工具 Schema,大模型也就不知道该传什么类型的参数,会直接导致工具调用失败。
(1) 基础类型注解
最常用的基础类型包括str、int、float、bool,直接给参数加上即可,可选参数必须设置默认值:
@tooldefsearch_customer_info(customer_name:str,max_result:int=6)->str:"""查询客户信息,当用户需要查找指定客户的资料时调用此工具。 Args: customer_name: 要查询的客户姓名,必填项 max_result: 返回的最大结果条数,默认为6条 """returnf"找到{max_result}条名为【{customer_name}】的客户信息"(2) 复杂参数:用 Pydantic 模型做强校验
frompydanticimportBaseModel,FieldfromtypingimportOptional# 定义参数类型classEmailInput(BaseModel):"""邮件发送参数"""to:str=Field(description="收件人邮箱地址")subject:str=Field(description="邮箱主题")body:Optional[str]=Field(default="",description="邮件正文内容(可选)")# 定义工具,发邮件@tooldefsend_email(param:EmailInput)->str:""" 发送邮件到指定收件人。 当用户需要发送邮件、通知或报告时使用此工具。 """#模拟发送成功响应returnf"邮件已发送至{param.to},主题:{param.subject},正文内容:{param.body}"用 Pydantic 模型的核心优势:
- 自动做参数校验
- 用Field给每个参数加精细化说明,比写在 docstring 里更清晰
- 支持可选值Optional,支持枚举值Literal
2. 工具重命名
默认情况下,工具名称 = 函数名。但你可以通过传入字符串参数来显式指定:
# 默认:工具名 = "search"@tooldefsearch(query:str)->str:"""搜索网页内容"""returnf"搜索结果:{query}"# 自定义:工具名 = "web_search"@tool("web_search")defsearch(query:str)->str:"""搜索网页内容"""returnf"搜索结果:{query}"3. 覆盖描述
默认情况下,工具描述 = 函数的 docstring。但你可以通过 description 参数强制覆盖:
# 方式1:依赖 docstring(默认)@tooldefcalculator(expression:str)->str:"""执行数学计算"""# 模型看到的就是这行returnstr(eval(expression))# 方式2:显式覆盖(优先级最高)@tool(description="专门用于处理加减乘除和复杂数学表达式。当用户询问任何数学问题时必须调用此工具。")defcalculator(expression:str)->str:"""执行数学计算"""# 这行会被忽略!returnstr(eval(expression))四、ToolRuntime 运行环境注入
LangChain v1.2 中,ToolRuntime 是一项核心升级。它会在工具执行时,自动将当前 Agent 的运行环境注入到工具中,而且这个参数不会暴露给 LLM,完全不影响工具的输入 Schema,既保留了功能又保障了安全性。
通过 ToolRuntime,你的工具可以访问常用三类核心信息:
- State(状态):短期记忆,包括当前对话的消息列表、自定义字段等。
- Context(上下文):不可变的配置信息,如用户ID、会话ID等,在调用时传入。
- Store(存储):长期记忆,跨会话持久化保存的数据。
1. 访问会话(State)
通过 runtime.state[“messages”],工具可以读取当前会话的 State(短期记忆),比如对话历史、自定义的会话状态等,从而做出更智能的决策。
@tooldefget_user_question_count(runtime:ToolRuntime)->str:"""统计当前会话中用户提出的问题总数,当用户询问自己问了多少个问题时调用。"""# 从State中获取所有对话消息all_messages=runtime.state["messages"]# 统计用户发送的消息数量user_message_count=len([msgformsginall_messagesifisinstance(msg,HumanMessage)])returnf"当前会话中,您一共提出了{user_message_count}个问题"agent=create_agent(model=llm,tools=[get_user_question_count],system_prompt="你是一个智能客服助手,回答用户的提问和帮助用户解决问题",)# 调用时注入具体上下文response=agent.invoke({"messages":[HumanMessage("我想买iPhone 16"),AIMessage("好的,iPhone 16 是我们的热门商品。"),HumanMessage("现在有货吗?"),AIMessage("库存充足,需要我帮你下单吗?"),HumanMessage("我问了多少个问题了?"),]},)print(response["messages"][-1].content)#输出:你一共提出了3个问题2. 获取用户上下文(Context)
通过 runtime.context,可以让你获取调用时传入的不可变配置信息,例如用户ID、权限等级等。这对于实现个性化服务至关重要。
fromlangchain.toolsimportToolRuntime,toolfromdataclassesimportdataclass# 定义上下文的数据结构@dataclassclassUserContext:user_id:struser_level:int=2# 用户等级,1:vip用户,2:普通用户,默认普通用户# 工具:获取当前用户信息@tooldefget_user_info(runtime:ToolRuntime[UserContext])->str:""" 获取当前用户信息,并根据等级返回不同的权限说明。 """ctx=runtime.context# 获取当前上下文user_id=ctx.user_id# 获取用户IDlevel=ctx.user_level# 获取用户等级iflevel==1:return(f" 用户{user_id}(VIP会员)特权:无限制访问,可查询历史订单、专属折扣。")else:returnf" 用户{user_id}(普通用户)限制:仅可查询本月订单,每日限查询 3 次。"# 创建 Agentagent=create_agent(model=llm,tools=[get_user_info],context_schema=UserContext,# 指定 context_schemasystem_prompt="你是客服助手,了解用户身份后,再提供相应服务。",)# 调用时注入具体上下文response=agent.invoke({"messages":[{"role":"user","content":"请问我的账户有什么权限?"}]},context=UserContext(user_id="user_1",user_level=1),# 注入用户配置:VIP 用户)print(response["messages"][-1].content)# 输出:用户 user_1(VIP会员)特权:无限制访问,可查询历史订单、专属折扣。五 、 多工具协同:让 Agent 完成复杂任务
实际业务场景中,一个复杂的用户需求,往往需要多个工具配合才能完成。LangChain 的 Agent 天生支持多工具协同,你只需要把所有工具注册到 Agent 中,它会自动规划调用顺序,完成多步操作,无需你手动写流程控制代码。
来看下面一个经典示例:
# 定义上下文的数据结构 @dataclassclassUserContext:user_id:str # 获取用户所在城市 @tool defget_user_location(runtime:ToolRuntime[UserContext])->str:"""获取当前用户所在的城市名称"""user_id=runtime.context.user_id # 从上下文获取用户ID# 模拟用户位置数据库数据 user_locations={"user_1":"北京","user_2":"上海","user_3":"广州",}returnuser_locations.get(user_id,"未知城市")# 查询指定城市天气 @tool defget_city_weather(city:str)->str:"""查询指定城市的天气"""# 模拟查询天气数据,实际从api获取 weather_data={"北京":"北京小雨,气温10-18℃","上海":"上海暴雨,气温15-22","广州":"广州晴转多云,气温20-28℃",}returnf"{weather_data.get(city, '未知天气')}"agent=create_agent(model=llm,tools=[get_user_location,get_city_weather],)# 调用时注入具体上下文 response=agent.invoke({"messages":[HumanMessage("今天的天气如何?")]},context=UserContext(user_id="user_3"),)print(response["messages"][-1].content)# 输出:今天广州的天气是晴转多云,气温在20到28摄氏度之间。运行这段代码,你会看到 Agent 的自动执行流程:
1.收到用户问题,发现没有指定城市,先调用get_user_location工具,拿到用户所在城市 “广州”
2.再调用get_city_weather工具,传入 “广州”,拿到天气信息
3.最后整理成自然语言,回答用户
全程 Agent 自动规划、自动调用,无需我们写任何流程控制代码,这就是 LangChain 中 Agent 提供的强大能力。
六 、 工具调用调试
Agent 的决策过程对开发者来说并非完全透明——我们知道它有哪些工具可用,但如果不主动观察,就难以直观了解它何时调用了工具、传入了哪些参数、哪一步出现了问题。
想看清Agent的"思考过程",最直接的方式是查看response[“messages”],观察工具调用痕迹,仍以上面的多工具协同示例为例:
print(response["messages"])输出结果中,可以看到如下关键信息:
[HumanMessage(content='今天的天气如何?',....),AIMessage(content='',tool_calls=[{'name':'get_user_location','args':{},'id':'...','type':'tool_call'}],invalid_tool_calls=[]),ToolMessage(content='广州',name='get_user_location',id='...',tool_call_id='...'),AIMessage(tool_calls=[{'name':'get_city_weather','args':{'city':'广州'},'id':'...','type':'tool_call'}],invalid_tool_calls=[]),ToolMessage(content='广州晴转多云,气温20-28℃',name='get_city_weather',...),AIMessage(content='今天广州的天气是晴转多云,气温在20到28摄氏度之间。')]通过分析这些消息,可以清晰看到 Agent 的每一步决策与执行:
- 第一个 AIMessage:tool_calls 非空,表示模型决定调用 get_user_location 工具,参数为空,且未进入 invalid_tool_calls,说明参数格式合法,可正常执行。
- 第一个 ToolMessage:表示工具执行完成,返回内容为“广州”。
- 第二个 AIMessage:再次发起工具调用,这次是 get_city_weather,参数为 {“city”: “广州”}。
- 第二个 ToolMessage:返回天气信息。
- 最后一个 AIMessage:模型根据工具返回结果生成最终回答。
说明:若 invalid_tool_calls 数组非空,则表示模型生成的工具调用参数格式错误,框架会跳过执行,这也是排查工具调用失败的重要线索。
可以使用如下代码过滤保留工具调用关键信息:
formsginresponse["messages"]:ifhasattr(msg,"tool_calls")and msg.tool_calls:# AIMessage 中的工具调用意图print(f"【模型想调用】: {msg.tool_calls}")elifisinstance(msg,ToolMessage):# 工具执行后的返回结果print(f"【工具返回】: {msg.content[:100]}...")运行输出:
【模型想调用】:[{'name':'get_user_location','args':{},'id':'019d3de71518050cae285450f221b027','type':'tool_call'}]【工具返回】:广州...【模型想调用】:[{'name':'get_city_weather','args':{'city':'广州'},'id':'019d3de7188dc957ba14814eaba5b41c','type':'tool_call'}]【工具返回】:广州晴转多云,气温20-28℃...七、总结
工具是 Agent 连接外部世界的桥梁,掌握了工具开发,你就能给 Agent 赋予无限的能力:比如让它帮你查实时股票、操作数据库、发送邮件、控制智能家居、调用任意第三方 API 等等。在下一篇教程中,我们将继续学习Agent“结构化输出“。