第 14 章:前端解决方案与系统设计
本章定位
本章把前面所有能力组合成端到端系统设计。目标是能从需求、数据流、状态、错误和扩展点出发设计 AI Agent 前端,而不是只讨论组件。
必须掌握清单
必须掌握
能设计 Session Log 和派生视图。
相关答案: Session Log 以有序、可追踪的事件记录会话事实,消息列表、工具状态和统计信息由投影函数派生。日志事件要有稳定 ID、会话 ID、时间和版本,避免把多个相互冲突的 UI 状态分别持久化。
具体例子:
user_message、assistant_delta、tool_started和tool_finished依次追加;重启后回放同一日志即可恢复聊天视图和工具卡片状态。能设计 Tool Registry 和工具生命周期。
相关答案: Registry 统一保存工具名称、说明、输入 schema、权限策略和执行器,并保证名称唯一;一次调用经历校验、授权、执行、结果标准化与清理。注册和卸载也应有明确生命周期。
具体例子: 搜索插件注册
web.search后返回 disposer;模型请求调用时,运行时先校验查询参数和权限,再执行并把结果转换为统一ToolResult,禁用插件后注册项被移除。能设计 LLM Provider 适配层。
相关答案: 适配层把不同供应商的请求、流事件、工具调用、用量和错误映射成领域统一接口,同时保留供应商特有能力的显式扩展位。上层不应解析厂商原始 SSE 格式或错误码。
具体例子: OpenAI 与另一模型返回的 delta 格式不同,两个 adapter 都向运行时产生
text_delta、tool_call、usage和done事件,聊天 UI 无需写供应商分支。能设计前端流式运行时和不可变快照。
相关答案: 运行时负责消费事件、维护内部可变缓冲、合批并发布带版本的不可变快照;UI 只读快照并按选择器订阅。旧快照不能被后续 token 原地修改,否则并发渲染和调试会看到时间穿越。
具体例子: 连续 delta 先写入当前消息 buffer,每帧构造新消息节点和
snapshot.version + 1;React 用useSyncExternalStore读取一致快照。能设计取消、失败、重试和恢复状态机。
相关答案: 每个请求或工具调用使用显式状态及合法转换,区分用户取消、可重试故障、业务拒绝和不可恢复错误。重试要决定复用哪个输入、是否产生新 attempt ID,以及如何防止副作用重复执行。
具体例子: 请求从
running可转为succeeded、failed或cancelled;网络失败重试生成新 attempt 并从最后确认事件恢复,已成功扣款的工具不能被自动重复调用。
原理讲解
一个 AI Agent 前端的核心对象不是组件,而是 Session、Message、Tool Call、Tool Result 和 Request。Session Log 是只追加事件源,UI 只是从事件源派生出来的快照。
Tool Registry 把工具定义、执行和 UI 展示解耦。工具来源可以是本地插件,也可以是 MCP,前端工具视图只消费统一的 render intent。
LLM Provider 适配层把模型 API 和流式响应封装成统一接口。前端只依赖这个接口,而不是直接耦合某个模型供应商。
请求状态机至少包含 idle、streaming、waitingForTool、completed、cancelled、failed 和 retrying。不同状态决定 UI 如何展示,也决定哪些操作可以继续。
代码示例或模板
type AgentStatus =
| "idle"
| "streaming"
| "waitingForTool"
| "completed"
| "cancelled"
| "failed";
type AgentSnapshot = {
status: AgentStatus;
messages: readonly unknown[];
activeToolId: string | null;
error: string | null;
};
function nextStatus(
current: AgentStatus,
event: "tool_start" | "tool_result" | "done" | "error" | "cancel",
): AgentStatus {
if (current === "streaming" && event === "tool_start") {
return "waitingForTool";
}
if (current === "waitingForTool" && event === "tool_result") {
return "streaming";
}
if (event === "done") {
return "completed";
}
if (event === "cancel") {
return "cancelled";
}
if (event === "error") {
return "failed";
}
return current;
}这个状态机是前端流式运行时的最小骨架,实际系统还需要记录事件、时间戳和工具结果。
AI Agent 概念对照
- Session Log 对应事件溯源。
- Tool Registry 对应插件扩展。
- Provider Seam 对应适配层。
- Frontend Streaming Runtime 对应 UI 运行时。
面试追问
1. 设计一个 AI 客服前端,你会先定义哪些核心对象?
查看深度解析
标准回答: 先定义 Session、Turn/Request、Message、Stream Event、Tool Call/Result、Permission Decision 和 Participant,并明确 ID、所有权、状态与关系;UI 组件只是这些对象的投影。
原理展开: 对象模型先于页面结构,才能统一流式更新、重试、回放和多端同步。外部 DTO 经适配器进入领域模型,不让厂商格式渗透 UI。
工程示例: 一个 request 关联多条 assistant delta 和多个 tool call,Session Log 追加事件,消息列表与工具卡由 projector 派生。
常见误区: 只定义 messages: {role,content}[],无法表达权限等待、并行工具、取消和部分失败。
继续追问: Turn 和 Request 为什么可能不是同一个对象?
回答方向: 一个用户 turn 可能经历重试、恢复或多个模型 request;分开能保留 attempt 与用户语义边界。
2. 如何让工具 UI 不感知工具来源是本地还是 MCP?
查看深度解析
标准回答: 在 Tool Registry/adapter 层把本地和 MCP 定义映射成统一 ToolDefinition、Call、Result、权限和 Render Intent;UI 只按工具语义与状态渲染,不读取 transport 或 provider 字段。
原理展开: 来源差异留在发现、连接、协议错误和执行 adapter 中。统一接口要保留扩展 metadata,但不能把最小公分母做得失去能力。
工程示例: 本地 search 与 MCP search 都产生 {kind:'search-results', items} render intent,同一结果组件展示;诊断区仍可显示来源和服务器状态。
常见误区: UI 用 if (source === 'mcp') 解析原始 payload,协议升级后所有组件被牵连。
继续追问: 不同来源同名工具怎么处理?
回答方向: registry 使用命名空间和稳定 tool ID,展示名可相同;策略、版本和来源属于定义 metadata。
3. 会话日志应该保存原始事件还是派生视图?
查看深度解析
标准回答: 以规范化原始事件作为事实源,派生视图通常可重建并作为带版本缓存保存。若只存消息列表,会丢失工具、权限、重试和诊断事实;若只存供应商原始帧,又难以长期兼容。
原理展开: 事件要不可变、排序、版本化并经过脱敏;projector 是确定函数。checkpoint 加速恢复,但必须能验证其对应事件位置。
工程示例: 保存 assistant_delta 和 tool_finished,每 500 个事件保存 message snapshot;升级投影逻辑时可重放生成新视图。
常见误区: 把每个网络字节都永久存储,成本和隐私失控;或原地修改旧事件纠正 UI。
继续追问: 事件 schema 升级怎么办?
回答方向: 每个事件带版本,读取时用纯迁移器升级;保留测试 fixture,迁移失败进入明确隔离而非静默丢字段。
4. 一个 Agent 请求被取消、失败和重试,状态机如何建模?
查看深度解析
标准回答: 使用显式状态如 queued、running、waiting_permission、succeeded、failed、cancelled,并定义合法转换与终态。每次重试创建新的 attempt ID,关联同一逻辑 request,取消通过 signal 向子工具传播。
原理展开: failure 要分类为可重试、业务拒绝和未知结果;状态转换由事件驱动且幂等,迟到事件不能把 cancelled 改回 running。
工程示例: 网络失败后 attempt 1 为 failed,用户重试创建 attempt 2;已成功的非幂等工具不会自动重放,而是查询结果或再次确认。
常见误区: 用 loading/error 两个布尔值表达所有情况,产生互相矛盾状态;重试直接清空历史导致无法审计。
继续追问: 取消一定能成功吗?
回答方向: 取消是请求,不是时间倒流;本地可停止等待,但远端副作用可能已完成,应查询最终状态并展示“取消结果未知”。
5. 如何证明这个方案比直接调用 API 更值得做?
查看深度解析
标准回答: 先列清直接调用无法满足的已验证需求,例如多 Provider、工具生命周期、权限、恢复、回放和可观测性,再用指标比较交付成本、故障率、扩展时间和用户任务成功率。复杂架构本身不是价值。
原理展开: 需要给出基线、替代方案和增量演进路径;如果只有单模型、单页面、无工具,直接 API 可能正是最优解。
工程示例: 引入统一 runtime 后新增 Provider 从改五个页面降为一个 adapter,断线恢复成功率提升,并能追踪工具失败;同时记录新增维护成本。
常见误区: 用设计模式数量、代码复用率或“未来可能需要”证明架构,而没有真实场景和数据。
继续追问: 如何控制过度设计?
回答方向: 先实现一条纵向主链,只为已出现的第二种实现抽接口;每个扩展点有明确消费者、测试和删除条件。
实践任务
写一份 AI Agent 前端架构一页纸,包含核心对象、事件流、工具 UI、错误处理和可观测性。
验收标准
- 能解释原始事件和 UI 快照的关系。
- 工具 UI 不直接依赖工具来源。
- 请求状态覆盖取消、失败和重试。
- 能画出端到端数据流。