OpenAI Chat Completions 与 Spring AI

最后更新: 2025-04-20 05:43:09 +0800 +0800
tags: Ai | Llm | Openai | Spring-Ai | Api | Json
原文以英文撰写。您正在阅读AI翻译版本。

OpenAI Chat Completions API 是一种广泛实现的、基于 JSON over HTTP 的对话式语言模型接口。客户端发送按顺序排列的消息列表和生成选项;服务器返回一条或多条包含元数据和令牌使用量的 assistant 消息。

其基本结构如下:

text
HTTP client
  -> POST /v1/chat/completions
  -> request JSON containing messages and options
  -> model inference
  <- response JSON containing choices and usage

许多网关和模型服务器即使底层模型并非由 OpenAI 托管,也会实现 OpenAI 兼容端点。因此,这种格式可作为事实上的互操作 API,但它不是像 HTTP、JSON-RPC 或 MCP 那样独立的正式标准。不同的兼容提供商可能支持不同的字段、模型、限制、错误格式或扩展。

Spring AI 在这种线格式之上为 Java 应用提供了与提供商无关的模型。应用代码使用 Prompt、Message、ChatOptions 和 ChatResponse;OpenAI 或 OpenAI 兼容适配器负责在这些对象与提供商 JSON 之间进行转换。

HTTP 请求 #

简化后的请求如下:

http
POST /v1/chat/completions HTTP/1.1
Host: api.example.com
Authorization: Bearer ...
Content-Type: application/json

{
  "model": "gpt-4.1",
  "messages": [
    {
      "role": "system",
      "content": "Answer concisely."
    },
    {
      "role": "user",
      "content": "Explain optimistic locking."
    }
  ],
  "temperature": 0.3,
  "max_tokens": 1000,
  "stream": false
}

model 和 messages 用于标识应运行的模型以及模型应看到的对话上下文。其余字段控制生成行为。一些网关通过配置、部署名称、URL 或请求头选择模型,而不是要求请求体中包含 model。

消息 #

messages 是有序的。其顺序属于提示的一部分:

json
[
  {
    "role": "system",
    "content": "You are a support assistant."
  },
  {
    "role": "user",
    "content": "Where is order 42?"
  },
  {
    "role": "assistant",
    "content": "I need to look up that order."
  },
  {
    "role": "user",
    "content": "It belongs to account 7."
  }
]

常见角色如下:

RoleMeaning
system应用程序指令和行为
developer较新的模型 API 支持的开发者指令
user用户提供的输入
assistant之前的或新生成的模型输出
tool应用程序工具调用的结果

文本通常表示为字符串。支持多模态的模型可能接受由类型化内容部件组成的数组,其中可以包含文本、图像、音频或其他媒体。

该端点通常是无状态的。调用方仅仅复用会话 ID,并不会使模型记住之前的 HTTP 请求。要继续会话,客户端必须再次发送选定的历史消息:

text
load conversation history
  -> append current user message
  -> send all selected messages
  -> receive assistant message
  -> save new user and assistant messages

为了保持在模型上下文限制之内,历史记录可以被截断、摘要化,或从外部存储中检索。

生成选项 #

常见的请求字段包括:

FieldPurpose
model模型或部署标识符
temperature控制采样随机性
top_p核采样阈值
max_tokens在旧版或兼容 API 中限制生成的令牌数
max_completion_tokens某些模型使用的新版 completion 令牌数上限
n候选选项的数量
frequency_penalty抑制重复使用频率较高的令牌
presence_penalty鼓励引入尚未使用的令牌
stop在配置的序列处停止生成
stream选择返回一个完整响应还是流式传输多个块
response_format在支持时请求 JSON 或受 schema 约束的输出
tools描述模型可以请求的函数
tool_choice限制是否可以选择工具,以及可以选择哪个工具

不同提供商的支持范围和语义可能不同。一个兼容端点接受的字段,可能在另一个端点中被拒绝、忽略、重命名或以不同方式解释。即使是同一端点背后的不同模型版本,也可能存在差异。

采样选项也会相互作用。应用程序通常会调节 temperature 或 top_p 其中之一,而不是在没有评估的情况下同时更改两者。

HTTP 响应 #

简化后的非流式响应如下:

json
{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1784778339,
  "model": "gpt-4.1",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Optimistic locking detects concurrent updates..."
      },
      "finish_reason": "stop",
      "logprobs": null
    }
  ],
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 18,
    "total_tokens": 43
  }
}

主要字段如下:

FieldMeaning
id提供商生成的请求或 completion 标识符
object响应类型,通常为 chat.completion
created提供商分配的 Unix 时间戳
model生成结果的模型
choicesassistant 输出候选项
usage输入、输出和总令牌用量
system_fingerprint可选的提供商侧模型配置标识符

