OpenAI协议02、AgentForge 适配OpenAI接入核心实践

coverImg

前言

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

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
git clone https://github.com/changluya/AgentForge.git
cd AgentForge
mvn clean install -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;
    ChatRequestParameters parameters;
}

ChatRequestParameters {
    String modelName();
    Double temperature();
    Integer maxTokens();
    Double topP();
    List<String> stopSequences();
    Map<String, Object> customParameters();
}

2.2、统一出参

ChatResponse {
    AiMessage aiMessage;          // text + toolExecutionRequests
    TokenUsage tokenUsage;
    FinishReason finishReason;
    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 AgentForge FinishReason
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、非流式

OpenAiChatModel model =
        OpenAiChatModel.builder()
                .baseUrl("https://api.openai.com/v1")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName("your-model")
                .temperature(0.2)
                .maxTokens(1024)
                .build();

ChatRequest request =
        ChatRequest.builder()
                .message(SystemMessage.from("You are a concise Java assistant."))
                .message(UserMessage.from("What is CAS?"))
                .build();

ChatResponse response = model.chat(request);
System.out.println(response.aiMessage().text());

运行输出(模拟终端)

$ curl -s https://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、流式

OpenAiStreamingChatModel streaming =
        OpenAiStreamingChatModel.builder()
                .baseUrl("https://api.openai.com/v1")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                .modelName("your-model")
                .build();

streaming.chat(request, new StreamingChatResponseHandler() {
    @Override public void onPartialResponse(String partial) { System.out.print(partial); }
    @Override public void onCompleteResponse(ChatResponse response) { System.out.println(); }
    @Override public void onError(Throwable error) { error.printStackTrace(); }
});

运行输出(模拟终端)

CAS means Compare-And-Swap.

4.3、OpenAI-compatible 服务

OpenAiChatModel model =
        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 未映射)

developer role、图片 / 音频多模态 content、流式 onPartialToolCall、refusal、logprobs、structured outputs、audio、provider reasoning(AiMessage.thinking() 字段位已留未接线)、多 choices。

customParameters 可临时透传请求字段;但若返回结构需要框架理解,仍须正式扩展 Core / Provider 类型。


六、总结与演进

已落地:tool role 的 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 文本生成指南


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

评论区请在客户端页面查看