《WaLiOffice》LLM 客户端与流式 SSE 实现:多 Key 轮询、模型选择与 StreamEvent 事件流设计

发布时间:2026/9/26 2:30:04
《WaLiOffice》LLM 客户端与流式 SSE 实现:多 Key 轮询、模型选择与 StreamEvent 事件流设计 文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载导读本文是 WaLiOfficeAI Agent 智能办公平台第 2-2 节的核心讲解聚焦于整个平台大脑——LLM 客户端的工程化封装。你将从这里掌握两套客户端的设计LlmClient非流式调用服务 ReAct 循环与LlmStreamClient流式调用支撑前端 SSE 实时打字效果并深入理解多 Key 轮询、容错重试、extract_json容错解析与StreamEvent事件枚举等 Agent 工程里的通用基础设施。读完本文你将具备独立封装一个 OpenAI 兼容 LLM 客户端、并把它接入 Agent 循环与流式前端的完整实战能力。一、为什么 LLM 调用封装这么复杂5 个必须解决的工程问题上一节第2-1节工程初始化与项目结构我们把 WaLiOffice 的工程架子搭好并成功跑通但服务里还没有任何功能。第一个要填充的模块就是 LLM 客户端——用户说一句话Agent 要调用 LLM 理解意图、决定工具、生成内容没有它后面的 ReAct 循环、工具调用、产物生成全是空中楼阁。表面看LLM 调用封装只是调个 API但落到真实工程里有 5 个绕不开的问题#工程问题说明1多 Key 轮询一个 API Key 触发限速429或鉴权失败401/403时怎么办要能自动换 Key 继续调用2流式 SSE用户输入后要实时看到 AI 在打字不能等 AI 全部生成完再一次性返回3模型选择不同用户、不同任务可能需要不同的模型要支持按用户 Profile 自动匹配4容错重试LLM 服务不稳定401/403/429/5xx要自动重试而不是直接把错误抛给用户5视觉输入用户上传图片时要支持多模态图文混合消息输入这 5 个问题正是本章本节 后续第 2-32-8 节要逐一解决的基础设施。二、LLM 客户端分层架构双客户端 共享逻辑WaLiOffice 的 LLM 调用封装拆分为两个客户端它们共享同一套 HTTP 请求逻辑Key 轮询、重试判定区别只在等完整响应还是边收边推客户端用途特点LlmClient非流式调用ReAct 循环中调用工具后的总结、工具内结构化 JSON 生成支持多 Key 轮询、模型选择、工具调用LlmStreamClient流式调用前端 SSE 推送返回mpsc::ReceiverStreamEventchannel逐个推送事件两者之间的差异可以一句话概括LlmClient等待完整响应返回ChatCompletionResponseLlmStreamClient实时解析 SSE 事件通过 channel 逐个推送StreamEvent。从本节配套的架构图可以更直观地看到这层结构对应仓库图片 walioffice-2-2-01.png架构图清晰地展示出三层设计调用入口层上游调用方是chat.rs、agent_loop.rs、各类tools分别对接两套客户端客户端层LlmClient对应client.rs提供纯文本对话chat()、多模态对话chat_with_attachments()以及for_user()从 DB 加载用户 Profile、extract_json()JSON 容错解析等工具方法LlmStreamClient对应stream.rs提供流式对话stream_chat()返回Receiver接收流数据基于 Tokio runtime 实现 SSE 解析tokio::spawn 128 容量的 channel并定义流式事件枚举StreamEvent共享逻辑层client.rs中的独立公共函数包括密钥轮询rotate_keys()、模型选择优先级apply_profile()、重试判定should_retry_next_key()针对 401/403/429/5xx、请求体构造build_request_messages()、模型兼容性校验is_chat_compatible_model()排除纯媒体模型。底层 HTTP 依赖统一由reqwest::Client承担通过 POST 请求调用 LLM 的/chat/completions接口并携带 Bearer 鉴权的 API Key通用数据结构集中在types.rs请求消息、对话请求/响应、函数定义、流相关结构。三、LlmClient非流式调用的工程化封装LlmClient服务的是等完整响应的场景——典型如 ReAct 循环中工具调用后的总结以及各工具内部让 LLM 产出结构化 JSON详见 第2-5节ReAct循环核心实现 与 第3-1节Markdown工具。它的封装要点如下。3.1 for_user()按用户创建客户端与模型选择在后端 Chat 路由中创建客户端的标准入口是LlmClient::for_user(user_id, model)——即根据用户身份从数据库加载其 Profile包括其配置的 API Key 与模型偏好再结合本次会话指定的模型创建客户端实例。这一点从 第2-7节后端Chat路由与SSE端点 的chat_stream全流程可以看到POST /api/chat/stream (AuthUser ChatRequest) ↓ ① 意图识别 — IntentAnalyzer.analyze(message, session_id, has_image) ↓ ② 创建 LLM 客户端 — LlmClient::for_user(user_id, model) ↓ ③ 会话管理 — 有 session_id 就加载没有就创建新会话 ...模型选择在共享逻辑层由apply_profile()实现按用户 Profile 的优先级规则挑选合适模型。WaLiOffice 整体支持多 LLM 端点分离配置——文本、图片、视频分别对接独立 API见 walioffice.md 的多 LLM 端点分离配置文本/图片/视频独立 API设计因此这里模型选择的语义是在对话/文本链路中结合用户 Profile 与任务类型确定本次调用的 chat 模型。3.2 多 Key 轮询rotate_keys() 与独立 scope 游标多 Key 轮询是整个平台限流应对的基础设施rotate_keys()是它的核心实现。从架构图与配套流程图walioffice-2-2-02.png可以看到其设计要点轮询游标基于Lazy懒初始化的 Mutex 游标实现 round-robin 轮换每次调用从游标位置取下一个 Keyscope 隔离支持default、deepseek等自定义 scope每个 scope 独立维护自己的轮询游标避免不同用途的 Key 池互相干扰Key 来源密钥列表来自 DB 的user_profiles.api_keys用户在 Profile 中配置、逗号分隔多 Key解析后形成有序 Key 列表ordered_keys全量有序返回——这一约定同样出现在 第3-8节图像生成——API调用与多Key轮询 的AgnesCredentials设计中。3.3 容错重试should_retry_next_key() 的判定规则配合轮询的是重试判定should_retry_next_key()。完整流程如下对应流程图调用 LLM API/chat/completionsBearer Key 鉴权HTTP 200 OK直接解析接口响应并返回结果非 200先判定是否可重试——仅 401鉴权失败、403权限不足、429限流、5xx服务端错误判定为可重试其他错误如 4xx 参数错误直接终止不再浪费 Key可重试时检查是否还有未尝试的密钥有则记录 WARN 日志、切换到下一个 Key 重试没有则判定所有 Key 全部失败返回错误结果。这一按序换 Key 有界重试的机制在多 Key 池场景下能把单 Key 失效/限流对用户体验的影响降到最低。图像生成链路第 3-8 节更进一步套用了两层重试外层generate_agnes_image两次尝试间隔 2s 内层post_json_url按序换 Key401/403/429/5xx 触发可见这套判定规则在整个项目中是统一复用的。3.4 多模态调用chat_with_attachments() 的三级降级针对用户上传图片的视觉输入需求LlmClient提供chat_with_attachments()多模态对话。架构图显示其采用三级降级策略从高到低逐级回退视觉 工具完整的多模态消息图片 工具定义能力最全纯视觉无工具保留图片理解能力但不再携带工具调用纯文本最终兜底只用文字继续对话。这样即使模型或链路某环节不支持工具调用对话也不会中断而是降级到能力更小的模式继续完成。3.5 extract_json()LLM 输出的容错解析LLM 输出 JSON 时经常包裹 Markdown 代码围栏fence或附带多余文字直接serde_json::from_str必然失败。extract_json()专为此设计采用两级容错去 Markdown fence剥离json ...之类的代码围栏包裹截取花括号在剩余文本中定位首个{与末尾}截取中间内容再解析。在 Excel 等结构化场景中它扩展为三级降级去围栏 → 首尾大括号截取 → 数组截取见 第3-4节Excel工具与XLSX渲染。这一解析能力是后续所有LLM 直出结构化数据工具的公共底座——Markdown、Word、Excel、ECharts 图表的链路中都出现extract_json的身影见 第3-2节Word工具、第3-5节ECharts图表工具解析失败时各工具还有降级草稿兜底确保始终有产物返回。3.6 is_chat_compatible_model()模型兼容性判断平台同时对接文本模型与图片/视频等媒体模型但媒体模型不能用于 chat 对话。is_chat_compatible_model()就是这层准入校验判断给定模型是否为可对话chat 兼容的模型排除纯媒体模型。它保证LlmClient/LlmStreamClient在按 Profile 或按端点选模型时不会把一个只用于生图的模型塞进对话链路。四、LlmStreamClient流式调用与 SSE 事件解析前端对话页要实时看到 AI 打字就必须走 SSEServer-Sent Events。LlmStreamClient是这条链路的客户端侧封装。4.1 stream_chat() 与 mpsc channelstream_chat()是流式对话的入口它不返回完整响应而是返回一个mpsc::ReceiverStreamEventchannel。调用方Chat 路由拿到 Receiver 后把它包装成SseReceiverStream直接推给前端见 第2-7节后端Chat路由与SSE端点 的 SSE 端点设计。4.2 StreamEvent 枚举三种事件的统一抽象流式数据不能一坨字符串往前端推StreamEvent枚举把流内容规范化为三类事件事件语义Delta(String)文本增量模型生成的一段文字前端逐段追加渲染ToolCallDelta工具调用增量模型开始/正在请求调用工具的相关增量信息Done流结束整个响应完成前端据此终止接收这样一个增量 收尾的枚举既支撑了打字机效果的文本流也为 Agent 场景下的工具调用留出了事件位。值得对照的是ReAct 循环对外面向 Chat 路由推送的是更上层的AgentEvent——WaLiOffice 通过 mpsc channel 推送 7 种事件Thinking / ToolCall / ToolResult / Artifact / Message / TurnEnd / Done实现前端实时反馈见 walioffice.mdStreamEvent是其中LLM 客户端 → Agent/路由这一层的细粒度事件二者是上下游关系。4.3 tokio::spawn 异步解析与 channel 容量架构图显示流式解析基于 Tokio runtime客户端通过tokio::spawn启动异步任务消费 LLM 的 SSE 流逐条解析事件并通过128 容量的 mpsc channel 传递给调用方。异步任务 有界 channel 的组合带来两个好处背压可控channel 有固定容量128消费者前端推送跟不上时不会被无限积压拖垮内存与 axum 异步模型天然契合Receiver 可以直接桥接为 SSE 响应流解析、转发、落盘全部异步化。五、与其他模块的串联从 LLM 客户端到完整对话链路LLM 客户端不是孤立的它在整个 WaLiOffice 对话链路中的位置如下Chat 路由第 2-7 节chat_stream入口用LlmClient::for_user(user_id, model)创建客户端意图识别后用run_agent_loop启动 Agent 循环循环中通过非流式LlmClient做总结ReAct 循环第 2-5 节循环内组装消息历史 → 调用 LLM携带工具定义→ 检查 tool_calls → 执行工具 → 回到循环这里的调用 LLM走的是LlmClient的非流式封装超过max_turns默认 8 轮时用无工具的 LLM 调用生成总结避免无限循环工具内部第 3 章Markdown / Word / Excel / 图表等工具各自调用LlmClient.chat()让 LLM 产出结构化 JSON再经extract_json容错解析成工具产物前端 SSE第 2-8 节LlmStreamClient的StreamEvent流最终经 Chat 路由的 SSE 端点到达前端对话界面实现实时渲染。所以本节完成的双客户端 共享逻辑基础设施是后续 第2-3节数据库设计与会话持久化、第2-4节工具Trait定义与注册表机制、第2-5节ReAct循环核心实现 乃至第三章全部工具落地的地基。六、小结本节完成了 WaLiOffice 智能体的大脑接入层核心收获可以归纳为一条设计主线分层LlmClient非流式与LlmStreamClient流式职责分离共享 Key 轮询与重试逻辑避免重复代码健壮性rotate_keys()按 scope 独立轮询 should_retry_next_key()只对 401/403/429/5xx 重试配合extract_json容错解析与多模态三级降级让服务不稳定不再直接表现为功能不可用流式体验stream_chat()通过 128 容量 mpsc channel 推送Delta / ToolCallDelta / Done三类StreamEvent为前端打字机式渲染和 Agent 工具调用反馈奠定基础准入控制is_chat_compatible_model()保证媒体模型不会误入对话链路。掌握这套 LLM 客户端封装你就能在任何 OpenAI 兼容的 LLM 项目里复用它——多 Key 高可用、流式响应、结构化输出容错解析这些能力恰好是 AI Agent 工程最通用的三类基础件。赞分享文档教程后端【免费下载链接】CodeGuide:books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总旨在为大家提供一个清晰详细的学习教程侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助请给予支持(关注、点赞、分享)项目地址https://gitcode.com/gh_mirrors/code/CodeGuide点击查看免费下载相关推荐Dify Agent 官方示例实践从 Pydantic AI 网关模型到 Run 事件轮询、同步客户端与 SSE 流式消费Dify Agent 官方示例实践从 Pydantic AI 网关模型到 Run 事件轮询、同步客户端与 SSE 流式消费 本文基于 Dify Agent 示人工智能大模型LLMOpsAI 应用RAGAI Agent低代码终极Chokidar性能调优指南轮询模式与事件驱动的最佳选择终极Chokidar性能调优指南轮询模式与事件驱动的最佳选择 Chokidar作为一款轻量级且高效的跨平台文件监听库为开发者提供了强大的文件系统监控能力。本开发工具ToastFish 完整教程用 Windows 通知栏背英语和日语单词ToastFish 完整教程用 Windows 通知栏背英语和日语单词 上班时不敢打开背单词软件、上课又总找借口说没时间这是很多人记不住单词的真实原因。桌面应用教育上一篇如何15分钟搭建个人专属的微信公众号RSS订阅服务下一篇WechatMoments微信朋友圈导出工具从新手到高手的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询