微信消息进入 Agent 核心并完成真实执行与证据回流的无文字技术架构封面

上一篇文章回答的是“为什么要把 AI 接进微信”。这一篇只回答工程问题:一条微信消息进来以后,究竟经过了什么,什么时候只是回答,什么时候会变成一次真实执行,以及系统凭什么相信它真的做完了。

这次我把视角放到执行者这一侧。这里的“我”不是一个拟人化角色,而是 Agent / Executor 这一层:我收到消息,判断意图,选择是否调用工具,建立任务身份,把执行交给真实工作节点,再等待状态和证据回到系统里。

整套东西最核心的一条约束其实很简单:

MODEL_DONE
    ≠
MESSAGE_DELIVERED
    ≠
TASK_EXECUTED
    ≠
TASK_VERIFIED
    ≠
USER_ACCEPTED

如果这五件事混成一个“完成”,Agent 很快就会变成一种很会说“已经好了”的聊天机器人。


1. 总体架构

如果把它画成“微信 → 大模型 → 微信”,这套系统就解释错了。

真正的结构至少分成五层:

结构图
┌──────────────────────────────────────────────────────────────┐
│  Layer 1 · Channel Plane                                     │
│  WeChat / openclaw-weixin                                    │
│  负责登录、收消息、媒体、账号监控、把回复送回微信            │
└───────────────────────┬──────────────────────────────────────┘
                        │ normalized message
                        ▼
┌──────────────────────────────────────────────────────────────┐
│  Layer 2 · Agent Runtime                                     │
│  OpenClaw Gateway + Front Agent                              │
│  负责会话、上下文、模型路由、工具选择、快路径回复            │
└───────────────┬───────────────────────┬──────────────────────┘
                │ CHAT / READ           │ ACTION
                │                       ▼
                │        ┌─────────────────────────────────────┐
                │        │ Layer 3 · Capability Plane          │
                │        │ LMN516 MCP                          │
                │        │ list / capture / reserve / report   │
                │        └────────────────┬────────────────────┘
                │                         ▼
                │        ┌─────────────────────────────────────┐
                │        │ Layer 4 · Control Plane             │
                │        │ Control Tower                       │
                │        │ canonical Q / RUN / Event /         │
                │        │ Projection / Evidence               │
                │        └────────────────┬────────────────────┘
                │                         ▼
                │        ┌─────────────────────────────────────┐
                │        │ Layer 5 · Execution Plane           │
                │        │ Heavy Worker / Executor             │
                │        │ Repo / Git / CI / API / Deploy      │
                │        └────────────────┬────────────────────┘
                │                         │ status + evidence
                └─────────────────────────┴───────────────┐
                                                          ▼
                                              OpenClaw → WeChat

这里最重要的不是多了几层,而是职责被拆开了

微信层不负责理解任务;OpenClaw 不应该成为任务真相数据库;MCP 不负责假装执行;Control Tower 不直接代替 Worker 修改代码;Worker 也不能仅靠一句自然语言“完成了”来改变系统事实。

这也是为什么 LMN516 MCP 的意义不是“给微信加几个 API”。它实际上是在 Agent Runtime 与 LMN516 的真实系统之间建立一个能力边界 + 身份边界 + 状态边界

在微信这一侧,我们没有自己重新实现微信协议。当前 OpenClaw 的微信接入由腾讯维护的 @tencent-weixin/openclaw-weixin channel plugin 提供:Gateway 加载插件,扫码授权账号,插件启动微信监控,入站消息被规范化后再路由到选定 Agent,出站回复再从插件返回微信。

所以从一开始,架构就是:

微信协议细节        → 交给 Channel Plugin
Agent 会话与推理     → 交给 OpenClaw Runtime
LMN516 系统能力      → 通过 MCP 暴露
任务真相             → 交给 Control Tower
真实执行             → 交给 Executor
完成证明             → 交给 Evidence

这比“把模型塞进微信”复杂得多,但它解决的是后面真正会出问题的地方。


2. 消息链路

先只看一条最普通的微信消息。

假设我在微信里发送:

看一下首页移动端那个问题现在到哪一步了。

从 Channel 层开始,链路更接近这样:

