1. 为什么一锅糊的Agent迟早要翻车
1.1 一个让我印象深刻的评审现场
AI Agent工程化做得越久,我越觉得“分层交付”四个字不是锦上添花,而是保命的底线。2026年行业里普遍有种判断正在形成:工业智能体要从概念演示走向工程化落地,能不能把Agent拆成可独立交付的层次,几乎决定了项目上线后的稳定程度。我上个月帮一家做工业智能化的企业做技术方案评审,他们的Agent已经能跑通大部分场景,但主流程就是一层层Prompt嵌套,多种决策逻辑叠加,线上并发一冲就出问题。Demo环境永远稳定,因为现场只有那么几个固定case;一旦上了生产、多用户并发,问题就像蚂蚁搬家一样涌出来:上下文超长、错误累积、超时、重试风暴、响应内容互相污染。根本原因不在于某个模型不够强,而是整个系统把“智能”和“业务逻辑”糊成了一大锅。
之所以写这篇,就是因为看到太多类似项目。AI Agent工程化强调的不是谁家模型更强,而是怎么把智能体拆成可以单独交付、单独测试、单独扩容的层级。分层交付的核心原则简单粗暴:不要让任何一个文件、一个Prompt、一个函数同时承担调度、决策、工具调用、记忆管理四件事。别把智能糊进一锅,这句话值得做成横幅贴在工位上。
1.2 “智能”也需要工程约束
很多团队容易产生一个错觉:大模型是黑盒,所以围绕它的代码也应该保持“混沌”。这话我完全不赞同。大模型本身不可解释、不可强约束,这是事实;恰恰因为这样,系统里其余部分才更要用足工程手段去兜底。Prompt是代码,但它比普通代码更脆弱,一个标点符号都可能改变输出。既然你把业务逻辑写在Prompt里,那就要像对待代码一样对待它:要有版本、要有测试、要有可回滚的发布路径。同样,工具调用是代码,编排逻辑是代码,它们都必须能被单测覆盖、能灰度、能监控。如果所有东西全部堆在一个大Prompt里,本质上等于把几十个if else写在一个没有编译器的文本文件里,出问题几乎是必然的。
工程上有个直观类比:单体应用没有分层之前,也不是不能跑,但改一个功能要动整个模块、加一个字段要担心影响别处。分层不是银弹,它是在系统复杂度到达临界点之后,唯一还能让人睡稳觉的组织方式。Agent系统因为多了一整套“大模型决策”的不确定性,复杂度比普通后端高一个量级,所以更需要早年后端领域已经验证过的分层思想。你不需要在第一个版本就分得无比精细,但至少要意识到:智能和工程不能揉成一团,否则出了问题连拆都拆不开。
1.3 分层交付到底在“层”什么
谈到“分层交付”,很多人的第一反应是画一张三层架构图。其实分层交付的关键不在于“画几个层”,而在于回答两个问题:每一层的输入输出是什么,每一层能不能独立上线、独立回滚。所谓“交付”,是跟DevOps语境绑定的:不是只有最终产品才算交付,接口层、编排层、智能模型层、工具适配层,每一层都应当有自己的制品、版本和验收标准。
例如智能层交付的是“给定结构化上下文,返回结构化决策结果”的模型服务;编排层交付的是“把决策结果转换为下一步动作序列”的状态机;工具层交付的是“对上游稳定可用、对下游收敛异常”的函数契约。这样定义之后,“别把智能糊进一锅”就有了清晰的含义:不要让某一段代码或配置同时承担模型路由、业务编排和外部工具适配的职责。每一层只做一件事,并且每一层的交付物边界保持稳定。这就是整个工程化改造的总纲领。
2. 一套可落地的分层架构
2.1 五层模型的职责划分
基于过去几轮实战,我倾向于把Agent拆成五层:接入层、编排层、智能层、工具服务层、数据与记忆层。不是层越多越好,而是这五层正好卡在“变化频率”不同的位置上,彼此之间的耦合度最低。
| 层级 | 核心职责 | 常见实现 | 变化频率 |
|---|---|---|---|
| 接入层 | 鉴权、限流、协议转换 | FastAPI、GRPC网关 | 低 |
| 编排层 | 状态流转、步骤切换、容错策略 | LangGraph、自研状态机 | 中 |
| 智能层 | 模型路由、提示词管理、结果校验 | OpenAI、开源模型封装 | 高 |
| 工具服务层 | 外部API、数据库、内部系统封装 | Function Calling、工具注册中心 | 高 |
| 数据与记忆层 | 向量库、缓存、会话历史、长期记忆 | Redis、Milvus、PostgreSQL | 低 |
接入层面向终端用户,负责鉴权、限流、协议转换,典型实现是FastAPI或GRPC网关。这一层的痛点是并发,跟普通后端服务没有本质区别,重点是不要让它代理任何业务决策。编排层是整个Agent的骨架,负责定义状态流转、步骤切换、容错策略,这是“智能体”和“普通接口”最大的分水岭所在。智能层则是对模型服务的封装,输入是固定schema的Prompt或消息序列,输出是结构化决策建议,层内可以做模型路由、多模型切换、结果校验。
工具服务层把外部API、数据库操作、内部系统调用统一封装成工具函数,只暴露工具名、入参出参、错误码。数据与记忆层负责向量库、缓存、会话历史、长期记忆,它服务上层,但也要接受冷热分层、过期清理等治理。这五层的变化频率差异很大:模型层几乎每个月都在变,工具层跟着业务节奏走,编排层相对稳定但需要根据线上反馈微调,数据层则更像基础设施。如果让模型升级影响编排逻辑、让工具变化触发接入层发布,那就是分层失败。
2.2 层间契约比代码本身更重要
分层最难的不是画架构图,而是定义一个稳定、可测试、可演进的语言,让每一层之间只用这个语言说话。我通常先定下一套统一的输入输出结构,核心是Message和AgentEvent。Message是标准消息协议,包含role、content、tool_calls、meta等字段;AgentEvent是编排层向外暴露的运行时事件,包含step、status、duration、input_hash、output_ref等字段。
有了这两个契约,智能层只需要做到“Message进,Message出”,不需要知道外面的业务是跨境电商售后还是工业质检。工具层每个工具都是“JSON进,JSON出”,并且强制返回稳定错误码,而不是自由抛异常。这样做的收益很大:当模型从GPT切到某个开源模型时,只需要改智能层内部实现;当某个工具需要迁移到新服务时,编排层完全不动。契约是静态的,但实现可以动态替换,这是分层交付能带来灵活性背后的根本原因。
2.3 每层独立交付,独立演进
分层之后,“交付”这件事也要跟着分层。我所在的团队会把每个层次视为独立的Git仓库,拥有独立的CI流水线和制品。智能层会单独发版,模型提示词和温度等参数的调整不再需要拉上整个项目发布。工具层的变更可以做到独立灰度,先让5%流量试用新工具实现,稳定后全量,异常时单独回滚这一层的版本。编排层则把图配置作为版本化配置管起来,我见过太多团队用一份巨大的Python文件定义图,改了还说不清改了哪块。
当然,独立交付不是推翻一切然后一次性迁移。实践里最稳妥的做法是先把一个现有“一锅糊”的Agent从某个任务边界处“切一刀”,把智能层剥离出去,接着把工具调用改造成标准工具,最后才轮到编排层改造。一步一步走,每一小步都能独立上线,比轰轰烈烈搞三个月重组体感好太多。这套思路不仅适合企业级项目,个人练手项目同样适用,只是规模小一点而已。
3. 从0到1搭建分层Agent的核心实操
3.1 第一步:先画状态机,再定接口
很多项目都是从写代码开始的,我建议反过来:先在纸上把Agent的状态流转画出来。哪怕是最简单的客服Agent,也会经历“接收输入、意图识别、工具调用、结果合成、人工兜底”几个状态。注意这里不需要漂亮的专属流程图,用文本状态表就可以。状态表里明确每个状态下系统能做什么、不能做什么、超时了怎么办。状态表定完,接口契约基本也就定了,后面写代码只是翻译工作。
举例来说,一个售后Agent的状态可以是:
- idle:等待输入,超时清空会话
- intent_recognizing:调用模型做意图分类,失败重试最多2次
- tool_executing:执行具体工具,单次工具超时10秒
- answer_generating:基于工具结果生成回复,做格式校验
- human_handoff:转人工,工单落库
状态表的价值会在上线后体现:业务方说“你这个Agent步骤不对”,你可以直接指着状态表讨论;而如果逻辑藏在一个500行的图配置里,讨论门槛会高很多。更重要的是,状态表能帮你提前发现死循环和缺失分支,比如“工具调用失败后没有出口”,这种问题在设计阶段就能被干掉。
3.2 第二步:用LangGraph把编排层落地
编排层我推荐使用LangGraph,或者任何支持显式状态机的框架。LangGraph的好处是图的结构在代码里一眼可见,节点之间通过共享state传递数据,天然适合分层。下面的示例展示了一个最小可跑的售后Agent图:
from langgraph.graph import StateGraph, END from typing import TypedDict, Optional class AgentState(TypedDict): user_input: str intent: str tool_result: Optional[dict] final_answer: str def recognize_intent(state: AgentState) -> AgentState: # 调用智能层,这里只做一层薄封装 from intelligence.router import decide_intent state["intent"] = decide_intent(state["user_input"]) return state def execute_tool(state: AgentState) -> AgentState: # 工具执行,统一走工具服务层 from tools.registry import invoke_tool state["tool_result"] = invoke_tool(state["intent"], state["user_input"]) return state def generate_answer(state: AgentState) -> AgentState: from intelligence.generator import generate_reply state["final_answer"] = generate_reply(state) return state g = StateGraph(AgentState) g.add_node("intent", recognize_intent) g.add_node("tool", execute_tool) g.add_node("answer", generate_answer) g.set_entry_point("intent") g.add_edge("intent", "tool") g.add_edge("tool", "answer") g.add_edge("answer", END)这段代码里有个习惯值得学:每个节点都尽可能薄,只做状态搬运和调用下层接口,不写具体业务判断。业务判断放在智能层的决策模型里,异常处理放在工具服务层的wrapper里。这样图本身就是一张可读的状态表,换人接手也不会迷路。
3.3 第三步:把智能层设计成可替换的插槽
智能层是整个系统里最容易“变更”的部分,所以要把它设计成可替换插槽。我的做法是抽象一个IntelligenceProvider接口,所有模型调用都实现这个接口:
from abc import ABC, abstractmethod from typing import Any class IntelligenceProvider(ABC): @abstractmethod def decide_intent(self, user_input: str, context: dict[str, Any]) -> str: ... @abstractmethod def generate_reply(self, state: dict[str, Any]) -> str: ...线上实现可以是OpenAI、通义、DeepSeek或者某个私有化部署的开源模型,但编排层永远只依赖这个接口。模型切换时,我只需要新增一个Provider类,在配置中心把路由目标改过去,不需要改动编排层一行代码。这里有个容易翻车的地方:有些人图省事,让模型直接返回一段自然语言决策,然后在编排层用字符串匹配去判断意图。这种做法其实又把“决策”和“规则”糊在了一起。正确的姿势是让模型输出JSON结构化结果,再在智能层内部做一次schema校验,校验不过就重试或走兜底。分层可以带来灵活,但前提是每一层的产出都必须稳定到可以被下一层可靠消费。
3.4 第四步:工具层与外部系统解耦
工具服务层的核心工作是“把不可靠的外部系统包装成可靠的内部函数”。很多Agent在线上出问题都是因为工具调用失败后没有标准错误码,异常信息满天飞。工具层的每个工具都要返回统一结构:
def invoke_tool(intent: str, user_input: str) -> dict: tool = find_tool(intent) try: result = tool.execute(user_input) return {"ok": True, "status": "success", "data": result, "error": None} except TimeoutError: return {"ok": False, "status": "timeout", "data": None, "error": "TOOL_TIMEOUT"} except Exception as e: return {"ok": False, "status": "error", "data": None, "error": f"TOOL_ERROR:{type(e).__name__}"}有了这个结构,编排层可以用非常清晰的规则处理失败:遇到TOOL_TIMEOUT就重试一次并缩短超时时间,遇到TOOL_ERROR就转人工或者换一个替代工具。而不是让异常一路穿透到接入层,用户只看到一个干巴巴的“服务器内部错误”。
3.5 第五步:FastAPI接入与异步化
接入层我默认用FastAPI,生态成熟,对异步支持好。需要提醒的是,Agent请求通常比普通HTTP请求慢得多,动辄几秒甚至几十秒,所以接入层从一开始就应该考虑异步和超时。下面是一个常见的POST接口:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel class ChatRequest(BaseModel): user_id: str message: str class ChatResponse(BaseModel): answer: str trace_id: str app = FastAPI() @app.post("/v1/chat", response_model=ChatResponse) async def chat(req: ChatRequest): # async模式 + 编排层接口 result = await run_agent_async(user_id=req.user_id, message=req.message) return ChatResponse(answer=result.answer, trace_id=result.trace_id)接入层不应该自己实现Agent逻辑,它只负责拿到HTTP请求,转成内部协议,异步调用编排层,然后等结果返回。如果用户请求量大,可以再套一层消息队列或者WebSocket,但核心原则一样:接入层只做接入,不做决策。很多团队把接入层写得又胖又重,实际上是把接入层和编排层的边界给抹掉了。
3.6 并发上量之后要面对什么
AI Agent怎么扛并发,是每个工程团队最终都要回答的问题。模型调用是有并发上限的,外部工具有可能被瞬时流量打垮,所以要在各层做“并发预算”管理。我的经验是分三个层次去控:
第一层,接入层限流。基于用户ID做令牌桶,普通用户每分钟最多N次,VIP用户可以放宽。第二层,智能层的模型并发控制,用一个并发信号量限制同时对模型发起的请求数,避免把模型服务打满导致所有请求都排队超时。第三层,工具层超时兜底,给每个工具设置明确超时时间,并且提供降级方案。
这里有一个容易被忽视的问题:重试风暴。当某次模型调用超时,所有请求几乎同时重试,会把模型打得更挂。所以重试必须加随机退避,而且要限制重试次数。我见过不少线上故障,追查下来不是模型不够好,而是重试策略太暴力,把本来还能抢救的系统活活压垮了。分层架构在这里的好处是,你可以针对智能层和工具层分别配置重试参数,而不是在接入层一刀切。
4. 分层系统里的常见问题和排查技巧
4.1 分层之后性能反而下降怎么办
分层带来的最直观副作用是多次网络调用和数据转换,性能确实会有损耗。如果发现比单层慢,不要急着把层合并回去,先看损耗发生在哪里。常见情况是智能层和工具层之间来回传大对象,尤其是把整个会话历史反复读取、序列化、传入模型。对策是引入轻量级上下文缓存:对固定不变的工具描述、系统提示词做缓存;对会话历史做增量传递,而不是每次全量组装。
另一个常见原因是同步调用太多。比如编排层串行调用三个工具,每个工具300毫秒,总耗时接近一秒。如果三个工具之间没有依赖,可以改成并发调用。LangGraph里可以用并发节点,Python侧则用asyncio.gather去并行执行。优化完通常能找回一大截性能。要记住,分层的目标不是追求理论上的零损耗,而是让性能损耗可以被度量、被定位、被优化。
4.2 跨层追踪:没有trace_id无从排查
分层系统的最大排查难点是“问题到底出在哪一层”。没有统一追踪,生产环境一报错,只能靠猜。我的建议是第一版就引入trace_id,从接入层生成,贯穿智能层、工具层、数据层。日志每一行都要带trace_id和layer字段,模型调用记录要打上模型版本、输入长度、输出内容摘要、耗时。工具调用要记录工具名、参数、返回状态码。
| 常见现象 | 可能原因 | 排查动作 |
|---|---|---|
| 单条请求特别慢 | 工具层外部接口劣化 | 看trace_id下每个工具耗时分布 |
| 模型返回乱码 | 智能层schema校验失效 | 看模型输出摘要是否包含非预期字段 |
| 偶发性超时 | 并发信号量被打满 | 看智能层并发占用率与排队长度 |
| 用户多次反馈不一致 | 编排层状态被并发覆盖 | 确认state是否按user_id隔离 |
如果做得好,排查链路会变成这样:用户反馈“回复太慢了”,拉到trace_id,看编排层每个节点的耗时分布,发现90%时间花在某个工具调用上,再去查是不是那个外部接口性能劣化。没有trace_id,这些问题基本只能靠开发直觉拍脑袋定位。
4.3 工具层超时和异常兜底
工具调用是整个Agent里最不稳定的一环。外部接口会挂,网络会抖,数据会缺字段,如果不做兜底,Agent会表现出“随机性故障”。我总结了一个三层兜底策略:第一层超时,工具执行超过阈值立刻返回超时错误码,不让调用方无限等待。第二层重试,对幂等工具可以安全重试,对接单、支付这类非幂等操作严禁盲目重试。第三层降级,工具挂了就返回一个明确提示让用户知道,同时转人工或者推荐替代路径,而不是让模型硬着头皮编答案。
另外还要注意工具入参的校验。外部系统接口五花八门,传参格式稍有不对就会报错。工具层应该在入口处做一次参数校验,缺字段就返回清晰错误码,方便快速判断是调用方的问题还是工具本身的问题。
4.4 模型限流与并发预算
模型限流问题在分层架构里会变得很突出,因为编排层可能在不同状态下多次调用同一个模型。比如一个客服Agent可能先做意图识别,又做回复生成,一次用户请求消耗两次模型配额。如果接入层只做用户维度的限流,就可能在模型层互相踩踏。我的做法是给智能层单独设置一个全局并发信号量,同时把“每一步模型调用”的配额都记录在日志里,做成按用户、按状态的预算报表。
这样做下来,调优就有了依据,而不是凭感觉加并发。比如说某天模型网关报警,打开报表一看,某个状态的调用量飙得厉害,那就可以精准地在该状态前加一道限流,而不是把整个Agent的限流阈值调低,误伤其他正常用户。这套机制本质上就是把“智能”当成一种可计量的资源来管理。
5. 一些经验与心得
5.1 不要为了分层而分层
我见过有些团队把架构图画得非常漂亮,但每层之间没有明确边界,最后还是把所有逻辑放在一堆Service文件里互相调用。分层的前提是你真的能定义出稳定的层间契约。如果你现在还说不清楚每个层的输入输出是什么,那就先别急着拆,先从小范围试点开始,把一个完整链路跑通后再切。画了几层架构图不代表分层,真正分层的标志是:改上层不用动下层,改下层不用动上层,出了问题能迅速定位到某一层。
5.2 先相信“最小分层”,别一上来就是中台
行业里经常能听到“AI Agent中台”这类词,但个人项目和小团队不建议上来就搞中台。中台是好东西,但它本质上是多个业务线共用基础设施后的自然演进,不是一开始设计出来的。从0到1搭建AI Agent,优先做好五层分离中的“切一刀”:先保证智能层和编排层不混在一起,其他层可以慢慢演进。很多项目死在过度设计上,而不是死在不够分层上。等你真的有三个以上业务方需要共用同一套Agent能力,再回头建设中台完全来得及。
5.3 个人练手项目也能运用分层
如果你的目标是练手,建议挑一个很小的场景,比如个人定个“会议纪要助手”或者“私人知识问答Agent”,强制自己按分层的方式写。我的经验是,哪怕只有几百行代码,分层写法和一锅糊写法带来的心智负担差异也很大。练手项目跑起来之后,再用JMeter或者Locust压一下并发,看看限流和超时策略有没有作用。这些能力才是从“概念演示”走向“工程化落地”的真正分水岭。
最后再分享一个小技巧:每次给Agent加新功能,先问自己一句——这个功能的“决策逻辑”应该放在智能层还是编排层?如果答案是“可以写进结构化规则里”,就放在编排层;如果答案是“需要模型根据上下文判断”,就放在智能层。这个判断做顺了,分层的路就不会走偏。