中文 English

AI 助手为什么把一句话复读四遍:一次 OpenClaw 微信通道排障复盘

发布时间: 2026-05-29
OpenClaw 微信 AI Agent 模型路由 故障排查 OpenAI兼容接口 MiniMax 工程实践

先说结论

这次问题看起来像“微信通道把同一条回复发了四遍”,但真正的重复发生在更早的位置:消息还没有进入微信发送层之前,OpenClaw Agent 的最终可见文本就已经被模型执行链路重复拼接了。排障的关键不是一上来改微信插件,而是把链路拆成发送层、会话层、模型路由层三段,用最小 prompt 复现,再比较“兼容网关路径”和“原生 provider 路径”的输出差异。最后的修复也很朴素:让个人 IM 通道显式走原生 provider,移除容易被误选的故障候选模型路径,重启 Gateway 后复测主会话。

这篇文章不会出现任何真实内网地址、账号、token、会话 ID、个人微信标识或私有路径。所有配置片段都已经脱敏,并用 <PLACEHOLDER> 形式表示。重点是分享一套可复用的排障方法,而不是公开某个具体环境的细节。

AI 回复重复的抽象排障题图

图 1:本文自制题图。一个看似“微信重复发送”的问题,最后被定位成模型执行链路里的重复输出。

1. 问题背景:个人 IM 入口里的 Agent,比命令行更容易暴露“最终体验问题”

现在很多人已经不满足于在终端里和 AI Agent 对话。真正方便的形态,是把 Agent 接进日常消息入口:个人微信、企业 IM、Telegram、Slack、飞书、钉钉、iMessage,或者其它自己常用的通知和对话渠道。这样做的好处很明显:不用专门打开一个终端,也不用记住某个 dashboard 地址,随手发一句话,Agent 就能帮你查状态、写摘要、跑脚本、整理资料、提醒事项。

但 IM 入口也会把问题放大。命令行里多输出一段内容,你可能只觉得有点烦;如果这段内容被发送到微信里,它就是一条正式消息。命令行里一次输出重复,你还能滚屏忽略;微信里一条消息内部重复四遍,就非常刺眼。更重要的是,IM 通道通常跨过了多个层次:消息接收、去重、会话路由、模型选择、Agent 执行、文本归一化、发送 API。任何一层出了问题,最终用户看到的都只是“这条消息不对”。

这次遇到的问题就是这样:本地 OpenClaw 已经接入个人微信。用户在微信里和 OpenClaw 对话时,不管问什么,OpenClaw 都会把同一句回复在一条消息里重复多次。注意,这不是连续收到四条消息,而是一条消息内部出现四份完全相同的文本。这个差异很重要,因为它直接影响排障方向。

如果是连续收到四条消息,优先怀疑 webhook 重试、轮询 offset、消息去重、发送 API 被调用多次、队列重复消费。如果是一条消息里有四段相同文本,优先怀疑最终 reply payload 本身已经重复,或者发送层把多个文本 fragment 拼成了一条消息。两者看起来都叫“重复回复”,但根因完全不同。

这也是我想写这篇文章的原因。AI Agent 的故障越来越不像传统脚本报错:它可能不是崩溃、不是 500、不是 timeout,而是“看起来能工作,但输出很怪”。这类问题最忌讳凭直觉改配置。因为 Agent 链路太长,直觉很容易把你带到最外层:既然是在微信里看到的,那就改微信插件;既然是模型回答重复,那就调 prompt;既然刚升级过,那就回滚版本。真正可靠的做法,是先把重复发生的位置钉住。

2. 问题表现:不是多条消息,而是一条消息里复读

这次现象可以简化成一句话:

用户问:只回复这两个字:测试
期望:测试
实际:测试测试测试测试

真实问题里并不一定刚好是四个“测试”,但模式一致:Agent 不是回答了四条消息,而是在一个 reply body 里把同一段文本重复粘贴。对用户来说,这种体验很差。它会让人怀疑三件事:

  1. 微信插件是不是重复发送?
  2. OpenClaw 是不是接收到了重复输入?
  3. 模型是不是自己复读?

这三个怀疑都合理,但排查顺序不能乱。因为“看起来重复”不等于“发送重复”。如果发送层只调用一次,而调用参数里的 text 已经重复,那么继续改发送层只会浪费时间。反过来,如果 Agent raw text 是正常的,发送 API 却调用了多次,那再怎么换模型也没用。

我最终采用的是一个很小的最小复现 prompt:

只回复这两个字:测试

这个 prompt 有两个好处。第一,它足够短,避免复杂上下文导致模型自由发挥。第二,它有明确期望,输出只要不是单个“测试”,就可以判定异常。排障时不要一开始就拿真实长问题测试,因为长问题会引入太多噪声:系统提示、历史会话、工具调用、摘要、语言风格、模型思考、markdown 格式,都可能让你误判。

