☰
AgentForge HTTP 工具模块核心设计原理
2026/10/8 13:19:11 网站建设 项目流程

前言

在进入正文之前,先交代一下这些文章的来龙去脉。

AgentForge是一个面向 Java 开发者、从LLM 最底层能力开始构建的开源 Agent 框架。它不从高度封装的 Agent API 起步,而是先建立稳定、统一、可扩展的模型抽象,再逐层向上锻造 Tool、Memory、Middleware、Reasoning 与 Agent Runtime 等能力。

AgentForge = Agent + Forge:Agent代表能理解目标、进行推理、调用工具并完成任务的智能体,Forge则强调把原始智能持续加工、塑形、强化,最终锻造成真正可用的产品。

本系列《AgentForge 核心模块设计原理》沿着这条自底向上的路径,逐个模块拆解它的设计原理与实现细节。本文聚焦AgentForge HTTP 工具模块的核心设计——如何把一组 REST 接口声明式映射为统一的ToolSpecification与ToolExecutor,让模型像调用本地方法一样调用 HTTP 接口。

  • 开源仓库(GitHub):https://github.com/changluya/AgentForge
  • 项目文档站:https://changluya.github.io/AgentForge/
  • Gitee 镜像:https://gitee.com/changluJava/agent-forge
  • 开源协议:MIT
gitclone https://github.com/changluya/AgentForge.gitcdAgentForge mvn cleaninstall-DskipTests

如果这套「自底向上」的设计对你有帮助,欢迎到 GitHub 给 AgentForge 点一个 Star。


一、背景与问题引入

1.1、场景驱动:接入钉钉机器人时,我们卡在了哪

在为 AgentForge 做外部能力接入时,我们遇到一个很现实的场景:

团队希望 Agent 能发钉钉消息、查 CRM 客户、拉工单列表。这些能力既没有 MCP Server,也不值得为每个接口写一个 Java@Tool方法——它们只是普通的 REST API。

由此带来三个具体痛点:

  1. 接口多:一个 SaaS 动辄几十个接口,逐个写@Tool方法维护成本极高;
  2. 模型无从下手:把"一个通用 HTTP 工具"丢给模型,它会自己编 URL、乱加参数,既不可控也不安全;
  3. 核心零依赖:AgentForge 核心是 Java 8 + 零第三方库,不能为接 HTTP 引入 OkHttp / Retrofit / Jackson。

1.2、问题引导:如何让模型"说人话"就能调 HTTP?

问题:如何让模型把一句自然语言,变成一次参数正确、地址可控的 HTTP 调用?

答案不是"让模型自己拼 HTTP",而是:

  • 由开发者预先声明每个接口(地址、方法、参数)→ 生成ToolSpecification;
  • 模型只在已声明的工具里选择、只填声明的参数;
  • 运行时由执行器把参数"填"进 URL / Header / Body 并发起请求。

这正是本模块要做的事。


1.3、设计目标与约束

目标说明
声明式用HttpPlugin描述一组接口,不写胶水代码
零依赖复用内置Json与HttpTransport(JdkHttpTransport)
可控模型只能调用已声明工具,URL 由插件固定
统一产物是Map<ToolSpecification, ToolExecutor>,与 local / mcp 同一张工具表
可扩展IHttpPluginSPI + curl 互转,降低接入成本

二、核心概念讲解

2.1、三个核心抽象

HttpPlugin # 一个服务:baseUrl + 公共请求头 + 一组方法 └── HttpPluginMethod # 一个工具:name / description / httpMethod / uri / parameters └── HttpToolParameter # 一个参数:参数名 + 映射名 + 使用位置 + 数据类型 + 默认值 + 是否必填

2.2、参数的四维模型(重点)

一个参数被四个维度描述,正是它让"声明"到"真实请求"的映射成为可能:

维度枚举作用
使用位置ParameterUseType:QUERY / BODY / PATH / HEADER决定参数最终落到 URL、请求体还是请求头
数据类型ParameterType:STRING / INTEGER / NUMBER / BOOLEAN / ARRAY / OBJECT决定 JSON Schema 类型与运行时类型转换
映射名mappedName模型参数名 ↔ 真实接口参数名(内外解耦,可改名)
约束defaultValue/required缺省填充与必填校验

重点:模型看到的是methodParamName(工具参数名),真正发给接口的是mappedName——这让"给模型看的名字"和"接口要的名字"可以不同。


2.3、与 Agent 工具抽象的关系

HTTP 模块AgentForge 契约
HttpPluginMethod+ 参数ToolSpecification(parameters→ JSON Schema)
HttpToolExecutorToolExecutor(execute/executeWithResult)
HttpToolFactory.buildHttpTools产出Map<ToolSpecification, ToolExecutor>,交给ToolService

2.4、为什么还要支持 curl

