流式回复
一条 AI 回复是一个流:开始后,平台以机器人的身份给用户写入一条“生成中”的 AI 消息;你按顺序写入增量,平台实时推给用户;最后结束或报告失败。概念和上限见 AI 对话。
使用服务端 SDK 时不需要直接调用本页的接口:SDK 的 StartStream 返回的流对象自动合批、编号、选择写入方式和重试,Pipe* 直接把 OpenAI、Anthropic、Gemini 的流转为回复。
直接调用接口时,一条回复的完整过程如下。用户 zhangsan 刚给机器人 assistant 发了一条消息,ai.message_received 中这条消息的 seq 为 356:
# 1. 开始回复,用户的客户端立即显示“生成中”
STREAM_ID=$(curl -s -X POST "$IM_API/$ORG/$APP/ai/streams" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"bot":"assistant","to":"zhangsan","reply_to_seq":356}' | jq -r .stream.stream_id)
# 2. 把模型生成的内容写入两段,用户看到文字逐段出现
curl -s -X POST "$IM_API/$ORG/$APP/ai/streams/$STREAM_ID/deltas:batch" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"deltas":[
{"seq":1,"ops":[{"op":"append","index":0,"type":"text","text":"明天上海"}]},
{"seq":2,"ops":[{"op":"append","index":0,"type":"text","text":"**多云**。"}]}
]}'
# 3. 结束回复,平台保存最终消息并发离线推送
curl -s -X POST "$IM_API/$ORG/$APP/ai/streams/$STREAM_ID/finish" \
-H "Authorization: Bearer $APP_TOKEN" -H "Content-Type: application/json" \
-d '{"last_seq":2,"model":"gpt-4.1"}'| 要做什么 | 接口 |
|---|---|
| 开始一条回复 | 开始回复 |
| 写入模型生成的内容 | 写入增量:持续写入或按批写入 |
| 正常结束 | 结束回复 |
| 模型出错时结束 | 报告失败 |
| 查看一条回复的状态和已写入的内容 | 查询流 |
流对象
{
"stream_id": "1841000000000000123",
"bot": "assistant",
"to": "zhangsan",
"conversation_id": "1840098765432101",
"message_id": "1841000000000000124",
"seq": 358,
"client_stream_id": "req-7f3a",
"reply_to_seq": 356,
"status": "streaming",
"last_seq": 21,
"finish_reason": null,
"error": null,
"model": null,
"usage": null,
"content_bytes": 0,
"first_delta_ms": 420,
"started_at": "2026-11-02T08:00:00.000Z",
"finished_at": null
}| 字段 | 类型 | 说明 |
|---|---|---|
stream_id | String | 流 ID |
bot、to | String | 机器人和接收回复的用户 |
conversation_id、message_id、seq | — | 这条 AI 消息所在的会话、消息 ID 和它在会话中的序号 |
client_stream_id | String | 开始时给出的幂等标识 |
reply_to_seq | Number | 回复的是会话中的哪条消息 |
status | String | streaming、completed、stopped、failed |
last_seq | Number | 已接受的最后一段增量的序号;与 seq 无关 |
finish_reason、error | — | 结束原因和失败信息,见结束原因 |
model、usage | — | 结束时给出的模型名和用量 |
content_bytes | Number | 结束时保存的内容大小 |
first_delta_ms | Number | 从开始到第一段增量的毫秒数 |
started_at、finished_at | String | 时间 |
content | Array | 只在查询流且流还在生成中或结束不到 10 分钟时返回:当前累积的片段 |
开始回复
以机器人的身份给用户写入一条 status 为 streaming 的 AI 消息,用户的客户端立即显示“生成中”。没有单聊会话时创建。
/{org_name}/{app_name}/ai/streams请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
bot | String | 是 | 机器人的用户名 |
to | String | 是 | 接收回复的用户名 |
reply_to_seq | Number | 否 | 回复的是会话中的哪条消息,一般为 ai.message_received 中消息的 seq;消息中显示为引用 |
client_stream_id | String | 否 | 幂等标识,1~64 个可见 ASCII 字符。重复时返回已有的流,不论它是否已结束 |
replace | Boolean | 否 | 会话中已有正在生成的回复时,先把它以 stopped(replaced)结束;默认 false,此时返回 409 stream_in_progress |
请求示例
curl -X POST "$IM_API/$ORG/$APP/ai/streams" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bot": "assistant",
"to": "zhangsan",
"reply_to_seq": 356,
"client_stream_id": "req-7f3a"
}'响应
新建时返回 201 Created,client_stream_id 重复时返回 200 OK;响应体为 { "stream": 流对象 }。
错误
| HTTP 状态码 | code | 说明 |
|---|---|---|
| 400 | invalid_argument | bot 不是机器人(details.reason 为 not_bot)、to 是机器人(recipient_is_bot),或参数格式不对 |
| 403 | permission_denied | 机器人已停用(bot_disabled) |
| 403 | user_blocked | 用户拉黑了机器人;应用开启了单聊好友校验而双方不是好友时为 not_friend |
| 404 | not_found | 机器人或用户不存在 |
| 409 | stream_in_progress | 会话中已有正在生成的回复,details.stream_id 为它的 ID |
| 409 | limit_exceeded | 机器人同时生成的回复达到上限(bot_active_streams) |
写入增量
回复的内容按增量写入。每段增量有一个序号 seq,从 1 开始连续递增,包含一个或多个操作:
{ "seq": 12, "ops": [ { "op": "append", "index": 1, "type": "text", "text": "最高 23" } ] }op | 字段 | 说明 |
|---|---|---|
append | index、type(text 或 reasoning)、text | 向第 index 个片段追加文字;index 等于当前片段数时新建一个片段 |
put | index、part | 把第 index 个片段整体设为 part,用于图片、文件和工具调用的状态变化(如 running 改为 done);index 等于当前片段数时新建 |
- 片段只增不删,
index从 0 开始;跳过index、向非文字片段追加时返回400 invalid_argument(invalid_ops)。 - 已接受过的序号再次写入时直接跳过,不报错,所以失败后可以放心重发;序号不连续时返回
409 seq_gap,details.expected_seq为期望的下一个序号。 - 流已结束(用户停止、超时、被替换等)时写入返回
409 stream_closed,details.reason为结束后的状态,details.finish_reason为结束原因。收到后应停止调用模型。
有两种写入方式,可以交替使用。
持续写入
一个长请求:请求体是 NDJSON,每行一段增量,边生成边写;平台逐行处理,不等请求体结束,处理后在响应体中写一行确认。
/{org_name}/{app_name}/ai/streams/{stream_id}/deltas- 请求头
Content-Type: application/x-ndjson,请求体用分块传输(HTTP/1.1)或 HTTP/2。 - 平台收到请求后立即返回
200 OK和响应头,响应体也是 NDJSON:每处理完一批写一行{"acked_seq": 21};某一行出错时写一行错误{"error": {"code": "…", "message": "…", "details": {…}}}后结束,已确认的增量有效。 - 请求体结束时写最后一行确认并返回。连续
idle_timeout_seconds没有新的一行时平台结束这个请求;请求最长与一条回复的时长相同(30 分钟),不受普通接口 30 秒超时的限制。 - 一行最大 64 KB,超过时返回错误
payload_too_large(line)。 - 反向代理须关闭这个路径的请求体缓冲(nginx 为
proxy_request_buffering off),否则平台要等请求结束才能收到内容。
printf '%s\n' \
'{"seq":1,"ops":[{"op":"append","index":0,"type":"text","text":"明天上海"}]}' \
'{"seq":2,"ops":[{"op":"append","index":0,"type":"text","text":"**多云**。"}]}' |
curl -N -X POST "$IM_API/$ORG/$APP/ai/streams/1841000000000000123/deltas" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/x-ndjson" \
--data-binary @-
# {"acked_seq":1}
# {"acked_seq":2}按批写入
普通的请求,一次写入一批增量。用于运行环境不支持流式请求体(如部分云函数),或持续写入被代理断开后续写。服务端 SDK 在持续写入失败时自动改用它。
/{org_name}/{app_name}/ai/streams/{stream_id}/deltas:batch| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
deltas | Array | 是 | 增量,按序号排列;一次最多 100 段、64 KB |
成功返回 200 OK 和 { "acked_seq": 21 }。
curl -X POST "$IM_API/$ORG/$APP/ai/streams/1841000000000000123/deltas:batch" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"deltas": [
{ "seq": 3, "ops": [ { "op": "put", "index": 1, "part": { "type": "image", "url": "https://cdn.example.com/a.png", "width": 800, "height": 600 } } ] }
]
}'结束回复
全部增量确认后结束,状态为 completed。平台保存最终内容、更新消息、对不在前台的用户发离线推送,并发送 ai.stream_finished。
/{org_name}/{app_name}/ai/streams/{stream_id}/finish| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
last_seq | Number | 是 | 最后一段增量的序号,必须等于平台已接受的最后一段,否则返回 409 seq_gap,确保结束前的内容全部到达 |
finish_reason | String | 否 | 如 stop、length、tool_calls,默认 stop |
model | String | 否 | 模型名 |
usage | Object | 否 | { "input_tokens": 120, "output_tokens": 380 } |
成功返回 200 OK 和 { "stream": 流对象 }。内容审核没有通过或内容超过上限时,返回的流状态为 failed。流已结束时返回 409 stream_closed。
报告失败
模型调用失败等情况下结束回复,状态为 failed(developer),已生成的内容保留,客户端显示“生成中断”和你给出的说明。
/{org_name}/{app_name}/ai/streams/{stream_id}/fail| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
code | String | 是 | 1~64 个字符,由字母、数字、_、-、. 组成,如 model_error |
message | String | 否 | 显示给用户的说明,最多 256 个字符 |
成功返回 200 OK 和 { "stream": 流对象 };流已结束时返回 409 stream_closed。
查询流
/{org_name}/{app_name}/ai/streams/{stream_id}成功返回 200 OK 和 { "stream": 流对象 }。生成中和结束 10 分钟内带 content;之后不带,以消息正文为准(用查询单条消息的接口读取)。不存在返回 404 not_found。
