概述
小欣AI·服务台是欣农互联的AI智能体平台,以类 OpenAI Responses API 的形态提供统一的 AI 调用入口。 平台底层适配 DeerFlow、Dify 等多种引擎,对应用暴露一致的 Java 接口,更换引擎只需一行配置。
修改记录
| 日期 | 变更 |
|---|---|
| 2026-06-30 | 新增 Assistants API:XnAiClient.assistants() 提供 create/list/retrieve/modify/delete Agent 管理接口,对齐 OpenAI Assistants API 规范;DeerFlow Provider 内部调 /api/agents 并完成字段映射(instructions ↔ soul、asst_{name} ↔ name);Dify 侧全方法抛 UnsupportedOperationException。 |
| 2026-06-25 | Audio API 文档:新增 audio.transcribe 语音转文字接口、AudioRequest/AudioResponse 参数说明、热词管理(createVocabulary/updateVocabulary/deleteVocabulary/listVocabularies)完整文档与调用示例。 |
| 2026-06-25 | ToolCallItem.status 字段序列化透出(值 "in_progress" / "completed");DeerFlow updates 通道工具调用事件序列对齐 OpenAI 规范:type="ai" 消息含 tool_calls 时立即产出 OUTPUT_ITEM_ADDED + FUNCTION_CALL_ARGUMENTS_DONE,type="tool" 消息到达时产出 OUTPUT_ITEM_DONE(替代原来的单一 OUTPUT_ITEM_ADDED);ask_clarification 降级路径用 content 文本兜底 arguments。 |
| 2026-06-24 | 新增语音转文字(STT)能力:XnAiClient.audio() 提供 transcribe() 语音转文字与热词管理接口,对齐 OpenAI Audio API;STT Provider 与 AI Provider 解耦(audio.provider 配置独立选择);首批实现阿里百炼 DashScope Provider,支持异步转写与 Hotword 热词增强。 |
| 2026-06-05 | 修复续话(previousResponseId)场景下误返回上一轮的附件与工具调用:DeerFlow 续话时上游会在流开头重放上一轮历史,SDK 不再把其中上一轮的附件(container_file_citation)与工具调用(write_file/present_files 等)当本轮内容下发;续话只携带本轮真实产出,前端无需再自行去重。 |
| 2026-06-05 | 可观测性增强:trace 增 sessionId(=会话标识,多轮续话归入 Langfuse Sessions 视图同一 session);流式 TTFT 改用原生 completionStartTime;新增可选配置 release / environment(版本/环境过滤)、sample-rate(采样率)、request-timeout-ms(上报独立超时)。 |
| 2026-06-03 | 可观测性按 用户 / 租户 / Agent 三维度统计 token:新增可选 ResponseRequest.builder().userId(str) → Langfuse trace 的 userId(不传则不带、不再用租户编码顶替);租户改以 tag tenant=<code> 表达、Agent 以 tag agent=<name> 表达;trace.name 改用 SDK 方法名(如 Responses.create)。 |
| 2026-06-02 | 新增「取消生成」:StreamHandle.awaitResponseId(timeoutMs) + responses().cancel(responseId);明确续话用 getResponseId()(含 threadId)、停止用 awaitResponseId()(含 runId)的取值区别;instructions 透传为 system message;修复同步 getOutputText() 误返回对话标题;品牌名更新。 |
| 2026-05-29 | 附件操作与集成(上传 / 下载 / 在线预览,金蝶云苍穹后端 + 前端完整示例)。 |
| 2026-05-21 | 新增 StreamHandle.getResponseId()(落库 / 续话);修复 output_text 增量泄漏;API 文档与上线链接优化。 |
设计原则
- 接口形态对齐 OpenAI Responses API,仅扩展必填
agent字段标识 Agent - 应用代码只依赖
XnAiClient/Responses/Files,不感知底层引擎差异 - SDK 内部通过
engine配置自动路由,DeerFlow 和 Dify 间的切换对业务透明
概念映射
| SDK 概念 | OpenAI 对应 | 说明 |
|---|---|---|
Responses.create() | POST /responses | 创建模型响应(同步聚合 / 流式回调 / SSE 透传) |
Responses.retrieve() | GET /responses/{id} | 按 ID 取回历史响应 |
Responses.delete() | DELETE /responses/{id} | 删除已存储的响应 |
Files.upload() | POST /files | 上传文件供 Agent 引用 |
ResponseRequest | Request body | 请求参数对象,Builder 模式构建,不可变 |
Response | Response object | 聚合后的完整响应,含 output / usage |
ResponseStreamEvent | SSE event | 归一化后的流式事件,统一 type 枚举 |
快速开始
三步:拿到客户端 → 构造请求 → 调用同步或流式接口。
new 一个会有显著开销,更严重的是流式调用:
若把 createStream / createStreamAsSse 放进 try (XnAiClient ...) { } 块内,
方法立即返回后外层 client.close() 会关闭线程池,正在后台读流的任务被腰斩 → 流瞬间断开。
正确做法:在 Spring @Bean / ServletContextListener 等容器初始化时构造一次,应用关闭时再 close。
import xn.ai.platform.XnAiClient; import xn.ai.platform.XnAiClientFactory; import xn.ai.platform.responses.*; // 应用启动时构造一次(Spring @Bean / ServletContextListener / static 字段) static final XnAiClient client = XnAiClientFactory.create(); // 业务请求中复用 ResponseRequest req = ResponseRequest.builder() .agent("research-assistant") .input("用 100 字介绍 RAG") .mode("thinking") .temperature(0.3) .build(); Response r = client.responses().create(req); System.out.println(r.getOutputText());
流式版本只需把 create 换成 createStream,并用 try-with-resources 阻塞等待:
try (StreamHandle h = client.responses().createStream(req, new StreamCallback() { public void onEvent(ResponseStreamEvent ev) { if (ev.type() == ResponseStreamEvent.Type.OUTPUT_TEXT_DELTA) { System.out.print(ev.delta()); } } public void onComplete(Response r) { /* 流正常结束 */ } public void onError(Throwable t) { t.printStackTrace(); } })) { // close() 阻塞等待后台流任务自然结束 }
安装与依赖
<dependency> <groupId>com.xnlink</groupId> <artifactId>xnai-platform-sdk</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
- 编译目标:JDK 8
- HTTP 栈:OkHttp 3.x
- 日志:SLF4J(SDK 不绑定具体实现)
XnAiClientFactory
SDK 客户端工厂,提供三个静态构造入口 + Provider 扩展注册钩子。
| 方法 | 说明 |
|---|---|
static XnAiClient create() |
零参构造,从 System.getProperty("xn.ai.*") 加载默认前缀配置。最常用。 |
static XnAiClient create(String prefix) |
自定义系统属性前缀(多实例场景,如 xn.ai.tenantA.*)。 |
static XnAiClient create(XnAiConfig config) |
显式传入手动构造的 XnAiConfig(测试用 / 配置中心拉取后回填)。会执行 config.validate()。 |
static void register(String engine, AiClientProvider provider) |
注册自定义 Provider。同名注册会覆盖(含覆盖内置实现,便于测试)。 |
static void unregister(String engine) |
取消注册。主要用于测试隔离。 |
内置 Provider deerflow / dify 通过反射延迟加载,无需手动 register。
XnAiClient
SDK 客户端接口(AutoCloseable)。线程安全,应作为应用级单例使用。
| 方法 | 说明 |
|---|---|
Responses responses() |
Responses 子接口:create / createStream / createStreamAsSse / retrieve / delete。 |
Files files() |
Files 子接口:upload / list / retrieve / delete / getLimits。 |
void refreshAuth() |
显式刷新认证状态。DeerFlow:清空 Cookie + CSRF 后下次请求重新登录;Dify:no-op。凭证轮换或鉴权异常恢复时调用。 |
Object getNativeApi() |
逃生舱口:返回 Provider 特定的原生客户端(DeerflowNativeApi / DifyNativeApi)。
⚠ 使用即与 Provider 强耦合,引擎切换将失效。仅在 SDK 抽象暂不覆盖的能力时使用。
|
void close() |
释放底层资源(OkHttp 连接池等)。幂等。应用关闭时调用一次即可,不要每请求 close。 |
XnAiConfig
SDK 配置对象。三种构造方式:
- 静态工厂从系统属性加载(最常用)
- 手动 setter 链式构造(配置中心场景)
- 多租户工厂(按租户前缀加载)
核心字段
| 字段 | setter | 说明 |
|---|---|---|
| baseUrl | setBaseUrl(String) | AI 引擎 base URL,例如 http://127.0.0.1:2026(必填)。 |
| engine | setEngine(String) | 引擎名:deerflow / dify / 已 register 的自定义名。必填。 |
| timeoutSeconds | setTimeoutSeconds(int) | 请求超时(秒),默认 60。流式建议 ≥1800。 |
| authUsername / authPassword | setAuthUsername / setAuthPassword | DeerFlow 登录凭证。 |
| authApiKeys | putAuthApiKey(agent, key) / setAuthApiKeys(Map) | Dify 按 agent 维度的 API Key 映射。 |
| extra | putExtra(key, value) / setExtra(Map) | Provider 特定扩展配置(KV 字符串,由各 Provider 自行解析)。 |
| retryPolicy | setRetryPolicy(RetryPolicy) | 客户端重试策略。未显式设置时使用 RetryPolicy.defaults();显式传 null 表示禁用重试。 |
静态工厂方法
| 方法 | 说明 |
|---|---|
static XnAiConfig fromSystemProperties() | 从默认前缀 xn.ai 加载。 |
static XnAiConfig fromSystemProperties(String prefix) | 从自定义前缀加载。 |
static XnAiConfig forTenant(String tenantCode) | 多租户专用:按 xn.ai.<tenantCode>.* 加载。 |
static XnAiConfig forTenant(String tenantCode, String basePrefix) | 自定义基础前缀的多租户加载。 |
void validate() | 校验必填字段,create(config) 内部已调用。 |
系统属性约定
默认前缀 xn.ai(常量 DEFAULT_PREFIX),完整 key 例:
xn.ai.base-url=http://127.0.0.1:2026 xn.ai.engine=deerflow xn.ai.timeout-seconds=1800 xn.ai.auth.username=admin xn.ai.auth.password=***
可观测性(Langfuse)配置
可选。开启后 SDK 会将每次调用上报到 Langfuse 作链路追踪,与主配置同前缀加载(多租户场景按各自前缀)。未开启则零开销、不影响主流程。
# 必填(enabled=true 时缺任一项则自动降级为关闭) xn.ai.platform.langfuse.enabled=true xn.ai.platform.langfuse.endpoint=http://langfuse.internal:3000 xn.ai.platform.langfuse.public-key=pk-lf-*** xn.ai.platform.langfuse.secret-key=sk-lf-*** # 可选(异步上报队列调优,给出的是默认值) xn.ai.platform.langfuse.queue-capacity=1000 xn.ai.platform.langfuse.batch-size=20 xn.ai.platform.langfuse.flush-interval-ms=2000 # 可选(运维 / 成本,给出的是默认值) xn.ai.platform.langfuse.release= # 版本号 → trace.release,便于按版本过滤 xn.ai.platform.langfuse.environment= # 环境标签(prod/staging/dev) → trace.environment xn.ai.platform.langfuse.sample-rate=1.0 # 采样率 0..1;<=0 视为 1.0(全采)。关闭观测用 enabled=false xn.ai.platform.langfuse.request-timeout-ms=10000 # 上报请求独立超时,不复用业务长超时
会话维度(Sessions):trace 的 sessionId 取会话标识(DeerFlow threadId / Dify conversation),同一会话多轮续话归到 Langfuse Sessions 视图的同一 session,可按整场对话聚合 token 与调用链。流式 TTFT 以原生 completionStartTime 上报,Langfuse 原生展示首 token 延迟。
调用时通过请求构建器 ResponseRequest.builder().traceId("<业务唯一ID>") 传入的 traceId(如前端透传)会作为 Langfuse trace 的主键:同一 traceId 的多次上报聚合到同一条 trace;该字段仅 SDK 内部消费、不透传给上游 Provider。不传则按内部规则生成。
按 用户 / 租户 / Agent 三维度统计 token:Langfuse trace 会写入以下维度,可在 Dashboard 分别过滤聚合 token 用量:
- 用户(userId):通过
ResponseRequest.builder().userId("<用户标识>")传入,映射为 trace 的内置userId。可选;不传则该 trace 不带 userId(不会用租户编码顶替)。该字段仅 SDK 内部消费、不透传给上游 Provider。 - 租户(tenant):取自配置的
tenantCode,以 tagtenant=<code>与metadata.tenant表达。 - Agent:取自
ResponseRequest.agent,以 tagagent=<name>表达;同时engine=<引擎>、model=<真实模型>、mode=<模式>一并作为 tag。
trace 的 name 为 SDK 方法名(如 Responses.create / Responses.createStream),便于按操作聚合。
多租户
同一进程承载多家租户时,给每个租户一份独立前缀,分别构造 client:
// -Dxn.ai.tenantA.baseUrl=... -Dxn.ai.tenantA.engine=deerflow ... // -Dxn.ai.tenantB.baseUrl=... -Dxn.ai.tenantB.engine=dify ... XnAiClient a = XnAiClientFactory.create(XnAiConfig.forTenant("tenantA")); XnAiClient b = XnAiClientFactory.create(XnAiConfig.forTenant("tenantB"));
完整示例
覆盖三种调用方式:SSE 透传(最常见)、同步调用、流式回调。统一使用 try-with-resources + 按异常类型分发 HTTP 状态码。
1. SSE 透传 + 多轮对话
最常用的集成方式:Controller 接收前端请求,SDK 直接把归一化后的 OpenAI 标准 SSE 字节写入 HttpServletResponse.getOutputStream()。
input 支持两种形态:简单文本(String)用于单轮,List<Message> 用于自构造多轮历史或多模态输入。
// client 在 Spring @Bean 中注入,本类持有引用(不要在方法里 new + close) @Autowired private XnAiClient client; @PostMapping(value = "/chat/stream", produces = "text/event-stream;charset=UTF-8") public void chatStream(@RequestBody ChatRequest body, HttpServletResponse response) throws IOException { response.setHeader("Cache-Control", "no-cache"); response.setHeader("X-Accel-Buffering", "no"); response.flushBuffer(); // 先把响应头刷出,再开始流式写入 ResponseRequest req = ResponseRequest.builder() .agent(body.getAgent()) .input(body.hasMessages() ? body.getMessages() : body.getInputText()) .mode(body.getMode()) .previousResponseId(body.getPrevId()) .temperature(body.getTemperature()) .maxOutputTokens(body.getMaxTokens()) .store(body.getStore()) .putMetadata("tenantCode", tenantCode) .putMetadata("traceId", traceId) .build(); // ✅ 必须 try-with-resources:close() 阻塞等待后台流任务自然结束 // 若不阻塞,方法立即返回 → Servlet 容器关闭 response → 后台写流时拿到 IOException try (StreamHandle h = client.responses().createStreamAsSse(req, response.getOutputStream())) { // getResponseId() 同步返回,与后台 SSE 流并发执行,不阻塞输出 String responseId = h.getResponseId(); conversationDao.save(body.getUserId(), responseId); // 落库,供下轮 previousResponseId } catch (AuthenticationException e) { response.sendError(401, "AI 平台认证失败,请检查凭证"); } catch (AuthorizationException e) { response.sendError(403, "无权访问该 Agent"); } catch (RateLimitException e) { response.setHeader("Retry-After", String.valueOf(e.getRetryAfterSeconds())); response.sendError(429, "请求过于频繁,请稍后重试"); } catch (NetworkException e) { response.sendError(504, "AI 平台连接超时"); } catch (ValidationException e) { response.sendError(400, "请求参数有误: " + e.getMessage()); } catch (AiPlatformException e) { response.sendError(502, "AI 平台异常"); } } // ChatRequest DTO,前端可传 inputText 或 messages static class ChatRequest { String agent; String inputText; // 简单文本:.input(String) List<Message> messages; // 多轮/多模态:.input(List<Message>) String mode; String prevId; Double temperature; Integer maxTokens; Boolean store; boolean hasMessages() { return messages != null && !messages.isEmpty(); } // getters/setters ... }
XnAiClient为应用级单例,启动时构造、应用关闭时 close。切勿每请求 new + close,流式调用会因线程池被销毁而立即断开。- 多轮对话两种方式:
previousResponseId续接上文,或List<Message>自构造完整历史(含 system / user / assistant)。 - SSE 事件由 SDK 直接写入
response.getOutputStream(),业务代码不解析事件。 h.getResponseId()在 handle 返回后即可同步调用,与后台 SSE 流并发执行,不阻塞输出。落库后前端下一轮请求传入prevId即可续话。- 流式接口(
createStream/createStreamAsSse)返回的StreamHandle必须用 try-with-resources 包裹,close() 内部阻塞等待后台流任务自然结束。
2. 同步调用
import xn.ai.platform.responses.Message; // 脚本场景下用 try-with-resources(用完即关);Web 服务请用单例 client try (XnAiClient client = XnAiClientFactory.create()) { // ① String 输入 — 单轮 Response r1 = client.responses().create( ResponseRequest.builder() .agent("pig-farm-agent") .input("帮我分析最近一周的养殖数据") // String .mode("thinking") .temperature(0.3) .topP(0.9) .maxOutputTokens(2000) .reasoning(Reasoning.of("medium", null)) .text(TextConfig.of(null, "medium")) .parallelToolCalls(true) .truncation("auto") .store(true) .putMetadata("traceId", traceId) .build() ); // ② List<Message> 输入 — 多轮自构造 List<Message> messages = Arrays.asList( Message.user("第一个问题"), Message.assistant("上一轮的回复..."), Message.user("追问:能详细展开第二点吗?") ); Response r2 = client.responses().create( ResponseRequest.builder() .agent("pig-farm-agent") .input(messages) // List<Message> .mode("pro") .build() ); } catch (AuthenticationException e) { // 通知管理员更新凭证 } catch (RateLimitException e) { // 延迟 e.getRetryAfterSeconds() 秒后重试 } catch (NetworkException e) { // 重试或降级 } catch (AiPlatformException e) { // e.getProviderName(), e.getRequestId(), e.getHttpStatus() }
3. 流式回调(需解析事件)
// client 为应用级单例(Spring @Bean / static 字段),不要在方法里新建 // ① String 输入 — 必须 try-with-resources 等流自然结束 try (StreamHandle h = client.responses().createStream( ResponseRequest.builder() .agent("pig-farm-agent") .input("帮我分析最近一周的养殖数据") // String .mode("ultra") .build(), event -> { switch (event.type()) { case OUTPUT_TEXT_DELTA: sendToFrontend(event.delta()); break; case RESPONSE_COMPLETED: closeFrontend(); break; case RESPONSE_FAILED: log.error("流异常: {}", event.response().getError().getMessage()); break; default: break; } })) { // close() 阻塞等待流任务结束 } catch (AiPlatformException e) { log.error("连接失败", e); } // ② List<Message> 输入 — 多轮 List<Message> messages = Arrays.asList( Message.user("上轮数据有异常波动,帮我分析原因"), Message.assistant("波动来自供给端,需求侧平稳。"), Message.user("那供给端具体是哪个环节?") ); try (StreamHandle h = client.responses().createStream( ResponseRequest.builder() .agent("pig-farm-agent") .input(messages) // List<Message> .mode("pro") .build(), event -> log.info("{}: {}", event.type(), event.delta()))) { // 阻塞至流结束 }
responses.create
同步调用:SDK 内部使用流式协议拉取,聚合所有事件后返回完整 Response。
典型参数组合
agent+mode+ 字符串input:指定运行模式的一次性问答agent+List<Message>:多轮自构造历史agent+previousResponseId:基于上轮继续agent+ 多模态Content[]:含图 / 文件
抛出
ValidationException· agent / input 缺失AuthenticationException· 认证失败RateLimitException· 限流NetworkException· 网络 / 超时ProviderException· 上游其它错误
ResponseRequest req = ResponseRequest.builder() .agent("research-assistant") .mode("pro") // "flash" / "thinking" / "pro" / "ultra" .input("对比 BM25 与向量检索") .temperature(0.3) .maxOutputTokens(800) .store(true) .build(); Response r = client.responses().create(req);
responses.createStream
强类型流式调用。SDK 把上游 SSE 归一化为 ResponseStreamEvent,逐个投递给 StreamCallback。
StreamCallback
| 方法 | 触发时机 |
|---|---|
| onEvent(ResponseStreamEvent) | 每一个归一化事件。 |
| onComplete(Response) | 流正常结束(RESPONSE_COMPLETED),含最终聚合对象。 |
| onError(Throwable) | SDK / 网络层异常,等价于收到 ERROR 事件。 |
StreamHandle
| 方法 | 说明 |
|---|---|
| cancel() | 主动中止流,幂等。前端断开时务必调用,否则占用连接。 |
| close() | AutoCloseable,等价于 cancel()。 |
| isActive() | 是否仍在接收事件。 |
| getResponseId() |
同步返回本次请求的初始 Response ID(DeerFlow 下含 threadId、runId 可能为空),可立即调用、不阻塞。
续话 / 落库足够(续话只用 threadId);不可用于 cancel(可能缺 runId),取消请用 awaitResponseId()。DeerFlow 返回非 null;Dify 当前返回 null。
|
| awaitResponseId(long timeoutMs) |
阻塞至 run_id 就绪,返回含 runId 的 Response ID(可直接 cancel);超时返回当前值(至少含 threadId),不抛异常。
用于「停止生成」:拿含 runId 的 id 落库,另一请求凭它 responses().cancel(id)。
|
responses.createStreamAsSse
归一化后的标准 SSE 字节直接写入 OutputStream。内部由 SseWriter 负责格式输出:
上游原始 SSE → *SseNormalizer 归一化为 ResponseStreamEvent →
按 event:xxxdata:{...} 逐帧写出,同时注入 sequence_number、
映射上游错误码为 OpenAI 兼容格式、保留 function_call delta 为字符串增量。
Servlet 拿到 HttpServletResponse.getOutputStream() 即可转发:
@PostMapping(value = "/chat", produces = "text/event-stream") public void chat(HttpServletResponse resp, @RequestBody ChatDTO dto) throws IOException { resp.setHeader("Cache-Control", "no-cache"); resp.setHeader("Connection", "keep-alive"); resp.setHeader("X-Accel-Buffering", "no"); // 关掉 Nginx 缓冲 ResponseRequest req = ResponseRequest.builder() .agent(dto.getAgent()) .input(dto.getInput()) .mode(dto.getMode()) // "flash" / "thinking" / "pro" / "ultra" .previousResponseId(dto.getPrevId()) .build(); try (StreamHandle h = client.responses().createStreamAsSse(req, resp.getOutputStream())) { // 阻塞至流结束 / 取消 } }
sequence_number、错误码、function_call 字符串增量),
可直接被 openai-node / openai-python 等官方客户端消费。
落库场景:续话 vs 停止生成,取值不同
threadId + runId。
续话只用 threadId → getResponseId() 立即取即可;
停止生成(cancel)需要 runId → 必须用 awaitResponseId() 取含 runId 的版本。
① 续话落库(立即取,够用,不阻塞输出):
try (StreamHandle h = client.responses().createStreamAsSse(req, resp.getOutputStream())) { String responseId = h.getResponseId(); // 立即返回(含 threadId),与 SSE 流并发 conversationDao.save(userId, responseId); // 落库,供下轮 previousResponseId }
② 支持「停止生成」(取含 runId 的 id,供 cancel;通常等 <1s):
// 对话端点:拿含 runId 的 id 落库 try (StreamHandle h = client.responses().createStreamAsSse(req, resp.getOutputStream())) { String responseId = h.awaitResponseId(3000); // 阻塞至 run_id 就绪,含 runId sessionDao.save(sessionId, responseId); // 供「停止」端点使用 } // 停止端点(另一个请求):凭 responseId 服务端取消,跨实例可靠 public void stop(String sessionId) { String responseId = sessionDao.get(sessionId); client.responses().cancel(responseId); }
responses.retrieve
按对外不透明 ID 取回历史响应。SDK 内部自动将平台原生 ID 编码为稳定统一的 responseId。
部分引擎不支持 retrieve 时抛 UnsupportedOperationException。
responses.delete
删除一次响应,会一并释放底层 thread / conversation。
ResponseRequest 全部参数
统一通过 ResponseRequest.builder().xxx().build() 构造,构建后不可变。
字段对齐 OpenAI Responses,仅扩展 agent(必填)、mode、thinking、extraConfig。
流式/同步由调用方法决定(create / createStream / createStreamAsSse),请求体本身无需 stream 字段。
| 字段 | Builder 方法 | 说明 | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| agent必填 | .agent(str) |
目标 Agent 标识。对应 AI 平台后台发布 Agent 时生成的调用凭据(名称或 ID)。
不传或传空串直接抛 ValidationException。
|
||||||||||||||||||||||||||||||
| input必填 | .input(String) 或.input(List<Message>) |
用户输入,支持两种形态:
previousResponseId 时 input 通常只需本轮新消息。
|
||||||||||||||||||||||||||||||
| model | .model(str) |
指定 LLM 型号,不传则使用 Agent 默认模型。 | ||||||||||||||||||||||||||||||
| traceId | .traceId(str) |
外部 traceId(如前端透传),作为 Langfuse trace 主键以聚合同一会话的多次调用。仅 SDK 内部消费、不透传给上游 Provider;未开启 Langfuse 时无副作用。详见可观测性配置。 | ||||||||||||||||||||||||||||||
| userId | .userId(str) |
用户标识(可选),映射为 Langfuse trace 的 userId,用于按用户维度统计 token。不传则该 trace 不带 userId(不会用租户编码顶替)。仅 SDK 内部消费、不透传给上游 Provider。详见可观测性配置。 |
||||||||||||||||||||||||||||||
| mode | .mode(str) |
运行模式,一行设置即可驱动 thinking、推理深度、plan、子代理等多项参数,
无需手动拼 extraConfig。有效值(SDK 内置的四档预设):
null 则不展开任何参数,由 Agent 自身默认行为决定。传入未知值:记录 WARN 日志并透传,不抛异常。 覆盖优先级: extraConfig > thinking > mode 展开的默认值。
例如 mode("ultra").thinking(false):保留 ultra 的 plan + 子代理,但关闭 thinking。断线续传:设置 mode 后客户端断开时上游继续执行任务, 需自行保存 resp_id 以便重连。
|
||||||||||||||||||||||||||||||
| thinking | .thinking(boolean) |
思维链开关。设 true 开启扩展推理;与 mode 同时使用时,thinking 优先级高于 mode 展开的对应值。
|
||||||||||||||||||||||||||||||
| instructions | .instructions(str) |
System prompt。当前版本暂不支持,SDK 记录 WARN 日志并忽略,不抛异常。 如需 system prompt,请在 AI 平台的 Agent 定义中配置。 | ||||||||||||||||||||||||||||||
| previousResponseId | .previousResponseId(str) |
多轮对话上下文锚点。传入上一轮 Response.getId()(SDK 黑盒字符串,无需解析),
SDK 内部自动关联历史对话。传了后无需重复传历史 messages。传 null 或不传表示新会话。
⚠ 只持久化 Response.getId() 返回值,不要尝试解析或重组;引擎切换后自构造的 ID 将不可用。
|
||||||||||||||||||||||||||||||
| temperature | .temperature(Double) |
采样温度,0.0~2.0,值越大输出越随机。与 topP 通常二选一。 |
||||||||||||||||||||||||||||||
| topP | .topP(Double) |
核采样参数,0.0~1.0。与 temperature 二选一。 |
||||||||||||||||||||||||||||||
| maxOutputTokens | .maxOutputTokens(Integer) |
输出 token 截断阈值。达到后流式产生 RESPONSE_INCOMPLETE,Response.status="incomplete"。 |
||||||||||||||||||||||||||||||
| tools | .tools(Object) |
工具定义列表,SDK 原样透传。常用格式:List<Map>,每个含 type / name / description / parameters。 |
||||||||||||||||||||||||||||||
| toolChoice | .toolChoice(Object) |
"auto" / "required" / "none",或指定 tool 对象(含 type:"function" + name)。 |
||||||||||||||||||||||||||||||
| parallelToolCalls | .parallelToolCalls(Boolean) |
是否允许并行工具调用。 | ||||||||||||||||||||||||||||||
| reasoning | .reasoning(Reasoning) |
推理模型配置,对齐 OpenAI Responses。用 Reasoning.of(effort, summary) 构造:
REASONING_TEXT_DELTA 和 REASONING_SUMMARY_TEXT_DELTA 事件。
|
||||||||||||||||||||||||||||||
| text | .text(TextConfig) |
文本输出格式,对齐 OpenAI Responses,用 TextConfig.of(format, verbosity) 构造:
|
||||||||||||||||||||||||||||||
| truncation | .truncation(str) |
"auto" — 超长自动截断历史;"disabled" — 不截断。 |
||||||||||||||||||||||||||||||
| store | .store(Boolean) |
是否在服务端持久化本次响应。默认 true。置 false 后无法 retrieve。 |
||||||||||||||||||||||||||||||
| include | .include(List<String>) |
响应中包含的额外字段,如 List.of("reasoning.encrypted_content")。 |
||||||||||||||||||||||||||||||
| metadata | .metadata(Map) 或.putMetadata(key, val) |
透传业务标识(SDK 不解析,原样回写)。典型用例:tenant / userId / traceId。putMetadata 可多次追加。 | ||||||||||||||||||||||||||||||
| extraConfig | .extraConfig(Map) 或.putExtra(key, val) |
引擎特定扩展参数,优先级最高,可覆盖 mode 展开的任意默认值。
建议优先用 mode,仅在确需精细控制时才使用此字段。
|
Input 内容类型
input 接受两种形态:纯字符串、或 List<Message>。Message 持有 role 与 List<Content>。
Message 工厂
Message.user(String text)· 单条用户文本Message.user(Content... content)· 多模态用户消息Message.system(String text)· 系统提示Message.assistant(String text)· 拼接历史助手回复
Content 子类
| 类型 | 用途 |
|---|---|
| TextContent | 纯文本,new TextContent("hi")。 |
| InputImageContent | 图片,fileId 或 imageUrl 二选一。 |
| InputFileContent | 文件附件。fileId(已上传)或本地 Path / InputStream(自动上传)。 |
| CustomContent | 原始 JSON 直通,应付 SDK 暂未覆盖的扩展类型。 |
List<Message> msgs = Arrays.asList( Message.system("你是一名审计师"), Message.user( new TextContent("分析这张报表"), new InputImageContent("file_abc", null) ) ); ResponseRequest req = ResponseRequest.builder() .agent("audit") .input(msgs) .build();
files.upload
上传本地文件 / 字节流,返回 SDK 编码的统一 file_id,可在 InputImageContent / InputFileContent 中引用。
FileUploadRequest
| 字段 | 说明 |
|---|---|
| file Path | 本地文件路径。与 stream 二选一。 |
| stream InputStream | 字节流。配合下方 filename。 |
| filename string | 显示名。流式上传时必填。 |
| purpose string | 用途标签("assistants" / "vision" 等),由具体引擎解释。 |
| previousResponseId string | 关联到已有会话,不传则新建。 |
| agent string | 归属 Agent 标识。 |
| user string | 用户标识,用于权限与统计。 |
FileObject f = client.files().upload(FileUploadRequest.builder() .file(Paths.get("/tmp/report.pdf")) .purpose("assistants") .agent("doc-qa") .build()); System.out.println(f.getId()); // 不透明 ID System.out.println(f.getBytes());
files.list
列出某会话已上传的文件。
files.retrieve
查询单个文件元信息。部分引擎不支持时返回 null。
files.download
下载附件内容(用户上传的文件,或 Agent 生成的附件),返回流式 FileDownload(字节流 + 元数据)。
供业务后端转存,或经业务后端代理给前端在线预览。返回对象必须由调用方 close()(建议 try-with-resources)。
fileId 来自上传结果(FileObject.getId())或流式事件
response.output_text.annotation.added 里的 file_id。上传文件与生成文件共用同一下载逻辑。
DeerFlow 端点需登录态(由 SDK 内部鉴权),前端/业务后端无法直连,必须经此方法取字节。
// 业务后端:取字节转存(大文件流式,避免全载内存) try (FileDownload dl = client.files().download(fileId)) { String name = dl.getFilename(); String mime = dl.getContentType(); // 可空 boolean inline = dl.isInline(); // 预览提示:活动内容(html/svg)为 false try (InputStream in = dl.getStream()) { // 拷贝到本地/对象存储,或直接写入 HTTP 响应回吐前端 } }
files.delete
files.getLimits
当前引擎的上传限额。
语音转文字(Audio STT)
通过 XnAiClient.audio() 获取 Audio 实例,调用 transcribe() 将音频文件转写为文字。
支持异步转写(HTTP URL)和热词增强,对齐 OpenAI Audio API 规范。
配置
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
xn.ai.platform.audio.provider | String | 是 | - | STT 引擎:dashscope |
xn.ai.platform.audio.endpoint | String | 是 | - | STT 服务地址,如 https://dashscope.aliyuncs.com/api/v1 |
xn.ai.platform.audio.api-key | String | 是 | - | API 密钥 |
xn.ai.platform.audio.model | String | 否 | fun-asr | 默认语音模型 |
audio.transcribe
提交音频文件转写为文字。传入 fileUrl(远程 HTTP URL)走异步转写,返回 AudioResponse(含完整文本、分句、时长、语言)。
// 传入远程音频 URL,带热词增强
AudioResponse resp = client.audio().transcribe(
AudioRequest.builder()
.fileUrl("https://oss.example.com/audio/query.wav")
.language("zh")
.hotwords(Arrays.asList(
new Hotword("后备母猪", 5),
new Hotword("死淘率", 4)
))
.build());
System.out.println("转写结果: " + resp.getText());
System.out.println("语言: " + resp.getLanguage());
System.out.println("时长: " + resp.getDuration() + "s");
for (Sentence s : resp.getSentences()) {
System.out.println(s.getBeginTime() + "-" + s.getEndTime() + "ms: " + s.getText());
}
AudioResponse {
text: "后备母猪期末存栏282头",
sentences: [
{ beginTime: 0, endTime: 3000, text: "后备母猪期末存栏282头" }
],
language: "zh",
duration: 3
}
AudioRequest 参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
fileUrl | String | 是 | 远程音频文件 HTTP URL(需外网可访问) |
file | Path | 否 | 本地文件路径(需先上传 OSS) |
model | String | 否 | 覆盖默认语音模型 |
language | String | 否 | 语言提示,如 "zh" |
vocabularyId | String | 否 | 预注册热词表 ID(优先级高于 hotwords) |
hotwords | List<Hotword> | 否 | 内联热词列表,每个含 text / weight(1-5, 默认4) / lang |
enableDiarization | boolean | 否 | 说话人分离(仅异步接口支持) |
enableTimestamp | boolean | 否 | 时间戳(仅异步接口支持) |
fileUrl 必须是外网可访问的公网 URL(如阿里云 OSS 公网地址),DashScope 服务端拉取音频文件。内网地址 / localhost / 需鉴权的 CDN 会导致转写失败。
AudioResponse 字段
| 字段 | 类型 | 说明 |
|---|---|---|
text | String | 完整转写文本 |
sentences | List<Sentence> | 分句结果,含 beginTime(ms) / endTime(ms) / speakerId / emotion |
language | String | 识别出的语言代码 |
duration | Long | 音频时长(秒) |
热词管理
支持预注册热词表提升农业领域专有名词识别率。
// 创建热词表
String vocabId = client.audio().createVocabulary(
VocabularyRequest.builder()
.name("pig-farm-terms")
.model("fun-asr")
.hotwords(Arrays.asList(
new Hotword("后备母猪", 5),
new Hotword("死淘率", 4)
))
.build());
// 转写时引用
AudioResponse resp = client.audio().transcribe(
AudioRequest.builder()
.fileUrl("https://oss.example.com/audio/query.wav")
.vocabularyId(vocabId)
.build());
// 管理热词表
client.audio().listVocabularies(); // 查询全部
client.audio().deleteVocabulary(vocabId); // 删除
附件集成(金蝶云苍穹后端 · 照着实现)
file_id 是不透明串,前端只原样透传、不解析、不拼接 URL。
整体链路
前端 业务后端(苍穹) SDK DeerFlow
│ ① 选文件+提问 ────►│ /ai/chat │ │
│ (multipart) ├─ Content.inputFile(流,名) ──► createStreamAsSse ────► /uploads + runs/stream
│ ◄── ② SSE 直通 ────┤ (SDK 的 SSE 字节直接写回) │ (含附件 annotation 帧) │
│ │ │ │
│ ③ 点附件(file_id)─►│ /ai/file?fileId=&mode= ─────► files().download() ───► GET /artifacts/{path}
│ ◄── ④ 字节+头 ─────┤ (设 Content-Disposition) │ FileDownload(流) │
业务后端需提供 2 个 HTTP 接口(契约)
| 接口 | 方法 / 参数 | 响应 | 用途 |
|---|---|---|---|
/ai/chat | POST · multipart:message 文本、file 文件(可选)、previousResponseId(可选) | text/event-stream(SDK SSE 直通) | 带附件提问 + 流式回答;附件以 annotation 帧下发 |
/ai/file | GET · fileId(必填,前端从 annotation 透传)、mode=preview|download | 文件字节 + Content-Type + Content-Disposition | 下载/预览附件(上传文件与生成文件通用) |
接口一:/ai/chat — 上传 + 流式对话(SSE 直通)
用 createStreamAsSse 把 SDK 的 SSE 字节直接写进响应流,前端即可收到全部事件(含附件 annotation)。
用户选的文件用 Content.inputFile(流, 文件名) 内联到对话,SDK 自动上传到本次对话 thread,无需手动管 thread。
// 在自定义 WebApi/服务的 doService(req, resp) 中(苍穹提供 javax.servlet 风格 req/resp) public void doChat(HttpServletRequest req, HttpServletResponse resp) throws IOException { // 1) 解析 multipart:文本 + 可选文件(苍穹/Commons-FileUpload 均可,这里示意) String message = req.getParameter("message"); String prevRespId = req.getParameter("previousResponseId"); // 续话可空 Part filePart = req.getPart("file"); // 可空 // 2) 组装对话内容:有文件则内联上传(SDK 自动同 thread) List<Content> contents = new ArrayList<>(); if (filePart != null) { contents.add(Content.inputFile(filePart.getInputStream(), filePart.getSubmittedFileName())); } contents.add(Content.text(message)); ResponseRequest aiReq = ResponseRequest.builder() .agent("doc-qa") .input(Arrays.asList(Message.user(contents))) .previousResponseId(prevRespId) // null=新会话 .mode("pro") .build(); // 3) SSE 直通:SDK 把符合 OpenAI 规范的 SSE 字节写进 resp.getOutputStream() resp.setContentType("text/event-stream;charset=UTF-8"); resp.setHeader("Cache-Control", "no-cache"); resp.setHeader("X-Accel-Buffering", "no"); // 关闭代理缓冲,保证实时 client.responses().createStreamAsSse(aiReq, resp.getOutputStream()); }
response.output_text.annotation.added 帧(含 file_id/filename)即代表"有附件生成"。具体前端解析见 SSE 事件参考 · 附件。接口二:/ai/file — 下载 / 预览代理
前端把 annotation 里的 file_id 原样传来;后端用 files().download(fileId) 取字节回吐。预览与下载只差 Content-Disposition。
public void doFile(HttpServletRequest req, HttpServletResponse resp) throws IOException { String fileId = req.getParameter("fileId"); boolean wantPreview = "preview".equals(req.getParameter("mode")); try (FileDownload dl = client.files().download(fileId)) { // Content-Type resp.setContentType(dl.getContentType() != null ? dl.getContentType() : "application/octet-stream"); if (dl.getContentLength() >= 0) { resp.setHeader("Content-Length", String.valueOf(dl.getContentLength())); } // 预览仅对可内联类型生效;活动内容(html/svg)isInline()=false,强制下载防 XSS boolean inline = wantPreview && dl.isInline(); String disposition = (inline ? "inline" : "attachment"); // 中文文件名用 RFC 5987 filename*,避免乱码 String enc = URLEncoder.encode(dl.getFilename(), "UTF-8").replace("+", "%20"); resp.setHeader("Content-Disposition", disposition + "; filename*=UTF-8''" + enc); // 流式拷贝(不全载内存) byte[] buf = new byte[8192]; int n; InputStream in = dl.getStream(); OutputStream out = resp.getOutputStream(); while ((n = in.read(buf)) > 0) out.write(buf, 0, n); out.flush(); } catch (ResourceNotFoundException e) { resp.sendError(404, "file not found"); // 文件不存在 } // try-with-resources 自动 close FileDownload,释放底层连接 }
上传的另一种姿势(先上传后引用)
若需先单独上传、之后多轮引用:用 files().upload() 拿 file_id,但后续对话必须带
previousResponseId 保证同一 thread,否则 Agent 读不到文件。多数场景用上面的"对话内联"更简单。
FileObject f = client.files().upload(FileUploadRequest.builder() .stream(filePart.getInputStream(), filePart.getSubmittedFileName()) .agent("doc-qa").build()); String fileId = f.getId(); // 可下载、可在后续对话用 Content.inputFile(fileId)+previousResponseId 引用
关键坑点
- 必须 close:
FileDownload用 try-with-resources,否则泄漏底层连接。 - 中文文件名:用
Content-Disposition: filename*=UTF-8''<urlencoded>,不要用裸filename=。 - 预览安全:只在
isInline()==true时用 inline;HTML/SVG 等活动内容isInline()返回 false,强制 attachment,避免脚本在你的域执行。 - SSE 不缓冲:网关/Nginx 关闭对
/ai/chat的响应缓冲(X-Accel-Buffering: no),否则前端收不到实时增量。 - file_id 黑盒:前端不要解析/缓存为路径;它由 SDK 编码、跨会话有效期以上游为准。
- 同 thread:续话与"先上传后引用"都靠
previousResponseId串起同一 thread。
Assistants — Agent 管理
通过 XnAiClient.assistants() 获取 Assistants 实例,管理 Agent 的创建、列表、获取、修改、删除。
接口形态与字段命名对齐 OpenAI Assistants API 规范,
业务系统只依赖 Assistants 接口,切换 AI 平台无需改代码。
DeerFlow 字段映射
SDK 在 Provider 层完成 OpenAI 接口与 DeerFlow /api/agents 端点之间的转换:
| SDK(OpenAI 规范) | DeerFlow 存储 | 说明 |
|---|---|---|
id(如 asst_my-agent) | agent 目录名 | SDK 自动编解码,业务系统只看到 ID |
name | agent 目录名 | create 时必填 |
instructions | SOUL.md 文件 | 系统提示词,create/modify 可选 |
description | config.yaml → description | create/modify 可选 |
model | —(不使用) | DeerFlow 由 Agent 配置决定模型,SDK 接受但不发送 |
metadata.skills | config.yaml → skills[] | 逗号分隔字符串 ↔ 数组,参见 skills 配置 |
createdAt | —(SDK 生成) | Unix 秒时间戳,SDK 本地填充 |
Dify 行为
Dify 不支持 Agent 管理 API,engine=dify 时所有方法抛出 UnsupportedOperationException,消息指明使用 Dify 管理后台。
assistants.create
创建新 Agent。name 必填(DeerFlow 以此作为 agent 目录名),instructions / description 可选。
最简创建
Assistant a = client.assistants().create( AssistantRequest.builder() .name("my-agent") .instructions("You are a helpful assistant.") .build()); // → POST /api/agents body: {name, soul} // → 返回 Assistant: id="asst_my-agent", instructions="You are..."
带描述和 skills
Assistant a = client.assistants().create( AssistantRequest.builder() .name("pig-farm-expert") .description("猪场成本分析专家") .instructions("你是资深猪场成本分析专家,基于数据驱动决策。") .putMetadata("skills", "pig-farm-cost-analysis,data-analysis,deep-research") .build()); // config.yaml 结果: // name: pig-farm-expert // description: 猪场成本分析专家 // skills: // - pig-farm-cost-analysis // - data-analysis // - deep-research
异常
| 条件 | 异常 |
|---|---|
name 为空 | ValidationException |
| name 已存在 | ValidationException(后端 422) |
| 未认证 | AuthenticationException(后端 401) |
| agents_api 未开启 | AuthorizationException(后端 403) |
assistants.list
列出当前用户所有 Agent,返回 OpenAI list 格式。
AssistantListResponse list = client.assistants().list(); // → GET /api/agents // → {"object":"list","data":[...],"first_id":"asst_...","last_id":"asst_...","has_more":false} System.out.println("共 " + list.getData().size() + " 个 Agent"); for (Assistant a : list.getData()) { System.out.println(a.getId() + " | " + a.getName() + " | instructions=" + (a.getInstructions() != null ? a.getInstructions().substring(0, Math.min(40, a.getInstructions().length())) : "null")); // skills 透传在 metadata 中 String skills = a.getMetadata().get("skills"); if (skills != null) System.out.println(" skills: " + skills); }
assistants.retrieve
获取单个 Agent 的完整信息(含 instructions、metadata.skills)。
Assistant a = client.assistants().retrieve("asst_my-agent"); // → GET /api/agents/my-agent System.out.println(a.getId()); // "asst_my-agent" System.out.println(a.getName()); // "my-agent" System.out.println(a.getDescription()); // "猪场成本分析专家" System.out.println(a.getInstructions()); // SOUL.md 内容 System.out.println(a.getMetadata().get("skills")); // "skill-a,skill-b"
异常
| 条件 | 异常 |
|---|---|
| Agent 不存在 | ResourceNotFoundException(后端 404) |
ID 格式非法(不由 asst_ 开头) | ValidationException |
assistants.modify
修改 Agent,partial update——只更新显式设置的字段,省略的字段保持原值。
修改 soul(instructions)
// 只改系统提示词,其他不动 Assistant updated = client.assistants().modify( "asst_my-agent", AssistantRequest.builder() .instructions("你是养猪专家,用通俗易懂的中文回答所有问题。") .build()); // → PUT /api/agents/my-agent body: {soul: "你是养猪专家..."}
同时修改 description 和 instructions
Assistant updated = client.assistants().modify( "asst_my-agent", AssistantRequest.builder() .description("更新后的描述") .instructions("新的系统提示词") .build()); // → PUT /api/agents/my-agent body: {description, soul}
assistants.delete
删除 Agent。幂等——Agent 不存在时视为成功,不抛异常。
// 安全删除:先尝试删,不存在也不报错 client.assistants().delete("asst_my-agent"); // → DELETE /api/agents/my-agent // 完整流程:创建 → 使用 → 清理 String testId = "asst_demo-test"; try { client.assistants().delete(testId); } catch (Exception e) {} Assistant a = client.assistants().create(...); // ... 测试 ... client.assistants().delete(testId);
skills 配置
skills 是 DeerFlow agent config.yaml 中的标准字段(字符串数组)。
SDK 通过 metadata 的 "skills" 键透传——逗号分隔的字符串 ↔ YAML 数组。
| 操作 | metadata.skills 值 | DeerFlow config.yaml |
|---|---|---|
| 设置 | "skill-a,skill-b" | skills: [skill-a, skill-b] |
| 追加 | "skill-a,skill-b,skill-c" | skills: [skill-a, skill-b, skill-c] |
| 清空 | "" | skills: [] |
| 不传 | 不设此 key | skills 字段不修改(保持原值) |
创建时设置 skills
Assistant a = client.assistants().create( AssistantRequest.builder() .name("cost-analyst") .instructions("你是成本分析专家。") .putMetadata("skills", "pig-farm-cost-analysis,data-analysis,deep-research") .build()); // config.yaml: // skills: // - pig-farm-cost-analysis // - data-analysis // - deep-research
修改 skills
// 更新 skills 列表 client.assistants().modify("asst_cost-analyst", AssistantRequest.builder() .putMetadata("skills", "xn-api,pig-farm-metrics") .build()); // 清空 skills client.assistants().modify("asst_cost-analyst", AssistantRequest.builder() .putMetadata("skills", "") .build());
读取 skills
Assistant a = client.assistants().retrieve("asst_cost-analyst"); String skills = a.getMetadata().get("skills"); // → "xn-api,pig-farm-metrics"(或 null 表示后端未返回)
SSE 事件总览
所有事件由 ResponseStreamEvent.Type 枚举定义,sseName() 返回 OpenAI 兼容字符串。
type() 的 switch 必须带 default 分支,
并把未知事件视为无害进度信号。
生命周期事件
| 枚举 | SSE 名 | 触发时机 |
|---|---|---|
| RESPONSE_CREATED | response.created | 分配 response_id 的瞬间,流的第一个事件。 |
| RESPONSE_IN_PROGRESS | response.in_progress | 进入内容生成阶段。 |
| RESPONSE_COMPLETED | response.completed | 正常结束,含完整 output 与 usage。 |
| RESPONSE_FAILED | response.failed | 服务端业务失败。 |
| RESPONSE_INCOMPLETE | response.incomplete | 触发 max_tokens 截断等提前终止。 |
| ERROR | error | 网络层 / SDK 异常,含 error()。 |
输出项事件
| 枚举 | SSE 名 | 说明 |
|---|---|---|
| OUTPUT_ITEM_ADDED | response.output_item.added | 新 OutputItem 开始(含类型与 id,内容暂空)。event.outputIndex() 为该 item 在 output[] 中的真实位置(从 0 递增)。 |
| OUTPUT_ITEM_DONE | response.output_item.done | 当前 OutputItem 全部内容接收完毕。event.item() 含完整内容。 |
| CONTENT_PART_ADDED | response.content_part.added | item 内新增内容片段。 |
| CONTENT_PART_DONE | response.content_part.done | 当前内容片段结束。 |
文本与推理事件
| 枚举 | SSE 名 | 说明 |
|---|---|---|
| OUTPUT_TEXT_DELTA | response.output_text.delta | 文本增量,delta() 取片段。event.item().getId() 为所属 MessageItem 的真实 id(非固定 msg_0),event.outputIndex() 为该 item 在 output[] 中的位置,event.contentIndex() 为 content part 序号。 |
| OUTPUT_TEXT_DONE | response.output_text.done | 当前文本片段结束,delta() 是完整文本(SSE payload 字段名为 text)。event.item()、event.outputIndex() 与对应 DELTA 事件一致。 |
| OUTPUT_TEXT_ANNOTATION_ADDED | response.output_text.annotation.added | 引用 / 注释 / 高亮。 |
| REFUSAL_DELTA | response.refusal.delta | 拒答增量。 |
| REFUSAL_DONE | response.refusal.done | 拒答结束。 |
| REASONING_TEXT_DELTA | response.reasoning_text.delta | 推理文本增量(思维链)。 |
| REASONING_SUMMARY_TEXT_DELTA | response.reasoning_summary_text.delta | 推理摘要增量。 |
| REASONING_SUMMARY_TEXT_DONE | response.reasoning_summary_text.done | 推理摘要结束。 |
工具调用事件
| 枚举 | SSE 名 | 说明 |
|---|---|---|
| FUNCTION_CALL_ARGUMENTS_DELTA | response.function_call_arguments.delta | 工具入参 JSON 字符串增量。payload: item_id, output_index, delta。 |
| FUNCTION_CALL_ARGUMENTS_DONE | response.function_call_arguments.done | 入参完整,可执行工具。payload: item_id, output_index, arguments。 |
| WEB_SEARCH_CALL_SEARCHING | response.web_search_call.searching | 联网搜索开始。 |
| WEB_SEARCH_CALL_COMPLETED | response.web_search_call.completed | 联网搜索结束。 |
| WEB_SEARCH_CALL_FAILED | response.web_search_call.failed | 联网搜索失败。 |
| FILE_SEARCH_CALL_SEARCHING | response.file_search_call.searching | 文件搜索开始。 |
| FILE_SEARCH_CALL_COMPLETED | response.file_search_call.completed | 文件搜索结束。 |
| CODE_INTERPRETER_CALL_IN_PROGRESS | response.code_interpreter_call.in_progress | 代码解释器准备执行。 |
| CODE_INTERPRETER_CALL_CODE_DELTA | response.code_interpreter_call_code.delta | 代码片段增量。 |
| CODE_INTERPRETER_CALL_CODE_DONE | response.code_interpreter_call_code.done | 代码完整。 |
| CODE_INTERPRETER_CALL_INTERPRETING | response.code_interpreter_call.interpreting | 代码执行中。 |
| CODE_INTERPRETER_CALL_COMPLETED | response.code_interpreter_call.completed | 代码执行完成。 |
典型工具调用事件序列
RESPONSE_CREATED RESPONSE_IN_PROGRESS OUTPUT_ITEM_ADDED ← 工具调用 item 开始 FUNCTION_CALL_ARGUMENTS_DELTA × N FUNCTION_CALL_ARGUMENTS_DONE ← 在此执行实际工具 OUTPUT_ITEM_DONE OUTPUT_ITEM_ADDED ← 文本 item 开始 CONTENT_PART_ADDED OUTPUT_TEXT_DELTA × N OUTPUT_TEXT_DONE CONTENT_PART_DONE OUTPUT_ITEM_DONE RESPONSE_COMPLETED
对象:ResponseStreamEvent
每个流式事件投递到 StreamCallback.onEvent(ResponseStreamEvent),全部字段通过 getter 访问:
| Getter | 类型 | 说明 |
|---|---|---|
| type() | ResponseStreamEvent.Type | 事件枚举,同时含 sseName() 返回 OpenAI 兼容字符串。 |
| item() | OutputItem | 与本事件关联的输出项(ITEM_ADDED/DONE、TEXT_DELTA/DONE 等携带)。 |
| outputIndex() | Integer | item 在 Response.output[] 中的位置(从 0 递增);非 item 事件为 null。 |
| contentIndex() | Integer | content part 在 item 内的序号;仅 TEXT_DELTA/DONE、CONTENT_PART_* 事件携带。 |
| delta() | String | 文本片段(TEXT_DELTA);OUTPUT_TEXT_DONE 时为完整文本(SSE payload 字段名 text)。 |
| toolCall() | FunctionCall | 工具调用信息(FUNCTION_CALL_ARGUMENTS_* 事件携带);含 id / name / arguments。 |
| response() | Response | 完整响应对象(RESPONSE_CREATED / COMPLETED / FAILED 等生命周期事件携带)。 |
| error() | ResponseError | 错误详情(ERROR 事件携带);含 getMessage() / getOpenAiErrorCode()。 |
| rawData() | Object | 上游原始 JSON 节点(CustomItem 场景使用)。 |
把 SSE 转发到前端
把 createStreamAsSse 写入 HttpServletResponse.getOutputStream(),前端用任意 OpenAI 兼容 SDK 直连即可。详细的前端样例见 SSE 协议教程。
对象:Response
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 对外不透明 ID(由 SDK 编码)。 |
| object | string | 恒为 "response"。 |
| status | string | in_progress / completed / incomplete / failed。 |
| model | string | 实际使用的模型名。 |
| output | List<OutputItem> | 输出项,详见。 |
| outputText | string | 所有 MessageItem 中文本拼接的便捷视图。 |
| usage | Usage | token 统计。 |
| createdAt | Instant | Unix 时间。 |
| previousResponseId | string | 上轮 ID(请求里传过的)。 |
| metadata | Map | 原样回写。 |
| error | ResponseError | status=failed 时含 code/message/param/type。 |
| incompleteDetails | IncompleteDetails | status=incomplete 时含原因。 |
| reasoning | Reasoning | 推理过程(开启 thinking 时)。 |
| text / tools / toolChoice / truncation / temperature / topP / maxOutputTokens / parallelToolCalls | — | 原样回显请求参数。 |
对象:OutputItem
抽象基类,按 type 区分:
| 子类 | type | 关键字段 |
|---|---|---|
| MessageItem | message | role / content(含 OutputTextContent) |
| ReasoningItem | reasoning | summary / 推理文本 |
| ToolCallItem | function_call | name / arguments / call_id / status |
| CustomItem | 其它 | 原始 JSON 直通 |
对象:Usage
| 字段 | 说明 |
|---|---|
| inputTokens | 输入 token 数。 |
| outputTokens | 输出 token 数。 |
| totalTokens | 合计。 |
| inputTokensDetails / outputTokensDetails | 明细(缓存命中、推理 token 等,按引擎而定)。 |
对象:FileObject
| 字段 | 类型 | 说明 |
|---|---|---|
| id | string | 统一文件 ID。 |
| filename | string | 原文件名。 |
| bytes | long | 字节数。 |
| purpose | string | 用途标签。 |
| createdAt | Instant | 创建时间。 |
对象:FileDownload
files().download() 的返回值,实现 Closeable,用完必须 close(释放底层连接)。
| 方法 | 类型 | 说明 |
|---|---|---|
| getStream() | InputStream | 字节流;调用方读取并最终 close。 |
| getFilename() | string | 文件名(basename),用于 Content-Disposition。 |
| getContentType() | string | MIME,可空。 |
| getContentLength() | long | 字节数;未知为 -1。 |
| isInline() | boolean | 预览提示:可安全内联为 true;活动内容(HTML/SVG/XHTML)为 false(建议作附件下载)。 |
对象:FileLimits
| 字段 | 类型 | 说明 |
|---|---|---|
| maxFiles | int | 单会话最大文件数。 |
| maxFileSizeBytes | long | 单文件最大字节数。 |
| maxTotalBytes | long | 单会话累计最大字节数。 |
| allowedMimeTypes | List<String> | MIME 白名单;空表示不限制。 |
错误处理
所有异常继承 AiPlatformException。建议 catch 基类后按子类细分:
| 异常 | 典型 HTTP | 场景 |
|---|---|---|
| ValidationException | —(本地校验) | 请求参数非法(缺 agent / input、文件超限等)。 |
| AuthenticationException | 401 | 账号密码 / API Key 无效。 |
| AuthorizationException | 403 | 已登录但无权限访问该 Agent。 |
| ResourceNotFoundException | 404 | responseId / fileId 不存在。 |
| RateLimitException | 429 | 上游限流,可读取 retryAfterSeconds。 |
| NetworkException | — | 连接 / 超时 / 协议错误。 |
| ProviderException | 4xx/5xx | 未匹配前述类型的上游错误,getCode() / getStatus() 携带原始信息。 |
try { Response r = client.responses().create(req); } catch (RateLimitException e) { Thread.sleep(e.getRetryAfterSeconds() * 1000L); } catch (AuthenticationException e) { client.refreshAuth(); } catch (AiPlatformException e) { log.error("AI 调用失败 code={}", e.getCode(), e); }
小欣AI·服务台