ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从单体Agent到Agency Agents:AI编程智能体生态架构与实践

从单体Agent到Agency Agents:AI编程智能体生态架构与实践

去年我们在做一个跨平台AI编程辅助系统的时候,遇到一个特别典型的困境:单个Agent的问答效果已经很好了,能改bug、能写测试、能解释代码,但一旦把它放进真实工程环境——多仓库、多语言、多工具链并行,还挂上CI、接入IDE插件和命令行——整个系统就开始失控。任务之间互相污染上下文,工具调用经常卡在权限审批上,模型响应时快时慢,排障只能靠翻日志和肉眼对时间戳。折腾了两个迭代之后,我们意识到问题不在模型,而在一整套缺少“生态”支撑的Agent架构上。这个方向后来被戏称为“Agency Agents”,本质上是把单个AI编程智能体扩展成一整层基础设施:调度、编排、治理、观测全部工程化,让Agent不只是一个对话接口,而是可跨平台运行的软件工程执行引擎。这篇文章就围绕这套生态的架构拆解和工程实践展开,适合正在做AI编程工具、内部提效平台、编辑器插件或Agent编排服务的开发者和架构师参考。

1. 从“一个Agent一把梭”到“Agent生态工程”:复杂度拐点在哪儿

1.1 单体Agent为什么会在真实工程里失灵

先说实话:单体Agent在demo阶段表现很好。你给它一个仓库路径,要求“找到所有未处理的异常并修复”,它能按顺序读文件、调用静态分析工具、生成补丁,一套流程跑完。但真实工程不是单线任务,而是大量并行、交错、互相依赖的操作流。

我总结过单体Agent的三类典型崩溃方式:

  • 上下文串扰:同一个会话窗口里先后处理代码审查、依赖升级、接口重构三件事,Agent会把上一次任务的结论当成当前任务的输入。早期我们甚至遇到过把A仓库的报错信息当成B仓库问题根因去修的情况。
  • 工具调用无状态化:Agent每次调工具都把完整参数带一遍,没有统一的工具注册和鉴权层。结果就是执行权限难以控制,禁止写生产环境的Agent偶尔也会因为误解析指令去动只读文件。
  • 任务路径不可复现:Agent的内部决策过程不落盘、不追踪,出问题时只能重跑一遍撞运气。在分布式执行、异步任务很常见的场景里,这种“黑盒式”Agent根本没法做质量保障。

这三个问题单独看都有绕过去的办法,但它们同时出现时,唯一的出路就是调整架构形态:从“一个Agent处理一切”变成“一组Agent各司其职,外面套上生态基础设施”。

1.2 Agent生态的分层模型:调度、执行、治理

“Agency Agents”这个概念,我自己的理解是一套三层结构:调度层、执行层、治理层。这三层不一定要拆成独立服务,但它们必须在逻辑上清晰分离,否则后面每次扩展能力都会变成在现有的面条代码上打补丁。

层级职责关键组件对应真实世界的类比
调度层接收任务、拆解、路由、排期任务队列、事件总线、计划器公司里的项目经理
执行层调用工具、读写上下文、生成结果Agent运行时、工具注册表、上下文存储具体干活的工程师
治理层权限、审计、监控、回滚权限沙箱、观测平台、策略引擎质量合规部门

在实际工程里,调度层和执行层往往会合并到一个Agent编排框架里,治理层则以中间件或旁路组件的形式存在。我们最初犯的错误就是把治理能力塞在Agent的业务代码里——每加一个权限策略都要改Agent主流程,后来才把权限判断抽成了独立的拦截器接口。这个经验排在所有架构决策之前:分层的核心目的不是看起来规范,而是让每一层的变更可以被独立测试、独立回滚。

2. 跨平台调度层:把LLM调用、工具注册和运行环境解耦

2.1 平台适配器的设计:IDE、命令行、网页端不完全一样

“跨平台”在这个项目里不是指一次写代码到处编译,而是指同一个Agent内核要能跑在多个宿主环境里:IDE插件、终端CLI、网页聊天框、甚至通过API对接CI系统。不同宿主对Agent的调用形态完全不同。

  • IDE插件要求低延迟、增量分析,用户不希望每次补全都等一个完整Agent推理。
  • CLI工具偏向任务批处理,要能串行执行几十个步骤并输出结构化报告。
  • 网页聊天框则更在意多轮对话的连贯性和流式输出体验。