这里还有一个细节:我不只看用户最终看到什么,还看 Agent 执行结果里的 raw text、provider、model、winner provider 和 attempts。也就是说,我要知道这次回答到底是由哪条模型路径生成的,模型返回给 OpenClaw 的最终可见文本是什么。只有拿到这些证据,才能判断重复发生在哪一层。

分层排障路径

图 2:先确认重复发生在哪一层,再决定要改哪个配置。

3. 第一轮假设:微信发送层是否重复调用

很多 IM 集成问题,第一反应都是看发送层。这是合理的。因为 IM 平台通常有重试、回调、ack、offset、队列、去重等机制。如果接收端没有正确确认,或者发送端在失败后重试,就可能出现重复消息。

但这次有一个关键区别:用户看到的是一条消息里重复多次,而不是多条消息。为了验证这一点,我查看了微信插件的发送路径。脱敏后可以抽象成下面这个结构:

Agent reply payload
  -> dispatch reply
    -> build item_list
      -> sendMessage(channel, target, text)

发送层构造的是一个单条文本 item。也就是说,至少从代码路径看,它不是天然会把同一段文本循环发送四次。接下来要看的就是运行时:发送 API 是否被调用多次?如果没有多次调用,那问题就要往上游找。

在这一步,最重要的不是立刻修改代码,而是先建立一个判断标准:

这次证据指向第二种:微信发送层不是根因。重复文本已经在发送之前形成。这个结论非常关键,因为它把排查范围从“微信插件”缩小到了“Agent 执行链路”。

4. 第二轮假设:会话层是否复用了旧状态

Agent 和普通 API 调用不一样。普通 API 调用通常是无状态的:传入 messages,得到 completion。Agent 通常有 session:它会记住上一轮对话、系统提示是否已经发送、模型 override、通道、目标、工具调用历史、压缩状态、父会话关系等。

会话层如果有旧状态,也可能造成奇怪输出。例如:

  1. 某个会话曾经设置过模型 override,后续一直沿用。
  2. 主会话和通道会话绑定在一起,导致你以为开了新会话,实际复用了旧会话。
  3. 历史消息里已经有重复内容,模型在总结或延续时又重复了一遍。
  4. 上一次执行失败后保留了 fallback 选择,下次继续走 fallback。

所以我做了两种测试:一种是新 session key 的最小复现;另一种是个人微信实际使用的主会话复现。这样可以区分“只有旧主会话有问题”还是“新会话也会复现”。结果是,新会话同样会出现重复。这说明问题不是单纯的某个历史会话污染,而是更底层的模型路由或 provider 处理问题。

同时,主会话确实很有价值。因为最终用户走的是主会话,不是我临时造出来的 debug session。修复后必须回到主会话验证,否则只能证明“某条测试命令没问题”,不能证明“微信入口已经恢复”。

5. 第三轮假设:模型路由是否走错了 provider

这一步是整个问题的转折点。

OpenClaw 的模型配置里,模型选择不只是一个模型名。完整路径通常包含 provider 和 model,例如:

provider/model

如果两个 provider 都暴露了同名模型,例如都叫 SomeModel-X,那只看 model ID 就不够了。你必须同时看 provider、API 协议、base URL、兼容层、streaming 行为和最终执行轨迹。

这次就出现了这样的情况:同一个模型名,在两条路径上都能被看到。

第一条是原生 provider 路径,使用模型官方或原生适配方式。

第二条是 OpenAI-compatible 网关路径,也就是把后端模型包装成 OpenAI Chat Completions 或类似协议对外提供。

理论上,只要兼容层实现正确,两条路径都可以工作。但“可以工作”不等于“在 Agent 流式输出、reasoning 字段、content fragment、工具调用兼容、缓存统计、最终文本拼接上完全等价”。尤其是 Agent 框架通常会处理 streaming delta、reasoning、content block、usage、fallback、重试等细节。如果兼容层返回格式和框架期待之间有一点偏差,就可能出现肉眼很奇怪的问题:内容重复、空输出、reasoning 被当成正文、正文被拼接两次、工具调用重复、stop reason 不一致。

我做了一个对照实验:同样的 prompt,分别走兼容网关路径和原生 provider 路径。

结果非常明确:

兼容网关路径:测试测试
原生 provider 路径:测试

这就把根因范围进一步缩小了:不是模型能力本身突然喜欢复读,也不是微信发送层循环发送,而是某条 OpenAI-compatible 执行路径在 OpenClaw Agent 场景里产生了重复最终文本。

这里还要强调一句:这并不等价于“OpenAI-compatible 网关都不可靠”。兼容层是很有价值的,它能把不同模型统一到一个 API 形态里,便于客户端接入。但兼容层也确实是一个额外抽象层。对简单 curl 来说,返回一次文本就算通过;对长链路 Agent 来说,还要验证 streaming、上下文预算、reasoning、tool call、fallback、会话状态和最终可见文本。两者验证强度不是一个级别。

