
前言在进入正文之前先交代一下这些文章的来龙去脉。AgentForge是一个面向 Java 开发者、从LLM 最底层能力开始构建的开源 Agent 框架。它不从高度封装的 Agent API 起步而是先建立稳定、统一、可扩展的模型抽象再逐层向上锻造 Tool、Memory、Middleware、Reasoning 与 Agent Runtime 等能力。AgentForge Agent ForgeAgent代表能理解目标、进行推理、调用工具并完成任务的智能体Forge则强调把原始智能持续加工、塑形、强化最终锻造成真正可用的产品。本系列《AgentForge 核心模块设计原理》沿着这条自底向上的路径逐个模块拆解它的设计原理与实现细节。本文聚焦AgentForge Local 工具模块的核心设计——如何用Tool/P注解与反射把普通 Java 方法自动扫描为ToolSpecification并完成 JSON 参数到方法入参的绑定与调用。开源仓库GitHubhttps://github.com/changluya/AgentForge项目文档站https://changluya.github.io/AgentForge/Gitee 镜像https://gitee.com/changluJava/agent-forge开源协议MITgitclone https://github.com/changluya/AgentForge.gitcdAgentForge mvn cleaninstall-DskipTests如果这套「自底向上」的设计对你有帮助欢迎到 GitHub 给 AgentForge 点一个 Star。一、背景与问题引入1.1、场景驱动把内部方法顺手暴露给模型在把 AgentForge 接入内部 CRM、工单系统时我们遇到一个很现实的场景团队已经有CrmService.queryCustomer(...)、TicketService.createTicket(...)这样一批现成的 Java 方法希望几乎零改动地让 Agent 调用它们。由此带来三个具体痛点手工登记为每个方法手写工具描述与 JSON Schema重复且易错参数难绑模型给的是 JSON要手动解析、做类型转换枚举、数组、数值边界……不能引重依赖核心是 Java 8 零第三方库不能用 Jackson / Gson 做反序列化。1.2、问题引导如何让普通 Java 方法零成本变工具问题如何让一个已经写好的 Java 方法被打个注解就变成模型可调用的工具核心要解决三件事声明从方法签名自动生成ToolSpecification名字、描述、参数 Schema绑定把模型返回的argumentsJSON 字符串绑定到方法入参转换把 JSON 里的Long/String等安全地转成方法期望的int/Enum/List/BigDecimal。1.3、设计目标与约束目标说明注解驱动只加Tool/P不改业务逻辑强类型绑定支持枚举、数值边界、BigDecimal / BigInteger、UUID、集合 / Map零依赖复用内置Json不引 JSON 库统一产物是MapToolSpecification, ToolExecutor与 http / mcp 同一张工具表可观测可选的执行日志装饰器二、核心概念讲解2.1、两个注解Tool与PTool(namegetWeather,value查询城市天气)publicStringgetWeather(P(namecity,description城市名)Stringcity){returnweatherApi.query(city);}Tool标记方法为工具name缺省用方法名value为描述P覆盖参数名与描述当 javac 未开-parameters时反射拿到的是arg0靠它兜底。重点参数名称决定模型 JSON 的键若未加P且未开-parameters键会变成arg0所以生产环境务必显式命名。2.2、反射生成 SchemaToolSpecifications.toolSpecificationFrom(method)从方法签名推导方法要素映射到Tool.name/ 方法名ToolSpecification.nameTool.valueToolSpecification.description每个参数名P.name/ 反射名JSON Schemaproperties键参数类型JSON Schematypestring / integer / number / boolean / array / object参数一律 required当前实现固定true2.3、LocalToolExecutor反射调用器它持有对象实例 方法execute时把参数绑定后反射调用arguments(JSON) ──► LocalToolExecutionRequestUtil.argumentsAsMap │ ▼ prepareArguments(originalMethod, map) 每个参数P 名 → 取值 → coerceArgument 类型转换 │ ▼ methodToInvoke.invoke(object, args) 代理场景用原始方法取参数名 │ ▼ 返回值规范化 → String回灌模型2.4、返回值约定重点方法返回类型回给模型的内容void字面量SuccessString原样返回其它类型Json.stringify(result)2.5、异常策略LocalToolExecutor提供两个开关默认均false开关关闭默认打开wrapToolArgumentsExceptions参数错误按原样抛出包装为ToolArgumentsExceptionpropagateToolExecutionExceptions方法内部异常 → 返回异常消息文本抛出ToolExecutionException(cause)注意默认方法抛异常 → 把消息当结果回给模型利于模型自纠需要强失败语义时打开propagateToolExecutionExceptions。三、实现思路3.1、三种方案对比方案一为每个方法手写ToolExecutor优点完全可控。弊端说明大量重复的 JSON 解析与类型转换代码维护成本高。方案二引入 Jackson / Gson 直接反序列化到 DTO优点开发体验好。弊端说明破坏核心零依赖Java 8、无第三方库且与 AgentForge 内置Json体系割裂。方案三注解 反射 自研类型转换本模块采用优点零依赖、注解驱动、参数绑定与转换内聚、代理/日志可插拔。弊端说明需自行维护类型转换矩阵复杂泛型如ListCustomDto需自行兜底。结论采用方案三——复用内置Json用一层可测的类型转换矩阵换取零依赖。3.2、扫描与建表流程LocalToolFactory.buildLocalTools(objects) for each object: 拒绝 Class必须是实例 for each declaredMethod with Tool: spec ToolSpecifications.toolSpecificationFrom(method) 重名 → 抛 IllegalArgumentException(Duplicated definition for tool: x) executor new LocalToolExecutor(object, method) hasLoggingtrue 时再包一层 LoggingToolExecutor 结果放入 MapToolSpecification, ToolExecutor → 交给 ToolService 按 spec.name() 建表3.3、参数绑定流水线参数为空? → 基本类型给默认值(0/false/\0)对象类型给 null 参数非空? → coerceArgument(值, 参数名, 目标Class, 泛型Type)参数名解析优先级P.name()→parameter.getName()。代理兼容AOP 代理方法会丢失参数名故构造器支持(object, originalMethod, methodToInvoke)——用原始方法取参数名用代理方法实际调用。3.4、类型转换矩阵核心coerceArgument按目标类型逐一处理越界 / 非法值直接报错目标类型转换策略Stringargument.toString()枚举Enum.valueOf失败再试toUpperCase()Boolean/boolean仅接受Boolean否则报错Double/Float字符串或数字 → doubleFloat 做边界检查BigDecimalBigDecimal.valueOf(double)Integer/Long/Short/Byte走getBoundedLongValue非小数 边界校验BigInteger非小数 double →BigDecimal.toBigInteger()UUIDUUID.fromStringCollection/Map交给LocalToolArgumentConverter解析 JSON重点所有数值转换都走double 中间态 边界检查避免静默溢出非整数赋给整型会明确报错。3.5、复杂参数与双重编码容错模型有时会把数组 / 对象再编码成字符串传进来例如[1,2,3]或\{...}\。LocalToolArgumentConverter的策略参数本就是对象 → 原样返回是字符串 →Json.parse类型匹配则返回不匹配 →cleanJsonString去外层引号、还原转义后再试仍失败 → 抛IllegalArgumentException。LocalToolExecutionRequestUtil.argumentsAsMap还额外容忍尾逗号、整体被引号包裹的 JSON。3.6、日志装饰LoggingToolExecutor是ToolExecutor的装饰器在执行前后用java.util.logging打印工具名、入参、出参与耗时。buildLocalToolsWithLogging(objects, true)即启用。四、实战代码4.1、定义工具publicclassCrmTools{Tool(namequeryCustomer,value按客户名查询客户信息)publicStringqueryCustomer(P(namename,description客户名称)Stringname,P(namelimit,description返回条数)intlimit){returncrmService.query(name,limit);}}4.2、注册到 ToolServiceMapToolSpecification,ToolExecutortoolsLocalToolFactory.buildLocalToolsWithLogging(Collections.singletonList(newCrmTools()),true);ToolServicetoolServicenewToolService();toolService.tools(tools);ReActAgentagentReActAgent.builder().chatModel(chatModel).toolService(toolService).build();运行输出模拟终端# 1) 注册后模型可见的工具tools[] 摘要[queryCustomer]按客户名查询客户信息 parameters:{name: string(required), limit: integer(required)}# 2) 打开 FINE 日志后一次工具执行LoggingToolExecutor 装饰工具执行请求工具名称: queryCustomer 参数:{name:长路,limit:5}记忆ID: session-1工具执行响应结果:{name:长路,level:VIP}耗时:3毫秒4.3、一次模型调用如何被闭环对应单测LocalToolLoopTest#shouldCloseTheCrmLoopThroughToolService——离线完整走通扫描 → 注册 → 命中 → 绑定 → 反射调用 → 回灌。以 4.1/4.2 的queryCustomer为例逐环节拆解① 扫描与注册build 期MapToolSpecification,ToolExecutortoolsLocalToolFactory.buildLocalTools(Collections.singletonList(newCrmTools()));ToolServicetoolServicenewToolService();toolService.tools(tools);// toolExecutors[queryCustomer] LocalToolExecutor(crmTools, method)ToolSpecifications.toolSpecificationFrom(method)生成ToolSpecification{ namequeryCustomer, parameters{ name:string(required), limit:integer(required) } }LocalToolExecutor绑定对象实例 方法AOP 代理时用原方法取参数名、用代理方法实际调用。② 模型侧 function call{name:queryCustomer,arguments:{\name\:\长路\,\limit\:5}}③ 归一化为请求ToolExecutionRequest{ idcall_1, namequeryCustomer, arguments{\name\:\长路\,\limit\:5} }④ ToolService 定位执行器toolExecutors.get(queryCustomer)→ 命中LocalToolExecutor。⑤ 参数绑定prepareArguments方法参数参数名来源取值coerceArgumentString nameP.name() name长路STRING →长路int limitP.name() limit5Json 解析为数值double 中间态 非小数 边界校验 →(int) 5注意参数缺失时基本类型给默认值int → 0、boolean → false对象类型给null参数名优先取P.name()否则取反射参数名。⑥ 反射调用与返回规范化methodToInvoke.invoke(crmTools, [长路, 5])返回值按约定处理String原样、void→Success、其它 →Json.stringify。⑦ 回灌模型工具返回值作为结果回灌 → 模型据此回答。运行输出模拟终端[user]查一下客户长路的信息[think]调用 queryCustomer[act]queryCustomer{name:长路,limit:5}[reflect]CrmTools.queryCustomer(长路,5)→ customer:长路,limit:5[think]已查询到客户长路。[final]已查询到客户长路。五、验证测试5.1、单元测试概览LocalToolFactoryTest覆盖工厂扫描、日志装饰、参数工具容错LocalToolExecutorTest覆盖参数绑定与类型转换矩阵全部离线执行。5.2、测试清单当前实现全部通过用例覆盖点shouldBuildToolsFromAnnotatedObject扫描Tool构建映射shouldDecorateExecutorWithLogging日志装饰器shouldReturnEmptyMapForNullOrEmptyInput空输入边界shouldBindStringArgumentUsingPAnnotationP参数名绑定shouldCoerceNumericArguments数值类型转换shouldUsePrimitiveDefaultWhenArgumentMissing基本类型默认值shouldCoerceEnumNameIgnoringCase枚举忽略大小写shouldConvertJsonStringIntoListCollection字符串 → ListshouldPassThroughNativeList原生 List 透传shouldConvertJsonStringIntoMap字符串 → MapshouldReturnSuccessForVoidMethodvoid →SuccessshouldSerializeNonStringReturnValueAsJson非 String 返回 → JSONshouldReturnThrownMessageByDefault默认异常降级文本shouldThrowWrappedExecutionExceptionWhenPropagating传播异常开关shouldWrapArgumentsExceptionWhenConfigured参数异常包装开关shouldTolerateTrailingCommaWhenParsingArguments尾逗号容错shouldConvertDoubleEncodedJsonString双重编码 JSON 容错LocalToolLoopTest#shouldCloseTheCrmLoopThroughToolService端到端闭环Tool扫描 → ToolService → function call → 参数绑定 → 反射调用 → 回灌对齐 4.35.3、边界与风险参数名依赖未开-parameters且未用P时模型看到arg0务必显式命名。required 固定为 true当前所有参数都必填可选参数需上层兜底或默认值。泛型擦除ListCustomDto只能解析为Map/List结构无法自动实例化为具体类型。布尔严格Boolean字段只接受布尔值字符串true不做隐式转换。六、总结与展望本质Tool注解 反射 类型转换把普通 Java 方法变成模型可调用的工具。核心ToolSpecifications生成 SchemaLocalToolExecutor绑定参数并反射调用。零依赖复用内置Json与 Java 8 / 零第三方库约束一致。工程性异常策略可配、日志可装饰、代理可兼容。展望可选参数required 可控、泛型 DTO 反序列化、SpEL / 表达式默认值。参考资料[1]. Java 反射教程Oracle[2]. JSON Schema 官方站点[3]. Java 注解教程Oracle[4]. 相关内部文档[HTTP 工具模块核心设计原理](…/http/适配HTTP协议转换FunctionCall协议01、AgentForge HTTP工具模块核心设计原理)、MCP 框架调研与 AgentForge 接入实现整理者:长路 创建时间:2026.10.5 更新时间:2026.10.5