生产级 Agent 系统要先划边界再做功能:Core 只跑 model/tool 循环,Runtime 管持久化语义,宿主层做翻译,三层依赖方向单向不可逆。Agent Host Protocol 是一等交付物,幂等键、事件可见性、版本握手必须先于功能设计——重放同一幂等键换 payload 是错误,不是静默覆盖。Session / Run / RuntimeStatus 三维状态必须独立分离,能力扩展靠窄接口 facet 注册,Core 不应该知道 Memory、Scheduler 是什么。
一、从"包一层"到"独立产品"
很多 Agent 系统的起点都是包装一个已有的 LLM 服务——加一些胶水代码,把请求转发出去,把响应拿回来。这个阶段会工作,但很快会遇到一个根本矛盾:Agent 的生命周期被宿主系统控制,而不是被 Agent 自己控制。
每一次想给 Agent 加新能力,都要先问"宿主允不允许"、"宿主的 session 模型兼不兼容"。这不是 Agent 的问题,是设计边界划错了。
正确的做法是把 Agent 设计成一个独立产品,有自己的 Core、Runtime、Protocol、存储和 CLI。外部系统变成消费者——通过一个版本化的协议与 Agent 对话,而不是持有 Agent 的内部状态。
这带来一个直接的好处:Agent 和宿主系统可以独立发版,互不耦合。
二、分层要分得干净
一个生产级 Agent 系统至少需要三层,每一层的职责边界都应该是硬性的:
Agent Core 只做一件事:执行一次 model/tool 循环。它接收一个已经组装好的上下文和工具集,返回一个事件流。它不知道数据库、HTTP、session、用户身份、调度是什么。
Agent Runtime 管理持久化语义:session 创建和分支、run 的状态机、context 的组装和压缩、capability 的发现和快照、steering 和 follow-up 邮箱。它通过 Port 接口访问存储和事件总线,不直接依赖任何具体实现。
宿主适配层 是外部系统侧的翻译层和反腐层:做认证、翻译外部请求为 Agent 命令、做事件投影、管理凭证生命周期。它只能依赖协议包,不能触碰 Agent Core 的内部类型。
这个分层的关键不是"画了几个框",而是依赖方向是单向的,而且每一层的变更不需要另一层同步改动。
三、协议是一等交付物
在很多系统里,"协议"是个事后概念——先把功能做出来,再想怎么暴露它。但对于一个 Agent 来说,协议应该反过来:Agent Host Protocol 是最先确定的东西之一,它决定了 SDK、CLI、daemon、二进制包能做什么,也决定了外部系统能要求 Agent 做什么。
协议设计的几个关键决策:
- 命令信封携带幂等键:
commandId标识一次尝试,idempotencyKey标识这个逻辑操作。重放同一个幂等键但换了 payload 是错误,不是"静默覆盖"。 - 事件信封携带可见性:
public / internal / sensitive不是授权机制,是投影元数据。消费者自己决定用哪些事件,但不能假装不知道有些事件是敏感的。 - 序号是单调的,replay 是无缝的:事件按 session 维度单调递增,断线重连后可以从任意 cursor 继续,不会出现空洞也不会重复。
- 握手在前,能力协商在前:任何凭证传输之前必须先协商协议版本。消费者遇到不兼容的 major 版本必须拒绝连接,而不是"先发了再说"。
四、循环算法里的细节
Agent Core 的 loop 看起来简单:请求模型 → 执行工具 → 再请求模型。但魔鬼在细节里。
工具结果的顺序:工具可以并发执行,但结果必须按 assistant 消息里的调用顺序写回。这是因为模型在下一步会看到这段历史,顺序混乱会影响模型的行为。
Steering 的时机:外部注入的 steering 消息(比如用户在 run 进行中发来的补充指令)不能直接插入正在进行的 provider 请求。它必须在"安全边界"——当前模型响应和它触发的工具都 settle 之后——才能被消费。消费后的 steering 消息需要以适当可见性持久化,确保事件重放能还原当时的上下文。
幂等策略是前置条件,不是事后补救:每个有副作用的工具在 dispatch 之前必须声明幂等策略。如果工具状态是 unknown_completion(既不知道成功也不知道失败),不能自动重试。这是一个硬约束,不是建议。
预算检查要双次:在开始新工作之前检查一次预算,在实际用量到达之后再检查一次。只在事后检查是不够的——对于昂贵的或有副作用的操作,发现超限的时候已经晚了。
abort 语义要精确:abort 信号传播给 provider 和活跃工具后,Core 必须停止接受新的工具调用,等待有界的清理,用明确的 cancelled 结果关闭未完成的工具序列,最终发出且只发出一个 terminal event。"恰好一次 terminal event"是一个不变量,不是努力目标。
五、状态不要压缩成一个枚举
一个常见的错误是用单一的状态字段描述 session + run + runtime 三个维度。
正确的做法是把它们明确分开:
- SessionStatus:
OPEN | CLOSED | ERROR— 描述对话是否可用 - RunStatus:
QUEUED | STARTING | RUNNING | WAITING_APPROVAL | CANCELLING | SUCCEEDED | FAILED | CANCELLED— 描述一次执行的状态机 - RuntimeStatus:
DETACHED | ATTACHING | ATTACHED | DETACHING | FAILED— 描述当前进程是否 attach 在一个 run 上
一个 session 可以是 OPEN 同时没有任何 runtime 在运行。一个 run 失败了,session 不应该自动出错。一次进程重启后,可以重新 attach 到 session 继续工作,已有的 run 状态不变。
把这三个维度压进一个枚举,看起来简单,实际上丢失了这些组合语义,最终会在某个边界情况里出错。
六、数据所有权要清晰
Agent 必须是自己的对话和执行状态的唯一 source of truth。没有外部文件、外部运行时、外部进程持有另一份"权威版本"。
几个所有权规则:
- 消息是模型可见的对话历史的权威来源
- Run 和 Step是执行状态的权威来源
- Tool call 记录是副作用审计和幂等性的权威来源
- Context snapshot是"这次 run 组装了什么上下文"的权威来源
- 事件是有序观测和重放的权威来源,不是重建所有领域状态的来源
这些规则的价值在于:当系统出问题时,你知道去哪里找答案,不用在几个系统之间对比"谁的版本是对的"。
七、Context 压缩不是破坏历史
当对话历史太长撑爆 context window 时,压缩是不可避免的。但压缩不应该是破坏性的——原始消息应该保留,压缩节点插入在消息树里,后续的 context 组装从压缩节点开始,而不是从根节点开始。
这个设计让"从压缩前的某条消息分支"依然是合法操作——走原始路径;"从压缩节点分支"则走摘要路径。两种分支都通过同一个消息树遍历逻辑处理,不需要对压缩节点特殊处理。
压缩后的 context snapshot 会记录 compaction_artifact_id,使得任意一次历史 run 都是可重现的,不会因为后来发生了压缩而失效。
八、能力扩展的接口设计
一个 Agent 系统想要长期演进,能力扩展的方式至关重要。推荐的模式是窄接口 facet:
能力模块向 Runtime 注册,Runtime 在固定时机调用能力——context 组装、工具 dispatch、事件通知、run 触发、run 后处理。能力不能反向调用 Runtime 的内部状态,只能通过类型化接口(如 RunTrigger)发起新的 run。
Runtime → 调用能力
能力 → 只通过类型化接口和 Runtime 交互
这个设计的好处是:每个能力只实现它需要的 facet。Memory 模块实现 context 贡献和 run 后处理,Scheduler 模块实现 run 触发,互相不干扰。新加一个能力不需要修改 Core 或 Runtime 的任何代码。
在 run 开始时,Runtime 把当前激活的能力版本、工具定义、策略等冻结成一个 capability snapshot。这意味着 run 执行过程中的配置变更不会影响正在执行的 run,只会影响后续的 run——这对调试和问题排查非常重要。
九、Evolution:受控的自我改进
Agent 的 Evolution 能力描述了一条受控的自我改进流水线:
已完成的 run
→ 观测
→ 候选 skill / prompt / workflow
→ 隔离评估
→ 策略或人工审批
→ 不可变发布
→ 有范围的激活
→ 结果监控
→ 可回滚
几个硬约束值得注意:不能直接修改 Core 或 Runtime 的源代码,不能自动扩大权限或工具策略,不能静默替换活跃版本,每个候选都必须有来源和评估证据,每次激活都是可逆的。
这些约束的存在不是为了限制 Agent 的能力,而是为了确保演进过程是可观测、可解释、可回退的。一个能自我改进但改进过程不透明的系统,在生产环境里很难信任。
小结
回顾整个设计过程,有几个反复出现的主题:
-
边界优先于功能:先想清楚谁拥有什么,再想怎么实现。边界划清了,功能加起来会很自然;边界划错了,越加功能越乱。
-
协议是契约,不是实现细节:协议一旦发出去就很难改,要把它当成一等交付物来设计。
-
状态要分离,不要合并:把不同维度的状态强行合并进一个枚举,短期看简单,长期看是债务。
-
能力扩展靠接口,不靠特殊处理:Core 不应该知道 Memory、Scheduler、Subagent 是什么,它只知道
ContextContributor、ToolProvider、LifecycleSubscriber。 -
可观测性和可回退是设计目标,不是事后补充:幂等键、审计日志、capability snapshot、Evolution 的回滚机制——这些都不是"有空再加"的东西,它们决定了系统在出问题时是否可以被理解和修复。
