[Bug] Agent 工具输出含空字节时 tool_result 持久化失败并无限重试,本轮后续回复丢失

当 Agent 工具输出包含 NUL 字节( \x00 ,常见于 curl 拉取到 ZIP 等二进制后又被当作文本处理)时, tool_result 写入 PostgreSQL 会因 invalid byte sequence for encoding "UTF8": 0x00 而失败,导致工具消息

快速结论:当 Agent 工具输出包含 NUL 字节(\x00,常见于 curl 拉取到 ZIP 等二进制后又被当作文本处理)时,tool_result 写入 PostgreSQL 会因 invalid byte sequence for encoding "UTF8": 0x00 而失败,导致工具消息 content 为空、无限重试刷日志,并连带丢失本轮后续 assistant 回复。优先排查工具输出是否含二进制/空字节,以及持久化写入路径是否做了 UTF-8 清洗。

适用环境:LobeChat Web 端(桌面 Chrome);macOS 客户端;自托管 Docker 部署(镜像 lobehub/lobehub,版本 v2.2.17,2026-10-02 拉取);PostgreSQL 17。Issue 明确指出该问题为服务端持久化 bug,与客户端/浏览器无关。

最快修复方案:暂无确认的一步修复方案。Issue 中的定位与修复建议(在 MessageModel.updateToolMessage / HeterogeneousPersistenceHandler 写入前接入现有 sanitizeUTF8 清洗,并为持久化重试加上限)尚未合入主线,仅在贡献者分支的对比提交中验证。

注意事项:清洗方案需注意不能破坏源文本——修复分支的回归测试特意保留了“字面反斜杠 + u0000”这类合法文本。贡献者提供的验证仅为数据库层测试(新回归用例复现 SQLSTATE 22021,接入清洗后通过),渲染后的工具结果与父级回复在真实产品中的表现仍待验收,因此不能据此断定 UI 层问题已解决。此外失败 payload 会原样写入容器日志,任何“读取容器日志”的后续工具调用会再次携带 \x00,形成自我传播。

问题场景

在自托管 Docker + PostgreSQL 17 的 LobeChat v2.2.17 中,让 Agent 通过 Bash 工具用 curl 请求一个在部分请求下实际返回 ZIP 二进制的 URL(例如 daily treasury rates 的 CSV 端点)。二进制被当作文本输出,大量 \x00 随 intent.content 直接进入 updateToolMessage 的 content 参数,触发服务端持久化失败,并表现为工具消息 content 为空、UI 上报错、本轮后续 assistant 回复未能入库、话题卡住。

报错原文

Error in tRPC handler (lambda) on path: aiAgent.heteroIngest, type: mutation
Error [TRPCError]: Failed to persist tool_result for message msg_xxx
  [cause]: Error: Failed to persist tool_result for message msg_xxx
  [cause]: error: invalid byte sequence for encoding "UTF8": 0x00
    code: '22021',
    where: 'unnamed portal parameter $1',
    file: 'mbutils.c',
    routine: 'report_invalid_encoding_int'

原因分析

可能原因(Issue 已定位到具体代码路径):PostgreSQL 的文本字段不能存储 NUL 字节,而 MessageModel.updateToolMessage 在写入工具输出时绕过了已有的空字节清洗。按 Issue 描述,消息插入路径会对 content 做清洗,但工具结果更新路径把原始输出直接写入,导致该行更新事务回滚,tool 消息 content 始终为空。评论摘要也确认了这一点:更新路径绕过了 sanitizeUTF8,PostgreSQL 拒绝 NUL,进而回滚持久化事务。

仓库中已有现成清洗工具 packages/utils/src/sanitizeUTF8.ts(剔除 \u0000、控制字符、孤立代理对,含单测),但此前只用于 apps/server/src/routers/async/file.ts 与已废弃的 dataImporter,未接入 heterogeneous agent 持久化路径。另外,持久化失败缺少重试上限,且单条消息失败会阻断本轮后续 assistant 回复的持久化。同类 PG 报错曾在知识库分片路径出现(#3663 / #3551 / #4975),本 Issue 是 Agent 工具结果持久化路径上的新问题。

环境排查

  • LobeChat 版本:是否 v2.2.17,镜像来源与拉取时间(Issue 为 2026-10-02 拉取的 lobehub/lobehub)。
  • 部署方式:自托管 Docker,服务端日志是否出现 aiAgent.heteroIngest 路径下的 Failed to persist tool_result。
  • 数据库:PostgreSQL 17,确认报错包含 invalid byte sequence for encoding "UTF8": 0x00 与 SQLSTATE 22021。
  • 工具输出:触发工具(如 Bash + curl)的返回内容是否实际为二进制(ZIP)而非文本,输出中是否含 \x00。
  • 重试与日志:同一条报错是否在日志中大量重复,重试是否无上限。

解决步骤

  1. 先确认故障链:在服务端日志中检索 Failed to persist tool_result 与 SQLSTATE 22021,并核对对应工具的原始输出是否含 NUL 字节或二进制内容。
  2. 临时规避(可优先尝试):调整工具调用方式,避免把二进制响应直接作为文本返回,例如在命令侧过滤或截断输出(Issue 复现命令本身用了 | head -4,但该端点在部分请求下仍返回 ZIP),或先确认响应确实是文本再交给工具输出。
  3. 注意日志自传播:失败 payload 会被原样写入容器日志,之后“读取容器日志”的工具调用可能再次携带 \x00 触发相同报错,排查期间避免直接用工具读取含该报错的大型日志。
  4. 应用修复(需等待合入或自用分支):在 MessageModel.updateToolMessage / HeterogeneousPersistenceHandler 写入 content 和 pluginError 前调用 sanitizeUTF8,或下沉到数据库层统一处理;建议同时为持久化重试增加上限,并避免单条消息失败阻断后续 assistant 回复入库。
  5. 若参考贡献者分支:对比提交范围 311459f65f71b5a66ee6d1af42d267d84b6911bf...ad32d2a093947cfba9bf6862125f4cd4eb4ec2f8 包含有界修复与回归测试,但尚未合入主线,需自行评估。

验证方法

应用清洗后,用含 NUL 字节的工具输出(如 Issue 中会返回 ZIP 的 curl 场景)重新触发 Agent 工具调用,确认:数据库 messages 表中 tool 消息 content 不再为空、不再是 0 长度;服务端日志不再反复出现 SQLSTATE 22021 报错且无重试刷屏;UI 上工具调用正常显示且话题可继续;本轮后续 assistant 最终回复成功入库。贡献者报告的回归测试做法是:原更新路径复现 22021,接入清洗后通过,并保留“字面反斜杠 + u0000”源文本不被误伤;但真实产品中工具结果渲染与父级回复的验收尚未完成,需以实际页面与数据库结果为准。

参考来源

lobehub/lobe-chat #20292

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 29345

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注