做一个靠谱的定时智能体:官方“每日简报”参考实现拆解
根据 Anthropic 的文章整理:用 Claude Managed Agents 做一个定时读取 Slack 和 GitHub、把要点发到 Slack 的智能体。按来源、去向、代理、定时、记忆、护栏六个部分,讲清每一处常见的失败和官方的六条规则。
本文根据公开分享整理,本站尚未实测。操作前请对照文末的原始来源。
这是“Claude Code 开发系列”的第八篇。
这一篇解决的问题是:定时在后台跑的智能体,很容易悄悄地坏掉。 某个来源读不到了没人发现,同一件事报了两遍,或者你改过的偏好它还在按旧的来。原文用 Claude Managed Agents(测试版)做了一个参考实现:定时读 Slack 频道和 GitHub 的 pull request,记住上次读到哪里,把你需要知道的事发到一个 Slack 频道。
如果只想要结论,是这六条规则:
- 每个来源从书签读起,不按固定的时间窗口。
- 读取失败要报告为“读不到”,不能当成“今天没事”。
- 发出之前,把每一条再核对一遍。
- Slack 确认之后才算发出,然后再更新书签和台账。
- 每次运行都重新读偏好,偏好放在代理改不了的地方。
- 只需要读的地方只给只读权限,并给每次运行设花费上限。
即使你不用 Managed Agents,这六条对任何定时任务都适用。
怎么拿到代码
原文给了参考实现的仓库(链接在原文里),也可以让 Claude Code 带你走一遍。先更新:
claude update
然后在 Claude Code 里运行:
/claude-api managed-agents-onboard https://claude.dev/blog/building-effective-agent-automations/
claude-api skill 会读这篇文章,提出一套配置,把文件写到项目的 agents/ 目录,并用 ant apply 创建资源。原文说把它当作起点,再按自己的来源、去向和偏好去改。
需要准备的:一个 Slack 应用(用仓库里的 manifest 创建)和一个 GitHub token。
文件结构:
daily-brief/
├── agent.md 模型、工具、指示
├── deployment.md 定时、时区、预算、每次运行的第一条消息
├── environment.yaml 网络白名单
├── memory_store_preferences.yaml 用户的偏好
├── memory_store_state.yaml 代理自己的书签、台账、笔记和运行记录
├── vault.yaml 存放凭据的保险库
├── claude-lock.json 资源 ID,由 ant apply 写入
└── slack/manifest.yaml 一个机器人应用
ant apply 是 ant 命令行工具的一个命令:读这些文件,在你的 Claude API 工作区里创建对应的资源,把 ID 记到 claude-lock.json。配置好之后,任务在 Anthropic 的基础设施上按时运行,你自己的电脑不用开着。
下面按原文的顺序讲六个部分。
一、来源:读哪里
默认两个来源:Slack 频道和 GitHub 的 pull request。具体是哪些频道和仓库,写在偏好文件里。
给代理单独的、限定范围的凭据
Managed Agents 把凭据放在保险库(vault)里。代理能引用它们,但真实的值留在保险库里,不进入 Claude 运行代码的沙箱。两种用法:
| 方式 | 例子 | 凭据怎么走 |
|---|---|---|
| MCP 服务器 | GitHub | 代理通过沙箱外的代理服务调用 MCP 工具,由它按服务器的 URL 找到对应的凭据 |
| 命令行 | Slack | 代理在沙箱里用 curl 调 Slack API。沙箱里只有一个占位符 $SLACK_BOT_TOKEN,请求离开沙箱时,平台才对你允许的主机换上真实的 token |
创建保险库:
ant apply vault.yaml
然后用 TypeScript SDK 把每个凭据加进去。原文里添加 Slack 凭据的例子:
const vaultId = process.env.VAULT_ID!; // 保险库的 ID,来自 claude-lock.json
await client.beta.vaults.credentials.create(vaultId, {
display_name: "SLACK_BOT_TOKEN",
auth: {
type: "environment_variable",
secret_name: "SLACK_BOT_TOKEN",
secret_value: process.env.SLACK_BOT_TOKEN!,
networking: { type: "limited", allowed_hosts: ["slack.com"] },
injection_location: { header: true },
},
});
最后把 claude-lock.json 里的保险库 ID 填进 deployment.md 的 vault_ids。
从上次读到的地方接着读
常见的错误:让代理读一个固定的窗口,比如“过去 24 小时”。这次跑晚了就漏一段,跑早了就重复。
做法:每个来源一个书签。每次运行结束时,代理把每个来源里读到的最新一条的时间戳写进 bookmarks.json,例如 "slack": "2026-09-14T13:02:11Z"。下一次从书签开始,窗口自动伸缩,正好覆盖上次之后的全部内容。
书签存在名叫 state 的记忆库里。记忆库就是一个文本文件的文件夹,平台把它挂载到每次运行的沙箱里的 /mnt/memory/ 下,并在两次运行之间保留。代理用普通的文件工具读写。
别把“读取失败”当成“今天没事”
常见的错误:MCP 服务器挂了或者 token 过期了,运行照样开始,只是没有那个服务器的工具。会话日志里有一条错误,但代理从这个来源什么也看不到,于是汇报“没有新内容”。
做法:agent.md 里有三条规则。某个来源失败时,代理要:
- 让这个来源的书签停在原处;
- 用其他来源照常写简报;
- 在简报最后加一行,点名读不到的是什么(例如“本次无法读取 pull request”)。
二、去向:写到哪里
模板只往一个 Slack 频道发,每次运行发一条带日期的帖子。代理在沙箱里用 bash 工具发,用的是读取时的同一个机器人 token。
发帖不需要谁批准:内置的 bash 工具默认不询问就运行,slack.com 也在环境的白名单里。就是一个请求:
curl -s https://slack.com/api/chat.postMessage \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{"channel": "C0123456789", "text": "Daily brief, Tue Sep 15 ..."}'
确认发出去了,再记账
常见的错误有两种。记了一条其实没发出去的帖子,书签往前走了,那些内容永远不会被报告。或者拿不准第一次发没发成,又发一次,读者收到两份。
做法:agent.md 里的三条规则。
- 发之前,先在频道最近的消息里找今天的标题,已经有了就不发。
- 只有 Slack 返回
"ok": true和消息的 ts,才算发出。 - 得到确认之后,才更新台账和书签。结果不明确时,把这次运行标成“可能已发”,其他什么都不动,这样不会丢东西。
代理在记忆库里给每次运行留一份记录(runs/<日期>.md):发之前标“发送中”,之后标“已发”并带上消息 ID,或者标“可能已发”。
三、代理:模型、工具和步骤
在 Managed Agents 里,一个代理是一份带版本的配置:模型、系统提示词和工具。每次运行按步骤做完就停。参考实现的 agent.md:
---
name: Daily brief
model: claude-sonnet-5-5
mcp_servers:
- type: url
name: github
url: https://api.githubcopilot.com/mcp/
tools:
- type: agent_toolset_20260401
configs:
- name: web_search
enabled: false
- name: web_fetch
enabled: false
- type: mcp_toolset
mcp_server_name: github
default_config:
permission_policy:
type: always_allow
---
[八个编号的运行步骤,全文见仓库里的 agent.md。]
头部写名字、模型、工具和 MCP 服务器,正文是给代理的指示。注意一点:MCP 工具默认要询问批准,而定时运行时没有人在场,所以 GitHub 工具集设成了 always_allow,同时 GitHub 的 token 是只读的。放开批准和收紧权限是配套的。
八个步骤里,原文展示了两个。
让简报保持简短
第 4 步“取舍”的大意(译自原文):
4. 取舍。一条内容值得占一行,是因为读者今天会据此行动,或者它会改变读者即将做的一个决定。
拿不准就不写。多数日子是几条,有时一条也没有。
一个数字(“12 个待评审”)不算一条内容,把其中被卡住的链接出来。
已经在台账里而且还没解决的,只带一行标注(“仍在等待,第 3 天”),不重新报告;
已经关闭的直接去掉,不用说明。偏好文件里已经停掉的话题,不要再提。
发之前再核对一遍
从读取到发出之间,事情可能已经变了。第 5 步“核实”的大意(译自原文):
5. 核实。你读的时候,世界在变。对每一条要报告的内容,发之前到它的来源再查一次:
读完之后已经解决的,去掉;还没解决但有变化的,改这一行;
确认不了的,去掉,并记到运行记录的“删去”里。
一条过时的“还在等你”,比漏掉十条更伤信任。所以不要对状态含糊其辞:要么肯定地说,要么不说。
每个链接都从来源自己的链接字段复制(pull request 的 html_url、Slack 的 permalink),不要自己拼。
四、定时
代理只是一份配置,要由一个“部署”来运行它。部署指定用哪个代理、哪个环境、每次运行的第一条消息,还带着定时、保险库、记忆库和预算。每次到点,平台开一个全新的代理会话。
模板的 deployment.md,正文就是第一条消息:
---
name: Daily brief
agent: ./agent.md
environment_id: ./environment.yaml
schedule:
type: cron
expression: "32 7 * * 1-5"
timezone: America/New_York
vault_ids: [vlt_...] # 在“来源”一节创建的保险库
resources:
- path: ./memory_store_preferences.yaml
access: read_only
instructions: The reader's preferences. Re-read them every run. Never write here.
- path: ./memory_store_state.yaml
access: read_write
instructions: Your state. Bookmarks, ledger, notes, proposals, and run records.
---
Write today's brief.
The reader's time zone is America/New_York. Work out every date in that zone.
Follow your run steps in order. Today's edition is titled "Daily brief, <weekday> <month> <day>".
这份配置的意思是工作日早上 7:32(纽约时间)运行。创建部署:
ant apply deployment.md
不想等到点,可以手动跑一次:ant beta:deployments run --deployment-id <id>,ID 在 claude-lock.json 里。
按你的时区算日期
常见的错误:代理按服务器的时区算日期,把今天早上说成“昨天”。
做法:两处都要写。timezone 字段决定什么时候触发,正文第二行告诉代理算日期用哪个时区。国内用户两处都改成 Asia/Shanghai。
五、记忆
每次运行都在一个全新的沙箱里开始,不记得上一次。没有记忆,你的反馈留不住。但过时的记忆也会添乱:一件事已经解决了还报“仍在等待”,或者一件还没解决的事因为“已经报过”被丢掉。
模板用两个记忆库:
| 记忆库 | 属于谁 | 权限 | 放什么 |
|---|---|---|---|
| preferences | 你 | 代理只读 | 读哪些频道和仓库、哪些不要、长度上限、发到哪里、什么时候停 |
| state | 代理 | 读写 | 书签、报告过的内容的台账、每次运行的记录、它想对你的偏好提的修改建议、各个来源的特点(比如“只返回最新的 50 条”) |
ant apply deployment.md 会创建 preferences 这个库,但不会创建里面的文件。第一次运行之前,要用仓库里的 scripts/seed-preferences.sh 把你的 preferences.md 写进去。
每次运行开始时重新读偏好
常见的错误:把偏好抄了一份写死在提示词里,你已经改了的规则它还在执行。
做法:让代理每次都读文件。读不到就停下来说明,而不是按默认值跑。
记一本台账,只报告变化
代理在 ledger.md 里记下每一条报告过的内容,简报才不会重复。每行记四样:什么时候报的、来自哪里、一个不会变的 ID(Slack 消息的时间戳或 pull request 的编号)、最后知道的状态。
2026-09-09 slack:C0123456789 1788963600.000100 refund thread: customer waiting on a decision
2026-09-11 github 481 review blocked, day 2 (still waiting)
2026-09-11 slack:C0234567891 1789117333.000300 enterprise escalation: owner named, in progress
六、护栏
任务在后台按时运行,没有人盯着,所以要限制它能做什么、能花多少。
限制它能做的事
代理读的是别人写的消息和 issue,这些文字可能被当成指令。办法是限制它即使照做了也能造成多大的后果:
- GitHub 的 token 是只读的;
- preferences 记忆库是只读的;
- 环境只能访问白名单上的主机。
原文对剩下的风险说得很直接:被人埋进去的指令仍然可能改变简报的内容,包括通过代理在两次运行之间保留的笔记。但它写不了 GitHub,也改不了你的规则。
Slack 是例外:同一个 token 既能读也能发。所以只把机器人邀请进它需要读或发的频道。
根据实际运行来设花费上限
- 起步设成一次正常运行成本的 3 到 5 倍,看到真实数字后再收紧。
- 达到上限的运行是暂停,不是失败。所以上限设得太低,看起来就像“简报不发了”。
- 每次运行都有完整的额度,达到后以
budget_reached为停止原因暂停。
上限写在 deployment.md 的 budget 里:
budget:
type: limit
max_list_cost:
amount: "500" # 字符串,单位是美分:"500" 就是 5 美元
currency: USD
对你有什么用
只是用 Claude Code 的人:
- 你用 Claude Code 的定时任务、
/loop或云端会话做的任何周期性工作,都可以拿开头那六条来对照。最容易忽略的是第 2 条:一个什么都没报的结果,要分得清是真没事还是没读到。 - 给后台任务的权限按“它被骗了会怎样”来定,而不是按“它正常工作需要什么”来定。
想自己搭一个的人:
- 从官方的命令和模板开始,改来源和偏好,不要从零写。
- 上线前手动跑几次,看一次的真实花费,再设上限。
- 时区两处都要改。
- 这是测试版功能,字段和命令可能会变,动手时以仓库和官方文档为准。