企业里一接大模型智能体,最扎眼的矛盾就来了:既要 Agent 快速见效,又不能动已经稳定跑了三年的业务主线。可现实常常是对话记录、向量切片、任务流水一股脑塞进主库,模型一超时,整条下单链路跟着抖。
常规做法之所以不够用:在主服务里直接写 LLM 调用、复用主库表存会话、同步等待模型返回,会让依赖膨胀、事务边界被打穿,Agent 每迭代一次就得带着整个系统重新发版。
本文分享一套可直接落地的侧挂式方案:独立 Agent 服务 + 独立数据库 + 主线零侵入的事件契约,前端组件松耦合挂载、随拆随上。
一、架构选型:智能体侧挂而不是嵌入主干
先做一次选型对照,再决定 Agent 到底落在哪一侧:
| 维度 | 嵌入主服务 | 侧挂独立服务 |
|---|---|---|
| 发版节奏 | 随业务系统全量发布 | 独立灰度、独立回滚 |
| 依赖引入 | 主工程引入模型 SDK | 主线仅留一个出站接口 |
| 故障半径 | 模型超时拖垮业务线程池 | Agent 挂掉后主线无感 |
| 数据归属 | 会话与业务表混放 | 独立库独立生命周期 |
- 侧挂定义:Agent 作为旁路服务存在,主线只认「事件进、结果回」两条通道。
- 主线保留:只留一个轻量出站适配器,代码里不出现任何模型 SDK、提示词与解析逻辑。
- 判定标准:把 Agent 目录整个删掉,主线仍能正常下单收款,才算真正零侵入。
核心结论: 把智能体当外部协作方接入,而不是当业务代码的延伸。
二、服务边界:网关与业务域各管一段
拆分之前先把职责切干净,边界模糊比跑得慢更致命:
- 业务服务 :只负责产生事实数据,发出
order.paid之类的领域事件,完全不关心谁来消费。 - Agent 网关 :承接鉴权、会话路由、模型适配与限流,对内统一暴露
POST /agent/run。 - 结果回流:通过回调或事件写回业务侧,主线只按任务号幂等落账,不做二次加工。
事件进 → 网关编排 → 模型执行 → 结果回流 → 主线落账
核心结论: 边界越硬,后期换模型、换向量库的代价就越低。
三、数据隔离:会话与向量落独立库
智能体的数据有自己的生命周期,先列清单再决定建几个库:
| 数据类别 | 归属库 | 保留策略 |
|---|---|---|
| 会话与消息 | agent_db | 90 天滚动清理 |
| 向量与切片 | agent_vector | 随文档版本重建 |
| 任务流水 | agent_db | 按审计要求归档 |
| 业务事实数据 | 原业务库 | 不复制、不冗余 |
- 不跨库事务 :主线与 Agent 之间只追求最终一致,靠
taskId做对账与补偿。 - 只读共享:确需业务上下文时,用只读账号同步一份快照表,严禁 Agent 反向写主库。
核心结论: 数据一旦同库,所谓零侵入就只剩一句口号。
四、零侵入对接:事件驱动与异步回调解耦
主线要的从来不是能力,而是稳定,接入方式直接决定侵入程度:
- 同步直调:模型动辄十几秒,长期占用业务线程池,一次抖动会被放大成全局故障。
- 事件异步:主线发完事件即可返回,Agent 慢、挂、重试都不影响主流程的响应时间。
- 回调幂等 :回写结果必须带
taskId,上游重复投递时按状态机去重,避免重复落账。
领域事件 → 消息队列 → Agent 消费 → 执行任务 → 回调主线 → 幂等落库
核心结论: 主线代码里只出现「发」和「收」两个动作,这就是零侵入的可验证标准。
五、接口契约:三类端点撑起前后端约定
前后端能并行开发,靠的是一张稳定的契约表:
| 端点 | 方法 | 用途 | 关键参数 |
|---|---|---|---|
/agent/session |
POST | 创建会话 | bizType、bizId |
/agent/run |
POST | 触发任务 | taskId、prompt、stream |
/agent/task/{id} |
GET | 查询进度 | status、resultUrl |
- 契约稳定:字段只增不改,后端升级模型版本不会波及任何前端代码。
- 状态可查 :任务永不阻塞请求,前端拿到
taskId后按节奏轮询。
下面这段是 Vue3 + Vite 前端里触发任务并轮询状态的最小可用封装:
js
// 触发智能体任务并轮询其状态,适配 Vue3 浏览器环境
async function runAgent(bizType, bizId) {
const res = await fetch('/agent/run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ bizType, bizId, prompt: '生成本周经营简报' })
});
const { taskId } = await res.json();
return pollTask(taskId, 2000);
}
发任务拿 taskId → 定时轮询 → 状态变 SUCCESS 取 resultUrl
核心结论: 契约一旦冻结,前后端就能各自独立发版。
六、前端挂载:侧边栏组件松耦合接入
组件接入要做到拔掉就恢复原样,成本只有一行代码:
- 独立目录 :Agent 组件单独放
components/agent-panel,主页面只保留一行引用。 - props 传上下文 :业务方只传
bizType与bizId,组件内部自己建会话、自己拉状态。 - 事件回抛 :需要主线刷新时用
emit('done'),组件里不 import 任何业务 store。
业务页面里唯一的接入代码,删掉这一行即可整块下线:
vue
<template>
<AgentPanel v-if="agentEnabled" :biz-type="'order'" :biz-id="order.id" @done="reload" />
</template>
核心结论: 一个 v-if 开关加一次 emit,就是前端侧零侵入的全部成本。
七、任务编排:状态机保证可重试可追踪
任务一旦异步化,就必须有明确的状态流转与落库记录:
sql
-- agent_db 中的任务主表,status 字段驱动重试与补偿
CREATE TABLE agent_task (
task_id VARCHAR(64) PRIMARY KEY,
status TINYINT NOT NULL DEFAULT 0, -- 0待执行 1执行中 2成功 3失败 4补偿中
retry INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL
);
- 状态机 :
PENDING → RUNNING → SUCCESS / FAILED,连续失败超阈值进入COMPENSATING自动补偿。 - 防双跑 :同一
task_id只允许一个执行者抢锁,靠数据库行锁避免并发重复调用模型。
核心结论: 有状态机才有可追责的执行链,失败任务才能被自动捡起来重跑。
八、安全配额:密钥隔离、限流与审计留痕
能力放开之前,先把风险面收住,这三件事缺一不可:
- 密钥隔离:模型 Key 只存在 Agent 服务的配置中心,业务侧与前端永远拿不到明文。
- 调用配额 :按
bizType + 租户统计 token 消耗,超阈值自动降级为规则模板回答。 - 审计留痕 :每次执行落
prompt 摘要、模型版本、耗时、token 数,供事后复盘与追责。
核心结论: 没有配额与审计的智能体,上线那天就是失控那天。
九、部署排错:容器拆分与高频故障对照
部署形态直接决定回滚速度,先看拓扑再看故障清单:
yaml
# docker-compose 片段:Agent 独立成组,可单独重启与回滚
services:
agent-svc:
image: registry.local/agent-svc:1.4.0
environment:
- SPRING_PROFILES_ACTIVE=prod
depends_on: [agent-db]
| 现象 | 可能原因 | 处置动作 |
|---|---|---|
| 任务长期 PENDING | 消费者未启动或队列积压 | 查消费者实例数与积压量 |
| 回调重复落库 | 上游重复投递 | 校验 taskId 幂等键 |
| 主线接口变慢 | 误改回同步调用 | 恢复事件异步并加超时 |
核心结论: 服务与库都独立,排错时才能做到单点重启、整体无感。
十、可观测性:链路追踪与效果回归评估
上线只是开始,还得能拿出证据说明它真的有用:
- 链路串联 :事件、任务、回调统一携带
traceId + taskId,把主线与 Agent 两段日志串成一条链。 - 指标看板:盯住任务成功率、平均耗时、token 成本、降级次数四条曲线,异常自动告警。
- 效果回归:固定一批样例问题每周跑一遍,回答质量掉档就直接拦住发版。
核心结论: 可观测做到位,智能体才从演示效果变成可运营能力。
结语
这套侧挂架构把智能体的改动收敛在三处:一个独立 Agent 服务、一组独立数据库、一个前端挂载组件。主线只保留事件与回调两条通道,既拿到了大模型的能力,又不必承担它的不稳定性;要下线时关掉一个开关、停掉一个容器组即可,业务代码一行不改。
真正难的从来不是调通一次模型,而是让智能体在企业里长期可控地跑下去------可回滚、可对账、可限流、可追责。
独立服务、独立数据库、事件契约,三件事凑齐,智能体才算真正接进了企业系统。