如果把这些差异全部写进Agent内核,内核很快就会变成一团乱麻。我们采用的方案是定义一个平台适配器接口,内核只面对抽象接口,各平台自行实现。这个接口的粒度非常关键,不能细到每个UI事件,也不能粗到只给一个execute方法。最终我们收敛为四个基本方法:

interface PlatformAdapter { startSession(config: SessionConfig): Promise<Session>; sendMessage(sessionId: string, message: UserMessage): AsyncIterable<AgentEvent>; executeTool(toolCall: ToolCall): Promise<ToolResult>; handleUserInterrupt(sessionId: string, feedback: UserFeedback): Promise<void>; }

重点说一下sendMessage返回的是AsyncIterable,因为跨平台场景里流式输出几乎是刚需。IDE侧需要边想边展示思考过程,网页侧需要打字机效果。如果用一次性返回的Promise,两个平台的体验都会被拖成“等待一个完整周期”的糟糕状态。

2.2 调度配置与模型路由:单一配置源驱动多端行为

跨平台调度层另外一个容易踩坑的地方是调度配置。早期我们的做法是每个平台写一份配置,IDE配置里写模型名、CLI配置里也写一遍,改模型参数要同步修改N个文件。后来统一改成单一配置源,平台适配器只负责读取与自身相关的子集。

一个最小调度配置长这样:

execution: default_worker: code_agent_v2 concurrency: 4 max_steps_per_task: 30 timeout_seconds: 300 model_router: - task_type: code_generation model: fast_model fallback: strong_model max_tokens: 4096 - task_type: deep_analysis model: strong_model fallback: null max_tokens: 8192 tools: allowed_by_default: - file_read - git_diff - unit_test_runner require_approval: - file_write - remote_deploy - package_install

这个配置体现了一个很实际的设计理由:不是所有任务都需要最贵的模型。代码格式化、简单问答、信息检索这类任务用快速模型就够,深度架构分析再启用最强模型。路由逻辑放在调度层而不是Agent内部,好处是调整模型策略不用改Agent代码,运营侧改配置就能灰度。模型路由还承担了fallback职责——当主模型因服务波动不可用的时候,自动切换到备用模型并降级为保守执行模式(比如不调用写操作工具)。

2.3 事件总线:为什么同步HTTP调用不适合Agent编排

调度层的通信基础设施我们最初选的是简单HTTP调用,一个Agent执行完直接POST给下一个Agent。跑通Demo没问题,一旦加入并发、重试、追踪之后,HTTP同步调用的缺点就很明显:任务链路断裂时中间状态全部丢失,无法做跨系统的消息回溯,也无法实现优雅的超时和降级。

后来改用事件总线的模式。调度层把任务封装成事件发布出去,执行层的Worker订阅自己感兴趣的任务类型。事件带有一个全局traceId,贯穿任务拆解、Agent执行、工具调用、结果聚合全过程。这个设计带来两个非常大的收益:

  • 可恢复性:Worker崩溃后,事件还在队列里,其他空闲Worker可以直接接手,任务不会被硬中断。
  • 可观测性:以traceId为维度,能还原一次任务从进来到结束的完整事件流,哪个环节耗时高、哪次工具调用失败一目了然。

代价是引入了一个新的基础设施组件,对中小团队来说学习成本不低。但如果目标是做真正的Agent生态而不是单机脚本,这个投入是值得的。

3. 核心编排内核:任务分解、上下文管理与记忆复用

3.1 Planner-Executor-Critic:让任务拆解可审查而不是一个黑盒

编排内核是Agent生态最核心的部分。我们的第一代内核只有一个大循环:让Agent反复执行“思考-调用工具-观察结果”,直到任务结束。这种模式在简单任务上表现不错,问题在于难以控制:Agent可能在一个细枝末节上反复尝试20轮,也可能在语义上误解了用户意图却没有机制纠正。

第二代内核改成了三角色分解:Planner负责把用户目标拆解成子任务清单,Executor逐项执行,Critic负责对中间结果做质量评估并把不合格的结果打回重做。三个角色共用一个底层模型,通过不同的System Prompt约束行为,而不是真的部署三个模型实例。

这种做法带来的好处非常实际:

  • 可审查的任务清单:用户和开发者都能看到Agent打算做什么、顺序是什么,而不再是猜测。
  • 局部重试:某个子任务失败时,Critic可以只要求重做该步骤,不需要从零开始跑整个任务。
  • 上下文分段管理:每个子任务有自己独立的上下文窗口,子任务之间只传递结构化摘要,减少上下文漂移。

