上一篇文章回答的是“为什么要把 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
│ │
└─────────┬─────────┘
▼
ExecutorCHAT:不要过度工程化
比如“这段话是什么意思”。
这类请求不需要建立任务身份,直接走快路径。它的主要目标是低延迟和可见回复。
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 ProjectionQ 与 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
COMMITRUN 也是类似逻辑:
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 | 含义 | 例子 | 能不能单独证明完成 |
|---|---|---|---|
E0 | Observation | 页面出现、DOM 活动、某个 turn 可见 | 不能 |
E1 | Self-report | Worker 自己说 RUNNING / DONE | 只能做临时状态依据 |
E2 | Artifact | 文件、diff、branch、产物真实存在 | 能证明“产出了东西” |
E3 | System verified | 确定性测试、查询、审计通过 | 能支撑验证完成 |
E4 | Landed | 目标 Production / 数据 / 内容面已存在结果 | 支撑真正落地 |
E5 | Accepted | 用户或明确验收规则接受 | 需要验收的任务最终闭环 |
这一套最直接地解决了“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 Agent | Heavy 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 monotonicity8 月 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 不能只会讲话,它必须能进入真实系统、承担真实动作、暴露真实失败,并且留下足够的证据让人知道:这一次,它是真的做了。