← 全部示例
// 连接你的技术栈

Express 客服 API

通过 HTTP 接收客服工单,执行固定的「分类 → 起草 → QA」流水线,再返回经 schema 校验的 JSON。

适合希望把可重复 AI 工作流嵌进后端 API 的开发与客服运营团队

01 使用情境

生产级客服接口不能只产出一段看似合理的回复。输入必须校验,步骤之间要有可预测的数据结构,失败要映射成明确的 HTTP 行为,而且每次请求都不该重新推导同一套拓扑。

一次 POST /tickets 请求

JSON body 包含工单标题与正文;Express 路由会在任何模型调用前拒绝格式错误或字段缺失的输入。

02 工作方式

一支分工明确、交接清楚的团队。

每个专职 Agent 只处理它需要的证据;互不依赖的工作同时进行,需要上游结果的工作则按顺序等待。

  1. 给工单分类

    第一项任务返回经过 schema 校验的类别与紧急程度。

    分类 Agent

    判断问题类别并标记紧急程度。

  2. 起草回复

    起草 Agent 通过显式任务依赖拿到原始工单和分类结果。

    客服起草 Agent

    撰写有同理心、面向客户的回复。

  3. 返回前复核

    最后一步结合原始工单与分类结果检查回复。

    QA 复核 Agent

    检查语气、同理心与事实一致性。

03 最终结果

带类型的 HTTP 响应

接口组装三个结构化结果,并把输入错误、流水线失败和超时映射成明确状态码。

  1. 01类别与紧急程度
  2. 02面向客户的回复草稿
  3. 03QA 备注,以及 400 / 502 / 504 错误行为
  • 固定的 runTasks() DAG 让高频接口保持可预测。
  • Zod schema 让每次交接都能直接被应用代码消费。
  • 独立 QA 步骤会在回复离开流水线前完成检查。
范围与限制
这个可克隆运行的应用展示 API 与编排边界;它没有连接真实 CRM、订单系统或工单平台,也不会代替客服执行账户操作。
04 开发者实现

先运行,再查看具体实现。

上面是面向业务的解释;下面的命令、运行前提、API 和源码继续与真实仓库同步。

实现信息OpenMultiAgentAgentConfigSupportedProvider
连接你的技术栈235 行

运行

在仓库的克隆里运行这个文件:

terminal
cd packages/core/examples/integrations/express-customer-support
npm install
export DEEPSEEK_API_KEY=sk-... # default — all three agents use DeepSeek
npm start
前置条件
  • DEEPSEEK_API_KEY for the default provider configuration.
展开完整同步源码 · 235 行

完整示例,从固定的 Framework commit 同步。

integrations/express-customer-support/index.ts
/**
* Express Customer Support
*
* POST /tickets { subject, body } → runs a three-agent pipeline
* (classifier → drafter → QA reviewer) and returns structured JSON.
*
* Run:
* npm install && npm start
*
* Prerequisites:
* API key for the chosen provider(s) — defaults to DEEPSEEK_API_KEY.
* CLASSIFIER_PROVIDER / DRAFTER_PROVIDER / QA_PROVIDER (optional, default 'deepseek')
* CLASSIFIER_MODEL / DRAFTER_MODEL / QA_MODEL (optional, see defaults below)
* PORT (optional, default 3000)
*/
 
import { fileURLToPath } from 'node:url'
import express from 'express'
import { z } from 'zod'
import { OpenMultiAgent } from '@open-multi-agent/core'
import type { AgentConfig, SupportedProvider } from '@open-multi-agent/core'
 
// ---------------------------------------------------------------------------
// Schemas
// ---------------------------------------------------------------------------
 
const ClassifierOutput = z.object({
category: z.enum(['billing', 'technical', 'shipping', 'returns', 'general']),
urgency: z.enum(['low', 'medium', 'high', 'critical']),
})
 
const DrafterOutput = z.object({
draft_reply: z.string().describe('Polished customer-facing reply'),
})
 
const QAOutput = z.object({
qa_notes: z.string().describe('Tone and accuracy feedback for the draft'),
})
 
