OpenHuman Summarizer Agent 系统提示词与运行时设计深度解析:为 Orchestrator 压缩超量工具结果的内置子代理
【免费下载链接】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
导读:在 OpenHuman 的 Agent 编排链路中,工具调用的返回体动辄是几十 KB 甚至上百万 token 的 JSON、日志或网页抓取结果,直接灌入上下文会迅速烧穿 context budget。本文以 summarizer 系统提示词 为骨架,完整剖析其"提取契约—输出格式—边界情况—Token 预算"四层设计,并结合 payload_summarizer.rs、context.rs 等源码,讲清它的触发阈值、熔断机制、配置项与"只对 Orchestrator 生效"的接线逻辑。读完你既能按契约复刻一个高质量结果压缩器,也能理解 OpenHuman 如何在代码层面防止该内部子代理被误调用、误展示、误递归。
一、Summarizer 是什么:一次调用、无工具、只做一件事
summarizer是 OpenHuman 注册在 agent registry 中的一个内置 Agent。它的定位在提示词开头一句话讲完:
Your one job is to compress a single oversized tool result into a compact, information-dense note that the Orchestrator can use without re-invoking the tool.
翻译成工程语言就是:它把"单个超量工具结果"压成一张信息密度极高的便签,让 Orchestrator 不必重新调用工具就能继续推理。三个设计约束与源码一一对应:
| 约束 | 提示词表述 | 对应实现 |
|---|---|---|
| 单次运行 | "You run exactly once per invocation" | agent.toml 中max_iterations = 1 |
| 无工具可用 | "with no tools and no follow-up iterations" | 同一文件[tools] named = [],即工具白名单为空 |
| 直接返回 | "Return the summary directly as your only response" | 运行时通过invoke_with_events以非流式 unary 方式取run.text() |
值得注意的一个细节:agent.toml的when_to_use明确写着"Do NOT call from an LLM — this agent is runtime-dispatched only"。也就是说它不是给用户或其他 Agent 通过spawn_subagent之类工具手动拉起的"公开子代理",而是由运行时在特定条件下自动分发的内部工具。temperature = 0.2的低温度设定同样服务于压缩任务对稳定性的要求——每次压缩都应严格遵循契约,而不是发挥"创意"。
agent.toml 中omit_identity、omit_memory_context、omit_safety_preamble、omit_profile、omit_memory_md全部为true,配合model.hint = "summarization"(运行时据此解析一个偏便宜/偏快的模型),说明这是一个刻意剥离了一切个人化上下文的纯管道型 Agent——它不需要记住用户是谁、不需要加载记忆文件,只需执行机械的压缩函数。
二、提取契约(Extraction Contract):输入三要素
提示词把 Summarizer 的输入定义为一个三元组,这也是它在一次运行中唯一能看到的东西:
- tool name——产生该返回体的工具名(如
GITHUB_LIST_ISSUES、GMAIL_FETCH_MESSAGE、file_read); - parent task hint(可选)——一句话描述 Orchestrator 当时想完成什么任务;
- raw tool output——工具的原始输出。
这三者在运行时如何被组装成一条用户消息,在 payload_summarizer.rs 的build_summarizer_prompt中有清晰的代码佐证:
fn build_summarizer_prompt(tool_name: &str, parent_task_hint: Option<&str>, raw: &str) -> String { let hint_line = parent_task_hint .map(|h| format!("Parent task hint: {}\n\n", h)) .unwrap_or_default(); format!( "Tool name: {}\n\n{}Raw tool output (summarize per the extraction contract in your system prompt):\n\n--- BEGIN ---\n{}\n--- END ---", tool_name, hint_line, raw ) }这里有两个工程细节值得展开:
--- BEGIN ---/--- END ---边界标记:原始 payload 被明确包裹在标记之间,子代理可以无歧义地区分"payload 本体"与"提示词脚手架",不会把Tool name:那几行当成待压缩内容。- hint 出现在 payload 之前:这样 Summarizer 在读到正文前就掌握了父任务意图,可以在压缩时优先挑选与该意图相关的事实。
注意parent_task_hint是Option<&str>——不是每次工具调用都有任务提示。没有时该行整体省略,提示词对此的处理是"如果没有 hint,就依靠 payload 自身结构推断重点"。
三、压缩的黄金法则:保留什么、丢弃什么
提取契约是这份提示词的灵魂,它把"压缩"从玄学变成可执行的三条指令:
3.1 必须保留:Required facts(标识符是最高优先级)
Identifiers are the single most important thing. Never drop them.
任何 Orchestrator 在后续工具调用中可能需要用来"动手操作"的标识符都必须完整保留:ID、哈希、URL、文件路径、邮箱地址、用户名、SKU、订单号等。逻辑很直白:Orchestrator 拿到摘要后若要继续操作(比如"把 issue #42 关闭""回复这封邮件"),依赖的正是这些标识符——丢一个就等于切断一次后续动作。
3.2 按需保留:Optional supporting context
从 payload 中挑出3–5 个人类回答 parent task 时最关心的事实,且优先级必须服从 parent task hint。提示词给了两个对照例子:
- hint 是"找出最紧急的未关闭 issue"→ 优先保留紧急度 / 严重性 / label;
- hint 是"总结昨天的邮件"→ 优先保留主题 / 发件人 / 时间戳。
这条规则的实质是:压缩不是均匀抽样,而是朝任务方向倾斜的信息筛选。
3.3 保留结构线索:Structural hints
如果 payload 是列表,说明共有多少项;如果分页,说明页边界;如果是文件,给出行数或章节标题。这些结构信息让 Orchestrator 能判断"要不要用更窄的查询重新抓取"——例如"第一页 30 条,共 3 页"足以让 Orchestrator 决定是直接基于现有数据继续,还是再拉一页。
3.4 必须丢弃
- 原始标记/格式噪音:HTML 标签、CSS、JSON 包裹、样板化的表头——除非"标记本身就是信息"(比如你要总结的就是一个 HTML 页面的结构);
- 项与项之间无差异的重复字段:100 条记录里每条都带同一个常量字段,保留一个即可;
- Provider 元数据:Orchestrator 无法据其行动的字段,如
X-Request-ID头、毫秒级时间戳、内部服务器 ID。
3.5 违反规则的失败判据
注意 payload_summarizer.rs 的handle_summarizer_result给了压缩一条硬性验收线:若摘要不小于原始 payload,视为失败并回退(summary.len() >= raw.len()时record_failure()并返回Unavailable(Failed))。这与提示词里 "If the summary is the same size as the payload, you have failed" 完全互为表里——代码把提示词中的"失败"定义落实成了可自动判定的守卫。
四、输出格式规范:标准摘要模板
提示词要求只输出摘要文本本身:没有 "Here is the summary..." 式开场白、没有 "Let me know if you need more details" 式收尾、没有 JSON 包裹,纯 Markdown,为 Orchestrator 的下一步推理优化。规范模板如下(完整继承自提示词):
[Tool output summary — <tool_name>] <1-2 sentence overview: what the payload is, how many items/how much data> ## Key facts - <fact 1 with identifier> - <fact 2 with identifier> - ... ## Identifiers preserved - <id_1>: <one-line description> - <id_2>: <one-line description> - ... (Only include this section if the payload contained IDs/URLs/hashes. Skip otherwise.) ## Original size <original_bytes> bytes → summary of <this note>模板各节的设计意图:
- 首行
[Tool output summary — <tool_name>]:一个可 grep 的元信息头,让 Orchestrator 一眼识别"这是一份压缩摘要"以及它源自哪个工具; - 1–2 句总览:payload 是什么、多少项 / 多少数据;
- Key facts:带标识符的关键事实列表——上文的 3–5 条 supporting context 落在这里;
- Identifiers preserved:仅当 payload 含 ID / URL / hash 时才出现,每行一个
id: 一句话说明。把标识符单独成节而非混在 Key facts 里,是为了让 Orchestrator 能机械地扫描、复制这些 ID 用于后续工具调用; - Original size:原始字节数 → 摘要长度,既满足观测需要,也形成压缩率自证。
五、边界情况处理
提示词用四条规则覆盖了压缩中最容易翻车的场景:
| 场景 | 处理规则 |
|---|---|
| payload 已经很短 | 产出短摘要,不要注水("Don't pad") |
| payload 完全是错误输出 | 在摘要顶部逐字保留错误信息——Orchestrator 需要看到精确错误才能决定下一步路由 |
| 含二进制噪音(base64、hex dump) | 只总结其存在与长度,不要尝试解码 |
| parent task hint 与 payload 矛盾(要邮件却给了 GitHub issues) | 以 payload 为准——你报告的是工具实际返回了什么,而不是"被要求了什么" |
最后一条尤其精辟:它把 Summarizer 从"任务执行器"定位成"事实报告器"。压缩器无权因为 hint 与 payload 不符就篡改内容或强行圆场,忠实报告返回体是它的职业底线。
六、Token 预算
提示词给出明确的量化指标:
- 大多数 payload 的目标:800–1500 输出 token;
- 硬上限:2000 token,绝不超出。
这个预算和运行时如何配合?在 payload_summarizer.rs 中,子代理分发时用MaxTokensModel::new(source.build_summarizer(&model, ...), max_output_tokens)包装模型,max_output_tokens取definition.max_turn_output_tokens,否则回落到AGENT_TURN_MAX_OUTPUT_TOKENS——即提示词定的 2000 上限在运行时还有一道模型层的强制闸门。更妙的是,模型提示词中的omit_memory_md = true、构建提示时agents_md_global / agents_md_local传None,源码注释解释得直白:AGENTS.md 之类的项目指令对"压缩工具返回体"这个狭窄内部任务是纯噪音,"would waste the tight token budget this summary path is trying to reclaim"。
七、禁止事项清单(行为边界)
提示词用六个 "Do not" 圈定了 Summarizer 的能力边界,每一条都是对 Agent 失控风险的具体防御:
- Do not ask clarifying questions——你只有一次机会,没有澄清回合;
- Do not emit tool calls——你没有任何工具;
- Do not try to "solve" the parent task——你是预处理器(preprocessor),不是 Orchestrator;
- Do not fabricate information——payload 里没有的字段写
(no value)或直接省略,严禁编造; - Do not copy the raw payload verbatim——摘要与原文等长即失败(与 3.5 的代码守卫呼应);
- 隐含的Do not recurse——这正是 agent.toml 注释与 context.rs 源码中反复强调的:为 summarizer 构建的窄子代理提示词不含
spawn_subagent等工具([tools] named = []),从而在源头杜绝了"压缩器递归调用自身"的经典故障。context.rs 的注释记录了这段历史:阈值默认值曾因递归分发根因被置为 0 关闭,修复后重新以 4000 tokens 启用。
八、运行时剖析:SubagentPayloadSummarizer 如何调度它
提示词定义了"怎么做",而 payload_summarizer.rs 定义了"何时做、失败怎么办"。核心是SubagentPayloadSummarizer,它实现PayloadSummarizertrait 的唯一入口maybe_summarize_in_parent,执行流程如下:
8.1 四个 pass-through 判定(按顺序)
- 低于阈值:
estimate_tokens(raw) < threshold_tokens→ 直接放行,返回SummarizeOutcome::NotNeeded(这是唯一"安静"的退出,模型被告知任何信息——小结果上任何提示都是噪音); - 高于上限:
tokens > max_payload_tokens→ 跳过 LLM 调用,返回Unavailable(PayloadTooLarge),交给下游既有的tool_result_budget_bytes截断兜底(对百万级 token 的 blob 付一次 LLM 调用没有经济性); - 熔断器已跳闸:连续失败 ≥ 3 次 → 本会话内 Summarizer 变成 no-op,返回
Unavailable(Disabled),防止一个坏掉的压缩器拖垮每一次工具调用; - 分发失败 / 返回空 / 未缩小:回退到原始 payload,返回
Unavailable(Failed),作为安全网。
其中estimate_tokens的实现是text.len().div_ceil(4),即按约每 4 字符 1 个 token 估算,与tree_summarizer::estimate_tokens的启发式一致。
8.2 三态结果模型
SummarizeOutcome刻意把"不需要压缩"和"压缩没发生"区分成两个状态:
Summarized(SummarizedPayload):用summary替换原始 payload 进入 agent history,同时记录original_bytes与summary_bytes供观测;NotNeeded:payload 本就不大,保持原样且不打扰模型;Unavailable(UnavailableReason):raw payload 原样给模型,但在截断阶段全部跑完之后,把一段notice文本前缀到工具结果上。
8.3 给模型的通知:为什么强调 "Do not re-run the tool for a summary"
UnavailableReason::notice()为三种失败各生成一条以[openhuman: summarization unavailable — ...]开头、以"Do not re-run the tool for a summary."结尾的说明。源码注释详细记录了这个措辞的演化:如果模型面对被截断的大输出,最合理的本能反应就是"再调一次同一个工具拿摘要"——这会在用户侧呈现为无声的重分发挂起(hang)。因此通知必须是前置的(notice在调用方的截断阶段之后、且以 prefix 而非 append 方式应用,否则先被截断的是通知本身),并且必须是无理由的纯指令——注释用大段篇幅论证了三个候选理由("重新调用会返回相同结果""重新调用不会产生摘要""完整输出已经在这里了")分别对时间变化型 API 工具、Failed 变体、截断场景为何都是假的。
8.4 只对 Orchestrator 生效的接线
模块文档明确:只有 orchestrator 会话会接入PayloadSummarizer。在 factory.rs 中,构造条件写得很直白:
if agent_id == "orchestrator" && config.context.summarizer_payload_threshold_tokens > 0 { // ... 用 threshold_tokens 构造 SubagentPayloadSummarizer 并注入 }Welcome、integrations_agent、researcher、planner、archivist 等其他类型子代理得到None,工具结果不被触碰;Summarizer 自身也是None——它永远无法递归压缩自己的输入。
8.5 静默执行:内部文本不进用户视野
invoke_tinyagents_summarizer_in_parent中有一处极易被忽视但极重要的选择:子代理用invoke_with_events(unary、streaming = false)而非invoke_in_parent运行。原因写在了源码注释里:invoke_in_parent会继承父会话的streaming = true,于是压缩器的内部文本(如[Tool output summary — <tool>])会以AgentProgress::TextDelta形式流到共享EventSink,进而被 Web 桥渲染成chat_interim气泡展示给用户——这恰恰违背了 Summarizer "只为 Orchestrator 上下文而存在"的初衷。unary 路径在共享 sink 的前提下(子代理 start/completed 生命周期事件仍可达观察者)对父事件流保持静默,与reprompt_for_required_block的内部修复调用同样克制。
九、配置项:三个旋钮一个开关
Summarizer 的所有行为都可通过 ContextConfig 调整,对应配置段为[context]:
| 配置项 | 默认值 | 语义 |
|---|---|---|
context.summarizer_payload_threshold_tokens | 4000(约 16000 字符) | 触发压缩的下界,单位是估算 token(chars / 4)。设为 0 可完全禁用。存在历史别名summarizer_payload_threshold_bytes |
context.summarizer_max_payload_tokens | 2_000_000 | 硬上限,超过则跳过 LLM 压缩,交给tool_result_budget_bytes截断兜底。存在历史别名summarizer_max_payload_bytes |
context.summarizer_model | None(跟随调用方当前模型) | 可选覆盖:为自动压缩指定更便宜/更快的模型,降低长会话下压缩成本 |
两点事实校准:
- 两个阈值配置项本身不依赖估算函数,配置值直接按"估算 token"解释;
estimate_tokens(chars / 4)只在运行时判定 payload 大小是否越界时使用; - agent.toml 的
when_to_use文案中写有 "default 500000" 的旧注释,但 context.rs 中default_summarizer_payload_threshold_tokens()的实际返回值为4000。以 schema 源码为准:当前仓库中默认触发阈值为 4000 估算 token,且该默认值曾在递归分发根因修复后被重新启用(见 7.6 节及 context.rs 注释)。
十、从提示词到代码:这套设计的工程启示
最后把这份提示词与它的运行时实现合起来看,它其实是"提示工程 + 系统工程"双层防御的范例:
- 提示词层定义"正确行为":提取契约(标识符优先)、标准模板、边界规则、token 预算、禁止清单——全部是可被模型直接执行的指令,也是
ARCHETYPE常量(include_str!("prompt.md"),见 prompt.rs)被pub暴露的原因:它邀请 embedder 提供自定义 summarizer 时,不必重新发明这套来之不易的契约; - 代码层定义"不可能越界的行为":
max_iterations = 1、空工具白名单(防递归)、temperature = 0.2、三态结果模型、熔断器、非流式静默执行、max_output_tokens强制闸门、仅在 orchestrator 会话接线——把提示词里的每一条软约束都落成了硬约束。
对一个想要自行实现"超量工具结果压缩"的开发者来说,这份设计最值得抄走的四件事是:标识符永远不能丢、摘要必须显著小于原文(否则回退原文)、内部修复调用必须对用户静默、连续失败要熔断而不是无限重试。这四点中的任何一点缺失,都可能让一个看似精巧的压缩器在实际 Agent 编排中变成上下文杀手或挂起制造机。
延伸阅读:本文涉及的源码与配置相对路径汇总——summarizer 系统提示词、summarizer 注册配置、提示词构建器 prompt.rs(含其单元测试 prompt_tests.rs)、运行时压缩器 payload_summarizer.rs、上下文配置 schema/context.rs、Orchestrator 会话接线 factory.rs。
【免费下载链接】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),仅供参考