Berth Bot 开发地图:写什么、怎么搭、有哪些词

模型和对话循环都是现成的,Anthropic 给脑子,Agents SDK 给循环。我们真正花时间的地方是四件事:给它什么工具,喂它什么上下文,什么时候叫它起来干活,哪些事必须先问人。

01我们写哪些代码

按工作量从大到小排。横条只是我读目录结构后的粗略感觉,不是统计出来的行数。

工具

agent 能做多少事,取决于你给它多少工具。现在有两类只读工具:查 HL(行情、K 线、账户、挂单、成交、组合表现)和查网页(Exa / Tavily)。还有一类 propose 工具,只能「提议」写画像或建 Routine。

researchTools.ts · ResearchService.ts · WebResearchService.ts · proposeTools.ts
调度与触发

让 bot 不用人叫也会干活。定时 Routine 靠 DO alarm;条件 Routine 先用普通代码判断条件,命中了才叫模型。

RoutineService · ConditionCheckService · conditionEvaluator · nextFireAt · routineAlarms.ts
模型接入与成本

接四家 provider,保存 key 时先真调一次验活,把各家五花八门的报错归成几类,按 Run 记 token 用量和费用。

providerClient · probeModel · classifyProviderError · runUsageMiddleware · estimateModelCost
上下文组装

代码不多,但最影响效果。system prompt 里放职能说明、画像和工具使用规则;大块数据先压缩成摘要再给模型。

systemPrompt.ts · threadSummary · candleSummary
人在回路

模型想改持久数据时,只能弹一张确认卡。答这张卡的只能是用户带会话的 HTTP 请求,模型自己答不了,WebSocket 帧也答不了。

proposals.ts · AgentRuntime.answerProposal
底座适配与存储

把 Think 接到我们的端口上,装配所有者 DO,写 SQLite 迁移和 repository。

thinkRuntime.ts · BotOwner.ts · infrastructure/sqlite/
外围

HTTP 路由、推送、休眠和硬删、全局暂停、监控指标、每小时一次的 HL 红线巡检。很琐碎,但上线前一样都不能少。

