基础代理通常依赖解析LLM的文本输出来确定下一步动作及其参数,但这种方式容易出错且一致性差。LLM每次响应的格式可能略有不同,或者解析逻辑未能准确获取复杂参数。对于需要更高稳定性的生产系统,许多现代LLM提供了一种更直接的机制:结构化工具调用,OpenAI等供应商常将其称为功能调用。结构化工具调用的机制LLM不生成描述工具调用的自由格式文本(例如,“我应该使用搜索工具,查询‘LangChain内存’”),支持结构化工具调用的模型能输出具体指令,通常是JSON格式,精确指出要调用哪个工具以及传递什么参数给它。LLM经过专门训练,能识别用户请求何时需要调用某个可用工具,并根据预设的模式生成参数。这表明它比文本解析有显著改进,原因如下:提升稳定性: LLM的输出在设计上就是结构化且机器可读的。这大大降低了从自然语言指令中获取信息时可能发生的解析错误。增强准确性: 模型被训练为根据提供的模式填充参数。这通常能带来更准确的参数提取,特别是对于具有多个或复杂参数的工具。简化代理逻辑: 代理执行器的任务变得更简单。它不再需要复杂的正则表达式或解析逻辑,而主要是解析LLM的结构化输出,识别目标工具,并用提供的参数来执行它。提高可预测性: 系统的可预测性得到提升,因为LLM调用工具的决定及其提供的参数通过工具模式变得更明确和受限。LangChain整合:工具调用的抽象LangChain提供抽象层,以应对不同LLM供应商(如OpenAI、Google Gemini、Anthropic Claude等)的结构化工具调用能力,使您能够一次定义工具,并在兼容的模型和代理中使用它们。重要组成部分包括:工具定义: 您将工具定义为Python函数,通常通过装饰器或使用Pydantic模型来指定它们的参数和描述。LangChain使用这些定义(包括类型提示和文档字符串)来生成LLM所需的模式。绑定工具到LLM: bind_tools(用于基于LCEL的新方法)或类似机制将可用工具的模式附加到LLM或聊天模型实例。这告知模型在推理期间可用的功能。专用代理: LangChain提供代理构造器,例如create_openai_tools_agent、create_tool_calling_agent或create_structured_chat_agent,它们专门设计用于利用这些模型特性。这些代理处理通信循环,将工具模式传递给LLM并解析结构化工具调用响应。定义工具以实现可靠调用工具定义的清晰度和准确性是影响LLM使用效果的重要因素。使用函数和装饰器: 一种常用方法是定义标准Python函数,并使用LangChain的@tool装饰器(或StructuredTool.from_function)。from langchain_core.tools import tool from pydantic import BaseModel, Field # 可选,用于复杂参数 @tool def get_stock_price(symbol: str) -> float: """ 获取给定股票代码的当前股价。 用于获取最新的金融市场数据。 """ # 替换为实际的API调用逻辑 print(f"Fetching price for {symbol}...") if symbol.upper() == "LC": return 125.50 elif symbol.upper() == "AI": return 300.10 else: return 404.0 # 表示未找到 # 代理执行器现在将可以使用此工具 # LangChain根据函数签名自动生成模式 # 和文档字符串。类型提示symbol: str告知LLM,symbol参数应该是一个字符串。文档字符串非常重要;它充当提供给LLM的描述,指导LLM决定何时使用该工具。为复杂参数使用Pydantic: 对于具有多个参数或复杂输入结构的工具,为参数定义Pydantic模型可以提供更好的结构和验证。from langchain_core.tools import tool from pydantic import BaseModel, Field import datetime class WeatherRequestArgs(BaseModel): location: str = Field(..., description="城市和州,例如:旧金山,CA") date: datetime.date = Field(..., description="天气预报的具体日期") @tool(args_schema=WeatherRequestArgs) def get_weather_forecast(location: str, date: datetime.date) -> str: """提供特定地点和日期的天气预报。""" # 实际实现会调用天气API print(f"Fetching weather for {location} on {date}...") return f"Forecast for {location} on {date}: Sunny, 25°C" # LangChain使用WeatherRequestArgs为LLM生成详细的JSON模式。在此情况下,args_schema参数明确将Pydantic模型与工具关联起来,确保LLM获得预期的输入结构的清晰定义,包括每个字段的描述。代理执行流程当使用配置为结构化工具调用的代理时,典型的执行循环如下:digraph G { rankdir=LR; node [shape=box, style=rounded, fontname="Arial", fontsize=10]; edge [fontname="Arial", fontsize=9]; UserInput [label="用户输入"]; AgentExec [label="代理执行器"]; LLM [label="LLM\n(带工具模式)"]; ToolImpl [label="工具实现\n(Python代码)"]; FinalResp [label="最终响应"]; UserInput -> AgentExec [label="1. 查询"]; AgentExec -> LLM [label="2. 格式化提示 +\n 工具模式"]; LLM -> AgentExec [label="3. 结构化工具调用\n(例如,JSON: {tool: 'X', args: {...}})"]; AgentExec -> ToolImpl [label="4. 解析与调用工具X\n带参数"]; ToolImpl -> AgentExec [label="5. 工具结果"]; AgentExec -> LLM [label="6. 格式化结果 +\n 继续提示"]; // 如果需要更多步骤,可能循环 LLM -> AgentExec [label="7. 最终回答文本"]; AgentExec -> FinalResp [label="8. 交付给用户"]; } 使用结构化工具调用时的代理执行流程。LLM直接输出结构化请求,代理执行器解析该请求并用它来调用正确的工具实现。输入: 用户向代理执行器提供输入。LLM调用: 执行器格式化输入、对话历史以及可用工具的模式,并将它们发送给LLM。结构化响应: LLM分析请求。如果它判断需要工具,则以结构化输出来响应,指定工具名称和参数(例如,{ "tool": "get_stock_price", "tool_input": {"symbol": "LC"} })。如果不需要工具,它会生成最终文本响应。解析与调用: 代理执行器解析此结构化响应。它识别指定的工具(get_stock_price)并提取参数({"symbol": "LC"})。然后它调用相应的Python函数(get_stock_price(symbol="LC"))。结果处理: 工具执行并返回其结果(例如,125.50)。后续LLM调用(如果需要): 执行器将工具的结果发回给LLM,可能要求LLM综合一个最终答案或决定下一步操作。最终输出: LLM根据工具结果和对话历史生成最终响应。交付: 代理执行器将最终响应返回给用户。生产环境的考虑事项描述的质量: 文档字符串和参数描述很重要。它们是LLM了解您的工具功能和使用方法的主要方式。请确保它们清晰、具体和准确。说明工具何时应该被使用。模式设计: 在捕获必要信息的同时,保持参数模式尽可能简单。使用适当的类型(字符串、整数、布尔值、列表等)。模型支持: 确认您使用的特定LLM稳定可靠地支持您所需的工具/功能调用特性。不同模型和供应商之间的性能和稳定性可能有所差异。错误处理: 尽管结构化调用减少了解析错误,但工具执行本身仍可能失败(例如,网络问题、下游输入无效)。请确保您的代理包含工具执行失败的错误处理,如同前一节(处理工具错误与代理恢复)中讨论的。Token使用: 提供工具模式会消耗上下文窗口的token。对于具有许多复杂工具的代理,这可能成为上下文长度限制和成本的一个因素。借助结构化工具调用,您可以摆脱脆弱的文本解析,转向LLM推理能力与应用程序自定义功能之间更可靠、可预测的交互。这是构建能够有效且持续执行涉及外部交互的复杂任务的生产就绪代理的重要一步。