从零打造CyberClaw:真实工具与长期记忆落地
【从0开发类OpenClaw】Day 5:AI 终于"能干活"了——四个真实工具 + 一份差距分析 + 一颗长期记忆
从零写一个类 OpenClaw 的 AI 助手框架,全程记录踩坑。今天是第五天:把 Day 4 欠下的"工具占位"账一次性还清(web-search / translate / file-ops / shell 全部落地),顺手做了一份 OpenClaw 能力差距分析给自己定位,然后用 SDD(Spec 驱动开发)+ TDD 范式干了 Phase 2 的长期记忆——AI 不再只是"记得你这轮聊了什么",而是跨会话记住你是谁。
前情回顾:Day 1 立了 monorepo 架子;Day 2 后端出生,NestJS 管 CyberClaw.json,三套 CRUD + 联动校验;Day 3 智能体开口说话(SSE 流式 + langchain 双包陷阱结案);Day 4 还了两笔账(reasoning_delta 思考透出 + langgraph 会话记忆),但结尾自己立的 Flag 还在:真实工具执行器全是占位,agent 收到的是冷冰冰的「[工具未实现]」。
今天 Day 5 的主题就一个字:还账。而且这一天的剧情比预想的丰富——不止工具落地,还有差距分析、SDD 开发范式、一颗长期记忆,外加一场"历史对话点击没反应"的破案(差点又以为是自己写崩了)。
第一笔账:web-search + translate,搜索和翻译终于"真"了
工具占位是 Day 3 就欠的账。Day 5 开干,先把最常用的两个落地:web-search 和 translate。
坑①:国内网络,谷歌全家桶一个都连不上
写搜索工具的时候想得很美:Google → Brave → DuckDuckGo 三级 fallback,个个是海外大厂,稳。实测全崩——国内网络下 Google 搜索、Brave、DuckDuckGo 全部 UND_ERR_CONNECT_TIMEOUT,Google 翻译更是能拖 12 秒超时。
现实是残酷的,能用的只有:
| 服务 | 可用性 | 备注 |
|---|---|---|
| Tavily Search API | ✅ 可达 | 需要 API key(TAVILY_API_KEY) |
| Bing(cn.bing.com HTML) | ✅ 国内直连 | 免 key,解析 li.b_algo 结果块 |
| MyMemory 翻译 API | ✅ 国内直连 | 免 key,不支持语言自动检测 |
| Google / Brave / DuckDuckGo | ❌ 超时 | 只能当"有代理时的加分项" |
所以 fallback 顺序被实测教育成了:Tavily → Brave → Bing(cn) → DuckDuckGo;翻译是 Google(6 秒短超时,国内连不上就快速放弃)→ MyMemory。每条网络请求都挂 AbortSignal.timeout() 防挂起——这是写外部调用工具的底线。
坑②:流式工具参数分片,tool_start 拿到空壳
工具真跑了,又发现一个流式细节:模型输出工具调用参数是分片到达的(tool_call_chunks 一段段拼),tool_start 事件触发那一刻参数可能还是空的。解法:ChatService 里用 Map<toolCallId, args> 累积分片,tool_end 时回填完整参数——前端就能看到"这次工具到底传了什么"。
第二笔账:差距分析,给自己泼了盆冷水
工具落地途中,我抽空干了一件自我审视的事:把 OpenClaw 的能力逐项拉出来对比,写了份 docs/OpenClaw能力差距分析.md。结论很扎心:
对话引擎 ██████████ 9/10 ← 同级,这是我们的底子
配置管理 ████████ 8/10 ← 同级
模型接入 ██████ 6/10
工具执行 █ 1/10 ← 当时 8 个工具全是占位符!
长期记忆 ██ 2/10 ← 只有会话级记忆
渠道接入 █ 1/10 ← 仅 WebUI
调度/自动化 ▏ 0/10
多智能体 ██ 2/10
安全沙箱 ██ 2/10
综合 ≈ 2.7 / 9.2
CyberClaw ≈ OpenClaw 能力的 15~20%。但也看清了两件事:
- 架构是对的——对话引擎(SSE + reasoning_delta + 工具事件 + SQLite 线程记忆)和 OpenClaw 同级,且传输层与核心解耦,补能力都是"插拔"不是"重构";
- 路线图是清晰的——按投入产出比排出 Phase 1(真实工具)→ Phase 2(长期记忆)→ Phase 3(渠道+调度)→ Phase 4(浏览器/压缩/OpenAI 兼容端点)→ Phase 5(安全/沙箱/插件)。
第三笔账:file-ops + shell,AI 能碰你的文件系统了
搜索翻译落地后乘胜追击,把 file-ops(文件操作)和 shell(命令执行)也做了——这两个是"本地能力"的核心。
设计上最花心思的是安全边界:
- file-ops:read / write / list / mkdir / move / delete / stat 七种操作,但所有路径被锁死在工作区根内(复用
findMonorepoRoot()定位仓库根,FILE_OPS_ROOT可覆盖)——绝对路径拒绝、..越界拒绝,模型想读C:\Windows\win.ini?没门。 - shell:默认
enabled: false(要用户在配置里显式打开才生效),cwd 同样锁在工作区根内,输出截断 4000 字符防撑爆上下文,超时强制终止(默认 30s)。
坑③:return promise 的拒绝,try/catch 根本接不住
这个坑值得单独大写——它让我和 Jest 较劲了快一个小时,最后发现是 JavaScript 语言本身的陷阱:
try {
return readOp(full, pathArg); // ❌ readOp 的 promise 被拒绝时,catch 接不到!
} catch (err) {
return `[工具错误] ...`; // 这段永远不会执行
}
return someAsyncFn() 时,promise 的拒绝发生在 try 块之外——async 函数收养这个 promise 是在 return 之后,catch 鞭长莫及。症状极具迷惑性:文件不存在时 ENOENT 异常直接逃逸成 rejection,测试报错、堆栈里还看不到执行器函数(因为帧确实不在)。
解法就一个字:return await。
return await readOp(full, pathArg); // ✅ await 在 try 内,catch 接得住
教训:异步函数里 return promise 和 return await promise 在错误处理上不是等价的,后者才是"try 能兜住"的版本。
重头戏:SDD 范式开发 Phase 2——长期记忆
工具都真了,轮到差距分析里最要命的一块:长期记忆(当时 2/10)。这次没急着写代码,而是先按 SDD(Spec-Driven Development) 走流程——先写规格文档,再照着拆解实现。
第一步:一份能照着写的 Spec
参考项目里的 DEV_SPEC.md 结构,写了一份 docs/spec-phase2-long-term-memory.md:背景目标 → 用户故事 → 需求清单(FR/NFR)→ 架构图 → 目录结构(交付清单) → 任务拆解(每任务都带"目标 / 修改文件 / 实现类函数 / 验收标准 / 测试方法"五段式)→ 进度跟踪表 → 验收标准 AC-1~8 → 风险对策。
核心设计决策(ADR):
data/memory/ ← 已 gitignore,人类可读的 Markdown
├── MEMORY.md 长期记忆(每次会话注入 systemPrompt)
├── USER.md 用户画像(可选层)
└── journal/YYYY-MM-DD.md 日记(工作记忆,检索范围含近 3 天)
注入 = 拼 systemPrompt ← 模型"天生知道",不用工具调用
检索 = 纯关键词打分(零依赖) ← 中文二元组 + 英文分词,向量检索留接口
写入 = 追加 + 日期前缀 + 50KB 上限 + Promise 写队列
四个硬约束:零新 npm 依赖、记忆全在 gitignore 的 data/、路径由代码生成、MEMORY_INJECT=0 一键关闭。
第二步:TDD 红绿循环,8 个任务 46 个测试
照着 Spec 的 A1→D2 逐个打:
| 阶段 | 内容 | 测试 |
|---|---|---|
| A1 | MemoryStore 存储层(读写/上限/写队列/注入拼接) |
17 用例 |
| A2 | search.ts 关键词检索纯函数 |
12 用例 |
| B1 | memory 工具执行器(add/search/list)+ 注册 + 配置同步 |
12 用例 |
| B2 | chat.service 注入 systemPrompt(三态测试) | +3 用例 |
| C1 | USER.md 画像 + journal 日记层 | A1 已覆盖 |
| C2 | MemoryModule 单例 + GET /api/claw/memory 调试接口 |
2 用例 |
坑④:Nest 的模块隔离——token 不是全局的
MemoryController 放在 ClawModule,MEMORY_STORE 却注册在 ChatModule——启动直接报 Cannot resolve dependency Symbol(MEMORY_STORE) ... different module hierarchy。
Nest 依赖注入是模块隔离的:token 必须由本模块或 imports 进来的模块提供。解法:建独立 MemoryModule(providers + exports),ChatModule 和 controller 都 import 它。顺带两个 TS 细节坑:export const x = new Class() 不能写在 class 声明之前(模块加载即执行,TDZ 报错);构造器 param?: T 后面不能跟必填参数(TS1016),要写成 param: T | undefined + @Optional()。
第三步:端到端,"跨会话记住你"那一刻
spec 里最动人的验收标准 AC-3:新开一个会话,模型还能复述之前记住的信息。验证过程:
对话 1: "请用 memory 工具记住:我是一名律师,擅长知识产权方向"
→ SSE: tool_start: memory → tool_end: ok=true "已记住:用户是一名律师…"
→ 落盘 data/memory/USER.md(模型自动选了画像层,注入照样读取)
对话 2(全新 conversationId):"你还记得我的职业吗?"
→ 模型:"当然记得!您是一名**律师**,擅长**知识产权**方向。😊"
那一刻和 Day 4 的"你是小李呀"一样,值得截图。而且这次是跨会话的——不是同一会话连续聊,是换个新线程,AI 依然记得你。
破案:历史对话点击"无反应"——这次真不是代码的锅
Phase 2 全部提交后,用户反馈:页面上历史对话点击都没反应了。
第一反应:完了,是不是记忆模块把历史加载搞崩了?查证过程:
GET /api/claw/chat/history逐个会话测试——模型启用的 agent 返回 200 且历史完整,另外两个返回 422"智能体关联的大模型未启用";git show确认 422 校验逻辑早在 Phase 2 之前就在,本次 diff 一行没碰;- 前端
chatbot/index.tsx最后一次修改是 Day 4 的 commit,Phase 2 从未动过它; - 前端 98 个测试全绿,后端日志无异常。
破案了:deepseek-v4-flash 模型(agent 关联的)enabled 字段不知何时丢了(未启用),而页面默认选中的第一个 agent 恰好关联它——所以用户看到的历史会话全部 422。前端 loadHistory 又是 catch 后静默返回空数组,配置问题被完美隐形——看起来就是"点击无反应"。
数据一分没丢,还是那句话:90% 的"坏了"是配置/显示层误会,先查后端,再动前端(Day 4 的破案心得再次应验)。
修复分两层:
- 立即恢复:把 deepseek 模型
enabled: true补上,历史秒回 200; - 根治体验:
loadHistory不再静默吞错——失败时把后端错误信息(如"智能体关联的大模型未启用")抛出来,前端message.error明确提示。以后配置再出问题,一眼可见,不用再"破案"。
验证:159 + 100 全绿,跨会话记忆实锤
- 后端 API:159 个测试全绿(Day 4 是 75,本日 +84:4 个工具执行器 49 + 记忆模块 31 + 注入三态 3 + 调试接口 2)
- 前端 webui:100 个测试全绿(+2:历史加载失败提示)
- api / webui
tsc --noEmit双零错误 - 端到端:跨会话复述通过;
data/memory/落盘验证;GET /api/claw/memory调试接口可用 - 开发服务器双活,热更新正常
今天的成果
* 468f5da fix(webui): surface history load errors instead of silently clearing
* 942404e merge: feature/memory → dev (long-term memory)
* a4313f6 feat(memory): long-term memory (memory tool + systemPrompt injection + debug API)
* 559752b merge: feature/tools → dev (real tool executors)
* a97ac8d feat(api): implement real tool executors (web-search/translate/file-ops/shell)
(5 个 commit 已合入 dev,尚未推送 origin——按 Day 4 的习惯,明天收尾一起推。)
- 四个真实工具落地:web-search(四级 fallback)、translate(短超时兜底)、file-ops、shell(安全边界 + 默认关闭)
- 自我定位清晰了:差距分析 15~20%,路线图 Phase 1-5 排好
- SDD + TDD 范式跑通:spec 先行 → 五段式任务拆解 → 红绿循环 → 端到端验收
- 长期记忆上线:MEMORY.md / USER.md / journal 三层,systemPrompt 注入,跨会话"记得你"
- 历史点击疑案结案:不是代码回归,是模型配置失效 + 前端静默吞错——错误提示已根治
明天干点啥(Phase 3:渠道 + 调度)
差距分析路线图的下一站,目标从"网页里的 AI"变成"随身 AI":
- Telegram Bot 渠道——我们的
streamChat是纯 AsyncGenerator 与 HTTP 解耦的,接渠道只是插拔:SSE 事件流转成 Telegram 消息(文本 + Markdown),TELEGRAM_BOT_TOKEN环境变量 + 长轮询,预计 100~200 行就能让机器人进手机。这是差距分析里 0→1 最大的一个能力域(渠道 1/10),也是"随身助手"的第一步 - 飞书 Bot 渠道——参考Telegram Bot,只不过各自的接入api上有区别
- 定时任务调度(简化版 automations)——
@nestjs/schedule的 cron 能力,先做最实用的两种触发:定时(每天固定时刻干件事)和间隔(每 N 小时巡检)。配合 Phase 2 的记忆,可以让 AI 做"每天早晨汇总昨日日记"这类真活,也为后面 dreaming 记忆整理铺路 - 顺手补的体验项——Phase 2 spec 里挂着的扩展:向量检索(embedding API 换掉关键词纯函数,接口已预留)和 journal 过期清理(依赖调度能力,正好和 2 一起做)
今天到这儿。AI 从"会说话、记得你"进化到"能干活、跨会话记得你"——距离"随身助手"还差一个手机入口和一颗定时的心。
仓库地址:https://github.com/Li-Esquire-codeverse/CyberClaw(欢迎 star 监督进度)
更多推荐


所有评论(0)