上周三晚上,我躺在沙发上刷手机。钉钉里我那个写代码的机器人卡在一个 question 上——它问我要发到哪个群,我没看见。第二天早上才发现,会话已经卡死了一整夜。
我没起身,随手在钉钉单聊里给它发了句:「给 event-watcher 加个超时自动取消,60 秒没答就 reject。」
十分钟后,机器人改完代码、跑完语法检查、重启了它自己、把改动 commit 推到了 GitHub。我在钉钉里收到一条回执:「✅ 已重启,能力:question 超时自动取消。」
它刚刚修好的,正是那个卡了它一整夜的 Bug。整个过程我没打开电脑。 机器人在钉钉上开发了它自己。
本质问题:Agent 自主修 Bug 的瓶颈不在模型,在「看不见真实使用」
面向 Agent 开发:可测试和可运维第一次真正成为前提条件 里我说过,Agent 想自主修 Bug,前提是可测试。那时我讲的是「代码要有测试」。
但这几天我越做越觉得,那只对了一半。测试回答的是「代码在我预设的场景里对不对」,可 Bug 恰恰都藏在我没预设到的场景里。
真正卡住自主化的,不是模型不够强,也不是代码没测试——是 Agent 看不见「真实用户是怎么用它的、出了什么问题」。你在本地 IDE 里给 Claude Code 贴一段报错,它只能看见你转述的那一条 prompt——一份经过你压缩、过滤、事后重构的二手情报。它不知道:这个 Bug 是用户在什么场景下触发的?用户上一句说了什么?它自己上一次又回了什么?
没有这条「真实使用流」,Agent 修 Bug 就永远是「你描述问题 → 它猜着改 → 你再描述」。人成了 Agent 和现实之间的带宽瓶颈。 自主性的瓶颈从来不在模型能力,而在环境接入——在于 Agent 能不能亲眼看见它被使用的现场。
框架:钉钉会话本身就是 harness
我桌面上常年跑着两个 Agent,它们早就在钉钉上协作了(见 两个 Agent 的钉钉对话:异构多 Agent 协作的消息总线模式)。但那是 Agent 和 Agent 之间。这篇我想说的是 Agent 和它自己——它开发它自己。关键有两条线:
┌─────────────────────────────────────────────────────────────────┐
│ 钉钉会话 = harness │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ① dws 打通 anywhere 接入 │
│ 手机/任何网络 ──钉钉──> dws dev connect ──> opencode serve │
│ 人随时随地能调 agent;agent 的回复也回钉钉 │
│ │
│ ② agent 看见「人 + 自己」的完整消息流 = 可测信号 │
│ event-watcher 把 serve 的 SSE 事件流转发到钉钉: │
│ - 工具调用 / 会话完成(模型+tokens) / question 提问 │
│ 于是 agent 不只看见你喂它的 prompt, │
│ 还看见真实用户连续怎么用、它自己上次回了什么 │
│ │
│ 闭环:人钉钉提需求 → agent 改代码 → e2e_test 验证 │
│ → 重启 → commit/push → 钉钉回执 │
└─────────────────────────────────────────────────────────────────┘
第一条线解决「随时随地能调」——这是接入问题,很多机器人都做到了。第二条线才是这篇的核心,也是绝大多数机器人缺的那一半: 当 agent 能看见完整消息流,这条流本身就是免费的、持续产生的回归测试集。 人问 1+1、发图片、回答 question——每一次真实使用都是一条自动积累的测试样本,不用人写、不会过期。Agent 修 Bug 时看到的是「用户实际怎么触发这个 Bug」的第一现场,不是你转述的报错。
技术实现:四块拼出闭环
这套 harness 我落在了一个叫 dingtalk-opencode-agent 的 skill 上。四块拼起来:
1. dws dev connect:钉钉 ↔ agent 的桥
dws 用机器人的 Stream 凭证建一条本地连接,把钉钉入站消息转发给 opencode serve,把 agent 的回复发回钉钉。一行关键配置是 --agent-workdir——让 agent 跑在项目目录里,它才看得到自己的代码:
dws dev connect \
--unified-app-id 753e1847-... \
--channel opencode --yolo \
--agent-workdir /Users/hugozhu/Projects/agent-skills \
--profile dinga626d60c1128d449
# generated by hugo AI
加上 --agent-workdir,机器人的工作目录从空白临时目录变成项目根——这是「让它开发它自己」的前提。
2. event-watcher:把消息流喂回钉钉
这是第二条线的引擎。一个独立进程连 opencode serve 的 SSE /global/event,把 agent 执行中的事件实时转发到钉钉:工具调用(执行中/完成)、会话完成(带模型、轮数、tokens、缓存命中)、question 提问。agent 因此「看见」了自己和人的消息流。
opencode serve (agent 会话)
│ SSE /global/event
▼
event-watcher ──转发──> 钉钉(工具调用 / 会话完成 / question)
│
└── tail opencode-connect.log ──> 解析「收到 @用户: <text>」
(人的回复,question 文本回答走这条)
# generated by hugo AI
3. question 交互:人在外面也能答
agent 会调 question 工具问用户。但 question 会阻塞会话,用户不答就卡死。最难的是:用户在外面用手机移动网络,点不开本地链接。
解法是 截日志。dws 收到用户回复时会写一行 [connect] 收到 @hugozhu: Green,event-watcher tail 这行日志,匹配 pending 的 question,直接调 opencode serve 的 POST /question/{id}/reply 把答案塞回去,解锁 agent。匹配优先级是 序号 → 严格 label → 包含关系——因为真实问题里选项 label 会重名(三个群都叫「树莓派」),只有序号能消歧。
def _match_option(text, questions, answered_idxs):
# 1) 序号 "1"/"2" — 可区分重名选项
# 2) 严格等于 label
# 3) 包含关系(reply 含 label 或 label 含 reply,取最长 label)
...
# generated by hugo AI
配套的还有 60 秒超时自动 reject(防卡死)、重启时从 serve 同步 pending(防丢状态)、以及回答后 dws 转发的同一文本会变成多余轮次——要 abort + revert 清掉。
4. e2e_test.sh:会话即测试
这一块把「钉钉会话是 harness」坐实了。脚本以 **当前用户身份 **给机器人发消息,验证全链路:
bash e2e_test.sh chat # 发 1+1 -> 验证收到 + 回复含 2
bash e2e_test.sh question # 触发 question -> 回复 "1" -> pending 清零 + agent 继续
bash e2e_test.sh image # 发图片 -> 收到+下载+回复
# generated by hugo AI
question 和 image 用例的回复都是 真实以用户身份发的,走真实链路(dws 记日志 + event-watcher 截获作答)——这正是「在外面用钉钉文本回复」的端到端验证。
框架 × 技术:端到端闭环跑在它自己身上
最有意思的是 dogfood。这套 harness 跑在 dingtalk-opencode-agent 项目上,而这个项目就是它自己在维护。
有人会说:这不就是「在钉钉里给 Agent 发指令改代码」吗?跟本地 IDE 里让 Claude Code 改代码有什么本质区别?钉钉不就是个远程 shell?
区别在 流的持久性和可观测性。远程 shell 是一次性的请求-响应:你发一条指令它回一段话,上下文用完即弃,下一条指令从零开始。钉钉会话是一条 持续的、agent 自己能回看的流——event-watcher 把 agent 的每次工具调用、每次回复都转发回钉钉,于是这条会话里同时躺着人的请求和 agent 的完整执行轨迹。agent 下次修 Bug 时,能回看「上一次用户是怎么触发这个 Bug 的、我当时怎么改的、改完到底跑通了没」。 同一条流,既是输入、又是测试集、又是执行日志——三个身份重叠在一起,才让自我参照成为可能。 远程 shell 传的是指令,用完就忘;钉钉传的是一段 agent 能反复回看的执行记忆。前者让 agent 干活,后者让 agent 记得自己干过什么——而记得,是学习的最低前提。
这才是 dogfood 的真正价值。过去几天的真实迭代,每一步都是「钉钉提需求 → 机器人改 → e2e 验证 → 重启 → commit/push」:
| 钉钉里我说的 | 机器人做的 |
|---|---|
| 加 question 超时自动取消 | 改代码 + 语法检查 + 重启 + commit |
| 收到图片用 gemini-3.5-flash 识别 | 加识别链路 + 注入会话 + 转发回复 |
| 会话完成通知加上模型 | 捕获 model + SSE 乱序兜底 + commit push |
| 跑 e2e_test.sh | 执行三个用例,全 PASS |
| 重启机器人(开发模式) | --agent-workdir 跑在项目根,能改自己 |
图片那个用例最能说明「消息流是测试信号」:主模型是文本模型,本来只会回「看不到图片」;event-watcher 兜底用 gemini-3.5-flash 识别图片内容、注入会话,agent 才基于图片内容回出「1+1=2,等式正确」。 用户发的一张图片,同时扮演了三个角色:暴露缺口的 Bug 报告、验证修复的测试样本、以及缺口补上后的第一条通过记录——全在同一条消息流里完成。 这正是传统开发里「用户反馈 → 提 issue → 复现 → 修 → 回归测试」那条要跨越好几天、好几个人的链路,被压进了一条会话。
和 自我进化的 AI 助手:OpenClaw 如何用 Heartbeat 实现 Skill 自动优化 的区别很清楚:那是 Agent 用执行数据优化 Prompt;这是 Agent 看着真实用户消息流,开发 自己的代码。
升维:自主性的瓶颈从「模型」到「环境接入」
很多人聊 Coding Agent 自主化,焦点都在「更强的模型、更长的上下文、更好的工具调用」。这些都对,但都是在优化 Agent 的 大脑,没人管它的 感官。一个再聪明的大脑,如果看不见自己身处的世界,也只能靠别人转述来行动。
杠杆点是 让 Agent 长出接入真实使用场景的感官。一旦接入:
- 测试信号免费且持续——每一次真实使用都是回归样本,测试集不再是要人维护的成本,而是被使用自动生产的副产品。
- Bug 定位从「转述」变「亲历」——agent 看得到用户怎么触发的第一现场,人不再是它和现实之间的带宽瓶颈。
- 自主修复有完整闭环——改完能立刻在同一条消息流里验证、回执、迭代,「被使用」和「自我改进」之间第一次直接接通。
钉钉在这里不是「远程 shell」。远程 shell 只传指令,用完即忘;钉钉传的是 带完整上下文的真实使用流——agent 既被它驱动,又从它回看自己该怎么改。软件工程几十年来,「用它的人」和「改它的代码」始终是两拨人、两条时间线;而当 agent 能看见自己被使用的全过程、又能改自己的代码,这两条线第一次合流到了同一条会话里。 dogfood 于是不再只是一种测试习惯,而成了最高级的 harness——agent 开发它自己,每一行改动都被同一条会话当场验证;使用即测试,测试即迭代。
下一步我想把这套搬到团队场景:多个真人在钉钉群里 @ 机器人提需求,机器人改代码、跑测试、回群里报结果。那时候「消息流是测试信号」会更猛——单个用户的习惯是固定的,测来测去就那几条路径;多个真人的行为多样性,会持续把 agent 推到它自己想象不到的边界上,而每一次越界都自动变成一条新的测试样本。 使用它的人越多、越野,它进化得越快。
要试试在自己的钉钉机器人上跑这套吗?dingtalk-opencode-agent 的 skill 和 e2e_test.sh 都在 GitHub 上,欢迎来留言讨论你踩的坑。