一个 while 循环而已?—— 为什么我用 DDD 来构建企业级 AI Agent
系列「企业级 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 框架的组件抽象(ChatModel、Tool、Retriever)也是同样的思路——通过接口隔离具体实现,让业务逻辑对底层 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 提供了 ChatModel、Tool、Retriever 等组件抽象和开箱即用的 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 的状态机怎么设计
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)