兰 亭 墨 苑
期货 · 量化 · AI · 终身学习
首页
归档
编辑文章
标题 *
URL 别名 *
内容 *
(支持 Markdown 格式)
# WeClaw 的消息路径全景:一条消息的六种旅程 > 把微信、API、定时任务、多 Agent 协同这些入口摊开看一遍——你会发现这个"微信 AI 桥"本质上是一台**消息路由器**。本文按代码实现逐条梳理 WeClaw 现在的消息路径,并给出每条路径的设计取舍。 ## 一、总览:消息从哪来,到哪去 WeClaw 的定位是"把微信接到 AI Agent"。但跑起来之后,它其实同时承担了六件事: ``` 入站 出站 ┌─────────────────────────────┐ ┌──────────────────────────┐ │ ① 微信消息(ilink 长轮询) │ │ 微信回复(文本分片/媒体) │ │ ② API 反代(/v1/chat/...) │ ────────► │ API 响应(JSON / SSE) │ │ ③ 定时任务(cron / timer) │ Agent │ relay 推送(问答对) │ │ ④ 主动推送(/api/send) │ 执行 │ 自动归档(archive) │ │ ⑤ 多 Agent(debate/roundtable│ │ 发布(博客/知识库/播客) │ │ ⑥ 媒体(图片/文件/语音) │ └──────────────────────────┘ └─────────────────────────────┘ ``` 下面逐条拆。 --- ## 二、微信主路径:HandleMessage 的六道闸门 微信消息由每个账号一个的 **monitor 长轮询**拉取(`cmd/start.go` 里按账号启动),拿到消息后统一进入 `Handler.HandleMessage`。这个函数是一条**六级瀑布**,每级按优先级拦截: | 级别 | 判断 | 去向 | |---|---|---| | 0 | 消息类型/状态过滤 + `message_id` 去重 | 丢弃(语音会重复推 finish) | | 1 | **shell 模式**(用户 `/sh` 进入后) | 直接执行白名单命令,不进 AI | | 2 | 纯 URL | 存 linkhoard;微信公众号文章额外送分析队列 | | 3 | 「重试」/ `/retry` | 重发最近一次失败的发布任务 | | 4 | `/` 开头且命中**命令表** | `/info` `/new` `/save` `/hub` `/debate` `/publish`… | | 5 | `@agent` 前缀 | `sendToNamedAgent`(指定 agent,跳过意图路由) | | 6 | **媒体附件** | `sendMediaToAgent`(自动挑 vision agent 或 draw) | | 7 | 其余全部 | **`sendToDefaultAgent`**(默认入口,见下节) | 这个顺序是有讲究的:**越靠前的闸门越"确定"**。用户敲 `/` 命令时不需要 AI 参与;URL 保存不需要 AI 参与;而只有第 7 级才是"让 AI 理解意图"的地方——把不确定性收敛到最后一站。 --- ## 三、默认入口:意图分发的五层路由 `sendToDefaultAgent` 是所有"普通对话"的总入口,也是最近改造最集中的地方。它内部是五层路由: ### 第 1 层:生图/改图改道 `routeImageIntent` —— 命中"画一张""把图改成蓝色"这类词时,把 agent 改道到 draw,并注入图片上下文(最近发的图、画布路径、多图融合)。 ### 第 2 层:闲聊短路 `isDefiniteChitchat` —— "你好""谢谢""👍"这类极高置信寒暄**直接对话**,连意图决策都不做。词表取反设计(只收"确定闲聊"),与"确定意图"的正向词表天然不冲突。 ### 第 3 层:意图代理(核心) 对于**没有原生 function calling 能力的 agent**(`NoFC`:无 MCP 或走 `mcp_text` 文本协议,典型如 dsfree),调用 **gemini 作为意图代理**: ```go dec, ok := h.routeIntentWithAgent(ctx, router, userID, text, cachedSummary) // dec.Intent ∈ {search, publish, image_draw, image_edit} 或未调用工具=chat ``` 实现上是一次**带 `route_decision` 工具的 chat/completions 请求**——gemini 用原生 function calling 返回结构化决策,参数零解析。三个关键设计: - **`chat` 不是枚举值**,而是"模型没调用工具"本身——工具不存在即意图不存在,最高频路径零协议开销; - **决策失败降级到收紧词表**(子句首约束 + 否定过滤 + 动作连词),绝不把"猜"当主路径; - **决策结果按意图分发**:search → 检索后注入事实再交回本尊;publish → 执行发布;image → draw。 ### 第 4 层:降级链(意图代理不可用时) `matchTriggerStrict`(子句首约束)/ `routeTwoPhaseToolCallStrict`(两段式)/ `isShortSendIntent`。这层是**安全网**,不是主路径。 ### 第 5 层:有原生 FC 的 agent 直接进工具循环,模型自主决定调什么工具,不需要意图代理代劳。 --- ## 四、API 路径:从"直通管道"到"也懂意图" WeClaw 暴露两个 OpenAI 兼容入站端点: - `POST /v1/chat/completions`(标准 OpenAI 格式) - `POST /api/ask`(简化格式,`{"prompt": "...", "agent": "..."}`) 两者都进 `handleAsk`,然后按目标 agent 的类型分流: | agent 配置 | 处理器 | 行为 | |---|---|---| | `web_search: true` | `proxyResponsesSearch` | 走 `/v1/responses` + web_search 工具 | | 有 MCP servers | `proxyMCPChat` | function-calling 循环(原生 FC / Responses / 文本协议) | | **无 MCP(NoFC)** | **`tryIntentRoutingForNoFC`**(新) | **补意图路由**,失败则纯转发 | | 非 HTTP(acp/cli) | `handler.Ask` / `AskStream` | 走本地 agent 进程 | **这里有一个刚补上的重要缺口。** 原来 API 路径是"透明管道"——直接把请求转发给上游 agent。但无工具能力的 agent(如 dsfree)在 API 端因此**既不能搜索也不能发布**:意图代理只挂在微信路径上。 修复方式是让 API 路径复用同一套意图代理,但对 search 采用**"注入后转发"**而非"代答": ```go case "search": facts, _ := s.handler.CollectFactsForAPI(...) injected := injectFactsIntoBody(bodyBytes, facts) // 把事实追加到最后一条 user 消息 return injected, false // 继续转发,不劫持 ``` 理由很实际:API 客户端(pi、opencode、OpenAI SDK)要的是**流式输出 + 该 agent 自己的风格**。如果 weclaw 自己生成回答,这两点都会丢。注入事实后原样转发,agent 基于事实作答——既拿到了实时信息,又保住了它的人设。 而 publish / image 这类"动作型"意图则直接执行,打包成标准 `chat.completion` 返回。 --- ## 五、Agent 执行层:三种进程模型,四种工具协议 消息路由决定"谁来处理",Agent 层决定"怎么处理"。WeClaw 支持三类 agent: | 类型 | 通信 | 进程模型 | 典型场景 | |---|---|---|---| | **ACP** | stdio JSON-RPC 长连接 | 常驻子进程 | Claude Code、Codex、Hermes | | **CLI** | 每消息启动 + `--resume` | 逐条新进程 | `claude -p`、`codex exec` | | **HTTP** | OpenAI 兼容 REST | 无本地进程 | 各种 API 模型、自建反代 | HTTP 型内部又按上游能力分**四种工具协议**(`SelectMCPChat` 统一裁决): 1. **原生 function calling**(chat/completions)——支持 FC 的模型直接调工具; 2. **Responses API**——Poe/DeepSeek 等只在 `/v1/responses` 暴露 FC 的上游; 3. **文本协议(`mcp_text`)**——上游完全不支持 FC 时的兜底:工具清单进提示词,模型用 `tool_call` 围栏表达,weclaw 解析执行; 4. **无工具**——纯对话。 这个分层是 WeClaw 能在"免费/反代/官方"各种七拼八凑的上游池里稳定工作的关键:**能力探测 + 协议降级**。 --- ## 六、多 Agent 与定时:把"一条消息"变成"一场协作" 除了单 agent 对话,命令表里还有三条**多 Agent 协同命令**(注意:它们属于"命令路径"的子路由,不是独立入站路径): - **`/debate`**:多个 agent 就一个议题辩论; - **`/roundtable`**:圆桌讨论; - **`/workflow`**:按工作流串行调度多个 agent。 另外有**失败恢复**机制(不属于协同,但同属"一条消息的后续旅程"):发布/发送失败时用 `recordFailedTask` 把任务落盘,用户说「重试」或 `/retry` 即可重放——这正是第二节六级瀑布的第 3 级闸门。 定时侧有两条: - **cron**:周期性任务,支持三类 job——`text`(定时发文本)、`agent`(定时让 agent 以某用户上下文作答)、`workflow`(定时跑工作流); - **timer**:一次性延时提醒。 定时任务和微信消息**共用 agent 与会话缓存**——所以 cron 触发的 agent 回复会出现在该用户的微信里,就像他自己问的一样。 --- ## 七、出站:回复不止一种形态 一条回复的出口也有多条: | 出口 | 说明 | |---|---| | **微信回复** | 长文自动分片;图片/文件走 CDN 上传后发送 | | **API 响应** | 非流式 `chat.completion`;流式 SSE(含"上游忽略 stream 时包装成 SSE"的兜底) | | **relay** | 把问答对推送到远端(博客/知识库/剪贴板) | | **archive** | 自动归档到按日期分目录的归档区 | | **发布** | publisher MCP:`publish` 工具支持 `blog` / `ima` / `nas` / `knowly` / `kb` 五个目标 | 发布这条值得单说:`publish` 是一个 MCP 工具,由 executor 池的 agent 调用。它的五个目标各有独立契约——博客走 REST `/api/publish`,IMA 笔记走 `/api/ima/import`,播客走同一 REST 的 `targets:["nas"]`,Knowly 走 multipart 上传,而 IMA 知识库走官方 openapi 的 `create_media → COS → add_knowledge` 三步链路。 --- ## 八、一张表看完 | 入站路径 | 入口 | 是否过意图路由 | 典型用途 | |---|---|---|---| | 微信消息 | `HandleMessage` | ✅(默认分支) | 日常主入口 | | 微信 `@agent` | `sendToNamedAgent` | ❌(用户已指定) | 点名某个模型 | | 微信命令 `/xxx` | 命令表 | ❌ | 管理操作 | | 微信媒体 | `sendMediaToAgent` | 部分(生图走 draw) | 看图/改图 | | API 反代 | `tryProxyToAgent` | ✅(NoFC agent) | pi/opencode/第三方客户端 | | 定时任务 | cron / timer | ❌(直接进 agent) | 定时提醒/报告 | | 多 Agent 协同(命令触发) | `/debate` `/roundtable` `/workflow` | ❌ | 协作任务 | > 注意层级:前四行都是**同一条微信消息**在 `HandleMessage` 里按优先级落进的子路由(一条消息只走其中一条);最后一行则是第三行"微信命令"的子路由。真正并列的**入站来源**只有四个:微信、API、定时、以及出站用的 `/api/send`。 --- ## 九、设计心得 梳理完会发现几条反复出现的设计原则: **1. 不确定性收敛到最后一站。** 命令、URL、媒体这些"确定"输入在前置闸门就地处理;只有真正需要理解的自然语言才进意图路由。这让绝大多数消息的路径是**可预测、可测试**的。 **2. 意图判断交给擅长判断的组件。** 用 gemini 的原生 function calling 做意图决策,而不是 weclaw 用正则猜;正则退居安全网。字面上是"多一次模型调用",实际是把误判率从"口语词命中即劫持"降到"模型理解上下文"。 **3. 注入优于代答。** 需要外部信息时,把事实注入上下文再交给本尊作答,而不是让路由层自己生成回答——保住流式、保住风格、保住职责边界。 **4. 能力探测 + 协议降级。** 上游五花八门(原生 FC / Responses / 文本协议 / 无工具),同一份配置能在不同能力的上游上跑起来,靠的就是"探测能力、选择协议、失败降级"。 **5. 每条路径都有安全网。** 意图代理失败 → 收紧词表;MCP 循环失败 → 纯转发;发布失败 → 落盘待重试。没有一条路径会把消息"吞掉"。 --- *本文基于 WeClaw 当前代码实现梳理,含 2026-09-10 的意图路由改造(意图代理 + API 路径接入)。* *修订说明(二次发布):修正首版把「pending task」误列入「多 Agent 协同」的分类错误——它是失败重试机制(`HandleMessage` 第 3 级闸门),与多 Agent 协同无关;同时补充了"入站来源 vs 子路由"的层级说明。*
配图 (可多选)
选择新图片文件或拖拽到此处
标签
更新文章
删除文章