6. 真正根因:同名模型 + 故障候选仍可见 + Agent 执行链路选到了兼容路径

把证据串起来,根因可以写成一句工程化结论:

OpenClaw 的默认模型和可见模型集合中存在同名模型的多 provider 候选;其中 OpenAI-compatible 网关路径在当前 Agent 执行链路中会产生重复最终文本。虽然配置层看起来已经把默认模型指向原生 provider,但实际执行轨迹仍命中了兼容 provider,导致个人微信通道收到的 reply payload 本身就是重复文本。

这句话里有几个重点。

第一,问题不是“微信发了四次”。发送层不是根因。

第二,问题不是“用户输入重复”。最小 prompt 下也能复现。

第三,问题不是“模型名不对”。模型名看起来是同一个,真正差异在 provider 和协议路径。

第四,问题不是“重启就好”。重启 Gateway 后,如果故障候选仍然可见,执行轨迹仍可能继续命中它。

第五,修复不能只看 models status 或配置表面值。必须看实际执行轨迹里的 winner provider 和 final raw text。

模型路由修复前后对比

图 3:同名模型在不同 provider 下可能是完全不同的运行路径。排障时必须看 provider。

7. 修复过程:从“切默认值”到“收窄可见候选”

最开始我尝试的是最直观的修复:把默认模型切到原生 provider。

脱敏后的配置大概是这样:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "native-provider/Model-X",
        "fallbacks": ["compatible-gateway/Model-X"]
      }
    }
  }
}

这一步看起来合理,但验证时发现实际执行仍然走兼容路径。这说明“默认值显示正确”不代表“运行时一定按这个默认值执行”。Agent 系统还可能受到以下因素影响:

  1. 会话级模型 override。
  2. 通道级模型 override。
  3. fallback 选择。
  4. 模型 allowlist。
  5. 同名模型解析。
  6. provider catalog 的候选顺序。
  7. 历史会话里的运行状态。

于是我继续做第二步:清空默认 fallback,避免自动回落到故障候选。

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "native-provider/Model-X",
        "fallbacks": []
      }
    }
  }
}

这仍然不够。因为故障 provider 只要还在模型可见集合里,某些解析路径仍可能选到它。

第三步是给个人 IM 通道加通道级模型覆盖。这样即使其它通道暂时保留不同模型,个人微信入口也能被强制绑定到验证过的 provider。

{
  "channels": {
    "modelByChannel": {
      "personal-im-channel": {
        "*": "native-provider/Model-X"
      }
    }
  }
}

第四步,也是最终生效的关键,是把故障的兼容网关候选从 OpenClaw 的可见模型配置中移除。注意这里不是泄愤式删除所有网关配置,而是遵循最小可用原则:当前通道不再暴露这条已知会产生重复文本的候选路径。只要它不可见,Agent 就不会在这个入口继续选到它。

修复后再次运行最小复现:

输入:只回复这两个字:测试
输出:测试
provider:native-provider
winner:native-provider

再回到个人 IM 主会话验证,同样只返回一次。这才算真正修复。

8. 为什么我不建议用“后处理去重”当主修复

看到重复文本,很多人会想到一个简单办法:发消息前做去重。如果发现 abcabcabcabc,就压缩成 abc。这个方法看起来很快,但我不建议把它当主修复。

原因有四个。

第一,后处理不知道什么是“合法重复”。用户可能真的要求“把这句话重复三遍”,也可能让 Agent 生成诗歌、歌词、测试数据、表格、日志样例。盲目去重会破坏内容。

第二,后处理掩盖上游故障。如果 provider 路径持续返回异常,你把文本去重了,表面体验好了,但工具调用、结构化输出、长文本摘要仍可能继续出问题。

第三,重复不一定总是完美重复。有时是段落重复,有时是 reasoning 与正文混合,有时是 markdown 块重复,有时是中间 fragment 重复。后处理规则会越写越复杂,最后变成另一个不可靠系统。

第四,Agent 的排障重点是证据链。正确修复应该让执行轨迹变干净,而不是让最终展示层“看起来干净”。

当然,发送层可以有一些防御性保护,例如避免同一个 message id 短时间重复发送,或者对明显的网络重试做幂等。但这和“把重复文本压缩掉”不是一回事。前者是消息幂等,后者是内容篡改。

9. 可复用排障清单

如果你也遇到类似“AI 助手回复重复”的问题,可以按下面顺序排查。

重复回复排障清单

图 4:不要先猜;先确认重复发生在哪一层。

9.1 先区分重复类型

第一问:用户看到的是多条重复消息,还是一条消息内部重复文本?

9.2 用最小 prompt 复现

不要拿复杂任务排障。先用最小 prompt:

