【免费下载链接】unreal-agent
Async-first agent harness
Unreal Agent(Unreal Labs 出品的 Async-first agent harness)通过在每次请求的系统提示(system prompt)最前面注入一段固定的preamble(harness/contextbuilder/prompts/preamble.md),向模型传递"回合(turn)制、异步工具调用、心跳保活、无保姆式等待"这套运行约定。本文以该 preamble 为骨架,结合 contextbuilder 的组装逻辑、coordinator 的事件循环与相关测试,讲解这段提示词每一句话对应的真实机制,以及它如何让模型在本 harness 下"走得更宽、睡得安心、任务达成后才收工"。读完你可以理解:为什么同一模型在 Unreal Agent 中的行为会与在普通单轮对话框架中不同,以及这套约定背后的实现证据。
一、Preamble 是什么:系统提示的第一段"出厂说明书"
在 Unreal Agent 中,模型的每一条请求都从一个 system message 开始,它的文本由三部分顺序拼接而成:
preamble(固定内嵌) + "\n\n" + system prompt(运行时可配置)拼装发生在 contextbuilder/builder.go 与 SetSystemPrompt:
//go:embed prompts/preamble.md var preambleFile string var preamble = strings.TrimSpace(preambleFile) ... current.committedPrefix[0] = llm.Item{Type: llm.ItemMessage, Data: llm.Message{ Role: llm.RoleSystem, Text: strings.TrimSpace(current.preamble + "\n\n" + current.systemPrompt), }}也就是说,无论运行方传入什么样的 system prompt,preamble 永远位于最前面。测试 TestBuilderLeadsSystemPromptWithPreamble 明确断言最终 system message 文本等于preamble + "\n\nBe concise."。运行方(cmd/internal/agentrunner/run.go)默认提供的 system prompt 是 "You are an AI agent running inside an isolated sandbox container..." 一类的行为准则,也可以由调用方通过 JSON 请求中的system_prompt字段覆盖,但 preamble 始终打头。
另外,如果注册了 skills,formatSkillsForPrompt 会把 skill-preamble.md 与<available_skills>XML 清单追加到 preamble 之后(对应测试 TestBuilderAppendsSkillsToPreamble)。因此最终的 system 文本顺序是:
- preamble(固定)
- skills 引导语 + 可用技能清单(可选)
- 运行方 system prompt(可选)
二、回合(Turn)模型:一次读对话、一次回复
Preamble 的第一条约定是:
You work in turns. A turn is one reading of the conversation and one reply: text, tool calls, or both. Each turn re-sends the whole conversation, so prefer to go wider with tool calls — they are cheap — rather than chaining them across a longer sequence of turns.
它在 harness 里的实体是session.Turn:每次向模型发起请求前,coordinator 都会创建一个带唯一 ID 的新 turn(loop.go),并把这一轮的所有输入、模型回复、工具调用与结果作为会话历史持久化;下一轮请求会重新发送"整个对话"(committed prefix + staged suffix),这正是 preamble 所说 "Each turn re-sends the whole conversation" 的机制来源。
由此产生一条直接可用的行为准则:当多个操作互不依赖时,把它们放进同一个回合并发执行,而不是串行地"一步一回合"。preamble 给出的理由很直白——工具调用很便宜(cheap),而整段对话重发是有成本的;把调用摊到更多回合里等于重复付费。典型场景:需要同时查看多个文件、同时跑构建和测试、同时验证两个假设时,就在同一回合发出多个独立的工具调用。
三、异步工具调用:发起即后台运行
Tool calls are asynchronous: each starts the moment you issue it and runs in the background, so issuing one never blocks you and many run at once.
这是整个 harness 的架构核心(项目自述即为 "Async-first agent harness",见 README.md)。在 Unreal Agent 中,工具调用不会阻塞模型:模型在回合里发出的 tool call 会被 coordinator 解析为operation(可序列化的工作描述,见 README 术语表),交由 operation manager 异步执行;模型这一回合立即结束,去等待结果。因此 preamble 才敢告诉模型"发出调用不会阻塞你,多个调用可以同时运行"。
- 同时运行的机制:coordinator 的
Run事件循环把 inbox 输入、operation 更新、心跳、模型响应都接入一个select多路复用(loop.go),任意操作完成都会唤醒新回合,而不是排队等待。 - 结果到达的编排:每个完成的操作更新被累积到 tool call 状态,当某个 tool call 关联的全部 operation 都进入终态(completed/failed/canceled,见 operationIsTerminal)后,翻译器把结果组装成模型可见的 tool result(addToolResultToLocalState)。
四、运行中结果的占位符:让模型知道"还在跑"
As each finishes, its result is appended and wakes a new turn; results that land together arrive in the same turn, and a call still running shows a placeholder until its own result comes.
"still running 显示占位符"在代码中是字面实现的。contextbuilder 定义了常量:
const ToolCallRunningPayload = "Tool call is still running. Its result arrives in a later turn: continue with independent work, or end your turn to wait for it."见 builder.go。当 coordinator 发现某次工具调用仍有非终态 operation 时,会调用AddToolResult(..., running=true),此时 builder 会把ToolCallRunningPayload作为该调用的暂定结果注入 staged suffix(builder.go)。后续真实结果到达时,builder 会先删除旧的 running 占位条目,再追加最终结果;测试 TestBuilderRemovesOnlyStagedRunningResultsForUpdatedCall 验证了"只替换对应调用、不影响其他调用"的行为。
因此模型在本回合看到的对话形如:调用 A 的结果是"仍在运行",于是它可以选择继续做独立工作,或者结束回合睡觉等结果——这正对应 preamble 说的 "continue with independent work, or end your turn to wait for it."。
五、心跳(Heartbeat):不用盯梢,十分钟没动静就唤醒你
You never have to babysit a running call: harness does it for you. As a backup, if calls are active and nothing has happened for ten minutes, a heartbeat wakes you, and this is an opportunity to check that all is well.
这是 preamble 中最容易被误解的一句:"十分钟"是配置示例,不是硬编码。真正的机制在 coordinator 中:
- 当模型没有在等待模型响应、没有待处理输入、但仍有工具调用在跑时(isWaitingForOnlyToolCalls),事件循环会启动一个
time.After(ToolHeartbeatInterval)定时器(loop.go)。 - 定时器到点后,postHeartbeat 把当前仍在运行的工具调用列表序列化,构造一条
Mode: inbox.Heartbeat的控制消息,其 Reason 形如:
Heartbeat: waited 60 seconds for tool calls. Running: [{"CallID":"call-0","Name":"ViewImage","Arguments":"{}"},...]这条消息经 inbox 回到事件循环,被转成一条 user 消息注入下一回合(AddControlMessage 中的inbox.Heartbeat分支)。模型读到它就是一次"检查一切是否安好"的机会。
- 间隔由运行方通过
-tool-heartbeat-interval命令行参数配置,最终写入Dependencies.ToolHeartbeatInterval(run.go);负值会被拒绝(TestCoordinatorRejectsNegativeHeartbeatInterval)。coordinator 层面的默认值(零值)为 0,表示完全禁用——例如在 cmd/internal/agentrunner/heartbeat_test.go 的集成测试中就以10ms触发心跳来验证 Bash 结果的释放。因此 "ten minutes" 只是说明"这是一个保底唤醒机制",实际节奏完全可控。
值得注意的细节:心跳只在"仅有工具调用在跑"时启动,且一旦有新的外部输入或模型响应到来,旧的心跳 deadline 会被丢弃(TestHeartbeatDiscardsPreviousDeadline),心跳不会打断正在进行的模型请求(TestQueuedHeartbeatPreservesActiveModelResponse)。
六、回合结束规则:睡觉 vs 收工
Ending a turn with no tool calls while calls are running means you sleep until one finishes; ending a turn with nothing running ends the session, so do that only when the task is complete.
这条"睡眠/收工"语义对应 coordinator 的状态判定:
- 有工具调用仍在运行、没有模型响应在途、没有待处理输入时,模型处于"等待工具结果"状态;此时结束回合不会终止会话,而是睡眠直到某个操作完成唤醒(loop.go 的 select 持续监听
operationUpdates)。 - 没有任何在运行的东西时,事件循环就进入了 idle 状态(isIdle)。此时如果收到了
StopWhenIdle控制消息,Run会返回结束会话(loop.go);如果没有任何 stop 控制,harness 会一直待命,等待新的外部输入。所以 preamble 的告诫"只有任务完成时才结束回合"是有工程含义的:模型结束回合且无运行中调用,等价于把会话交给 harness 的停止判定逻辑。
测试 TestCoordinatorHeartbeatsWhileWaitingForTools 完整演示了这一节奏:工具调用运行期间模型可以结束回合睡觉;模型响应"Still waiting." 之后,操作完成才把结果送达;全部完成后停掉不再有心跳。这验证了 preamble 描述的"睡觉—唤醒—检查—收工"闭环。
七、最后一句:把 prompt 当作目标
Treat the prompt as a goal and keep working until it is met. I believe in you!
这是对模型的行为指令,也是 harness 设计目标的人格化表达:会话不会因为一次"无工具调用的回合"就结束,除非明确收到停止控制或上下文取消。结合上一节的停止机制(StopHard强制取消、StopWhenIdle空闲停止,见 handleStop),可以这样理解:任务何时算完成,由停止控制消息决定,而不是由"模型想休息"决定。因此 preamble 鼓励模型持续工作到目标满足。
结语:preamble 是 async-first 行为契约的浓缩
Unreal Agent 的 preamble 表面上是写给模型的一段行为建议,实际上逐句对应着 harness 的硬机制:回合制上下文重发(turn + session store)、异步 operation 执行、运行中占位符(ToolCallRunningPayload)、可配置心跳(ToolHeartbeatInterval)与"睡觉 vs 收工"的停止判定。如果你要为 Unreal Agent 编写或调试 agent 行为,这段提示词就是最值得先读的"运行约定说明书";相关代码从 contextbuilder/builder.go 到 coordinator/loop.go 一脉相承,测试文件(如 builder_test.go、heartbeat_test.go)则为每条约定提供了可复现的验证证据。
【免费下载链接】unreal-agent
Async-first agent harness
相关推荐
制作者(Maker)Agent Prompt 完全拆解:learn-harness-engineering 中实现型 Agent 的角色设计、五步工作流与实战模板
制作者(Maker)Agent Prompt 完全拆解:learn harness engineering 中实现型 Agent 的角色设计、五步工作流与实战模
learn-harness-engineering 仓库设计文档规范:如何用 DESIGN.md 沉淀 agent-first 的持久化设计决策
learn harness engineering 仓库设计文档规范:如何用 DESIGN.md 沉淀 agent first 的持久化设计决策 导读 本指南以
Agent-first 仓库中的设计文档入口模式:解析 learn-harness-engineering 的 DESIGN.md 模板
Agent first 仓库中的设计文档入口模式:解析 learn harness engineering 的 DESIGN.md 模板 本篇文章围绕 lear
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考