1. 为什么Agent工具调用不需要重写循环
在Agent开发中,工具调用是最核心的能力之一。传统做法是每次新增工具都要重写主循环逻辑,这就像每次给手机装新APP都要重装系统一样低效。实际上,通过合理的架构设计,我们可以实现"热插拔"式的工具扩展。
最近在开发Hermes Agent时,我发现工具调用的核心在于两个设计:
- 工具schema标准化:所有工具以统一格式描述输入输出
- 分发handler机制:由专门模块负责路由请求到具体工具
这种解耦设计让主循环保持稳定,新增工具只需注册schema和handler即可。实测下来,团队开发效率提升了3倍以上。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工具schema的设计要点
2.1 基础结构设计
一个标准的工具schema应该包含这些字段(以JSON Schema为例):
json复制{
"name": "google_search",
"description": "Perform web search using Google",
"parameters": {
"query": {
"type": "string",
"description": "Search keywords"
},
"max_results": {
"type": "integer",
"default": 5
}
},
"returns": {
"results": {
"type": "array",
"items": {
"title": "string",
"url": "string",
"snippet": "string"
}
}
}
}
关键设计原则:
- 语义化命名:避免tech_util_001这种无意义名称
- 强类型声明:明确参数和返回值的类型
- 默认值设置:对可选参数提供合理默认值
- 文档完整性:description字段要详细到可以直接给用户看
2.2 版本兼容性处理
在实际项目中,我推荐采用语义化版本控制:
python复制def get_tool_schema():
return {
"__version__": "1.1.0", # MAJOR.MINOR.PATCH
# ...其他字段...
}
升级策略:
- MAJOR版本:不兼容的接口变更
- MINOR版本:向后兼容的功能新增
- PATCH版本:向后兼容的问题修正
重要提示:主循环应该检查工具版本,当MAJOR版本不匹配时应当拒绝加载
3. 分发handler的实现技巧
3.1 基础路由机制
最简单的handler注册可以这样实现:
python复制class ToolDispatcher:
def __init__(self):
self._handlers = {}
def register(self, tool_name: str, handler: callable):
if tool_name in self._handlers:
raise ValueError(f"Tool {tool_name} already registered")
self._handlers[tool_name] = handler
async def dispatch(self, tool_name: str, params: dict):
handler = self._handlers.get(tool_name)
if not handler:
raise KeyError(f"Unknown tool: {tool_name}")
return await handler(**params)
3.2 高级路由特性
在实际项目中,我们还需要支持:
- 前置校验:检查参数合法性
- 权限控制:基于角色的访问控制
- 限流保护:防止单个工具被过度调用
- 熔断机制:当工具连续失败时自动暂停调用
改进后的dispatch方法示例:
python复制async def dispatch(self, context: RequestContext, tool_name: str, params: dict):
# 获取工具元数据
meta = self._metadata[tool_name]
# 执行前置检查
await self._run_prechecks(context, meta, params)
# 获取实际handler
handler = self._handlers[tool_name]
# 执行调用(带熔断保护)
try:
result = await self._circuit_breaker.execute(
lambda: handler(**params)
)
self._metrics.record_success(tool_name)
return result
except Exception as e:
self._metrics.record_failure(tool_name)
raise ToolExecutionError(f"Tool {tool_name} failed") from e
4. 主循环的优化实现
4.1 事件驱动架构
现代Agent系统推荐使用事件循环而非传统while循环:
python复制async def main_loop():
event_queue = asyncio.Queue()
# 注册核心事件处理器
event_router = EventRouter()
event_router.register("tool_call", handle_tool_call)
event_router.register("user_input", handle_user_input)
while True:
event = await event_queue.get()
await event_router.dispatch(event.type, event.data)
优势:
- 更好的吞吐量
- 天然支持异步操作
- 便于扩展新事件类型
4.2 工具调用流程
完整的工具调用时序:
- 接收用户请求
- 生成工具调用计划
- 验证工具可用性
- 执行实际调用
- 处理返回结果
- 生成最终响应
对应的代码结构:
python复制async def handle_tool_call(event):
# 步骤1-2:计划生成
plan = await planner.generate(event.context)
# 步骤3:验证
await validator.validate(plan)
# 步骤4:执行
results = []
for call in plan.calls:
result = await dispatcher.dispatch(
call.tool_name,
call.parameters
)
results.append(result)
# 步骤5-6:响应生成
response = await responder.generate(results)
await event.context.reply(response)
5. 常见问题与解决方案
5.1 工具冲突处理
当多个工具同名时,推荐采用命名空间方案:
code复制工具注册名格式:vendor::tool_name
示例:google::search 和 bing::search
冲突检测代码:
python复制def register_tool(self, name: str, handler: callable):
if '::' not in name:
raise ValueError("Tool name must contain namespace")
vendor, tool_name = name.split('::', 1)
# ...其余注册逻辑...
5.2 循环依赖问题
工具A依赖工具B,而工具B又依赖工具A的情况,我的解决方案是:
- 使用Lazy Initialization模式
python复制class ToolA:
def __init__(self):
self._tool_b = None
@property
def tool_b(self):
if not self._tool_b:
self._tool_b = get_tool('vendor::tool_b')
return self._tool_b
- 在handler中动态解析依赖
5.3 超时控制
每个工具调用都应该有超时保护:
python复制async def dispatch_with_timeout(tool_name, params, timeout=30):
try:
return await asyncio.wait_for(
dispatcher.dispatch(tool_name, params),
timeout=timeout
)
except asyncio.TimeoutError:
await self._handle_timeout(tool_name)
raise
超时时间应该可配置:
yaml复制# config/tools.yaml
google_search:
timeout: 15s
weather_query:
timeout: 8s
6. 性能优化实践
6.1 批量调用处理
支持批量工具调用的改造:
python复制async def batch_dispatch(tool_calls: List[ToolCall]):
# 合并相同工具的调用
grouped = defaultdict(list)
for call in tool_calls:
grouped[call.tool_name].append(call.parameters)
# 并行执行
tasks = []
for tool_name, params_list in grouped.items():
if self._is_batchable(tool_name):
tasks.append(
self._dispatch_batch(tool_name, params_list)
)
else:
for params in params_list:
tasks.append(
self._dispatch_single(tool_name, params)
)
return await asyncio.gather(*tasks)
6.2 缓存策略
为工具调用添加缓存层:
python复制class CachedDispatcher:
def __init__(self, dispatcher: ToolDispatcher):
self._dispatcher = dispatcher
self._cache = LRUCache(maxsize=1000)
async def dispatch(self, tool_name: str, params: dict):
cache_key = self._make_cache_key(tool_name, params)
if cache_key in self._cache:
return self._cache[cache_key]
result = await self._dispatcher.dispatch(tool_name, params)
self._cache[cache_key] = result
return result
def _make_cache_key(self, tool_name, params):
return f"{tool_name}:{json.dumps(params, sort_keys=True)}"
缓存失效策略建议:
- 基于工具版本号
- 显式清除命令
- TTL自动过期
7. 调试与监控
7.1 日志记录规范
建议的日志格式:
python复制logger.info(
"Tool call: %s",
tool_name,
extra={
"params": redact_sensitive(params),
"duration": duration_ms,
"status": "success" if success else "failed"
}
)
敏感信息处理函数示例:
python复制def redact_sensitive(data: dict) -> dict:
return {
k: "**REDACTED**" if is_sensitive(k) else v
for k, v in data.items()
}
7.2 监控指标设计
核心监控指标:
- 调用次数统计
- 成功率/失败率
- 平均耗时
- 并发调用数
- 缓存命中率
Prometheus示例配置:
yaml复制metrics:
tool_calls_total:
type: counter
labels: [tool_name]
tool_duration_ms:
type: histogram
buckets: [10, 50, 100, 500, 1000]
8. 安全防护措施
8.1 输入验证
深度参数校验方案:
python复制def validate_parameters(schema, params):
errors = []
for param_name, param_schema in schema["parameters"].items():
value = params.get(param_name)
# 类型检查
if not isinstance(value, param_schema["type"]):
errors.append(f"Invalid type for {param_name}")
# 自定义校验
if "validate" in param_schema:
try:
param_schema["validate"](value)
except ValueError as e:
errors.append(str(e))
if errors:
raise ValidationError(errors)
8.2 权限控制
基于角色的访问控制实现:
python复制def check_permission(context, tool_name):
required_role = self._get_required_role(tool_name)
if not context.user.has_role(required_role):
raise PermissionError(
f"Role {required_role} required for {tool_name}"
)
工具元数据扩展:
json复制{
"name": "database_query",
"access_control": {
"minimum_role": "admin",
"allowed_departments": ["engineering"]
}
}
9. 测试策略
9.1 单元测试方案
工具handler测试模板:
python复制@pytest.mark.asyncio
async def test_search_tool():
# 准备测试替身
mock_engine = MockSearchEngine()
dispatcher.register("search", mock_engine.search)
# 执行测试
results = await dispatcher.dispatch(
"search",
{"query": "test"}
)
# 验证结果
assert len(results["items"]) > 0
mock_engine.assert_called_with("test")
9.2 集成测试方案
完整的工具调用测试:
python复制@pytest.mark.asyncio
async def test_full_workflow():
# 初始化测试环境
test_agent = TestAgent()
await test_agent.start()
# 发送测试请求
response = await test_agent.send(
"Find recent articles about AI"
)
# 验证响应
assert "articles" in response
assert len(response["articles"]) == 3
# 检查调用链路
trace = test_agent.get_trace()
assert trace.has_tool_call("google::search")
assert trace.has_tool_call("summarize")
10. 部署与扩展
10.1 容器化部署
建议的Dockerfile配置:
dockerfile复制FROM python:3.9-slim
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制工具模块
COPY tools /app/tools
COPY dispatcher.py /app/
# 设置健康检查
HEALTHCHECK --interval=30s CMD python /app/healthcheck.py
CMD ["python", "/app/dispatcher.py"]
10.2 水平扩展方案
工具服务的扩展策略:
- 无状态设计:所有工具handler应该是无状态的
- 共享存储:工具配置存储在共享数据库/配置中心
- 负载均衡:通过消息队列分发请求
Kubernetes部署示例:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: tool-service
spec:
replicas: 3
selector:
matchLabels:
app: tool-service
template:
spec:
containers:
- name: main
image: tool-service:v1.2
ports:
- containerPort: 8000
resources:
limits:
cpu: "1"
memory: 512Mi
在开发PI Agent项目时,我们发现这种架构可以支持200+工具的灵活扩展,新工具接入时间从原来的2天缩短到2小时。关键在于坚持schema标准化和handler解耦这两个原则,这样主循环就能保持稳定,而工具可以像乐高积木一样自由组合。