结构图
[WeChat Client]
      │
      │ message
      ▼
[Tencent Weixin Channel Plugin]
      │
      │ 登录态 / 账号监控 / 入站接收
      ▼
[OpenClaw Gateway]
      │
      │ normalize + route
      ▼
[Selected Agent Session]
      │
      ├─ conversation context
      ├─ tool registry
      ├─ model route
      └─ channel/session identity

这里有一个容易误解的地方:LMN516 接到的不是“微信原始协议包”

微信登录、账户令牌、媒体上传下载、账号 monitor 这些事情属于 Channel Runtime。到了 Agent 层以后,真正应该关心的是经过渠道契约规范化后的消息、会话和调用上下文。

可以把 Agent 实际需要的消息抽象成下面这样——这是架构示意,不是 OpenClaw 的原始内部 payload:

结构图
MessageEnvelope
├── channel      = openclaw-weixin
├── account      = <current account>
├── peer         = <sender/session peer>
├── session      = <agent session>
├── message_id   = <channel message id>
├── text         = "看一下首页移动端那个问题现在到哪一步了"
└── received_at  = <timestamp>

然后真正的第一处分叉发生了:

结构图
                    ┌─ 普通聊天 ────────────────┐
                    │                           ▼
微信消息 → Agent → 意图判断                 Model
                    │                           │
                    │                           ▼
                    │                     Visible Reply
                    │
                    └─ 系统读取 / 执行意图 ─→ MCP / Control Tower

为什么要分叉?因为如果每一句“你好”“这个什么意思”都先经过 Control Tower、创建 Q、分配 RUN,再回来回复,系统不仅会慢,而且会制造大量没有意义的任务身份。

反过来也一样。如果一句“继续修那个问题”只被当作聊天,模型回复一句“好的,我继续处理”,那它看起来像 Agent,实际上什么都没执行。

所以消息链路的正确问题不是:

模型有没有回答?

而是:

这条消息属于哪一种语义?
它只需要语言结果,还是需要系统事实?
如果需要事实,事实从哪里读?
如果需要动作,谁来执行?
执行以后,什么东西能证明完成?

回复链路也必须单独看:

结构图
Model Turn Ended
      │
      ▼
Assistant Content Generated?
      │ yes
      ▼
OpenClaw Outbound
      │
      ▼
WeChat Delivery
      │
      ▼
User Visible

我们实际碰到过一句非常典型的错误:

I finished the turn, but it did not produce a visible reply.

这句话几乎把问题说透了:turn 完成只是内部计算状态,不是消息交付状态。

因此后来我们不再把“模型结束”当成“微信完成”。


3. Agent 决策链路

真正让它从 Bot 变成 Agent 的,不是多调用几个工具,而是决策链路开始产生可追踪的系统动作

我更愿意把一次消息分类成四种基本类型:

结构图
                     ┌──────────────┐
Incoming Message ───▶│ Intent Router│
                     └──────┬───────┘
                            │
          ┌─────────────────┼──────────────────┬───────────────────┐
          ▼                 ▼                  ▼                   ▼
       CHAT               READ               ACTION             FOLLOW-UP
    普通对话          读取系统事实         新执行任务          继续已有任务
          │                 │                  │                   │
          ▼                 ▼                  ▼                   ▼
      Fast Path       list_open_items      capture_item      找 canonical Q
          │                 │                  │                   │
          ▼                 ▼                  ▼                   ▼
       Reply             Reply             reserve_run        reuse / new RUN
                                                │                   │
                                                └─────────┬─────────┘
                                                          ▼
                                                     Executor

CHAT:不要过度工程化

比如“这段话是什么意思”。

这类请求不需要建立任务身份,直接走快路径。它的主要目标是低延迟和可见回复。

READ:不要靠聊天记忆猜

比如“现在还有哪些任务没完成”。

这类请求最危险的做法,是让模型根据上一次聊天“回忆”。因为聊天上下文不是任务数据库。

LMN516 MCP 里专门加入了 list_open_items。它读取的是 Control Tower 的 live projection,返回当前未解决事项及其:

