深扒 Unreal Agent 异步内核:LLM 不等工具,40% token 到底是怎么省出来的? 深扒 Unreal Agent 异步内核LLM 不等工具40% token 到底是怎么省出来的【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agentAgent 跑一趟真实任务账单为什么总是比想象中高多数人归咎于模型贵但真正烧钱的往往是运行时本身的低效模型在等工具时被迫空转输出占位文本每一条 bash 结果都触发一轮全新的完整上下文重发串行链式调用让同一段对话前缀被反复计费。Unreal Labs 开源的 Unreal Agent一个 Go 实现的 async-first agent harness给出的答案是让模型在工具运行时彻底睡觉工具回来才叫醒它。官方在 Harbor 基准与社区评测中的口径是——相比 Codex 类同步框架可省约 40% 成本、对比同类异步方案约 20%且性能无损。本文直接走进仓库源码拆解这套省钱逻辑的三根支柱协调器如何解耦模型推理与工具执行、异步化之后 token 的账怎么算、以及上下文构建器如何为 prompt cache 量身定制。同步循环的钱都烧在哪先明确基线是什么。经典的 ReAct 式 agent 循环是严格串行的模型输出一个工具调用 → 等待工具执行 → 把结果拼进对话 → 再次调用模型。这带来三个结构性的浪费空等轮长任务如跑测试、装依赖执行期间模型必须被再次唤醒以确认还在等每次唤醒都是对全部历史上下文的一次完整重发与重新计费碎轮次明明可以并行下发的多个独立命令被拆成多次串行调用每多一轮前缀 tokens 就多被计价一次缓存命中差频繁在对话尾部追加等待中这类临时消息污染了本来可以稳定命中的前缀缓存区。Unreal Agent 的架构文档在 README.md 里定义了一组清晰的概念把上述问题逐一拆解Tool translator 只做校验与翻译把模型调用翻译成可序列化的 Operation真正的执行交给独立的 Operation managerSession 是只追加、可分叉的历史账本Coordinator 则是串起这一切的单线程事件循环。理解了这个分工省钱逻辑就浮出水面了。协调器模型与工具之间的防火墙核心实现在 harness/coordinator/loop.go。协调器运行一个极简的 select 事件循环同时监听五类信号select { case -ctx.Done(): // 全局取消 case received, open : -inboxOutput: // 外部输入/控制消息 case received, open : -operationUpdates: // 工具操作的状态变更 case -heartbeat: // 工具长期运行时的心跳唤醒 case -current.state.grace: // 结果聚合的宽限期到期 case received : -modelResponses: // 模型异步响应 }关键在requestModelResponse同文件模型请求被放进独立 goroutine协调器持有cancelModel句柄随时可以中断在途推理。而工具调用则走了完全不同的路径——Bash 的 translator 在 harness/tool/bash/bash.go 中只做两件事校验参数、ctx.Submit(spec)生成一个带 UUID 的 Operation 描述符然后立刻返回。真正的进程启动、输出捕获发生在 harness/operation/local_manager.go 的 actor 运行时里通过primitiveEvents通道异步汇报进程退出、IO 读取等事件。于是模型与工具之间被一层严格的契约隔开translator 在事件循环里同步运行且不做任何 IOOperation 在独立 actor 里异步执行。这层解耦带来第一个省钱红利——toolCallStatusesRequireModelResponse决定了是否还要叫醒模型func toolCallStatusesRequireModelResponse(statuses []sessionstore.ToolCallStatus) bool { for _, status : range statuses { if status.Status.Error ! || len(status.Status.WaitingFor) 0 { return true } } return false }翻译过来的语义是只要当前所有工具调用都还在等待对应的 Operation 完成WaitingFor非空且无错误协调器就根本不会发起新的模型请求。模型不需要在工具跑的时候说任何话——它睡到结果到达为止。这正是 preambleharness/contextbuilder/prompts/preamble.md里那句引导的落地实现Tool calls are asynchronous: each starts the moment you issue it and runs in the background... issuing one never blocks you and many run at once.异步化之后token 的账怎么算光有不等工具还不够还得防止两种新的浪费结果一条条到达时触发连环唤醒以及同批结果被拆进多轮。第一道闸是输入记账。协调器状态里维护一对计数器availableInputs可交付给模型的待处理输入数与deliveredInputs已交付数。外部输入、心跳、工具完成都会令前者递增只有真正发起了一轮模型请求并收到响应deliveredInputs才会追上。pendingInputs()非零才是触发新一轮的唯一依据之一。这意味着每一条新信息至多引发一轮推理不存在结果到了但没人消费或同一结果触发多轮的泄漏。第二道闸是grace 聚合窗口。processEvents里有两个 1 秒常量const ( toolCallRunGracePeriod time.Second toolCallCompletionGracePeriod time.Second )当某个工具完成、但同一轮里还有其他兄弟调用在跑时协调器启动toolCallCompletionGracePeriod宽限在这 1 秒内到达的其余结果会被slurpChannel1ms 空闲超时、最多 100 条批量捞起统一追加进同一次模型请求。配合availableInputs的累加逻辑一批并行工具的结果最终只触发一轮推理。这也是 preamble 反复强调go wider with tool calls——they are cheap的原因并行度越高聚合收益越大轮次越少。把两道闸合并看异步化的 token 账就清晰了轮次 输入批次数而不是工具调用数。同步框架中 N 个串行工具 ≈ N 次完整上下文重发异步框架中 N 个并行工具 ≈ 1 次上下文重发 N 次增量结果。省下的不是推理 tokens而是每次重发时全部前缀输入 tokens 的重复计费——当上下文已经积累到几万 tokens 时这笔账非常可观。prompt cache 友好性为缓存而生的前缀设计异步省轮次但每一轮仍要重发完整上下文所以 cache 命中率才是决定最终账单的放大器。Unreal Agent 的上下文构建器harness/contextbuilder/builder.go对此做了专门设计type builder struct { request llm.Request committedPrefix []llm.Item // 已提交的历史只追加、永不改写 stagedSuffix []llm.Item // 本轮新增的待提交项 prefixTokens []prefixToken // 每个检查点的累计 tokens }每次模型响应到达AddModelResponse把输出原样 append 进committedPrefix工具结果先进入stagedSuffix直到Commit()才固化。前缀在相邻轮之间保持字节级稳定——新消息永远只出现在对话尾部这是各家 prompt cache 命中硬前缀的完美形态。协作者的缓存设计在两层适配器里都有体现OpenAI Responses APIharness/llm/responsesapi/adapter.goRequestOptions.CacheKey用会话 ID 的 SHA-256 生成prompt_cache_key写入请求体同时支持通过CacheKeyPlacement把 key 放进自定义 header兼顾 Anthropic 等把缓存键放在头部/上游路由的场景Anthropic Messages APIharness/llm/messagesapi/request.go请求顶层携带cache_control: ephemeral并暴露 5m/1h 的 TTL 配置让长对话、跨长时间工具调用期间的缓存不失效。基准适配器在 benchmarks/harbor/README.md 中把这条链路写得很直白OpenRouter 请求opt into automatic prompt caching with a one-hour TTL and carry the session id保证不断增长的对话被缓存、且停留在同一上游——这正是长工具调用与多轮推理中间不击穿缓存的关键。上下文利用率压缩有预算不牺牲在途工具prompt cache 解决重复付费上下文压缩则解决预算失控。Unreal Agent 的压缩策略在 harness/contextbuilder/compaction.go 里是一套带预算的算法// This fixed budget is part of the replay contract. const compactionRetainedTokens 20_000当累计 tokens 超过模型配置的CompactionThreshold默认取 context window 的一半见 harness/settings/settings.go时构建器在保留 2 万 tokens 的硬预算内寻找最后一个完整轮次检查点把更早的历史交给一个专门用于压缩的 summarizer 请求产出手写交接摘要prompt 见 harness/contextbuilder/prompts/compaction.md。这里有两个易被忽视的细节一是在途工具调用不丢失。carryForwardContext会扫描被压缩掉的前缀把所有仍在运行的 tool call 连同其最新的 running 结果搬运进新上下文——模型醒来时依然知道哪些工具还挂着二是压缩本身也有 cache 收益prefixTokens检查点机制让新摘要替换旧历史后剩余前缀依然稳定。token 账的透明度也写进了数据模型llm.TokenUsageharness/llm/model.go把InputTokens、CachedInputTokens、CacheWriteInputTokens、OutputTokens、ReasoningTokens分开统计基准轨迹benchmarks/harbor/src/harness_harbor/trajectory.py逐轮记录 prompt/cached/cache_write/completion/reasoning 五类数值——省在哪、省多少都可以被精确审计。40% 的账其实由三笔组成回到标题的数字。把源码里的机制折算成账单40% 的节省可以拆成三笔空等轮清零模型只在有实质输入时才被唤醒工具运行期间彻底休眠消灭了同步框架里等工具的占位推理与整轮上下文重发轮次合并并行工具结果经 1 秒 grace 窗口聚合N 次串行重发收敛为一次批量增量前缀输入 tokens 的重复计费按并行度等比例下降缓存命中率抬升稳定前缀 会话级缓存键 长 TTL让每一轮重发的绝大部分输入 tokens 以缓存价结算而非全价。值得注意的是社区文章中40% vs Codex、20% vs Pi这类数字在仓库内对应的可验证产物是 token 级别的分项统计而非直接标价——trajectory.py 的注释写得很严谨Costs are unknown: runner usage contains tokens, not billed amounts.。换句话说Unreal Agent 的贡献不是发明了更便宜的模型而是把 token 的每一次重复计费都设计掉了让 LLM 不空转、让轮次变粗、让前缀可缓存。对任何想把 agent 账单打下来的工程团队这三条设计都值得直接抄进自己的 harness。【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考