举一个真实的例子。用户说“给这个接口写单元测试并修复暴露出的bug”,Planner会拆成四个子任务:1)分析接口签名;2)生成测试用例;3)运行测试;4)修复失败并重新验证。Critic在步骤4发现修复方案改变了接口行为,会自动把步骤1和2标记为需要重验。这个循环逻辑在单体Agent里也可以做到,但绝不会有这么清晰的可观测路径。

3.2 上下文分层的工程实践:短期、工作、长期三种记忆模式

上下文管理是Agent系统里最容易失控的部分。一句“记住之前的结论”在实现上远比听起来复杂。我们实践后稳定下来的方案是三层记忆结构:

  • 短期记忆:当前会话内所有消息和工具结果,放在执行内存里,会话结束即销毁。
  • 工作记忆:当前任务链路上的关键结论、代码片段、决策原因,以结构化文档形式存入任务工作区,任务结束归档。
  • 长期记忆:跨会话的偏好、项目结构、历史决策模式,通过向量检索按需召回。

这里有一个很关键的设计原则:长期记忆不能隐式注入,必须显式检索。我们踩过的坑是试图把长期记忆里的“项目使用微服务架构”这类信息全部塞进每个Agent的Prompt里,结果上下文迅速膨胀,模型开始混淆不同项目的信息。后来改成按需检索:只有当Planner判断当前任务涉及架构决策时,才调用检索工具加载相关长期记忆。这个机制让上下文长度平均下降了约40%,输出准确率反而提升了。

3.3 工具协议:统一输入输出,避免Agent在参数格式上浪费推理

执行层另一个重要组件是工具注册表。每个工具暴露一个JSON Schema描述的接口,Agent根据Schema生成调用参数,工具执行后返回统一格式的结果对象。越早统一越好,我们早期有些工具返回纯文本,有些返回结构化JSON,Agent每次都要通过自然语言理解去解析结果,既慢又容易出错。

统一工具协议其实也便于做权限控制,因为所有工具调用都会经过同一条管道,拦截和审批逻辑只需要写一次。一个规范的工具定义如下:

{ "name": "run_unit_test", "description": "在指定仓库运行单元测试并返回成功/失败统计", "params_schema": { "type": "object", "properties": { "repo_path": { "type": "string" }, "test_filter": { "type": "string", "optional": true } } }, "result_schema": { "type": "object", "properties": { "passed": { "type": "integer" }, "failed": { "type": "integer" }, "duration_ms": { "type": "integer" } } } }

这里值得强调一下:工具的description字段不要写太宽泛,比如“运行测试”就太模糊。更好的写法是包含触发条件,例如“当代码变更涉及已有测试文件时,用于回归验证”。Agent对工具的理解完全依赖这个描述,写清楚描述比多写几个示例更管用。

4. 工程化三件套:可观测性、权限沙箱与灰度发布

4.1 可观测性不是锦上添花,是Agent系统的救命稻草

Agent系统的排障难度和分布式系统很接近,但比分布式系统多一层复杂性:除了“发生了什么”,还要面对“模型为什么这么决策”。所以我们的可观测性平台在传统log、metric、trace之外,特地把模型决策轨迹作为一等公民来记录。

每次Agent推理,我们都会记录以下字段:

  • 触发该次推理的事件上下文摘要
  • 模型的输入Prompt(经脱敏处理)
  • 模型输出的完整内容
  • 选中的工具和实际参数
  • 评分/采纳结果

这些数据直接服务三个场景:线上排障、Prompt迭代、用户行为分析。有一次用户反馈Agent生成了破坏性的重构建议,我们通过决策轨迹回放,发现是某个工具返回的错误信息被模型误解为“需要删除备份分支”。如果没有这套轨迹,这种问题几乎不可能定位。

可观测性的关键指标我们也做了收敛,不追求大而全,重点盯几个:

指标定义理想范围
任务完成率成功数/总任务数> 85%
平均步数任务消耗的推理步数< 25步
工具调用成功率成功调用/总调用> 95%
上下文切换率跨任务上下文切换次数越低越好

这些指标定下来之后,Agent的性能优化就变成了有数据支撑的迭代,而不是靠感觉调Prompt。

