
工具调用的安全边界前言1.安全边界2. Action 解析3. 工具执行4. 实现局限后记源码前言Agent 可以让模型根据任务选择工具例如查询天气或搜索景点。但模型生成的 Action 只是一段文本可能格式错误也可能请求调用不该使用的工具。如果程序把它直接当作代码执行模型输出就越过了程序应当控制的边界。本文先介绍 Agent 工具调用的基本安全原则再以我近期在写的旅行 Agent 该项目源码在文末已贴出为例看看程序如何把模型生成的 Action 转换成一次受限的工具调用以及这套实现仍有哪些局限。1.安全边界模型输出的 Action 是调用工具的请求不是程序必须执行的指令。以旅行Agent中的天气查询为例即使模型写出了get_weather(city北京)程序也要先判断这是不是允许的调用形式get_weather是否已注册参数能否传给这个工具只有通过检查才进入执行环节。这条原则适用于各种 Agent由模型提出意图由程序决定执行范围。 工具的能力通常比一段聊天文本更具体也可能产生真实影响因此不能仅凭模型“想调用”就运行任意代码。程序需要把可调用的工具和可接受的输入限定在明确的范围内。旅行 Agent 的 v0.1.0 版本采用文本形式的 Action。它从模型回复中提取 Action解析工具名与参数再交给工具注册表执行执行结果作为 Observation 进入下一轮。这里的关键分界点是模型只生成文本真正选择函数并调用它的是程序。接下来沿着这条路径看 Action 如何被解析。2. Action 解析沿用前面的例子模型可能输出Action: get_weather(city北京)。程序先提取Action:后的文本再判断它是否符合约定的工具调用形式。下面是旅行 Agent v0.1.0 中负责解析调用的代码完整保留了这段方法的检查路径def_parse_tool_call(self,action_str:str,)-tuple[str,dict[str,Any]]|None:解析工具调用只允许关键字参数。try:expressionast.parse(action_str,modeeval,).bodyexceptSyntaxError:returnNoneifnotisinstance(expression,ast.Call):returnNoneifnotisinstance(expression.func,ast.Name):returnNoneifexpression.args:returnNone# 后续参数处理省略这段代码中的ast.parse(..., modeeval)负责把文本解析成 Python 的抽象语法树并要求输入是一个表达式。它只是在识别结构并没有执行表达式。随后程序要求节点必须是函数调用ast.Call函数名必须是简单名称ast.Name并拒绝位置参数。参数值再通过ast.literal_eval转换forkeywordinexpression.keywords:ifkeyword.argisNone:returnNonetry:kwargs[keyword.arg]ast.literal_eval(keyword.value)except(ValueError,TypeError,SyntaxError):returnNonereturnexpression.func.id,kwargsast.literal_eval只接受字符串、数字、布尔值、列表等字面量不能执行函数调用或任意表达式。当前实现因此没有使用 eval但这仍然只是受限解析不是完整沙箱代码没有设置输入长度、嵌套深度等限制也没有覆盖所有业务层面的风险。解析完成后程序只返回工具名称和参数字典。此时还没有调用任何函数工具是否真的允许使用要交给下一层的注册表判断。3. 工具执行解析出工具名和参数后程序还不能仅凭这个名字调用函数。旅行 Agent 把可用工具预先注册到 ToolRegistry执行时只从注册表中查找。下面的代码展示了注册、查找和参数绑定的关键路径为控制篇幅省略了描述信息和执行记录相关代码。defregister_function(self,name:str,func:Callable,description:str,parameters:list[ToolParameter]|NoneNone,)-None:ifnameinself._tools:raiseValueError(f工具已存在:{name})self._tools[name]func# 描述信息和参数信息的保存省略defhas_tool(self,name:str)-bool:returnnameinself._toolsdefexecute(self,name:str,**kwargs)-str:校验参数后执行工具。start_timetime.perf_counter()ifnotself.has_tool(name):resultf错误:未定义的工具 {name}# 执行记录相关代码省略returnresult funcself._tools[name]tool_signaturesignature(func)try:bound_argumentstool_signature.bind(**kwargs)exceptTypeErrorasexc:resultf错误:工具 {name} 参数错误:{exc}# 执行记录相关代码省略returnresulttry:resultstr(func(**bound_arguments.arguments))exceptExceptionasexc:resultf错误:工具 {name} 执行失败:{exc}# 执行记录相关代码省略returnresult# 执行记录相关代码省略returnresult注册表在这里充当工具白名单模型可以写出任意工具名但程序只会查找事先注册的名称。找到函数后signature.bind(**kwargs)检查这些关键字参数能否绑定到函数签名例如是否缺少必填参数、是否传入了不接受的参数。绑定成功程序才调用这个已注册的函数并把结果转成字符串返回给 Agent。需要注意参数绑定不等于参数校验完成。signature.bind()不检查参数值是否符合类型标注也不判断城市名等输入在业务上是否有效。这些检查如果需要应由工具或额外的校验逻辑负责。4. 实现局限沿着前面的调用路径看旅行 Agent 的 v0.1.0 已经建立了几道边界程序解析模型生成的文本只接受特定的调用形状和字面量参数再从注册表中选择已注册的工具。它不使用 eval但也不是完整沙箱。 解析器没有限制输入长度或字面量的嵌套深度即使调用形式合法已注册工具仍可能执行网络请求等实际操作。参数检查也有明确范围。signature.bind()只判断参数能否绑定到函数签名不检查类型标注也不判断输入是否符合业务规则。例如一个参数在签名中标注为 str并不意味着绑定时会拒绝其他类型的字面量。工具自身仍需按业务需要校验输入并控制它能够产生的实际影响。此外当前解析代码把关键字参数依次写入字典。如果 Action 中重复使用同一个参数名后面的值会覆盖前面的值这段代码没有显式拒绝重复参数。因此“通过解析并成功绑定”只能说明调用符合当前规则不能证明输入安全或结果正确。设计其他 Agent 时也应逐层说明每项检查实际保证了什么再针对工具的能力补上相应约束。后记Agent 工具调用的关键是把模型生成的 Action 当作请求由程序决定能否执行。先明确允许哪些工具和输入再让程序逐层检查最后由工具承担自身的业务校验。源码本文项目案例源码travel-agent-labv0.1.0。文中的实现分析均以该版本为准。