1. 项目概述:当AI学会“动手”与“观察”
在AI领域,尤其是大语言模型(LLM)驱动的智能体(Agent)开发中,我们常常面临一个核心瓶颈:模型本身是一个“超级大脑”,它精通语言、逻辑和知识,但它没有“手”去操作外部系统,也没有“眼”去感知真实世界的数据。它被困在文本的牢笼里,空有满腹经纶,却无法付诸实践。这就是“工具调用”技术要解决的根本问题——为Agent装上可以执行具体任务的“手”,以及获取实时信息的“眼”。
想象一下,你有一个无所不知的AI助手。你问它:“帮我查一下明天从北京飞上海的航班,选最便宜的那个,然后预订。”一个纯粹的LLM可能会给你一段非常详细的文字说明,告诉你应该去哪个网站、点击哪个按钮、填写哪些信息。但它无法替你完成点击、查询、比价和支付这一系列动作。工具调用,就是让这个AI助手不仅能“说”,还能“做”。它通过调用预先定义好的工具(比如一个查询航班的API、一个模拟点击的脚本、一个执行计算的函数),将LLM的“思考”转化为实际的“行动”。
这个领域正随着AI Agent的兴起而变得无比火热。从简单的命令行工具调用,到复杂的业务流程自动化,工具调用是Agent实现价值、从“聊天玩具”升级为“生产力伙伴”的关键一跃。而实现这一过程,涉及到LLM如何理解任务、如何选择工具、如何传递参数、如何解析结果等一系列技术挑战。本文将深入拆解“工具调用”作为Agent“手”和“眼”的核心机制,结合当前主流的技术栈(如gRPC、WASM),分享一套从设计到落地的实战经验,无论你是刚接触Agent开发的初学者,还是希望优化现有系统的资深工程师,都能从中找到可直接复用的思路和代码。
2. 核心架构:工具调用的三层设计哲学
一个健壮、灵活的工具调用系统绝非简单的“if-else”判断,它需要清晰的分层架构来应对复杂性。在我的实践中,我将其抽象为三层:意图理解层、工具路由层和执行沙箱层。这三层共同构成了Agent感知、决策、行动的核心循环。
2.1 意图理解层:LLM的“任务拆解”能力
这是整个流程的起点。当用户下达一个自然语言指令(如“把/data目录下所有.log文件压缩成backup.zip”)时,LLM的首要任务不是直接执行,而是理解。这一层的核心是让LLM将模糊的指令转化为结构化的“工具调用请求”。
关键设计:结构化输出(Function Calling)主流LLM API(如OpenAI GPT、Claude、DeepSeek)都提供了“函数调用”(Function Calling)或“工具调用”(Tool Calling)能力。你需要预先向LLM定义一套“工具清单”。每个工具包含:
- name: 工具的唯一标识,如
compress_files。 - description: 工具功能的自然语言描述,这是LLM选择工具的主要依据。描述必须精准、无歧义,并包含关键参数线索。
- parameters: 遵循JSON Schema格式的参数定义,包括类型、描述、是否必需等。
当LLM收到用户指令后,它会根据工具描述,判断是否需要调用工具、调用哪一个、以及参数应该是什么。它会输出一个结构化的JSON对象,而不是一段文本。例如,对于上面的压缩指令,LLM可能输出:
{ “tool_call_id”: “call_abc123”, “name”: “compress_files”, “arguments”: { “directory_path”: “/data”, “file_pattern”: “*.log”, “output_filename”: “backup.zip” } }实操心得:描述的艺术工具描述的写法直接决定了LLM调用的准确率。切忌使用笼统的描述。对比以下两种:
- 差的描述:
“一个处理文件的工具”。- 好的描述:
“将指定目录下匹配特定通配符模式的所有文件,打包压缩成一个ZIP格式的归档文件。例如,可以用来备份日志文件。”好的描述明确了功能(压缩)、输入(目录、通配符)、输出(ZIP文件)和典型场景(备份日志)。在实践中,我们甚至会将常见的用户问法示例写入描述,以提升意图识别的泛化能力。
2.2 工具路由层:高效可靠的“调度中心”
LLM输出结构化调用请求后,请求被发送到工具路由层。这一层负责将抽象的“工具名”映射到具体的执行逻辑。它的设计直接影响系统的可维护性和扩展性。
核心挑战与方案选型
- 动态注册与发现:系统需要支持热插拔式地添加或移除工具,而无需重启Agent服务。我通常采用一个“工具注册表”模式。每个工具在启动时向注册表注册自己的元信息(名称、描述、参数schema)和一个执行函数(或端点)。
- 协议与通信:工具可能以多种形式存在:本地Python函数、远程HTTP API、gRPC服务,甚至是一段WASM字节码。路由层需要统一适配。
- 本地函数:最简单直接,通过注册表调用即可,延迟最低。
- HTTP/gRPC:适用于远程或跨语言工具。gRPC因其高效的二进制编码(Protocol Buffers)和强大的流式处理能力,在需要高性能、强类型约束的内部服务间调用中优势明显。例如,一个负责图像识别的工具可能是一个独立的C++服务,通过gRPC暴露接口。
- WASM:这是一个越来越重要的方向。WebAssembly允许你将用C/C++/Rust等语言编写的工具编译成安全的、可移植的字节码,在沙箱中运行。这对于运行不可信的用户自定义工具或需要高性能计算的工具(如FFmpeg转码)至关重要。
路由层实现示例(Python伪代码)
class ToolRegistry: def __init__(self): self._tools = {} def register(self, name: str, description: str, func: callable, schema: dict): self._tools[name] = { ‘description’: description, ‘func’: func, ‘schema’: schema } async def execute(self, tool_call: dict) -> str: tool_name = tool_call[‘name’] if tool_name not in self._tools: return f“Error: Tool ‘{tool_name}’ not found.” tool = self._tools[tool_name] try: # 参数验证(可根据schema进行) arguments = tool_call[‘arguments’] # 执行工具 result = await tool[‘func’](**arguments) return str(result) except Exception as e: return f“Error executing tool ‘{tool_name}’: {str(e)}” # 注册一个本地工具 registry = ToolRegistry() registry.register( name=“get_weather”, description=“获取指定城市的当前天气情况。需要提供城市名称。”, func=get_weather_function, # 这是一个实际的异步函数 schema={“type”: “object”, “properties”: {“city”: {“type”: “string”}}} )2.3 执行沙箱层:安全可控的“操作车间”
这是工具真正运行的地方,也是安全风险最高的地方。特别是当Agent能够执行诸如文件操作、系统命令、网络请求等能力时,一个恶意的工具调用或一个意外的bug都可能导致灾难性后果。
安全是首要考量
- 权限隔离:每个工具应根据“最小权限原则”运行。例如,一个“读取文件”的工具不应该拥有“删除文件”的权限。在Linux环境下,可以考虑使用容器(如Docker)或命名空间进行隔离。
- 资源限制:必须限制工具的执行时间、内存占用、CPU使用率和网络带宽。防止一个工具调用拖垮整个Agent系统。Python的
resource模块或使用subprocess配合超时设置是基础手段。 - 沙箱技术:对于运行不可信代码(如用户上传的工具脚本),沙箱是必须的。
- WASM沙箱:如前所述,WASM设计之初就考虑了安全性,它运行在一个内存安全的沙箱中,无法直接访问主机文件系统或网络,除非通过明确定义的宿主接口(Host API)。这使得它成为运行第三方工具的理想载体。例如,你可以让用户上传一个用Rust编写的图像处理工具(编译为WASM),在沙箱中安全执行。
- 专用语言解释器:对于简单脚本,也可以使用如
PyPy的沙箱模式或seccomp等系统调用过滤机制,但复杂度和安全性通常不如WASM。
WASM工具执行示例(概念性)假设我们有一个用Rust编写的、计算MD5哈希的工具,编译成了calc_md5.wasm。
import wasmtime class WASMToolRunner: def __init__(self, wasm_file_path): self.engine = wasmtime.Engine() self.module = wasmtime.Module.from_file(self.engine, wasm_file_path) # 定义宿主函数,用于让WASM模块读取输入(如文件内容) def host_read_input(pointer, length): # ... 从WASM内存中读取数据 ... pass linker = wasmtime.Linker(self.engine) linker.define_func(“env”, “read_input”, host_read_input) self.store = wasmtime.Store(self.engine) self.instance = linker.instantiate(self.store, self.module) async def run(self, input_data: bytes) -> bytes: # 将input_data写入WASM模块的线性内存 memory = self.instance.get_memory(“memory”) # ... 内存写入逻辑 ... # 调用WASM模块的导出函数`compute` func = self.instance.get_func(“compute”) result = func(self.store) # 从内存中读取结果 # ... 内存读取逻辑 ... return result通过这种方式,工具代码在完全隔离的环境中运行,即使它存在缓冲区溢出等漏洞,也无法危及主机系统。
3. 关键技术点深度解析
3.1 LLM与工具的协同模式:思维链与ReAct
工具调用不是一次性的请求-响应。复杂的任务需要LLM与工具进行多轮交互,逐步逼近目标。这里主要有两种范式:
- 思维链(Chain-of-Thought, CoT)驱动:LLM先进行一系列“思考”,规划步骤,然后依次调用工具。这适合步骤清晰、依赖关系明确的任务。例如,“生成季度报告”可能被分解为:1. 调用
query_database获取数据;2. 调用analyze_data生成图表;3. 调用generate_doc合成报告。 - ReAct(Reason + Act)范式:这是更主流和强大的模式。LLM的每一次输出都包含“思考(Reason)”和“行动(Act)”两部分。
- 思考:分析当前情况、已获得的信息、下一步该做什么。
- 行动:决定调用哪个工具,并生成调用参数。 执行工具后,工具返回的结果会作为上下文再次输入给LLM,LLM基于新结果进行下一轮的“思考”和“行动”,形成一个循环。这赋予了Agent强大的动态规划和纠错能力。
ReAct循环示例(伪代码)
context = “用户:帮我找出服务器上最近一天错误日志中最常出现的错误信息。” tools = [search_files, analyze_text] # 可用工具列表 max_steps = 10 for step in range(max_steps): # 将当前上下文和工具描述喂给LLM prompt = f“”" 当前情况:{context} 你可以使用的工具:{tools_descriptions} 请按照‘Thought: ... Action: ...’格式回应。 “”" llm_response = call_llm(prompt) # 例如: “Thought: 我需要先找到今天的错误日志文件。Action: search_files {‘path’: ‘/var/log’, ‘pattern’: ‘*.log’, ‘modified_within’: ‘1d’}” # 解析出思考和行动 thought, action = parse_react(llm_response) if action is None: # LLM认为任务已完成,输出最终答案 break # 执行工具调用 tool_result = execute_action(action) # 将结果加入上下文,进入下一轮 context += f“\n行动结果:{tool_result}”这种模式使得Agent可以处理“打开文件,发现是压缩包,于是先调用解压工具,再分析内容”这类非预设路径的任务。
3.2 通信桥梁:gRPC在高性能工具调用中的实践
当工具作为独立的微服务存在时,高效的通信协议至关重要。相比传统的RESTful HTTP+JSON,gRPC在Agent工具调用场景下有显著优势:
- 性能:使用Protocol Buffers二进制序列化,数据体积小,编解码速度快。HTTP/2协议支持多路复用,减少了连接开销。这对于需要频繁、低延迟调用的工具(如实时数据查询、向量数据库检索)意义重大。
- 强类型接口:
.proto文件明确定义了服务和消息格式,相当于一份严格的工具调用合同。这避免了JSON解析时的类型错误,也方便不同语言(如Python Agent调用Go/C++工具)的集成。 - 流式支持:gRPC原生支持客户端流、服务器端流和双向流。这对于工具调用来说非常有用。例如,一个“监控日志”工具可以以服务器端流的形式,持续将新的日志行推送给Agent;一个“上传大文件”的工具可以使用客户端流分块发送数据。
实战踩坑:gRPC的异步集成在Python的异步框架(如FastAPI、asyncio)中集成gRPC客户端需要注意。标准的grpcio库是同步的,在异步事件循环中阻塞调用会导致性能问题。解决方案是使用grpcio.aio(异步IO版本)或grpclib库。
# 使用 grpclib 示例 import asyncio from grpclib.client import Channel from your_tool_proto import ToolServiceStub async def call_remote_tool_via_grpc(request): async with Channel(‘tool-service-host’, 50051) as channel: stub = ToolServiceStub(channel) # 调用远程工具方法 try: reply = await stub.ExecuteTool(request, timeout=10) return reply.result except Exception as e: return f“gRPC调用失败:{e}”同时,需要为每个工具服务设计合理的超时、重试和熔断机制,防止因某个工具服务不可用而导致整个Agent卡死。
3.3 安全与扩展性的基石:WASM沙箱
WASM在工具调用领域的价值日益凸显,它解决了两个核心痛点:安全和跨平台/语言。
安全沙箱:如前所述,WASM模块无法直接访问任何系统资源。所有对文件、网络、甚至内存的访问,都必须通过宿主环境(Host)显式提供的函数接口。这意味着你可以精细控制一个WASM工具能做什么。例如,一个“图片滤镜”WASM工具,你只授予它读取输入图片内存和写入输出图片内存的权限,它根本无法触及服务器的真实文件系统。
跨语言工具生态:你的Agent核心可能是Python写的,但某个性能关键的工具(如视频解码、密码学运算)用C++或Rust写会更高效。传统方式需要折腾FFI(外部函数接口)或网络服务。而WASM允许你将任何语言编写的工具编译成统一的字节码,然后在Python的WASM运行时(如wasmtime-py)中直接加载调用。这极大地丰富了Agent的工具生态。
WASM工具开发流程
- 工具开发:使用Rust/C++等语言编写工具逻辑,并定义好与宿主通信的接口(通常通过导入/导出函数)。
- 编译:使用对应语言的WASM工具链(如
wasm-packfor Rust)编译为.wasm文件。 - 宿主集成:在Agent中集成WASM运行时,加载
.wasm文件,并将宿主功能(如“读取文件”)作为导入函数提供给WASM模块。 - 调用:Agent通过WASM运行时调用WASM模块中的导出函数。
注意事项:WASM的性能与限制WASM虽然安全,但并非没有代价。WASM与宿主环境之间的数据交换(跨越“边界”)存在序列化和拷贝的开销,对于频繁交换大量数据的场景(如流式视频处理),需要精心设计数据传递方式(例如使用共享内存)。此外,WASM目前对线程(Threads)、SIMD等高级特性的支持仍在演进中。对于纯粹计算密集型且无需太多I/O的工具,WASM优势巨大;对于需要深度操作系统集成的工具,则可能仍需考虑传统方式。
4. 实战:构建一个支持多协议工具调用的Agent系统
下面,我将勾勒一个简化但完整的生产级Agent系统核心部分,它支持本地函数、gRPC服务和WASM模块三种工具类型。
4.1 系统架构图景
系统核心是一个工具网关(Tool Gateway)。它向上接收来自LLM推理引擎的结构化工具调用请求,向下根据工具类型路由到不同的执行器。
- 本地执行器:直接调用注册的Python函数。
- gRPC客户端池:管理到各个gRPC工具服务的连接,负责负载均衡和容错。
- WASM运行时管理器:管理多个WASM模块的加载、实例化和安全沙箱。
4.2 核心代码实现
第一步:定义统一工具接口
from abc import ABC, abstractmethod from typing import Any, Dict from pydantic import BaseModel class ToolCallRequest(BaseModel): tool_name: str arguments: Dict[str, Any] class ToolCallResponse(BaseModel): success: bool result: Any error_message: str = None class BaseToolExecutor(ABC): “”“所有工具执行器的基类。”“” @abstractmethod async def execute(self, request: ToolCallRequest) -> ToolCallResponse: pass第二步:实现gRPC工具执行器
import asyncio from grpclib.client import Channel from my_grpc_tools_pb2 import ExecuteRequest, ExecuteResponse from my_grpc_tools_grpc import ToolServiceStub from .base import BaseToolExecutor, ToolCallRequest, ToolCallResponse class GRPCToolExecutor(BaseToolExecutor): def __init__(self, service_endpoint: str): self.endpoint = service_endpoint self._channel = None self._stub = None async def _ensure_connection(self): if self._channel is None: self._channel = Channel(*self._parse_endpoint(self.endpoint)) self._stub = ToolServiceStub(self._channel) async def execute(self, request: ToolCallRequest) -> ToolCallResponse: await self._ensure_connection() grpc_request = ExecuteRequest( tool_name=request.tool_name, arguments=json.dumps(request.arguments) ) try: # 设置超时和重试 async with asyncio.timeout(30): response: ExecuteResponse = await self._stub.Execute(grpc_request) return ToolCallResponse(success=True, result=response.result) except asyncio.TimeoutError: return ToolCallResponse(success=False, error_message=“gRPC调用超时”) except Exception as e: return ToolCallResponse(success=False, error_message=f“gRPC调用异常:{e}”) finally: # 可选:实现连接池,这里简单关闭 # await self._channel.close() pass第三步:实现WASM工具执行器
import wasmtime from .base import BaseToolExecutor, ToolCallRequest, ToolCallResponse class WASMToolExecutor(BaseToolExecutor): def __init__(self, wasm_file_path: str): self.wasm_path = wasm_file_path self.engine = wasmtime.Engine() self.module = wasmtime.Module.from_file(self.engine, wasm_file_path) self.store = wasmtime.Store(self.engine) self.instance = None self._link_and_instantiate() def _link_and_instantiate(self): “”“链接宿主函数并实例化模块。”“” linker = wasmtime.Linker(self.engine) # 提供宿主函数,例如让WASM能申请内存 linker.define_func(“env”, “allocate”, self._host_allocate) linker.define_func(“env”, “deallocate”, self._host_deallocate) # 导入其他必要函数... self.instance = linker.instantiate(self.store, self.module) def _host_allocate(self, size: int) -> int: “”“宿主函数:为WASM分配内存,返回内存偏移量。”“” # 简化实现,实际需管理WASM内存 return 0 async def execute(self, request: ToolCallRequest) -> ToolCallResponse: if self.instance is None: return ToolCallResponse(success=False, error_message=“WASM模块未正确初始化”) # 1. 将参数序列化并写入WASM内存 arg_bytes = json.dumps(request.arguments).encode(‘utf-8’) memory = self.instance.get_memory(“memory”) # ... 将arg_bytes写入memory ... arg_ptr = 0 # 假设写入后的指针 # 2. 调用WASM模块的入口函数 main_func = self.instance.get_func(“main”) if main_func is None: return ToolCallResponse(success=False, error_message=“未找到入口函数”) try: # 调用函数,传入参数指针和长度 result_ptr = main_func(self.store, arg_ptr, len(arg_bytes)) # 3. 从WASM内存中读取结果 # ... 从memory的result_ptr处读取字节 ... result_bytes = b“” # 假设读取到的字节 result_str = result_bytes.decode(‘utf-8’) return ToolCallResponse(success=True, result=result_str) except wasmtime.WasmtimeError as e: return ToolCallResponse(success=False, error_message=f“WASM执行错误:{e}”)第四步:构建统一工具网关
class ToolGateway: def __init__(self): self.executors: Dict[str, BaseToolExecutor] = {} self.tool_metadata: Dict[str, dict] = {} # 存放工具描述和schema def register_tool(self, tool_name: str, metadata: dict, executor: BaseToolExecutor): self.tool_metadata[tool_name] = metadata self.executors[tool_name] = executor async def dispatch(self, tool_call: dict) -> str: tool_name = tool_call.get(‘name’) if tool_name not in self.executors: return json.dumps({“error”: f“Unknown tool: {tool_name}”}) executor = self.executors[tool_name] request = ToolCallRequest( tool_name=tool_name, arguments=tool_call.get(‘arguments’, {}) ) response = await executor.execute(request) if response.success: return response.result else: return json.dumps({“error”: response.error_message}) # 初始化网关并注册工具 gateway = ToolGateway() # 注册本地Python工具 gateway.register_tool(“local_calc”, local_metadata, PythonFunctionExecutor(local_calc_func)) # 注册gRPC工具 gateway.register_tool(“remote_query”, grpc_metadata, GRPCToolExecutor(“10.0.0.1:50051”)) # 注册WASM工具 gateway.register_tool(“wasm_filter”, wasm_metadata, WASMToolExecutor(“./tools/filter.wasm”))4.3 与LLM引擎的集成
最后,将工具网关与LLM(例如通过OpenAI API)连接起来,形成完整的ReAct循环。
import openai from openai.types.chat import ChatCompletionToolParam class AgentCore: def __init__(self, llm_client, tool_gateway): self.llm = llm_client self.tools = tool_gateway # 将工具元数据格式化为LLM需要的格式 self.llm_tools = [ ChatCompletionToolParam( type=“function”, function={ “name”: name, “description”: meta[‘description’], “parameters”: meta[‘schema’] } ) for name, meta in tool_gateway.tool_metadata.items() ] async def run(self, user_query: str, max_turns=10): messages = [{“role”: “user”, “content”: user_query}] for turn in range(max_turns): # 1. 调用LLM,传入当前对话历史和工具定义 response = await self.llm.chat.completions.create( model=“gpt-4”, messages=messages, tools=self.llm_tools, tool_choice=“auto” ) message = response.choices[0].message messages.append(message) # 将LLM回复加入历史 # 2. 检查LLM是否想调用工具 if message.tool_calls: for tool_call in message.tool_calls: # 3. 执行工具调用 tool_result = await self.tools.dispatch({ “id”: tool_call.id, “name”: tool_call.function.name, “arguments”: json.loads(tool_call.function.arguments) }) # 4. 将工具执行结果作为新消息加入历史,供LLM下一轮参考 messages.append({ “role”: “tool”, “tool_call_id”: tool_call.id, “name”: tool_call.function.name, “content”: tool_result }) else: # LLM没有调用工具,直接返回最终答案 return message.content return “达到最大交互轮数,任务可能未完成。”5. 常见问题与排查技巧实录
在实际开发和运维中,你会遇到各种各样的问题。以下是我从多个项目中总结出的“避坑指南”。
5.1 LLM工具调用不准或“幻觉”
现象:LLM要么不调用该调的工具,要么调用了错误的工具,或者生成的参数完全不对。
排查与解决:
- 检查工具描述:这是最常见的原因。描述是否清晰、无歧义?是否包含了关键参数信息?尝试用更具体、更场景化的语言重写描述。例如,将“处理文件”改为“读取文本文件内容并返回前N行”。
- 提供少量示例(Few-Shot):在系统提示词(System Prompt)中,给出一两个用户指令和正确工具调用示例。这能极大地引导LLM遵循你期望的格式和逻辑。
- 调整温度(Temperature)参数:对于需要严格遵循指令的工具调用场景,将温度值调低(如0.1或0),减少LLM的随机性。
- 后置参数校验与修正:不要完全信任LLM的输出。在执行工具前,对参数进行强制校验(类型、范围、必填项)。如果校验失败,可以将错误信息反馈给LLM,要求它重新生成调用。这构成了一个自我修正的循环。
5.2 工具执行超时或失败导致Agent卡死
现象:某个网络工具响应慢,或WASM工具陷入死循环,导致整个Agent线程被阻塞。
解决方案:
- 设置超时(Timeout):为每一个工具调用设置合理的超时时间。本地函数可以用
asyncio.wait_for,网络请求用相应客户端的超时参数。 - 实现熔断器(Circuit Breaker):如果某个工具在短时间内连续失败多次,熔断器会“跳闸”,暂时禁止对该工具的调用,直接返回一个预设的降级结果(或错误),并定期尝试恢复。这可以防止故障扩散。
- 异步(Async)架构:确保你的整个Agent核心和工具执行器都是异步的。这样,当一个工具在等待I/O(如网络响应)时,事件循环可以去处理其他请求或思考步骤,极大提升并发能力。
5.3 WASM工具与宿主环境数据交换效率低
现象:WASM工具本身执行很快,但传入大量数据(如图片)和获取结果时速度很慢。
优化技巧:
- 使用共享内存(Shared Memory):这是WASM MVP标准的一部分。你可以在宿主和WASM模块之间建立一块共享的
WebAssembly.Memory。宿主可以直接将数据写入这块内存的某个偏移量,然后告诉WASM函数数据的位置和大小,反之亦然。这避免了昂贵的跨边界拷贝。 - 批量操作:尽量避免在循环中频繁进行宿主-WASM调用。一次性传入所有需要处理的数据ID或配置,让WASM内部进行循环处理。
- 选择高效的序列化格式:如果必须通过函数参数传递数据,考虑使用像MessagePack或CBOR这样的二进制序列化格式,而不是JSON,以减少数据体积和解析开销。
5.4 工具权限管理与安全审计
现象:担心恶意用户通过精心构造的指令,让Agent调用危险工具(如rm -rf /)。
防御策略:
- 工具白名单:不是所有注册的工具都对所有用户或所有会话开放。根据用户身份、会话上下文,动态过滤可用的工具列表。一个普通用户可能只能使用“查询天气”和“计算器”,而管理员则可以使用“重启服务”工具。
- 输入净化与校验:对于接收自LLM的参数,尤其是文件路径、系统命令等,必须进行严格的校验和净化。防止路径遍历(
../../../etc/passwd)和命令注入。 - 完整的审计日志:记录每一次工具调用的详细信息:谁(会话ID)、何时、调用了什么工具、参数是什么、结果是什么、执行耗时。这不仅是安全审计的需要,也是后期分析和优化的重要数据。
- 沙箱化执行:对于高风险操作,强制在容器或WASM沙箱中执行。即使参数被恶意利用,其破坏范围也被限制在沙箱内。
构建一个强大的Agent工具调用系统,是一个在功能、性能、安全性和易用性之间不断权衡的艺术。从清晰的架构设计开始,选择适合你场景的通信协议(gRPC用于性能,WASM用于安全),并始终将安全审计和错误处理放在首位。随着工具生态的丰富,你会发现你的Agent真正拥有了“千手千眼”,能够解决越来越多真实世界中的复杂问题。这个过程充满挑战,但当你看到AI从“夸夸其谈”变为“真抓实干”时,那种成就感是无与伦比的。