← 技术专栏
阅读模式

Agent 架构:用稳定的边界容纳变化

从 Agent 实践出发,讨论如何用稳定的职责与接口约定,让模型、任务策略、执行环境和权限实现各自演进。

文章目录 6 节

模型会换,工具会增加,执行环境也可能从本机搬到云端。一个 Agent 能否经得起这些变化,取决于变化会传到多远。

如果只是换一种模型,任务推进、文件保存和权限审批就都要跟着修改,我们很难放心地继续扩展它。组件之间共享了太多内部假设,每一次局部改进,都需要重新检查整个系统。

这篇文章讨论的,就是怎样缩小这种影响。我们的思路是:先找出 Agent 长期需要承担的职责,约定它们之间交换什么、各自承诺什么,再把模型选型、循环策略、执行环境和审批方式留在各自的实现内部。

云端 Agent 是我们这次实践的载体。这套思路也适用于本地 Agent:执行位置可以改变,推进任务、执行操作、保留记录和控制权限的责任仍然需要有人承担。本文会从这些职责出发,追问它们为什么值得分开,以及拆到哪里应该停下来。

本项目的核心职责关系:任务循环连接模型、执行、权限与会话记录,执行组件按需访问持久文件。

图 1 · 先看横向主线,再看下方的支撑职责。这是本项目的拆分,不是所有 Agent 的必选清单。 点击图片可查看原图。

稳定的是职责,变化的是实现

先澄清“不变”的含义。我们无法提前写出一份永远不需要修改的接口,也不该把今天的方法签名当成永久标准。值得稳定下来的,是调用方可以依赖的职责和行为。

例如,执行组件接受操作请求,报告执行结果。它内部用本地进程还是云端沙箱,由实现选择;但它不能在没有确认操作完成时,给出一个成功结果。这条约定一旦被打破,任务循环就可能作出错误决定。

Anthropic 在《Scaling Managed Agents》中,把任务循环、会话记录与执行环境分开,让它们能够分别演进。这给我们的启发是:寻找边界时,要看哪些事情有理由独立变化。Anthropic 原文

我们据此反复检查一个问题:替换这个组件时,其他部分究竟需要知道多少?

任务循环依赖执行接口的行为约定,本地进程与云端沙箱是可替换的实现。

图 2 · 虚线连接可选实现,不表示同时执行。调用方依赖约定,具体实现可以替换。 点击图片可查看原图。

这个例子也划出了适用范围。只有新实现仍能履行原有约定,替换才可以限制在边界内。如果加入实时语音、协作编辑等新能力,原有约定已经不够,就应当承认接口也需要演进。

为什么要分开“推进任务”和“执行操作”

一个最小的 Agent 会重复做几件事:把当前信息交给模型,取得回答或操作建议,执行操作,再把结果带回下一轮。

很容易把它们写进同一个循环。程序短,路径直观。真正需要考虑的是:模型调用、任务策略和执行环境,会不会总是一起变化?

我们的判断是,它们各有自己的变化来源。

模型服务会更换,请求格式和能力也会不同。把接入细节放在模型接口(ModelGateway)之后,任务循环才能围绕统一的响应工作。不过,格式兼容不代表能力等价;新模型能否完成任务,仍需单独验证。

任务循环(Harness)则决定如何推进:给模型哪些上下文,如何利用工具结果,何时继续、何时结束。随着模型能力变化,今天有效的上下文整理或重试策略,明天可能需要重写。把这部分作为可替换的组件,可以让策略调整集中在这里。

执行组件(Execution)负责把操作落实到环境中。创建沙箱、启动进程、读写文件,都不该要求任务循环了解某一家云厂商的细节。循环需要知道的是环境是否可用、操作是否完成,以及拿到了什么结果。

这就是把“大脑”和“双手”分开的实际含义。换一种任务推进策略,不应顺带要求重写沙箱管理;更换执行环境,也不应迫使我们重做模型接入。

这种边界同样适用于本地部署。本地进程与云端沙箱的差异留在执行实现里。权限范围、可用工具和环境能力仍要明确告知调用方,不能为了统一接口,把真实差异藏起来。

为什么记录和成果不能依附于执行环境

任务推进与执行分开以后,还有一个容易遗漏的问题:工作结束或环境被销毁以后,什么需要留下?

在我们的实践里,沙箱可以过期,但用户上传的文件、明确保存的成果和会话记录需要保留。它们的寿命显然长于某个执行环境。让临时环境同时负责保管这些数据,就把“换一个工作现场”变成了“可能丢掉整项工作”。

