系列「企业级 AI Agent 实现拆解」第一篇。本系列从真实生产代码出发,逐层拆解一个企业级 Agent 平台的设计与实现。


从一段"骗人"的伪代码说起

网上搜"如何实现 AI Agent",满屏都是这样的代码:

def run_agent(user_input):
    messages = [{"role": "user", "content": user_input}]
    while True:
        response = llm.chat(messages)
        if response.tool_calls:
            for call in response.tool_calls:
                result = execute_tool(call)
                messages.append(result)
        else:
            return response.content

看着很美。ReAct 论文的精髓确实就这几行:Reason → Act → Observe,循环到出答案为止

作为验证原型,这段代码两小时就能跑通,逻辑上也挑不出毛病。问题在于,它只解决了"Agent 能不能工作",完全没有回答"Agent 能不能上生产"。


从 demo 到生产,差的不止一步

把这段代码放到真实的企业需求面前,立刻就会暴露一连串的缺口:

“多个客户同时用,数据会串吧?”
while 循环里没有租户隔离的概念,所有请求共享同一个内存空间。

“Agent 调外部 API 花了多少钱,谁来出?”
token 计费、配额管理——完全没考虑。

“Agent 要删数据库的话,能拦一下吗?”
不能。循环一旦跑起来,没有任何拦截机制。

“出了事,怎么查它当时干了什么?”
日志有,但没有结构化的审计链,无法追溯每一步决策。

“跑到一半服务挂了,能接上吗?”
不能。状态全在内存里,进程一死就全丢了。

五个问题,五个缺口。这不是代码写得不好,而是这段代码从一开始就不是为企业环境设计的。