4.2 权限沙箱:给Agent装一套“最小权限安全带”

AI编程Agent和普通程序最大的不同在于它有自主行动能力,所以权限控制绝不能被忽略。我们会把所有工具按照影响面分成三类:无损操作(读文件、搜索)、低风险操作(修改本地未提交代码、运行测试)、高风险操作(写远程分支、执行部署、修改依赖锁文件)。策略上规定Agent默认只能执行无损操作和低风险操作,高风险操作必须走人工审批。

有一个容易忽略的点:权限沙箱不只是限制Agent,还要给用户一个清晰易懂的“为什么被拦”提示。不能让用户觉得是系统在无理取闹。所以拦截器返回的错误信息里必须包含策略名称和建议动作,例如“策略remote_deploy_requires_approval:正在尝试部署到staging环境,需要人工批准后继续”。

我们还做过一个安全加固,就是为Agent提供虚拟化工作区,尤其针对那种会做批量文件修改的任务。Agent实际工作在一个隔离目录,所有改动经过diff审查后一次性合入真实仓库。这个设计借鉴了代码审查的思路,成本不高但规避了大量误改风险。

4.3 灰度发布:Prompt和模型也要版本管理

传统软件发版要灰度,Agent系统的Prompt和模型选择同样要灰度,而且往往比代码变更更容易引发事故——因为改一句Prompt可能改变Agent在所有任务上的行为。

我们的策略是给Agent系统引入一套“运行策略版本”的概念。每个版本包含:Planner和Executor的Prompt、模型路由配置、工具白名单、权限阈值。新版本先在内部沙箱环境跑一遍回归测试集,然后按5%->20%->100%的比例灰度,每个阶段都会自动比对任务完成率指标,一旦下滑超过阈值就自动回滚。

这套机制的早期版本很简陋,只是把Prompt文件存进Git仓库,部署时手动切换。后来发现这种方式无法按比例分流,也无法做到多版本同时在线,才改成配置中心管理加流量染色。现在团队里调整Prompt已经成了标准化流程:每天可以安全迭代多次,而不必担心改坏全量用户。

5. 实测复盘与踩坑记录:从上下文漂移到吞吐瓶颈的一次完整排障

5.1 问题现象:任务成功率暴跌、错误结果出现“串味”

系统上线大约三周后,我们收到一个集中爆发的问题:多任务并行场景下,Agent的生成结果会偶尔出现“串味”——明明在处理支付模块的重构,工具调用记录里却出现了用户管理模块的文件读取。任务成功率从正常时的88%掉到了62%。

第一反应是模型出问题了,但查看模型服务端指标,没有异常超时,也没有调用失败。接着怀疑是任务调度把事件发错了队列,翻调度日志也没发现异常。真正定位到问题是在查看决策轨迹数据的时候:有个Agent在处理A任务的中途,读到的上下文里混入了B任务刚产生的文件变更事件。

根因很隐蔽:上下文存储使用了全局共享事件流,多个Worker订阅同一个topic,A任务的上下文管理器把B任务的事件当成自己的增量记忆吸收了。单体Agent时代不会出这个问题,因为只有一个执行体;一旦引入了并行Worker,共享上下文就必须做严格隔离。

5.2 修复方案:任务级上下文快照

修复办法并不复杂,但设计上需要想清楚边界。我们在每个任务启动时生成一个独立的上下文版本,Worker在任务执行期间只读取自己版本的数据,事件流入口增加taskId过滤。

class TaskContext: def __init__(self, task_id): self.task_id = task_id self.version = create_new_version() self.events = EventStore.filter(task_id=task_id) # 只订阅本任务事件 def append(self, event): if event.task_id != self.task_id: raise ContextIsolationError("foreign event rejected") self.events.append(event)

另外在Critic环节增加了一个一致性检查:如果工具调用的目标文件路径与当前任务的仓库路径不匹配,任务会被挂起并提示人工确认。这个启发式规则虽然简单,却挡住了大量“Agent跑偏”的情况。修复上线后,成功率回升到91%左右,比崩溃前还高了3个百分点,原因是我们顺便排掉了以前那些没被发现的串扰型错误数据。

5.3 问题延伸:吞吐瓶颈和审批风暴

并行度调高后,第二个瓶颈很快暴露:当并发任务从10涨到50,任务完成时间反而变长。这次问题出在LLM调用层——所有推理请求共用同一个连接池,没有队列和优先级控制,高峰期低优先级任务把高优先级任务的请求挤爆。