每个 choice 通常包含 index、assistant message、finish_reason 和可选的对数概率。常见的结束原因包括:

  • stop:正常完成;
  • length:达到输出或上下文限制;
  • tool_calls:模型请求了一个或多个工具;以及
  • content_filter:安全过滤器停止了生成。

HTTP 响应成功并不意味着答案正确。它只表示提供商接受了请求,并生成了语法有效的模型结果。

提供商扩展 #

OpenAI 兼容并不意味着字节级完全相同。例如:

  • Azure 兼容响应可能包含 prompt_filter_results 和 content_filter_results。
  • 网关可能增加延迟检查点、路由详情、缓存统计或服务层级元数据。
  • 令牌详情可能区分推理、音频、缓存、已接受预测或已拒绝预测令牌。
  • 不同提供商的错误响应和速率限制请求头可能不同。

在可能的情况下,应用程序应忽略未知字段,并避免让领域模型绑定到每一个提供商扩展。当某个扩展是必需的时,应在提供商专用配置或元数据中明确表示它。

Schema 中的工具调用 #

请求可以包含工具定义:

json
{
  "model": "gpt-4.1",
  "messages": [
    {
      "role": "user",
      "content": "What is the status of order 42?"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "find_order",
        "description": "Look up an order by ID",
        "parameters": {
          "type": "object",
          "properties": {
            "order_id": {
              "type": "string",
              "description": "Canonical order ID"
            }
          },
          "required": [
            "order_id"
          ]
        }
      }
    }
  ]
}

模型不会执行该函数。它可以返回包含结构化请求的 assistant 消息:

json
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "function": {
        "name": "find_order",
        "arguments": "{\"order_id\":\"42\"}"
      }
    }
  ]
}

宿主应用程序负责验证参数、调用本地或远程操作,并在后续请求中发送结果:

json
{
  "role": "tool",
  "tool_call_id": "call_123",
  "content": "{\"status\":\"SHIPPED\"}"
}

因此,通常的通信过程如下:

text
model request with tool schemas
  <- assistant tool-call request
application executes tool
  -> model request with tool result
  <- final assistant answer

工具调用通常至少需要两次模型往返。授权、验证、幂等性、超时和副作用仍由应用程序负责,而不是由模型负责。

流式响应 #

当 "stream": true 时,服务器通常返回 Server-Sent Events,其数据包含增量的 chat.completion.chunk 对象:

text
data: {"object":"chat.completion.chunk","choices":[{"delta":{"role":"assistant"}}]}

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":"Optimistic"}}]}

data: {"object":"chat.completion.chunk","choices":[{"delta":{"content":" locking"}}]}

data: [DONE]

块中包含的是增量,而不是完整消息。客户端必须聚合内容、工具调用片段、结束原因,以及在响应提供时聚合令牌使用量。流式传输可以缩短首次可见输出的时间,但不一定会降低总延迟或令牌成本。

Spring AI 的对象模型 #

Spring AI 将面向应用的聊天概念与提供商线协议 DTO 分离。主要类型如下:

Spring AI typeResponsibility
ChatClient用于提示、Advisor、工具和输出转换的流式应用程序 API
ChatModel调用聊天模型的提供商无关接口
Prompt有序消息和请求选项
Message表示一条会话记录的通用接口
SystemMessagesystem 角色指令
UserMessageuser 角色输入
AssistantMessageassistant 文本和可选工具调用
ToolResponseMessage与 assistant 工具调用关联的结果
ChatOptions可移植的请求选项
OpenAiChatOptions使用 OpenAI 适配器时的 OpenAI 专用选项
ChatResponse包含生成结果和元数据的提供商无关响应
Generation一个 assistant 候选项
ChatResponseMetadata模型、用量、速率限制和提供商元数据
Usage输入、输出和总令牌数

底层应用流程如下:

java
ChatOptions options = OpenAiChatOptions.builder()
    .model("gpt-4.1")
    .temperature(0.3)
    .maxTokens(1000)
    .build();

Prompt prompt = new Prompt(
    List.of(
        new SystemMessage("Answer concisely."),
        new UserMessage("Explain optimistic locking.")
    ),
    options
);

ChatResponse response = chatModel.call(prompt);
AssistantMessage answer = response.getResult().getOutput();

ChatClient 提供了更高层次的流式等价 API:

java
ChatResponse response = chatClient.prompt()
    .system("Answer concisely.")
    .user("Explain optimistic locking.")
    .options(options)
    .call()
    .chatResponse();

call() 是同步终止操作。提示构建方法只负责配置请求;call() 会调用 Advisor 链和模型适配器,并等待完成。相比之下,stream() 返回响应式流。

Spring AI 如何序列化请求 #

Spring AI 通常不会将 Prompt.toString() 序列化为 HTTP 请求体。提供商适配器负责执行映射:

text
Spring AI Prompt
  messages:
    SystemMessage
    UserMessage
    AssistantMessage
    ToolResponseMessage
  options:
    ChatOptions
       |
       v
