1. 插件化架构的黄金时代:它解决了什么,又埋下了什么
先说一个我自己的背景。过去三年我一直在做某智能助手平台的后端架构,平台从零到一,经历过单体、模块化拆分、插件化演进,再到最近半年因为接入大模型而被迫重新审视整套架构。这个过程中最深的感受是:插件化架构曾经帮我们挡了很多刀,但它从诞生的第一天起,就带着一套和智能体系统天然冲突的假设。
1.1 当年怎么用插件化扛住业务需求的
2019年前后,我们平台的核心场景是给企业客户提供一套可配置的自动化流程引擎。业务方经常要接不同的内部系统——有老掉牙的SOAP接口,有部门自建的HTTP服务,还有一堆放在共享盘上的Excel表格,业务人员希望这些都能被"一键接入"。
当时我选了插件化架构。理由非常朴素:插件化能让我们把每个对接系统的差异封装在独立模块里,业务方提供一份配置或者一个jar包,主程序不用改,新接入的系统就像插U盘一样插上去就能跑。这套设计在早期确实很香。我们定义了一个统一的Plugin接口,约定init()、execute()、destroy()三个生命周期方法,每个插件内部自行搞定协议转换、参数映射、异常处理。主流程只认这个接口,剩下的全是插件自己的事。
后来插件数量增长到几十个,我们又加了一个插件注册中心,用一张配置表管理插件的名称、版本、依赖关系和启停状态。运行时通过反射加载,主程序连插件类都不直接依赖。这一套在当时的技术论坛上还被不少同行讨论过,我也分享过不少"如何设计一个优雅的插件系统"的经验。现在回头看,那个阶段的架构设计并没有错,它精准匹配了当时的业务形态——系统是调用方,插件是被调方,调用关系永远由主程序发起。
1.2 插件化架构的边界开始失效的几个瞬间
转折点出现在大模型接入之后。我们想给平台加上一个自然语言交互入口,用户直接说"帮我把上个月的销售数据汇总后发给财务",系统需要自动拆解任务、选工具、调用插件、汇总结果。
第一次让大模型调用插件时,我按老思路写了一个LLMPlugin,内部封装了调用大模型API的逻辑。跑通demo的那天团队都很兴奋,但很快就发现不对劲:
- 插件是被动等待调用的,而智能体需要主动发现并选择插件。我们原来的插件注册中心只能按名称精确找插件,模型拿到用户指令后根本不知道"汇总销售数据"对应哪个插件,我们需要给它一份"工具清单"让它自己挑。
- 插件的输入输出是强类型、定死的。原来
execute(Map<String, Object> params)约定得非常清楚,但模型生成的参数经常缺字段、多字段、类型错误,甚至直接给你传一个JSON字符串进来。 - 插件的执行结果是结构化的,模型要的是语义化的。插件返回一个
ResultCode=200, Data=[{...}],模型拿到这堆东西根本没法组织语言回答用户。 - 最要命的是状态管理。插件之间完全不共享上下文,但一次对话里模型可能先查了A系统的数据,接着要基于这个结果调用B系统的操作,中间还有用户的纠偏和反问。原来插件模型里根本没有"会话状态"这个维度。
另一个让我印象深刻的瞬间是上线第一周遇到的"级联幻觉"。用户问"今天仓库还有多少库存",模型先调用了库存查询工具,又自作主张调用了一个"订单预测"插件,最后把预测结果当成真实库存报给用户。这个错误链路用传统插件架构的眼光看几乎是不可理解的——我们从来没允许插件之间互相调用,但模型把多个工具串成了一个它自己想象中的流程。控制权发生了根本性转移:原来是我的代码在编排,现在是模型在编排我的代码。
1.3 认真盘点插件化设计的资产与负债
这不是一篇否定插件化的文章。插件化架构积累的经验有一部分是"可再生资产",换到智能体系统里依然成立;另一部分是"沉没负债",继续沿用会变成系统的阻力。
先盘资产。插件化教会了我们契约先行——每个插件定义清晰的输入输出边界,这几乎是所有可扩展系统的基础。还教会了我们隔离变化——每个外部系统的差异被封装在单个模块内,升级、替换、故障都不至于影响全局。这两条原则在智能体系统里依然是刚需,只是"契约的定义方式"需要升级。
再看负债。负债的核心是假设了编排者是确定的。传统插件化架构里的编排逻辑在代码里、在流程引擎里、在人工设计的规则里,它预先知道每一步做什么。但智能体的编排者是模型,它在运行时才决定"下一步做什么",这意味着:
- 契约必须能被"描述"而不只是被"调用"。模型需要知道插件是干什么的、参数是什么含义、什么情况适合调用,这要求插件元信息必须机器可读且语义清晰。
- 边界必须能被"校验"而不是"信任"。模型填的参数不可靠,我们要在边界处做强校验和兜底。
- 依赖关系必须能被"观察"而不是"隐藏"。插件之间通过模型间接产生的调用链条要能被完整追踪,出了事能定位能回滚。
想通这一点后,我开始明白一个问题:**光靠给插件系统打补丁,解决不了控制权转移带来的架构级变化。**我们需要重新设计一套适配"模型编排"的架构形态。"AI-Ready"这个词就是在那时候进入我们视野的。
2. AI-Ready不是一句口号:智能体对架构提出了哪些新要求
"AI-Ready"这个词被很多厂商拿来当营销话术糊弄人,广告里说"我们的产品已为AI全面准备",实际上就是加了一个聊天框。但如果把AI-Ready翻译成工程语言,它其实包含几个非常具体的、可验收的架构能力。我的理解是:架构层面对AI接入的准备程度,体现在控制权转移后,系统是否依然能保证正确性、可控性和可观测性。
2.1 从"我的代码调用它"到"它来编排我的代码"
我们常说做AI应用要把Prompt写好,但很多人忽略了架构层面,模型不是"调用一下API"就接进来的。它接入后会成为整个系统的运行时决策者。
打个比方。传统插件化架构像一个餐厅后厨:主厨(主程序)拿着菜单(业务流程),按顺序喊"备菜""炒菜""装盘",每个岗位(插件)听令行事。而智能体系统更像一个"顾客直接下单给后厨"的餐厅:顾客(用户)描述想吃什么,服务员(模型)自己决定让哪个厨师做、按什么顺序做、菜品不合口味怎么调整。后厨的岗位、设备、备料流程还在,但指挥权从主厨转移到了服务员手里。
这个转移带来三类架构变化,不是修饰性的,是结构性的:
- 需要具备"自我描述"能力。系统里每一个可调用的能力(原来的插件)必须能向模型完整、准确、无歧义地说明自己。就像服务员要知道每个厨师的拿手菜是什么、需要什么材料。
- 需要具备"容错对话"能力。模型会出错、会瞎编、会选错工具,架构上必须有校验、纠正、重试、降级这些机制。
- 需要具备"动态编排"能力。系统不能再靠预先画好的流程图运行,而是要能响应模型发起的调用序列,同时保证每次调用都在受控边界内。
这三类能力恰好是整洁架构(Clean Architecture)一直强调的依赖倒置和边界控制的用武之地。
2.2 工具注册表:为什么插件时代没有真正的工具发现机制
很多团队接入大模型后做的第一件事是给模型配一套Function Calling的工具描述。这本质上就是搭一个"工具注册表",但如果你是从插件系统迁移过来的,会发现一个尴尬的问题:你可以用插件接口快速暴露工具,但模型根本不知道该用哪个工具,以及什么情况下用哪个。
传统插件的注册中心解决的是"按名称找到实现类"的问题,而智能体的工具注册表需要回答三个完全不同的元级问题:
- 语义匹配:用户的自然语言请求对应哪个工具?这需要每个工具有用途说明、适用场景、参数含义,而且这些描述要能帮助模型做语义判断。老插件只写了"接口是什么",没写"什么时候用"。
- 冲突消解:两个工具都能完成类似功能时,模型怎么选?我们后来给工具加了
priority和preconditions字段,还写了conflict_policy来指定"当多个工具匹配时怎么处理"。 - 动态可见性:对不同用户、不同上下文,能看到的工具集合应该不同。一个普通用户让模型调用"删除生产环境数据"的工具,这绝对不行。插件时代靠权限系统控制用户的调用权,AI时代要控制的是模型对工具的可见性。
当时我的处理方式是,给每个工具的元数据增加一组AI专用的字段,和一个工具可见性过滤器。过滤器在每次模型请求前,根据会话身份、上下文状态、操作类型,过滤掉不该被这个模型实例看到的工具。这个机制后来被证明是防"工具滥用"最有效的一道闸门,远比事后审核调用日志来得可靠。
2.3 上下文传递机制:依赖注入解决不了的问题
插件系统里,如果插件A执行完的结果要传给插件B,传统做法是在主程序里做数据流转,或者用依赖注入把共享对象传给插件实例。这在智能体系统里走不通,因为模型编排的工具调用序列不是预先可知的,你没法在代码里写"先A后B"的数据传递逻辑。
比如用户问:"这个项目的预算还剩多少?如果不够的话,帮我看看哪些供应商的合同还可以调整付款周期。"模型可能先查预算,发现不够,再查合同列表,筛选出可调整的合同,然后计算调整方案。这个过程中每一步的中间结果都要透传给下一步,而这个链路是模型在运行时动态构建的。
我们专门设计了一个会话上下文总线(Session Context Bus)。它本质上是一个有命名空间的键值存储,每个工具执行完毕后,可以选择性地把结果写入某个命名空间;后续的工具可以根据模型给出的引用键读取这些数据。设计上参考了消息队列的生产者-消费者模式,但更简化——生产者写,消费者读,没有复杂的路由规则。
这个机制的关键在于上下文数据的序列化格式要统一。我们的总线存储的不仅仅是JSON字符串,而是包含schema_version、data_type、confidence、source_tool这些元信息的结构化对象。这样当模型的后续调用需要用到前面某步的结果时,工具适配器能判断数据类型是否匹配,而不是盲目地把字符串塞进去。这在防止"错误数据被当正确数据继续使用"上面非常有效。
2.4 可观察性和可回滚性:模型不可控后的架构兜底
智能体和传统系统的另一个本质区别是,传统系统的行为是确定性的,同一输入一定走同一分支;智能体的行为是概率性的,同一个Prompt同一批工具,可能在两个请求里给出不同方案。这意味着架构上必须设计出可观察、可回滚的机制,否则你根本无法解释系统为什么这么做。
我们做了三层兜底:
- 全链路追踪。不是简单打日志,而是从用户进入对话开始,完整记录每次模型决策、每轮工具调用、每个参数生成过程,甚至包括模型当时的Prompt模板。之后复盘时能精确回答"它为什么调用了这个工具"。
- 执行沙箱与预检。高危工具(删除、写入、转账这类)在执行前必须经过一个预检阶段,把"将要执行的动作"压成一个可读的执行计划,经过确认或规则审核后才真正落地。本质上是从架构层面把工具的调用和执行拆分成两个阶段。
- 会话级回滚。我们维护了每个会话的"工具执行快照",如果用户在对话中发现模型做了一连串错误操作,可以一键回滚到操作前的状态。这个快照在插件时代是不需要的——没人会撤销一个确定的流程,但AI时代用户天然会期望"说得不对可以撤回往前重来"。
这三项能力落到架构上,最终都指向同一个方向:**把模型的影响范围限制在"决策层",把它的手脚绑在"受控边界"内。**这和整洁架构的初衷完全一致——让核心业务逻辑稳定,让外部变化可控。
3. 整洁架构在智能体系统中的实际落地方案
聊完了为什么要变,下面进入正题:具体怎么落地。
我最终采用的方案,是把整洁架构的核心思想——分层、依赖倒置、边界控制——应用到智能体系统的结构设计上。但和传统的整洁架构相比,有几处针对AI场景的明显调整。
3.1 按依赖方向重新划分四层:Entities、Use Cases、Adapters、AI接入层
传统的整洁架构通常分三层或四层:实体层(Entities)、用例层(Use Cases)、适配器层(Adapters)、框架层(Frameworks & Drivers)。在智能体系统中,我保留了前三层的基本职责,把最外层替换为"AI接入层",并且在每一层里都增加了面向智能体的新组件。
┌─────────────────────────────────────────────┐ │ AI接入层 (AI Interface) │ │ 模型网关 / Prompt模板管理 / 工具选择策略 / │ │ 输出解析与语义校验 │ ├─────────────────────────────────────────────┤ │ 适配器层 (Adapters) │ │ Tool Adapter / 工具注册表 / 会话上下文总线 / │ │ 检索与记忆适配器 / 外部系统对接 │ ├─────────────────────────────────────────────┤ │ 用例层 (Use Cases) │ │ 任务规划 / 工具编排 / 权限校验 / 执行计划 │ │ 状态管理 / 降级策略 │ ├─────────────────────────────────────────────┤ │ 实体层 (Entities) │ │ 业务实体 / 工具元数据 / 会话模型 / 领域规则 │ └─────────────────────────────────────────────┘依赖规则依然是:外层依赖内层,内层不感知外层。AI接入层可以调用适配器层的接口,但适配器层绝对不直接依赖某个具体的模型API。用例层只面向抽象接口编程,不知道外面接的是GPT还是别的模型。
这样划分后,最直接的好处是:换模型或者加模型供应商,只动最外层的AI接入层。我们后来接入了第二家模型服务商的时候,只新增了一个网关适配实现和一套Prompt模板,核心用例一行没改,这个验证让我彻底相信这套方向的正确性。
3.2 核心业务定义成"纯逻辑",让模型只能看到协议
分层之后最关键的问题是:哪一层可以暴露给模型?
我的答案是:**只暴露用例层定义的协议和实体层定义的元数据,绝不暴露内部实现。**模型看到的是"工具描述、参数Schema",而不是"这个工具内部调用了哪个类、哪个接口、哪个库"。这就像餐厅给顾客的是菜单,而不是后厨的电路图。
为了确保这个目标,我们给整个系统添加了一条架构红线,依赖规则检查器会在CI/CD阶段自动校验:**任何适配器层或AI接入层的代码不得反向导入用例层以外的包,任何工具描述不得引用实现类名称。**一开始团队成员觉得这个限制太死,但后来发现它倒逼我们把工具描述写得足够语义化,反而提升了模型选择的准确率。
与之配套的,我把核心用例层的逻辑尽量写成"纯逻辑"。什么叫纯逻辑?就是不依赖具体模型、不依赖外部服务、不依赖数据库读写,只做业务规则判断和状态流转的那部分代码。比如"用户是否有权限调用某个工具"是一个纯逻辑,"根据工具执行结果判断是否需要降级"也是一个纯逻辑。把这些逻辑和外部系统隔离之后,它们可以被单元测试全覆盖,不受到模型输出波动的影响。
3.3 Tool Adapter:插件到工具的适配器模式
这是整个改造中最实用也最出活的一个模式。老系统里有几十个插件,我不想全部推翻重写,于是设计了一个Tool Adapter模式,把原有的插件包装成新架构里的工具。
class BaseToolAdapter(ABC): """工具适配器基类:将内部执行逻辑包装为模型可用的工具""" @property def metadata(self) -> ToolMetadata: """返回工具元数据,供模型做工具选择""" ... @property def parameters_schema(self) -> dict: """返回参数JSON Schema,供模型做参数生成""" ... def validate_and_normalize(self, raw_params: dict, session_ctx: SessionContext) -> dict: """参数校验与规范化,将模型生成的参数转换为内部接口可用的格式""" ... def execute(self, params: dict, session_ctx: SessionContext) -> ToolResult: """核心执行逻辑,内部可以委托老插件的实现""" ... def describe_example(self) -> str: """返回一两个使用示例,帮助模型理解合适调用场景"""每个老插件要接入新架构,只需要实现这个适配器的四个能力:描述自己、定义参数、校验参数、执行外部调用。原来的插件类不需要改动,只需要写一个新的适配器类包装它。这个模式我印象里一个普通的插件迁移半天到一天就能搞定,真正动代码的地方其实很少。
参数校验这一步我遇到过不少坑。模型生成的参数在语义上是合理的,但格式上经常不达预期。比如模型返回了{"start_date": "2024-07"},而内部接口要求{"startDate": "20240701"}。校验和规范化这一步要做的不是简单强转,而是要仔细设计转换逻辑和错误提示,因为转换失败后,框架要给模型返回一个结构化的错误信息,指导模型重新生成参数。这个错误信息写得好不好,直接影响模型的重试成功率。
3.4 会话状态与会话上下文:一个有界上下文问题
智能体系统区别于传统接口服务最大的设计难题,就是会话状态。传统REST接口是无状态的,服务端不关心请求之前发生了什么;但智能体的一次对话,天然是一个有状态的过程,模型要能记住前面说了什么、做过了什么。
我们采用了一个"会话有界上下文"的方案:每个会话维护独立的上下文窗口,内部包含对话历史、工具执行记录、临时数据存储、以及当前状态机信息。上下文不再放在模型侧,而是放在我们自己的系统中,每次请求时只把必要的上下文摘要注入给模型。这有几个好处:
- 可控的上下文长度。模型输入有token限制,我们不能无限塞历史记录,所以上下文总线会自动执行摘要压缩。
- 可控的数据安全。上下文存储在内部,不会被动地全部落到第三方模型服务商的请求里。
- 可控的记忆策略。用户可以随时重置会话,"忘了之前的吧",系统只需清空上下文总线。
会话上下文总线是适配器层的一个核心组件,但它读取和写入的数据结构定义在实体层。这样领域层知道"会话里有什么",但不知道"会话数据存在Redis还是数据库里"。我个人的体会是,这个边界如果没划分好,后面很容易出现"为了给模型塞更多上下文,把数据库表结构都暴露了"的尴尬局面。
4. 迁移路径:如何在老系统上逐步完成改造而不推倒重来
架构改造最怕的就是"推倒重来"。团队积累了三年多的业务逻辑,插件资产丰富,直接重写意味着要冒着巨大的回归风险和时间成本。我的经验是,把迁移当成一连串增量重构,每一步都可验证、可回滚,最终在不停止业务的情况下完成架构升级。
4.1 第一步:找出核心业务边界,先划网线再改代码
我做的第一件事不是写代码,而是画了一张"依赖边界图"。把老系统里所有模块走一遍,标记出:
- 哪些是核心业务逻辑(稳定、不值钱,不该被替换的逻辑);
- 哪些是对接外部系统的适配代码(易变、容易因外部接口变化而变化的部分);
- 哪些是已经被AI调用直接影响的入口(比如原来被主流程调用的插件,未来会被模型间接调用)。
这张图只用了大概两天时间,但价值极大。它让我们第三周就发现,有几个老插件之间存在"隐形的共享状态"——它们通过一个全局缓存互相读写数据。这种耦合如果不提前识别,直接套适配器模式会埋下大雷。所以迁移第一步一定要先花时间认清边界,不要急着写代码。
之后我按"内聚性"把老模块重新分组。核心业务逻辑换到新的用例层和实体层,外部对接代码留在适配器层,中间用接口隔开。这一步不改行为、不换技术栈,只做代码搬家+接口抽象。我在实际操作中会给这批接口写合约测试,保证搬家前后行为一致。合约测试的花费在我看来是最值得的一笔成本,后续大规模改造时,没有它兜底我根本不敢动。
4.2 第二步:把最痛的三个工具做适配器
完善边界图之后,我建议不要一次性把所有插件都迁移到Tool Adapter,而是选最痛的三个工具做试点:
- 使用频次最高的那个;
- 出错率最高的那个;
- 被模型调错次数最多的那个(如果已经接了模型)。
为什么选三个而不是全部?因为迁移本身需要迭代优化的,特别是参数校验逻辑和错误反馈信息的设计,需要真实流量来检验。我选的那三个工具改造之后效果很明显:一个原来经常被模型传错参数的工具,参数required字段和枚举值描述改清晰之后,调用成功率从60%多提升到了90%以上。
做完试点之后,再逐步把其他插件按优先级全量迁移。每个迁移完成都走同一套代码审查和测试流程。三个月后我们完成了全部核心工具的新架构改造,老插件的实现类全部只作为适配器内部的委托对象存在,不再直接被主流程调用。
4.3 第三步:引入AI网关,把模型路由和协议转换收口
全量迁移完毕后,有一件事是绕不开的:需要一个统一的模型访问入口。我把它叫做AI网关(AI Gateway),在整洁架构里属于AI接入层的核心组件。
AI网关的职责有三块:
- 模型路由:根据任务类型(文本生成、工具调用、图像理解等)选择不同的模型。比如工具调用类的指令路由到逻辑能力更强的模型,闲聊类的对话路由到性价比更高的模型。
- 协议统一:各家模型API的请求格式不一样,网关统一适配,对上层屏蔽差异。
- 安全与限流:模型请求的鉴权、Rate Limit、内容安全过滤,全部在网关层做。
做好这个网关之后,替换模型或者增加供应商就会变得非常丝滑。我们在中期做过一次大模型版本切换,仅仅是调整了网关路由配置和Prompt模板参数,用例层完全无感知。这也再次验证了依赖倒置在设计上的价值——业务不依赖具体模型,依赖的是抽象的"模型接口"。
4.4 第四步:清理技术债,消灭跨层依赖
迁移过程中,你会不断发现老系统遗留的各种跨层依赖。比如某个适配器内部偷偷用了实体层未暴露(但因为是同一个运行进程所以能堆到)的内部状态;或者某个用例层的函数要加日志,居然直接调用了日志框架写文件。
我的做法是:每发现一例,就记录到"架构债清单"里,按严重程度排优先级,每迭代清掉一批。其中最需要优先清理的是下面两类跨层依赖:
- 直接依赖具体实现的静态方法调用。比如在用例层里面直接调用了一个适配层的工具类的静态方法,这会让依赖关系彻底乱套。
- 共享可变全局状态。模型编排导致的并发调用比传统系统更频繁,多个工具并行执行时对同一全局缓存的读写,会产生非常难盘的竞态问题。
清理技术债没有捷径,唯一的办法是持续重构+自动化校验。我在CI里加入了一个"分层依赖检查"步骤,每次提交代码自动扫描依赖方向,凡是违反边界规则的都Fail掉。半年后再看,成员的架构自律性明显提升,新代码基本能一次通过依赖检查。
5. 实测中容易翻车的细节清单
架构层面的设计是一回事,真实运行中的问题又是另一回事。这一节我想集中写一写实测中踩过的坑,都是网上文档里很少提到的细节。
5.1 超时与流式响应的架构问题
传统的插件调用一般是同步的,快则几毫秒慢则几秒。但接了大模型之后,一次"模型选工具->执行工具->模型汇总结果"的完整链路,可能耗时几十秒甚至几分钟。如果工具本身执行就要很久(比如查一个几千万行的报表),整个链路对用户来说就是不可接受的。
我们做了两个调整:
- 异步化。所有工具执行拆分为"提交->执行->轮询/回调"三段,模型拿到的是执行状态而不是阻塞等待。执行过程中模型可以继续做别的动作,比如先回答用户一个简单问题,再汇报耗时任务的结果。
- 流式输出。模型在汇总阶段如果是逐字生成,架构要把中间结果流式推给前端。这里有个容易被忽略的问题:后端工具执行结果很长时,不能全部塞给模型去重新生成,而是要先做信息抽取和摘要压缩,再进模型的上下文。
这两种调整在适配器层做,不碰用例层和实体层。我们为此给工具执行接口增加了一个AsyncToolExecutor的实现,支持提交任务、查询状态、取消任务。这个能力在传统插件化架构里几乎是完全缺失的。
5.2 模型返回格式校验不能只靠Prompt
很多团队刚开始接大模型,会让模型返回JSON,然后在Prompt里写"请严格返回JSON格式"。这个做法在演示环境没问题,生产环境一定会翻车。模型偶尔会返回带Markdown代码块标记的JSON、带多余注释的JSON、甚至直接给出一个人类才看得懂的"伪JSON"。
我们的对策是三层校验:
- 格式修复层。尝试从模型输出中提取JSON片段(去掉首尾的代码块标记,处理多余逗号等),用宽松解析器尽量还原。
- Schema校验层。还原后的JSON对标准JsonSchema校验,不通过的返回结构化错误给模型,告诉它"哪个字段类型不对,请重新生成"。
- 语义校验层。字段值和业务规则冲突时,比如日期范围倒挂、金额为负数,这里拦截住并触发一个"模型重试"或"人工介入"的分支。
这三层全部在AI接入层实现,用例层拿到的已经是校验通过、语义合理的数据。我特别想说一句:模型输出的校验比用户输入校验还要重要,因为用户乱输入影响的是单个请求,模型乱输出影响的是它后续所有关联工具的执行。
5.3 工具失败后的降级策略
模型调用的工具如果执行失败,不能像传统REST接口那样直接返回一个500就完事。因为模型拿到错误后,要么会一遍遍地重试同样的操作,要么会编造一个"我假装成功"的结果来取悦用户——这两种都是灾难。
我的经验是,工具适配器在失败时必须返回结构化的、带有补救建议的错误信息。比如:
{ "status": "FAILED", "error_code": "UNAUTHORIZED", "message": "当前账号无权限访问营销报表", "model_suggestion": "请告知用户没有权限,并引导联系管理员开通。不要尝试其他工具获取相同数据。", "retry_allowed": false }model_suggestion字段是我们后期加上的,非常管用。它直接告诉模型"接下来应该怎么回应用户",极大降低了模型在错误分支上的自由发挥概率。实际操作中,retry_allowed=false的失败绝大多数场景会让模型转向"告知用户+建议下一步动作",而不是陷入无意义的自动重试循环。
当然还有一些失败是可以重试的,比如外部服务超时、限流。我们会标记retry_allowed=true,并建议"等待2秒后重试"。此处的重试逻辑放在适配器层内部做,不让模型自己来决定"要不要重试",因为模型对限流和超时的理解远不如一个退避算法可靠。
5.4 可观测性改造:别等出事了才想起看日志
传统系统的日志通常按"请求维度"组织,但智能体系统的日志必须按"会话维度"和"决策链路维度"组织。一次会话里可能发生了多次模型调用、工具执行、参数修正、重试回滚,如果日志没有关联ID贯穿始终,事后复盘会非常痛苦。
我设计了一个Event Log模型,每个会话的所有关键事件都写成一串时间戳事件流,包含:
- 用户的每次输入(脱敏后);
- 模型每次调用的Prompt模板和输出摘要;
- 工具选择的候选列表和最终选择结果;
- 工具执行的参数、耗时、返回状态;
- 校验错误和修正过程;
- 降级或回滚触发的决策链。
配合可视化工具,我们可以在事后重放某次会话的完整"决策路径"。有一次客户投诉模型做了一笔错误操作,重放之后发现,问题根源是一个工具的参数描述里有个可选项没说明默认值,模型选了一个意外的默认值导致结果偏差。这种问题不看链路日志根本定位不了,只能在整个架构的可观测性上做文章。
说到最后,AI时代的架构设计没有标准答案,但有一个方向我认为是确定的:**不要让模型直接操控你的业务逻辑,而是给它一条受控的、可观察的、可回滚的通道。**整洁架构提供的方法论——依赖倒置、边界隔离、核心稳定——放在智能体系统里不是过时了,而是更有必要了。我自己在这个改造过程中最大的体会是,真正有价值的不是某个具体的框架或技术,而是把"模型不可控"当作默认前提去设计系统的每一个决策。架构师的功课,就是在这个充满不确定性的世界里,给业务划出一条尽可能稳定的底线。