第1674篇:工具注册中心的设计——动态工具发现与版本管理
第1674篇:工具注册中心的设计——动态工具发现与版本管理
最近有个同学问我:他们的Agent项目工具越来越多,几十个Tool分散在各个微服务里,Agent经常找不到合适的工具,或者调用了版本不兼容的工具,怎么解决?
这个问题我遇到过,而且踩了不少坑。当工具数量少的时候,直接把Tool定义写在Agent配置里没问题。但当工具数量增长到几十上百个,分散在不同团队不同服务里,就需要一个专门的工具注册中心了。
今天聊聊怎么设计这个注册中心,不只是功能层面,还要解决版本管理、动态发现、降级兜底这些工程问题。
没有注册中心时的典型问题
先说问题,这样后面的方案才有针对性。
问题1:工具定义分散,Agent不知道有哪些工具可用。
每个微服务团队都有自己的工具,文档东一块西一块,Agent的Prompt里要手写工具描述,改一个工具的描述还要改多处。
问题2:工具版本管理混乱。
同一个工具的v1版本和v2版本接口不兼容,但Agent不知道该调用哪个版本。有时候服务升级了,但Agent的配置没跟上,调用方式还是旧的。
问题3:工具不可用时没有降级。
某个工具所在的服务宕机了,Agent直接报错,整个任务失败。
问题4:无法按能力动态选择工具。
"我需要一个能发邮件的工具"——Agent只能精确匹配工具名,不能模糊地按功能描述找工具。
注册中心的核心设计
工具注册中心本质上是一个服务发现系统,只不过注册的不是"服务实例"而是"工具能力"。
先定义核心数据模型:
// 工具注册信息
@Data
@Builder
public class ToolRegistration {
private String toolId; // 全局唯一ID:provider:tool-name:version
private String provider; // 提供方:哪个服务/团队
private String name; // 工具名(如 send_email)
private String version; // 语义版本(如 1.2.0)
private String displayName; // 展示名(给Agent看的)
private String description; // 工具描述(给LLM看的,关键!)
private List<ToolParameter> parameters; // 参数定义
private ToolEndpoint endpoint; // 调用地址
private ToolCapabilities capabilities; // 工具能力标签
private ToolPolicy policy; // 调用策略(限流/权限/超时)
private RegistrationStatus status; // ACTIVE/DEPRECATED/UNAVAILABLE
private LocalDateTime registeredAt;
private LocalDateTime lastHeartbeatAt;
}
@Data
public class ToolParameter {
private String name;
private String type; // string/number/boolean/object/array
private String description;
private boolean required;
private Object defaultValue;
private String format; // email/url/date等格式约束
private List<String> enumValues; // 枚举值
}
@Data
public class ToolCapabilities {
private List<String> categories; // 如: ["communication", "email"]
private List<String> tags; // 如: ["async", "external-api"]
private String costLevel; // LOW/MEDIUM/HIGH(帮Agent做成本决策)
private boolean idempotent; // 是否幂等
private boolean readOnly; // 是否只读
private Duration avgLatency; // 平均延迟(帮Agent规划超时)
}
@Data
public class ToolPolicy {
private int rateLimit; // 每分钟最大调用次数
private Duration timeout; // 调用超时
private List<String> allowedRoles; // 允许调用的角色
private boolean requiresApproval; // 是否需要人工审批
}工具注册与心跳
工具提供方(各微服务)在启动时向注册中心注册,然后定期发送心跳:
// 工具提供方的注册客户端
@Component
public class ToolRegistryClient {
private final RestTemplate restTemplate;
private final ToolRegistrationProperties properties;
private ScheduledFuture<?> heartbeatTask;
@PostConstruct
public void registerTools() {
// 扫描当前服务里所有标注了@AgentTool的方法
List<ToolRegistration> tools = toolScanner.scanTools();
for (ToolRegistration tool : tools) {
try {
restTemplate.postForObject(
properties.getRegistryUrl() + "/api/tools/register",
tool,
Void.class
);
log.info("工具注册成功: {}", tool.getToolId());
} catch (Exception e) {
log.error("工具注册失败: {}", tool.getToolId(), e);
}
}
// 启动心跳
startHeartbeat();
}
private void startHeartbeat() {
heartbeatTask = scheduler.scheduleAtFixedRate(() -> {
List<String> toolIds = toolScanner.getRegisteredToolIds();
HeartbeatRequest request = new HeartbeatRequest();
request.setToolIds(toolIds);
request.setProviderInstance(getInstanceId());
request.setTimestamp(LocalDateTime.now());
try {
restTemplate.postForObject(
properties.getRegistryUrl() + "/api/tools/heartbeat",
request,
Void.class
);
} catch (Exception e) {
log.warn("心跳发送失败,注册中心可能不可达", e);
}
}, 0, 30, TimeUnit.SECONDS);
}
@PreDestroy
public void deregisterTools() {
if (heartbeatTask != null) {
heartbeatTask.cancel(false);
}
List<String> toolIds = toolScanner.getRegisteredToolIds();
restTemplate.postForObject(
properties.getRegistryUrl() + "/api/tools/deregister",
DeregisterRequest.builder().toolIds(toolIds).build(),
Void.class
);
}
}
// 用注解声明工具
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface AgentTool {
String name();
String description();
String version() default "1.0.0";
String[] categories() default {};
String[] tags() default {};
String costLevel() default "LOW";
boolean idempotent() default false;
boolean readOnly() default false;
int rateLimit() default 100;
long timeoutMs() default 5000;
}自动扫描和注册工具的逻辑:
@Component
public class ToolScanner {
@Autowired
private ApplicationContext applicationContext;
public List<ToolRegistration> scanTools() {
List<ToolRegistration> tools = new ArrayList<>();
// 扫描所有Bean中标注了@AgentTool的方法
for (String beanName : applicationContext.getBeanDefinitionNames()) {
Object bean = applicationContext.getBean(beanName);
Class<?> clazz = AopUtils.getTargetClass(bean);
for (Method method : clazz.getDeclaredMethods()) {
AgentTool annotation = method.getAnnotation(AgentTool.class);
if (annotation == null) continue;
ToolRegistration registration = buildRegistration(
bean, method, annotation
);
tools.add(registration);
}
}
return tools;
}
private ToolRegistration buildRegistration(Object bean, Method method,
AgentTool annotation) {
// 从方法签名自动生成参数定义
List<ToolParameter> parameters = Arrays.stream(method.getParameters())
.map(param -> {
ToolParameter tp = new ToolParameter();
tp.setName(param.getName());
tp.setType(mapJavaTypeToJsonType(param.getType()));
// 如果有@Param注解,用注解里的描述
Param paramAnnotation = param.getAnnotation(Param.class);
if (paramAnnotation != null) {
tp.setDescription(paramAnnotation.description());
tp.setRequired(paramAnnotation.required());
}
return tp;
})
.collect(Collectors.toList());
String provider = applicationContext.getEnvironment()
.getProperty("spring.application.name", "unknown");
String toolId = provider + ":" + annotation.name() + ":" + annotation.version();
ToolCapabilities capabilities = new ToolCapabilities();
capabilities.setCategories(Arrays.asList(annotation.categories()));
capabilities.setTags(Arrays.asList(annotation.tags()));
capabilities.setCostLevel(annotation.costLevel());
capabilities.setIdempotent(annotation.idempotent());
capabilities.setReadOnly(annotation.readOnly());
ToolPolicy policy = new ToolPolicy();
policy.setRateLimit(annotation.rateLimit());
policy.setTimeout(Duration.ofMillis(annotation.timeoutMs()));
return ToolRegistration.builder()
.toolId(toolId)
.provider(provider)
.name(annotation.name())
.version(annotation.version())
.description(annotation.description())
.parameters(parameters)
.capabilities(capabilities)
.policy(policy)
.status(RegistrationStatus.ACTIVE)
.registeredAt(LocalDateTime.now())
.build();
}
}注册中心服务端:工具发现与版本管理
@RestController
@RequestMapping("/api/tools")
public class ToolRegistryController {
private final ToolRegistryService registryService;
// 工具注册
@PostMapping("/register")
public ResponseEntity<Void> register(@RequestBody ToolRegistration registration) {
registryService.register(registration);
return ResponseEntity.ok().build();
}
// 心跳
@PostMapping("/heartbeat")
public ResponseEntity<Void> heartbeat(@RequestBody HeartbeatRequest request) {
registryService.updateHeartbeat(request.getToolIds(),
request.getTimestamp());
return ResponseEntity.ok().build();
}
// 查询可用工具列表(Agent调用)
@GetMapping("/available")
public List<ToolRegistration> getAvailableTools(
@RequestParam(required = false) String category,
@RequestParam(required = false) String tag,
@RequestParam(defaultValue = "false") boolean includeDeprecated) {
return registryService.findAvailable(category, tag, includeDeprecated);
}
// 语义搜索工具(按描述找工具)
@PostMapping("/search")
public List<ToolSearchResult> searchTools(@RequestBody ToolSearchRequest request) {
return registryService.semanticSearch(request.getQuery(), request.getTopK());
}
}
@Service
public class ToolRegistryService {
private final ToolRegistrationRepository repository;
private final EmbeddingClient embeddingClient;
// Redis缓存:工具列表,5分钟TTL
@Cacheable(value = "available-tools", key = "#category + ':' + #tag")
public List<ToolRegistration> findAvailable(String category, String tag,
boolean includeDeprecated) {
return repository.findActiveTools(category, tag, includeDeprecated);
}
/**
* 语义搜索:根据自然语言描述找工具
* 比如:"我需要能发邮件的工具" -> 找到 send_email
*/
public List<ToolSearchResult> semanticSearch(String query, int topK) {
float[] queryEmbedding = embeddingClient.embed(query);
// 向量相似度检索
List<ToolRegistration> similar = repository.findSimilarByEmbedding(
queryEmbedding, topK
);
return similar.stream()
.map(tool -> new ToolSearchResult(
tool,
computeSimilarity(queryEmbedding, tool.getDescriptionEmbedding())
))
.sorted(Comparator.comparingDouble(ToolSearchResult::getSimilarity).reversed())
.collect(Collectors.toList());
}
/**
* 版本解析:当Agent请求 send_email 时,返回最新的稳定版本
*/
public Optional<ToolRegistration> resolveVersion(String toolName,
String versionConstraint) {
List<ToolRegistration> allVersions = repository.findByName(toolName);
if (allVersions.isEmpty()) {
return Optional.empty();
}
// 版本约束解析(类似npm semver)
VersionRange range = VersionRange.parse(versionConstraint); // 如 "^1.0.0"
return allVersions.stream()
.filter(t -> t.getStatus() == RegistrationStatus.ACTIVE)
.filter(t -> range.includes(SemanticVersion.parse(t.getVersion())))
.max(Comparator.comparing(t -> SemanticVersion.parse(t.getVersion())));
}
/**
* 健康检查:定期标记心跳超时的工具为不可用
*/
@Scheduled(fixedDelay = 60000)
public void checkToolHealth() {
LocalDateTime threshold = LocalDateTime.now().minusSeconds(90);
int unhealthy = repository.markUnhealthyTools(threshold);
if (unhealthy > 0) {
log.warn("{}个工具心跳超时,已标记为不可用", unhealthy);
// 清除缓存
cacheManager.getCache("available-tools").clear();
}
}
}Agent侧的工具路由器
Agent不直接调用工具,而是通过工具路由器。路由器负责工具发现、版本解析、失败降级:
@Service
public class ToolRouter {
private final ToolRegistryService registryService;
private final ToolExecutor executor;
private final CircuitBreakerRegistry circuitBreakerRegistry;
/**
* Agent调用工具的统一入口
*/
public ToolResult invoke(String toolName, Map<String, Object> params,
String versionConstraint) {
// 1. 解析工具版本
ToolRegistration tool = registryService.resolveVersion(
toolName, versionConstraint != null ? versionConstraint : "latest"
).orElseThrow(() -> new ToolNotFoundException("工具不存在或无可用版本: " + toolName));
// 2. 检查工具状态
if (tool.getStatus() != RegistrationStatus.ACTIVE) {
// 尝试找同类的替代工具
ToolRegistration fallback = findFallback(tool);
if (fallback != null) {
log.warn("工具{}不可用,使用替代工具{}", tool.getToolId(), fallback.getToolId());
tool = fallback;
} else {
throw new ToolUnavailableException("工具不可用且无替代: " + toolName);
}
}
// 3. 检查调用策略(权限、限流等)
checkPolicy(tool.getPolicy(), params);
// 4. 带熔断器执行
CircuitBreaker cb = circuitBreakerRegistry.circuitBreaker(tool.getToolId());
return cb.executeSupplier(() -> {
return executor.execute(tool, params);
});
}
/**
* 查找替代工具:同类别、同标签的其他工具
*/
private ToolRegistration findFallback(ToolRegistration original) {
if (original.getCapabilities().getCategories().isEmpty()) {
return null;
}
String primaryCategory = original.getCapabilities().getCategories().get(0);
return registryService.findAvailable(primaryCategory, null, false)
.stream()
.filter(t -> !t.getToolId().equals(original.getToolId()))
.filter(t -> t.getStatus() == RegistrationStatus.ACTIVE)
.findFirst()
.orElse(null);
}
/**
* 为Agent动态生成工具描述(给LLM的system prompt用)
* 每次对话开始时调用,保证工具描述是最新的
*/
public String generateToolsDescription(List<String> requiredCategories) {
List<ToolRegistration> tools = requiredCategories.isEmpty()
? registryService.findAvailable(null, null, false)
: requiredCategories.stream()
.flatMap(cat -> registryService.findAvailable(cat, null, false).stream())
.distinct()
.collect(Collectors.toList());
StringBuilder sb = new StringBuilder();
sb.append("你可以使用以下工具:\n\n");
for (ToolRegistration tool : tools) {
sb.append(String.format("## %s\n", tool.getName()));
sb.append(String.format("描述:%s\n", tool.getDescription()));
sb.append("参数:\n");
for (ToolParameter param : tool.getParameters()) {
sb.append(String.format("- %s (%s%s): %s\n",
param.getName(),
param.getType(),
param.isRequired() ? ", 必填" : "",
param.getDescription()
));
}
if (tool.getCapabilities().getCostLevel().equals("HIGH")) {
sb.append("⚠️ 注意:此工具调用成本较高,请谨慎使用。\n");
}
sb.append("\n");
}
return sb.toString();
}
}工具版本兼容性管理
版本管理是注册中心最容易被忽视但又非常重要的功能。
@Service
public class ToolVersionManager {
/**
* 注册新版本工具时,检查与旧版本的兼容性
*/
public CompatibilityReport checkCompatibility(ToolRegistration newVersion,
ToolRegistration oldVersion) {
List<String> breakingChanges = new ArrayList<>();
List<String> warnings = new ArrayList<>();
// 检查参数变更
Map<String, ToolParameter> oldParams = oldVersion.getParameters().stream()
.collect(Collectors.toMap(ToolParameter::getName, p -> p));
Map<String, ToolParameter> newParams = newVersion.getParameters().stream()
.collect(Collectors.toMap(ToolParameter::getName, p -> p));
// 新版本删除了旧版本的参数 -> 破坏性变更
for (String paramName : oldParams.keySet()) {
if (!newParams.containsKey(paramName)) {
breakingChanges.add("删除了参数: " + paramName);
}
}
// 新版本把可选参数改成必填 -> 破坏性变更
for (String paramName : newParams.keySet()) {
if (oldParams.containsKey(paramName)) {
ToolParameter oldParam = oldParams.get(paramName);
ToolParameter newParam = newParams.get(paramName);
if (!oldParam.isRequired() && newParam.isRequired()) {
breakingChanges.add("参数从可选改为必填: " + paramName);
}
if (!oldParam.getType().equals(newParam.getType())) {
breakingChanges.add("参数类型变更: " + paramName
+ " " + oldParam.getType() + " -> " + newParam.getType());
}
}
}
// 新增了必填参数 -> 破坏性变更
for (String paramName : newParams.keySet()) {
if (!oldParams.containsKey(paramName) && newParams.get(paramName).isRequired()) {
breakingChanges.add("新增了必填参数: " + paramName);
}
}
boolean isCompatible = breakingChanges.isEmpty();
// 自动决定版本号
SemanticVersion oldSemver = SemanticVersion.parse(oldVersion.getVersion());
SemanticVersion suggested;
if (!isCompatible) {
// 有破坏性变更,主版本号+1
suggested = oldSemver.incrementMajor();
} else if (!warnings.isEmpty()) {
// 有警告,次版本号+1
suggested = oldSemver.incrementMinor();
} else {
// 纯增量,修订号+1
suggested = oldSemver.incrementPatch();
}
return new CompatibilityReport(isCompatible, breakingChanges,
warnings, suggested.toString());
}
/**
* 工具版本迁移:自动将使用旧版本的Agent迁移到新版本
* 仅适用于向后兼容的版本升级
*/
@Transactional
public void migrateAgentsToNewVersion(String toolName,
String oldVersion,
String newVersion) {
CompatibilityReport report = checkCompatibility(
getToolVersion(toolName, newVersion),
getToolVersion(toolName, oldVersion)
);
if (!report.isCompatible()) {
throw new IncompatibleVersionException(
"版本不兼容,无法自动迁移: " + report.getBreakingChanges()
);
}
// 标记旧版本为deprecated
repository.updateStatus(toolName, oldVersion, RegistrationStatus.DEPRECATED);
// 通知所有使用旧版本的Agent
List<String> affectedAgents = agentRepository.findByToolVersion(toolName, oldVersion);
for (String agentId : affectedAgents) {
eventPublisher.publish(new ToolVersionDeprecatedEvent(
agentId, toolName, oldVersion, newVersion
));
}
log.info("工具版本迁移完成: {}@{} -> {}@{}, 影响{}个Agent",
toolName, oldVersion, toolName, newVersion, affectedAgents.size());
}
}实战:给Agent注入动态工具列表
把注册中心和Agent连起来,实现真正的动态工具发现:
@Service
public class DynamicToolAwareAgent {
private final ToolRouter toolRouter;
private final ToolRegistryService registryService;
private final LLMClient llmClient;
public AgentResult run(AgentTask task) {
// 根据任务类型动态加载需要的工具
List<String> requiredCategories = task.getRequiredCapabilities();
// 生成动态工具描述
String toolsDesc = toolRouter.generateToolsDescription(requiredCategories);
// 构建system prompt,包含动态工具描述
String systemPrompt = """
你是一个智能助手,可以使用工具来完成任务。
%s
当你需要调用工具时,请使用以下格式:
<tool_call>
{"tool": "工具名", "params": {参数}}
</tool_call>
""".formatted(toolsDesc);
// 开始ReAct循环
List<Message> messages = new ArrayList<>();
messages.add(Message.system(systemPrompt));
messages.add(Message.user(task.getInstruction()));
for (int step = 0; step < task.getMaxSteps(); step++) {
String response = llmClient.chat(messages);
messages.add(Message.assistant(response));
// 解析是否有工具调用
Optional<ToolCall> toolCall = parseToolCall(response);
if (toolCall.isEmpty()) {
// 没有工具调用,任务完成
return AgentResult.success(response);
}
// 执行工具调用
ToolResult result = toolRouter.invoke(
toolCall.get().getTool(),
toolCall.get().getParams(),
null // 使用最新版本
);
messages.add(Message.tool(result.toString()));
}
return AgentResult.failed("超过最大步骤数");
}
}几点实际经验
关于工具描述的质量: 工具描述的质量直接影响LLM的工具选择准确性。描述里要说清楚工具能做什么、不能做什么、适用场景。我们专门为工具描述建了一个评审流程,新工具上线前要过一轮Prompt质量检查。
关于版本兼容性测试: 每次工具版本升级,要有自动化的回归测试验证兼容性。我们用Golden dataset测试:一批历史工具调用请求,新版本处理结果要和旧版本一致。
关于注册中心的可用性: 注册中心本身要高可用,Agent不能因为注册中心挂了就完全不能工作。我们在Agent侧做了本地缓存,注册中心不可达时用缓存的工具列表,保证基本可用。
关于工具描述的向量化: 定期对工具描述做向量化并更新索引,是注册中心维护工作的一部分。工具描述更新了但忘了重新向量化,语义搜索就会失效。
工具注册中心是多工具、多团队协作场景下的必要基础设施。刚开始可能觉得过度设计,但当工具数量超过20个、工具开发团队超过2个的时候,没有注册中心的痛苦会让你后悔没早建。
