Appearance
Agent 前端 / 后端 / 存储基本架构
回到总览:AI 时代的工程师能力相关模块:SSE 流式传输、网络中断恢复与大模型打字机打点 · Agent Tool Calling (Function Calling) 架构与安全防护 · 07. MCP (Model Context Protocol) 开放标准、资源互通与 Agent 安全控制
一句话定义
Agent 产品(AI 助手 / 对话式工作流)的工程架构可以稳定地拆成三层 + 一个状态机:前端层负责展示与交互(流式打字机、任务进度、中断按钮)、后端层负责编排与工具执行(对话管理、tool calling、任务调度)、存储层承载会话 / 记忆 / 任务状态;三层围绕同一个任务状态机协作,才能支撑"流式输出、中断恢复、长任务续跑"这类真实体验。
为什么需要
- 为什么 Agent 产品不能把"前端调 LLM API"当成完整架构?
- 一句话答:LLM 调用只是后端编排里的一个环节;没有独立的编排层与存储层,就无法支撑流式输出、工具调用、中断恢复、多轮记忆与长任务续跑,前端直连模型只能做玩具级 demo。
- 为什么"中断恢复"必须依赖任务状态机而不是前端状态?
- 一句话答:页面刷新或 App 被杀后前端状态必然丢失;任务是否完成、执行到哪一步、工具调用的中间结果必须落到存储层(任务表),由后端按状态机推进,前端只是展示窗口。
- 存储层为什么要区分"会话 / 记忆 / 任务状态"三类数据?
- 一句话答:会话是消息序列(展示用),记忆是跨会话的用户事实(个性化用),任务状态是执行进度(恢复用);三者生命周期与读写频率不同,混在一起会导致恢复逻辑混乱。
底层机制:三层 + 一个状态机
1. 三层职责边界
图注补充:前端层 (App / Web / IDE 插件)";存储层 (DB / Redis / 对象存储)";SSE 流式事件 + 用户操作(中断/重试)。
- 前端层:只做展示与交互,不持有业务真相;中断/重试通过事件发给后端,由后端决定状态转移。
- 后端层:唯一权威执行者;负责把用户请求拆成任务、驱动 LLM + 工具循环、持久化每一步结果。
- 存储层:状态与事实的落点;任务表是"中断恢复"的核心依据。
2. 任务状态机(Agent 长任务的恢复依据)
- 观察重点:每个状态变更都必须先写存储、再推送事件(先持久化后通知);否则中途崩溃时,前端看到"completed"但任务表还停在"running",恢复逻辑就失去依据。
代码示例:最小三层协作(SSE + 任务状态)
以"让 Agent 读仓库并出报告"为例,展示三层如何围绕任务状态协作:
python
# ── 后端:任务状态机推进 + SSE 推送(伪代码)
class TaskService:
def start_task(self, user_id, request):
task_id = uuid4()
# 1. 先落库:pending → running
db.tasks.insert(Task(id=task_id, user=user_id,
state="running", steps=[], result=None))
# 2. 异步推进(LLM + 工具循环)
self.worker.submit(self._run_agent(task_id, request))
return task_id
def _run_agent(self, task_id, request):
steps = []
for step in self.plan(request): # 拆步骤
steps.append(step)
db.tasks.update_state(task_id, "running", steps=steps) # 先持久化
self.sse_broadcast(task_id, {"type": "step", "data": step}) # 再推送
result = self.execute_tool(step) # 执行工具
db.tasks.append_result(task_id, result)
db.tasks.update_state(task_id, "completed")
self.sse_broadcast(task_id, {"type": "done"})
def cancel(self, task_id):
db.tasks.update_state(task_id, "cancelled") # 用户中断:状态落库typescript
// ── 前端:监听 SSE 事件流 + 中断按钮
const taskId = await api.startTask('读仓库并出报告');
const events = new EventSource(`/api/tasks/${taskId}/stream`);
events.addEventListener('step', (e) => renderStep(JSON.parse(e.data)));
events.addEventListener('done', () => { renderFinal(); events.close(); });
document.getElementById('cancel-btn').onclick = () =>
fetch(`/api/tasks/${taskId}/cancel`, { method: 'POST' }); // 中断交给后端决策- 可能执行顺序:用户提交 → 后端建任务(pending→running)→ 逐步执行(running→waiting_tool→running→streaming…)→ 每步先写库再推送 → 完成后置 completed。
- 预期现象:前端逐步显示步骤;页面刷新或 App 被杀后,重新拉
/api/tasks/{id}能看到任务真实状态并续跑展示。 - 观察重点:先持久化、后推送是这条链路的正确性关键;中断按钮调用的是后端接口而不是前端直接停,这样状态机才一致。
Android / Flutter / Web / Backend 对照
| 层 | 移动端 (Flutter/Android) | Web 前端 | 后端 |
|---|---|---|---|
| 展示层 | EventSource 类库 / http 流式解析 | fetch ReadableStream / EventSource | — |
| 编排层 | — | — | Agent Engine(LLM + tools + 状态机) |
| 存储层 | 本地缓存断点(可选) | IndexedDB / localStorage | DB + Redis + 对象存储 |
常见误配、事故后果与排障
1. 事故:前端直连 LLM,功能一复杂就失控
- 误配原因:demo 期图快,把 API Key 放前端、前端直接调 LLM,没有后端编排层。
- 后果:Key 泄露、无法做工具调用鉴权、无法恢复长任务、多端状态不一致——产品能力天花板被锁死在单次对话。
- 排障与修法:至少加一层后端网关:Key 只在服务端、前端只走代理接口;工具调用与状态机逐步收口到后端。
2. 事故:先推送事件再写库,中途崩溃后前后端状态不一致
- 误配原因:
_run_agent里先sse_broadcast再db.update。 - 后果:前端显示任务"已完成",服务端任务表却停在 running;用户刷新后任务"消失"或无限等待。
- 排障与修法:统一"先落库、后推送";推送失败由前端轮询任务接口兜底。
3. 事故:把任务状态放在内存里,进程重启即丢
- 误配原因:用进程内 Map 存任务状态,未落库。
- 后果:后端发布 / OOM 重启后,所有进行中任务状态丢失,用户只能重头再来。
- 排障与修法:任务状态落 DB(或 Redis + 定期快照),重启后按状态机从
pending/running恢复或标记失败重试。
与相近概念对比
| 概念 | 它回答什么 | 不回答什么 |
|---|---|---|
| SSE 流式传输 | 事件如何从后端推到前端 | 任务如何编排与恢复 |
| tool calling | 模型如何调用外部工具 | 工具执行结果如何持久化 |
| 任务状态机 | 长任务如何被推进与恢复 | 单轮对话如何组织 |
| MCP 协议 | Agent 如何对接外部工具/资源 | 会话与记忆如何存储 |
对应实验
计划补齐的实验:
labs/shared/ai-engineering/agent-arch-demo/— 最小 Agent 三层架构(SSE 流式 + 任务状态机 + SQLite 存储)Demo
复习检查题
为什么 Agent 产品必须区分"前端层 / 后端编排层 / 存储层"?
答:因为展示、执行与状态持久化是三个不同生命周期与职责的领域:前端只管渲染与交互,后端负责编排与工具执行,存储负责状态与事实落点。不分层,流式输出、工具调用、中断恢复、长任务续跑这些核心体验都无法可靠实现。
中断恢复为什么必须依赖存储层而不是前端状态?
答:前端状态随页面刷新或 App 被杀必然丢失;任务执行到哪一步、工具结果是什么,必须落在存储层的任务表里,由后端状态机推进,前端只做展示窗口。
为什么状态变更要"先写库、再推送事件"?
答:推送只是通知,写库才是事实。先推送后写库,若中途崩溃会出现"前端看到新状态、存储仍是旧状态"的不一致;先写库后推送,即使推送失败,前端也可通过轮询接口兜底对齐。
速记
- 三层分工:前端展示,后端编排,存储落状态。
- 一个状态机:pending → running → streaming/tool → completed/failed/cancelled。
- 先写后推:状态变更先落库再推送,崩溃后靠任务表恢复。
- 中断是后端动作:前端只发事件,状态转移由后端决定。