开发者手里最常见的"接口样本"就是一段 curl。支持curl → HttpPlugin,能把"复制粘贴一段 curl"直接变成可注入 Agent 的工具;反向HttpPlugin → curl则方便调试与文档化。


三、实现思路

3.1、三种方案对比

方案一:每个接口写一个@Tool方法

  • 优点:类型安全、可复用 local 模式。
  • 弊端说明:接口多则代码膨胀;改一个字段要改代码;维护成本高。

方案二:一个通用 HTTP 工具(模型传 url / method / body)

  • 优点:实现最简单。
  • 弊端说明:模型可任意构造 URL / 参数,不可控、不安全,且易编造接口;难以做鉴权与白名单。

方案三:声明式HttpPlugin映射(本模块采用)

  • 优点:地址固定、参数受控、零依赖、可批量描述、可与 local / mcp 统一进ToolService。
  • 弊端说明:需定义插件结构;复杂鉴权(OAuth 刷新等)仍需宿主层补。

结论:采用方案三——用声明换可控,用映射换零依赖。


3.2、核心流程:List → Spec + Executor

HttpToolFactory.buildHttpTools(httpPlugin)对每个HttpPluginMethod生成一对:

HttpPluginMethod ├─ methodName / description ───────────► ToolSpecification.name / description ├─ parameters[].toJsonSchema() ────────► ToolSpecification.parameters(JSON Schema) └─ (baseUrl+uri, httpMethod, headers, 参数配置) ─► new HttpToolExecutor(...)

重点:与 MCP 的serverAlias__tool不同,HTTP 工具不加前缀——工具名直接用methodName,天然由开发者控制唯一性。


3.3、参数处理流水线(核心)

HttpToolExecutor.execute内部按固定顺序处理:

arguments(JSON) ──► argumentsAsMap │ ▼ processArguments ① 取实参 / 套默认值 / 校验必填 / 按 dataType 转换 │ ├─ buildUrlWithPathParams ② PATH 参数替换 {mappedName} ├─ addHeaderParameters ③ HEADER 参数写入请求头 ├─ shouldUseJsonBody ? │ ├─ true → BODY 参数 → JSON Body(Content-Type: application/json) │ └─ false → appendQueryParameters(QUERY 参数拼到 URL) ▼ HttpRequest(url, method, headers, body) ──► HttpTransport.execute
  • 必填缺失:抛IllegalArgumentException("缺少必填参数 'x'");
  • 类型转换:convertValueToCorrectType支持 INTEGER / NUMBER / BOOLEAN / ARRAY / OBJECT / STRING,并对 JSON 字符串做容错解析。

3.4、请求体的"智能判断"

GET / DELETE → 强制用 Query(即使配了 BODY 也不发 Body) POST / PUT / PATCH → 只有当存在 BODY 参数(已传或有默认值)时才发 JSON Body

注意:GET 即使配了 BODY 类型参数也不会发送请求体(由shouldNotSendJsonBodyForGetEvenWithBodyConfig用例保证)。


3.5、重试与容错

  • 仅对GET / DELETE的connection reset做一次重试(幂等语义下安全);
  • 其余异常在execute中兜底为文本"HTTP请求失败: {message}"。

弊端说明:当前execute把错误降级为文本,isError仍为false;需要结构化错误时可在上层用ToolExecutionResult包装(对比:MCP 执行器已做isError无损映射)。


3.6、curl 解析与互转

方向入口说明
curl → 插件CurlParser.parse+CurlToHttpPluginConverter.convert拆出 url / method / headers / query / body,生成参数
插件 → curlHttpPluginToCurlConverter.convert(plugin[, methodName])反向生成可复现命令;多方法插件需指定方法名

注意:一个插件含多个方法时,convert(plugin)会拒绝,必须显式指定methodName。


3.7、能力矩阵:请求方法与参数格式

① 支持的 HTTP 方法(HttpPluginEnums.HttpMethod)

方法Code请求体幂等重试说明
GET1否(强制 Query)✅ 连接重置重试 1 次查询
POST2有 BODY 参数时发 JSON❌创建
PUT3有 BODY 参数时发 JSON❌全量更新
DELETE4否(强制 Query)✅ 连接重置重试 1 次删除(幂等)
PATCH5有 BODY 参数时发 JSON❌局部更新

重点:GET / DELETE 配置为 BODY 的参数不会进请求体,会被忽略(见shouldNotSendJsonBodyForGetEvenWithBodyConfig)。


② 参数使用位置(ParameterUseType)

位置Code落点生成规则
QUERY1URL 查询串encode(mappedName)=encode(value)
BODY2JSON 请求体{mappedName: value}→Json.stringify
PATH3URL 路径占位符替换{mappedName}
HEADER4请求头mappedName: value

③ 参数数据类型(ParameterType)

