「如一」是我们做的一个面向 C 端的云端数字分身:它在云端 7×24 运行,有自己的浏览器、文件系统和记忆,能替你持续完成任务。这个系列逐层解读如一的真实架构——不是 Demo 级玩具,而是一个多租户生产系统。开篇先讲最核心的执行模型:一个 Agent 到底"活"在什么结构上。
为什么大多数 Agent 只是 Demo
先看几个 Agent 项目的经典死法:
- 进程崩一次,会话上下文全丢,用户只能从头再来;
- 模型追问"你确认吗?",然后整个线程挂在那里等用户回复,一等就是三天;
- 模型被限流,重试逻辑无限循环,账单爆炸;
- 上下文越来越长,直到某一天 prompt 超出窗口,整个会话永久报废。
这些都不是模型能力问题——换个更强的模型,一个都解决不了。它们是架构问题:把 Agent 当成了一次函数调用,而它实际上是一个持续运行系统。
如一从第一天就是奔着"长期在线的数字分身"去设计的,所以这些问题必须在架构层回答。这篇讲清楚我们的答案:四个时间尺度、一条决策与执行的分工线,以及几个反直觉但被生产验证过的设计决策。
先给定义:如一眼中的 Agent
Agent 不是"带工具的聊天模型",也不是一条从 Prompt 到 Answer 的请求链。如一的定义是:
一个由消息驱动、以持久事实为连续性基础、由模型进行开放决策、由运行时保证确定性执行,并能持续对外部世界产生可验证结果的智能运行系统。
五个要素,缺一不可:
- 消息驱动:外部世界以统一消息进入。Agent 不直接依赖具体渠道——WebSocket、IM 协议、摄像头驱动,都是接入层的事。
- 事实连续:会话历史、工具结果、交付状态进入权威存储。进程内对象不是长期记忆。
- 开放决策:目标理解、路线选择、内容判断交给模型。
- 确定执行:状态迁移、权限、事务、幂等、并发、恢复由程序保证。
- 真实结果:输出不只是文本,还可以是文件、应用、卡片,以及对外部系统的真实操作。
三个平面:Input / Runtime / Output
flowchart TB
WORLD[`外部世界<br/>用户 / 设备 / 系统事件 / 定时任务`]
subgraph INPUT[`Input Plane`]
IN[`身份归一 / 鉴权 / 多模态接入<br/>幂等 / 排队 / 信箱`]
end
subgraph RUNTIME[`Agent Runtime Plane`]
RUN[`会话调度 - Start Run - 恢复事实<br/>- AgentLoop Turn x N- End Run`]
end
subgraph OUTPUT[`Output Plane`]
MSG[`Message<br/>沟通与反馈`]
ART[`Artifact<br/>可复用成果`]
CMD[`Command<br/>改变外部世界`]
end
WORLD --> INPUT
INPUT -- `RuntimeCommand` --> RUNTIME
RUNTIME -- `RuntimeEvent` --> OUTPUT
MSG --> WORLD
ART --> WORLD
CMD --> WORLD
三个平面各有一条容易被忽视的设计原则。
Input Plane 的核心是"信箱",它不是 UI 比喻,是并发模型。 外部消息可以随时到达;同一会话的主执行必须串行消费;进程故障后未完成的输入必须能被重新认领。Runtime 不应该知道手机号、Cookie、WebSocket 或某个 IM 的报文结构——它只处理通道中立的命令和消息。
Output Plane 的核心是"交付不等于回答"。 如一的输出至少有三类:Message(解释、确认、进度、最终文本)、Artifact(文件、报告、图片、音视频、可交互应用)、Command(对文件、浏览器、外部系统的真实操作)。三者都可以是中间结果或最终交付。是否展示、何时展示、用什么媒介展示,取决于用户目标,而不是"某个工具刚刚返回了文件"。
Runtime Plane 是决策与执行内核,也是本文剩下部分要拆的东西。
把三个平面展开,如一的完整架构长这样(实线是命令、执行与交付的主路径,虚线是发现、读取、持久化等事实关系):
flowchart TB
WORLD[`外部世界<br/>用户 / 设备 / 系统 / 时间`]
subgraph CHANNELS[`交互与接入`]
direction LR
WEB[`Web / App / IM`]
MEDIA[`语音 / 图像 / 视频`]
SENSOR[`传感器 / 系统事件`]
TIMER[`定时任务`]
end
subgraph GATEWAY[`Input / Output Plane / Gateway`]
direction LR
INGRESS[`身份与协议边界<br/>鉴权 / 归一 / 附件接入`]
MAILBOX[`可靠消息信箱<br/>幂等 / 排队 / 同会话串行`]
PROJECTION[`消息投影与连接<br/>历史恢复 / WebSocket`]
end
subgraph RUNTIME[`Agent Runtime Plane`]
direction TB
SCHEDULER[`会话调度<br/>认领 / 租约 / 追加消息`]
subgraph RUN[`Agent Run / 有限执行边界`]
direction TB
START[`Start Run<br/>固定身份 / Profile / Deadline`]
RESTORE[`Restore Facts<br/>Transcript / Compaction / Memory`]
subgraph LOOP[`AgentLoop`]
direction TB
GUARD[`Guard<br/>Run 边界守护`]
BEGIN[`Begin Turn<br/>turnNo + 1`]
OBSERVE[`Observe<br/>排干消息`]
CONTEXT[`Build Context<br/>按当前决策投影`]
ATTEMPT[`Model Attempt<br/>attemptNo + 1 / 模型开放决策`]
ACT[`Act<br/>工具批 / 领域用例`]
COMMIT[`Commit Assistant<br/>提交模型事实`]
DECISION_ROUTE{`Route Decision`}
TOOL_COMMIT[`Commit Tools<br/>提交观察事实`]
FINISH_TURN[`Finish Turn<br/>唯一执行终态`]
ROUTE{`Continue Run?`}
GUARD --> BEGIN --> OBSERVE --> CONTEXT --> ATTEMPT
ATTEMPT -- `failed / retry<br/>同一 Turn` --> OBSERVE
ATTEMPT -- `valid response` --> COMMIT
COMMIT --> DECISION_ROUTE
DECISION_ROUTE -- `recover / continuation<br/>同一 Turn` --> OBSERVE
DECISION_ROUTE -- `tools` --> ACT --> TOOL_COMMIT --> FINISH_TURN
DECISION_ROUTE -- `stop / abort` --> FINISH_TURN
FINISH_TURN --> ROUTE
ROUTE -- `next Turn` --> GUARD
end
FINISH[`End Run<br/>Completed / Failed / Aborted`]
START --> RESTORE --> GUARD
ROUTE -- `end Run` --> FINISH
end
EVENTS[`RuntimeEvent<br/>过程反馈 / 权威交付 / 终态`]
SCHEDULER --> START
FINISH --> EVENTS
COMMIT -. `模型增量事件` .-> EVENTS
TOOL_COMMIT -. `工具事件` .-> EVENTS
end
subgraph CAPABILITIES[`Capability & Execution Plane`]
direction LR
SKILL[`Skill<br/>领域选路`]
TOOL[`Tool / Harness<br/>确定性能力`]
USECASE[`Application Use Case<br/>事务与一致性`]
SANDBOX[`Sandbox / Browser<br/>隔离 I/O`]
EXTERNAL[`外部服务`]
end
subgraph FACTS[`Authoritative Fact Plane`]
direction LR
MYSQL[`MySQL<br/>Transcript / Run / Memory / Task`]
REDIS[`Redis<br/>RunSlots / Queue / Stream`]
OBJECTS[`OSS / Workspace<br/>附件 / Artifact`]
end
subgraph DELIVERY[`Delivery`]
direction LR
MESSAGE[`Message<br/>沟通与反馈`]
ARTIFACT[`Artifact<br/>可复用成果`]
COMMAND[`Command<br/>改变外部世界`]
end
WORLD --> CHANNELS
WEB --> INGRESS
MEDIA --> INGRESS
SENSOR --> INGRESS
TIMER --> MAILBOX
INGRESS --> MAILBOX
MAILBOX -- `RuntimeCommand` --> SCHEDULER
ATTEMPT -. `发现并选择` .-> SKILL
ACT --> TOOL --> USECASE
TOOL --> SANDBOX
USECASE --> EXTERNAL
SANDBOX --> EXTERNAL
TOOL -- `观察结果` --> TOOL_COMMIT
USECASE -- `业务结果` --> TOOL_COMMIT
MAILBOX -.-> REDIS
RESTORE -. `读取` .-> MYSQL
COMMIT -. `追加事实` .-> MYSQL
TOOL_COMMIT -. `追加事实` .-> MYSQL
SCHEDULER -.-> REDIS
EVENTS -- `Stream / PubSub` --> REDIS
SANDBOX -.-> OBJECTS
REDIS --> PROJECTION
EVENTS --> PROJECTION
PROJECTION --> MESSAGE
PROJECTION --> ARTIFACT
ACT --> COMMAND
MESSAGE --> WORLD
ARTIFACT --> WORLD
COMMAND --> WORLD
这张图的密度很高,值得放大看的部分后面都会逐一讲到。本文聚焦 Runtime Plane 内部,也就是 Run 和 Turn 那一块。
四个时间尺度:Session / Run / Turn / Model Attempt
Agent 系统最容易混淆的概念就是这四个,它们不是同一层东西:
| 尺度 | 定义 | 结束条件 | 连续性靠什么 |
|---|---|---|---|
| Session | 用户与 Agent 的长期会话空间 | 产品生命周期决定 | transcript、会话投影 |
| Run | 一次被消息触发、连续推进到终态的执行 | completed / failed / aborted | 持久事实 + runId |
| Turn | Run 内一次"观察—决策—行动—提交"的节拍 | 继续下一 Turn 或终结 Run | 本轮提交的 assistant/tool 事实 |
| Model Attempt | Turn 内真正发起的一次模型调用 | 合法返回、失败或取消 | 逐次调用事实记录 |
这个区分带来几个直接结论:
- 一个 Session 包含多个 Run。用户回答 Agent 的提问,是启动一个新 Run,不是恢复一条阻塞的线程。
- 一个 Run 通常包含多个 Turn——模型调用工具后需要看到结果再继续判断。
- 一个 Turn 包含一到多个 Model Attempt。恢复重试、协议重建、超长续写,只增加 Attempt,不增加 Turn。
- Model Attempt 是推理事实和计费事实,不是 Agent 任务的生命周期。
坦白说,最后一层区分是如一的最近一次线上重构里长出来的。早期实现里,模型被限流重试一次,前端的"第 N 步"就 +1,用户眼睁睁看着分身"走了 20 步"其实一件事还没做完;运行统计里 turn 数和模型调用数混在一起,排障时根本对不上。重构之后:Turn 只统计认知—行动节拍,Attempt 只统计真正进入模型的调用,恢复绝不虚增 Turn。
Run 内部的结构展开是这样:
flowchart TB
START[`Start Run<br/>固定身份 / Profile / Deadline`] --> RESTORE[`Restore Facts<br/>从持久存储恢复历史`]
RESTORE --> GUARD[`Guard<br/>abort / deadline / 上限守护`]
GUARD --> BEGIN[`Begin Turn`]
BEGIN --> OBSERVE[`Observe<br/>排干排队消息`]
OBSERVE --> CONTEXT[`Build Context<br/>为当前决策构造投影`]
CONTEXT --> ATTEMPT[`Model Attempt<br/>模型开放决策`]
ATTEMPT -- `失败 / 限流 / 截断<br/>恢复后仍在同一 Turn` --> OBSERVE
ATTEMPT -- `合法响应` --> COMMIT[`Commit<br/>提交模型事实`]
COMMIT --> ROUTE{`Route`}
ROUTE -- `执行工具` --> ACT[`Act<br/>工具批执行`]
ACT --> TOOLCOMMIT[`Commit Tools<br/>提交观察事实`]
TOOLCOMMIT --> FINISH[`Finish Turn<br/>唯一执行终态`]
ROUTE -- `停止 / 终止` --> FINISH
FINISH --> NEXT{`继续?`}
NEXT -- `下一 Turn` --> GUARD
NEXT -- `结束` --> END[`End Run<br/>Completed / Failed / Aborted`]
注意这张图刻意把模型放在 Turn 的"Decide"位置,而不是系统中心。系统的中心是从消息到事实、再从事实到行动的闭环,模型只是这个闭环里最擅长开放判断的那个零件。
一条分工线:决策自由与执行确定
Agent 系统设计的关键,不是尽可能多地把控制交给模型,而是把不同性质的决策放到正确的一侧。
模型负责开放决策:理解用户真正想完成什么;判断事实是否充分;选择能力、顺序和探索路径;根据工具观察调整路线;判断内容、审美和交付媒介;决定继续行动、向用户询问还是结束。
平台负责确定机制:消息幂等与同会话串行;参数与协议校验;权限、隔离、资源和 deadline;事务、幂等与副作用边界;超时、取消、重试预算和崩溃恢复。
一句话概括:
模型拥有路线选择权,平台拥有事实裁决权和执行约束权。
很多 Agent 项目的问题正是这条线画错了:让模型用 prompt 约束去"保证"幂等、用自然语言去"维护"事务、用系统提示词去"防止"路径越权。这些都是机械可验证的机制,交给一个概率模型去守护,出事只是时间问题。反过来,把路线选择写死成固定工作流的,也不叫 Agent,叫 RPA。
五个反直觉的设计决策
以下每一条都和直觉相反,也都在如一的生产环境里真实运行着。
1. Ask = 工程上结束 Run
分身需要用户补充信息或授权时怎么办?直觉答案是"挂起线程等回复"。我们的答案是:提交问题,发出事件,然后正常结束当前 Run。用户回复作为新消息启动新 Run,从持久历史里重新规划。
当前 Run:提交问题 → 发出 QUESTION_ASKED → COMPLETED
用户回复:新 RuntimeCommand → 新 Run → 从 transcript 继续
系统里不存在 SUSPENDED 状态,没有挂起线程,没有等待句柄。分身可以 7×24 在线,靠的不是一个永不退出的 while(true),而是持久事实 + 新消息的再次驱动。
2. 恢复不是新 Turn
模型被限流、流式响应超时、prompt 超长、输出被截断——这些都在同一个 Turn 内恢复,而不是"进入下一轮"。每个已开启的 Turn 必须且只能闭合一次(继续 / 完成 / 中止 / 失败);每次真正的模型调用必须落入 completed / failed / cancelled 之一。
好处不止是统计口径干净:Turn 上限这种安全护栏不会被瞬态故障消耗掉,排障时"第 3 轮第 2 次尝试"也能精确定位到单次模型调用的耗时和用量。
3. 连续性来自提交点,不来自"模型想过什么"
分身能不能从崩溃中正确恢复,取决于哪些事实已经提交,而不是模型生成过什么。所以提交纪律是硬不变量:
- 非法或残缺的工具调用不能提交,也不能执行;
- assistant 的工具调用一旦提交,每个调用必须有且只有一个结果;
- 用户取消导致未执行的工具,要补稳定的 cancelled 结果,防止历史形成残链;
- 中断的模型输出,只保留可安全回放的正文和完整工具调用。
每个 Turn 都必须把"模型在下一次恢复时能合法理解的事实链"作为提交单位,而不是只追求实时输出看起来连续。
4. 历史 ≠ 上下文
Transcript 是已发生事实的权威记录;Context 是为了当前 Turn 的决策、从历史和其他信息中临时构造的投影。每个 Turn 可以组合:未被摘要覆盖的历史、压缩摘要、本轮刚到的用户消息、system prompt、记忆与技能提醒、当前附件。
两条推论:上下文压缩改变的是模型看到的决策界面,绝不篡改已经发生的历史;system prompt 这类"每轮计算的运行环境"不写进历史,因为它不是会话中发生的消息。
判断某段信息该不该进上下文,只需要问一个问题:
它会改变当前 Turn 的哪一个决策?
答不上来的,就不该常驻上下文。
5. 失败是追加事实,不是抹除历史
Run 异常终止时,除了向用户投影错误,如一还会向历史追加一条经过裁剪的 failure observation。下一次 Run 能知道"上一次任务没有完整完成",同时继续信任此前已经提交的消息、工具结果和交付物。
失败不是把历史回滚到"没发生过",而是向历史增加一个新的现实事实。
几条可靠性不变量
最后留几条我们认为比任何具体实现都更稳定的不变量,供对照你自己的系统:
- 同一会话最多一个活跃主 Run,追加消息可靠入队。
- 每个 Run 都能从持久存储重建,不依赖旧进程的任何对象。
- assistant 工具调用与工具结果必须合法配对。
- 已提交事实不会因后续失败被撤回;失败本身追加为新事实。
- 恢复必须有原因、有预算、有终点;达到上限必须明确终止,不许静默消失。
- 外部可见事件从单一出口产生,最终结果可由权威链路恢复。
- 进程内缓存、连接、注册表只能加速当前执行,不能成为可恢复业务状态。
结尾
回到开头的定义,把它串成一句话:
如一是一个以消息为刺激、以 Run 为执行边界、以 Turn 为认知节拍、以持久事实维持连续性、以模型作开放决策、以平台完成确定执行,并最终通过 Message、Artifact 和 Command 改变用户世界的智能运行系统。
这个系列后面还计划写三篇:上下文工程(transcript / 压缩 / 记忆 / 临时投影的边界)、多 Agent 委派(子 Agent 与后台 Worker 的生命周期和血缘)、可靠性设计(幂等、outbox、崩溃恢复的完整链路)。有兴趣可以关注,也欢迎评论区交流你踩过的坑。