请求与返回约定
这一页统一解释:在 AI4J 里,请求应该怎么构造,返回应该怎么读,哪些字段是 SDK 本地语义,哪些字段才会真正进入 provider payload。
很多接入问题看起来像 provider 不稳定,实际上只是没有建立统一的请求/返回约定。
1. AI4J 的基本策略是什么
AI4J 当前在模型访问层采用的是一种很明确的策略:
- 用统一请求对象承载主语义
- 用本地辅助字段承载工具注册和运行时上下文
- 在 provider service 层显式构造最终 payload
- 用 listener 或 response entity 聚合返回结果
这套策略在两条主线上都能看到:
ChatCompletionResponseRequest
2. 哪些字段是“本地注册语义”,不是 provider 原生字段
以 ChatCompletion 为例,下面这些字段在 AI4J 中很重要,但并不意味着它们会原样发送给 provider:
functionsmcpServicesbuiltInToolContextstreamExecutionpassThroughToolCalls
对应到 ResponseRequest,也有类似情况:
functionsmcpServicesstreamExecution
这些字段的作用是:
- 帮 SDK 在本地先解析工具和执行策略
- 而不是直接成为 provider JSON payload
如果把这层边界搞混,业务层就很容易误以为“请求对象里写了就一定已经发给模型了”。
3. 最终 payload 是在哪里确定的
Chat
OpenAiChatService 发送前会:
- 先用
ToolUtil.getAllTools(...)展开functions/mcpServices - 再把结果写进
chatCompletion.tools - 视情况清空
parallelToolCalls - 最后序列化整个
ChatCompletion
Responses
OpenAiResponsesService 则更显式:
- 先
ResponseRequestToolResolver.resolve(request) - 再调用
buildOpenAiPayload(request) - 只把允许字段放进最终
payload - 再从
extraBody中补允许的扩展字段
这说明 AI4J 并不是“请求对象怎么长,provider 就收什么”,而是在 service 层有一层正式的投影。
4. extraBody 的角色到底是什么
ChatCompletion 和 ResponseRequest 都提供了 extraBody。
它的定位不是让业务层随意绕过 SDK,而是:
- 在不破坏主语义建模的前提下
- 给 provider 特定扩展字段留一个正式出口
尤其在 OpenAiResponsesService 中,extraBody 还会经过允许字段过滤后才进入 payload。
所以正确用法应该是:
- 主语义优先走正式字段
- provider 特殊扩展才放
extraBody
而不是把所有东西都一股脑堆进 extraBody。
// 主语义走正式字段
ChatCompletion chat = ChatCompletion.builder()
.model("gpt-4o-mini")
.message(ChatMessage.withUser("你好"))
.reasoningEffort("medium") // 正式字段,跨 provider 统一
.build();
// 只有 provider 特有的、SDK 没建模的扩展,才走 extraBody
ChatCompletion chat2 = ChatCompletion.builder()
.model("deepseek-chat")
.message(ChatMessage.withUser("你好"))
.extraBody("thinking", mapOf("type", "enabled")) // DeepSeek 专属
.build();
extraBody 在 ChatCompletion 中会通过 @JsonAnyGetter 展开到 JSON 顶层;同名时 extraBody 的值会覆盖正式字段,所以用它前先确认 SDK 是否已有对应字段。
5. 返回值读取为什么也要有统一约定
如果没有统一约定,最常见的坏结果是:
- 不同调用点各自解析 provider 原始 JSON
- 同步和流式结果混着读
Chat和Responses返回对象被业务层当成同一种结构
AI4J 当前给出的推荐读取方式其实很清晰:
Chat 非流式
优先围绕:
ChatCompletionResponseChoiceChatMessageUsage
ChatCompletionResponse resp = chatService.chatCompletion(req);
String text = resp.getChoices().get(0).getMessage().getContent().getText();
long total = resp.getUsage().getTotalTokens();
Chat 流式
优先围绕:
SseListener.outputreasoningOutputtoolCallsfinishReason
SseListener listener = new SseListener() {
@Override protected void send() { System.out.print(getCurrStr()); }
};
chatService.chatCompletionStream(req, listener);
String full = listener.getOutput().toString(); // 聚合后的完整文本
String reasoning = listener.getReasoningOutput(); // reasoning 片段
String stop = listener.getFinishReason();
Responses 非流式
优先围绕:
Response
Response response = responsesService.create(req);
// 遍历 output items 读取文本(不是 choice.message)
StringBuilder text = new StringBuilder();
for (ResponseItem item : response.getOutput()) {
if (item.getContent() == null) continue;
for (ResponseContentPart part : item.getContent()) {
if (part.getText() != null) text.append(part.getText());
}
}
int inputTokens = response.getUsage().getInputTokens();
Responses 流式
优先围绕:
ResponseSseListener.eventsoutputTextreasoningSummaryfunctionArgumentsresponse
ResponseSseListener listener = new ResponseSseListener() {
@Override protected void onEvent() { System.out.print(getCurrText()); }
};
responsesService.createStream(req, listener);
String full = listener.getOutputText().toString();
String reasoning = listener.getReasoningSummary().toString();
6. 一个很实用的团队规则
建议统一这条规则:
- 业务层先读统一实体
- provider 特有细节只在适配层或极少数边界层读取
- 不要在业务层到处手搓原始 JSON 路径
这不是“写法更优雅”的问题,而是为了让 SDK 演进时不把 provider 差异扩散到整条调用链。
7. 常见误区
误区一:把 functions 当成最终 provider 字段
它只是本地工具注册输入,最终真正送给 provider 的是解析后的 tools。
误区二:把流式 listener 当作打印回调
实际上它们是聚合器,里面已经有大量结构化状态。
误区三:业务层直接依赖 provider 原始 JSON 结构
短期灵活,长期会让所有适配成本泄漏到业务层。
误区四:extraBody 变成主通道
一旦所有字段都走 extraBody,统一请求对象就失去意义了。
8. 这一页的结论
AI4J 在请求/返回层的核心约定是:统一实体承载主语义,本地辅助字段承载注册与运行时语义,provider service 负责把它们投影成最终 payload,而 listener/response entity 负责把返回重新聚合成稳定读取面。理解这层约定,才能避免把 provider 差异和本地运行时语义混在一起。