routes/* · PushService · OwnerLifecycleService · platform/*

一个工具由什么组成

写工具最容易低估的是描述。模型看不到你的代码,只能读描述来决定什么时候用、怎么用。

描述写给模型看的说明书:什么时候该用,什么时候别用。写含糊了,模型会乱调或者不调
入参 schema用 zod 的 strictObject,模型多编一个字段就直接拒掉。不严格的话,模型会自己发明参数
出参裁剪K 线最多 200 根,榜单最多 20 条,成交最多查 30 天。不裁剪,一次调用就能撑爆上下文
错误回传出错时返回一个错误码给模型,让它自己决定换个方式,而不是整轮崩掉。直接抛异常,用户只会看到「出错了」

02整体架构长什么样

从上往下读:客户端怎么连进来,Worker 里有什么,往外又连了谁。

MobileApp
Web浏览器
桌面Electron
apps/bot · Cloudflare Worker(bot.berth.pro)
Hono 路由/ws · /bots · /credential · /me …
BotRouter验 ticket,按用户分发
BotOwner · 每个所有者一个 Durable Object
SQLitebot / 线程 / Routine / Run / 画像
Think 循环经 AgentRuntime 端口
Alarm到点叫醒跑 Routine
KV全局暂停开关
Secrets Store加密模型 key 的 KEK
Analytics Engine运营指标
Cron每小时 HL 红线巡检
模型厂商Anthropic · OpenAI · DeepSeek · OpenRouter,用户自带 key
Hyperliquidmainnet,只读
apps/serverRPC 验会话;推送走队列
Exa / Tavily网页检索

业务代码住在 contexts/bot(领域、应用、存储),apps/bot 只是壳:把 DO、路由、bindings 拼起来。这跟仓库里其它 context 的分法一致。

03几个关键设计,以及为什么

每条都是一个取舍。看懂「为什么」,比记住「是什么」有用。

每个所有者一个 Durable Object

一个用户的全部 bot、线程、Routine 都在同一个 DO 的 SQLite 里。DO 是单线程的,不用加锁,用户之间天然隔离,WebSocket 也直接挂在上面。代价是一个用户的负载没法拆到多台机器上,不过一个人的 bot 量远远到不了那个级别。

中间隔一层 AgentRuntime 端口

业务代码只认 submit / wait / abort / thread / answerProposal 这五个方法,不认识 Think。Agents SDK 还在 experimental,版本锁死在 0.30.0。哪天它不好用了,只换 thinkRuntime.ts 这一个文件,注释里也写了 pi 是备选。

一个 bot 恒一条线程,一次唤起算一个 Run

没有「新建对话」。bot 的所有产出都落进同一条线程,用户回头看只有一个地方。Run 是记账单位:花了多少 token、为什么失败、是谁触发的,都挂在 Run 上。

用户聊天和服务端派活走两条路

用户发消息走 Think 自带的聊天协议,流式输出、断线重连都是现成的。Routine 和确认卡续跑这类「服务端主动」的活,走端口的 submit。两种来源在线程里分开显示,Routine 不能冒充用户发言。

Bot 单独一个 Worker,不塞进 apps/server

Worker 一发版,名下所有 DO 都会重启,所有 WebSocket 都会断。拆开以后,bot 发版不会连累主 API。两边只能 bot 单向调 server,用户数据库从不绑定到 bot 这边。

永远不是金融状态的权威源

HL 一律只读 mainnet。模型再怎么出错,也写不坏账本。将来 M2 要下单,走的是 ADR-0188 那套服务端托管 agent key 的拓扑,动钱之前还要过手机确认这一关。

成本护栏放在代码里,不靠模型自觉

每个 Run 最多调 12 次工具(RUN_TOOL_CALL_BUDGET),条件 Routine 先用代码判断,最后有配额兜底,用户自带 key 所以账单在他自己那儿。模型很容易陷进「再查一次」的循环,这条硬上限是最后一道闸。

拿不准就停(fail-closed)

全局暂停开关存在 KV 里,读不到时就当作「已暂停」。宁可 bot 少跑一次,也不要在该停的时候还在跑。

测试不打真网

HL、检索、四家模型全部换成替身。模型替身靠一种小脚本驱动,比如 [tool:NAME*N@{json}] 表示「调 N 次这个工具」,这样编排逻辑能确定地测。但要注意,这测不出模型本身答得好不好,那需要另做评估。

04走一遍:风控巡检半夜被触发

用一个具体场景把上面的东西串起来。绿色步骤不花模型钱,蓝色步骤花。

  1. DO 的 alarm 到点响了,所有者 DO 被唤醒。不花钱
  2. 先读 KV:平台是不是暂停了?所有者是不是休眠了?只要有一个是,就到此为止。不花钱
  3. 条件 Routine 用普通代码去 HL 拉数据,判断「强平距离是不是小于阈值」。没命中就记个时间,等下次再说。绝大多数巡检在这一步就结束了。不花钱
  4. 命中了。通过端口 submit 一个 Run,来源标成 routine,并带上命中的数据。开始花钱
  5. Think 组装上下文:职能说明、画像、线程历史、工具清单、命中数据。然后交给模型。花钱
  6. 模型可能会再调几次工具看看细节(最多 12 次),最后写出一段解读。花钱
  7. 解读落进这个 bot 的线程,按推送策略投到队列,由 server 发出通知。Run 结算,记下 token 和费用。不花钱

第 3 步是整套设计里最省钱的一招。要是每次巡检都让模型来判断「危不危险」,一个用户一天就要叫好几十次模型。

05核心概念

先看它们之间是什么关系,再看每个词的意思。

所有者 (一个 user_id = 一个 DO) ├─ 模型凭证 所有 bot 共用 ├─ 画像 所有 bot 共用的长期记忆 ├─ 配额 · 状态 活跃 / 已停用 / 休眠 └─ Bot × N ├─ 名字 · 头像 · 职能说明 ├─ 线程 恒一条 │ └─ 消息 … ├─ Routine × N 定时 / 条件 └─ Run × N 每次唤起一个,来源:用户 / Routine / 确认续跑
所有者 Owner
拥有一组 bot 的那个人,用 user_id 当键,不用钱包地址。只读观察的账户建不了会话,所以没有 bot。
Bot
一个职能分身。同一个人的 bot 共享凭证、画像和配额,数量不设上限。代码里别叫它 agent,那个词留给 HL 的 Agent Wallet。
职能说明
bot 的角色设定,决定它关注什么、怎么说话。最终会进 system prompt。
模板
官方给的起点,现在只有早报员、风控巡检、复盘员三个。实例化之后就跟模板脱钩了。
线程 Thread
用户和一个 bot 之间唯一的对话。bot 的回复、Routine 的结果都落在这里。
Run
一次唤起。有状态、有失败原因、有 token 用量,是记账和排查的单位。
Routine
例行任务,分定时和条件两种。bot 会「主动」找你,全靠它。
画像 Profile
所有 bot 共享的偏好和事实,比如常看哪些币、风险容忍多高。bot 自己没有独立记忆,它记得的只有画像和自己的线程。
提议 / 确认卡
bot 想改持久数据时只能提议,用户点了确认才落库。跟 M2 交易的审批是两回事。
模型槽 ModelSlot
聊天和 Routine 可以用不同的模型,比如聊天用贵的,巡检用便宜的。
休眠 / 硬删
30 天没连上就自动休眠,连上即唤醒。停用或休眠满 180 天,全部数据硬删。
全局暂停
平台级的维护开关,改 KV 就生效,不用发版。