export const SupportTicketResponse = z.object({
category: ClassifierOutput.shape.category,
urgency: ClassifierOutput.shape.urgency,
draft_reply: DrafterOutput.shape.draft_reply,
qa_notes: QAOutput.shape.qa_notes,
})
export type SupportTicketResponse = z.infer<typeof SupportTicketResponse>
 
// ---------------------------------------------------------------------------
// Provider / model configuration
// ---------------------------------------------------------------------------
// Each agent's provider and model are independently overridable via env vars,
// so free-tier users can mix providers per tier. Validate at startup to fail
// fast instead of erroring deep inside an HTTP request's LLM call.
 
const PROVIDER_ENV_KEYS: Record<string, string> = {
anthropic: 'ANTHROPIC_API_KEY',
openai: 'OPENAI_API_KEY',
gemini: 'GEMINI_API_KEY',
grok: 'XAI_API_KEY',
copilot: 'GITHUB_TOKEN',
deepseek: 'DEEPSEEK_API_KEY',
minimax: 'MINIMAX_API_KEY',
'azure-openai': 'AZURE_OPENAI_API_KEY',
}
 
function pickAgent(envPrefix: string, defaultProvider: SupportedProvider, defaultModel: string) {
const provider = (process.env[`${envPrefix}_PROVIDER`] ?? defaultProvider) as SupportedProvider
const model = process.env[`${envPrefix}_MODEL`] ?? defaultModel
const envKey = PROVIDER_ENV_KEYS[provider]
if (envKey && !process.env[envKey]?.trim()) {
console.error(`Missing ${envKey}: required for ${envPrefix}_PROVIDER="${provider}".`)
process.exit(1)
}
return { provider, model }
}
 
const classifierCfg = pickAgent('CLASSIFIER', 'deepseek', 'deepseek-v4-flash')
const drafterCfg = pickAgent('DRAFTER', 'deepseek', 'deepseek-v4-pro')
const qaCfg = pickAgent('QA', 'deepseek', 'deepseek-v4-pro')
 
// ---------------------------------------------------------------------------
// Agents
// ---------------------------------------------------------------------------
 
const classifier: AgentConfig = {
name: 'classifier',
provider: classifierCfg.provider,
model: classifierCfg.model,
systemPrompt: 'You are a customer support classifier. Given a ticket subject and body, classify it into exactly one category (billing, technical, shipping, returns, general) and one urgency level (low, medium, high, critical). Respond ONLY with valid JSON: {"category":"<one of the above>","urgency":"<one of the above>"}.',
outputSchema: ClassifierOutput,
maxTurns: 3,
temperature: 0.1,
}
 
const drafter: AgentConfig = {
name: 'drafter',
provider: drafterCfg.provider,
model: drafterCfg.model,
systemPrompt: 'You are a customer support specialist. Your task prompt will contain the original support ticket and a "Context from prerequisite tasks" section with the classifier\'s JSON output (category and urgency). Use both to write a clear, empathetic customer-facing reply. Respond ONLY with valid JSON.',
outputSchema: DrafterOutput,
maxTurns: 4,
temperature: 0.4,
}
 
const qaReviewer: AgentConfig = {
name: 'qa-reviewer',
provider: qaCfg.provider,
model: qaCfg.model,
systemPrompt: 'You are a QA reviewer for customer support. Your task prompt will contain a "Context from prerequisite tasks" section with the classifier\'s category/urgency and the drafter\'s reply. Review the draft reply for tone, empathy, and accuracy against the original ticket. Provide concise QA notes. Respond ONLY with valid JSON.',
outputSchema: QAOutput,
maxTurns: 3,
temperature: 0.2,
}
 
// ---------------------------------------------------------------------------
// HTTP server
// ---------------------------------------------------------------------------
 
export function createApp() {
const app = express()
app.use(express.json())
app.use((err: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof SyntaxError && 'status' in err && (err as { status: number }).status === 400) {
res.status(400).json({ error: 'Invalid JSON body' })
return
}
next(err)
})
 
const orchestrator = new OpenMultiAgent({
onProgress: (event) => {
const agent = 'agent' in event ? event.agent : ''
const extra = event.type === 'error' && 'data' in event ? ` — ${JSON.stringify(event.data)}` : ''
console.log(`[${event.type}] ${agent}${extra}`)
},
})
 
