结构化输出正逐渐成为大型语言模型(LLM)工具调用领域中,一种极具竞争力的替代方案。传统函数调用方法面临诸多挑战,尤其是在保证JSON格式的有效性和模型输出的可预测性方面。本文将深入探讨结构化输出如何克服这些挑战,并为构建更稳定、更高效的LLM代理系统提供新的思路。
函数调用的困境:JSON格式的脆弱性
传统函数调用依赖于LLM生成符合特定JSON Schema的输出,然后由外部框架解析和执行。然而,这种方法的缺陷在于,LLM并不能保证始终生成有效且结构正确的JSON。尤其是在使用本地开源模型或不太知名的API服务时,JSON格式错误、Schema不匹配等问题时常发生。例如,一个LLM被要求调用一个名为get_weather
的函数,该函数需要location
和可选的unit
参数,期望的JSON格式为{"location": "New York", "unit": "celsius"}
。但模型可能返回{"location": "New York", "units": "celsius"}
,由于键名错误导致解析失败。这种不确定性给调试和预测模型行为带来了巨大的困难,也限制了函数调用的可靠性。尽管OpenAI等领先供应商已经引入了诸如“严格模式”之类的解决方案来缓解这些问题,但对于更广泛的LLM生态系统来说,仍然需要一种更通用的解决方案。
结构化输出的优势:强约束的JSON生成
结构化输出的核心优势在于其对模型输出的严格约束。它强制模型生成完全符合预定义JSON Schema的响应。 OpenAI官方文档指出,结构化输出是JSON模式的进化,它不仅保证了JSON的有效性,而且强制执行严格的Schema合规性。这意味着开发者可以定义一个明确的Schema,然后确保LLM的输出始终遵循该Schema,从而消除了JSON格式错误的风险。 例如,使用Pydantic库,开发者可以定义一个WeatherParams
类,包含location
和unit
字段,并将其作为response_format
传递给LLM。这样,模型将始终生成符合WeatherParams
类定义的JSON对象,例如 {"location": "London", "unit": "fahrenheit"}
。这种强约束的特性极大地提高了工具调用的可靠性和可预测性。
动态Schema生成:灵活适应各种工具参数
结构化输出的另一个显著优势是其支持动态Schema生成。这意味着开发者可以根据不同的工具及其参数,灵活地调整Schema。当LLM需要调用多个具有不同参数结构的工具时,这种灵活性至关重要。例如,一个LLM可能需要调用一个get_weather
函数,以及一个book_flight
函数。get_weather
函数需要location
和unit
参数,而book_flight
函数需要departure_city
、arrival_city
和date
参数。使用结构化输出,开发者可以分别为这两个函数定义不同的Pydantic Schema,并根据需要动态地将其传递给LLM。这种动态性使得结构化输出可以轻松适应各种工具的参数,极大地提高了工具调用的通用性。
OpenAI兼容性:无缝集成现有工具调用流程
结构化输出的设计考虑了与现有OpenAI 函数调用流程的兼容性。通过模仿函数调用的历史格式,结构化输出可以实现与现有系统的无缝集成。这意味着开发者可以在不大幅修改现有代码的情况下,将结构化输出集成到他们的LLM代理系统中。例如,结构化输出可以生成类似于OpenAI 函数调用的JSON格式,包含函数名称和参数。这样,现有的解析器和执行器可以继续工作,而无需进行重大更改。这种兼容性降低了采用结构化输出的门槛,并加速了其在实际应用中的推广。
两步法流程:清晰分离决策与参数生成
为了更好地利用结构化输出,一个有效的策略是采用两步法流程:首先,LLM分析用户查询,并决定是直接回答还是调用工具。然后,如果需要调用工具,LLM生成该工具的参数。第一步,被称为“决策步骤”,LLM使用AnswerDecision
Pydantic 模型来决定是否需要使用工具。AnswerDecision
模型包含 reasoning
、answer
和 use_tool
三个字段。 reasoning
字段解释模型的逻辑推理, answer
字段包含对用户的最终或中间响应,而 use_tool
字段指定要调用的工具的名称(如果需要)。如果用户询问天气,模型可能会填充use_tool
字段为get_weather
。如果用户只是打招呼, use_tool
字段则保持为空,直接在answer
中返回问候语。第二步,即“参数生成步骤”,仅在第一步确定需要调用工具时执行。在该步骤中,LLM使用对应于所选工具的特定Pydantic Schema生成参数。 这种两步法流程清晰地分离了决策过程和参数生成过程,使得工具调用更加有条理和易于管理。它也模拟了人类解决问题的过程,首先确定需要采取什么行动,然后才确定如何执行该行动。
上下文效率:减少冗余信息的干扰
传统函数调用的一个潜在问题是,每次调用都需要将所有可用工具的完整Schema注入到系统提示中。当代理可以访问数十个工具时,这可能会导致上下文窗口变得非常拥挤,从而降低模型的性能。例如,如果一个代理可以访问Jira、Confluence和其他API,那么将所有这些API的Schema都放入提示中会占用大量的上下文空间,使得模型难以专注于当前的任务。相比之下,使用两步法结构化输出方法,模型在决策步骤中仅看到工具的名称和简短描述,这足以做出选择。只有在参数生成步骤中,模型才会收到所选工具的详细Schema。 这种方法显著提高了上下文效率,减少了冗余信息的干扰,使得模型能够更准确地执行任务。这意味着即使在拥有大量工具的情况下,代理也可以保持较高的性能。
权衡与效益:延迟与可靠性的博弈
尽管结构化输出带来了诸多优势,但也存在一些权衡。最主要的权衡是额外的LLM调用。与传统的单步函数调用相比,结构化输出需要两个连续的LLM调用,这会增加响应时间和成本。此外,结构化输出的实现也需要自定义的编排逻辑,因为主流框架并不直接支持它。然而,考虑到结构化输出带来的可靠性提升和上下文效率的提高,这些权衡通常是值得的。在许多情况下,尤其是当涉及大量工具时,结构化输出的优势远远超过了其劣势。
实例:天气查询与航班预订
为了更具体地说明结构化输出的优势,让我们考虑两个例子:天气查询和航班预订。
天气查询:
用户查询:“纽约的天气怎么样?”
- 决策步骤:LLM分析查询,并确定需要调用
get_weather
工具。它生成以下JSON:
json
{
"reasoning": "用户询问天气,因此应该使用 get_weather 工具",
"answer": "让我查询纽约的天气",
"use_tool": "get_weather"
}
- 参数生成步骤:LLM使用
WeatherParams
Schema生成get_weather
工具的参数:
json
{
"location": "New York",
"unit": "celsius"
}
然后,该JSON被解析并传递给get_weather
函数,该函数返回结果。
航班预订:
用户查询:“预订明天从伦敦到巴黎的航班。”
- 决策步骤:LLM分析查询,并确定需要调用
book_flight
工具。它生成以下JSON:
json
{
"reasoning": "用户希望预订航班,因此应该使用 book_flight 工具",
"answer": "让我查询从伦敦到巴黎的航班",
"use_tool": "book_flight"
}
- 参数生成步骤:LLM使用
FlightParams
Schema生成book_flight
工具的参数:
json
{
"departure_city": "London",
"arrival_city": "Paris",
"date": "2024-08-25"
}
然后,该JSON被解析并传递给book_flight
函数,该函数执行航班预订。
这些例子展示了结构化输出如何处理不同类型的查询,并确保LLM始终生成有效的参数。
结论:构建更健壮的LLM代理系统
总而言之,使用结构化输出的两步法是一种强大而可靠的工具调用方法,尤其是在使用开源LLM时。它完全解决了JSON无效的问题,确保了可预测性,并简化了调试。尽管存在更高的延迟这一权衡,但可靠性和上下文效率的提高,特别是在涉及许多工具的情况下,使得该方法对于构建复杂的、健壮的代理系统极具吸引力。随着LLM技术的不断发展,结构化输出有望在未来的工具调用中发挥越来越重要的作用。通过采用结构化输出,开发者可以构建更稳定、更高效的LLM代理系统,从而为用户提供更好的体验。