摘要
通用 agent 运行时的工程难点不在单次模型调用,而在长时间运行中的三件事:状态在实时视图、刷新与重连之间保持一致;上下文成本随会话增长而失控;模型输出必须被当作不可信输入来对账。本文介绍一个单人完成的通用 agent 运行时的架构与演进。系统以三条约束为起点------模型在云端、用户不管理系统、单一事实源------经历能力展开、一致性危机、系统收束三个阶段。最终形态把验证从"提交门"搬到工具边界,把上下文压缩从用户按钮变成四级自动梯子,把两套流式协议收敛为一套类型化事件契约。本文重点讨论三类特大重构(后端、中间层、前端)的判据与做法,以及由真实事故驱动出的可靠性机制。
关键词:agent 运行时;事件溯源;上下文工程;可信输出;重构方法
1. 引言
1.1 问题
一个能连续运行数小时的 agent,本质上是一个有状态的长事务服务。它同时面对三类失败:
- 一致性失败:流式视图、刷新后的历史、断线重连后的快照,三者对同一轮对话给出不同答案。
- 成本失败:上下文随轮次线性增长,直到模型窗口或账单先崩溃。
- 可信失败:模型声称做了某件事,但证据链并不存在;验证发生在草稿写完之后,只能整篇重答。
大多数 agent 项目在 demo 阶段绕开了这三类问题;一旦进入日常使用,它们会以"卡住""刷新后答案没了""答非所问"的形式集中爆发。本文讨论的正是一个系统如何从"能跑"走到"敢每天用"。
1.2 三条设计约束
| 约束 | 表述 | 推导出的决策 |
|---|---|---|
| C1 模型在云端 | 端侧不做推理,只做宿主与本地语音 | 核心编成动态库,进程内宿主进不同平台 |
| C2 用户不管理系统 | 压缩、思考强度、恢复、验证都不应是用户旋钮 | 策略沉入机制,UI 只保留必要的高级路径 |
| C3 单一事实源 | 同一状态只能有一个权威表达 | 事件总线 + 单调序号 + 双端同构 fold |
C2 是最容易被低估的一条。它意味着每一次"要不要加个开关"的讨论,答案默认是否定的;只有当机制无法覆盖时才允许出现旋钮,且旋钮必须能被自动路径取代。
1.3 本文贡献
- 描述一个五层、二十余个 crate 的 agent 运行时,及其三个数据面(事件、上下文、能力)的收口方式。
- 给出三次架构收束的决策与反转,包括一次被明确记录为反例的双协议决策。
- 总结三类特大重构的可操作判据:后端的行为保持、中间层的契约先行、前端的黄金母本。
- 给出一套事故驱动的可靠性机制,以及上下文逐阶段裁剪的方法。
2. 系统总览
2.1 分层
rust
kernel/ 共享类型、上下文协议、遥测、会话模型
infra/ 数据库、沙箱、网络、MCP、知识、向量、更新、能力探针
app/ agent 循环、服务端、工作流、A2A、IM、调度中心
adapters/ 第一方平台适配器(注册进适配器表)
tools/ 独立的搜索/浏览/打包工具
Copy
依赖方向严格单向:kernel ← infra ← app ← adapters/tools。服务端只做组装,不允许反向依赖;后期专门修过一轮"编排层反向依赖路由层"的倒挂,把卡生命周期、会话设置、发现配置下沉回编排层。
2.2 三个数据面
| 面 | 单一入口 | 职责 |
|---|---|---|
| 事件面 | 会话总线(单调序号 + 环形缓冲 + 增量 fold) | 运行状态、UI 渲染、重连快照的唯一真相 |
| 上下文面 | 单一装配入口 + 阶段流水线 | 所有上下文只从这里进入模型 |
| 能力面 | 工具注册表 + 贡献注册表 | 工具、技能、工作流、MCP、通道统一接入 |
单一事实源不是口号,而是可检查的结构约束:同一份状态不允许出现第二个写入口;两套实现(Rust 服务端与 TypeScript 前端)必须共用同一套折叠规则,并用同一组 fixture 断言。
2.3 主链路
sql
前端(编译进服务端二进制)
→ HTTP/SSE
→ 服务端 flow(命令分发 / 上下文装配 / 事件泵 / 收尾)
→ agent 循环(observe → solve → act → converge → close)
→ 工具 / 沙箱 / 子代理池
Copy
移动端不在主链路中,只是把同一个核心宿主进系统级应用------是宿主形态,不是架构分叉。
3. 演进:三次收束
3.1 第一段:能力展开(第 1--2 周)
从零建立 workspace,先落数据模型与 trait 边界,再实现 ReAct 循环、权限代理、沙箱、记忆与上下文阶段。这一阶段的策略是先展开:把能想到的能力面都铺出来,允许重复与粗糙。
事后看,这一步最重要的产出不是功能,而是边界:工具 trait、上下文阶段 trait、事件契约、数据库接口。边界一旦稳定,后续所有重构都有落脚点。
3.2 第二段:产品化与一致性危机(第 3--6 周)
系统开始被真实使用后,问题从"功能缺失"转向"状态不一致"。典型症状:
- 流式期间看到的内容,刷新后消失;
- 同一轮的工具卡片在实时视图和历史视图里数量不同;
- 断线重连后,终态事件丢失或重复。
根因是同时存在两套流式协议 和两套折叠实现。引入第二套协议时的理由是"字节兼容",但代价是此后每次修复都要在两条链路上各做一遍,且两边行为逐渐分叉。
这一阶段最值得记录的不是修了多少 bug,而是承认双协议是错误决策,并规划一次彻底替换。
3.3 第三段:系统收束(第 7--13 周)
收束以"单一事实源"为原则,分四步:
- 删协议:两套流式协议收敛为一套类型化事件契约,旧帧格式在一个发布窗内双解析后删除。
- 删能力面:角色/人格栈、跨机 UI 中继、插件舰队、手动压缩按钮、零消费路由与幽灵阶段集中退役。
- 重构骨架:两千行的流程函数拆成生命周期段;八十个扁平状态字段按域收敛。
- 换前端:旧前端冻结为黄金母本,新前端分批追平后接管出货。
转折点的判据很朴素:一致性类缺陷是否断崖式下降。删除第二套协议后,这类缺陷从"每天都有"降到"偶发且可定位"。
3.4 决策与反转
| 决策 | 当时的理由 | 事后判定 |
|---|---|---|
| 引入第二套流协议并保留旧编码器 | 字节兼容、渐进迁移 | 反例:双份实现导致重复修复与行为分叉 |
| 删除第二套协议,建立总线 + 序号 + 权威视图 | 单一事实源 | 全局转折点 |
| 手动压缩按钮退役 | 四级自动梯子已覆盖 | 正确:减少用户心智负担 |
| 跨机 UI 中继退役,协议只承载通信 | 职责单一 | 正确:减少跨机状态耦合 |
| 手机端改为系统级宿主 | 权限与能力上限 | 正确:把运行时与形态解耦 |
4. 关键设计
4.1 可靠性:以总线作为存活信号
一次真实故障暴露了设计缺陷:数据库工作线程被慢查询拖住,运行已经结束,但终态事件写不进数据库,于是客户端永远停在"运行中"。
修复的关键不是给数据库加超时,而是换一个在数据库阻塞时仍然可用的存活信号------会话总线。总线记录最后一次发布事件的时刻;看门狗每 30 秒扫描一次,超过静默阈值(默认 900 秒)的运行被取消并收到错误事件。停在等待用户输入或确认的运行被跳过,避免误杀。
配套机制:
- 取消贯通:取消信号同时到达模型提供方、沙箱与进程树,避免"界面停了、进程还在跑"。
- 排空宽限:命令本身退出后,若后台子进程仍持有输出管道,等待 2 秒后放弃排空并用已收集输出返回,而不是无限等待。
- 停放上限:等待人工输入的运行有 24 小时上限,到期持久化一条系统说明并收敛为断开状态。
- 熔断:提供方连续失败 5 次后打开 30 秒,期间快速失败,避免拖垮整个进程。
这些机制有一个共同点:由事故驱动,但落成常驻结构,而不是一次性补丁。
4.2 上下文:统一入口、分阶段预算、四级压缩
上下文是 agent 成本与质量的主战场。系统把所有上下文收口到一个装配入口,内部是 18 个阶段,按稳定优先级排序,每个阶段有独立的 token 预算。
压缩不是一个按钮,而是四级自动梯子:
| 触发条件 | 动作 |
|---|---|
| 窗口占用约 85% | 后台滚动摘要 |
| 窗口占用约 85% | 观测掩码:保留最近 5 条工具结果,其余折叠 |
| 窗口占用约 90% | 自动压缩悬崖 |
| 任务边界 / 模型主动调用 | 任务边界滚动、模型自调 compact |
历史窗口不再用固定条数,而由 token 预算驱动;摘要水位线用于避免"摘要行 + 原始行"重复注入。
可观测性是裁剪的前提:每个阶段输出一份快照,记录名称、描述、消息数、估算 token、是否贡献。零贡献阶段在 UI 中置灰但不隐藏,装配顺序完整保留------否则删着删着,就没人知道少了什么。
裁剪方法不是凭感觉删 prompt,而是逐阶段回归:固定任务集与真实会话,逐项判断"这个阶段是否改变了结果",对零贡献或高重复的片段做删除或合并。效果等价是删除的前提,token 下降是结果,不是目标。
4.3 可信输出:从门控到内生
早期方案是在草稿写完后跑一组提交门,不通过就整篇重答。问题是:成本高、破坏已经流出的内容、且重提示是泛化的。
收束后的方案把验证拆成三层:
- 动作时记账 :检索类工具的结果进入证据账本,论断以
[E#]绑定。 - 提交时对账:只检查覆盖率,缺哪补哪,不再整篇重写。
- 运行后修正:独立复核以"锚点编辑"的形式修改正文------唯一匹配、删除预算有上限、最多 8 处、必须带来源、代码围栏平衡;任何一条不满足,整批降级为失败且原文不动。
这套设计的核心判断是:验证不应表现为拦截,而应融入上下文与后处理。显式的门只保留在基准测试路径上,用于钉住字节形态。
4.4 并发:三类并行,各自的闸门
并行不是无条件 join_all,而是三类场景、三套边界:
| 场景 | 并行条件 | 约束 |
|---|---|---|
| 同轮工具调用 | 只读白名单(搜索、读文件、代码检索) | N 次调用从 Σ(latency) 降到 max(latency) |
| 子代理扇出 | 子任务可分解为独立部分 | 上限 6 个;并行成本不超过剩余墙钟一半 |
| 后台任务 | 与主循环无状态依赖 | 独立注册表、可取消、结果持久化 |
子任务如果是顺序依赖的,明确禁止扇出------多智能体链式执行在依赖任务上会显著劣化。相位机(侦察 → 深挖 → 验证 → 提交)按剩余预算推进,保证并行不会挤掉综合与收尾的时间。
4.5 自适应思考强度
思考强度不是用户旋钮,而是一条单向阶梯:每个运行从低档开始,遇到难度信号、连续失败或验证未过时升级,运行结束重置。
实现上刻意选择进程级共享而非按运行隔离:它是一个每次调用的推理深度提示,不是正确性状态;并发运行读到升级值只会多花少量思考 token,不影响结果。为伪需求把状态穿透共享客户端,收益为零。
4.6 终态:单一出口
长事务服务最容易出错的地方是结束路径。系统把循环拆成五段,每段用哨兵类型把 continue/return 映射成显式状态,保证只有一条终态路径。
重连流的退出条件不是"收到终态事件",而是 运行中 ∪ 后台任务 ∪ 事后校验 三个注册表同时为空------因为终态可能先于异步校验到达,提前关流会把校验结果丢掉。
5. 特大重构的方法学
5.1 后端:行为保持 + 逐提交绿 + 搬迁审计
两千行的流程函数拆成四个模块(命令分发、上下文装配、事件泵、收尾),两千行的循环拆成五段(干预、执行、收敛、收尾),八十个扁平状态字段按域收敛。
规则:
- 只按扣除测试后的代码行判断是否拆分,不为行数均匀而拆;
- 每个提交独立通过全量测试,任何一步都能单独回滚;
- 纯搬迁用
--color-moved逐段审计,证明"只是移动"; - 控制流改造用哨兵枚举,而不是在外面包一层 if。
5.2 中间层:契约先行,迁移在后,删旧最后
中间层最容易"改一半"。固定顺序是:
- 测绘:盘清两套协议、两套折叠、两套适配器的真实调用面;
- 契约先行:定义单一类型化事件,补跨语言 fixture;
- 双端同构:服务端与前端实现同一套折叠规则,fixture 被两侧同时断言;
- 删旧路:旧 shim 与旧帧格式在一个发布窗内双解析,确认无调用方后整段删除。
5.3 前端:黄金母本 + 分批对齐 + 零后端改动
旧前端冻结为黄金母本,只作对照与回滚基线;新前端按批次追平(会话骨架 → 对话体验 → 视觉体系),每批独立验证。视觉重做坚持"只换 token 与组件形态",零服务端改动、零契约改动;已有变量名保留,避免组件类名连带重写。
5.4 共同规则
- 先测绘,再动刀;
- 每步可回滚;
- 行为保持要有机器判据(测试、fixture、搬迁审计);
- 删除旧路径放在最后,并留一个发布窗;
- 文档同步,过期的描述要标注"描述漂移",而不是假装没发生。
6. 可维护性是怎么被设计出来的
| 手段 | 做法 | 效果 |
|---|---|---|
| 单一入口 | 上下文、事件、扩展各只有一个入口 | 新增能力不需要改 N 个调用点 |
| 单一协议 | 两套流协议收敛为一套 | 消灭"两边各修一遍"的重复缺陷 |
| 契约即测试 | fixture 被两种语言同时断言 | 跨语言漂移在 CI 被拦住 |
| 度量面 | 阶段快照、token 账本、环形日志、慢查询告警 | 问题可定位,不靠猜 |
| 删除机制 | 三份测绘(crate / 路由 / 特性)+ 逐项拍板 | 删得动,且不误删运维与 AI 调用面 |
| 质量门 | fmt + 静态检查 + 全量测试 + 前端构建,工具链固定 | 重构不会悄悄破坏别处 |
| 文档系统 | 现状、任务步骤、变更理由分开记录 | 上下文能跨会话存活 |
一个值得单独说的判据:删除时**"前端零调用"并不充分**,还必须排除运维面与 AI 调用面。否则会把"当前没人点、但 agent 会调用"的能力误删。
7. 证据与评估
7.1 可复现的过程指标
- 27 个 crate,约 18 万行 Rust,3.4 万行前端代码;
- 1,814 个自动化测试;18 个跨语言 golden fixture;
- 84 天内 1,084 次提交(至第 90 天为 1,136 次),55 个有产出的工作日;
- 40 余个版本;单日提交峰值 58 次;
- 插入 46.3 万行 / 删除 20 万行;收束期单月删除量达到插入量的 62%。
7.2 事故驱动的验证
三起真实事故被完整记录为"现象 → 证据 → 根因 → 机制 → 验证":
- 数据库工作线程阻塞导致终态丢失 → 总线看门狗 + 慢语句告警;
- 后台子进程持有管道导致运行挂死 → 排空宽限;
- 设备反复关机 → 从引导原因、电源管理日志与电池节点定位为框架温度断电,最终用只读文件系统覆盖 + 守护进程钳制。
这些修复的价值不在单个 bug,而在于它们都变成了常驻机制,后续同类问题不再复发。
7.3 效度威胁
必须诚实说明:
- 这是单人、单工作负载的系统,过程指标不能直接外推为团队效率;
- 上下文逐阶段裁剪是观察性回归,不是随机对照实验;效果等价是工程判断,不是统计结论;
- 基准测试脚本存在,但成绩不作为结论引用------样本小、污染风险高、不可复核;
- 部分版本号在历史中缺失,说明发布流程并非完全严格。
8. 经验与反例
- 一致性先于功能。没有单一事实源之前,新增功能只会增加不一致的入口。
- 减法需要判据。"没人用"不是判据,"是否构成 agent 能力面"才是。
- 重构需要机器证明。全绿、fixture、搬迁审计,把"我以为没改坏"变成"证明没改坏"。
- 事故要沉淀为机制。修一个 bug 不算完成,去掉这类 bug 的生存条件才算。
- 反例同样有价值。双协议决策被完整记录并写进复盘,比假装一直正确更有用。
9. 结论
通用 agent 运行时的成熟度,不取决于接入了多少工具,而取决于三件事是否被结构化地解决:状态是否只有一个真相、上下文是否可计量地裁剪、输出是否被当作不可信输入来对账。本文描述的演进路径可以概括为一句话:
先把能力展开到边界,再用单一事实源把它们收束成一个系统。
收束的代价是持续的删除与重构;收益是系统开始变得可预测------而这正是"能每天用"和"能演示"之间的差别。
附录 A:个人贡献与工程规模
本项目由单人独立完成,覆盖架构设计、核心实现、前端、移动端宿主、部署与长期维护。
- 周期:约 90 天,55 个有产出的工作日;
- 提交:1,136 次,单日峰值 58 次;
- 规模:27 个 crate,约 18 万行 Rust,3.4 万行前端,5.5 千行移动端;
- 质量:1,814 个测试,跨语言 fixture 18 个,完整本地质量门;
- 交付:40 余个版本,桌面端、移动端与调度中心均可运行。
这些数字只说明投入强度;真正的工作量在正文的三次收束与三类重构里。