Q id
summary
priority / risk
questionStatus
trafficStatus
effectiveStatus
blockerType / blockerDetail
actionRequired
runIds / issueIds
dueAt / landingAt / landingNote
updatedAt

所以 Agent 回答“现在还剩什么”,理论上应该来自 canonical state,而不是语言模型的记忆。

ACTION:先建立身份,不要先说“开始了”

新任务首先调用 capture_item

它会建立一个稳定 Q:

Q-20260823-001

但这里有一个故意设计出来的细节:capture_item 的返回里有:

executionStarted: false

也就是说:记录了一件事,不等于已经开始执行。

接着 reserve_run 才会给这次执行分配 RUN:

RUN-20260823-001

但它的返回仍然是:

executionStarted: false

这不是多余字段,而是一道“防吹牛”的设计。

Q 已创建       ≠ 已执行
RUN 已预留     ≠ 已执行
模型说开始了   ≠ 已执行
只有真实 Executor 开始动作并回报 RUNNING,才叫开始执行

FOLLOW-UP:不要重复造任务

“继续”“再试一次”“刚才那个接着做”这类消息,难点是语义连续性。

如果已经存在同一 canonical Q,就应该复用它;如果当前 RUN 仍然是可复用的非终态执行,可以继续使用兼容 RUN;如果上一次 RUN 已经 DONE / FAILED / CANCELLED / SUPERSEDED,新的真实执行应该新建 RUN,而不是把旧 RUN 从坟里拉回来。

这就是为什么 Agent 决策不是一个大 Prompt 能解决的问题。它最后会碰到身份、状态机、幂等和并发


4. MCP 协议层

LMN516 这一侧的 MCP 入口目前落在:

/api/control-tower/mcp

实现不是一个随手写的 REST webhook,而是基于 @modelcontextprotocol/server 建立的 MCP Server:

server name    = lmn516-control-tower
server version = 1.3.0
runtime        = nodejs
dynamic        = force-dynamic
schema         = zod/v4
auth           = Bearer / OAuth verifier
required scope = email

请求通过 OAuth verifier 后,认证主体会被解析为 LMN516 的 appUserId。后面的 Q、RUN、Projection 都围绕这个用户做所有权约束。

当前最关键的四个工具是:

Tool读/写真正做什么故意不做什么
list_open_items读取 live projection不修改状态
capture_item建立/复用 canonical Q不启动执行
reserve_run分配稳定 RUN + Q→RUN 关系不宣称 RUNNING
report_run_status写事实状态、阻塞和证据不允许凭空创建 Q↔RUN

一条新任务的 MCP 调用更接近:

capture_item({
  summary: "修复首页移动端返回后重新加载",
  kind: "TASK",
  status: "READY",
  idempotencyKey: "wechat:<stable-message-or-semantic-key>"
})

→ {
    questionId: "Q-20260823-001",
    created: true,
    executionStarted: false
  }

然后:

reserve_run({
  questionId: "Q-20260823-001",
  title: "Fix mobile home revisit reload",
  idempotencyKey: "Q-20260823-001:execute:v1"
})

→ {
    runId: "RUN-20260823-001",
    status: "PLANNED",
    executionStarted: false
  }

这里还有一个很工程化但很重要的东西:idempotencyKey

微信和 Agent 链路里一定会遇到重试。网络抖了一下、tool result 没显示、模型重新发起调用,如果每次重试都创建新的 Q/RUN,几分钟就会出现:

结构图
同一个需求
├── Q-...-001
├── Q-...-002
├── Q-...-003
└── Q-...-004

所以“重试同一个语义动作”必须尽量复用稳定 key。

RUN 预留内部甚至会把幂等键做 SHA-256 摘要,构造稳定 source key:

control-run-reserve:<questionId>:<sha256(idempotencyKey)[0:24]>

这类设计看起来不像 AI,其实恰恰是 AI Agent 真正进入工程系统以后必须补上的部分。


5. Control Tower

如果说 OpenClaw 是“脑和入口”,MCP 是“手能摸到的接口”,那么 Control Tower 更像事实控制面

它最重要的工作不是列任务,而是维护三个东西:

结构图
Identity + State + Evidence
          │
          ▼
    Reconciliation
          │
          ▼
    Live Projection

