告别 Agent 大循环:后端工程师从脚本原型到可维护架构的进阶指南
作者:逆境不可逃
技术永无止境
希望我的内容可以帮助到你!!!!
本文承接《复杂 Agent 如何真正落地:讲透 ReAct、Plan-and-Execute、Workflow、Reflection 与人工接管》。 如果规划与执行解决的是 “复杂任务怎样可靠地跑完”,框架与架构解决的就是 “这些能力怎样被组织、替换、测试和长期维护”。
摘要
很多 Agent 项目的第一版都很相似:准备 Prompt,调用模型,解析工具请求,再循环执行。这个结构能够快速验证想法,却会随着模型、工具、记忆、工作流和多 Agent 协作不断加入,逐渐变成难以测试、难以恢复、也不敢升级的 “大循环”。
真正可维护的 Agent 系统,不应把业务规则绑定在某个框架 API 上,而要先建立稳定的领域模型和运行契约,再通过 Port 与 Adapter 接入模型、工具、数据库、消息队列和编排框架。图负责表达执行路径,状态机负责约束生命周期;多 Agent 只在目标、上下文、权限或生命周期确实需要隔离时拆分;协作通过结构化任务包和产物完成,而不是互相转发整段聊天记录;终止则必须经过统一裁决、持久化和资源回收。
本文以 “技术迁移评估助手” 为贯穿案例,系统讲解 Agent 系统分层、框架核心抽象、图与状态机、多 Agent 拆分、上下文隔离、协作协议、终止裁决与资源回收,并给出目录结构、核心接口、测试策略和实施顺序。
关键词
Agent 架构、分层设计、依赖倒置、Port and Adapter、状态机、图编排、多 Agent、上下文隔离、协作协议、终止裁决、后端工程
一、为什么 Agent Demo 很快会遇到架构问题
一个最小 Agent 循环通常只有几步:
用户请求 → 组装 Prompt → 调用模型 → 执行 Tool Call → 将结果追加到消息 → 继续调用模型,直到模型回答完成问题不在于这个循环错误,而在于它同时承担了太多职责:
- 接收 HTTP 请求并校验参数;
- 维护会话、任务和运行状态;
- 选择模型、Prompt 与工具;
- 决定工作流下一步;
- 执行外部副作用;
- 处理重试、超时、暂停和恢复;
- 记录日志、指标、成本与审计证据;
- 判断成功、失败、降级或人工接管。
当这些逻辑全部写在一个函数里,系统会出现四类典型问题:
| 问题 | 表现 | 根因 |
|---|---|---|
| 框架锁定 | 更换模型或编排框架要重写业务逻辑 | 业务代码直接依赖 SDK 对象 |
| 状态混乱 | 消息、任务进度、生命周期状态混在一个字典里 | 缺少明确状态模型 |
| 测试困难 | 单元测试必须真实调用模型和外部工具 | 缺少可替换的边界接口 |
| 故障扩散 | 一个 Worker 超时导致整个任务失控 | 缺少隔离、裁决和回收机制 |
所以,第七阶段的核心并不是 “再学一个 Agent 框架”,而是掌握一套框架变化后仍然成立的架构方法。
二、先区分四个概念:框架、架构、运行时与业务
这四个概念经常被混在一起。
| 概念 | 主要回答的问题 | 例子 |
|---|---|---|
| 业务 | 系统为什么存在、必须满足什么规则 | 迁移报告必须覆盖依赖、风险和证据 |
| 架构 | 职责怎样划分、依赖朝哪个方向 | 分层、领域模型、Port、Adapter |
| 运行时 | 任务怎样被调度、暂停、恢复和终止 | 状态持久化、重试、租约、Checkpoint |
| 框架 | 用什么现成组件实现上述机制 | 图编排库、模型 SDK、工作流引擎 |
一个可靠的依赖方向应当是:
业务规则定义系统必须做什么 ↓ 架构定义职责与边界 ↓ 运行时实现执行语义 ↓ 框架和基础设施提供具体能力不能反过来让某个框架的类名、状态对象和回调机制决定业务模型。框架可以进入 Adapter,但不应渗透到领域核心。
三、推荐的总体分层:稳定规则向内,易变实现向外
对于中等复杂度以上的 Agent 应用,可以采用下面的混合分层:
┌─────────────────────────────────────────────┐ │ 接入层 Interface │ │ HTTP / CLI / WebSocket / Consumer │ ├─────────────────────────────────────────────┤ │ 应用层 Application │ │ StartRun / ResumeRun / CancelRun / Approve │ ├─────────────────────────────────────────────┤ │ 领域层 Domain │ │ Goal / Run / Artifact / Evidence / Policy │ ├─────────────────────────────────────────────┤ │ 编排层 Orchestration │ │ Graph / Node / Router / Termination Control │ ├─────────────────────────────────────────────┤ │ 端口 Ports │ │ Model / Tool / Store / Queue / Clock │ ├─────────────────────────────────────────────┤ │ 适配器 Adapters │ │ Model SDK / DB / Search / Framework / MQ │ └─────────────────────────────────────────────┘ Observability / Security 横切各层各层的职责要足够明确:
3.1 接入层:翻译协议,不做业务决策
接入层负责鉴权、参数校验、DTO 转换和响应映射。它可以把 HTTP 请求转换为StartRunCommand,但不应判断任务需要几个 Agent,也不应直接调用模型。
3.2 应用层:组织用例
应用层负责一个完整用户动作,例如启动任务、暂停任务、提交审批。它协调领域对象和端口,但不包含模型 SDK 细节。
3.3 领域层:保存最稳定的规则
领域层表达目标、运行、预算、证据、产物和策略等核心概念。例如:
- 已终止的 Run 不允许重新进入执行态;
- 高风险写操作必须存在审批记录;
- 报告结论必须引用可追踪证据;
- 成本不能超过 Goal Contract 中的预算。
3.4 编排层:决定执行顺序,不拥有所有业务规则
编排层负责节点、路由、并行、重试、暂停和恢复。它使用领域规则判断下一步,但不应把所有规则都藏在图的条件边中。
3.5 Port 与 Adapter:隔离变化
Port 是核心层声明的能力契约;Adapter 是外层针对具体技术的实现。模型供应商、数据库和框架变化时,核心用例无需跟着重写。
四、依赖倒置:Agent 架构最重要的一条规则
传统的直接依赖方式通常是:
业务服务 → 某模型 SDK → 某工具 SDK → 某编排框架更稳定的方式是:
业务服务 → ModelPort / ToolPort / RuntimePort ← 具体 Adapter例如,领域与应用层只认识这个接口:
from dataclasses import dataclass from typing import Protocol @dataclass(frozen=True) class ModelRequest: messages: list[dict] tool_specs: list[dict] temperature: float = 0.0 @dataclass(frozen=True) class ModelResult: text: str | None tool_calls: list[dict] input_tokens: int output_tokens: int finish_reason: str class ModelPort(Protocol): def generate(self, request: ModelRequest) -> ModelResult: ...具体 SDK 的请求格式、异常类型、流式事件和 Token 统计,都由 Adapter 转换:
class VendorModelAdapter(ModelPort): def __init__(self, client, model_name: str): self.client = client self.model_name = model_name def generate(self, request: ModelRequest) -> ModelResult: raw = self.client.responses.create( model=self.model_name, input=request.messages, tools=request.tool_specs, temperature=request.temperature, ) return map_vendor_result(raw)这样做的价值不只是 “方便换模型”,更重要的是可以在测试中注入FakeModel,稳定复现工具调用、超时、格式错误和预算耗尽等场景。
五、不要从框架 API 开始学:先掌握核心抽象
不同框架命名不同,但底层通常都围绕以下抽象展开。
| 抽象 | 职责 | 不应混淆为 |
|---|---|---|
| Model | 接收结构化请求并返回生成结果 | Agent |
| Prompt / Instruction | 组织模型输入和行为约束 | 安全策略引擎 |
| Message | 表达一次交互内容 | 运行事件 |
| Tool | 提供一个受控外部能力 | 自主 Agent |
| Agent | 围绕目标进行有限决策的执行单元 | 任意函数或节点 |
| State | 保存执行需要的数据 | 一整段聊天记录 |
| Node | 对状态执行一次明确转换 | 完整业务系统 |
| Edge | 定义节点之间的流转关系 | 领域规则本身 |
| Checkpoint | 保存可恢复执行位置 | 普通日志 |
| Runtime | 调度、恢复、中断和回收执行 | 模型推理 |
| Callback / Event | 提供观测与扩展点 | 业务事实的唯一来源 |
这里有三个特别重要的区分。
5.1 Model 不等于 Agent
Model 只负责推理;Agent 还要拥有目标、工具范围、状态、预算和终止条件。一次模型调用不是一个 Agent。
5.2 Prompt 不等于 Policy
Prompt 可以告诉模型 “不要执行危险操作”,但真正的安全策略必须在模型之外强制执行。权限校验、审批和参数限制不能依赖模型自觉。
5.3 Message 不等于 Event
Message 是模型上下文的一部分;Event 是系统发生过的事实。例如 “用户要求删除数据” 是消息内容,“删除工具调用被策略拒绝” 才是运行事件。两者的保留周期、访问权限和审计价值都不同。
六、三个 State 必须分开,否则恢复一定会混乱
Agent 框架里最容易混淆的是State。至少要区分三种状态:
| 状态 | 回答的问题 | 示例 |
|---|---|---|
| Graph State | 节点之间传递什么数据 | 目标、证据、产物、错误 |
| Lifecycle Status | 整个 Run 处于什么业务状态 | RUNNING、PAUSED、FAILED |
| Execution Position | 运行时执行到哪里 | 当前节点、待执行边、Checkpoint |
推荐的数据关系是:
AgentRun ├── lifecycle_status = RUNNING ├── graph_state = {goal, artifacts, evidence, budget...} └── checkpoint = {node_id, graph_version, sequence...}这样,恢复任务时才能分别回答:
- 这个任务是否允许继续?
- 恢复执行需要哪些业务数据?
- 应该从哪个节点和哪个图版本继续?
如果只序列化一份巨大的框架 State,一旦图结构、序列化格式或框架版本变化,历史任务就可能无法恢复。
七、图与状态机不是一回事
二者都会出现 “节点” 和 “转换”,但解决的问题不同。
7.1 执行图回答:下一步运行哪个计算节点
collect_requirements ↓ discover_repositories ↓ ┌────┴────┐ scan_code scan_docs └────┬────┘ join ↓ assess_risk → verify → deliver它适合表达顺序、条件分支、循环、并行和汇合。
7.2 状态机回答:当前生命周期能否发生某个转换
CREATED → RUNNING → PAUSED → RUNNING ↘ RUNNING → SUCCEEDED / FAILED / CANCELLED / BLOCKED状态机应显式定义:
State:允许出现哪些状态;Event:什么事件触发转换;Guard:满足什么条件才允许转换;Action:转换时执行什么操作;Illegal Transition:非法转换怎样被拒绝和记录。
7.3 二者怎样配合
状态机先判断 Run 是否允许执行,图再决定业务节点怎样推进。节点执行完成后产生事件,事件更新领域状态,运行时保存新的 Checkpoint。
一句话概括:
图管理 “执行路径”,状态机管理 “生命周期合法性”。
八、Node 必须是有契约的状态转换
不要把 Node 写成 “能访问所有服务、随意修改整个 State” 的大函数。一个可靠 Node 至少需要声明:
- 输入字段及前置条件;
- 输出字段和产物类型;
- 允许调用的 Port;
- 超时、重试和幂等策略;
- 可能产生的错误;
- 是否存在外部副作用;
- 可观测事件和验收条件。
可以统一为下面的形式:
@dataclass(frozen=True) class NodeResult: state_patch: dict artifacts: list["Artifact"] events: list["DomainEvent"] route_hint: str | None = None class Node(Protocol): name: str def execute(self, ctx: "NodeContext") -> NodeResult: ...Node 最好返回增量state_patch,再由受控 Reducer 合并,而不是原地修改共享字典。并行节点尤其需要明确合并规则:
| 数据类型 | 推荐合并策略 |
|---|---|
| 证据列表 | 按evidence_id去重追加 |
| 预算消耗 | 求和,但不得超过上限 |
| 风险等级 | 取更高等级 |
| 唯一结论 | 检测冲突后交给裁决器 |
| 生命周期状态 | 只能通过状态机转换 |
九、并行、循环与中断:编排最容易出事故的三个位置
9.1 并行不是把函数放进线程池
并行分支需要解决:
- 分支接收的是共享引用还是不可变快照;
- 一个分支失败是否取消其他分支;
- 汇合需要全部完成,还是达到法定数量即可;
- 重试是否重复写入产物;
- 冲突结果由谁裁决。
推荐使用不可变输入、分支独立输出和显式 Join Policy。
9.2 循环必须有可证明的退出条件
任何 Reflection、ReAct 或重新规划循环都至少要受以下条件限制:
最大迭代次数 最大 Token / 金额预算 最大运行时间 连续无进展阈值 重复动作检测 人工取消或安全阻断“模型认为已经完成” 只能作为信号,不能作为唯一终止条件。
9.3 中断必须可恢复
人工审批、外部事件等待和限流都可能让任务暂停。中断点需要持久化:
- Run ID 与图版本;
- 当前生命周期状态;
- 已提交的产物和证据;
- 待处理节点;
- 预算与租约;
- 恢复所需事件类型。
十、什么时候用图框架,什么时候保留原生代码
框架不是越多越好。可以按复杂度选择:
| 场景 | 推荐方式 |
|---|---|
| 单次模型调用、无工具 | 普通应用服务 |
| 少量固定步骤 | 原生 Workflow / Pipeline |
| 有条件路由、循环、并行、Checkpoint | 图编排框架 |
| 跨小时或跨天、依赖外部事件、强恢复要求 | 持久化工作流引擎 |
| 多主体独立协作 | 图或工作流之上的多 Agent 协议 |
引入框架前至少问四个问题:
- 它替我们实现了什么执行语义?
- 状态和 Checkpoint 能否迁移?
- 异常、重试和取消语义是否明确?
- 核心业务能否脱离框架进行单元测试?
如果第四个问题的答案是否定的,说明框架已经侵入核心边界。
十一、多 Agent 的第一原则:默认先不要拆
多 Agent 会增加通信、上下文同步、冲突裁决、权限配置、故障处理和成本追踪。很多所谓 “Agent” 其实只是:
- 一个工具;
- 一个 Workflow Node;
- 一段不同的 Prompt;
- 一个普通 Worker;
- 一个模型路由策略。
在以下方式不能解决问题时,再考虑拆分独立 Agent:
函数 → Tool → Node → 子图 → 单 Agent 多阶段 → 多 Agent真正值得拆分的信号包括:
| 信号 | 说明 |
|---|---|
| 独立目标 | 子任务可以单独定义完成条件 |
| 独立上下文 | 所需信息与主 Agent 显著不同 |
| 独立权限 | 必须隔离工具、数据或副作用权限 |
| 独立生命周期 | 可以独立暂停、重试、终止 |
| 独立质量标准 | 需要专门验证器或评估集 |
| 独立所有权 | 由不同团队长期维护 |
| 明确并行价值 | 并行收益大于通信和合并成本 |
一个简单判断公式是:
拆分收益 = 专业化 + 隔离性 + 并行性 + 独立演进 拆分成本 = 通信 + 重复上下文 + 裁决 + 观测 + 故障处理只有当收益长期大于成本时,多 Agent 才是架构,而不是演示效果。
十二、常见多 Agent 协作模式及选择
| 模式 | 结构 | 适用场景 | 主要风险 |
|---|---|---|---|
| Supervisor-Worker | 主管分派,Worker 返回结果 | 任务可拆分、需要集中控制 | 主管成为瓶颈 |
| Router-Specialist | 路由到单个专家 | 请求类别边界清楚 | 路由错误 |
| Pipeline | Agent 按阶段传递产物 | 固定生产流程 | 上游错误逐级放大 |
| Fan-out/Fan-in | 多 Agent 并行,统一汇总 | 调研、扫描、候选生成 | 合并冲突和成本上升 |
| Debate/Judge | 多方给出观点,裁判决策 | 高价值、可验证决策 | 成本高且可能伪共识 |
| Blackboard | 通过共享任务板协作 | 异步、开放式问题 | 共享状态污染 |
对技术迁移助手,可以采用受控的 Supervisor-Worker:
Orchestrator ├── CodeScanner Agent:只读代码仓库 ├── DocResearch Agent:只读文档与知识库 ├── RiskAnalyst Agent:读取标准化产物 └── ReportWriter Agent:生成报告,不访问生产系统注意,Orchestrator 负责流程和最终裁决,Worker 只对自己的局部任务负责。
十三、上下文隔离:不要把完整聊天记录广播给所有 Agent
上下文至少包含九类信息:指令、任务、运行状态、交互记录、记忆、知识、能力、安全信息、观测信息。它们不应默认共享。
推荐遵循五条原则:
- Need to Know:只提供完成当前任务所需的信息;
- Least Privilege:只授予必要工具与数据权限;
- Explicit Contract:通过字段定义传递内容,不靠隐含对话;
- Artifact over Transcript:优先传产物与证据,不传整段思考过程;
- Verify at Boundary:所有跨 Agent 输入都要重新校验。
可以把上下文分成三类:
| 类型 | 示例 | 建议 |
|---|---|---|
| 私有上下文 | Worker 的局部推理、临时草稿 | 不外传,任务结束后清理 |
| 共享上下文 | Goal、任务状态、已验收产物 | 结构化、版本化、可审计 |
| 外部上下文 | 用户输入、网页、文档、工具输出 | 默认不可信,标注来源 |
尤其要防止外部文档中的提示注入跨越边界。检索结果只是数据,不能自动升级为系统指令或工具授权。
十四、协作协议:传任务包和结果包,不传一句自然语言
多 Agent 之间如果只传 “请分析一下这个仓库”,接收方无法确定范围、权限、预算和完成标准。应使用结构化协议。
14.1 任务包
{ "task_id": "scan-repo-017", "run_id": "migration-run-001", "objective": "识别 Payment SDK v1 的调用位置", "inputs": [ {"type": "repository", "ref": "payment-order-service@a13f9c"} ], "constraints": { "read_only": true, "max_tool_calls": 30, "deadline": "2026-07-15T18:00:00+08:00" }, "required_outputs": ["usage-inventory.json"], "acceptance_criteria": [ "每条调用记录包含文件、行号和符号名", "结果通过 JSON Schema 校验" ], "trace": {"correlation_id": "corr-001"} }14.2 结果包
{ "task_id": "scan-repo-017", "status": "SUCCEEDED", "artifacts": [ {"artifact_id": "artifact-88", "type": "usage_inventory", "version": 1} ], "evidence": [ {"source": "src/payment/client.py", "locator": "L42-L67", "hash": "..."} ], "claims": [ {"text": "存在 4 处 v1 API 调用", "evidence_ids": ["ev-1", "ev-2"]} ], "metrics": {"tool_calls": 12, "input_tokens": 8010}, "errors": [] }协议还应包含:
schema_version:支持协议演进;idempotency_key:避免重复执行副作用;correlation_id与causation_id:串联调用链;classification:标记数据敏感级别;expires_at:防止过期任务继续运行;signature或可信身份:防止伪造任务。
十五、共享产物应有明确所有权
共享状态最危险的设计是 “任何 Agent 都能修改任何字段”。更可靠的方式是建立产物注册表:
Artifact ├── artifact_id ├── type ├── owner_agent ├── schema_version ├── content_ref ├── content_hash ├── status: DRAFT / VERIFIED / REJECTED ├── evidence_refs └── created_at建议遵循以下规则:
- 一个产物只有一个写入所有者;
- 其他 Agent 通过新版本或审查意见协作;
- 被消费前必须校验 Schema、来源和完整性;
- 关键结论必须绑定 Evidence;
- 产物状态转换由明确规则控制;
- 聊天文本不能直接作为最终业务事实。
这样,即使某个 Worker 被替换,其他组件仍然只依赖稳定的产物契约。
十六、终止不是done = true,而是一套裁决协议
复杂 Agent 系统至少存在三层终止:
| 层次 | 终止对象 | 典型原因 |
|---|---|---|
| Local Agent | 单个 Agent 尝试 | 局部完成、超时、权限不足 |
| Task | 一个子任务 | 验收通过、重试耗尽、被取消 |
| Global Run | 整个业务运行 | 成功、失败、降级、安全阻断 |
Worker 可以报告 “我的任务完成了”,但不应直接宣布全局成功。最终终止权必须由唯一的TerminationController或 Orchestrator 持有。
16.1 结构化终态
CompletionMode: SUCCEEDED PARTIALLY_SUCCEEDED FAILED CANCELLED BLOCKED EXPIRED TerminationReason: ACCEPTANCE_MET USER_CANCELLED POLICY_DENIED BUDGET_EXHAUSTED NO_PROGRESS REQUIRED_TASK_FAILED CONFLICT_UNRESOLVED16.2 终止检查顺序
顺序很重要,因为安全阻断不能被 “结果看起来不错” 覆盖:
def decide_termination(run): if run.policy_violation: return terminate("BLOCKED", "POLICY_DENIED") if run.cancel_requested: return terminate("CANCELLED", "USER_CANCELLED") if run.expired: return terminate("EXPIRED", "DEADLINE_EXCEEDED") if run.budget.exhausted: return terminate("PARTIALLY_SUCCEEDED", "BUDGET_EXHAUSTED") if run.required_task_failed: return terminate("FAILED", "REQUIRED_TASK_FAILED") if run.acceptance_contract.is_satisfied(): return terminate("SUCCEEDED", "ACCEPTANCE_MET") if run.no_progress_count >= run.max_no_progress: return terminate("BLOCKED", "NO_PROGRESS") return continue_running()终止记录应包含决策时间、最终模式、原因、证据、未完成事项、预算消耗和裁决者身份,并保持不可变。
十七、先封锁新工作,再回收旧工作
收到取消请求后立即把状态改成CANCELLED还不够,因为已经运行的 Worker 可能继续写入结果。安全终止一般分五步:
1. Fence:禁止创建新任务和新副作用 2. Signal:向活跃 Worker 发送取消信号 3. Drain:等待已开始操作进入安全点 4. Finalize:保存终态、产物和未完成事项 5. Reclaim:释放租约、锁、临时文件和计算资源为了防止迟到结果污染终态,可以采用:
- Run Epoch:每次恢复增加执行世代,旧世代写入被拒绝;
- Lease:Worker 必须持有未过期租约才能提交结果;
- Tombstone:终止后写入不可变终止标记;
- Idempotency Key:重复回调只能生效一次;
- Reaper:定期扫描并回收僵尸任务和过期资源。
外部副作用如果已经发生,取消操作通常无法 “撤回事实”。此时需要补偿动作,而不是假装事务回滚。例如已创建工单,可以关闭工单并记录补偿结果。
十八、贯穿实践:技术迁移评估助手
现在把前面的设计组合成一个完整系统。用户目标是:
分析多个代码仓库从 Payment SDK v1 升级到 v2 的影响,输出带证据的迁移报告;系统只读,不自动修改代码。
18.1 目录结构
migration_agent/ ├── interface/ │ ├── http/ │ └── dto/ ├── application/ │ ├── commands/ │ └── services/ ├── domain/ │ ├── models/ │ ├── policies/ │ └── events/ ├── orchestration/ │ ├── graph/ │ ├── nodes/ │ ├── reducers/ │ └── termination/ ├── ports/ │ ├── model.py │ ├── repository.py │ ├── artifact_store.py │ └── runtime.py ├── adapters/ │ ├── models/ │ ├── repositories/ │ ├── persistence/ │ └── workflow/ ├── observability/ └── tests/目录只是边界的外在表现,真正重要的是依赖规则:domain不导入adapters,application不导入具体 SDK,编排框架对象不出现在领域模型中。
18.2 核心领域对象
| 对象 | 作用 |
|---|---|
| Goal | 保存目标、范围、交付物与验收标准 |
| AgentRun | 保存生命周期、图版本和预算 |
| Task | 表达可调度工作与依赖关系 |
| Artifact | 保存版本化产物及所有权 |
| Evidence | 记录来源、定位信息和内容摘要 |
| Budget | 限制时间、Token、费用和工具次数 |
| FinalizationRecord | 保存终止裁决和未完成事项 |
18.3 执行流程
StartRun → 校验 Goal Contract → 创建 AgentRun 与预算 → 发现目标仓库 → 并行扫描代码和文档 → 校验并汇合产物 → 分析 API 差异与风险 → 生成迁移建议 → 验证结论和证据覆盖率 → 必要时人工审批 → 终止裁决 → 保存报告并回收资源18.4 为什么这里只拆三个专业 Worker
- 代码扫描与文档研究的数据源、工具和上下文明显不同;
- 风险分析只消费标准化产物,不需要仓库访问权限;
- 报告生成只是确定性较高的节点,不必再包装为独立 Agent;
- 最终裁决保留在 Orchestrator,避免多个 Agent 争夺控制权。
这体现了一个重要原则:多 Agent 架构的价值来自边界,而不是数量。
十九、测试策略:不要只测试 “回答看起来对不对”
Agent 系统应同时测试确定性规则和概率性行为。
19.1 领域单元测试
重点验证:
- 非法生命周期转换被拒绝;
- 预算不能出现负数或超额提交;
- 终态不可逆;
- 高风险工具调用缺少审批时被阻断;
- 没有证据的结论不能进入已验证产物。
19.2 Node 契约测试
使用 Fake Port 验证输入、输出、错误映射、幂等性和状态增量,不需要调用真实模型。
19.3 图与恢复测试
验证条件路由、并行汇合、循环上限、Checkpoint 恢复和图版本兼容。应专门模拟 “节点成功但保存 Checkpoint 失败” 这类边界故障。
19.4 多 Agent 协议测试
对任务包和结果包进行 Schema、版本、权限、过期时间和重复消息测试。还要验证迟到结果不会写入已终止 Run。
19.5 端到端评估
端到端测试不应只比较自然语言。可以使用以下指标:
| 指标 | 含义 |
|---|---|
| Task Success Rate | 是否满足 Goal Contract |
| Evidence Coverage | 关键结论中有证据支持的比例 |
| Unsupported Claim Rate | 无证据结论比例 |
| Recovery Success Rate | 故障后能否从 Checkpoint 恢复 |
| Human Takeover Quality | 接管包是否足以继续工作 |
| Cost per Successful Run | 每次成功任务的平均成本 |
最后加入故障注入:模型超时、工具限流、消息重复、Worker 失联、数据库短暂失败、审批超时和取消竞态。可靠性通常是在这些场景中被证明的。
二十、最常见的架构失败
| 失败方式 | 为什么有问题 | 改进方式 |
|---|---|---|
| 业务层直接使用框架 State | 核心模型被框架绑定 | 定义自己的领域状态和 RuntimePort |
| 所有逻辑都写成 Node | 领域规则散落在流程图 | 将不变量收回领域层 |
| 每种 Prompt 都拆成 Agent | 增加无意义通信成本 | 优先使用角色配置或阶段节点 |
| 所有 Agent 共享完整上下文 | 泄露权限、增加成本、互相污染 | 按任务构造最小上下文包 |
| Agent 之间传自然语言 | 无法校验、版本化和审计 | 使用结构化协议与产物 |
| Worker 可以宣布全局成功 | 终止权冲突 | 设置唯一终止控制器 |
| 只保存聊天记录 | 无法可靠恢复和验收 | 保存 Run、Task、Artifact、Evidence、Checkpoint |
| 只记录成功结果 | 失败无法诊断 | 记录事件、尝试、错误和裁决依据 |
| 取消后仍接受结果 | 终态被迟到写入污染 | Fence、Epoch、Lease 与 Tombstone |
| 一开始就追求通用平台 | 抽象脱离真实需求 | 从单一用例提取稳定接口 |
二十一、推荐的落地顺序
不要一次性搭建完整平台,可以按风险逐步演进。
第一阶段:让单 Agent 具备清晰边界
- 定义 Goal、Run、Artifact、Evidence 和 Budget;
- 把模型、工具和存储提取为 Port;
- 用 Fake Adapter 建立核心测试。
第二阶段:引入显式工作流
- 将大循环拆成有契约的 Node;
- 区分 Graph State、生命周期和执行位置;
- 增加 Checkpoint、重试与幂等控制。
第三阶段:增加可恢复运行时
- 支持暂停、恢复、取消和人工审批;
- 建立事件日志、租约和预算控制;
- 统一终止裁决与资源回收。
第四阶段:有证据地拆分多 Agent
- 先测量上下文冲突、权限隔离或并行收益;
- 为每个 Agent 定义目标、输入、输出和生命周期;
- 使用版本化协作协议和统一产物仓库。
第五阶段:持续评估与演进
- 建立离线评估集和线上运行指标;
- 对模型、Prompt、图和协议分别版本化;
- 定期回放失败任务并优化边界。
二十二、结语
一个 Agent 系统是否成熟,不取决于它使用了多少框架、节点或 Agent,而取决于它能否清楚回答这些问题:
- 业务规则属于哪一层?
- 替换模型或框架会影响哪些代码?
- 每个节点读取什么、产生什么、失败后怎样处理?
- 图状态、生命周期和执行位置是否分离?
- 为什么一定需要多个 Agent?
- Agent 之间共享了什么,谁有权修改?
- 谁拥有最终终止权?
- 取消后怎样阻止迟到写入并回收资源?
- 故障后能否从可信 Checkpoint 恢复?
- 最终结论是否有可追踪证据?
从 Demo 到工程系统,真正的变化不是把循环写得更复杂,而是把隐含行为变成显式契约:用分层确定职责,用依赖倒置隔离技术变化,用图描述执行路径,用状态机守住生命周期,用协议约束协作,用统一裁决保证任务能够安全结束。
当这些边界建立起来,框架才真正成为生产力工具,而不是系统未来无法摆脱的外壳。
