尽管基础代理通常依赖解析大型语言模型(LLM)的文本输出来决定下一步动作及其参数,但这种方法容易出错且可能出现不一致。LLM每次响应的格式可能略有不同,或者解析逻辑可能无法正确提取复杂参数。对于要求较高可靠性的生产系统,许多现代LLM提供了一种更直接的机制:结构化工具调用,OpenAI等一些提供商常称之为函数调用。结构化工具调用的机制模型支持结构化工具调用时,不会生成描述调用工具的自由形式文本(例如,“我应该使用搜索工具,查询为‘LangChain内存’”),而是能输出一条特定指令,通常以JSON格式呈现,明确指示要调用哪个工具以及传递哪些参数。LLM经过专门训练,能够识别用户请求何时需要调用可用工具之一,并根据预设架构生成参数。这比文本解析有很大的改进,原因如下:提高了可靠性: LLM的输出在设计上就是结构化且机器可读的。这大幅降低了从自然语言指令中提取信息时可能出现的解析错误。提高了准确性: 模型经过训练,能根据提供的架构填充参数。这通常带来更准确的参数提取,特别是对于具有多个或复杂参数的工具。简化了代理逻辑: 代理的任务变得更简单。它不再需要复杂的正则表达式或解析逻辑,主要需要解释LLM的结构化输出,识别目标工具,并使用提供的参数执行它。增强了可预测性: 由于LLM调用工具的决策及其提供的参数更加明确且受制于工具架构,系统变得更可预测。LangChain 集成:工具调用的抽象LangChain 提供了抽象层,用于处理跨不同LLM提供商(如OpenAI、Google Gemini、Anthropic Claude等)的结构化工具调用功能,让您可以定义一次工具并在兼容模型和代理中使用它们。主要组件包括:工具定义: 您将工具定义为Python函数,通常使用装饰器或Pydantic模型来指定它们的参数和描述。LangChain使用这些定义(包括类型提示和文档字符串)来生成LLM所需的架构。将工具绑定到LLM: bind_tools等方法将您可用工具的架构附带到LLM或聊天模型实例。这会告知模型在推理时可用的能力。专用代理: LangChain 提供 create_tool_calling_agent 等构造器,这些构造器专门设计用于使用这些模型功能。在现代生产架构中,它们通常通过 LangGraph 进行编排,LangGraph 处理状态和通信循环,将工具架构传递给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"正在获取 {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="城市和州,例如:旧金山,加利福尼亚") date: datetime.date = Field(..., description="天气预报的具体日期") @tool(args_schema=WeatherRequestArgs) def get_weather_forecast(location: str, date: datetime.date) -> str: """提供特定地点和日期的天气预报。""" # 实际实现会调用天气API print(f"正在获取 {date} {location} 的天气...") return f" {date} {location} 的预报:晴,25\u00b0C" # 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="用户输入"]; Agent [label="代理"]; LLM [label="LLM\n(带工具架构)"]; ToolImpl [label="工具实现\n(Python代码)"]; FinalResp [label="最终响应"]; UserInput -> Agent [label="1. 查询"]; Agent -> LLM [label="2. 格式化提示 +\n 工具架构"]; LLM -> Agent [label="3. 结构化工具调用\n(例如:JSON: {tool: 'X', args: {...}})"]; Agent -> ToolImpl [label="4. 解析并调用工具 X\n(带参数)"]; ToolImpl -> Agent [label="5. 工具结果"]; Agent -> LLM [label="6. 格式化结果 +\n 继续提示"]; // 如果需要更多步骤,可能循环 LLM -> Agent [label="7. 最终回答文本"]; Agent -> 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推理能力与应用程序自定义功能之间更可靠、可预测的交互。这是构建生产就绪代理的一大步,这些代理能够有效且一致地执行涉及外部交互的复杂任务。