第1935篇:动态工具生成——根据上下文运行时创建临时工具的工程模式
第1935篇:动态工具生成——根据上下文运行时创建临时工具的工程模式
这个话题比较冷门,很多做Agent的同学都没有用到过。但我第一次见到这个需求场景时,觉得挺有意思的。
事情是这样的:我们在做一个企业内部的BI助手,用户可以用自然语言查询各种数据报表。最开始的设计是把所有查询工具都预先定义好——查销售、查库存、查用户增长,一共大概三十几个工具。结果上线用了两周,产品来说:"有个新部门要接入,他们有二十几张新报表,你能帮他们快速加进来吗?"
按老思路,我需要为每个报表写一个工具,定义Schema,写实现类,重新部署。一套下来最快也要两三天。
然后我换了个思路:既然这些工具的底层逻辑都一样(拼SQL查数据库),能不能根据报表元数据动态生成工具的定义?用户说要查什么报表,系统就临时生成一个对应的工具,不需要预先硬编码。
这就是动态工具生成的核心思路。
动态工具生成的适用场景
先说清楚什么情况下值得用动态工具生成:
适合的场景:
- 工具数量多,但工具的执行模式相似(都是查某张表、都是调某类API)
- 工具集合需要根据用户、租户、权限动态变化
- 工具在运行时动态创建(比如用户定义了自定义工作流程)
- 想给Agent提供元编程能力(Agent自己生成工具)
不适合的场景:
- 工具数量少(20个以内),每个工具逻辑差异大
- 工具Schema需要人工精细调优
- 安全要求高,不能有运行时代码生成
基础模式:模板驱动的工具生成
最简单的动态工具生成是模板模式:预先定义工具模板,根据元数据渲染出具体的工具定义。
// 报表工具模板
public class ReportToolTemplate {
private static final String TOOL_NAME_TEMPLATE = "query_report_%s";
private static final String DESCRIPTION_TEMPLATE =
"查询%s报表数据。%s\n" +
"支持的过滤条件:%s\n" +
"返回字段:%s";
/**
* 根据报表元数据生成工具定义
*/
public Map<String, Object> generateTool(ReportMetadata report) {
Map<String, Object> tool = new LinkedHashMap<>();
tool.put("type", "function");
Map<String, Object> function = new LinkedHashMap<>();
function.put("name", String.format(TOOL_NAME_TEMPLATE, report.getId()));
function.put("description", buildDescription(report));
function.put("parameters", buildParameters(report));
tool.put("function", function);
return tool;
}
private String buildDescription(ReportMetadata report) {
String filterDesc = report.getFilters().stream()
.map(f -> f.getName() + "(" + f.getDescription() + ")")
.collect(Collectors.joining("、"));
String fieldDesc = report.getOutputFields().stream()
.map(f -> f.getName() + ":" + f.getDescription())
.collect(Collectors.joining(","));
return String.format(DESCRIPTION_TEMPLATE,
report.getName(),
report.getBusinessDescription(),
filterDesc,
fieldDesc
);
}
private Map<String, Object> buildParameters(ReportMetadata report) {
Map<String, Object> params = new LinkedHashMap<>();
params.put("type", "object");
Map<String, Object> properties = new LinkedHashMap<>();
List<String> required = new ArrayList<>();
for (ReportFilter filter : report.getFilters()) {
Map<String, Object> param = buildParameterFromFilter(filter);
properties.put(filter.getParamName(), param);
if (filter.isRequired()) {
required.add(filter.getParamName());
}
}
// 所有报表都支持分页
properties.put("page", Map.of(
"type", "integer",
"description", "页码,从1开始",
"minimum", 1,
"default", 1
));
properties.put("page_size", Map.of(
"type", "integer",
"description", "每页记录数",
"minimum", 1,
"maximum", 100,
"default", 20
));
params.put("properties", properties);
if (!required.isEmpty()) {
params.put("required", required);
}
return params;
}
private Map<String, Object> buildParameterFromFilter(ReportFilter filter) {
Map<String, Object> param = new LinkedHashMap<>();
switch (filter.getType()) {
case DATE_RANGE -> {
param.put("type", "object");
param.put("description", filter.getDescription());
param.put("properties", Map.of(
"start", Map.of("type", "string", "description", "开始日期,格式YYYY-MM-DD"),
"end", Map.of("type", "string", "description", "结束日期,格式YYYY-MM-DD")
));
}
case ENUM -> {
param.put("type", "string");
param.put("description", filter.getDescription());
param.put("enum", filter.getEnumValues());
}
case NUMBER_RANGE -> {
param.put("type", "object");
param.put("description", filter.getDescription());
param.put("properties", Map.of(
"min", Map.of("type", "number"),
"max", Map.of("type", "number")
));
}
default -> {
param.put("type", "string");
param.put("description", filter.getDescription());
}
}
return param;
}
}元数据存储与工具注册
动态工具需要一个元数据存储,以及运行时的工具注册机制:
@Service
public class DynamicToolRegistry {
private final ReportMetadataRepository metadataRepo;
private final ReportToolTemplate toolTemplate;
// 工具缓存:工具名 -> 工具定义
private final Map<String, Map<String, Object>> toolCache = new ConcurrentHashMap<>();
// 工具执行器缓存:工具名 -> 执行器
private final Map<String, ToolExecutor> executorCache = new ConcurrentHashMap<>();
@PostConstruct
public void initializeTools() {
loadAllReportTools();
}
private void loadAllReportTools() {
List<ReportMetadata> reports = metadataRepo.findAllActive();
reports.forEach(this::registerReportTool);
log.info("已加载{}个动态报表工具", reports.size());
}
public void registerReportTool(ReportMetadata report) {
String toolName = "query_report_" + report.getId();
// 生成工具定义
Map<String, Object> toolDef = toolTemplate.generateTool(report);
toolCache.put(toolName, toolDef);
// 生成执行器
ToolExecutor executor = buildReportExecutor(report);
executorCache.put(toolName, executor);
log.info("注册动态工具:{}", toolName);
}
// 根据用户权限获取工具列表
public List<Map<String, Object>> getToolsForUser(UserContext user) {
Set<String> allowedReportIds = user.getAllowedReportIds();
return toolCache.entrySet().stream()
.filter(entry -> {
String reportId = entry.getKey().replace("query_report_", "");
return allowedReportIds.contains(reportId);
})
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}
// 热更新:新增报表时不需要重启
public void addReport(ReportMetadata report) {
registerReportTool(report);
log.info("热添加报表工具:{}", report.getId());
}
// 删除报表工具
public void removeReport(String reportId) {
String toolName = "query_report_" + reportId;
toolCache.remove(toolName);
executorCache.remove(toolName);
log.info("移除报表工具:{}", reportId);
}
// 执行工具调用
public ToolResult executeTool(String toolName, Map<String, Object> params) {
ToolExecutor executor = executorCache.get(toolName);
if (executor == null) {
return ToolResult.failure(
ToolResult.ErrorType.RESOURCE_NOT_FOUND,
"TOOL_NOT_FOUND",
"工具不存在:" + toolName
);
}
return executor.execute(params);
}
private ToolExecutor buildReportExecutor(ReportMetadata report) {
return params -> {
// 根据参数生成SQL
String sql = report.getSqlTemplate();
Map<String, Object> sqlParams = extractSqlParams(params, report);
// 执行查询
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, sqlParams);
// 格式化结果
return ToolResult.success(formatQueryResult(rows, report));
};
}
}进阶模式:Agent自生成工具
更高级的用法是让Agent自己在对话过程中生成临时工具。这个场景通常出现在:用户提了一个Agent目前工具集里没有的能力,但这个能力可以由已有工具组合实现。
// 工具生成器工具(这是一个元工具,让Agent能生成新工具)
@Component
public class ToolGeneratorTool implements ToolHandler {
private final DynamicToolRegistry toolRegistry;
private final LlmClient llmClient;
@Override
public String getToolName() {
return "generate_tool";
}
@Override
public Map<String, Object> getToolSchema() {
return Map.of(
"name", "generate_tool",
"description", "当用户需要的功能无法由现有工具完成时,根据描述生成一个新的临时工具。只有在确认现有工具无法满足需求时才使用。",
"parameters", Map.of(
"type", "object",
"properties", Map.of(
"tool_description", Map.of(
"type", "string",
"description", "新工具需要完成什么功能的自然语言描述"
),
"base_tools", Map.of(
"type", "array",
"description", "新工具基于哪些现有工具实现",
"items", Map.of("type", "string")
)
),
"required", List.of("tool_description")
)
);
}
@Override
public ToolResult execute(Map<String, Object> params) {
String description = (String) params.get("tool_description");
List<String> baseTools = (List<String>) params.getOrDefault("base_tools", List.of());
// 让LLM生成工具的实现逻辑描述
String toolSpec = generateToolSpec(description, baseTools);
// 解析工具规格,创建一个组合工具
CompositeToolDefinition compositeTool = parseToolSpec(toolSpec);
// 注册到临时工具注册表
String tempToolId = "temp_" + UUID.randomUUID().toString().substring(0, 8);
toolRegistry.registerTempTool(tempToolId, compositeTool);
return ToolResult.success(String.format("""
{
"tool_id": "%s",
"tool_name": "use_tool_%s",
"description": "已生成临时工具,你现在可以调用use_tool_%s来完成任务",
"spec_summary": "%s"
}
""", tempToolId, tempToolId, tempToolId, compositeTool.getSummary())
);
}
private String generateToolSpec(String description, List<String> baseTools) {
String prompt = String.format("""
请为以下功能设计一个工具的执行规格:
功能描述:%s
可用的基础工具:%s
请描述:
1. 工具需要接收哪些输入参数
2. 执行步骤(按顺序调用哪些基础工具,传什么参数)
3. 如何整合结果
4. 输出格式
用JSON格式返回工具规格。
""", description, String.join(", ", baseTools));
return llmClient.complete(prompt);
}
}这个"元工具"让Agent具有了自我扩展能力,但需要非常谨慎地使用。在生产环境里,要加上严格的权限控制和沙箱隔离,防止生成恶意工具。
权限隔离:不同用户看到不同工具集
动态工具的一个重要应用:根据用户身份和权限动态调整工具集。
@Service
public class ContextualToolProvider {
private final DynamicToolRegistry toolRegistry;
private final PermissionService permissionService;
/**
* 根据用户上下文,返回该用户在当前对话中可用的工具列表
*/
public List<Map<String, Object>> getAvailableTools(UserContext user, ConversationContext conv) {
List<Map<String, Object>> tools = new ArrayList<>();
// 1. 基础工具:所有用户都有
tools.addAll(getBaseTools());
// 2. 角色工具:根据用户角色添加
if (user.hasRole("ANALYST")) {
tools.addAll(toolRegistry.getToolsByCategory("data_analysis"));
}
if (user.hasRole("MANAGER")) {
tools.addAll(toolRegistry.getToolsByCategory("approval_workflow"));
}
// 3. 数据权限工具:根据用户的数据访问权限
Set<String> dataPermissions = permissionService.getDataPermissions(user.getId());
tools.addAll(toolRegistry.getToolsForPermissions(dataPermissions));
// 4. 上下文工具:根据当前对话的业务场景
String bizContext = conv.detectBusinessContext();
if ("ORDER_SERVICE".equals(bizContext)) {
tools.addAll(getOrderServiceTools(user));
} else if ("FINANCE".equals(bizContext)) {
tools.addAll(getFinanceTools(user));
}
// 5. 去重,防止重复工具定义
return deduplicateTools(tools);
}
private List<Map<String, Object>> getOrderServiceTools(UserContext user) {
List<Map<String, Object>> orderTools = new ArrayList<>();
// 查询权限所有人都有
orderTools.add(toolRegistry.getTool("get_order_by_id"));
orderTools.add(toolRegistry.getTool("list_orders"));
// 操作权限按角色给
if (user.hasPermission("ORDER_CANCEL")) {
orderTools.add(toolRegistry.getTool("cancel_order"));
}
if (user.hasPermission("REFUND_APPROVE")) {
orderTools.add(toolRegistry.getTool("approve_refund"));
}
return orderTools;
}
}这个模式解决了一个常见问题:单一工具集时,模型看到的工具太多,会增加选择错误工具的概率。通过按权限和上下文动态裁剪工具集,让模型每次只看到相关的工具,准确率明显提升。
工具元数据的缓存策略
动态工具生成有一个性能问题:每次对话都重新生成工具定义,开销不小。需要合适的缓存策略:
@Service
public class ToolDefinitionCache {
// 两层缓存
private final Cache<String, Map<String, Object>> localCache; // 本地内存缓存
private final RedisTemplate<String, String> redisTemplate; // 分布式缓存
private final ObjectMapper objectMapper;
public ToolDefinitionCache() {
// 本地缓存:最多1000个工具,5分钟过期
this.localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
@SuppressWarnings("unchecked")
public Map<String, Object> getOrCompute(
String toolKey,
Supplier<Map<String, Object>> toolGenerator) {
// 先查本地缓存
Map<String, Object> tool = localCache.getIfPresent(toolKey);
if (tool != null) return tool;
// 再查Redis缓存
String cached = redisTemplate.opsForValue().get("tool:" + toolKey);
if (cached != null) {
try {
tool = objectMapper.readValue(cached, Map.class);
localCache.put(toolKey, tool); // 回填本地缓存
return tool;
} catch (JsonProcessingException e) {
log.warn("工具缓存反序列化失败", e);
}
}
// 生成工具定义
tool = toolGenerator.get();
// 写入两层缓存
localCache.put(toolKey, tool);
try {
redisTemplate.opsForValue().set(
"tool:" + toolKey,
objectMapper.writeValueAsString(tool),
30, TimeUnit.MINUTES
);
} catch (JsonProcessingException e) {
log.warn("工具缓存序列化失败", e);
}
return tool;
}
// 报表元数据更新时,失效对应的工具缓存
public void invalidateTool(String toolKey) {
localCache.invalidate(toolKey);
redisTemplate.delete("tool:" + toolKey);
log.info("工具缓存已失效:{}", toolKey);
}
}运行时工具验证与安全检查
动态生成的工具,在真正使用之前必须做安全验证:
@Service
public class ToolSecurityValidator {
// 禁止生成的工具名称(避免覆盖核心工具)
private static final Set<String> PROTECTED_TOOL_NAMES = Set.of(
"execute_code", "system_command", "file_write", "database_delete"
);
// 动态工具只能访问的白名单数据表
private static final Set<String> ALLOWED_TABLES = loadAllowedTables();
public ValidationResult validate(GeneratedToolDefinition tool) {
List<String> errors = new ArrayList<>();
List<String> warnings = new ArrayList<>();
// 1. 工具名称检查
if (PROTECTED_TOOL_NAMES.contains(tool.getName())) {
errors.add("工具名称与受保护工具冲突:" + tool.getName());
}
if (!tool.getName().matches("^[a-z][a-z0-9_]*$")) {
errors.add("工具名称格式不合法,只允许小写字母、数字和下划线");
}
// 2. SQL注入检查(针对报表类工具)
if (tool.hasSqlTemplate()) {
String sql = tool.getSqlTemplate();
if (containsSqlInjectionRisk(sql)) {
errors.add("SQL模板存在注入风险,请使用参数化查询");
}
// 检查SQL只访问白名单表
Set<String> accessedTables = extractTableNames(sql);
Set<String> unauthorizedTables = Sets.difference(accessedTables, ALLOWED_TABLES);
if (!unauthorizedTables.isEmpty()) {
errors.add("SQL访问了未授权的表:" + unauthorizedTables);
}
// 不允许写操作
if (sql.toUpperCase().matches(".*(INSERT|UPDATE|DELETE|DROP|TRUNCATE).*")) {
errors.add("动态工具不允许包含写操作SQL");
}
}
// 3. Schema验证
try {
validateJsonSchema(tool.getParameterSchema());
} catch (SchemaValidationException e) {
errors.add("参数Schema格式不合法:" + e.getMessage());
}
// 4. 描述长度检查
if (tool.getDescription().length() > 2000) {
warnings.add("工具描述过长,可能影响模型理解,建议精简");
}
return new ValidationResult(errors.isEmpty(), errors, warnings);
}
private boolean containsSqlInjectionRisk(String sql) {
// 检查是否使用了参数化查询
// 简单检查:如果SQL里有直接字符串拼接的迹象
return sql.contains("' + ") || sql.contains("\" + ") ||
sql.matches(".*'\\s*\\+\\s*.*");
}
}一个完整的动态工具流程图
动态工具生成是Agent工程里的高级技巧,真正能发挥威力的场景是:工具数量多、需要按权限/上下文动态裁剪、工具逻辑有共性可以模板化。
用好了,能让你的Agent系统的扩展性极大提升,新接入一个数据源或服务,只需要配置元数据,不用改代码。但用之前一定要把安全验证做扎实——动态生成的工具是攻击面,不能掉以轻心。
