从 ERP 系统出发,我是如何设计一套 LLM 多 Agent 系统的
本文以一个真实的电商 ERP 项目为背景,分享从架构设计到代码落地的全过程。
背景:一个「人工操作泥潭」
做电商的团队每天都在处理这样的场景:
- 销售订单审核通过 → 运营手动查库存 → 叫仓库发货 → 库存不够再去催采购 → 采购找供应商…
这条链路涉及 4 个角色、5 个系统,全靠人在中间传话。一旦流量上来,就是噩梦。
我们的 ERP 系统本身已经有完整的服务层 API:仓库库存、物流渠道、采购申请、供应商管理,数据能力完全够用。缺的是一个能把它们串联起来、会自己做决策的「大脑」。
这就是为什么我开始研究多 Agent 系统。
一、为什么选择多 Agent,而不是单个 LLM?
一开始我也想直接让 GPT 接一个大 Prompt,把所有工具塞进去,让它自己搞定。很快遇到问题:
| 问题 | 单 LLM + 大工具集 | 多 Agent |
|---|---|---|
| 工具太多,Prompt 爆炸 | 容易 | 每个 Agent 只关注自己领域 |
| 追踪问题困难 | 黑盒 | 每个 Agent 独立日志和步骤 |
| 并发执行 | 串行 | 多 Agent 可并行 |
| 职责边界 | 模糊 | 清晰,便于维护 |
核心思路:把复杂问题拆解给「专家团队」,而不是指望一个全能助手。
二、系统架构设计
整体分为四层:
┌─────────────────────────────────────────┐
│ 入口层(HTTP / 事件) │
│ 自然语言对话 / 订单事件 / 低库存告警 │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Orchestrator Agent(主控) │
│ LLM 规划器 → ReAct 循环 → 结果汇聚 │
└──────────────────┬──────────────────────┘
│ 工具调用
┌──────────────┼──────────────┐──────────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌─────────┐ ┌────────┐ ┌─────────┐
│ 库存 │ │ 物流 │ │ 采购 │ │ 供应商 │
│ Agent │ │ Agent │ │ Agent │ │ Agent │
└───────┘ └─────────┘ └────────┘ └─────────┘
│ │ │ │
┌───────────────────── 工具层(ERP API 映射)─────────────────────┐
│ WarehouseStockService OutOrderService PurchaseApplyService │
│ LogisticsChannelService SupplierService │
└────────────────────────────────────────────────────────────────┘
关键设计决策
Orchestrator 不直接操作业务 — 它只负责「想清楚该做什么、叫谁做」,具体执行全部委托给专业 Agent。这样主控的 System Prompt 可以保持精简,LLM 的推理质量更高。
专业 Agent 工具集小且内聚 — 库存 Agent 只认识 get_sku_stock、batch_get_stock,不会看到物流工具。这极大降低了 LLM 工具选错的概率。
两种调用模式共存 — 每个专业 Agent 同时提供:
- LLM 驱动模式:通过
execute()接入 ReAct 循环,供自然语言场景使用 - 快捷方法:如
queryAndDecide()/createApplyDirect(),确定性场景直接调用,不过 LLM,节省 Token
三、核心机制:ReAct 循环
ReAct = Reasoning(推理)+ Acting(行动),是目前主流 Agent 框架的基础范式。
工作流程
输入
|
▼
[LLM Thought] ← 我现在知道什么?下一步该做什么?
|
▼
[Tool Call] ← 调用工具(查库存、创建订单...)
|
▼
[Observation] ← 工具返回结果
|
▼
[重复] ← 直到 LLM 判断任务完成
|
▼
[Final Answer] ← 汇总输出
代码实现(核心 50 行)
// src/core/llm-client.ts — 简化版
async runReActLoop(
messages: ChatMessage[],
tools: ToolDef[],
onStep?: (step: AgentStep) => void,
): Promise<string> {
const history = [...messages];
let step = 0;
while (step < this.maxSteps) {
// 1. 让 LLM 决定下一步
const response = await this.client.chat.completions.create({
model: this.model,
messages: history,
tools: this.buildOpenAITools(tools),
tool_choice: 'auto',
});
const msg = response.choices[0].message;
history.push(msg);
// 2. 没有工具调用 → 得到最终答案
if (!msg.tool_calls?.length) {
return msg.content ?? '';
}
// 3. 并行执行所有工具调用(一轮可能调多个)
const results = await Promise.all(
msg.tool_calls.map(tc => this.executeTool(tc, tools))
);
// 4. 把结果作为 Observation 追加进历史
history.push(...results);
step++;
}
return '已达到最大步数限制';
}
关键细节:
- 每轮 LLM 可能返回多个工具调用(如同时查 3 个 SKU 的库存),全部并行执行
- 历史记录完整保留,LLM 始终能看到"它之前做了什么、结果是什么"
maxSteps防止死循环,通过环境变量MAX_AGENT_STEPS配置
四、Agent 桥接层的设计
这层是我觉得设计最精妙的部分,也是最容易忽视的。
问题:Orchestrator 调用的是 LLM 工具(ToolDef),但各专业 Agent 是 TypeScript 类,怎么连接?
解法:src/tools/agent-tools.ts — 把每个专业 Agent 的能力包装成 Orchestrator 的工具函数。
// 库存查询工具(连接 Orchestrator -> InventoryAgent)
{
name: 'query_inventory',
description: '查询 SKU 库存,返回可用数量和充足/不足/缺货状态',
parameters: { /* JSON Schema */ },
execute: async (args) => {
// 直接调用 InventoryAgent 的快捷方法(不走 LLM,确定性查询)
const decisions = await Promise.all(
skuCodes.map(sku => inventoryAgent.queryAndDecide(sku, requiredQtys?.[sku]))
);
return { decisions };
}
}
这样 Orchestrator 的 LLM 看到的是高层语义工具("查库存"),而不是底层 API 调用,推理质量更好。
五、典型流程走读
以「订单审核通过,自动处理」为例,完整流程如下:
用户/系统: POST /api/agent/order/808645/process
│
▼
OrchestratorAgent.processOrder(808645)
│
▼ 构建 Prompt:
"订单 808645 已审核,请检查库存,库存充足则发货,不足则补货..."
│
▼ LLM 第 1 轮思考:
Thought: 先查清楚订单里都有哪些 SKU,以及各 SKU 库存
Action: call query_inventory({ sku_codes: ["SKU-A", "SKU-B", "SKU-C"] })
│
▼ query_inventory 执行(调用 InventoryAgent.queryAndDecide):
← SKU-A: available=50, status=sufficient
← SKU-B: available=2, status=insufficient (需要 10)
← SKU-C: available=0, status=empty
│
▼ LLM 第 2 轮思考:
Thought: SKU-A 够用,需要发货
SKU-B 不够,需要补货 8 件
SKU-C 缺货,需要紧急补货并通知供应商
Action: 并行调用 3 个工具:
- coordinate_logistics (SKU-A → 创建出库单)
- trigger_purchase (SKU-B, deficit=8)
- trigger_purchase (SKU-C, deficit=10, urgent=true)
│
▼ 执行结果:
← 出库单 #20260310-001 已创建,DHL 渠道
← 采购单 #PO-2026-088 已创建,供应商: 广州某某,预计 3 天到
← 采购单 #PO-2026-089 已创建(紧急),供应商: 深圳某某,预计 1 天到
│
▼ LLM 第 3 轮思考:
Thought: SKU-C 紧急缺货,需要同时通知供应商
Action: call notify_supplier({ supplier_id: xxx, is_urgent: true })
│
▼ LLM 最终输出:
"订单 808645 处理完成:
- SKU-A: 出库单 #001 已创建,DHL,预计 3 天送达
- SKU-B: 采购单 #088 已提交,3 天后到货,届时自动发货
- SKU-C: 紧急采购 #089 已提交,1 天到货,供应商已收到催货通知"
六、技术架构总结
技术选型
| 模块 | 选型 | 理由 |
|---|---|---|
| 运行时 | Node.js 20 + TypeScript 5 | 与前端团队技术栈一致,类型安全 |
| LLM 接入 | openai SDK | 标准 Function Calling,支持 GPT/Gemini/Ollama 一键切换 |
| HTTP 服务 | Express | 够用,无需引入复杂框架 |
| 日志 | pino | 结构化 JSON,生产友好 |
| HTTP 客户端 | axios | 对接现有 ERP API |
接入 LLM 只需改 .env
# OpenAI
OPENAI_API_KEY=sk-xxx
LLM_MODEL=gpt-4o-mini
# Google Gemini(直接兼容)
OPENAI_API_KEY=AIza-xxx
LLM_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
LLM_MODEL=gemini-1.5-pro
# 本地 Ollama(零成本)
OPENAI_API_KEY=ollama
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL=llama3
七、我学到的几个教训
1. System Prompt 要有决策规则,不能只描述角色
❌ 弱 Prompt:你是一个库存助手,帮用户查库存。
✅ 强 Prompt:库存充足(available >= required)时触发物流协调;不足时触发采购;为零时同时触发采购并通知供应商。
2. 工具描述比代码更重要
LLM 选工具靠的是 description 字段。描述越清晰、越有歧义消除,工具选错的概率越低。反复测试和优化工具描述能显著提升准确率。
3. 先做快捷方法,再做 LLM 版本
对于确定性流程(如"查库存并判断充足/不足"),先用纯代码实现 queryAndDecide(),稳定后再套 LLM。这样既能保证核心逻辑可测试,又给 LLM 提供了可靠的工具基础。
4. 并发工具调用能大幅提速
OpenAI Function Calling 一轮可以返回多个工具调用,用 Promise.all 并行执行,可以把多步串行操作压缩到 2-3 轮 LLM 调用。
结语
这套架构目前还在接入真实 ERP 接口的过程中。但框架本身已经跑通,LLM 的多轮推理逻辑也经过验证。
如果你的系统也有一堆已有 API、但缺乏跨模块自动协作能力,这个思路应该适用。