目录
[(二)Demo 架构与生产架构差的不是模型](#(二)Demo 架构与生产架构差的不是模型)
干货分享,感谢您的阅读!
生产级 Agent 的架构图,不应该从「模型、工具、记忆」三个方块开始画,我们建议应该从一次真实请求的生命周期开始推。
这次我们以企业任务执行 Agent 为样本,沿着一句自然语言到一份回执的完整路径,推出接入、会话与上下文、编排、模型访问、工具与集成、数据与状态、治理与观测七层,说明每层的责任、层间契约、故障落点与信任边界,并给出单体、服务化、平台化三种落地形态的决策表与演进路线。
本节,作为读者你将获得什么
- 一张可以直接拿去做架构评审的七层全景图,以及每一层「为什么需要它」的推导过程,而不是一份组件清单。
- 五个写下来的层间契约(运行上下文、上下文包、模型调用、工具调用、提议),它们决定了这张图能不能用来分工和验收。
- 三条横切关注点(身份与租户、预算、可观测)在每一层的落点,以及它们最常见的断点位置。
- 一张故障落点地图:每层的典型故障、外在表现与兜底动作,可以直接转成告警文案与 Runbook 第一步。
- 单体分层、服务化分层、平台化三种形态的适用条件、不适用条件、七维对比与迁移退出成本。
关于数据的说明:本文的业务场景、占比与量级均为示例场景与示例值,用于说明结构关系,不是任何真实系统的实测结果。还有设计的代码入如果写的有问题的,请您在评论区留言提醒,感谢!
一、从一次请求开始,而不是从三个方块开始
(一)一次任务请求的完整生命周期
大多数 Agent 架构图长得很像:一个方块写「大模型」,一个方块写「工具」,一个方块写「记忆」,中间几根箭头。这类图的问题不是画错了,而是它回答不了任何一个工程问题------控制权在哪、状态存在哪、失败了谁兜底、出了事怎么查。
换一个起点:拿一句真实的请求,把它从进入系统到产出回执的全过程摊开。
请求是:「帮我给新来的三位同事开通测试环境权限,下周一之前生效」。

摊开之后是七个阶段:接入、理解、规划、取数、裁决、执行、交付。这条链路上一共发生了 4 次模型调用、9 次外部系统调用(6 次只读、3 次写)、2 次状态机持久化、1 次人工审批、1 次成本归账与 1 条完整轨迹。
模型调用只占其中不到五分之一。架构要覆盖的是全部这十七件事,而不只是那四次模型调用。
更重要的是,每个阶段都有它自己的失败方式:接入阶段可能租户越界、配额耗尽、重复提交;理解阶段可能意图误判、槽位缺失被静默填了默认值;规划阶段可能子任务依赖排错、并发度过高把下游打爆;取数阶段可能某一路超时、数据口径不一致;裁决阶段可能提议越权、循环不收敛、预算触顶;执行阶段可能重复写、部分成功、审批超时;交付阶段可能回执与实际动作不一致、轨迹缺失。
七个阶段,二十一类故障,没有一类的根因是「模型不准」。这就是为什么架构要从生命周期开始推:每个环节都会逼出一个具体问题------谁来做、失败了怎么办、状态存在哪、出了事怎么查------这些问题的答案加起来,才是架构。
(二)Demo 架构与生产架构差的不是模型

一个能演示的 Demo 大概是五个组件:用户输入、提示词拼接、模型调用、工具执行、返回结果。一个进程,一段脚本,一下午能跑通。
一个能上线的系统大概是二十二个组件,其中与模型直接相关的只有三个。
这不是在说 Demo 没有价值。Demo 回答的是一个真正不确定的问题:「这件事,模型到底能不能做?」这个问题必须先回答,而且应该用最便宜的方式回答。问题出在下一步------很多团队把 Demo 当成了架构的第一版,然后在它上面往外长。结果是那五个组件成了系统的骨架,而真正该做骨架的二十二个组件变成了补丁。
那十七个多出来的组件不是凭空加的,每一个都对应一条上线硬要求:
- 多租户 → 鉴权、配额、数据隔离;
- 有副作用 → 幂等、补偿、审批、审计;
- 长任务 → 状态机、持久化、恢复;
- 成本可控 → 预算闸门、归账、降级;
- 可运维 → 轨迹、告警、开关、回归。
去掉任何一条要求,对应的组件就可以不要。这也给出了架构设计的第一个动作:不是画方块图,而是把上线硬要求逐条列出来,再从每条要求推出需要哪些组件。 顺序反过来的后果是,组件多了没人能说清为什么需要它,组件少了要等到线上才发现,而谁也无法判断这版架构是否够用。
(三)参考系统的业务目标与硬约束
本文以及本专栏后续文章统一使用同一个参考系统:企业任务执行 Agent。员工用自然语言提任务,系统跨多个业务系统把事办完。
它要具备的能力:理解任务并判断是否需要澄清;检索企业知识库并提供可核验引用;以只读方式查询业务数据库;调用工单、权限、通知等业务接口;生成结构化报告与文件;对高风险操作发起人工审批;支持长任务、异步执行、断点续跑与取消;支持多租户、权限隔离、审计与成本统计。
它的硬约束有五条,本文后面的每一层都会被其中至少一条约束:
-
所有写操作必须可审计到人,且可回答「依据是什么」;
-
跨租户数据绝对隔离,检索与查询都不例外;
-
单次任务的模型成本有上限,触顶要有明确降级路径;
-
高风险动作必须有人工确认环节;
-
任一能力必须可以在不发版的情况下关闭。
参考技术栈按 Python 3.12、FastAPI、Pydantic、PostgreSQL、Redis、消息队列、OpenTelemetry、Docker 来谈。但下面的架构讨论与具体技术栈无关,换成 Java 或 Go 的同类组件,结论不变。
二、七层全景架构
(一)七层是怎么划出来的

划层的依据只有一条:把「变化原因相同」的东西放在一起,把「变化原因不同」的东西分开。
接入层随对外协议变化;会话与上下文层随交互形态变化;编排层随业务流程变化;模型访问层随供应商与模型版本变化;工具与集成层随下游系统变化;数据与状态层随数据模型与合规要求变化;治理与观测层随运维与合规要求变化。
七个变化源,七层。这个划法的好处是:当某一件事发生变化时,能立刻知道该改哪一层,以及需要通知谁。
需要强调的是最后一层的位置。治理与观测层被画在最下面只是版面原因,它实际上包在外面------它的每一项能力都要穿过全部六层才有意义。把它画成「最底下一层」,是很多架构图失真的原因。
还有一个观察值得先说出来:七层里只有「模型访问层」与模型有关,其余六层是任何一个严肃后端系统都需要的东西。这既是好消息也是坏消息。好消息是大部分工作在你熟悉的领域;坏消息是不能因为「这是个 AI 项目」就跳过这些工作。
(二)控制平面与数据平面
在展开每一层之前,先立一条贯穿全图的原则。

控制平面决定做什么:路由策略、计划与依赖、预算与配额、权限与策略、模型与提示词版本、开关与灰度。
数据平面实际去做:模型调用、工具调用、检索执行、状态读写、副作用提交、结果与轨迹上报。
分开之后能做到四件事:灰度只需改控制平面的分流比例,不动数据平面代码;回滚只需把版本号改回去,历史任务仍按当时版本可复现;审计有明确的规则快照而不是靠回忆;权限判定集中在一处,数据平面只执行已被批准的动作。
不分开的典型形态是:预算写在提示词里,权限判定散落在各个工具实现里,模型版本硬编码在调用处。后果是三件事同时做不了------无法在不发版的情况下调整策略,无法回答「这次任务用的是哪个版本」,无法在一个地方关掉某个能力。
落地时不需要一上来就拆成两个服务。最小要求是三条:
-
每次任务启动时生成一份「运行上下文」,把版本、预算、权限、开关固化成快照;
-
数据平面的所有组件只读这份快照,不再自行读配置;
-
快照随任务一起落库,可按任务号取回。
三、逐层展开
(一)第一层:接入层

接入层不产生业务价值,但它决定了系统能不能被安全地开放出去。它有四项责任。
1、身份与租户
解析调用方身份(可能是人,也可能是另一个系统),确定租户与数据边界,然后把身份放进运行上下文。关键纪律是:后续所有层不再重新解析身份,也不再从请求体里读取任何身份相关字段。 身份一旦在这里确定,它就是可信的;在别处重新解析,等于开了第二扇门。
2、配额与限流
按租户、用户、任务类型三级配额,控制并发任务数与排队深度上限。超限时要明确拒绝并给出原因,而不是排队------排队只是把问题推给下游,并且让用户失去判断依据。
3、幂等与去重
客户端传入请求幂等键,相同键在有效期内返回同一个任务号。这一条防的是两类很常见的重复:用户重复点击,以及网关层的自动重试。没有它,「网关超时重试一次」的直接后果就是同一份权限被开通两遍。
4、同步异步分派与对外契约
按预估耗时决定返回形态:短任务同步返回,长任务返回任务号。流式通道与取消通道也在这一层统一定义。
对外契约有四个,每一个都必须在这一层定义清楚:
- 提交任务 :返回
task_id, status, estimated_seconds, trace_id。任务号是后续一切查询、取消与审计的锚点。 - 查询状态 :返回
status, progress, partial_result, cost_so_far。长任务必须能中途看到进展,否则用户只能干等。 - 取消任务 :返回
accepted或already_done。取消语义必须有明确回答,「无响应」是最差的结果。 - 流式输出 :事件类型至少包含
step、delta、approval、done。审批与进度也是事件,不能只流式输出文本。
最常见的两个遗漏是:没有取消语义(用户关掉页面,后台循环还在烧钱且没人知道),以及没有幂等键。
接入层是系统的第一道信任边界:这一层之外的一切输入都是不可信的。
(二)第二层:会话与上下文层

这一层做的是一件事:把「模型这次能看到什么」变成一个有边界、可解释、可复现的决定。
1、上下文装配是一次预算分配,不是一次字符串拼接
装配流水线有五步:候选收集(会话历史、任务状态、检索结果、工具说明)、相关性筛选、预算切分、压缩改写、装配与封存。
为什么不能「把能塞的都塞进去」?五个理由,每一个都足以单独成立:
- 成本:上下文长度直接决定单次调用成本,而循环里每一圈都要重付一次;
- 注意力:无关内容会挤占模型对关键约束的关注,表现为「明明写了它却没照做」;
- 时延:长上下文的首 Token 时间明显变长;
- 泄露:把不该让当前租户看到的检索结果塞进上下文,等于绕过了权限系统;
- 不可复现:每次装配结果都不同的话,同一条请求就无法复算。
所以关键动作是「预算切分」这一步:先按角色分配 Token 配额(系统约束多少、任务描述多少、历史多少、检索多少、工具说明多少),再在各自配额内做筛选,而不是先到先得。
2、三类会话状态必须分开存
- 会话状态:多轮对话的消息与指代,生命周期跟随会话,可过期清理;
- 任务状态:这次任务做到哪一步、产出了什么、失败过几次,生命周期跟随任务,必须持久化;
- 用户状态:长期偏好与授权记录,生命周期跟随用户,需要可查询可删除。
三者混在一个 session 对象里,是多轮串线与跨租户泄露的主要来源。它们的存储介质、过期策略、权限模型、删除要求全都不同,混在一起意味着必须按最严格的那一套来管所有数据,或者按最宽松的那一套------前者浪费,后者出事。
3、上下文包应该是一个结构化对象
它不是一段字符串,而是一个带结构的对象:系统约束(不可被后续内容覆盖)、任务描述与已确认槽位、已执行步骤的结构化摘要、检索片段(带来源 ID 与权限标记)、工具说明(只含本次允许调用的)、预算余量提示。
结构化的好处是:每一部分都能单独计量、单独裁剪、单独在轨迹里被审查。当线上出现「模型忽略了约束」时,第一件事是去看那次运行的上下文包里,系统约束段是不是被挤掉了。
(三)第三层:编排层

编排层是整个系统里唯一决定「下一步做什么」的地方,也是唯一应该有这种权力的地方。
1、内部结构:路由、计划、状态机、舱室
意图路由按意图与租户策略选择处理通道,四条通道各有不同的 SLO、预算与降级策略:只读问询(检索加生成,无写操作,最短路径,不进状态机)、标准事务(模板展开,步骤固定,进状态机,带幂等与审批)、组合任务(多子任务并行加聚合,需要依赖管理)、长尾处置(进舱室,全程留痕,产出的提议由状态机再走一遍标准事务通道)。
计划与依赖 把任务展开为步骤:模板展开、依赖图与并发度、步骤级超时与重试。关键在于计划本身要被校验------无环、无越权、每个步骤都是已注册的动作。模型生成的计划里包含一个不存在的步骤,如果不在这里拦住,就会执行到一半才报错,而此时前面的副作用已经发生了。
任务状态机持有显式状态与转移表:待澄清、待执行、待审批、执行中、已完成、已取消。每次转移落库,重启后可恢复。
受限自主舱室只在长尾通道进入。它的出口只有结构化提议,执行交回状态机;步数、Token、墙钟三个硬上限由编排层持有。
2、编排层的四条纪律
控制流只能在这一层。 任何一个工具实现里出现「如果......就再调一次别的工具」,都是控制流泄漏。这类泄漏的危害是隐性的:它让编排层的计划不再等于实际执行的步骤,于是追踪、预算、审计三件事同时失真。
状态只能通过状态机改变。 直接改库绕过转移表,恢复逻辑立刻失效,因为恢复依赖「当前状态一定是转移表里的合法状态」这个前提。
预算由本层持有并向下传递,模型访问层与工具层只做计量与上报,不做判定。
舱室是可选组件。 关掉它,系统应当退化为纯确定性编排并继续工作。如果关掉舱室系统就跑不起来,说明确定性通道的覆盖不够,这是一个需要立刻处理的信号。
3、最容易出问题的四个地方
计划没有校验;状态机缺少超时转移(等审批的任务永远停在「待审批」,没人发现);并发度不受控(一个任务展开成三十个子任务同时打下游,把别人的服务打挂);舱室与状态机耦合(舱室直接改状态,于是「关掉舱室」变成一件危险的事)。
(四)第四层:模型访问层

这一层的判断标准只有一句:上层代码里不应该出现任何一个厂商专有字段。
1、这一层做五件事
能力抽象(把对话、工具调用、结构化输出、嵌入抽象成稳定接口)、模型路由(按任务难度、风险、预算选择模型,策略可热更新)、输出契约(JSON Schema 校验,校验失败时修复或重试,不放行)、重试与降级、计量与归账(按任务号记录输入输出 Token、缓存命中与耗时)。
2、抽象到什么程度算合适
过薄的抽象只是包一层 HTTP 调用,厂商字段照样透传给上层,等于没做。过厚的抽象试图抹平所有能力差异,结果是最强模型的能力被抽象成最弱模型的公约数,白白损失效果。
合适的做法是分两类:通用能力走统一接口;专有能力显式声明,调用方按能力探测决定用不用,而不是假装它不存在。
判断标准很简单:换一个供应商,需要改几个文件?答案应该是一到两个。
3、降级策略的四个层次
第一层是同模型重试:限流或超时,指数退避重试,通常能解决多数瞬时问题。
第二层是同级替换:切到能力相当的另一个模型或另一个区域,结果质量基本不变,但必须在轨迹里标记,否则评测结论会被污染------你以为在评测 A 模型,实际上有 8% 的样本跑在 B 模型上。
第三层是能力降级:切到更小的模型,同时收窄任务范围(例如只做分类不做生成),并向用户明确说明结果的局限。
第四层是明确失败。这一层最常被忽略,但它很重要:把失败伪装成低质量结果,代价通常比直接失败更高,因为用户会基于错误结果做决定。
(五)第五层:工具与集成层

工具层是系统里所有副作用的唯一出口,也是权限收口的地方。
1、一次工具调用要穿过六道关卡
名称解析(工具是否在本次白名单内)、参数校验(Schema 校验与范围检查)、权限判定(租户、用户、资源三元组)、幂等处理(幂等键生成与重复检测)、执行与熔断(超时、重试、熔断、降级)、结果规范化(统一错误类型与证据 ID)。
六道关卡里,第三道和第四道最容易被省掉,因为它们在 Demo 阶段看不出作用。
2、四类工具,四套治理要求
只读查询(查员工档案、查权限模型、查审批进度):无副作用,但需要按租户过滤的数据视图、查询预算、结果脱敏。
检索(知识库语义检索、条款精确匹配):无副作用,但权限过滤必须前置到检索请求里,返回结果必须带可核验来源 ID。
写操作(建单、改单、开通权限、发通知):有副作用且难撤销,需要幂等键、风险分级、人工审批、补偿动作登记。
执行环境(脚本执行、文件生成、浏览器访问):有副作用且半可控,需要沙箱隔离、网络白名单、CPU 内存时间限额。
3、工具描述本身就是接口文档
模型看到的工具名称、参数说明与错误类型,决定了它调用成功的概率。把内部字段名(如 bizType、flag2)直接暴露给模型,等于让它猜。工具描述应当按「给一个没读过代码的新同事看」的标准来写。
工具层是第二道信任边界:模型的输出在这里从「文本」变成「动作」。所有在编排层做过的校验,在这里都要再做一次------因为工具层无法假设调用它的一定是自己人。
(六)第六层:数据与状态层

五类数据,五套生命周期、五套权限策略。混在一起会同时出三种问题。
1、五类数据
任务状态(状态、步骤、产物、错误、版本快照):跟随任务,归档后长期保留,需要强一致存储。丢失的后果是长任务无法恢复。
会话与短期记忆(消息历史、指代、当前槽位):跟随会话,可过期清理,内存缓存加落盘。最大风险是串线导致跨用户信息泄露。
长期记忆(用户偏好、已授权事项):跟随用户,必须可查询可删除。最大风险是写入无门槛,导致错误被固化------一条错误的「该用户偏好自动审批」会一直影响后续所有任务。
知识与索引(文档、条款、向量与倒排):跟随知识版本。最大风险是权限未前置导致越权检索。
轨迹与审计(调用链、决策、成本、审批记录):保留期由合规要求决定,通常按年。缺失的后果是事故无法复盘。
2、三条容易踩的线
把任务状态放进缓存------缓存一重启,所有在途长任务全部失联。
把检索索引按全量建、查询时再过滤权限------只要有一处忘记过滤就是越权,正确做法是把过滤前置到检索请求里,让索引层本身不可能返回越权结果。
把轨迹和业务日志混在一起------两者的保留期与检索方式完全不同,混在一起的结果通常是两边都不好用。
3、状态与上下文不是一件事
这是一个很常见的混淆。任务状态是系统的事实 :做到第几步、产出了什么、谁批过。它必须完整、可恢复。模型上下文是本次调用的输入:它是任务状态的一个裁剪视图,可以有损、可以压缩、可以省略。
把上下文当状态存,会丢信息(恢复时发现关键中间结果被摘要掉了);把状态当上下文传,会爆预算(把三十步的完整记录全塞进提示词)。
4、多租户隔离的三个层次
逻辑隔离(同库同表加租户列,靠代码保证过滤)成本最低、风险最高,必须配合强制的数据访问层,让「不带租户的查询」在编译期或运行时直接失败。模式隔离(同库不同 schema,连接时绑定租户)能显著降低误查概率。物理隔离(独立实例)适用于有数据驻留要求的大客户。
选哪一层取决于合规要求,不取决于工作量。
(七)第七层:治理与观测层

这一层有六项能力:全链路追踪、指标与 SLO、成本归账、策略与护栏、评测与回归、开关与灰度。
为什么说它包在外面?因为这六项每一项都要穿过全部六层才有意义:追踪要串起接入到交付;成本要归到每一次模型与工具调用;策略要在编排层判定、在工具层强制;开关要能作用到任意一层。
上线前不需要把六项做全,但下面三项缺一不可:
-
追踪 ID 贯通,能按任务号取回完整轨迹;
-
成本能按任务归账,能回答「这一单花了多少」;
-
有一个可以在不发版的情况下关掉高风险能力的开关。
四、横切关注点与信任边界
(一)三条横切关注点及其断点

身份与租户、预算与配额、可观测------这三条不属于任何一层,它们穿透所有层。任何一层漏掉一条,整条链路上的保证就断了。
身份断在哪就在哪泄露。 最常见的断点是检索:编排层已经拿到了正确的租户,但检索请求没有把它带下去,于是模型在上下文里看到了别的租户的文档。这类问题不会报错,只会在某天被用户发现。
预算断在哪就在哪超支。 典型断点是工具层:某个工具内部又调了一次模型(例如做结果摘要),这次调用不在编排层的预算账本里。任务级预算于是永远算不准,而且这类隐藏调用最难被发现,因为它在架构图上根本不存在。
观测断在哪就在哪查不到。 典型断点是异步边界:任务被投递到队列之后,追踪 ID 没有随消息一起传递,于是执行侧的日志与提交侧对不上。一次事故的一半时间会花在把这两段日志手工拼起来。
三条横切关注点的共同落地方式是同一个:把它们放进运行上下文,并让每一次跨层调用都必须携带运行上下文。 在类型系统里把它做成必填参数,比写在规范文档里有效得多。
(二)信任边界与威胁落点

把系统里的数据分成三个区:
不可信区:用户原始输入、外部网页与文档、第三方 API 返回、知识库中的用户投稿。
半可信区:模型输出的文本、模型生成的提议、检索片段、历史会话内容。
可信区:系统配置与策略、工具白名单、权限判定结果、状态机转移表。
三个区之间有两道闸。不可信到半可信这一道靠「当数据不当指令」:所有外部内容在进入上下文时统一包裹为数据段,并显式标注来源,任何出现在数据段里的祈使句都不改变系统行为。半可信到可信这一道靠「重新校验」:模型产出的每一个字段,在变成动作之前都要按可信区的规则重新验证一遍。
威胁落点按层来看:接入层是伪造身份与越权访问;上下文装配是间接注入(检索到的文档里写着「忽略之前的指令」);编排层是诱导跳过审批、生成越权计划;工具层是参数注入与重复执行;数据层是跨租户读取与敏感字段外泄。
每一条的缓解手段都落在具体的层上,而不是靠一句「加强提示词防护」。
(三)一次高风险任务的跨层时序

把前面所有讨论放进一次真实执行里:给三位同事开通测试环境权限,含一次人工审批与一次部分失败重试。
这张图想说明三件事:
控制权全程在编排层。 模型层与工具层从不互相调用------模型不会直接触发工具,它只是产出一个「打算调用某工具」的意图,由编排层决定是否真的调用。这个区别在架构图上只是一根箭头的方向,在生产里是能不能做权限管控的分水岭。
每一次跨层调用都携带同一个运行上下文快照,包括异步投递到审批队列的那一次。审批通过之后回来的消息,仍然带着最初那份快照,所以「按当时的规则执行」是可以保证的。
失败发生在工具层,但决定「接下来怎么办」的仍然是编排层。 三个写操作里两个成功一个返回 409,工具层只负责把这个事实规范化上报,补偿与重试的决策在编排层做。如果让工具层自己决定重试,就会出现「工具层重试三次全失败,编排层还以为一次都没试过」这类问题。
五、契约、故障与可运维性
(一)五个关键的层间契约

架构图上的每一根连线,背后都应该是一个写下来的契约。五个最关键的是:
运行上下文 :trace_id, task_id, tenant, actor, model_version, prompt_version, budget(steps, tokens, seconds), allowed_tools[], policy_version。由编排层在任务开始时生成并封存,中途不可修改。
上下文包 :系统约束(不可覆盖)、任务与已确认槽位、已执行步骤、检索片段(带 chunk_id, source, acl)、工具说明(仅本次允许的)、预算余量提示。
模型调用请求 :能力类型、消息或 Schema 或工具规格、路由提示(难度与风险);返回内容、工具调用意图、用量(输入、输出、缓存命中)与实际 model_id。
工具调用请求 :工具名(须在白名单内)、参数(按 Schema 校验)、幂等键、actor、tenant;返回 ok 或 error(type, retryable),以及供引用与审计的 evidence_id。
提议:动作(须在动作枚举内)、参数、理由、置信度、证据列表(缺失则整条作废)。由舱室产出,由编排层校验,执行仍走标准事务通道。
用代码表达,运行上下文大致是这样一个不可变对象:
python
from dataclasses import dataclass, field
from typing import Mapping
@dataclass(frozen=True, slots=True)
class Budget:
max_steps: int
max_tokens: int
max_seconds: float
@dataclass(frozen=True, slots=True)
class RunContext:
"""任务开始时生成并封存;数据平面的每个组件只读它,不再自行读配置。"""
trace_id: str
task_id: str
tenant: str
actor: str # 发起人,用于权限判定与审计
model_version: str # 本次运行绑定的模型版本
prompt_version: str # 本次运行绑定的提示词版本
policy_version: str # 本次运行绑定的策略版本
budget: Budget
allowed_tools: frozenset[str] # 本次允许调用的工具白名单
flags: Mapping[str, bool] = field(default_factory=dict) # 开关快照
def with_scope(self, tools: frozenset[str]) -> "RunContext":
"""进入受限舱室时收窄工具集,只能变小不能变大。"""
if not tools <= self.allowed_tools:
raise ValueError("舱室工具集必须是外层工具集的子集")
return RunContext(**{**self.__dict__, "allowed_tools": tools})
这段代码的要点有三个:它是 frozen 的,所以中途没人能改;with_scope 只允许收窄不允许放宽,这让「进入舱室后权限只会更小」成为类型层面的保证;所有版本号都在里面,所以「这次任务用的是哪一版」永远有答案。
契约写下来之后,四件事成为可能:每一根连线可以单独测试;每一次跨层调用可以被记录成结构化事件而不是自由文本日志;版本演进有依据;回放成为可能。
契约演进有三条规则:只增不改 (新增字段必须有默认值);删除走两步 (先标记废弃并统计使用量,归零后再删);语义变更等同于新字段(把某个字段的取值含义改掉,比删掉它更危险,因为它不会报错,只会算错)。
这三条与普通的接口演进纪律完全一致,没有任何特殊之处。
(二)故障落点地图

这张表可以直接拿来用:把「外在表现」这一列当成告警的文案来源,把「兜底动作」这一列当成 Runbook 的第一步。如果某一行的兜底动作目前还不存在,那这一行就是下一个迭代要补的东西。
表里唯一一个跨层的问题是部分成功:它发生在工具层,但只有编排层知道该补哪一部分。所以补偿逻辑必须写在编排层,而不是写在工具实现里。这一点值得单独强调,因为「在工具里加个重试」是最自然的第一反应,也是最容易埋下不一致的做法。
六、三种落地形态与演进
(一)层是逻辑划分,服务是物理划分

同一份七层责任,有三种落地方式。
单体进程内分层:七层全部在一个服务里,以模块与包边界划分。适用于单团队、单租户或少量租户、日请求量十万级以内、业务边界仍在探索。优点是部署简单、调试直接、跨层调用零网络开销;代价是扩容只能整体扩、故障隔离弱、多团队协作时冲突多。
服务化分层:接入、编排、工具、治理拆成独立服务,模型访问做成网关。适用于多团队协作、多租户、工具数量上百、不同层的扩容需求差异大。优点是按层独立扩容与发布、故障隔离、权限收口清晰;代价是跨层调用要处理网络失败、分布式追踪成为必需、本地开发环境变重。
平台化:编排、模型网关、工具注册、治理做成共享平台,业务方只提供领域工具与配置。适用于公司内多个业务线都要做、需要统一治理与成本归账、有专职平台团队。优点是治理一次建设全员受益、新业务上线周期最短;代价是平台本身需要产品化、业务方灵活性受限、平台故障是全局故障。
一条常被忽略的事实是:三种形态的七层责任完全相同,区别只在于边界是模块边界、网络边界还是组织边界。这意味着从单体走向服务化并不需要重新设计架构------前提是单体阶段就按七层划清了模块,且层间契约是显式的。
(二)决策表与推荐流程

第一步先看四个硬条件,任一命中直接定档:
- 有数据驻留或物理隔离要求的租户 → 至少服务化,且数据层可独立部署;
- 多个业务线复用同一套治理 → 平台化;
- 团队人数少于六人且业务边界未定 → 单体分层,不要提前拆;
- 工具数量超过五十且由多方提供 → 至少服务化,工具层必须独立。
不满足任何硬条件时,默认单体分层。然后按七个维度(初始交付速度、独立扩容能力、故障隔离、治理一致性、多团队协作、运维复杂度、退出成本)对照决策表。
推荐决策流程是四步:先按硬条件定档;单体阶段就按七层划模块、把五个层间契约写成代码里的类型;当某一层出现独立扩容需求或独立发布需求时,把那一层拆出去;只有当第三个业务线提出同样需求时,才考虑平台化。
最贵的两个错误是过早服务化 (五个人维护七个服务,绝大部分精力花在联调与发布上)和过晚拆分(单体里层与层之间互相直接访问对方的数据结构,到了必须拆的时候发现契约根本不存在,只能重写)。两个错误的共同解药都是:单体阶段就把契约当真。
(三)演进路线与退出成本

四个阶段,每个阶段都能独立交付业务价值:
第一阶段·能跑通:单体,七层划模块,只做只读通道,状态在关系库,有追踪 ID 与成本归账。退出判据:只读通道端到端成功率达标,追踪与成本可按任务号查到。退出成本几乎为零。
第二阶段·能写:加入状态机与幂等、审批与补偿,工具注册表成形,契约写成类型。退出判据:写操作全部经过幂等与审批,重启后在途任务可恢复。退出成本低。
第三阶段·能扩:拆出工具服务与模型网关,接入分布式追踪,按层独立扩容,灰度与开关平台化。退出判据:任一层可以独立发布且不影响其他层。退出成本中等。
第四阶段·能共享:编排与治理产品化,多业务线接入,统一评测与门禁、统一成本与配额。退出判据:第二个业务线在不改平台代码的前提下完成接入。退出成本高。
没达到判据就进下一阶段,是返工的最常见来源。尤其是第二阶段------很多团队在幂等还没做好的时候就去拆服务,结果是把一个数据一致性问题变成了一个分布式数据一致性问题。
迁移成本的真实构成中,代码重构通常不是最贵的部分。最贵的是三样:契约变更引发的多方联调(参与方越多,成本越接近平方增长);历史数据迁移(状态机的状态定义一旦变更,在途任务要么迁移要么跑完,两条路都要写一次性代码);评测基线重建(架构变了,原有评测结论未必还成立,需要重新跑一遍回归并解释差异)。把这三项算进排期,迁移决策才是诚实的。
七、架构评审检查清单

分层与职责(四项)
- 七层责任已明确到人,没有「暂时先放这里」的组件;
- 控制流只出现在编排层,工具实现里没有编排逻辑;
- 模型访问层之上没有任何厂商专有字段;
- 所有副作用只经过工具层这一个出口。
契约与状态(四项)
- 五个层间契约已写成类型或 Schema,而不只是文档;
- 运行上下文在任务开始时封存且不可变;
- 状态机转移表可打印、可评审;
- 五类数据分开存储,各有生命周期与权限策略。
横切关注点(四项)
- 身份贯通到检索与数据访问,无断点;
- 预算贯通到每一次模型与工具调用,包括工具内部的模型调用;
- 追踪 ID 跨越异步边界仍然连续;
- 成本能按租户与任务类型归账。
安全与可靠(四项)
- 三个信任区与两道闸已明确;
- 外部内容统一作为数据段包裹,不作为指令;
- 每一层的典型故障都有兜底动作与告警;
- 部分成功的补偿逻辑写在编排层。
演进与退出(四项)
- 当前处于哪个演进阶段有明确结论;
- 进入下一阶段的退出判据已写下来;
- 迁移成本按三项构成分别估算过;
- 有一键关闭高风险能力的开关且演练过。
这二十项里,只有三项与模型有关。生产级 Agent 架构的主体,是把已经成熟的后端工程纪律,正确地施加到一个输出带有不确定性的组件周围。
八、结论与延伸
(一)本文结论
第一,架构不应该从「模型、工具、记忆」三个方块开始画,而应该从一次真实请求的生命周期开始推。生命周期上的每个环节都会逼出一个具体问题,这些问题的答案加起来才是架构。
第二,七层的划分依据是「变化原因」而不是「调用顺序」。接入、会话与上下文、编排、模型访问、工具与集成、数据与状态、治理与观测,对应七个不同的变化源。
第三,控制平面与数据平面必须分开,最小实现是一个在任务开始时封存、中途不可变、随任务落库的运行上下文。
第四,三条横切关注点(身份、预算、可观测)穿透所有层,它们的断点分别导致泄露、超支与查不到。把它们做成运行上下文里的必填字段,比写在规范文档里有效得多。
第五,五个层间契约决定了这张架构图能不能用来分工和验收。没有契约的架构图只是示意图。
第六,三种落地形态的七层责任完全相同,区别只在边界类型。单体阶段把契约当真,是后续所有演进的前提。
第七,二十项架构评审项里只有三项与模型有关------这既说明大部分工作在熟悉的领域,也说明不能因为「这是个 AI 项目」就跳过这些工作。
(二)与专栏其他文章的衔接
《Agent 工作流实战》专栏导读地图与阅读路径说明-CSDN博客
- 「这个需求到底要不要做成 Agent」的判定方法,在《先别急着做 Agent:Chatbot、Workflow 与 Agent 的边界》给出;
- 本文编排层里那个「受限自主舱室」的内部实现,在《不依赖框架实现最小 Agent:模型、工具、观察与终止》用最少代码拆开;
- 本文提到的每一层「可上线」标准如何量化成验收条件,在《「能跑」不等于「可上线」:生产级 Agent 的验收标准》展开;
- 本文故障落点地图里的每一类缺口,在《Agent Demo 上线为何频繁翻车:12 类隐性工程缺口》逐条给出成因与修法;
- 七层责任由自研还是由现成框架承担,在《Agent 框架怎么选:自研、编排框架、工作流引擎与托管平台》给出评估矩阵。
参考资料
架构方法与分层
- Domain-Driven Design Reference(Eric Evans) ------ 限界上下文与分层的经典表述,本文「按变化原因划层」与之同源。
- Fundamentals of Software Architecture 配套资源 ------ 架构特性、权衡分析与决策记录的方法。
- Architecture Decision Records ------ 把架构决策与其理由固化下来的轻量格式,配合本文的层间契约使用。
- C4 Model ------ 分层次绘制架构图的记法,适合把本文七层展开成不同抽象级别的图。
Agent 与 LLM 应用架构
- Building effective agents(Anthropic 工程博客) ------ 编排模式与「何时需要自主」的讨论。
- A practical guide to building agents(OpenAI) ------ 护栏、工具与编排的落地建议。
- Model Context Protocol 规范 ------ 工具与上下文接入的开放协议,对应本文工具层的注册与发现。
状态、工作流与一致性
- Temporal 文档:Workflows ------ 持久化执行与确定性重放。
- AWS Step Functions 开发者指南 ------ 状态机式编排的具体形态与限制。
- Saga Pattern(microservices.io) ------ 跨系统副作用的补偿模型,对应本文的部分成功处理。
- Idempotency-Key HTTP 头草案(IETF) ------ 幂等键在 HTTP 层的标准化尝试。
可观测性与成本
- OpenTelemetry 文档 ------ 追踪、指标与日志的统一模型。
- OpenTelemetry:Generative AI 语义约定 ------ 模型调用与工具调用的标准化字段。
- Site Reliability Engineering(Google,在线全文) ------ SLO、错误预算与故障响应。
安全与多租户
- OWASP Top 10 for LLM Applications ------ 提示注入与过度代理权限的标准分类。
- OWASP API Security Top 10 ------ 接入层与工具层的通用风险清单。
- PostgreSQL 行级安全策略 ------ 在存储层强制租户隔离的一种做法。
- NIST AI Risk Management Framework ------ 风险治理框架。