Unreal Agent Context Builder 设计解析:纯内存上下文构建与截断压缩报告机制
【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent
在 AI Agent 框架中,"上下文构建"决定了模型每一轮能看到什么、看不到什么。Unreal Agent 是一个异步优先(async-first)的智能体框架,其 Context Builder 采用纯内存上下文构建策略:不发起任何 I/O、不依赖持久化,只在内存中组装发给大模型的请求,并附带一份截断压缩报告(Report),透明地记录被省略、截断或压缩的内容。本文将带你读懂这套机制的设计思路。
Context Builder 在 Unreal Agent 中的位置 🧩
Unreal Agent 的 harness 库由多个协作组件构成,Context Builder 是其中职责最"纯粹"的一个。官方组件表对它的定义只有一句话(见 README.md):
有状态地在内存中组装模型输入,返回模型输入以及任何被省略、截断或压缩内容的记录;不执行 I/O,不接受持久化依赖。
它和相邻组件的分工如下:
| 组件 | 职责 | 与 Context Builder 的关系 |
|---|---|---|
| Session Inbox | 会话级输入去重 | 产出的输入交给 Builder 拼装 |
| Coordinator | 执行 LLM 轮次 | 每轮调用Build()取请求 |
| Session Store | 持久化会话历史 | Builder 完全不碰它 |
| LLM Adapter | 发送请求、认证、取消 | 只接收 Builder 构建好的请求 |
这种"单一职责 + 零 I/O"的划分,让上下文构建逻辑可以独立测试、独立替换——这是理解后续所有设计决策的起点。
纯内存设计:为什么禁止 I/O?🚫💾
核心约束写在包注释里(contextbuilder.go):
// Package contextbuilder defines I/O-pure, in-memory model request construction.禁止 I/O 带来三个实际好处:
- 可预测性——构建请求是同步、确定性的操作,不会因网络或磁盘延迟阻塞 Coordinator 的事件循环;
- 可测试性——所有行为都能在纯内存中用单元测试覆盖,参考实现的全部行为由 builder_test.go、control_test.go、submission_test.go 验证;
- 可替换性——接口 Builder 不绑定任何存储后端,你可以实现一个带真实截断/压缩策略的 Builder 注入进来。
截断压缩报告机制:Report 与 Change 📋
这套机制是 Context Builder 最有辨识度的设计。构建结果不是裸请求,而是Result结构(contextbuilder.go):
type Result struct { Request llm.Request // 发给模型的请求 Report Report // 构建过程中发生什么变化 }报告由若干Change记录组成,每种变化都有明确的类型:
| 变化类型 | 含义 |
|---|---|
omitted(省略) | 某段内容完全没进上下文 |
truncated(截断) | 某段内容被截短 |
compacted(压缩) | 某段内容被摘要或重写 |
每条Change还带有Source(来源)和Reason(原因)字段(contextbuilder.go)。这个设计的价值在于上下文透明性:调试"模型为什么没看到某个文件内容"这类问题时,你不需要猜,直接看 Report 即可。参考实现目前返回空报告(builder.go),因为内置策略不做删减;但接口已为自定义的截断/压缩策略预留了上报通道。
两段式状态:committedPrefix 与 stagedSuffix ✂️
Builder 的内部状态只有一组核心字段(builder.go):
type builder struct { request llm.Request preamble string systemPrompt string committedPrefix []llm.Item // 已提交前缀 stagedSuffix []llm.Item // 暂存后缀 }这体现了一种两阶段提交思想:
- 暂存(staged):外部输入、心跳消息、工具结果先进入
stagedSuffix,尚未"定稿"; - 提交(Commit):调用 Commit() 把暂存区整体并入前缀;
- 构建(Build):
Build()把committedPrefix + stagedSuffix拼成最终输入并返回,不改变 Builder 自身状态。
Coordinator 在每个 LLM 轮次开始前调用Build()(loop.go),拿到请求后交给 LLM Adapter 发送。Builder 本身作为 Coordinator 的依赖注入(coordinator.go),整个过程对持久层完全无感。
各类事件如何进入上下文?📥
Builder 接口按事件类型划分入口方法(contextbuilder.go),每类事件的处理策略值得细看:
1. 外部输入——AddExternalInput校验输入类型后解码为文本,作为用户消息进入暂存区(builder.go)。
2. 控制消息—— 分两种模式(builder.go):UpdateSettings只更新模型的推理强度等配置,不产生对话内容;Heartbeat(心跳唤醒)则把唤醒原因作为用户消息加入上下文。
3. 模型响应——AddModelResponse直接把模型输出追加到已提交前缀,因为模型自己的历史是"定稿"的。
4. 工具结果与运行占位符—— 这是异步优先架构的精华。Unreal Agent 的工具调用是异步的:发起后立即返回,后台执行。尚未完成时,上下文里放一个占位结果(builder.go):
"Tool call is still running. Its result arrives in a later turn: continue with independent work, or end your turn to wait for it."
结果真正到达后,AddToolResult会用真实输出替换掉占位符(builder.go),避免上下文里出现"过期状态"。
5. 技能(Skills)—— 通过go:embed嵌入提示词模板,把宿主选择的技能列表序列化成 XML 清单追加到系统提示词末尾(skills.go),让模型知道有哪些专长可用、何时用SkillUse加载。
系统提示词本身 = 内置前导词 + 宿主提示词 + 技能清单。内置前导词(prompts/preamble.md)专门教模型理解异步轮次语义:每个轮次都会重发完整对话,所以应并行发起相互独立的工具调用;调用运行中结束轮次等于"睡觉等待",而没有任何运行中调用时结束轮次则意味着会话结束。
这套设计给 Agent 开发者的启发 ✅
把 harness/contextbuilder/ 作为参照实现,可以提炼出四条实践:
- 请求构建与持久化解耦——上下文组装是纯函数式的内存操作,持久化交给 Session Store,两边各自可测;
- 透明优于静默——任何对上下文的删改都应通过 Report 上报(省略/截断/压缩 + 来源 + 原因),这是可调试性的关键;
- 占位符模式处理异步结果——运行中的工具调用先占坑,结果到达再替换,模型永远看不到矛盾状态;
- 两阶段提交管理"定稿"边界——已定稿的前缀与未定稿的暂存区分离,
Commit明确划定提交点。
如果你想动手实践,可以从 harness/contextbuilder/contextbuilder.go 的接口定义读起,再看 builder.go 的参考实现,最后结合 harness/coordinator/loop.go 观察它在真实轮次中的调用位置。纯内存 + 报告机制的组合,让"模型到底看到了什么"从玄学变成了可以逐条审计的工程问题——这正是 Unreal Agent Context Builder 设计中最值得借鉴的部分。
【免费下载链接】unreal-agentAsync-first agent harness项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考