「如一」是我们做的一个面向 C 端的云端数字分身:它在云端 7×24 运行,有自己的浏览器、文件系统和记忆,能替你持续完成任务。这个系列逐层解读如一的真实架构------不是 Demo 级玩具,而是一个多租户生产系统。开篇先讲最核心的执行模型:一个 Agent 到底"活"在什么结构上。
为什么大多数 Agent 只是 Demo
先看几个 Agent 项目的经典死法:
- 进程崩一次,会话上下文全丢,用户只能从头再来;
- 模型追问"你确认吗?",然后整个线程挂在那里等用户回复,一等就是三天;
- 模型被限流,重试逻辑无限循环,账单爆炸;
- 上下文越来越长,直到某一天 prompt 超出窗口,整个会话永久报废。
这些都不是模型能力问题------换个更强的模型,一个都解决不了。它们是架构问题:把 Agent 当成了一次函数调用,而它实际上是一个持续运行系统。
如一从第一天就是奔着"长期在线的数字分身"去设计的,所以这些问题必须在架构层回答。这篇讲清楚我们的答案:四个时间尺度、一条决策与执行的分工线,以及几个反直觉但被生产验证过的设计决策。
先给定义:如一眼中的 Agent
Agent 不是"带工具的聊天模型",也不是一条从 Prompt 到 Answer 的请求链。如一的定义是:
一个由消息驱动、以持久事实为连续性基础、由模型进行开放决策、由运行时保证确定性执行,并能持续对外部世界产生可验证结果的智能运行系统。
五个要素,缺一不可:
- 消息驱动:外部世界以统一消息进入。Agent 不直接依赖具体渠道------WebSocket、IM 协议、摄像头驱动,都是接入层的事。
- 事实连续:会话历史、工具结果、交付状态进入权威存储。进程内对象不是长期记忆。
- 开放决策:目标理解、路线选择、内容判断交给模型。
- 确定执行:状态迁移、权限、事务、幂等、并发、恢复由程序保证。
- 真实结果:输出不只是文本,还可以是文件、应用、卡片,以及对外部系统的真实操作。
graph TB
WORLD["外部世界<br/>用户 · 设备 · 系统事件 · 定时任务"]
subgraph INPUT["Input Plane"]
IN["身份归一 · 鉴权 · 多模态接入<br/>幂等 · 排队 · 信箱"]
end
subgraph RUNTIME["Agent Runtime Plane"]
RUN["会话调度 → Start Run → 恢复事实<br/>→ AgentLoop(Turn × 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 是决策与执行内核,也是本文剩下部分要拆的东西。
把三个平面展开,如一的完整架构长这样(实线是命令、执行与交付的主路径,虚线是发现、读取、持久化等事实关系):
graph 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 内部的结构展开是这样:
graph 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、崩溃恢复的完整链路)。有兴趣可以关注,也欢迎评论区交流你踩过的坑。