类型CodeJSON Schema运行时转换
STRING1stringvalue.toString()
INTEGER2integerInteger.parseInt/Number.intValue()
NUMBER3numberDouble.parseDouble/Number.doubleValue()
BOOLEAN4boolean"true"/"1"/"yes"→ true,否则 false
ARRAY5array(items: string)List 直用;JSON 字符串解析;否则包成单元素数组
OBJECT6objectMap 直用;JSON 字符串解析;否则空 Map

④ 必填与默认值(RequiredStatus+defaultValue)

项Code行为
REQUIRED1实参与默认值都缺失 → 抛IllegalArgumentException("缺少必填参数 'x'")
NOT_REQUIRED0缺失即不参与本次请求

重点:取值优先级为实参 >defaultValue> 必填校验;methodParamName是模型侧名字,mappedName才是发给接口的名字。


四、实战代码

4.1、最小接入

HttpPluginplugin=HttpPlugin.builder().baseUrl("https://api.example.com").staticHeaders(Collections.singletonMap("Authorization","Bearer "+token)).pluginMethods(Collections.singletonList(HttpPluginMethod.builder().methodName("getWeather").methodDescription("查询城市天气").httpMethodType(HttpPluginEnums.HttpMethod.GET.getValue()).uri("/v1/weather").parameters(Collections.singletonList(HttpToolParameter.builder().methodParamName("city").methodParamDescription("城市名").mappedName("q").useTypeValue(HttpPluginEnums.ParameterUseType.QUERY.getValue()).dataTypeValue(HttpPluginEnums.ParameterType.STRING.getValue()).required(HttpPluginEnums.RequiredStatus.REQUIRED.getCode()).build())).build())).build();ToolServicetoolService=newToolService();toolService.tools(HttpToolFactory.buildHttpTools(plugin));ReActAgentagent=ReActAgent.builder().chatModel(chatModel).toolService(toolService).build();

运行输出(模拟终端)

# 1) 注册后模型可见的工具(tools[] 摘要)[getWeather]查询城市天气 parameters:{city: string(required)}# 模型参数名 city → 实际接口参数名 ?q# 2) 执行 getWeather(city=杭州) 的 HTTP 往返(java.util.logging,INFO)===HTTP 请求详情===URL: https://api.example.com/v1/weather?q=%E6%9D%AD%E5%B7%9E 方法: GET 请求头:{Authorization=Bearer sk-***}QUERY 参数:{q=杭州}========================HTTP 响应详情===状态码:200耗时:143毫秒 响应体:{"city":"杭州","temp":26,"text":"晴"}=====================

4.2、通过IHttpPlugin扩展

publicclassDingTalkPluginimplementsIHttpPlugin{privatePropertiesprops;@Overridepublicvoidinit(Propertiesprops){this.props=props;}@OverridepublicStringgetPluginName(){return"dingtalk";}@OverridepublicvoiddoCheckProps(){if(props.getProperty("token")==null){thrownewIllegalArgumentException("missing token");}}@OverridepublicHttpPlugingetHttpPlugin(){/* 依据 props 构建 */returnplugin;}}toolService.tools(newDingTalkPlugin().buildHttpTools());

4.3、curl 一键转换

CurlParseResultcurl=newCurlParser().parse("curl -X POST 'https://api.example.com/v1/tickets' "+"-H 'Content-Type: application/json' "+"-d '{\"title\":\"bug\"}'");HttpPluginplugin=newCurlToHttpPluginConverter().convert(curl);toolService.tools(HttpToolFactory.buildHttpTools(plugin));

运行输出(模拟终端)

# CurlParser.parse(...) 解析出的关键字段method=POST url=https://api.example.com/v1/tickets headers={Content-Type=application/json}data={"title":"bug"}# CurlToHttpPluginConverter.convert(...) 生成的 HttpPluginbaseUrl=https://api.example.com uri=/v1/tickets method=POST params=[title: string(BODY:title, required)]# buildHttpTools(...) → 模型可见工具(方法名由 curl 自动生成 executePOST)[executePOST](由curl生成) body:{"title":"..."}

4.4、一次模型调用如何被闭环

对应单测:HttpToolLoopTest#shouldCloseTheWeatherLoopThroughToolService——注入RecordingHttpTransport,离线完整走通"声明 → 注册 → 命中 → 组装 → 发请求 → 回灌"。

以 4.1 的getWeather为例,逐环节拆解:

① 声明与注册(build 期)

Map<ToolSpecification,ToolExecutor>tools=HttpToolFactory.buildHttpTools(weatherPlugin(),transport);// transport 可注入(测试用假实现)ToolServicetoolService=newToolService();toolService.tools(tools);// toolExecutors["getWeather"] = executor
  • ToolSpecification.name = "getWeather",parameters = { city: string(required) };
  • HttpToolExecutor(baseUrl + "/v1/weather", GET, {Authorization=Bearer token}, {city → (QUERY, STRING, required)})。