这不是我一个人的困境。开源社区里,Eino(github.com/cloudwego/eino这样的 Go 框架已经把 ReAct 循环、Tool 调用、多 Provider 适配做成了开箱即用的组件——但框架解决的是"怎么跑起来"的问题。多租户隔离、审计合规、预算护栏、中断恢复这些企业级需求,不是任何一个框架能替你包办的,它们是架构层面的事。这也是我写这个系列的原因:不是教你跑通 demo,而是拆解那些"上生产之后才会遇到的真实问题"。


企业场景到底复杂在哪

Agent 在企业里面临的挑战,不外乎三个维度:

安全与合规——这是红线,碰不得

  • 多租户数据隔离:行级 RLS(Row-Level Security),一条记录都不能跨租户泄漏
  • 工具调用前的人工审批:高危操作必须 Human-in-the-Loop,谁执行、谁批准,全程留痕
  • 不可篡改的审计日志:append-only,月分表,支持归档和合规审查
  • 预算护栏:超配额自动中断,而不是让费用像水龙头一样哗哗流

可靠性——这是底线,崩不起

  • 会话状态持久化:断电、重启、容器漂移,状态不能丢
  • 中断恢复:Agent 等人工审批的时候,进程可以安全重启,审批通过后无缝继续
  • 工具调用失败的降级:不是一挂全挂,而是有重试、有 fallback

可观测性——这是眼睛,看不见就管不了

  • 每一轮 LLM 调用的 token 消耗和延迟
  • 工具调用的成功率、失败率、P99 耗时
  • 从用户提问到最终答案的完整链路追踪(Trace)

这些需求叠在一起,已经不是几个 if-else 能糊弄的了。你需要的是架构


DDD 怎么把这些复杂度"装"进去

领域驱动设计(DDD)最有价值的地方不是什么高大上的概念,而是它给了你一个分层的铁纪律

┌─────────────────────────────────────────────┐
│  interfaces/          协议适配层             │
│  REST handler · gRPC server · SSE streamer  │
├─────────────────────────────────────────────┤
│  application/         用例层                 │
│  RunTurn · ResumeInterrupt · ExpireInterrupt│
├─────────────────────────────────────────────┤
│  domain/              领域层(零外部依赖)    │
│  Session · Interrupt · Message · Events     │
├─────────────────────────────────────────────┤
│  infrastructure/      基础设施层             │
│  PostgreSQL · LLM client · Hook runner      │
└─────────────────────────────────────────────┘

最关键的一条规则:领域层零外部依赖

Session(会话聚合根)只关心一件事:这次对话的状态是什么、允许怎么流转。它不知道 Postgres 长什么样,不知道 HTTP 是什么协议,也不知道外面跑的是 GPT-4o 还是 DeepSeek。Eino 框架的组件抽象(ChatModelToolRetriever)也是同样的思路——通过接口隔离具体实现,让业务逻辑对底层 provider 完全无感。区别在于,DDD 把这个思路从单个组件扩展到了整个服务架构。

// domain/model/session.go · 状态机核心
var validTransitions = map[State]map[State]bool{
    StateRunning: {
        StateWaiting:   true,  // 触发 HITL 中断
        StateCompleted: true,  // 得到最终答案
        StateFailed:    true,  // 出错
        StateCancelled: true,  // 用户取消
    },
    StateWaiting: {
        StateRunning:   true,  // 人工审批通过,继续
        StateCancelled: true,  // 人工拒绝,终止
    },
    // completed / failed / cancelled 是终态,不允许再转移
}

这 10 行代码把"Agent 在什么状态下允许做什么"说得明明白白。任何非法的状态跳转会立刻报错,绝不会静默地搞出什么奇怪的副作用。

每个关注点待在自己该待的层,井水不犯河水:

关注点 住在哪里 为什么
租户隔离 infrastructure/persistence PostgreSQL RLS 是存储层的职责
工具审批 domain/model Interrupt 是业务规则,不是技术细节
审计日志 infrastructure/outbox Outbox 模式发事件,保证最终一致性
token 计费 application/command RunTurn 用例收集 usage,聚合到账单
状态恢复 domain/repo SessionRepo 接口在 domain 层定义,pg 实现分离

整体架构:10 个限界上下文,各司其职

一个完整的企业级 Agent 平台,按职责拆成独立服务:

                    ┌──────────────────┐
         用户请求 → │   gateway :8080  │
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼              ▼              ▼
       ┌─────────────┐ ┌──────────┐ ┌────────────┐
       │ agent-runner│ │    kb    │ │  llm-gw    │
       │   :8085     │ │  :8088   │ │   :8090    │
       │  ReAct 引擎 │ │ 向量检索 │ │ LLM 路由  │
       └──────┬──────┘ └──────────┘ └────────────┘
              │
   ┌──────────┼──────────┐
   ▼          ▼          ▼
┌──────┐ ┌────────┐ ┌────────┐
│ hook │ │  tool  │ │ audit  │
│:8092 │ │  :8086 │ │ :8087  │
│钩子链│ │工具调度│ │审计日志│
└──────┘ └────────┘ └────────┘

每个服务就是一个限界上下文(Bounded Context)——拥有独立的数据库 schema、独立的领域模型,服务间通过 gRPC 通信,不共享代码,不共享表。

实际收益很直接:

  • Agent 引擎换 LLM Provider?只改 llm-gateway 配置,一行业务代码不动
  • 审计策略升级?只改 audit 服务,其他服务完全无感知
  • 新增一种工具沙箱?只改 tool-broker,Agent 引擎不用改

在 Agent 编排层面,我们选择了 CloudWeGo Eino 作为底层agent框架。Eino 提供了 ChatModelToolRetriever 等组件抽象和开箱即用的 ReAct Agent 实现,它的 ADK(Agent Development Kit)还内置了 interrupt/resume 机制——这和我们在 DDD 领域层设计的 HITL 中断模式天然契合。Eino 解决的是"怎么优雅地把 LLM、工具、记忆编排在一起跑起来"的问题;DDD 解决的是"怎么让这套编排跑在企业级的安全、合规、多租户约束下"的问题。两者互补,不冲突。


这个系列讲什么

接下来的每篇文章,都会从真实的生产代码出发,拆解一个核心机制。不是伪代码示意,是跑过的真代码:

  • 第二篇:Session 聚合根 —— Agent 的状态机怎么设计
  • 第三篇:ReAct 循环的 50 行 Go 实现,逐行拆解
  • 第四篇:HITL 中断 —— 让人类随时叫停 AI 的正确姿势
  • 第五篇:SSE 实时推流 —— Token 怎么一个个蹦出来
  • 第六篇:Hook 系统 —— 插件化安全护栏的设计
  • 第七篇:工具调用 —— Agent 的手和眼
  • 第八篇:多 LLM Provider —— 不改一行业务代码换模型

下一篇:Session 聚合根 —— Agent 的状态机怎么设计

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