前言
在进入正文之前,先交代一下这些文章的来龙去脉。
AgentForge是一个面向 Java 开发者、从LLM 最底层能力开始构建的开源 Agent 框架。它不从高度封装的 Agent API 起步,而是先建立稳定、统一、可扩展的模型抽象,再逐层向上锻造 Tool、Memory、Middleware、Reasoning 与 Agent Runtime 等能力。
AgentForge = Agent + Forge:Agent代表能理解目标、进行推理、调用工具并完成任务的智能体,Forge则强调把原始智能持续加工、塑形、强化,最终锻造成真正可用的产品。
本系列《AgentForge 核心模块设计原理》沿着这条自底向上的路径,逐个模块拆解它的设计原理与实现细节。本文聚焦AgentForge 适配 OpenAI 的接入实践(统一类型与 Chat Completions 的双向映射),对应模块agentforge-model-openai。
- 开源仓库(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、场景驱动:一套 Core,适配多家模型
AgentForge 的 Agent Runtime 只依赖
ChatModel/StreamingChatModel接口。我们希望"换模型"只换一个 Provider 依赖,而不是改 Agent 代码。
于是 OpenAI Provider 的职责被限定为协议翻译层:
Agent Runtime │ ChatRequest / ChatResponse(Provider 无关) ▼ OpenAiChatModel / OpenAiStreamingChatModel │ Chat Completions wire ▼ OpenAI / OpenAI-compatible 服务1.2、问题引导:统一类型如何落到 OpenAI wire?
问题:AgentForge 的
ChatMessage、ChatRequestParameters、ChatResponse分别怎么落到 OpenAI 的messages/ 请求字段 / 响应字段?工具调用如何双向映射?流式如何聚合?
本文逐项给出映射表与实现位置。
1.3、实现边界
- Wire API:
POST {baseUrl}/chat/completions(默认https://api.openai.com/v1); - release_1.x 以 Chat Completions 为第一版统一协议,Responses API 采用"并存 Adapter"策略(见第六章)。
1.4、Builder 参数
| Builder 参数 | 默认值 | 说明 |
|---|---|---|
baseUrl | https://api.openai.com/v1 | 也支持 OpenAI-compatible 服务 |
apiKey | null | 非空时发送Authorization: Bearer ... |
modelName | null | 请求前必须有值 |
temperature | null | 非空才发送 |
maxTokens | null | 映射max_tokens |
topP | null | 映射top_p |
stopSequences | null | 映射stop |
customParameter | 空 | 透传 Provider 扩展顶层字段 |
customHeader | 空 | 追加自定义 HTTP Header |
httpTransport | JdkHttpTransport | HTTP SPI |
connectTimeoutMillis | 10000 | 连接超时 |
readTimeoutMillis | 60000 | 读取超时 |
重点:
OpenAiStreamingChatModel复用同一套配置,并强制写入stream=true与stream_options.include_usage=true。
二、核心概念
2.1、统一入参
ChatRequest{List<ChatMessage>messages;ChatRequestParametersparameters;}ChatRequestParameters{StringmodelName();Doubletemperature();IntegermaxTokens();DoubletopP();List<String>stopSequences();Map<String,Object>customParameters();}2.2、统一出参
ChatResponse{AiMessageaiMessage;// text + toolExecutionRequestsTokenUsagetokenUsage;FinishReasonfinishReason;Map<String,Object>metadata;}重点:Provider 的职责就是把 wire 字段无损地落到这两个统一对象上。
三、实现思路与映射
3.1、HTTP Headers
Content-Type: application/json Accept: application/json Authorization: Bearer ${apiKey} # apiKey 非空时之后追加customHeaders;企业内部网关可用customHeader(...)增加租户、路由、trace 等。
3.2、消息映射
| AgentForge | OpenAI role | content / 关键字段 |
|---|---|---|
SystemMessage | system | message.text() |
UserMessage(单文本) | user | message.text() |
UserMessage(多Content) | user | content[],逐条TextContent→{"type":"text","text":...} |
AiMessage(纯文本) | assistant | message.text() |
AiMessage(含工具调用) | assistant | content(可为null)+tool_calls[] |
ToolExecutionResultMessage | tool | tool_call_id = message.id(),content = message.text() |
工具调用 + 结果回填的 wire 形态:
{"messages":[{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"getWeather","arguments":"{\"city\":\"hangzhou\"}"}}]},{"role":"tool","tool_call_id":"call_1","content":"{\"temperature\":22}"}]}注意:
CustomMessage当前未实现 wire mapping,遇到会抛IllegalArgumentException。
3.3、参数映射
| AgentForge 参数 | Chat Completions 字段 | 当前行为 |
|---|---|---|
modelName | model | 必填;为空本地失败 |
temperature | temperature | 非空才发送 |
maxTokens | max_tokens | 非空才发送 |
topP | top_p | 非空才发送 |
stopSequences | stop | 非空才发送 |
tools | tools[] | {"type":"function","function":{name,description,parameters,strict}} |
toolChoice | tool_choice | AUTO→"auto"、NONE→"none"、REQUIRED→"required"、SPECIFIC→{"type":"function","function":{"name":X}} |
customParameters | 顶层原样写入 | 通用字段写入后覆盖同名 custom 字段 |
构建顺序(标准字段拥有最终优先级):
1. payload.putAll(customParameters) 2. 写入 model / messages 3. 写入 temperature / max_tokens / top_p / stop 4. 写入 tools / tool_choice重点:即使
customParameters写了另一个model,最终仍会被modelName覆盖。
3.4、响应映射
| OpenAI 字段 | AgentForge 字段 |
|---|---|
choices[0].message.content | ChatResponse.aiMessage().text() |
choices[0].message.tool_calls[] | ChatResponse.aiMessage().toolExecutionRequests() |
usage.prompt_tokens | TokenUsage.inputTokens() |
usage.completion_tokens | TokenUsage.outputTokens() |
usage.total_tokens | TokenUsage.totalTokens() |
choices[0].finish_reason | ChatResponse.finishReason() |
id/model/created | metadata["id"] / ["model"] / ["created"] |
tool_calls[]映射:id → ToolExecutionRequest.id、function.name → name、function.arguments → arguments(保留原始 JSON 字符串,不重新格式化)。
注意:当
tool_calls非空且content为空时,AiMessage.text()返回null;两者同时存在时,两者都会保留。
3.5、finish_reason 映射
| OpenAI | AgentForgeFinishReason |
|---|---|
stop | STOP |
length | LENGTH |
tool_calls | TOOL_EXECUTION |
function_call | TOOL_EXECUTION |
content_filter | CONTENT_FILTER |
| 其他非空 | OTHER |
null | null |
3.6、响应结构异常
以下情况直接抛ModelException(不静默返回空):
choices 不存在 / 为空 / choices[0].message 不存在3.7、流式实现
请求:同 Endpoint,强制stream=true+stream_options.include_usage=true。
SSE 解析:忽略空行、:注释、非data:行;每个data:chunk 读取choices[0].delta.content、delta.tool_calls[]、finish_reason。
delta.content→ 立即handler.onPartialResponse(...),同时本地StringBuilder聚合;delta.tool_calls[]→按index分桶累加(规则见协议篇 4.1),不回调半成品;- 结束 →
handler.onCompleteResponse(response)。
最终响应:
ChatResponse.builder().aiMessage(AiMessage.from(fullText,toolExecutionRequests)).finishReason(finishReason).tokenUsage(tokenUsage).metadata(metadata).build();重点:最终 usage chunk(
choices=[])由"先读 usage、再判断 choices 是否为空"处理;流中断时最终 usage 可能缺失,ChatResponse.tokenUsage()不保证一定存在。
四、实战代码
4.1、非流式
OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("https://api.openai.com/v1").apiKey(System.getenv("OPENAI_API_KEY")).modelName("your-model").temperature(0.2).maxTokens(1024).build();ChatRequestrequest=ChatRequest.builder().message(SystemMessage.from("You are a concise Java assistant.")).message(UserMessage.from("What is CAS?")).build();ChatResponseresponse=model.chat(request);System.out.println(response.aiMessage().text());运行输出(模拟终端)
$curl-shttps://api.openai.com/v1/chat/completions-H"Authorization: Bearer$OPENAI_API_KEY"\-d'{"model":"your-model","messages":[{"role":"system","content":"..."},{"role":"user","content":"What is CAS?"}]}'{"choices":[{"index":0,"message":{"role":"assistant","content":"CAS means Compare-And-Swap."},"finish_reason":"stop"}],"usage":{"prompt_tokens":20,"completion_tokens":10,"total_tokens":30}}4.2、流式
OpenAiStreamingChatModelstreaming=OpenAiStreamingChatModel.builder().baseUrl("https://api.openai.com/v1").apiKey(System.getenv("OPENAI_API_KEY")).modelName("your-model").build();streaming.chat(request,newStreamingChatResponseHandler(){@OverridepublicvoidonPartialResponse(Stringpartial){System.out.print(partial);}@OverridepublicvoidonCompleteResponse(ChatResponseresponse){System.out.println();}@OverridepublicvoidonError(Throwableerror){error.printStackTrace();}});运行输出(模拟终端)
CAS means Compare-And-Swap.4.3、OpenAI-compatible 服务
OpenAiChatModelmodel=OpenAiChatModel.builder().baseUrl("https://example.com/v1").apiKey("...").modelName("provider-model").build();五、兼容性、错误与边界
5.1、HTTP 错误
非 2xx →new ModelException("OpenAI request failed with HTTP " + statusCode, statusCode, responseBody);网络层 IOException →ModelException("OpenAI request failed", cause)。流式非 2xx 会累积 body 并通过handler.onError(...)返回。
5.2、协议差距(release_1.x 未映射)
developerrole、图片 / 音频多模态content、流式onPartialToolCall、refusal、logprobs、structured outputs、audio、provider reasoning(AiMessage.thinking()字段位已留未接线)、多choices。
customParameters可临时透传请求字段;但若返回结构需要框架理解,仍须正式扩展 Core / Provider 类型。
六、总结与演进
已落地:toolrole 的tool_call_id请求映射、tools/tool_choice(含SPECIFIC)、tool_calls非流式解析与流式按index聚合、UserMessage多Content→content[]。
OpenAI 官方建议新应用优先 Responses API,因此后续采用并存 Adapter:OpenAiChatModel → /chat/completions、OpenAiStreamingChatModel → /chat/completions + SSE,未来新增OpenAiResponsesModel → /responses。上层仍只依赖 Core 接口,协议迁移不侵入 Agent Runtime。
参考资料
[1]. OpenAI Chat Completions API(官方参考)
[2]. OpenAI 文本生成指南
[3]. 相关内部文档:OpenAI 底层协议快速理解、ChatModel 核心协议层设计
整理者:长路 创建时间:2026.10.5 更新时间:2026.10.5