调度层引入了两级削峰:任务排队按优先级分配权重,同一并发级别下限制推理并发数;低优先级任务最多等待60秒,超出自动降级为快速模型。同时治理层的审批也开始堆积,因为突发流量下大量高风险操作等待人工处理。这个问题的解法是减少“人工审批”的面:把一些历史审批通过率达到99%的高风险操作改成“提示后默认放行但记录审计日志”,只保留真正会造成不可逆影响的操作走审批。这个调整我们一开始比较保守,后来观察了两周数据,没有发生一例风险事故,才慢慢放大范围。

5.4 复盘:一套Agent系统最需要的三类排障手段

这次排障链路走了将近一周,事后复盘总结了三个关键手段,缺一不可:

  • 决策轨迹可回放:没有模型决策轨迹,我们只能猜是模型问题,可能要浪费两天时间。
  • 上下文隔离可验证:工具调用和事件记录必须携带任务ID,否则隔离是否正确无法确认。
  • 指标自动对比:灰度阶段一旦出现指标下滑,自动告警才能在最短时间内暴露问题。

这三条后来被写进团队内部的Agent系统上线checklist,任何新模块接入都必须先证明具备这三个能力。

6. 后续演进空间:多Agent协作模式和人机协同的扩展建议

6.1 从“单任务多步”到“多Agent并行协作”

当前这套生态已经能很好地处理“一个复杂任务由一个Agent主理、Critic监督”的模式。但实际业务里已经开始出现一个任务天然涉及多个专业领域的情况,比如一次版本升级需要同时动后端接口、前端页面和数据库迁移脚本。下一步的演进方向很明确:把Planner的角色升级为代理协调者,由它生成多个专业Agent执行子任务,再汇总结果做冲突消解。

这个方向上最大的难点不是通信机制,而是共享产物的冲突消解。三个Agent同时改一份接口定义文件,后写的那份很可能会覆盖先写的。我们的初步方案是给每个Agent分配独立工作区和专属产出路径,协调者统一做merge,merge阶段调用对比工具人工确认。这个方案还不完美,但已经比“一个Agent顺序执行”快了近一倍。

6.2 人机协同:不要让Agent单飞,也不要把人拖进循环

实践中我越来越觉得,好的Agent系统不是追求全自动,而是找到人机协同的最优比例。权限审批就是一种人机协同,代码review是另一种。我们总结出一个“信任分级”策略,可以帮助团队决定什么时候干预用户:

  • L1:Agent自动完成,无需通知用户。
  • L2:Agent完成并通知,供用户确认,不等待结果。
  • L3:Agent部分完成,遇到边界条件,需要用户决策后才能继续。
  • L4:Agent直接不做,转交给人工处理。

把决策权显式分类之后,产品体验变得很清晰。用户不会觉得系统在“自作主张”,也不会被大量低价值审批打扰。

6.3 架构扩展时最该保住的三个设计原则

聊到最后,我想把所有经验浓缩成三条设计原则。这三条在后续扩展时无论多着急都不要破坏:

第一,任何Agent行为都必须可追踪。即使是最简单的工具调用,也要带上任务ID、traceId和执行上下文摘要。这会让初期开发多花一点时间,但救命的场景远超预期。

第二,工具的权限边界必须集中治理。分散在业务代码里的权限判断一定是灾难。所有工具都在注册表里声明自己的敏感级别,治理层统一拦截,这是系统安全最后一道防线。

第三,模型策略和业务逻辑必须分离。Prompt、模型路由、工具白名单都属于可灰度配置,绝不能硬编码在Agent代码里。否则每次模型升级都变成一次全量回归测试。

我们内部现在把Agency Agents这套架构当成一个持续演化的系统在维护,每周都有新的工具接入、新的平台适配器开发。回看最初那个“单Agent一把梭”的版本,最大的变化不是功能变多了,而是系统的每一个行为都有迹可循、有权限约束、有指标衡量。就像把一个只会自己埋头干活的实习生,培养成了一个背后有完整项目管理、质量审查、权限治理体系的团队。这个比喻也是我在跟其他团队交流时最喜欢用的:做Agent生态,本质上不是让Agent变聪明,而是给Agent们配齐一整套靠谱的组织基础设施。如果你也在做类似的事情,希望这篇文章能让你少踩几个我们踩过的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进