Q 与 RUN 不是同一件事

Q = 用户到底要解决什么
RUN = 为解决这件事发生的一次具体执行尝试

例如:

结构图
Q-20260823-001
“修复首页移动端返回后重复加载”

      │ EXECUTED_BY
      ▼

RUN-20260823-001
第一次执行

RUN-20260823-002
失败后的第二次执行

Q 可以长期存在;RUN 代表一次具体执行生命。

ID 不是模型自己编出来的

Q 的编号分配会进入 PostgreSQL transaction,并使用 advisory transaction lock:

BEGIN
  pg_advisory_xact_lock(hashtext('control-question:20260823'))

  current max ordinal
      = max(
          control_question 中的编号,
          assistant_activity 中已经出现的 Q 编号
        )

  next ordinal = max + 1

  INSERT canonical control_question
  UPSERT Q_CREATED event
COMMIT

RUN 也是类似逻辑:

BEGIN
  pg_advisory_xact_lock(hashtext('control-run:20260823'))

  扫描已记录 run_ids
  分配 RUN-YYYYMMDD-NNN
  写 RUN_RESERVED
  更新 Q.runIds
  写 RELATION_CREATED
COMMIT

事务隔离使用 ReadCommitted

这意味着 Q/RUN 身份不是“模型看了一眼当前最大编号然后 +1”。后者在并行 Worker 环境里非常容易撞号。

Q→RUN 关系必须显式存在

Control Tower 有一条很严格的规则:

两个 ID 同时出现在一条 event 里
            ≠
它们自动建立关系

真正的图关系只能来自 canonical durable fields,或者显式的:

RELATION_CREATED
Q --EXECUTED_BY--> RUN

这能避免一种非常危险的“靠时间接近和标题相似猜关系”。

AI 特别擅长做这种语义猜测,但控制面恰恰不能这么干。


6. Worker 执行层

先说一下名字。这里继续使用 Worker,只是因为代码和架构里已经用了这个角色名;如果换一个更准确的名字,我更愿意叫它 Executor:它不是低等级代理,而是拿着真实工具、对真实系统产生副作用的执行节点。

Front Agent 与 Executor 的职责最好不要混在一起:

聊天快路径与真实执行路径

结构图
FAST PATH
WeChat → Front Agent → Model → visible reply

HEAVY PATH
WeChat → Front Agent
             │
             ▼
           MCP
             │
             ▼
          Q / RUN
             │
             ▼
         Executor
             │
      ┌──────┼────────┐
      ▼      ▼        ▼
    Repo    API      Git
      │      │        │
      └──────┼────────┘
             ▼
       Test / CI / Deploy
             │
             ▼
          Evidence

为什么要分?因为两类任务对模型和运行时的要求完全不同。

普通微信聊天最关心:

低延迟
上下文连续
回复可见
额度稳定

真实执行最关心:

工具权限
代码理解
长上下文
可恢复性
状态跟踪
验证
证据

如果永远用同一种模型、同一个执行循环处理所有东西,就会出现两种坏结果:

为了聊天速度 → 重任务能力不够
为了重任务能力 → 每句微信都又慢又贵

所以我们后来越来越倾向于把“前台响应”和“重执行”拆开。

Executor 真正开始工作以后,RUN 才应该进入:

结构图
PLANNED
   │
   ▼
QUEUED
   │
   ▼
RUNNING
   │
   ├──────────────┐
   ▼              ▼
VERIFYING      WAITING_USER / THROTTLED /
   │           BLOCKED_EXTERNAL / INTERRUPTED
   ▼
DONE / FAILED / CANCELLED / SUPERSEDED

这里最重要的是:reserve_run 不等于 RUNNING

只有 Executor 真正拿到任务并开始对系统产生动作,才可以报告运行态。

对于网站开发,一次执行可能真的包含:

读取 repo
→ 定位代码
→ 修改文件
→ TypeScript / compile / contract test
→ Git diff / commit / PR
→ CI
→ Production
→ Smoke
→ 回写 evidence

所以一句“我已经修改好了”根本不够。


7. 状态回传

这部分是我们后来专门补的一层,也是整套 WeChat Agent 最关键的闭环之一:report_run_status