const team = orchestrator.createTeam('support-team', {
name: 'support-team',
agents: [classifier, drafter, qaReviewer],
})
 
app.post('/tickets', async (req, res) => {
const { subject, body } = req.body ?? {}
if (typeof subject !== 'string' || typeof body !== 'string' || !subject || !body) {
res.status(400).json({ error: 'Request body must include non-empty string fields: subject, body' })
return
}
 
// `runTasks` resolves normally on abort (skipRemaining → success: false), so
// race it against a sentinel to distinguish a 60s timeout from a generic
// pipeline failure. Keep the AbortSignal wired through so in-flight LLM
// fetches still get cancelled when the timer fires.
const TIMEOUT_MS = 60_000
const abortController = new AbortController()
const timeoutSentinel = Symbol('timeout')
const timeoutHandle = setTimeout(() => abortController.abort(), TIMEOUT_MS)
const timeoutPromise = new Promise<typeof timeoutSentinel>((resolve) => {
abortController.signal.addEventListener('abort', () => resolve(timeoutSentinel), { once: true })
})
 
try {
const ticketContext = `Subject: "${subject}"\nBody: "${body}"`
const raced = await Promise.race([
orchestrator.runTasks(team, [
{
title: 'Classify ticket',
description: `Classify the following support ticket.\n\n${ticketContext}`,
assignee: 'classifier',
},
{
title: 'Draft reply',
description: `Write a customer-facing reply for the following support ticket.\n\n${ticketContext}`,
assignee: 'drafter',
dependsOn: ['Classify ticket'],
},
{
title: 'QA review',
description: `Review the draft reply for tone, empathy, and accuracy.\n\n${ticketContext}`,
assignee: 'qa-reviewer',
dependsOn: ['Classify ticket', 'Draft reply'],
},
], { abortSignal: abortController.signal }),
timeoutPromise,
])
 
if (raced === timeoutSentinel) {
res.status(504).json({ error: 'Pipeline timed out after 60 seconds' })
return
}
 
const result = raced
if (!result.success) {
res.status(502).json({ error: 'Pipeline did not complete successfully' })
return
}
 
const classifierResult = result.agentResults.get('classifier')
const drafterResult = result.agentResults.get('drafter')
const qaResult = result.agentResults.get('qa-reviewer')
 
const classOut = classifierResult?.structured as z.infer<typeof ClassifierOutput> | undefined
const draftOut = drafterResult?.structured as z.infer<typeof DrafterOutput> | undefined
const qaOut = qaResult?.structured as z.infer<typeof QAOutput> | undefined
 
if (!classOut || !draftOut || !qaOut) {
res.status(502).json({ error: 'One or more agents failed to produce structured output' })
return
}
 
const response: SupportTicketResponse = {
category: classOut.category,
urgency: classOut.urgency,
draft_reply: draftOut.draft_reply,
qa_notes: qaOut.qa_notes,
}
res.json(response)
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err)
res.status(502).json({ error: `LLM pipeline failed: ${message}` })
} finally {
clearTimeout(timeoutHandle)
}
})
 
return app
}
 
// Only start the server when this file is executed directly (e.g. `npm start`).
// Importers like the smoke test get the factory + schema with no side effects,
// so they can bind their own ephemeral port without colliding on PORT 3000.
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const PORT = parseInt(process.env.PORT ?? '3000', 10)
createApp().listen(PORT, () => console.log(`Support API listening on http://localhost:${PORT}`))
}
在 GitHub 查看 / 编辑
// 企业服务

要把它用到生产环境?

open-multi-agent 采用 MIT 许可、可自行免费运行。当你需要在期限内交付、集成,或获得支持时,元定义科技(YuanASI)提供商业交付与支持。

// 直接联系

把 Open Multi-Agent 用进真实业务

联系框架作者本人,帮你梳理 AI 落地目标、让 AI 真正与业务结合

可提供的工程服务
S-01

AI Agent 定制开发

业务梳理、Agent 设计、Prompt 评估、生产部署、私有化与持续支持。

S-02

多智能体系统集成

多 Agent 架构编排、RAG、CRM / ERP / API 对接、性能与稳定性调优。

S-03

企业 AI 咨询

AI 场景评估、技术选型、POC、ROI 估算与落地路线规划。