我们做过一个很小的验证:Agent 计算整数的平方和并保存结果,释放沙箱后,重新打开持久存储,文件仍然能够读取。这个实验只验证了持久化边界;它没有证明整个任务可以自动恢复。

这里还需要分清两类数据。

会话记录保存发生过什么:用户提出了什么要求,模型建议了什么操作,执行返回了什么。模型每次看到的上下文,可以从这些记录中筛选、压缩或重组。上下文策略改变时,原始记录应当仍然可用。这是会话存储(SessionStore)独立存在的理由。

成果文件则是用户要继续使用的东西。报告可能来自某次会话,却需要在别的会话里再次打开。我们让文件归属于工作空间,同时记录生成它的会话和任务;文件存储(ArtifactStore)负责保存与取回这些内容。

两者在业务上有关联,在职责上各自明确。分开接口,也不意味着必须使用两套数据库。

旧执行环境过期后重新创建环境,会话记录、上传文件和明确保存的产物继续保留。

图 3 · 横向看时间,纵向看不同内容的寿命。保留记录与文件,不等于恢复进程或自动续跑。 点击图片可查看原图。

这项拆分有前提:系统确实需要保留历史和交付文件。一个只回答单次问题、没有持久成果的 Agent,可以省去相应部分。架构应当回应真实需要。

为什么权限不能藏在执行逻辑里

执行组件知道如何运行命令,却未必有权决定这条命令是否应该运行。

我们最初让每条 shell 命令都等待批准。使用下来,频繁确认带来了审批疲劳,于是需要支持自动审核和更宽松的权限模式。这个变化说明,“怎样执行”与“允许谁在什么条件下执行”有不同的演进方向。

如果每个工具都各自实现审批,新模式就需要逐个工具修改,也很难确认它们遵循了同一套规则。

权限管控(PermissionControl)的职责,是对拟执行的操作给出授权决定。任务循环拿到允许决定后,再调度执行。至于决定来自人工、规则还是审核模型,可以留在权限组件内部。

这里真正需要稳定的,是授权与实际操作之间的对应关系。批准一条命令,不能被解释成批准后来替换过参数的另一条命令;拒绝一个操作,也不能只是在界面上显示“已拒绝”,后端却继续执行。

权限接口并不能保证判断永远正确。自动审核仍可能误判,简单的个人工具也可以采用固定策略。这些差别影响实现复杂度,却不改变“动作需要依据什么授权才能发生”这个问题。

接口拆到哪里,才算足够

走到这里,可以看出我们的拆分依据:模型接入、任务推进、执行环境、授权策略,以及需要长期保留的数据,各自承担不同责任,也各有变化来源。

但每增加一个接口,读者就要多理解一组关系,开发者也要多维护一份约定。我们不能只计算解耦的收益,还要计算理解成本。

我的判断标准是:能否说清楚,这个边界隔离了哪一种独立变化?

人工审批窗口、审核模型、规则列表,都是权限实现可以采用的手段。它们不必在核心架构中与权限管控并列。日志怎样分页、沙箱多久回收,也不值得仅仅因为现在有一段代码,就被提升为核心组件。

前端是类似的例子。用户需要清楚地看到任务进展、作出授权决定、取得结果;这些交互要求应该被保留。具体使用哪套界面组件,可以尽量复用成熟方案,不必让它决定后端怎样组织任务和状态。

拆分之后,还要检查接口有没有把关键事实说清楚。一个只写着“执行失败”的返回值,就可能不够用:网络超时后,命令也许已经修改了文件。如果调用方把它当成“尚未执行”而重试,操作就可能重复。此时接口需要允许表达“结果未知”。

这种语义约定,比把方法写成抽象类更重要。其他组件正是依据这些信息行动,边界才能真正承担隔离变化的责任。

让一次改动,尽可能只影响一处

回看这次实践,我认为最值得保留下来的,是一种判断架构的方法。

先明确系统长期承担哪些责任,再找出它们各自会因为什么而变化。对有必要独立演进的部分,用接口约定职责、数据归属和结果含义;把具体算法、产品选择和运行方式留给实现。

这样设计以后,模型、执行环境和审批策略仍然会变,但变化有了明确的容纳位置。评估一次拆分是否有效,也有了具体问题:换掉这个实现,哪些调用方仍然可以照常工作?哪些约定必须重新讨论?

我们用云端 Agent 做了第一版实现,这套判断方法可以继续用于本地 Agent 和其他任务场景。具体边界应随需求调整。值得坚持的是:让组件依赖彼此明确的承诺,尽可能少地依赖彼此内部的做法。


开源项目地址:【待补充,项目发布后更新】

· · ·

写到这里,留待实践继续检验。