它接受的不是一段随意文本,而是结构化事实:

report_run_status({
  runId,
  status,
  phase?,
  blockerType?,
  blockerDetail?,
  actionRequired?,
  evidence?: {
    kind?,
    level?,
    ref?,
    value?
  },
  idempotencyKey?
})

RUN status 不是只有“进行中 / 完成”,而是:

DISCOVERED
PLANNED
QUEUED
RUNNING
INTERRUPTED
THROTTLED
WAITING_APPROVAL
WAITING_USER
BLOCKED_EXTERNAL
VERIFYING
DONE
FAILED
CANCELLED
SUPERSEDED
STALE

这意味着微信里以后可以得到比“好了没”更真实的回答:

“正在执行”
“已经进入验证”
“被模型额度限制,当前 THROTTLED”
“需要你确认一个高风险动作”
“外部服务阻塞”
“代码已经产生,但还没有落到 Production”

Evidence 为什么要分 E0–E5

Control Tower 把证据强度拆成:

Level含义例子能不能单独证明完成
E0Observation页面出现、DOM 活动、某个 turn 可见不能
E1Self-reportWorker 自己说 RUNNING / DONE只能做临时状态依据
E2Artifact文件、diff、branch、产物真实存在能证明“产出了东西”
E3System verified确定性测试、查询、审计通过能支撑验证完成
E4Landed目标 Production / 数据 / 内容面已存在结果支撑真正落地
E5Accepted用户或明确验收规则接受需要验收的任务最终闭环

这一套最直接地解决了“AI 假装完成”。

结构图
Assistant: “已经完成。”
        │
        ▼
只有文字             = E1
有 commit/diff        = E2
测试通过              = E3
Production 已包含     = E4
用户确认符合预期      = E5

因此:

RUN = DONE
    不应该只靠
“Worker 说 DONE”

而且 Q 状态和 RUN 状态是独立的。

RUN = DONE
    ≠
Q = RESOLVED

一次执行做完了,不代表用户的原始目标一定解决了。对于需要最终验收的任务,Control Tower 的事实规则要求落地证据和接受证据分别存在。

还有一道重要防线:终态不可回退。

代码明确拒绝:

DONE       → RUNNING     X
FAILED     → VERIFYING   X
CANCELLED  → QUEUED      X
SUPERSEDED → RUNNING     X

如果真的开始了新的执行,就应该:

old RUN = DONE
new RUN = RUN-...-002

而不是改写历史。

“模型完成”“消息送达”“任务验证”必须拆开


8. 失败案例

这套东西不是一遍设计出来的。很多规则其实都是被失败逼出来的。

Case 1:模型结束了,但微信没有任何可见回复

真实出现过:

I finished the turn, but it did not produce a visible reply.

最开始如果只看模型 runtime,会觉得“这轮没有报错”。

但用户看到的是:

我发消息
   ↓
等
   ↓
什么都没有

所以我们后来把故障边界拆成:

Inference Success
      ↓
Tool Success
      ↓
Assistant Content Exists
      ↓
Outbound Send Success
      ↓
WeChat Visible

只要最后一层没过,用户体验就是失败。

Case 2:所有模型临时被限流

我们也实际遇到过:

⚠️ All models are temporarily rate-limited.
Please try again in a few minutes.

这类问题如果无限自动重试,会产生两个坏结果:

延迟越来越长
      +
额度继续消耗

所以更合理的状态不是让 Agent 一直假装“处理中”,而是明确进入:

RUN = THROTTLED
blockerType = MODEL_RATE_LIMIT

然后决定:

可以安全降级模型? → fallback
不能安全降级?       → fail fast + 状态可见
恢复后重试?          → 新的可追踪 transition

额度从这里开始不再只是账单问题,而是产品架构的一部分。

Case 3:语言上的“完成”,系统里什么都没有

这是 Agent 最危险的一类失败。

用户:继续做。
Agent:好的,已经开始处理。

——但没有 Q
——没有 RUN
——没有 repo 变化
——没有 CI
——没有 evidence

从自然语言看,它很顺。

从工程上看,它等于零。

所以我们最终要求:

说“记录了”      → 至少有 canonical Q
说“开始执行”    → 至少有 RUNNING fact
说“产出了”      → 至少有 E2
说“验证通过”    → 至少有 E3
说“已经上线”    → 至少有 E4
说“任务闭环”    → 需要对应的接受条件

Case 4:P-0312 并发撞号

在 WeChat Agent 这次执行链开发里,还有一个非常具体的并发问题:第一次本地分配到了 P-0312,但与此同时 main 已经落入了另一个完全不同的 P-0312

结果是:

结构图
Worker A                       main
   │                            │
   ├─ local P-0312              ├─ unrelated P-0312 landed
   │                            │
   └──────────── collision ─────┘

最后那个冲突 patch record 被主动丢弃,重新申请新的身份。

这不是微信本身的 bug,却非常能说明 Agent 系统的真实难点:当多个执行者同时工作时,身份分配、Git 状态、任务状态会互相碰撞。

也正因为发生过这类事,Q/RUN 分配里才更强调事务锁、canonical identity 和不可凭标题猜关联。

Case 5:代码里已经有工具,不等于 Production Agent 已经能调用

这是另一种经常被忽略的“假完成”:

source code has report_run_status
        ≠
CI passed
        ≠
Production deployed
        ≠
OpenClaw sees the tool
        ≠
WeChat → Worker → status smoke passed

这也是为什么 LMN-0381 的验收清单把这些状态分开记录,而不是在代码写完那一刻就宣布整个 WeChat Agent V1 完成。


9. 性能优化

把 AI 放进微信以后,性能问题会比网页聊天更敏感。

因为微信天然训练了用户对“即时回复”的预期。

我们当时希望普通对话尽量进入大约 4 秒左右的可聊天范围。这不是一个已经建立的 p95 SLA,也不是每次必须 4.000 秒,而是一个很实际的体验目标:4 秒还能保持对话节奏,十几秒就会明显让人感觉系统卡住了。

一次可见回复的总延迟可以粗略写成:

t_visible
  = t_wechat_in
  + t_channel_normalize
  + t_agent_route
  + t_context
  + t_model
  + Σ t_tool
  + t_compose
  + t_wechat_out

所以“换一个更快模型”只优化其中一项。

真正有效的优化方向是把整条链路拆预算。

9.1 快路径不要碰重控制面

“你好”
  ↓
Front Agent
  ↓
Model
  ↓
Reply

不要:

“你好”
  ↓
MCP
  ↓
Control Tower
  ↓
Q
  ↓
RUN
  ↓
Worker
  ↓
……

9.2 READ 和 ACTION 分开

查询任务可以 list_open_items,但查询不应该顺便制造执行。

READ  → read-only projection
ACTION → explicit mutation

这能减少无谓副作用,也减少调用链长度。

9.3 模型选择不是“永远用最强”

真正的路由指标至少包括:

维度Front AgentHeavy Executor
首字延迟高优先级次高
工具调用稳定性需要极高优先级
长上下文中等
推理深度中等
限流/额度稳定性极高
单次成本敏感可为复杂任务适度放宽
失败恢复快速降级必须可追踪

所以真正的问题不是“哪个模型最好”,而是:

这个任务值得花多少推理?
失败后能不能安全 fallback?
工具调用是否可靠?
上下文是不是必须全带?
额度是否足够支撑连续会话?

9.4 上下文要压,而不是无限长

微信很容易形成长会话。如果每一轮都把所有历史原样带上:

latency ↑
tokens ↑
rate-limit probability ↑
model attention noise ↑

更合理的是:

最近对话窗口
+
稳定 session state
+
需要时从 LMN516 读取真实事实
+
执行任务引用 Q / RUN,而不是复制全部历史

9.5 重任务不要阻塞成一个超长“正在输入”

复杂执行天然可能需要更久。

因此更合理的用户体验不是把整个工作压成一次超长同步回复,而是让状态成为协议的一部分:

收到任务
   ↓
Q created / RUN reserved
   ↓
RUNNING
   ↓
VERIFYING
   ↓
DONE + evidence

用户可以随时问“到哪一步了”,Agent 从 Control Tower 读事实,而不是靠自己记得上一次说到哪里。

