当LLM代理需要执行超出其文本生成能力范围的操作时,它会求助于工具。LLM的意图与工具执行之间的桥梁是工具接口。将此接口看作一份契约:它规定了LLM如何向工具请求服务以及可以获得什么回报。设计良好的接口对于代理的可靠和有效行为是根本的。如果LLM不明白如何使用工具,或者误解了其功能,整个系统就可能出问题。本节侧重于专门为LLM交互设计这些接口的原则与实践。我们不只是在讨论代码中的函数签名;我们是在考虑LLM如何感知和理解工具的入口点。工具接口的构成核心来说,呈现给LLM的工具接口包含几个主要组成部分:工具名称: 一个清晰、有描述性的名称,能暗示工具的功能(例如,get_current_weather,send_email)。这通常是LLM选择工具时使用的第一条信息。工具描述: 简洁地说明工具的作用、目的以及何时使用。这对于LLM的决策过程极其重要。(我们将在“理解工具规格和描述”一节中详细阐述描述)。输入参数:名称: 参数名称应直观且有意义(例如,location 而不是 arg1)。类型: 为每个参数明确定义数据类型(例如,字符串、整数、布尔值、列表、对象)。这有助于LLM正确地格式化其请求。描述: 对每个参数的说明,阐明其目的和任何特定的格式要求。必需与可选: 指明是否必须提供某个参数。输出结构(返回值): 定义工具成功执行后返回的内容,包括其数据类型和结构。这有助于LLM理解和处理工具的响应。以下图表说明了工具接口在LLM代理与底层工具逻辑之间进行交互调解的作用。digraph G { rankdir=TB; bgcolor="transparent"; node [shape=box, style="filled", fontname="sans-serif", margin=0.2]; edge [fontname="sans-serif", fontsize=10]; LLM [label="LLM代理", fillcolor="#74c0fc", fontcolor="#000000"]; ToolInterface [label="工具接口\n(名称, 参数, 描述, 输出模式)", width=3, fillcolor="#ffe066", fontcolor="#000000"]; ToolLogic [label="工具内部逻辑\n(例如:Python代码, API调用)", width=2.5, fillcolor="#8ce99a", fontcolor="#000000"]; LLM -> ToolInterface [label=" 使用接口定义调用工具", taillabel="1.", headlabel="2."]; ToolInterface -> ToolLogic [label=" 传递结构化输入", taillabel="3.", headlabel="4."]; ToolLogic -> ToolInterface [label=" 返回结果", taillabel="5.", headlabel="6."]; ToolInterface -> LLM [label=" 向LLM提供结构化输出", taillabel="7.", headlabel="8."]; }工具接口充当一份明确定义的契约,指导LLM代理如何请求操作以及从工具的底层功能接收结果。有效设计这些组成部分可确保LLM能够准确选择、调用和理解您的工具返回的结果。有效接口设计的指导原则为LLM设计接口与为人类开发者设计传统API所需的心态略有不同。LLM会“读取”您的接口定义来理解功能。1. 命名和描述的清晰度和表达力工具名称: 选择清晰、直接反映工具操作的名称。例如,search_knowledge_base 比 kb_lookup 更具表达力。参数名称: 使用清晰指明预期输入的名称。如果工具发送消息,recipient_email 和 message_body 优于 to_addr 和 payload。描述: 如前所述,描述非常重要。请确保它们是从LLM的角度编写的,即LLM正尝试判断此工具是否符合用户的请求。避免使用行话,除非预期LLM能够理解。2. 定义明确的参数:类型、可选性和默认值LLM在强类型和明确期望下工作效果最佳。显式类型: 始终为每个参数指定数据类型(例如,string、integer、boolean、array[string]、object)。这通常通过像JSON Schema这样的模式定义完成,我们将在“工具输入和输出模式的最佳实践”中提及。参数示例:{"name": "user_id", "type": "integer", "description": "用户的唯一标识符。"}可选性: 明确将参数标记为 required 或 optional。这可以防止LLM遗漏必要信息时出现错误。默认值: 对于可选参数,提供合理的默认值可以简化LLM的任务,因为它不需要总是指定所有选项。确保默认值已记录。提示: 设计参数时,考虑LLM会自然地从用户请求中提取哪些信息。如果用户说“伦敦明天天气如何?”,那么参数 city(字符串,必需)和 date(字符串,可选,默认为今天)将很好地对应。3. 为原子性和可组合性而设计单一职责: 每个工具理想情况下应执行一个特定的任务并做好它。一个名为 manage_user_profile 且处理获取、更新和删除配置文件的工具,其效果不如 get_user_profile、update_user_profile 和 delete_user_profile 等独立工具。原子性对LLM为何重要:选择更简单: 对于LLM来说,在多个特定工具中进行选择比弄清楚如何使用多功能工具的哪种模式更容易。减少错误面: 更简单的工具具有更简单的接口,从而减少了LLM在调用时出错的方式。更好的可组合性: 原子工具可以更容易地被LLM按顺序组合以实现复杂目标(第三章:工具选择和编排中的一个主题)。避免创建试图通过复杂参数处理过多不同操作的“万能工具”。虽然从代码角度看这可能显得高效,但这通常会使LLM难以理解接口。4. 参数和返回结构的一致性您的工具集中的一致性有助于LLM学习如何与其交互。命名约定: 对工具和参数名称使用一致的大小写(例如,snake_case 或 camelCase)。通用参数: 如果多个工具对类似实体进行操作,请为该实体使用相同的参数名称和结构(例如,如果多个工具操作项目,始终使用 item_id: string)。标准化错误响应: 尽管详细的错误处理将在后面介绍,但工具的错误响应结构应保持一致。这使得LLM(或编排器)能够更可预测地处理故障。可预测的输出: LLM需要知道会收到什么。如果一个工具可以根据输入返回不同的结构,则必须明确记录这一点,或者,更好地是,由不同的工具或不同的输出字段来处理。5. 考虑LLM的视角这也许是首要原则。总是问自己:“LLM会如何理解这个?”自然语言对齐: 参数名称和描述应与自然语言中概念的表达方式保持一致。如果参数是 target_date,请确保其描述明确了格式期望(例如,“YYYY-MM-DD”),如果LLM需要生成它的话。减少歧义: 如果像 query 这样的参数名称可能有多种含义,请使其描述非常具体(例如,“用于在公司公开文档中查找文章的搜索词。”)。信息密度: 为LLM提供足够的信息以正确使用工具,但又不过多,以免使其难以处理。描述应简洁但全面。向LLM传达接口细节LLM不会直接读取您的Python函数签名或API代码。它依赖于该接口的表示,通常以结构化格式与自然语言描述一同提供。工具清单/规格: 大多数LLM代理框架(如LangChain、LlamaIndex或OpenAI的函数调用)要求您使用特定结构(通常基于JSON)来定义工具。此结构包括名称、描述以及输入参数的模式(通常是JSON Schema)。用于参数的JSON Schema: JSON Schema是一种词汇表,允许您注释和验证JSON文档。它被广泛用于定义工具输入的预期结构、类型和约束。 示例:city 参数的简单JSON Schema:{ "name": "location", "description": "城市和州,例如:旧金山,加利福尼亚州", "type": "string", "required": true }(我们将在“工具输入和输出模式的最佳实践”一节中更详细地阐述JSON Schema。)输出模式: 同样,定义工具返回内容的模式很重要。这使得LLM能够预知响应的格式以及如何使用其中包含的信息。这些结构化定义的清晰度和准确性具有决定性作用。提供给LLM的定义与工具实际行为之间的任何不匹配都将导致错误和不可靠的代理表现。接口设计中的常见问题意识到常见的失误可以帮助您避免它们:模糊或通用名称: process_data 或 run_script 对LLM来说信息量很少。过于复杂的输入对象: 如果工具需要一个包含许多字段的深度嵌套JSON对象,LLM可能很难正确构建。考虑是否可以拆分工具,或者是否可以在内部处理一些复杂性。隐式依赖: 如果 parameter_b 仅在 parameter_a 具有特定值时才有意义,这种条件逻辑必须在描述中极其清晰,或者理想情况下,如果LLM框架支持,由独立的工具或不同的操作模式来处理。不一致的返回格式: 如果工具有时为相同的逻辑输出返回字符串,有时返回字符串列表,这会使LLM感到困惑。目标是单一、可预测的输出结构。缺少单位或格式规范: 如果参数是 duration,它是秒、分钟还是小时?如果是 date,预期是什么格式?在参数描述中明确说明这些。示例:简单计算器工具的接口让我们为一个可以执行加法、减法、乘法和除法的基础计算器工具设计一个接口。尝试1(不太理想):单个 calculate 工具工具名称: calculate描述: “执行一项计算。”参数:operand1:数字,“第一个数字”operand2:数字,“第二个数字”operation:字符串,“要执行的操作:'add'(加)、'subtract'(减)、'multiply'(乘)、'divide'(除)”输出: 一个数字(结果)。虽然这可行,但 operation 参数会使LLM的工作略微困难;它必须正确选择字符串。尝试2(更原子化,通常更适合LLM):独立工具工具1:add_numbers描述: “将两个数字相加。”参数: num1:数字,num2:数字输出: 一个数字(和)。工具2:subtract_numbers描述: “从第一个数字中减去第二个数字。”参数: num1:数字,num2:数字输出: 一个数字(差)。(multiply_numbers 和 divide_numbers 也类似)这种原子化方法通常更容易让LLM准确选择。如果LLM判断需要“加法”,它会直接选择 add_numbers。这与原子性设计原则非常吻合。选择取决于LLM的能力以及代理框架如何处理工具选择,但从原子工具开始是一个良好的实践。设计有效的工具接口是一个迭代的过程。您可能会随着观察LLM代理如何与它们交互而改进您的接口。目标是让LLM尽可能容易地理解您的工具做什么、何时使用它以及如何提供必要的信息。这种细致的设计是构建可靠和有能力的LLM代理的根本。