说句得罪人的话:现在很多号称“做AI Agent”的项目,本质上就是个玩具。模型选工具、模型调函数、把结果拼回去,循环几轮,Demo跑起来特别炫,一接到真实的业务流量就现原形:上下文一长就断片,请求绕来绕去它就蒙圈,工具调用出错不会恢复,连权限都没做,谁都能调底层接口。这种现象很普遍,也是这篇内容想聊透的事。
如果你正准备把Agent从“跑得通”升級到“扛得住生产环境”,或者你已经用某主流框架写了原型但不敢上线,这篇内容会非常合适。我会用一套虚拟的业务助手系统做例子,拆解从玩具级Demo到企业级系统之间的代码架构差异。这里没有公认的银弹,只有我实际趟过这些坑之后沉淀下来的分层方案、核心抽象和防翻车清单。
1. 先认清“玩具级”的边界
1.1 玩具级Agent的常见写法
大多数Agent雏形长这样:把用户问题喂给大模型,塞一堆函数名和函数描述,让模型决定“该调用哪个工具”,然后把工具返回的结果再交给模型继续推理。看起来像发生了“智能决策”,实际只是把工具调用协议封装成了一个小循环。我见过最典型的代码大概是:
async def toy_agent(user_input: str) -> str: messages = [ {"role": "user", "content": user_input} ] for step in range(8): resp = await llm.chat_with_tools(messages, tools) if resp.tool_calls is None: return resp.text for call in resp.tool_calls: result = await execute_function(call.name, call.arguments) messages.append(call.to_assistant_message()) messages.append(result.to_tool_message()) return "我已经尽力了,但还没搞定"这段代码如果跑在提前筛好的三五个例子上,效果惊艳。但你要理解它本质上是把一个没有边界、没有状态、没有恢复策略的“实习生”直接扔给了用户。这个实习生确实会用Excel、能查订单、会发退款,但它分不清用户是不是老板,也不知道同一时刻来了100个人时自己该优先干什么,更不会在操作到一半发现数据不对时主动停下来说“我需要复核”。能跑通,但很脆弱。
1.2 为什么玩具级Agent只能停留在玩具
先说个容易被忽视的问题:上下文管理。玩具级Agent通常把所有对话历史全部堆进大模型上下文里,没有摘要、没有裁剪、没有重点召回。当会话超过模型上下文窗口,要么硬性报错,要么截断丢信息,用户上一句说的关键信息直接被扔掉,表现成“AI失忆”。在企业场景里,用户可不会容忍这种失忆。
然后是错误恢复。真实工具调用会失败,数据库连接会超时,第三方接口会返回格式诡异的错误。玩具级Agent最常见的做法是把异常信息原样塞回给模型,让模型“自己理解重试”,结果就是反复调用同一个失败接口,既浪费Token又以同样方式失败。更麻烦的是,它没有明确的工作状态流转,谁知道它在Planning还是Executing?日志里除了“调用了某个函数”什么都没有,线上出了问题只能靠猜。
还有权限治理。玩具级Agent把函数列表一股脑暴露给模型,意味着模型只要能组合,就可能调用到高权限操作。有一次我在测试环境里遇到一个Agent把订单状态从“已支付”直接改成了“已取消”,它的判断依据仅仅是用户说“我不要了”。Demo里这最多算逻辑错误,在生产环境里这就是事故。
工具本身是否幂等、是否有超时、是否有审计记录,这些在玩具级阶段基本是空白。一旦并发上来,问题会更明显:多个请求共享一个内存队列、没有限流、没有熔断,只要一个慢接口卡住,整个进程都可能被拖垮。
2. 要做企业级,先改变架构思维
2.1 核心不是模型,而是可治理的流程
很多人误以为企业级Agent的重点是模型更强、Prompt写得更好。我的经验是恰恰相反,企业级的核心是“约束”:把自由度极高的模型行为,纳入可设计、可控制、可回滚的流程框架里。
你可以把大模型当成一个业务能力很强但不稳定的外包员工。你不能让它自己决定“我要访问哪些系统、我要操作哪些数据、我要何时停下来”。你必须给它划定边界:能调哪个系统、能查哪些客户、单次任务最多走几步、涉及高危操作时要不要人工审批。模型依然是决策者,但决策的上限和下限都是你代码里定义好的流程。
这也是我推荐“有限状态机驱动”而不是“完全让模型自驱”的原因。Agent确实需要弹性,但企业更需要确定性。状态机明确每一步叫什么、允许哪些转换、异常落在哪个状态,出了问题可以快速定位和恢复;而模型只负责在状态内产出决策,不负责创造流程。
2.2 能力分层:别把Agent写成一个大函数
从玩具到企业级,我做的第一件事就是把Agent拆层。不要在一段代码里既处理用户消息又直接调用业务接口,而是把系统拆成接入层、编排层、能力层和治理层。
接入层负责与外部渠道对接,比如Web、小程序、办公软件机器人。它只做协议转换,不承载Agent逻辑。编排层是AgentRuntime,负责状态流转、步骤计数、超时控制、记忆读写。能力层是工具注册中心,所有对业务系统的操作都通过这里,统一做参数校验、权限校验、限流和审计。治理层则是横切关注点,包括链路追踪、结构化日志、成本统计、评估系统。
这个分层直接解决了一个大问题:测试和替换。想让Agent从一个业务场景迁移到另一个,我不需要重写主循环,只需要更换工具注册表里的能力集合和Prompt模板;想升级底层大模型,也不需要改动业务代码,只需在编排层替换模型调用适配器。一个“大函数”是做不到这种复用的。
3. 核心代码架构拆解:接口、循环、记忆与工具治理
3.1 面向接口的核心抽象
企业级Agent的第一行代码,不是写业务逻辑,而是定义抽象接口。先把“运行时”和“业务工具”解耦。我通常在业务系统里定义这几个核心接口:
from abc import ABC, abstractmethod from typing import Any, AsyncIterator class AgentRuntime(ABC): @abstractmethod async def stream(self, request: AgentRequest) -> AsyncIterator[AgentEvent]: """以事件流方式返回Agent的每个中间步骤,便于前端展示和日志采集""" ... class ToolProvider(ABC): @abstractmethod async def execute(self, tool_name: str, args: dict, ctx: ToolContext) -> ToolResult: """统一工具执行入口。所有工具都从这里过,不能直接暴露给外部系统""" ... class Memory(ABC): @abstractmethod async def save(self, session_id: str, message: Any) -> None: ... @abstractmethod async def load(self, session_id: str, limit: int = 10) -> list: ...为什么强调接口,而不是直接写实现?因为我见过太多项目刚开始用某个框架的Agent类,后来发现框架封装的API满足不了业务需求,结果被框架拖着走。接口是你的“契约”,只要契约稳定,底层可以用任何框架实现,甚至自己写Loop都行。接口设计好了,后续换记忆存储、换工具集、加限流都不需要炸掉整座大楼。
事件流也是一个容易被忽略的点。玩具级Agent只返回一句最终结果,企业级Agent必须返回“中间发生了什么”:模型正在规划、准备调用工具、工具返回结果、执行异常、重试中。前端可以实时展示Agent的思考过程,后端可以把事件写入日志和审计系统。这个体验差异非常大,排查问题时更是救命。
3.2 用有限状态机驱动主循环
我建议把Agent主循环设计成有限状态机,而不是让模型在一大段自由文本里自己“发挥”。有限状态机的好处是每种状态都有明确入口和出口,任何异常都有默认兜底。一个典型的状态集合如下:
from enum import Enum, auto class AgentState(Enum): READY = auto() PLANNING = auto() WAITING_TOOL = auto() OBSERVING = auto() ASK_HUMAN = auto() DONE = auto() ERROR = auto()主循环的骨架可以这样理解:进入READY后读取记忆和上下文;进入PLANNING后让模型生成下一步意图;如果意图需要调用工具,进入WAITING_TOOL,调用工具并设置超时;拿到工具结果后进入OBSERVING,让模型判断任务是否完成;如果模型自己不确定,进入ASK_HUMAN,请求用户澄清;如果达到最大步数或时间预算,强制进入DONE或ERROR。
这个过程中,步数限制和超时是硬约束,不是装饰。我在生产环境里默认限制单轮任务最多12步,单步工具调用最长15秒,整体任务最长90秒。一旦超限,Agent必须主动说“我需要人工介入”,而不是无限循环烧Token。状态机的每个转移路径都要有日志,至少记下“from_state, to_state, reason”,否则状态机只会让你更混乱。
还有一个关键点:状态机不代表不能使用模型的“自由发挥”。模型仍然可以提出各种可能计划,但计划必须落到状态机的合法转移范围内。例如模型说“需要查询订单再退款”,系统会拆成“查询订单”状态和“退款审批”状态,高权限操作没有直接放行,而是先走审批。这样既利用了模型的理解能力,又没有把运行安全交到模型手里。
3.3 工具注册中心与权限网关
工具层是企业级Agent最容易出事的地方。我把所有工具统一放进一个注册中心,而不是让模型直接看到一堆任意函数。注册中心里每个工具都有元数据:名称、描述、参数JSON Schema、需要的权限等级、是否幂等、平均耗时、超时时间、是否允许并发调用。
TOOL_REGISTRY: dict[str, ToolSpec] = {} def register_tool(spec: ToolSpec): TOOL_REGISTRY[spec.name] = spec def tool_spec(name: str, description: str, authz: str, idempotent: bool, timeout=10): def decorator(fn): register_tool(ToolSpec( name=name, description=description, authz=authz, idempotent=idempotent, timeout=timeout, fn=fn )) return fn return decorator真正执行时,通通走一个execute入口。这个入口会做几件事:检查工具是否存在、校验参数是否符合Schema、检查当前用户和上下文是否具备该工具所需权限、做幂等判断、打审计日志、设置超时和熔断。这样一来,工具开发者只负责写业务逻辑,安全与治理全部由网关完成。
权限必须细粒度。不要只分“管理员”和“普通用户”,要按操作维度切分。比如“查询订单”可以是所有客服账号都能做,“修改订单金额”则必须有主管授权,“导出用户数据”必须走审批流并留痕。工具描述里不要写“可以执行任何操作”,那等于把管理后台裸奔给模型。
幂等也是一个必须考虑的细节。如果工具要调用的第三方支付接口没有幂等键,重复调用会造成资金损失。我规定:不能保证幂等的工具,必须提示模型“你只有一次执行机会”,并在代码层用Redis锁或UUID去重。这比事后对账便宜得多。
3.4 三层记忆:短期、总结、向量检索
上下文管理不能只靠大模型的窗口。我把记忆拆成三层:短期对话记忆、会话摘要、长期向量记忆。
短期对话记忆保存最近几轮原始消息,按时间窗口和token数双重限制。窗口设成多少要看模型能力,一般保留最近10到20轮就够日常对话,再多就该触发摘要。会话摘要是把更早的对话用模型压缩成结构化要点,比如“用户已确认退货地址为X,退款金额Y元”,然后每次请求前把摘要放回上下文。
长期向量记忆负责跨会话的信息召回。比如企业知识库、历史工单、用户偏好,用户提到“上次那个问题”时,系统先从向量库检索相关内容,再拼装进Prompt。这里要小心“记忆污染”:不要把检索到的每条内容都塞进去,应该按语义相似度和时间衰减加权排序,只取最相关的3到5条。否则上下文里堆满无关信息,模型容易被带偏。
三层记忆的落盘方式也不一样。短期记忆可以放Redis,TTL设成一小时;摘要和长期记忆放数据库或向量库。整个记忆读写也必须走接口,不能直接在代码里操作具体存储,否则后面换向量库、加权限隔离都会很痛苦。
4. 企业级补充:稳定性、安全、可观测性与评估机制
4.1 异常、超时、重试与兜底
Agent的稳定性问题和传统后端有类似之处,但更难,因为错误来源更多:模型服务超时、工具接口报错、参数校验失败、模型幻觉导致非法工具调用。我在生产环境里做了一套分层异常处理策略,不是把所有异常都丢给模型。
| 失败类型 | 处理策略 | 说明 |
|---|---|---|
| 模型调用超时 | 指数退避重试 | 最多重试2次,超时后切换到备用小模型 |
| 工具调用报错 | 按错误码分支 | 参数错误直接修正后重试;数据不存在则不再重试 |
| 工具接口限流 | 熔断降级 | 连续5次失败后熔断10秒,返回“功能暂不可用” |
| 模型连续多次同样计划 | 强制停止 | 检测到重复计划时人工接管 |
最容易踩的坑是“让模型自己重试所有错误”。模型对错误的原因判断并不可靠,重试一个会一直失败的接口只会烧钱。正确做法是在代码层定义哪些错误可重试、哪些不可重试。可重试的错误通常是瞬时故障,比如网络抖动、限流;不可重试的是业务性错误,比如用户不存在、订单已关闭。这个分类要在工具注册中心里一并声明。
兜底方案也必须提前设计。Agent整体失败时,不能只给用户一句“抱歉,我不行”,而是要提供人工客服入口,生成一个工单编号,并把Agent的完整执行轨迹一并保存。用户转人工时,客服打开后台就能看到Agent已经做了哪几步、在哪个环节失败。没有这种兜底设计,Agent一上线就会变成客服团队的灾难。
4.2 链路追踪与结构化日志
玩具级Agent排查问题靠“加print”,企业级Agent必须有全链路追踪。我要求Agent的每个请求从一开始就生成一个trace_id,这个ID贯穿模型调用、工具执行、记忆读写、状态转移和外部API调用。所有日志必须是结构化JSON,而不是散装字符串。
一个典型的日志记录长这样:
{ "ts": "2025-01-15T10:30:00.123Z", "trace_id": "8f6a2c91", "session_id": "chat_8821", "state_from": "PLANNING", "state_to": "WAITING_TOOL", "event": "state_transition", "step": 3, "latency_ms": 1240, "token_cost": 289 }只要这条日志齐全,复现问题时完全可以“回放Agent走过的路”。尤其是遇到“用户说退款了,但系统显示没退款”这种矛盾,一查trace就能看到Agent到底做了哪一步、有没有执行工具、还是在最后生成回答时编了瞎话。没有链路追踪的Agent系统,出了问题就只能靠用户截图还原现场,效率极低。
4.3 成本控制与并发防护
企业级Agent的另一大坑是成本失控。模型调用不是免费的,工具调用也可能触发第三方计费。我在系统里做了三层成本控制:请求级预算、用户级配额、全局熔断。
请求级预算是说单次Agent任务最多允许消耗多少Token、调用多少次付费工具。超预算时强制结束,并允许用户重新表述或者转人工。用户级配额是每个账号每天允许消耗的总Token和工具调用次数,防止个别用户拖垮整体资源池。全局熔断则是在某个时间段内失败率过高时,自动把Agent切换成低速模式或直接降级到FAQ机器人。
模型路由也很重要。不是所有请求都需要最强模型。简单分类任务用小模型,复杂多步任务才用大模型。成本差别能到几十倍,而用户感知的质量差距在简单任务上几乎为零。我在接入层做好意图预分类,把高频低难度请求导流到便宜模型,大幅降低账单。这个优化做起来不难,投入产出比极高。
4.4 回归评估与版本发布
Agent上线之后最大的焦虑是:模型一升级或Prompt一改动,行为就可能飘忽不定。因此必须有回归评估系统。我维护了一套评估集,包含几百条典型的用户请求,每条都标注了“预期工具调用序列”和“最终答复要点”。每次改动上线前,批量跑一遍评估集,统计工具调用准确率、最终答案命中率和平均轮次。
这套评估系统不要求大模型,也不要求多复杂的框架。其实就是离线跑批:输入请求、运行Agent、记录每一步、和期望结果比对。只要自动化通过率低于阈值,就不允许发布。对于新场景没有数据的,先人工抽检并逐步补充到评估集。把这套流程跑起来以后,“模型升级”才敢安心执行,否则每次升级都是在赌运气。
另一个经验是灰度发布。企业内部Agent可以先放开给一个小组,观察日志里的失败率和用户反馈,半天到一天后再全量放开。这个过程比传统后端灰度更不可少,因为模型行为无法靠代码review完全判断。灰度期间如果发现某个工具被高频误调,立刻从注册中心里降权或下线,过程不可怕,可怕的是没有退出按钮。
我最后再分享一个很小但很实在的经验:从设计第一行代码开始,就把trace_id、session_id、工具权限等级写进接口规范。这两个字段一开始不显眼,等到线上问题爆发时,它们就是救命的锚点。做企业级Agent没那么玄乎,就是把模型的自由度一点点管起来,把每一个决策和行为放到可追踪、可控制、可回滚的框架里。做到了这一点,你手里的东西才真正配得上“企业级”三个字。