9.6 限流必须成为显式状态

如果所有模型都限流,最糟糕的是静默失败。

更好的策略是:

结构图
provider failure detected
        ↓
THROTTLED
        ↓
can fallback safely?
   ┌────┴────┐
  yes       no
   │         │
fallback   visible blocker
   │         │
   └────┬────┘
        ▼
status remains traceable

性能优化到最后,其实已经不是“快一点”,而是:快路径够快,慢路径可见,失败路径诚实。


10. 最终系统形态

走到现在,这套系统已经很难再被准确地叫做“微信机器人”。

更准确的结构是:

结构图
                         ┌────────────────────────────┐
                         │          WeChat            │
                         │       human interface      │
                         └─────────────┬──────────────┘
                                       │
                                       ▼
                         ┌────────────────────────────┐
                         │ OpenClaw Channel + Gateway │
                         │ session / route / model    │
                         └───────┬────────────┬───────┘
                                 │            │
                         chat/read            │ action
                                 │            ▼
                                 │  ┌─────────────────────┐
                                 │  │ LMN516 MCP          │
                                 │  │ OAuth + tool schema │
                                 │  └──────────┬──────────┘
                                 │             ▼
                                 │  ┌─────────────────────┐
                                 │  │ Control Tower       │
                                 │  │ Q / RUN / Event     │
                                 │  │ Projection / Truth  │
                                 │  └──────────┬──────────┘
                                 │             ▼
                                 │  ┌─────────────────────┐
                                 │  │ Executor            │
                                 │  │ repo/API/Git/CI     │
                                 │  └──────────┬──────────┘
                                 │             ▼
                                 │  ┌─────────────────────┐
                                 │  │ Evidence E0–E5      │
                                 │  └──────────┬──────────┘
                                 │             │
                                 └─────────────┴───────────┐
                                                           ▼
                                                   visible WeChat reply

最后我给这套系统留下六条不变量:

1. 普通聊天可以没有 Q。
2. 要长期追踪的任务必须有 canonical Q。
3. 一次真实执行必须有独立 RUN。
4. “说完成”不是完成,完成必须有对应证据。
5. 终态 RUN 不允许回到非终态;新执行建新 RUN。
6. 模型内部成功不等于微信用户真正收到结果。

从源码上看,最关键的几块已经存在:

结构图
app/api/control-tower/mcp/route.ts
  ├─ list_open_items
  ├─ capture_item
  ├─ reserve_run
  └─ report_run_status

lib/control-tower/questions.ts
  └─ canonical Q + transaction + advisory lock + Q_CREATED

lib/control-tower/runs.ts
  ├─ RUN reservation
  ├─ explicit Q → RUN relation
  ├─ status machine
  ├─ idempotency
  └─ terminal monotonicity

8 月 21 日,list_open_items 被加进 OAuth-protected Control Tower MCP,让外部 Agent 能读真实未完成事项,而不需要再为微信单独造一套 REST/token 任务系统。

8 月 22 日,report_run_status 被补进执行链,开始把 Worker 的真实 lifecycle 和 evidence 回写到 Control Tower。

但这篇技术记录也必须保留一个不那么漂亮、却更真实的状态:截至当前仓库里的 LMN-0381,report_run_status、Q↔RUN 所有权校验、终态回退拒绝、E0–E5 evidence 记录以及本地 MCP contract test 已经完成;PR CI、Production MCP 暴露、完整 WeChat → Worker → status/evidence smoke 仍然被单独列为未完成验收项。

这其实正好说明我们为什么要做 Control Tower。

如果没有它,我完全可以写一句:

“微信 Agent 已经做完了。”

但现在系统会反过来问:

哪个 Q?
哪个 RUN?
当前是什么状态?
有什么 evidence?
Production 真的有吗?
用户真的收到了吗?

我觉得这才是这次接入里最有价值的部分。

把 AI 接进微信并不难到不可想象;真正难的是,当微信变成一个日常入口以后,后面的 AI 不能只会讲话,它必须能进入真实系统、承担真实动作、暴露真实失败,并且留下足够的证据让人知道:这一次,它是真的做了。