只回复这两个字:测试

如果最小 prompt 都重复,问题大概率不在业务上下文。如果最小 prompt 正常,复杂任务重复,则要查历史会话、系统提示、工具调用和模型对长上下文的处理。

9.3 同时记录 provider 和 model

不要只记录模型名。应该记录:

provider = ?
model = ?
api protocol = ?
final raw text = ?
winner provider = ?
fallback used = ?

同名模型在不同 provider 下可能走完全不同的协议和兼容层。

9.4 做 provider 对照实验

如果系统里有原生 provider 和兼容 provider,必须做同 prompt 对照。不要只 curl /models,也不要只测非流式 completion。Agent 场景至少要测一次真实 agent turn,并检查最终可见文本。

9.5 检查会话覆盖和通道覆盖

很多 Agent 系统都有多层模型选择:全局默认、agent 默认、会话 override、通道 override、fallback、allowlist。修复时要明确你改的是哪一层。尤其是 IM 入口,最好给关键通道设置明确模型,避免默认链路漂移。

9.6 收窄可见模型

如果某条模型路径已经被证明会产生错误输出,就不要继续让它留在默认可见候选里。保留一个“坏候选”作为 fallback,等于把问题变成随机复发。

9.7 重启后必须复测主入口

配置改完后,至少验证三件事:

  1. Gateway 正常运行。
  2. 目标 IM 通道正常运行。
  3. 目标主会话最小 prompt 返回一次,并且 winner provider 是目标 provider。

只有这三项都通过,才算修复。

10. Q&A

Q1:为什么 curl 直接请求模型没问题,Agent 里却重复?

因为 curl 测的通常是单次 API 行为,而 Agent 测的是完整执行链路。Agent 会处理系统提示、历史会话、streaming delta、reasoning、工具调用、fallback、上下文预算和最终文本归一化。兼容网关在简单请求里正常,不代表在 Agent streaming 场景里完全等价。

Q2:OpenAI-compatible 网关是不是不能用?

不是。兼容网关很有价值,尤其适合统一多模型入口。但它需要按真实客户端验证。对聊天 UI 来说,能返回文本就够;对 Agent 来说,还要验证工具调用、流式输出、reasoning 字段、stop reason、usage 和长上下文行为。问题不在“兼容”这个概念,而在“没有用目标工作流充分验证兼容性”。

Q3:为什么不保留故障路径作为 fallback?

fallback 的意义是主路径失败时提供可用替代。如果 fallback 本身会稳定产生错误内容,它就不是 fallback,而是一个延迟触发的故障源。对于 IM 通道这种用户直接可见的入口,fallback 宁可少,也不要把已知坏路径放进去。

Q4:通道级模型覆盖有什么价值?

不同通道的容错要求不同。命令行可以接受调试输出,微信入口则需要简洁、稳定、可读。给个人 IM 通道设置明确模型,可以减少全局默认变化带来的意外影响。以后你要实验新模型,也不会直接影响这个高频入口。

Q5:如果真的必须继续使用兼容网关怎么办?

那就要把它当作一个独立兼容性问题处理:抓取原始响应,比较流式和非流式输出,确认 content delta 是否被重复消费,检查 reasoning 字段是否被误拼进正文,确认客户端对 stop reason 和 content block 的处理是否符合预期。修复兼容层后,再把它放回可见候选,而不是带病上线。

Q6:这类问题能不能靠 prompt 解决?

不建议。你可以写“不要重复回答”,但如果重复来自客户端拼接、兼容层字段映射或 provider 路由,prompt 只是把系统性问题伪装成模型行为问题。排障要优先找证据链,而不是先给模型加道德约束。

11. 总结:AI Agent 排障,要像排生产链路一样分层

这次问题最有价值的地方,不是“把某个配置改好了”,而是再次证明:AI Agent 已经不是一个简单聊天窗口。它是一条生产链路。链路里有通道、有会话、有模型路由、有 provider、有兼容协议、有流式拼接、有 fallback。最终用户看到的一句重复回复,背后可能不是一个 bug,而是多个抽象层的交汇点。

我的经验是:遇到这类问题,先不要问“是不是微信坏了”,也不要问“是不是模型傻了”。先问三个更具体的问题:

  1. 重复发生在发送前还是发送后?
  2. 重复发生在会话层还是模型层?
  3. 真实 winner provider 是哪一个?

只要这三个问题有证据,修复就会变得很直接。反过来,如果没有证据,只看最终现象,任何改动都像是在雾里拧螺丝。

最后再强调一遍隐私边界:本文所有配置、路径、通道名、provider 名都做了抽象处理。真实环境里的地址、账号、token、会话 ID、用户标识都不应该出现在博客、截图、日志片段或公开 issue 里。排障文章可以分享方法,但不应该把生产环境当素材裸奔。

参考资料