② 模型侧 function call

{"name":"getWeather","arguments":"{\"city\":\"杭州\"}"}

arguments是JSON 字符串,不是对象。

③ 归一化为请求

ToolExecutionRequest{ id="call_1", name="getWeather", arguments="{\"city\":\"杭州\"}" }

④ ToolService 定位执行器

toolExecutors.get("getWeather")→ 命中HttpToolExecutor(HTTP 工具不加前缀,名字即methodName)。

⑤ 参数处理(processArguments)

步骤输入结果
argumentsAsMap"{\"city\":\"杭州\"}"{city: 杭州}
取实参key =city杭州
必填校验REQUIRED通过
类型转换STRING"杭州"
位置判定QUERY,映射名q待拼到 URL

⑥ 组装HttpRequest

  • PATH:无;HEADER:无(静态Authorization已在staticHeaders);
  • shouldUseJsonBody:GET →false(不发请求体);
  • appendQueryParameters→?q=%E6%9D%AD%E5%B7%9E;
  • 最终:GET https://api.example.com/v1/weather?q=%E6%9D%AD%E5%B7%9E,body=""。

⑦ 发起并取回响应

JdkHttpTransport.execute(request)→200,响应体{"city":"杭州","temp":26,"text":"晴"};HttpToolExecutor.execute直接返回响应体文本。

⑧ 回灌模型

响应体文本作为工具结果回灌 → 模型据此给出最终答复。

运行输出(模拟终端)

[user]查一下杭州天气[think]需要调用工具 getWeather[act]getWeather{"city":"杭州"}[http]GET https://api.example.com/v1/weather?q=%E6%9D%AD%E5%B7%9E →200{"city":"杭州","temp":26,"text":"晴"}[think]杭州当前26℃,天气晴。[final]杭州当前26℃,天气晴。

五、验证测试

5.1、单元测试概览

用RecordingHttpTransport记录收到的HttpRequest,断言 URL / 请求头 / 请求体,无需真实网络。

5.2、测试清单(当前实现,全部通过)

用例覆盖点
shouldSendGetQueryParametersQUERY 参数拼接
shouldSubstitutePathParametersPATH 占位符替换
shouldSendJsonBodyForPostBODY → JSON Body
shouldConvertArrayAndObjectBodyValuesARRAY / OBJECT 类型转换
shouldParseJsonEncodedArrayString字符串数组容错
shouldAddStaticAndDynamicHeaders静态 + HEADER 参数
shouldApplyDefaultValueWhenArgumentMissing默认值
shouldThrowWhenRequiredArgumentMissing必填校验
shouldRetryOnceOnConnectionResetForGet幂等重试
shouldReturnFailureTextWhenPostFails失败降级文本
shouldNotSendJsonBodyForGetEvenWithBodyConfigGET 不发 Body
HttpToolLoopTest#shouldCloseTheWeatherLoopThroughToolService端到端闭环:插件构建 → ToolService → function call → GET 请求 → 回灌(对齐 4.4)
HttpToolFactoryTestSpec / Executor 构建、IHttpPlugin、null 拒绝
CurlParserConverterTestcurl 解析、双向转换、多方法拒绝

5.3、边界与风险

  • 错误语义:execute把异常降级为文本,isError=false,需要结构化错误要自行包装。
  • 鉴权复杂度:静态头 / 参数够用;OAuth 刷新、签名等需宿主层实现。
  • 超时:默认连接 10 分钟、读取 15 分钟(构造时可调),偏宽松,按场景收紧。

六、总结与展望

  1. 本质:把"一组 HTTP 接口"声明式映射成模型可调用的ToolSpecification+ToolExecutor。
  2. 核心:四维参数模型(位置 × 类型 × 映射名 × 约束)+ 固定顺序的参数流水线。
  3. 零依赖:复用内置Json与JdkHttpTransport,与 Java 8 / 零依赖约束一致。
  4. 统一:产物与其他模式一样是Map<ToolSpecification, ToolExecutor>,可同表共存。
  5. 展望:结构化错误(isError)、OpenAPI 导入、更细的超时 / 重试策略。

参考资料

[1]. MDN HTTP 请求方法

[2]. JSON Schema 官方站点

[3]. curl 命令手册

[4]. RFC 7231:HTTP/1.1 语义与方法

[5]. OpenAPI 规范

[6]. 相关内部文档:[Local 工具模块核心设计原理](…/local/Java代码转换FunctionCall协议01、AgentForge Local工具模块核心设计原理)、MCP 框架调研与 AgentForge 接入实现、ChatModel 核心协议层设计


整理者:长路 创建时间:2026.10.5 更新时间:2026.10.5

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询