AI 相关场景题¶
前端 AI 应用开发的核心场景与实践,涵盖 Agent 架构、通信模式、上下文管理等关键技术点。
Agent 服务搭建¶
核心思路¶
Agent 不同于简单的 LLM 调用,它需要具备**理解、规划、执行、反馈**的完整能力。理解是指解析用户意图和任务需求;规划是指将复杂任务分解为可执行的步骤;执行是指调用工具和 LLM 完成各个步骤;反馈是指根据执行结果调整策略或生成最终答案。
前端搭建 Agent 服务时需要从四个维度综合考虑:
架构选型:根据应用规模、团队能力、安全要求选择合适的架构模式。小型项目可以用纯前端方案快速验证,生产环境必须采用 BFF 或微服务架构保证安全性和可扩展性。
安全性:API Key 泄露是最大风险,必须通过后端代理。同时需要防范注入攻击(用户输入可能被构造成恶意 Prompt)、防止滥用(实现认证和限流)、保护敏感数据(加密传输和存储)。
成本控制:LLM 调用按 Token 计费,Agent 通常需要多次调用(规划、工具执行、总结),成本是普通 LLM 的 3-10 倍。需要通过缓存、限流、模型选择、Prompt 优化等手段控制成本。
用户体验:Agent 执行时间较长(5-30 秒),必须提供实时反馈。使用 SSE 实现流式响应,让用户看到"思考中"、"正在搜索"、"生成答案"等状态,避免长时间白屏等待。同时支持取消请求、断点续传、错误重试等交互。
三种架构方案对比¶
1. 纯前端架构
将 Agent 逻辑完全放在浏览器端,使用 LangChain.js 直接调用 LLM API。适合快速原型验证和个人项目。
优势:部署简单(无需后端)、开发快速(前端工程师即可完成)、响应迅速(无网络中转)、调试方便(浏览器开发工具)。
限制:API Key 必然暴露在前端代码中(即使混淆也能被提取)、浏览器性能受限(无法处理大规模计算)、无法实现复杂的工具调用(如数据库操作)、难以实现成本控制和监控。
适用场景:技术 Demo、学习项目、内部工具(可信环境)、功能简单的对话应用。
2. BFF(Backend for Frontend)架构
前端负责 UI 交互,后端提供 Agent 服务的代理层。这是生产环境的标准方案。
优势:API Key 安全(存储在后端环境变量)、功能完整(可调用任何后端资源)、成本可控(后端实现限流和缓存)、便于监控(记录所有调用日志)、支持复杂逻辑(数据库、文件系统、第三方 API)。
限制:需要维护后端服务(增加开发和运维成本)、系统复杂度提升(前后端协作)、部署相对复杂(需要服务器资源)。
技术栈示例:前端 React/Vue + 后端 Express/Nest.js + LangChain.js + OpenAI API。通信方式推荐 SSE(流式响应)或 WebSocket(双向交互)。
适用场景:生产环境、企业应用、需要安全保障的场景、需要复杂工具调用的应用。
3. 微服务架构
将不同能力的 Agent 拆分为独立服务,通过编排层协调多个 Agent 协作。适合大型复杂应用。
优势:高度解耦(各 Agent 独立开发和部署)、可独立扩展(根据负载调整各服务资源)、便于团队协作(不同团队负责不同 Agent)、容错性强(单个 Agent 故障不影响整体)、技术栈灵活(各服务可用不同语言)。
限制:架构复杂(需要服务发现、负载均衡、熔断降级)、运维成本高(多个服务的部署和监控)、调试困难(分布式系统问题排查)、网络开销大(服务间通信)。
技术栈示例:Kubernetes + 多个 Agent 服务(搜索 Agent、分析 Agent、总结 Agent)+ 编排服务(协调各 Agent)+ 消息队列(RabbitMQ/Kafka)。
适用场景:大型应用(日活百万级)、多团队协作、需要高可用性、需要独立扩展各能力模块。
推荐方案:
- 原型阶段:纯前端架构,快速验证想法
- 生产环境:BFF 架构,平衡开发成本和功能完整性
- 大规模应用:微服务架构,支持高并发和团队协作
大多数场景下,BFF 架构是最佳选择。使用 LangChain.js 在 Node.js 后端构建 Agent 能力,前端通过 SSE 实现流式交互,既保证了安全性,又提供了良好的用户体验。
LangChain.js 快速入门¶
LangChain.js 是专为 JavaScript/TypeScript 设计的 AI 应用开发框架,提供了 Agent 开发的完整工具链:
- LLM 集成:统一接口支持 OpenAI、Anthropic、Google 等多个提供商
- Agent 框架:内置 ReAct、Plan-and-Execute 等多种模式
- 工具系统:预置常用工具(搜索、计算器等),支持自定义扩展
- 记忆管理:支持对话历史、向量存储、摘要记忆等策略
核心优势是快速搭建原型,避免从零实现基础能力。但生产环境必须通过后端代理保护 API Key。
关键设计要点¶
安全性设计
API Key 保护是首要任务。绝不能在前端代码中硬编码或通过环境变量暴露,即使使用代码混淆也能被逆向工程提取。正确做法是所有 LLM 调用都通过后端代理,API Key 存储在后端的环境变量或密钥管理服务(如 AWS Secrets Manager)中。
用户认证和授权:前端请求必须携带有效的用户 Token(JWT),后端验证 Token 有效性和用户权限。可以实现基于角色的访问控制(RBAC),不同用户有不同的调用额度和功能权限。
输入校验和过滤:用户输入可能包含恶意内容,需要进行严格校验。防止 Prompt 注入攻击(用户构造特殊输入绕过系统指令)、SQL 注入(如果 Agent 需要查询数据库)、XSS 攻击(如果回复内容会被渲染为 HTML)。可以使用白名单过滤、长度限制、敏感词检测等手段。
敏感数据保护:对话内容可能包含用户隐私,需要加密传输(HTTPS)和存储(数据库加密)。定期清理过期数据,遵守 GDPR 等隐私法规。
成本控制策略
请求限流:实现多层限流机制。用户级限流(每个用户每天最多 100 次调用)、IP 级限流(防止恶意攻击)、全局限流(保护系统不被压垮)。可以使用 Redis 实现分布式限流,或使用 Nginx 的 limit_req 模块。
智能缓存:相同或相似的问题不重复调用 LLM。实现多级缓存:内存缓存(最近 100 个问题)、Redis 缓存(热门问题)、数据库缓存(历史问题)。使用语义相似度匹配(向量检索)而非精确匹配,提高缓存命中率。
模型选择策略:根据任务复杂度动态选择模型。简单问答用 GPT-3.5-turbo(\(0.002/1K tokens),复杂推理用 GPT-4(\)0.03/1K tokens)。可以先用便宜模型判断任务类型,再决定是否升级到高级模型。
Token 优化:精简 Prompt(移除冗余描述)、压缩上下文(摘要历史对话)、使用 Function Calling 而非自然语言描述工具。监控每次调用的 Token 消耗,设置单次调用上限(如 4000 tokens)。
预算告警:实时监控 API 调用费用,设置预算阈值(如每天 $100)。超过阈值时发送告警邮件,甚至自动降级服务(切换到便宜模型或暂停服务)。
用户体验优化
流式响应:使用 SSE(Server-Sent Events)实现打字机效果。后端逐字或逐句推送生成的内容,前端实时渲染,让用户感知到进度。相比等待 30 秒后一次性返回结果,流式响应大幅提升体验。
状态反馈:清晰展示 Agent 的执行状态。"正在思考..."(LLM 规划阶段)、"正在搜索..."(调用搜索工具)、"正在分析数据..."(处理结果)、"生成答案中..."(最终回复)。可以配合进度条或动画效果。
请求取消:用户可能中途改变主意,需要支持取消正在执行的请求。前端发送取消信号,后端中止 LLM 调用和工具执行,释放资源。使用 AbortController API 实现。
断点续传:网络中断或浏览器崩溃后,用户刷新页面能恢复之前的对话。将对话历史持久化到 localStorage 或云端,页面加载时自动恢复。
错误处理:友好的错误提示。"网络连接失败,请检查网络"、"服务繁忙,请稍后重试"、"该功能暂时不可用"。避免直接展示技术错误信息(如 500 Internal Server Error)。
可扩展性设计
插件化工具系统:工具(Tool)是 Agent 的核心能力。设计统一的工具接口,新增工具只需实现接口即可。工具注册机制支持动态加载,无需修改核心代码。
配置化 Prompt 管理:Prompt 是 Agent 的"灵魂",需要频繁调整优化。将 Prompt 存储在配置文件或数据库中,支持热更新(无需重启服务)。可以实现 A/B 测试,对比不同 Prompt 的效果。
模块化设计:将 Agent 拆分为独立模块:LLM 接口层、工具管理器、记忆模块、规划器、执行器。各模块职责清晰,便于单独测试和替换。
扩展接口:预留扩展点,支持未来功能迭代。例如支持自定义 LLM 提供商(不仅限于 OpenAI)、支持自定义记忆策略(不仅限于对话历史)、支持自定义规划算法(不仅限于 ReAct)。
可观测性建设
完整日志记录:记录每次对话的完整流程。用户输入、LLM 调用(请求和响应)、工具执行(参数和结果)、最终输出、耗时统计、错误信息。使用结构化日志(JSON 格式),便于检索和分析。
性能监控:追踪关键指标。响应时间(P50、P95、P99)、Token 消耗(总量和单次平均)、成功率(成功/失败/超时比例)、工具调用次数、缓存命中率。使用 Prometheus + Grafana 可视化监控。
链路追踪:分布式系统中,一次请求可能经过多个服务。使用 OpenTelemetry 或 Jaeger 实现链路追踪,清晰看到请求在各服务间的流转和耗时。
用户反馈收集:在回复下方提供"有帮助"/"无帮助"按钮,收集用户反馈。分析低评分的对话,找出 Agent 的薄弱环节,持续优化。
告警机制:建立多级告警。错误率超过 5% 发送警告、超过 10% 发送紧急告警、服务完全不可用立即电话通知。集成 PagerDuty 或企业微信机器人。
实现示例¶
前端流式对话(使用 SSE 实现打字机效果):
class AgentService {
async *chatStream(message: string, sessionId: string) {
const response = await fetch("/api/agent/chat/stream", {
method: "POST",
body: JSON.stringify({ message, sessionId }),
});
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split("\n")) {
if (line.startsWith("data: ")) {
yield JSON.parse(line.slice(6));
}
}
}
}
}
后端 Agent 核心逻辑(使用 LangChain.js):
import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
import { DynamicTool } from "@langchain/core/tools";
// 1. 定义工具
const tools = [
new DynamicTool({
name: "search",
description: "搜索互联网获取最新信息",
func: async (query) => await searchAPI(query),
}),
new DynamicTool({
name: "calculator",
description: "执行数学计算",
func: async (expr) => eval(expr).toString(),
}),
];
// 2. 创建 Agent
const agent = await createOpenAIFunctionsAgent({
llm: new ChatOpenAI({ modelName: "gpt-4" }),
tools,
prompt: ChatPromptTemplate.fromMessages([
["system", "你是一个有用的助手,可以使用工具来帮助用户"],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]),
});
// 3. 执行任务
const executor = new AgentExecutor({ agent, tools });
const result = await executor.invoke({
input: "搜索今天的天气,然后计算华氏度转摄氏度",
});
LangChain.js 自动处理工具选择、参数解析、执行流程等复杂逻辑,大幅简化开发。
核心组件详解¶
工具系统(Tool System)
工具是 Agent 与外部世界交互的桥梁,让 Agent 从"只会说话"变成"能做事"。一个完整的工具系统包含三个部分:
工具定义:每个工具需要清晰的接口定义。名称(name)要简洁明确,如 "search" 而非 "search_the_internet"。描述(description)要让 LLM 理解工具的用途和使用场景,如"搜索互联网获取最新信息,适合回答时事问题"。参数(parameters)要定义类型和约束,如 query 是必填的字符串,maxResults 是可选的数字(默认 5)。
工具注册:实现工具的注册和管理机制。使用 Map 或对象存储所有可用工具,支持动态添加和移除。LangChain.js 提供了 DynamicTool 类,可以快速定义工具。
工具选择:LLM 根据用户问题和工具描述,决定调用哪个工具。OpenAI 的 Function Calling 功能会自动解析工具参数,生成结构化的调用请求。Agent 执行器接收请求,调用对应工具,将结果返回给 LLM。
常见工具类型:
- 搜索工具:调用 Google Search API、Bing API 或 Serper API 获取实时信息
- 计算器:执行数学运算,处理复杂计算
- 数据库查询:根据自然语言生成 SQL,查询业务数据
- API 调用:调用天气 API、股票 API、地图 API 等第三方服务
- 文件操作:读取、写入、搜索文件内容
- 代码执行:在沙箱环境中执行 Python、JavaScript 代码
错误处理:工具执行可能失败(API 超时、参数错误、权限不足)。需要捕获异常,返回友好的错误信息给 LLM,让 LLM 决定是重试、换工具还是告知用户。
状态管理(State Management)
前端需要管理 Agent 的多种状态,保证 UI 和数据的一致性。
对话历史:存储用户和 Agent 的所有消息。每条消息包含角色(user/assistant/system)、内容、时间戳、消息 ID。使用数组存储,支持分页加载(避免一次性加载过多消息)。
执行状态:Agent 的当前状态。空闲(idle)、思考中(thinking,LLM 规划阶段)、执行工具(executing,调用外部 API)、生成回复(generating,LLM 生成最终答案)、错误(error)。前端根据状态展示不同的 UI(加载动画、进度提示)。
会话信息:每个对话会话的元数据。会话 ID(sessionId,用于区分不同对话)、用户信息(userId、用户名)、创建时间、最后活跃时间。支持多会话管理,用户可以在不同会话间切换。
临时数据:当前正在输入的消息、选中的工具、错误信息、重试次数等。这些数据不需要持久化,页面刷新后可以丢弃。
持久化策略:对话历史需要持久化,避免刷新页面后丢失。短期存储用 localStorage(5MB 限制),长期存储用 IndexedDB(无限制)或云端数据库。
状态管理方案:
- Zustand:轻量级,API 简洁,适合中小型应用
- Redux:功能强大,生态丰富,适合大型应用,但样板代码多
- React Context:React 内置,适合简单场景,但性能较差(全局更新)
- Jotai/Recoil:原子化状态管理,适合复杂状态依赖
推荐使用 Zustand,它结合了简洁性和功能性,支持中间件(持久化、日志、DevTools)。
通信协议选择
前后端通信方式直接影响用户体验和系统复杂度。
HTTP/REST:最简单的方式,适合简单的请求-响应场景。前端发送 POST 请求,后端处理完成后返回完整结果。优点是实现简单、兼容性好、易于调试。缺点是无法推送(前端只能轮询)、长时间等待体验差、无法实时反馈进度。适合快速原型或简单对话。
SSE(Server-Sent Events):服务端推送技术,适合流式响应。后端逐步推送生成的内容,前端实时接收并渲染。优点是实现简单(基于 HTTP)、自动重连、支持流式传输。缺点是单向通信(只能服务端推送)、浏览器兼容性(IE 不支持)。适合 Agent 场景,实现打字机效果。
WebSocket:双向通信协议,适合实时交互。前后端建立持久连接,可以互相推送消息。优点是低延迟、双向通信、支持二进制数据。缺点是连接管理复杂(需要处理断线重连)、服务端资源消耗大(每个连接占用一个 socket)、调试困难。适合需要实时协作的场景(多人对话、实时编辑)。
轮询(Polling):前端定时发送请求查询结果。优点是兼容性最好(所有浏览器支持)、实现简单。缺点是效率低(大量无效请求)、延迟高(轮询间隔)、服务端压力大。适合作为降级方案,当 SSE 或 WebSocket 不可用时使用。
推荐方案:优先使用 SSE 实现流式响应,提供最佳用户体验。如果需要双向实时交互(如用户可以中途打断 Agent),使用 WebSocket。HTTP 作为基础方案,轮询作为降级方案。
常见追问¶
Q: 如何处理并发请求?
并发控制是保证系统稳定性的关键。主要有三种策略:
队列机制:将同一用户的请求放入队列,按顺序串行处理。这样可以保证对话的连贯性,避免多个请求同时修改上下文导致混乱。实现时使用 Promise 队列或消息队列(如 Bull、BullMQ)。每个用户维护一个独立队列,队列中的请求依次执行。
并发限制:设置全局最大并发数(如 3 个),超出的请求进入等待队列。这样可以控制服务端负载,避免同时处理过多请求导致系统崩溃。可以使用 p-limit 库实现并发控制,或使用 Redis 实现分布式并发限制。
会话隔离:不同用户的请求可以并发处理(提高吞吐量),但同一会话内的请求必须串行(保证上下文一致性)。这是最常用的策略,平衡了性能和正确性。实现时为每个会话分配一个锁,同一会话的请求必须获取锁才能执行。
其他考虑因素:
- 请求优先级:VIP 用户的请求优先处理,普通用户的请求可以排队
- 超时处理:请求超过 30 秒自动取消,释放资源,避免长时间占用
- 取消机制:用户可以主动取消请求,立即释放资源
- 降级策略:系统负载过高时,拒绝新请求或返回简化结果
Q: 如何优化响应速度?
响应速度直接影响用户体验,需要从多个维度优化:
流式响应:使用 SSE 实现打字机效果,让用户立即看到生成的内容,而不是等待 30 秒后一次性返回。这是最有效的体验优化,即使总耗时不变,用户感知的等待时间大幅缩短。
智能缓存:对相同或相似的问题缓存结果。精确匹配缓存(问题完全相同)命中率低但实现简单,语义相似度缓存(使用向量检索)命中率高但实现复杂。可以使用 Redis 存储缓存,设置合理的过期时间(如 1 小时)。
并行执行:如果 Agent 需要调用多个独立的工具(如同时查询天气和股票),可以并行执行而非串行。使用 Promise.all 实现并行调用,总耗时等于最慢的那个工具。
模型选择:简单任务用 GPT-3.5-turbo(响应快、成本低),复杂任务才用 GPT-4(响应慢、成本高)。可以先用 GPT-3.5 判断任务复杂度,再决定是否升级。或者根据用户等级自动选择(免费用户用 3.5,付费用户用 4)。
Prompt 优化:精简 Prompt 可以减少 Token 数量,加快生成速度。移除冗余描述、使用简洁的指令、避免重复信息。例如将"请你仔细思考并详细回答以下问题"简化为"回答问题"。
预加载和预热:提前加载常用工具和模型,避免首次调用的冷启动延迟。可以在服务启动时预热(发送一个测试请求),或使用连接池保持与 LLM API 的连接。
CDN 加速:前端资源(JS、CSS、图片)使用 CDN 加速,减少页面加载时间。后端 API 使用就近部署(多地域部署),减少网络延迟。
Q: 生产环境最佳实践?
生产环境需要考虑安全、稳定、性能、成本等多个方面:
架构设计:LangChain.js 必须部署在 Node.js 后端,绝不能在浏览器中运行(API Key 会暴露)。推荐架构:前端 React/Vue → 后端 Express/Nest.js + LangChain.js → OpenAI API。前端通过 SSE 接收流式响应,后端负责所有 LLM 调用和工具执行。
安全防护:后端代理保护 API Key(存储在环境变量或密钥管理服务)、前端请求携带 JWT Token(验证用户身份)、实现 RBAC 权限控制(不同用户有不同权限)、输入校验和过滤(防止注入攻击)、HTTPS 加密传输(防止中间人攻击)。
错误处理:完善的错误处理和降级方案。LLM API 调用失败时重试(最多 3 次,指数退避)、工具执行失败时返回友好错误信息、系统过载时拒绝新请求或返回简化结果、记录所有错误日志便于排查。
成本控制:实现请求限流(用户级、IP 级、全局级)、智能缓存(减少重复调用)、模型选择(根据任务复杂度选择)、Token 优化(精简 Prompt)、预算告警(超过阈值自动降级)。
监控告警:记录完整日志(请求、响应、耗时、错误)、监控关键指标(响应时间、成功率、Token 消耗)、建立告警机制(错误率过高立即通知)、使用 APM 工具(如 Datadog、New Relic)。
自定义工具:根据业务需求开发专用工具,而不是完全依赖预置工具。例如电商应用需要商品搜索工具、订单查询工具、库存检查工具。自定义工具可以直接访问内部数据库和 API,提供更准确的结果。
灰度发布:新功能先在小范围用户中测试,验证稳定性后再全量发布。可以使用 Feature Flag 控制功能开关,出现问题时快速回滚。
Q: 如何保证 API Key 安全?
API Key 泄露是最严重的安全问题,可能导致巨额费用损失和数据泄露。
绝不在前端硬编码:即使使用代码混淆、环境变量、加密,API Key 最终都会出现在浏览器中,可以被提取。任何前端代码都是公开的,不要存在侥幸心理。
后端代理:所有 LLM 调用都通过后端转发。前端发送请求到后端 API(如 /api/chat),后端使用存储在环境变量中的 API Key 调用 OpenAI API,将结果返回给前端。这样 API Key 永远不会暴露给前端。
用户认证:前端请求必须携带有效的用户 Token(JWT)。后端验证 Token 的有效性(签名、过期时间),确认用户身份后才处理请求。未认证的请求直接拒绝。
权限控制:后端验证用户权限,限制调用频率和额度。例如免费用户每天最多 10 次调用,付费用户每天 1000 次。超过额度的请求返回 429 Too Many Requests。
环境变量:API Key 存储在后端的环境变量中(如 .env 文件),不提交到代码仓库。.env 文件加入 .gitignore,避免意外提交。生产环境使用密钥管理服务(AWS Secrets Manager、Azure Key Vault)。
密钥轮换:定期更换 API Key(如每月一次),降低泄露风险。即使 Key 被泄露,也只在短时间内有效。使用多个 Key 轮换使用,单个 Key 被封禁不影响整体服务。
监控异常:监控 API 调用量和费用,设置异常告警。如果调用量突然激增(可能是 Key 被盗用),立即发送告警并暂停服务。
开发环境:开发环境也要使用 .env 文件,不要在代码中硬编码测试 Key。团队成员各自使用自己的 Key,避免共享。使用 OpenAI 的 Usage Limits 功能限制测试 Key 的额度。
Agent 通信方式¶
核心思路¶
多 Agent 协作是构建复杂 AI 系统的关键。单个 Agent 能力有限,通过多个专业化的 Agent 协作可以完成更复杂的任务。例如一个研究任务可以拆分为:搜索 Agent 收集信息、分析 Agent 处理数据、总结 Agent 生成报告。
选择合适的通信模式需要考虑几个因素:
系统规模:小型系统(2-3 个 Agent)可以用直接通信,大型系统(10+ 个 Agent)需要消息队列或协调者模式。
耦合度:紧耦合场景(Agent 之间强依赖)用直接通信,松耦合场景(Agent 独立工作)用消息队列或发布订阅。
实时性:需要实时响应用直接通信或 WebSocket,可以异步处理用消息队列。
可靠性:对可靠性要求高(消息不能丢失)用消息队列(持久化),要求低可以用内存队列或直接通信。
四种主流通信模式各有优劣,需要根据具体场景选择。
四种通信模式¶
1. 直接通信:Agent 之间直接调用 API,适合简单的点对点交互。
class SearchAgent {
async processQuery(query: string) {
const searchResults = await this.search(query);
// 直接调用分析 Agent
const analysis = await fetch("http://analysis-agent:3002/api/message", {
method: "POST",
body: JSON.stringify({ data: searchResults }),
}).then((r) => r.json());
return analysis;
}
}
2. 消息队列:通过队列实现异步通信,解耦 Agent 之间的依赖。
class MessageBus extends EventEmitter {
publish(channel: string, message: any) {
this.emit(channel, message);
}
subscribe(channel: string, handler: (message: any) => void) {
this.on(channel, handler);
}
}
class Agent {
constructor(private name: string, private bus: MessageBus) {
this.bus.subscribe(`agent:${this.name}`, this.handleMessage.bind(this));
}
sendTo(targetAgent: string, message: any) {
this.bus.publish(`agent:${targetAgent}`, {
from: this.name,
data: message,
timestamp: Date.now(),
});
}
}
3. 发布订阅:使用 Redis Pub/Sub 实现广播和主题订阅,适合一对多通知场景。
4. 协调者模式:通过中央协调器管理所有 Agent 通信和任务编排。
class AgentCoordinator {
private agents = new Map<string, Agent>();
registerAgent(agent: Agent) {
this.agents.set(agent.id, agent);
}
async orchestrate(workflow: Workflow) {
const results = [];
for (const step of workflow.steps) {
const agent = this.agents.get(step.agentId);
const result = await agent!.execute(step.task);
results.push(result);
}
return results;
}
}
// 使用
const coordinator = new AgentCoordinator();
coordinator.registerAgent(new SearchAgent("search-1"));
coordinator.registerAgent(new AnalysisAgent("analysis-1"));
await coordinator.orchestrate({
steps: [
{ agentId: "search-1", task: "search for AI news" },
{ agentId: "analysis-1", task: "analyze results" },
],
});
通信协议对比¶
协议 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
HTTP/REST | 简单请求-响应 | 简单、广泛支持 | 无法推送、开销大 |
WebSocket | 实时双向通信 | 低延迟、双向 | 连接管理复杂 |
gRPC | 微服务间通信 | 高性能、类型安全 | 学习曲线陡峭 |
Message Queue | 异步任务处理 | 解耦、可靠 | 增加系统复杂度 |
关键技术点¶
消息格式:定义统一的消息结构,包含 id、from、to、type、payload、timestamp 等字段,支持优先级和回复链。
错误处理:实现重试机制(指数退避),最大重试次数 3 次,记录失败原因便于排查。
消息追踪:记录消息的完整生命周期(发送、接收、处理、完成),便于调试和监控。
常见追问¶
Q: 如何保证消息顺序性?
使用序列号机制:每条消息分配递增的序列号,接收端按序列号排序后处理。或使用单一消息队列保证 FIFO 顺序。
Q: 如何处理通信超时?
使用 Promise.race
实现超时控制,默认 5 秒超时。超时后触发重试机制或降级处理。
Q: 如何实现负载均衡?
两种策略:轮询(Round Robin,依次分配)或最少连接(选择当前负载最低的 Agent)。根据 Agent 的响应时间和成功率动态调整权重。
Agent vs LLM 区别与优缺点¶
核心区别¶
理解 LLM 和 Agent 的区别是选择技术方案的前提。
LLM(Large Language Model)是大语言模型,本质是一个文本生成系统。它接收文本输入(Prompt),输出文本回复(Completion)。LLM 的能力边界是文本理解和生成,它不能直接访问外部数据、不能执行操作、不能使用工具。LLM 是被动的,只能响应用户输入,不会主动行动。
Agent(智能代理)是基于 LLM 构建的自主决策系统。它使用 LLM 作为"大脑"进行推理和决策,但能力远超 LLM。Agent 可以主动规划任务步骤、调用外部工具(搜索、计算器、API)、访问实时数据、执行多步骤操作、根据中间结果调整策略。Agent 是主动的,可以自主完成复杂任务。
类比:LLM 像一个博学的顾问,你问什么它答什么,但它不能帮你做事。Agent 像一个助理,不仅能回答问题,还能帮你搜索信息、预订机票、发送邮件、分析数据。
维度 | LLM | Agent |
---|---|---|
能力 | 文本理解和生成 | 规划、决策、执行、使用工具 |
交互 | 单次问答 | 多轮对话、主动行动 |
工具 | 不能调用外部工具 | 可调用 API、数据库等 |
记忆 | 仅限上下文窗口 | 可持久化长期记忆 |
成本 | 低(单次调用) | 高(多次调用) |
响应 | 快(1-3 秒) | 慢(5-30 秒) |
使用场景对比¶
LLM 适用场景
文本生成类:写作辅助(生成文章、邮件、广告文案)、翻译(多语言互译)、摘要(长文本压缩)、改写(调整语气和风格)。这些任务只需要文本输入输出,不需要外部数据。
问答系统:基于已有知识回答问题。例如"什么是 React?"、"解释闭包的概念"。LLM 的训练数据包含大量知识,可以直接回答。但无法回答实时问题(如"今天天气")或个性化问题(如"我的订单状态")。
内容分类:判断文本的类别(新闻/娱乐/科技)、情感(正面/负面/中性)、意图(咨询/投诉/建议)。输入文本,输出分类标签。
代码辅助:代码补全、代码解释、代码审查、生成单元测试。GitHub Copilot 就是基于 LLM 的代码助手。
特点总结:任务简单(单步完成)、不需要外部数据(LLM 内部知识足够)、单次交互(一问一答)、响应快速(1-3 秒)、成本低(单次调用)。
Agent 适用场景
需要外部数据:查询实时信息(天气、股票、新闻)、访问数据库(用户订单、商品库存)、调用第三方 API(地图、支付、物流)。这些数据 LLM 无法直接获取,必须通过工具调用。
多步骤任务:研究报告(搜索信息 → 分析数据 → 生成报告)、旅行规划(查询航班 → 预订酒店 → 安排行程)、数据分析(查询数据 → 清洗处理 → 可视化展示)。需要将复杂任务分解为多个步骤,逐步执行。
自动化工作流:定时任务(每天早上发送天气预报)、触发式任务(收到邮件自动分类和回复)、批量操作(批量处理文件、批量发送通知)。Agent 可以自主执行,无需人工干预。
需要记忆:长期对话(记住用户偏好和历史)、上下文管理(跨多轮对话保持一致性)、学习优化(根据反馈改进策略)。LLM 只有短期记忆(上下文窗口),Agent 可以持久化长期记忆。
决策和规划:根据中间结果动态调整策略。例如搜索结果不理想时换关键词重新搜索,API 调用失败时切换备用方案。Agent 具备自主决策能力。
特点总结:任务复杂(多步骤)、需要工具支持(调用外部资源)、多轮交互(持续对话)、响应较慢(5-30 秒)、成本较高(多次 LLM 调用)。
混合使用策略¶
实际应用中通常结合两者优势:先用 LLM 快速判断任务复杂度,简单任务直接用 LLM 响应(快速低成本),复杂任务才调用 Agent(功能完整但成本高)。
成本与性能对比¶
成本:LLM 单次调用成本低(GPT-4 约 $0.03/1K tokens),Agent 通常是 LLM 的 3-10 倍(需要多次调用:规划、执行、总结等)。
性能:LLM 响应快(1-3 秒),Agent 响应慢(5-30 秒,取决于任务复杂度和工具调用次数)。
常见追问¶
Q: 什么时候应该从 LLM 升级到 Agent?
当出现以下需求时:需要调用外部 API 或数据库、任务需要多个步骤、需要根据中间结果动态调整策略、需要长期记忆和上下文管理、需要自动化复杂工作流。
Q: 如何降低 Agent 成本?
四个策略:缓存相同任务的结果、使用便宜模型(GPT-3.5)做初步规划、只在关键步骤使用高级模型(GPT-4)、优化 Prompt 减少 Token 消耗。
Q: 如何保证 Agent 可靠性?
关键措施:验证输入输出、设置超时(如 30 秒)、实现降级策略(Agent 失败时降级到 LLM)、完善错误处理和重试机制、记录完整日志便于排查问题。
对话上下文传递¶
核心思路¶
对话上下文管理是 Agent 系统的核心挑战之一。上下文(Context)是指对话的历史信息,包括用户的所有问题和 Agent 的所有回复。LLM 需要上下文才能理解当前问题的背景,生成连贯的回复。
核心挑战是 Token 限制。LLM 的上下文窗口有限(GPT-3.5 是 4K tokens,GPT-4 是 8K/32K tokens),超出限制就无法处理。而一次完整对话可能有几十轮甚至上百轮,如果全部保留会超出限制。
解决思路是在 Token 限制内保留最有价值的信息。不是简单地保留最近 N 条消息,而是智能地选择哪些信息重要、哪些可以丢弃。主要策略包括:
限制消息数量:最简单的方法,保留最近 10 条消息,超出的自动删除。适合简单场景,但可能丢失重要信息。
Token 优化:估算每条消息的 Token 数量,超出限制时移除最旧的消息。比限制数量更精确,但仍然可能丢失重要信息。
智能摘要:用 LLM 对旧消息生成摘要,只保留摘要和最近消息。可以在有限空间内保留更多信息,但摘要可能丢失细节。
分层管理:将上下文分为不同层级(系统级、会话级、对话级、即时级),按优先级组合。灵活性高,可以精确控制保留哪些信息。
向量检索:将所有消息存入向量数据库,基于语义相似度检索相关历史。可以跨越时间限制,检索任意历史信息,但实现复杂。
实际应用中通常组合多种策略,根据场景选择最合适的方案。
五种管理策略详解¶
1. 基础管理:滑动窗口
最简单直接的方法,保留最近 N 条消息(如 10 条),超出的自动删除。就像一个固定大小的窗口,随着对话进行不断滑动。
实现原理:使用数组存储消息,每次添加新消息时检查数组长度,超过限制就删除最旧的消息(slice 操作)。
class ConversationContext {
private messages: Message[] = [];
private maxMessages = 10;
addMessage(role: "user" | "assistant", content: string) {
this.messages.push({ role, content, timestamp: Date.now() });
if (this.messages.length > this.maxMessages) {
this.messages = this.messages.slice(-this.maxMessages);
}
}
}
优点:实现简单、性能好(O(1) 复杂度)、可预测(总是保留最近的消息)。
缺点:可能丢失重要信息(如对话开始时的关键背景)、无法处理超长消息(单条消息可能就超过 Token 限制)、不够智能(不考虑消息的重要性)。
适用场景:简单对话应用、对话轮数较少(10 轮以内)、不需要长期记忆。
2. Token 优化:精确控制
不是简单限制消息数量,而是估算 Token 数量,精确控制在限制范围内。
实现原理:每次添加消息时估算总 Token 数(中文约 1.5 字符/token,英文约 4 字符/token)。超出限制时,优先保留系统消息(始终需要)和最近消息(最相关),移除中间的旧消息。
优点:更精确(不会浪费 Token 空间)、灵活(可以根据模型调整限制)、保留关键信息(系统消息和最近消息)。
缺点:估算不够准确(实际 Token 数可能不同)、实现稍复杂、仍然可能丢失重要信息。
适用场景:需要精确控制成本、对话较长(10-20 轮)、有明确的系统指令需要保留。
优化技巧:使用 tiktoken 库精确计算 Token 数(OpenAI 官方库),而非简单估算。为不同类型的消息设置不同优先级(系统消息 > 用户问题 > Agent 回复)。
3. 智能摘要:压缩历史
当消息超过阈值时,用 LLM 对旧消息生成摘要,只保留摘要和最近消息。就像把一本书压缩成目录和最后几章。
实现原理:设置摘要阈值(如 10 条消息)。超过阈值时,将前 N-3 条消息发送给 LLM,生成简短摘要(如 200 字)。用摘要替换旧消息,保留最近 3 条消息。
优点:可以在有限空间内保留更多信息、适合长对话(几十轮甚至上百轮)、保持对话连贯性。
缺点:摘要可能丢失细节、需要额外的 LLM 调用(增加成本和延迟)、摘要质量依赖 LLM 能力。
适用场景:长对话场景(客服、咨询)、需要保留完整上下文、对成本不敏感。
优化技巧:增量摘要(每次只摘要新增的消息,而非全部重新摘要)、分段摘要(将对话分为多个段落,分别摘要)、关键信息提取(提取人名、时间、地点等关键信息,单独保留)。
4. 分层管理:结构化上下文
将上下文分为不同层级,每层有不同的生命周期和优先级。就像操作系统的内存管理,分为栈、堆、静态区。
层级划分:
- 系统级:系统指令和角色设定,始终保留,如"你是一个专业的 AI 助手"
- 会话级:用户信息和偏好,整个会话期间保留,如用户名、语言偏好
- 对话级:历史消息,根据策略保留或删除,如最近 10 条消息
- 即时级:当前消息,处理完后可以丢弃,如用户刚输入的问题
实现原理:使用 Map 或对象存储不同层级的上下文,按优先级组合成最终的 Prompt。系统级和会话级始终保留,对话级根据策略管理,即时级临时使用。
优点:灵活性高(可以精确控制每层的内容)、结构清晰(便于维护和调试)、可扩展(容易添加新层级)。
缺点:实现复杂(需要管理多个层级)、需要仔细设计(层级划分不当会影响效果)。
适用场景:复杂应用(需要管理多种类型的上下文)、多用户系统(每个用户有独立的会话级上下文)、需要精细控制。
5. 向量检索:语义记忆
将所有消息存入向量数据库,基于语义相似度检索相关历史。不是按时间顺序保留,而是按相关性检索。
实现原理:每条消息通过 Embedding 模型(如 OpenAI text-embedding-ada-002)转换为向量,存入向量数据库(如 ChromaDB、Pinecone)。用户提问时,将问题也转换为向量,检索最相似的历史消息(如 Top 5),作为上下文。
优点:可以跨越时间限制(检索任意历史信息)、基于语义相关性(而非时间顺序)、适合长期记忆(几百轮甚至几千轮对话)。
缺点:实现复杂(需要向量数据库)、成本较高(Embedding 调用和数据库存储)、检索可能不准确(语义相似不等于真正相关)。
适用场景:需要长期记忆(如个人助理)、知识库问答(检索相关文档)、多主题对话(根据当前主题检索相关历史)。
优化技巧:混合检索(结合向量检索和关键词检索)、重排序(检索后根据时间、重要性重新排序)、缓存热门查询(避免重复 Embedding 调用)。
关键技术点¶
持久化存储策略
上下文需要持久化,避免刷新页面后丢失。根据数据量和使用场景选择不同的存储方案:
localStorage:浏览器本地存储,容量 5-10MB,适合短期上下文(最近几轮对话)。优点是实现简单、读写快速、无需后端。缺点是容量有限、无法跨设备、用户可以清除。使用场景:临时对话、不需要长期保存、单设备使用。
IndexedDB:浏览器数据库,容量无限制(受磁盘空间限制),适合大量数据(完整对话历史)。优点是容量大、支持索引查询、性能好。缺点是 API 复杂(需要封装)、异步操作、用户可以清除。使用场景:长期对话、需要查询历史、单设备使用。
云端存储:后端数据库(MySQL、MongoDB),容量无限制,适合跨设备同步。优点是数据安全、可跨设备、支持备份恢复。缺点是需要后端支持、网络延迟、增加成本。使用场景:多设备同步、需要数据安全、企业应用。
混合方案:本地存储 + 云端同步。本地存储提供快速访问,云端存储提供持久化和同步。定期将本地数据同步到云端(如每 5 分钟或每 10 条消息),页面加载时从云端恢复数据。配合 WebSocket 实现实时同步(一个设备的更新立即推送到其他设备)。
压缩算法
当上下文过大时,需要压缩以节省空间和 Token。
基于重要性评分:不是所有消息都同等重要。用 LLM 评估每条消息的重要性(0-1 分),保留高分消息,删除低分消息。评估标准:是否包含关键信息(人名、时间、地点)、是否是用户的核心问题、是否是 Agent 的关键决策。实现时可以批量评估(一次评估多条消息),降低成本。
基于相似度去重:对话中可能有重复或相似的内容。计算消息之间的相似度(使用 Embedding 向量的余弦相似度),相似度 >0.9 视为重复,只保留一条。例如用户多次问"今天天气怎么样",只保留第一次的问答。
基于时间衰减:越久远的消息越不重要。为每条消息设置权重,随时间衰减(如每天衰减 10%)。删除消息时优先删除权重低的。这样可以保留最近的消息,同时保留久远但重要的消息(如对话开始时的背景介绍)。
基于主题聚类:将消息按主题聚类(使用 K-means 或 DBSCAN),每个主题保留代表性消息(如最长的或最完整的)。适合多主题对话,可以保留每个主题的核心信息。
多会话管理
用户可能同时进行多个对话(如同时咨询不同问题),需要管理多个会话。
会话标识:每个会话分配唯一 ID(UUID),用于区分不同对话。前端在请求中携带 sessionId,后端根据 sessionId 加载对应的上下文。
会话存储:使用 Map 或对象存储多个会话的上下文。key 是 sessionId,value 是 ConversationContext 对象。支持快速查找和更新。
会话切换:用户可以在不同会话间切换。前端展示会话列表(标题、最后消息、时间),用户点击切换。切换时加载对应会话的上下文,渲染历史消息。
会话清理:定期清理过期会话,释放内存。设置过期时间(如 1 小时未活动),超时的会话自动删除。可以使用定时任务(setInterval)或懒清理(访问时检查)。
会话持久化:将会话列表持久化到 localStorage 或云端,页面刷新后恢复。每个会话的上下文也需要持久化,避免丢失。
会话限制:限制每个用户的最大会话数(如 10 个),超过限制时提示用户删除旧会话。避免无限创建会话导致资源耗尽。
常见追问¶
Q: 如何处理超长对话?
三种策略组合:分段摘要(每 10 条消息生成一个摘要)、保留关键消息(包含问题、决策、结论的消息)、保留最近消息(最近 5 条)。最终组合为:摘要 + 关键消息 + 最近消息。
Q: 如何实现跨设备同步?
方案:云端存储上下文,通过 API 同步(POST 上传、GET 下载)。实时同步使用 WebSocket,新消息自动推送到所有设备。需处理冲突(如时间戳优先)。
Q: 如何优化检索性能?
建立关键词索引:提取消息中的关键词,建立关键词到消息索引的映射。检索时根据查询关键词快速定位相关消息。对于语义检索,使用向量数据库(如 ChromaDB)更高效。
扩展阅读¶
相关文档:网络通信(SSE、WebSocket)、性能优化(并发请求处理)
推荐资源:
- LangChain.js 官方文档 - 完整 API 和教程
- OpenAI Function Calling - 工具调用指南
- Vercel AI SDK - 前端 AI 开发框架
最后更新:2024-10