企业应用架构:给 Agent 接工具,也是在设计业务入口
本文是 Agent 工程里的经典架构 系列的一篇。文中的业务场景和代码为讲解设计,产品能力以所链接的官方资料为准。
以前做管理后台,一个“批准订单”按钮会经过接口、业务服务和数据库。现在把同样的能力交给 Agent,有时却只写一个工具函数,允许模型直接改状态。入口变得更聪明,业务边界反而更薄了。这个变化值得停下来想一想。
企业应用架构模式目录整理了业务逻辑、数据访问、会话状态等常见组织方式。这里不准备逐个介绍所有模式,而是选择服务层、领域模型和数据映射这条关系,讨论一件具体的事:怎样让网页、定时任务和 Agent 遵守同一套订单规则。
我会先把批准订单定义为应用服务提供的操作。入口负责接收请求与确认身份;业务对象判断订单是否允许批准;存储组件负责保存。Agent 工具只是另一个调用入口。这样修改金额上限时,不必同时寻找网页接口、脚本和模型提示词里的三份规则。
代码中的订单对象约束状态转换,应用服务协调读取和保存,仓库用内存字典模拟存储。复制对象是为了防止调用方绕过保存接口,直接改到字典里的实例。它说明职责分配,不代表已经实现了数据库事务、并发控制和完整权限体系。
from dataclasses import dataclass
from copy import deepcopy
@dataclass
class Order:
amount: int
status: str = "pending"
def approve(self, ceiling):
if self.status != "pending" or self.amount > ceiling:
raise ValueError("订单状态或审批金额不满足条件")
self.status = "approved"
class Repository:
def __init__(self):
self.rows = {"O1": Order(8000)}
def get(self, key):
return deepcopy(self.rows[key])
def save(self, key, order):
self.rows[key] = deepcopy(order)
def approve_order(repo, key, ceiling):
order = repo.get(key)
order.approve(ceiling)
repo.save(key, order)
repo = Repository()
try:
approve_order(repo, "O1", 7000)
except ValueError:
pass
assert repo.get("O1").status == "pending"
approve_order(repo, "O1", 9000)
assert repo.get("O1").status == "approved"
print("规则由业务对象执行,拒绝后没有写入状态")
完整示例可下载,使用 Python 3.9 或以上版本运行:python3 example.py。只依赖标准库,不调用真实业务服务。
领域对象并不是一个只能放字段的容器。当它承担“什么情况下可以批准”的判断,状态转换就有了明确入口。与此同时,数据库连接、HTTP 请求和模型调用不必都塞进对象里面。哪些逻辑放在对象,哪些放在应用服务,要根据规则是否属于该实体来决定。
如果业务只是一个简单登记表,几段清楚的事务脚本可能已经足够。为了套模式而创建十几个接口、工厂和空实现,维护成本未必划算。只有当规则在多个用例中反复出现、对象之间有稳定约束时,更完整的领域模型才容易体现价值。架构选择应该回应复杂度,不能主动制造复杂度。
数据映射的意义也不是“必须写一个 Repository 类”。更实际的目标,是让业务规则不依赖表字段、ORM 会话和第三方 SDK 的细节。供应商字段名称变化时,影响可以留在适配层;业务层仍然处理有明确意义的报价对象。测试时也可以用小型替身验证规则,而不必启动完整环境。
生产场景必须补上示例省略的边界。读取到保存之间需要版本校验,身份来自可信认证上下文,金额上限来自审批记录而不是调用方随意传入。操作编号、审计记录和对外副作用还要与事务策略配合。这些能力可以由应用服务统一协调,但不能只靠类名表达已经存在。
与高并发架构的关系,在于拆分后的职责更容易独立治理。数据访问可以优化索引与批量读取,入口可以做限流,应用服务可以做任务编排,领域规则可以保持小而确定。性能优化时也更容易知道自己改变了哪一层,而不会顺手改变业务含义。
工具返回值也应该表达业务状态。相比统一返回一个字符串“失败”,可以区分参数错误、条件冲突、等待审批和结果未知。调用方才能选择修正参数、重新读取、继续等待或转人工。错误分类属于业务接口的一部分,不能完全依赖模型阅读异常堆栈。
测试这些层次时,我会先独立验证规则,再验证应用服务如何协调失败路径,最后检查真实存储适配器的事务行为。内存替身只能帮助检查职责组织,不能证明数据库并发写入也正确。
对于 Agent,我希望工具名称表达业务动作,例如“提交退款申请”,而不只是“执行 SQL”。前者可以附带明确的前置条件、权限与返回状态,后者往往把本应由系统承担的约束交给模型猜测。能够提供多少工具不是目标,让每个工具有清楚的业务承诺,才是长期维护时真正省力的地方。