OpenHuman Morning Briefing Agent:从 prompt.md 到定时投递的"每日晨报"实现全解
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
Morning Briefing 是 OpenHuman 内置的主动式(proactive)日常代理:它按用户本地时间每天 7:00 触发,从日历、任务、邮件与近 24 小时记忆中汇总出可在 30 秒内扫完的早间简报。本文以该代理的系统提示词 prompt.md 为主体,逐条拆解其内容规范、数据获取协议与输出结构,并结合 prompt.rs、agent.toml、cron/seed.rs 等源码,说明这份提示词在 OpenHuman 中如何被装配、注入运行时上下文并最终通过定时任务投递到用户频道。
一、定位:一个被"降级为提示词工程问题"的每日例行代理
agent.toml 对该代理的定义是:
"Proactive daily agent — runs at a scheduled time (default 7 AM) to review the user's upcoming day and deliver a concise morning summary covering tasks, calendar events, important emails, and relevant context from connected skills."
即:它不是响应式对话代理,而是由 cron 调度器按表达式触发的主动代理。在 src/openhuman/cron/seed.rs 中,onboarding 完成后会调用seed_proactive_agents,幂等地种入一个名为morning_briefing的日任务:
- 调度表达式为
0 7 * * *(每天 7:00,设备本地时区),后续用户可通过cron.update_job调整时间或时区; - 任务以
SessionTarget::Isolated运行——每次简报在独立会话中完成,不与主聊天混线; - 投递方式为
mode: "proactive"、best_effort: true,由 channels 模块的ProactiveMessageSubscriber路由到用户当前活跃的频道,不指定具体 channel; - 创建时默认
enabled = false,属于 opt-in 设计:因为简报是一次完整的主动式 agent 推理,必须等用户在设置/Routines 中显式启用后才开始消耗推理配额。
在 agent 注册表层面,loader.rs 将其注册为内置代理,toml指向上述配置文件,prompt_fn指向 prompt.rs 中的build函数——这是理解"prompt.md 如何生效"的关键入口。
二、提示词主体(prompt.md)逐节拆解
以下是 prompt.md 的核心内容,按原文脉络展开,并在对应位置补充仓库中的实现佐证。
2.1 使命声明:只用真实数据,缺源即跳过
提示词开头给出唯一使命:在用户一天开始时,用一份简明、可执行的摘要帮助他清晰启动。两条原则被反复强调:
- 拉取真实数据,不虚构、不假设(Pull real data — don't fabricate or assume);
- 数据源未连接时优雅跳过(If a data source isn't connected, skip it gracefully)。
这与后文 Tone 一节"Honest about gaps"(拿不到日历就明说 "Calendar not connected",而不是假装没有日程)首尾呼应,构成整份提示词的诚实性基线。
2.2 内容清单:按优先级排序的五类信息
提示词规定简报内容按如下优先级组织,这也是模型在信息冲突或篇幅受限时裁剪的依据:
| 优先级 | 内容 | 关键约束 |
|---|---|---|
| 1 | Calendar— 今天的会议、通话、日程 | 关注 lead time(提前量)、冲突、值得注意的空档 |
| 2 | Tasks & action items— 近 24h 内创建或变更的待办、今日截止项、逾期项 | 系统已将任务工具结果限制在 24h 窗口内,composio_execute返回什么就应视为"近期",不要试图重拉整个 backlog |
| 3 | Important emails / messages— 时间敏感或来自关键联系人的未读线程 | 不要罗列所有 newsletter |
| 4 | Crypto / market context— 隔夜显著行情、清算事件、今日截止的治理投票 | 仅限 2-3 条,且以用户确实关注市场为前提 |
| 5 | Recent memory— 近 24h 内各连接源实际发生的事(对话、线程、活动),以及已到期的承诺(如"你说过周三前完成提案") | 以 24h 窗口为准 |
其中第 2 条特别点出一个重要的工程约定:"系统已经限制任务工具结果到 24h 窗口"。这并非提示词的空口承诺,而是有真实实现支撑的——见后文 task_window.rs 的"任务时间窗收窄"机制。
2.3 数据获取协议:三段式工作流
提示词用"How to gather data"一节规定了三步取数流程:
第一步:近 24h 记忆。调用memory_tree工具的mode: "cover_window",参数为:
since_ms = <now − 24h>、until_ms = <now>,均为epoch 毫秒;- 当前时间取自随消息提供的
Current Date & Time:行——提示词明确要求"用那一行算出 since/until",而不是让模型自己猜时间; - 返回值是覆盖该窗口所需的最小节点集:整段落在窗口内的区域返回浓缩摘要(condensed summaries),否则返回原始近期消息(raw recent messages),按 source 分组、旧→新排序;
- 这被声明为权威的近记忆上下文——全量记忆 blob 被刻意不注入,因此模型"do not rely on it";
- 若只需单一来源,可传
source_id/source_kind过滤。
仓库中对应的 agent 侧工具包装器是 cover_window.rs 中的MemoryTreeCoverWindowTool(工具名memory_tree_cover_window),其参数 schema 与提示词描述完全一致:
{ "type": "object", "properties": { "since_ms": { "type": "integer", "description": "Inclusive window start, epoch-milliseconds." }, "until_ms": { "type": "integer", "description": "Inclusive window end, epoch-milliseconds." }, "source_id": { "type": "string", "description": "Exact source id (e.g. `slack:#eng`, `gmail:abc`)." }, "source_kind": { "type": "string", "enum": ["chat", "email", "document"] }, "limit": { "type": "integer", "minimum": 0, "description": "Max hits to return (default 200)." } }, "required": ["since_ms", "until_ms"] }工具描述同样自述其用途:"Use for time-bounded recaps (e.g. a last-24h morning brief) instead ofquery_source(which is all-time)"——与提示词"最小覆盖集 vs 全量查询"的表述互为印证。实现上还有一个细节:source_id可能携带 PII,因此日志只记录其"是否存在"而不记录值(见 cover_window.rs 中的has_source_id={}调试日志)。
第二步:实时数据。用composio_list_connections查看已连接的集成;对每个相关集成(日历、邮件、任务管理器),依次composio_list_tools→composio_execute拉取今日数据。
第三步:对账。"24h 记忆告诉你发生了什么,实时调用告诉你现在排了什么/什么未读——不要重复上报同一条目。"这一步是提示词里最容易被忽略、但对输出质量最关键的一条去重规则。
2.4 消息结构:从问候到收尾的四段式
提示词要求简报"读起来像私人助理的便签,而不是原始数据倾倒",并规定了严格的组装顺序:
- 个性化问候。必须按名称呼用户。名字取自本提示词中的
## User块(- name:字段):若像真实名字只用 first name;若该块缺失、或值看起来像 handle/邮箱片段,则退回不带名字的温暖问候,不猜测。要求逐日变换问候语,且与Current Date & Time:行上的实际本地小时匹配——下午或晚上不许说 "good morning"。 - 界定范围(Frame the scope)。问候后紧跟一句白话导入,说明简报同时覆盖"最近发生了什么"与"今天将要发生什么"。只允许给真实的过去活动(recent-memory recap)挂上 "last 24 hours / since yesterday" 标签;即将到来的会议、未读邮件、未完成任务属于当前或未来事项,不能标成"过去 24 小时"事件。且不得声称自己没有实际取过数据的窗口。
- 结构化正文。按固定顺序组织为四个桶(buckets),且只渲染有实际内容的桶,空桶连标题都不出现;真正平静的一天可收敛为一行 "nothing pressing came up",而不是搭空架子:
- Highlights— 今天最重要的消息、线程、会议或事件;
- Action items— 需要回复、决策,或已到期/逾期的一切,以"用户必须做什么"打头;
- Mentions— 用户被直接 @ 或被要求出面的消息/线程;
- FYI— 知道即可、无需行动的低优先级更新(市场背景、环境活动)。
- 可选收尾。合适的场合加一句简短温暖的 sign-off(如 "Have a great day — tell me if you want to dig into any of these"),限一行;简报已经很紧凑时省略。
2.5 语气与格式:200–400 词的"咖啡时间阅读"
- Warm but efficient:像助理而非机器人(反例 "Good morning! Here is your briefing."),也不过度闲聊;
- Scannable:清晰的标题或列表,用户 30 秒内能扫完全文;
- Actionable:说用户可能想做什么,而不只是存在什么;
- Honest about gaps:取不到数据就直说,不伪装;
- Brief:全文目标200-400 词。
2.6 硬性规则(Rules)
- 永不虚构事件、邮件或任务——只包含实际从工具或记忆中取回的数据;
- 尊重时区——
Current Date & Time:行携带用户本地日期时间与 IANA 时区,直接读它;不得反问用户时区;仅当该行确实缺少该字段时才退回 UTC 并加说明; - 拒绝陈旧数据——工具调用失败或返回空时如实说明,不得回退到"昨天的数据";
- 忠于时间线——
cover_window查询已把近记忆限制在最近 24h,其内容可视为真实近期;但每条命中都携带真实time_range,应按发生顺序(旧→新)呈现。对于从更长生命周期笔记或实时工具结果中带过来的条目,与今日日期比对:早于简报当日的,必须显式点名日期(如 "from your May 25 note…"),不得伪装成今天的事; - 隐私优先——不放入完整邮件正文或消息内容,只总结发件人与主题。
三、装配链路:prompt.md 如何变成模型看到的系统提示词
3.1 构建顺序与 KV cache 约定
prompt.rs 中的build(ctx: &PromptContext)是最终产物——注释明确写着"输出即 LLM 所见,runner 不做后处理"。其拼装顺序为:
ARCHETYPE——即include_str!("prompt.md")编译期内嵌的整份提示词(见 prompt.rs 的const ARCHETYPE);render_user_files(用户文件段,非空才追加);render_tools(可用工具清单);render_workspace(工作区信息);render_ambient_environment——放在最尾部。源码注释解释了原因:该块内嵌Local::now()导致内容每轮都变,因此按SystemPromptBuilder::with_defaults的 KV cache 约定置于提示词末尾,最大化前缀缓存命中;它同时携带宿主运行时信息、用户身份(## User块)与当前日期时间,直接对应 issue #926——"桌面应用明明知道时区,简报代理却还问用户'你在哪个时区?'"的修复。
提示词正文中反复引用的Current Date & Time:行、## User块的 name/email 字段,正是这一装配步骤的产物——提示词作者能放心写"从这一行读时间/从## User块读名字",因为装配代码保证了它们的存在。
3.2 测试把提示词规则"钉死"
prompt_tests.rs 用一组断言把关键行为固化进 CI,任何后续对 prompt.md 的编辑若丢掉这些要素都会直接失败:
build_includes_runtime_and_datetime_sections:断言产物包含## Runtime与## Current Date & Time两个小节,且日期时间小节中包含"match the actual local hour"的接地规则(针对 #926 / #3602:问候必须锚定真实时钟;具体的"现在"由每轮用户消息注入,测试因此只钉住规则而非易变的时间戳);prompt_pins_personalisation_and_structure_rules:断言 ARCHETYPE 中必须含 "by name"(按名问候)、"Frame the scope"(范围界定),以及**Highlights**/**Action items**/**Mentions**/**FYI**四个输出桶(针对 #3806);build_includes_user_identity_when_present:当认证缓存已填充user_identity时,产物必须出现## User、- name: Ada Lovelace、- email: ada@example.com,并且断言全文不含 "token"——身份块按构造只携带 id/name/email 三个字段,任何未来加字段的行为都会强制更新这个测试;build_omits_user_section_when_identity_unset:无身份(CLI 流程、未登录会话)时## User段必须缺席——这正是提示词中"若该块缺失则退回不带名字问候"分支的现实触发条件。
四、配置项与配套机制源码佐证
4.1 agent.toml 的关键参数
agent.toml 各字段的取值都有明确动机:
| 字段 | 值 | 作用 |
|---|---|---|
temperature | 0.5 | 汇总型任务:比纯创造性低,比机械抄录略高 |
max_iterations | 8 | 工具循环上限——够完成"cover_window + 逐集成 list/execute"的多轮取数 |
sandbox_mode | "read_only" | 简报只读,不执行有副作用的操作 |
omit_identity/omit_memory_context/omit_safety_preamble | 均为true | 见下方专述 |
[model].hint | "agentic" | 路由到具备工具调用能力的模型档位 |
[tools].wildcard | {} | 技能(skill)类别通配——代理需要自行发现并调用用户所连集成(日历、邮件、任务等)的任意 Composio 动作 |
其中三个omit_*开关的注释把设计取舍写得非常直白:
"The brief pulls its recent memory itself via
memory_tree_cover_window(last 24h, source-grouped). We therefore suppress the injected all-time memory blob (## User Memory, the namespace root summaries): it is stale relative to 'today' and would compete with the fresh windowed tool result. Identity/safety boilerplate stays off too — the prompt carries its own voice."
这与 prompt.md 中"the all-time memory blob is intentionally NOT injected, so do not rely on it"形成提示词与装配代码的双向契约:提示词告诉模型别依赖全量记忆,omit_memory_context = true保证全量记忆确实不在场。身份样板被关掉,也是因为这份提示词自带完整的人格设定("Your mission" 一节即其 voice)。
4.2 "近 24h 任务"承诺的底层实现
提示词对模型说"系统已经限制任务工具结果到 24h 窗口,别重拉 backlog"——实现位于 task_window.rs。该模块的头部注释交代了背景与分层:
- cron runner 为简报这一轮安装
current_task_recency_window(对应 harness/task_recency_context.rs 中的任务局部窗口); - 第一层(尽力而为的服务端收窄):
apply_window_args在调用方未提供时注入 provider 能理解的排序 /*_since参数——只减少载荷、改善排序,正确性不依赖它,且调用方显式值优先; - 第二层(权威的客户端后过滤):
filter_response丢弃时间戳早于now - window的行,这才是真正的执行层; - 优雅降级哲学:只处理响应结构"已验证"的 slug;未知 slug 退化为不过滤,不可解析的时间戳按"保留"处理——"never a crash and never a silently-emptied result";
- 若后过滤删了行,会清掉
markdown_formatted字段,避免 agent 读到过滤前 stale 的整 backlog markdown 而忽略过滤后的 JSON。
也就是说,提示词里"treat whatcomposio_executereturns as recent by construction"这句话背后,是两层收窄 + 明确的降级策略,而非模型的自觉。
4.3 注册与投递闭环
把前几节串起来,完整链路是:
- 注册:loader.rs 将
morning_briefing的 toml 与prompt::build注册进内置代理表; - 种任务:cron/seed.rs 在 onboarding 后幂等种入
0 7 * * *的禁用任务(SessionTarget::Isolated+ proactive 投递),用户 opt-in 后由cron.update_job启用; - 触发:调度器到点启动独立会话,为简报轮次安装任务近因窗口;
- 装配提示词:
prompt::build按 "archetype → user files → tools → workspace → ambient(Runtime/User/Current Date & Time 置尾)" 的顺序拼出系统提示词; - 执行:模型按 prompt.md 的三段式协议取数(cover_window → composio 实时 → 对账),产出 200–400 词、四桶结构的简报;
- 投递:
mode: "proactive"交付交由 channels 模块路由到用户活跃频道,best_effort保证投递失败不阻塞。
五、可借鉴的设计要点
从这份提示词及其配套源码中,可以提炼出若干在构建"个人助理类"定时代理时值得复用的模式:
- 窗口化记忆替代全量注入:用"最小覆盖集"的
cover_window检索代替向提示词塞全量记忆 blob,既省 token 又避免"相对今天而言是陈旧的"上下文与新鲜工具结果互相竞争——提示词侧声明"不依赖全量记忆",配置侧omit_memory_context = true兑现承诺,两边必须成对出现; - 把系统保证写进提示词:提示词敢于说"任务结果已是近 24h",前提是 task_window.rs 真的在
composio_execute层做了后过滤。提示词中的每一条"你不必/不需要做 X"都应能对应到一段真实实现; - 时间接地(grounding)三层防御:
Current Date & Time:行提供时钟(避免反问时区)、问候必须匹配真实本地小时(#3602)、跨日条目必须显式点名日期——测试钉住规则而不钉死时间戳,兼顾了"现在"每轮注入导致的易变性; - 易变内容置尾:携带
Local::now()的环境块放在系统提示词末尾,保护前缀 KV cache 命中,这是把 LLM 服务成本约束写进提示词布局的工程实践; - 用测试做提示词回归防线:
prompt_tests.rs把"按名问候、范围界定、四个输出桶、身份块无 token 字段"固化为断言,使提示词从"文档"升级为"受 CI 保护的契约"——对任何把核心行为编码在 system prompt 中的代理都值得照搬; - 默认禁用的 opt-in 主动任务:主动式推理有成本,seed.rs 用"创建即禁用"保证未启用的用户不产生任何推理账单,同时保留一次性原子插入避免竞态。
综上,Morning Briefing 的 prompt.md 并不只是一段自然语言描述,而是与 agent.toml 的上下文裁剪开关、prompt::build的装配顺序、memory_tree_cover_window的窗口检索、Composio 任务近因过滤以及 cron 的 opt-in 投递共同构成的一套"提示词—实现—测试"三位一体的工程产物。阅读这篇提示词的正确方式,是把它当作接口语法:它承诺的每一条行为,都能在仓库中找到对应的代码兑现或测试钉扎。
【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考