建议

  1. 主改点(云侧) :在official voice 链里让 turn 等到工具返回再收束 (或把工具等待上限从当前行为延长)——这是根因所在;
  2. 缓解(云侧) :把get_daily_report_modules_digest 的耗时压到 ~3 秒内(比如服务端缓存/预聚合),或在语音链里先给一句”
    正在查询,稍后播报”再异步回;
  3. 设备侧 :目前 没有 「finish 后立刻收会话」的代码可改(app 层、lschat 会话层都核对过),所以这条得从链路上改。

二、怎么改(按优先级,都属云侧/你这边可控)

① 框架层(治本,最该做) 把 本轮收束的条件改成”没有 pending tool call” :

  • 只要pendingToolCalls > 0 , 不允许结束 turn、不允许关闭会话 ;
  • 框架已经在做 keep-alive 了,说明它知道有 pending tool,只是 没有用它来阻止收束 。把这条接上即可。
    ② 提示词/模型层(治本,改动最小) 在official voice 链的 system prompt 里明确:
    一旦发起工具调用, 必须等工具结果返回后再作答 ;禁止在工具未返回时输出”请稍候/正在查询”作为最终答案。
    现在finalAnswerBytes=8 就是模型把”请稍候”当成了最终答案。锁死这一点,框架就不会提前收束。

③ 工具层(缓解,见效快) 把get_daily_report_modules_digest 压到 3 秒内返回(服务端缓存 / 预聚合 /
异步取数后回填)。你截图那个答案要查台账+实时七态+逐台判断,明显偏慢,正好踩中这个点。

④ 会话生命周期(兜底) 即使 ①②不做,也可以在语音链里把”工具 pending 期间”的会话设为 不可关闭 (等于给会话加一个”
任务未完成不收束”的标记)。
说明:①②③④ 都在 云端 agent / 你的 MCP server 侧,固件侧确实没有对应改动点(app 层、lschat 会话层都核对过:收到finish
只是发布事件,不会收会话)。

三、可直接转发给云端同学的问题描述 问题 :official voice 链在工具 pending 时提前收束本轮,导致工具被

AGENT_CANCELLED 。 SID :4ad7f88d772d4d7fad31fb6ca7006408 时间 :2026-10-14 17:49:12.702 工具发起 → 17:49:15/18/21 三次
response.tool_keepalive (pendingToolCalls:1)→ 17:49:22.734session.closed (disconnectOrigin:client) → 17:49:22.737
tool.failed durationMs=10034 errorCode=AGENT_CANCELLED →
react_run_failed: "Session disconnected", finalAnswerBytes:8 。 对比 :同一问题、同一 MCP server 在 网页 text 链 可完整作答。
诉求 :1) 本轮收束条件应以”无 pending tool”为前提;2) 模型在工具未返回前不得输出最终答案。 备注 :设备侧已确认 WS 未断开、系统未卡死;
totalTimeoutMs=120000 ,故非超时问题。

找到了 —— 你的候选 1(客户端 SDK)就是正解,而且能精确解释那个 10 秒

精确位置

.sdk/modules/lisa/net/websocket/porting/nopoll/lisa_websocket.c

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

`// L23-24
#define WS_PINGPONG_TIMEOUT_MS (2000) // 等一次 pong 的上限:2s
#define WS_PINGPONG_RETRY_CNT (5) // 连续丢 5 次就放弃

// L522-545 WS 收发循环里的 ping/pong 看护
if ((now - last_pong_time_ms) >= ins->pong_max_time_ms / 2) {
nopoll_conn_send_ping(ins->nopoll_conn); // 发 ping
}
if (lisa_semaphore_take(ins->pong_sem, 0) == 0) {
ins->ping_pong_lost_cnt = 0; // 收到 pong,清零
} else if ((now - last_pong_time_ms) >= (pong_max_time_ms + send_cost_time)) {
LISA_NLOGE(“recv websocket pong msg timeout, … lost cnt:%d”, ins->ping_pong_lost_cnt);
if (++ins->ping_pong_lost_cnt >= WS_PINGPONG_RETRY_CNT) {
break; // ← 放弃,跳出循环
}
}

// L560-562 跳出后直接关连接
nopoll_conn_close(ins->nopoll_conn);`

为什么是 10 秒

2s × 5 = 10s —— 连续 5 次 pong 超时(每次 2 秒)后设备侧 主动关闭 WS 。与实测的9.94~10.03s 完全吻合。

为什么之前查不到”设备侧关闭原因”

这条关闭路径是 break →nopoll_conn_close() , 不经过_ws_event_cb 的LISA_WS_ON_DISCONNECTED 分支 ,所以:

  • 设备日志里_ws_event_cb 全程只有启动 2 条 ✅(现在解释通了);
  • 云端看到的session.closed / disconnectOrigin:"client" 正是 设备侧客户端关的 ✅。

完整因果链

云端跑get_daily_report_modules_digest 期间(要查台账+实时七态+逐台判断,明显 >10s), 没能及时回设备发来的 WS ping →
设备侧连续丢 5 次 pong(10s)→ 主动关连接 → 云端记session.closed → 把仍在 pending 的工具AGENT_CANCELLED → 你只听到 3
次”请稍候”后没结果。

网页能答,是因为 text 链不依赖这个 WS 保活。

怎么改(候选 1,就是这里)

改法 说明 WS_PINGPONG_RETRY_CNT 5 → 30(或更大) 允许 2s×30 = 60s 的丢包,足够覆盖长工具调用;改动最小
WS_PINGPONG_TIMEOUT_MS 2000 → 更大 单个 pong 等待更宽松(不如调 retry 精准) 语义化 :有 pending 请求/会话进行中时
不因丢包关连接 最正确,但要改会话/连接联动

建议先做第一条(RETRY_CNT 调大),风险是”真断链”的发现会晚一点,但 60s 仍可接受。

⚠️ 注意:这是 .sdk/modules/lisa 的改动(SDK 源码,不进本仓库、升级会覆盖) ,需要按你们 SDK 的补丁流程维护;ling CLI
确实覆盖不到这层(符合你核实的结论)。

另两个候选的判定

  • 候选 2(固件收尾/断开路径) :不是。app 层和 lschat 会话层收到finish 都不会收会话(已逐个核对)。
  • 候选 3(平台 official voice 服务端另有一层 reply 超时) : 不是主因 。trace 里totalTimeoutMs=120000 ,且报错是
    AGENT_CANCELLED / "Session disconnected" (被会话断开取消),不是平台超时。
    要我把WS_PINGPONG_RETRY_CNT 改成 30 并编译验证吗?(改的是.sdk ,我会说明如何生成/回退补丁。)

副作用 :真·断链时,检测延迟从 10s 变成最长 10 分钟(这段时间 WS 线程还在空转等 pong)。

所以 建议保留重连机制 (lsc_reconnect )作为兜底——真断了靠它恢复。

这个”副作用”是什么

我们把 pong 丢包容忍从 10s 放宽到 10 分钟后: 长工具调用不会再被掐断 ,但代价是—— 如果链路是真的断了 ,WS 线程要等到”连续丢
300 次 pong(10 分钟)”才会自我了断。这 10 分钟里它一直在空转等 pong, 不会主动恢复 。

所以需要 第二条兜底:自动重连 。

好消息:重连机制已经有了,而且已启用

归档 GitHub