返回博客

AI Agent 部署完怎么对话?怡途Claw 流式对话 API 发消息到实时打字实战

2026年7月26日阅读约 24 分钟
AI Agent 部署完怎么对话?怡途Claw 流式对话 API 实战

部署好 AI Agent 只是第一步,真正难的是把对话接进自家 App。本文用怡途Claw 的 /api/app/chat/* 接口,把发消息、SSE 长连接、实时打字事件、工具调用卡片、移动端 slim 流量优化串成一条线,附 delta 累计全文坑和断线重连。

部署好一个 AI Agent,控制台里能跑能答,离"接进自家 App 让用户用上"还差着最硬的一段路:对话怎么调。不少团队第一反应是轮询——发完消息每秒 GET 一次拿结果,能跑起来,但 agent 一边思考一边吐字的过程全丢了,用户看到的是"转圈 30 秒、啪一下整段蹦出来",体验和 ChatGPT 差一个时代。怡途Claw 的 /api/app/chat/* 这组接口默认就是 SSE 长连接,这篇文章把"发消息 -> 接流 -> 解析实时打字 -> 渲染工具调用 -> 移动端省流量 -> 断线重连和取消"串成一条线,每个请求都带真实字段和踩坑点。

所有接口都要 JWT,放 Authorization: Bearer <token>,和创建实例、查计费是同一套登录态。下面假设你已经照着部署那篇建好实例、拿到 instanceId 和 token。

一、发消息:POST /messages

POST /api/app/chat/instances/:instanceId/messages
Content-Type: application/json
Authorization: Bearer <token>
{
  "content": "帮我查一下本月用量最高的实例",
  "sessionId": "main",
  "waitForReply": false
}

sessionId 不传默认 main,多轮对话用不同 id 隔离历史。waitForReplyfalse 就行——给 true 会让这个 HTTP 请求一直挂着等 agent 跑完才返回,长任务几十秒起步,网关早超时了,正确做法是发完立刻拿 streamUrl 去接流。响应长这样:

{
  "success": true,
  "data": {
    "sessionId": "main",
    "pending": true,
    "runId": "9c43a6f5-b432-4d5f-a17e-9f0dfb5c464a",
    "userMessage": { "id": "example-user-message" },
    "assistantMessage": null,
    "streamUrl": "/api/app/chat/instances/<instanceId>/stream?sessionId=main&afterEventSeq=31"
  }
}

runId 是这一次对话的执行 id,后面要取消就靠它。streamUrl 里已经把 afterEventSeq=31 带好了,直接拼上 host 去连,别自己从头算游标。

二、接流:这是 SSE 长连接,不是轮询

GET /api/app/chat/instances/:instanceId/stream?sessionId=main&slim=1
Accept: text/event-stream
Authorization: Bearer <token>

从 2026-07 起,这个接口的默认响应格式就是 clawpilot SSE(text/event-stream,header 带 X-Accel-Buffering: no 禁掉 Nginx 缓冲),不用传任何参数。App 端要用 SSE 客户端消费,不是每秒 GET 一次的轮询。老的 JSON 轮询格式得显式传 ?format=legacy 才走,每次还把最近 100 条消息全量重传一遍,移动端流量重、渲染慢,新接入别再用了。

afterEventSeq 是断线重连的游标:首次连接传 0,服务端从当前 run 开始回放;断线重连时传上一次收到的最大 seq,不会漏事件。想压流量就加 ?slim=1(见第五节),但得先把客户端适配好再开,不然会看到文字乱序。

三、chat 事件:一个状态机,delta 最容易踩坑

SSE 帧长这样:

event: chat
data: { 一段 JSON }

event: chat 是主对话流,靠 data.state 区分阶段:

state触发App 该做
started一次 run 开始新建回复气泡,显示 loading
deltaagent 在实时打字更新气泡文本(见下面坑)
finalrun 成功结束用 final 内容覆盖气泡,停 loading
aborted被中断标"已中断",停 loading
error运行失败显示错误,停 loading

delta 这条有三个坑,全踩过:

  1. data.message.content[0].text 默认是累计全文,不是增量。App 直接 bubble.text = text 替换,别 +=,否则同一段话会重复拼好几遍。只有开了 ?slim=1 且这帧带 patch:true 时,text 才是新增片段,这时候才用 +=
  2. delta 可能只带 loading:true、没有 message 字段(agent 还在思考、没开始吐字)。直接去取 message.content[0] 会崩,先判空。
  3. message.content 是数组final 里会混 type:'text'type:'tool_call' 两种块。取正文要按 type 过滤,别无脑取 [0]

final 到了就别再用 delta 的文本了——final 是定型的完整快照,拿它覆盖一次,能修掉 delta 过程里偶尔的乱序。

四、agent 事件:思考过程和工具调用走这条侧信道

光有正文还不够,agent 真正在干活(思考、调工具、拿结果)的过程走的是 event: agent

streamphase内容App 该做
assistant-reasoning / 思考塞进气泡下的"思考中"折叠区
toolstartedtoolCallId / name / args建 tool 卡片,标 running
toolresulttoolCallId / result按 toolCallId 找到卡片,写结果,标完成

工具卡片用 toolCallId 配对 started 和 result,别用 name——同一个 run 里 agent 可能调两次 bash,name 一样但 id 不同。assistant 流里的 data.text 是思考过程的灰字,和正文分开渲染,别混到主回复里。

这套事件齐了,App 端才能复刻 openclaw 那种"思考中 -> 正在执行 bash -> 拿到结果 -> 继续打字"的过程,而不是干等一个 final。

五、slim=1:长回答和工具输出别把流量放成 O(N²)

默认协议下,delta 每次推的都是自开头累计的全文——回答越长,后面每一帧的体积越大,整体流量是 O(N²)。一篇两千字的回答,到尾巴上每帧都在重传前面一千多字。工具输出更夸张:Bash / Read / Grep 动辄几十到几百 KB,一帧塞一个巨型 JSON,手机端 JSON.parse 直接卡主线程掉帧。

?slim=1 后语义变(其他事件不变):

  • deltapatch:true,text 变成真增量,accumulated += text 拼接。长回答流量从 O(N²) 降到 O(N),压测收益 80% 以上。
  • 单个 tool 的 argsresult 超 4KB 时,不再一帧塞完,改成 args_chunk / result_chunk 分片,每片配 chunkFinal。App 按 toolCallId 累积,chunkFinal:trueJSON.parse。200KB 的 grep 输出拆成约 50 个 4KB 小帧,边收边渲染,不再卡死。

坑写在前面:先把 slim 的新字段适配好再传 ?slim=1。没适配就开,会直接看到文字乱序、工具结果不显示——这不是后端的 bug,是客户端漏了 patch:true 和分片分支。

六、断线重连和中途取消

断线了别从头来,带上一次的最大 seq 重连:

GET /api/app/chat/instances/:instanceId/stream?sessionId=main&afterEventSeq=38&slim=1
Authorization: Bearer <token>

注意 seq 不保证连续——可能从 31 直接跳到 38,中间的号是服务端内部事件或被过滤掉的,不算丢事件。只要 lastEventSeq 往前走了,本地游标就跟着更新,哪怕这帧 events 是空的。

要中断正在跑的 run:

DELETE /api/app/chat/instances/:instanceId/runs/:runId
Authorization: Bearer <token>

runId 就是发消息时返回那个。删成功回 {"success":true,"message":"已取消运行"};run 已经结束了会回 404 没有正在运行的请求或已结束,App 端把这俩都当"已结束"处理就行,别把 404 当真错误弹给用户。

小结

把这条线接通,App 端就能像 ChatGPT 一样看到 agent 实时打字和工具执行:POST /messages 发消息拿 streamUrlGET /stream 当 SSE 长连接消费、event:chatdelta 驱动打字(默认替换、slim 下累加)、event:agent 渲染思考和工具卡片、移动端开 ?slim=1 省流量、afterEventSeq 断线重连、DELETE /runs/:runId 中途取消。接入时十有八九会踩的同一个坑:客户端漏了 delta 分支,于是用户看不到实时打字、干等一个 final 蹦出来——对照第三节那三个坑逐条查,基本就通了。