OpenAI adapter
  OpenAI request DTOs
  role/content conversion
  option and tool-schema conversion
       |
       v
JSON serializer
       |
       v
POST /v1/chat/completions

对于 Spring AI OpenAI 集成,传输类型位于 OpenAiApi 下,包括请求、消息、choice 和 completion 结构。应用程序通常应使用可移植类型,而不是直接构造这些传输 DTO。

响应沿相反路径处理:

text
HTTP response JSON
  -> OpenAI response DTOs
  -> AssistantMessage
  -> Generation
  -> ChatResponseMetadata and Usage
  -> ChatResponse

这一边界解释了为什么记录 ChatClientRequest 时可能显示 Java record 或 Prompt 表示,而 HTTP 线级日志显示具体的 OpenAI 兼容 JSON。前者是转换前的提供商无关请求;后者是转换后序列化的传输请求体。

Advisor 与会话记忆 #

ChatMemory 不属于 OpenAI 线协议 schema,而是应用侧抽象。记忆 Advisor 会在模型调用前加载消息,并在调用后保存新消息:

java
ChatMemory memory = MessageWindowChatMemory.builder()
    .maxMessages(20)
    .build();

ChatClient client = builder
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(memory).build()
    )
    .build();

每个请求都提供会话键:

java
ChatResponse response = client.prompt()
    .user(question)
    .advisors(spec -> spec.param(
        ChatMemory.CONVERSATION_ID,
        conversationId
    ))
    .call()
    .chatResponse();

从概念上看,Advisor 执行以下过程:

text
ChatMemory.get(conversationId)
  -> add historical messages to Prompt
  -> ChatModel.call(Prompt)
  -> ChatMemory.add(current user and assistant messages)

因此,序列化后的 messages 数组可能包含所选会话窗口的完整内容。会话 ID、存储、保留策略和访问控制属于宿主应用程序;Chat Completions API 不定义这些内容。

Spring AI 的工具序列化 #

Spring AI 使用 ToolCallback 表示可执行能力。带注解的 Java 方法可以被发现并转换为回调:

java
public class OrderTools {

    @Tool(description = "Look up an order by ID")
    public OrderStatus findOrder(
            @ToolParam(description = "Canonical order ID") String orderId) {
        return orderService.find(orderId);
    }
}

注册该对象:

java
String answer = chatClient.prompt()
    .user("Where is order 42?")
    .tools(orderTools)
    .call()
    .content();

会使 Spring AI 执行以下操作:

  1. 检查带注解的方法;
  2. 创建 ToolCallback;
  3. 为其参数推导 JSON Schema;
  4. 将回调定义映射到提供商的 tools 数组;
  5. 将返回的 tool_calls 映射为 AssistantMessage.ToolCall 值;
  6. 启用自动工具执行时,在宿主应用程序中调用回调;以及
  7. 将结果序列化为下一次模型请求的工具消息。

LLM 只能看到名称、描述和 schema。它不能反射 JVM,也不能调用任意方法。

记录两种表示 #

Spring AI Advisor 日志和 HTTP 线级日志回答的是不同问题。

SimpleLoggerAdvisor 等 Advisor 可以显示:

  • 之前的 Advisor 修改请求后的 Spring AI 消息;
  • ChatOptions;
  • 会话 ID 等 Advisor 上下文;以及
  • 提供商无关的 ChatResponse。

请求看起来像 Java toString() 输出,是因为日志记录的是 ChatClientRequest,而不是最终的 HTTP 请求体。

HTTP 客户端日志可以显示:

  • URL 和请求头;
  • 最终序列化的 JSON 请求;
  • 提供商特有的字段和默认值;
  • 原始 JSON 响应;以及
  • 流式块。

线级日志发生在提供商适配器创建传输 DTO 之后,因此适合诊断序列化问题和提供商兼容性。但它也存在风险:消息、检索到的文档、工具结果、授权请求头、Cookie 和个人数据都可能出现在日志中。只有在受控的开发环境中才应启用完整载荷日志,并应用脱敏和保留期限限制。

兼容性边界 #

OpenAI 兼容端点应被视为提供商契约,而不是行为完全相同的证明。请验证:

  • 端点路径和身份验证;
  • 支持的消息角色和内容类型;
  • 模型命名和部署选择;
  • 请求选项名称和限制;
  • 工具调用和结构化输出支持;
  • 流式事件格式;
  • 令牌使用量统计;
  • 安全和拒绝字段;
  • 错误正文和 HTTP 状态码;以及
  • 提供商特有的扩展。

Spring AI 通过将这些细节保留在模型适配器中来减少应用程序耦合,但它无法使不兼容的提供商行为变得一致。请将每个受支持的网关和模型作为独立的执行路径进行测试。

请参阅 Spring AI,了解更广泛的应用模型,包括 Advisors、RAG、向量存储、记忆仓库、可观测性和生产架构。

评论

在 GitHub 上阅读或评论(发表评论需要登录)