ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

中后台框架AI能力分层设计:模型网关、Agent工作流与流式交互实践

中后台框架AI能力分层设计:模型网关、Agent工作流与流式交互实践 去年年底我在重构团队内部的中后台框架时最头疼的一件事就是所有人都喊着要接入AI但没有人说清楚AI到底落在哪个位置。把聊天窗口塞进后台做几个会总结的组件还是干脆搞一堆Agent挂在菜单栏里GoWind Admin风行这套企业级全栈中后台框架的AI模块是我带着这个疑问做了几轮原型之后沉淀下来的方案。它解决的问题很实在AI不是后台里一个孤零零的智能助手页面而是渗透到表单、列表、报表、工单、知识库这些具体业务场景里的能力层。这篇文章不聊概念只讲GoWind Admin里AI模块是怎么分层设计的、模型接入层怎么做路由、Agent工作流怎么编排、流式交互有哪些工程细节以及我在真实项目中踩过的坑。适合正在做中后台框架、想把AI能力合理嵌入现有业务系统的人参考。1. AI模块的定位不是一个聊天页面而是整个后台的基础设施1.1 为什么单独的聊天窗口在企业场景里不实用大部分中后台框架第一次接入AI时做的都是同一个东西右上角加一个悬浮按钮点开是个聊天窗口让用户想问什么问什么。Demo阶段很惊艳上线两周后基本没人用。原因很简单——聊天窗口和业务是断开的。我举一个实际例子。运营人员在工单系统里遇到一个疑难工单他的真实诉求是这个客户的近30天退款率是不是异常。你把这个问题丢给聊天助手模型不知道客户ID是多少看不到订单数据也没权限查数据库它只能给出一个无比正确的废话。真正好用的体验应该是用户在工单详情页AI按钮已经把分析该客户退款异常和起草回复邮件两个动作直接列出来了点一下结果嵌在当前页面里。这就是GoWind Admin AI模块的核心定位转变AI不是独立应用而是模块级的服务能力。1.2 风行框架里AI模块的整体分层在GoWind Admin里AI模块按职责分成四层每一层都可以被业务模块单独调用分层核心职责典型落地接入层屏蔽不同模型服务的差异统一模型网关、供应商路由、参数标准化能力层封装可复用的AI业务能力知识库问答、内容生成、数据洞察、工具调用编排层让AI完成多步骤复杂任务Agent工作流、任务分解、多Agent协作应用层以组件形式嵌入业务页面智能工单助手、报表问答、代码辅助、文档摘要这四层只从上往下调用。应用层不需要关心底层用的是哪家模型、是串行调用还是并行调用接入层也不允许直接浮到业务页面里去。这样划分之后业务模块接入AI的成本低很多——不是从零对接模型API而是直接调用能力层提供的方法或者基于编排层配置一条工作流。1.3 设计时定的几条硬性约束我在这套模块上定了几个原则后来验证下来都是对的写在这里供参考第一所有模型调用必须走网关禁止业务代码里直接写某个大模型厂商的SDK。理由后面细说简单讲就是模型更新换代太快绑定一家就跟把数据库SQL写死在业务代码里一样痛苦。第二AI操作必须有权限判断。用户能不能让AI查某个工单、读某份文档、执行某个动作全部走现有RBAC体系AI不拥有超越用户身份的权限。第三AI输出必须可追溯。每次生成结果都记录模型版本、输入参数、消费token、耗时、命中知识库的文档片段这是企业落地AI的底线要求。第四所有异步长任务超过10秒的生成任务都要支持任务化——提交任务后用户可以离开页面完成后通过消息推送或者刷新任务中心查看结果。不能把HTTP请求卡死等模型出结果。2. 模型接入层的架构设计让换模型变成改一行配置2.1 统一协议抽象的必要性到今天为止各家大模型服务的API虽然都在往OpenAI兼容协议靠拢但细节差距依然非常大有的模型支持JSON输出约束有的不支持有的有独立的Embedding接口有的走统一接口有的流式返回字段名都不一样更别说温度、top_p这类参数在各家的默认行为差异。如果业务代码直接对接具体某个模型服务那每次模型升级、更换供应商都要改业务代码、重新测试。更重要的是同一个业务场景可能需要根据成本和质量要求用不同模型——简单分类用轻量模型复杂推理用顶级模型——如果代码写死了一家这条路直接堵死。GoWind Admin的模型网关就是一个独立服务它在所有模型供应商前面做了一层统一抽象对外暴露固定的接口协议内部再通过适配器模式连接到具体的模型服务。2.2 网关的核心接口设计网关对业务方暴露的核心接口做了简化大约是这样一组Go接口type ModelProvider interface { // 非流式对话补全 ChatCompletion(ctx context.Context, req ChatRequest) (*ChatResponse, error) // 流式对话补全 ChatCompletionStream(ctx context.Context, req ChatRequest) (StreamReader, error) // 文本向量化知识库召回用 Embedding(ctx context.Context, texts []string) ([][]float64, error) } type ChatRequest struct { Model string json:model Messages []ChatMessage json:messages Temperature float64 json:temperature,omitempty MaxTokens int json:max_tokens,omitempty Stream bool json:stream,omitempty Tools []ToolDef json:tools,omitempty ResponseFormat *RespFmt json:response_format,omitempty }所有模型供应商都实现这组接口。OpenAI兼容的供应商直接走通用适配器只需要配置base_url和api_key非兼容的供应商写一个轻量适配器做字段映射。业务方在代码里只依赖这个接口不感知背后是哪个模型。配置层面长这样model_providers: - name: default-gpt protocol: openai-compatible base_url: https://api.example.com/v1 api_key_env: LLM_API_KEY models: - model_id: fast-chat # 业务逻辑里用的别名 upstream_model: gpt-4o-mini - model_id: deep-reason upstream_model: claude-sonnet-4业务代码里指定model_id为fast-chat或deep-reason至于这俩背后实际是哪个模型由配置决定。换模型就改配置不碰代码。这个设计在后面某次模型供应商涨价的时候直接救了我们——把流量切到另一家只花了十分钟改配置业务侧无感。2.3 模型路由按任务类型自动选择合适模型模型网关还承担了一个很关键的路由功能。不是用户发一句话就固定用一个模型而是网关根据任务特征自动匹配模型。我把路由策略做成可配置的规则链支持按这些维度决策任务类型普通问答、代码生成、数据分析、知识库检索、内容改写输入长度短文本走轻量模型超长上下文走长上下文模型质量要求比如最终输出给客户看的文案走高质量模型内部草稿走低成本模型成本预算同一类任务可以设置每日预算超预算自动降级到低价模型举一个实际配置片段routing_rules: - name: code-helper match: task_type: code_generation provider: default-gpt model_id: fast-chat # 代码任务用代码能力强的轻量模型 - name: knowledge-qa match: task_type: knowledge_qa input_tokens_gt: 2000 provider: default-gpt model_id: deep-reason # 长上下文知识问答用深度模型有了这个路由层之后业务方完全不用管我这个功能该用哪个模型网关自动决策省了一大堆沟通成本。而且后续要接入新模型只需要加一个路由规则完全不影响已有业务。3. Agent工作流引擎从单轮问答到多Agent协作3.1 单次对话的天然局限第一版AI模块做的是单轮问答用户问一句模型答一句。很快发现这类交互只能解决解释概念翻译文本这种轻量问题一旦遇到帮我分析这批客诉数据并起草一份改善方案就抓瞎——数据在哪获取、用什么方式分析、方案要什么格式这些步骤一个模型调用根本完成不了。要让AI真正处理复杂任务必须引入Agent工作流。一个Agent本质上就是一个会思考的执行器它接收目标自己调用工具获取信息自己判断下一步做什么直到完成整个任务。GoWind Admin的Agent引擎参考业界主流方案做了简化核心由三部分组成任务规划器、工具注册表、执行控制器。3.2 任务规划把大目标拆成可执行的小步骤任务规划层收到用户的复杂请求后先把目标拆成若干子任务。这一步我用的是模型自规划加规则兜底的方式——让主模型输出一个JSON格式的执行计划再通过一个校验器检查计划是否合法比如引用的工具是否存在、步骤是否太笼统校验不过就退回请求模型重写。举例用户提交汇总本月各渠道的销售数据并生成周报{ plan: [ { step: 1, tool: query_sales_data, args: {timeRange: this_month, groupBy: channel}, description: 查询本月各渠道销售数据 }, { step: 2, tool: generate_report_doc, args: {template: weekly_sales, attachData: step1_result}, description: 基于销售数据生成周报文档 }, { step: 3, tool: send_message, args: {channel: internal, target: manager_group}, description: 将周报发送到管理群 } ] }每个步骤的输入可以引用前置步骤的输出这样就有了一条完整的数据链路。任务规划器本身也是可配置的同一个任务在不同业务场景下可以挂不同的规划模板确保规划结果不飘。3.3 工具注册与执行机制工具注册表是Agent能力边界的关键。没有工具调用的Agent只能空谈有了工具注册Agent才能真正的干活。在GoWind Admin里工具就是一组标准化的函数定义注册时包含名称、描述、入参JSON Schema、执行函数四个要素。type ToolDef struct { Name string json:name Description string json:description Parameters map[string]any json:parameters,omitempty // JSON Schema Handler func(ctx context.Context, args map[string]any) (any, error) }业务模块注册起来非常轻RegisterTool(ToolDef{ Name: query_sales_data, Description: 按时间范围和渠道维度查询销售额汇总数据, Parameters: map[string]any{ type: object, properties: map[string]any{ timeRange: map[string]any{type: string}, groupBy: map[string]any{type: string, enum: []string{channel, region, product}}, }, required: []string{timeRange}, }, Handler: func(ctx context.Context, args map[string]any) (any, error) { return salesRepo.Query(ctx, args) }, })执行控制器拿到规划后逐个步骤执行调用对应工具把结果回填给Agent上下文再由Agent决定是继续下一步还是结束任务。整个执行过程会落一条详细的执行轨迹日志方便事后查问题和做成本分析。3.4 多Agent协作的两种模式复杂场景下手写一个巨无霸Agent是噩梦更好的方式是把任务分给多个各司其职的Agent。实际中我主要用两种协作模式串行管线模式适合流程固定的任务。比如数据分析Agent产出结论 → 文案Agent改写为报告口径 → 审核Agent检查合规风险前一个的输出作为后一个的输入每一步职责单一出问题也好定位。并行编排模式适合互不依赖、需要同时处理的子任务。比如市场部门要生成一份综合竞品分析两个子Agent分别研究不同竞品的信息最后汇总Agent合并成一份报告。并行执行能大幅缩短整体耗时汇总结点保证输出格式统一。我在GoWind Admin里提供了一个简单的编排DSL配置一个工作流只需要定义节点和连线节点类型可以是Agent任务、工具调用、条件分支、人工审核。这个设计让我后来接业务方的需求时大部分场景都是调配置很少写新代码。4. 对话交互与流式响应的工程细节4.1 流式输出全链路都要处理流不只是前端打字机如果AI模块的交互只做到请求→等待→一次性渲染全文体验会非常差。中后台场景的真实用户往往同时开着好几个任务几秒甚至几十秒的白屏等待足够让人关掉页面。所以流式输出不是可选项是必需品。但流式这件事的复杂程度往往被严重低估。它不只是前端搞一个打字机效果而是要在整条链路上处理流数据模型服务返回SSE流 → 网关透传并做数据统计 → 应用层聚合增量 → 前端通过WebSocket或HTTP流接收 → 逐段渲染。比较常见的是HTTP协议来流式传输。前端用fetch加上ReadableStream读取再配合AbortController做中断控制async function streamChat(messages: ChatMessage[], onDelta: (text: string) void) { const controller new AbortController(); const response await fetch(/api/ai/chat/stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages, stream: true }), signal: controller.signal }); const reader response.body!.getReader(); const decoder new TextDecoder(); while (true) { const { done, value } await reader.read(); if (done) break; const chunkText decoder.decode(value, { stream: true }); for (const line of chunkText.split(\n)) { if (!line.startsWith(data:)) continue; onDelta(JSON.parse(line.slice(5)).content); } } }服务端则要注意缓冲策略。有个常见问题是模型服务其实已经产出了第一个token但因为网关做了一些聚合逻辑迟迟不把第一个字节写给前端用户看到的还是白屏。我后来的处理方式是网关收到首token立即转发给前端后续业务处理记录token数、查审计信息全部并行做绝不用前置逻辑阻塞首包。4.2 中断、重连与状态恢复模型开始输出后用户随时可能想停止也可能断网、浏览器刷新。这几个场景必须有明确的行为约定。第一中断。前端调AbortController.abort()网关收到连接取消后需要向上游模型服务发送中止信号避免后台继续白跑token烧钱。同时已经生成的增量内容要保存进会话历史不能让用户中断后重开对话就丢失所有内容。第二重连。在做长文档生成的场景里网络抖动是常态。我实现了一个基于任务ID的恢复机制——对话流中断后前端会带着任务ID重新请求网关检测到任务还在执行中直接从上次的最后偏移量继续推送而不是重新开始执行。第三刷新恢复。所有流式会话的执行状态持久化到Redis页面刷新后根据会话ID恢复上下文前端能重新渲染已经收到的内容。这一点在后台系统里很重要因为用户经常在多个页面之间跳来跳去不可能全程盯着一块对话区域。4.3 上下文窗口管理长对话不失控对话轮数一多上下文就会膨胀既浪费token又降低模型响应质量。光靠模型自带的上下文窗口硬扛是不可持续的。我在Agent引擎里实现了三层上下文管理策略滑动窗口裁剪只保留最近N轮对话默认10轮更早的直接裁剪。关键信息提炼每次对话结束时用轻量模型把前面的内容压缩成一段摘要下次对话用摘要加最近几轮完整对话拼成新上下文。业务数据外置从工具调用拿到的数据不塞进上下文而是存成内部索引需要时按需检索。比如销售数据查询结果有500行不会全部塞给模型而是转成分析摘要再加一个可查询的数据引用。这套策略上线后长对话的token消耗下降了约40%而且响应质量明显更稳定——模型不再被大量陈旧信息干扰。5. 权限、审计与内容安全企业级AI模块的底线设计5.1 数据权限必须做在检索层而不是靠模型自觉中后台系统里最危险的AI功能不是回答错了而是不该看的数据被AI看到了。很多团队做AI问答时把知识库或者数据库整库喂给模型这等于让所有用户都能通过AI旁路掉数据权限。这是绝对不行的事。GoWind Admin的做法是数据检索层强制注入权限过滤。AI在调用任何工具获取数据时必须携带当前用户上下文数据源在SQL或索引查询阶段就加上权限条件——用户没有权限的数据检索结果里根本不会出现AI再聪明也只能基于看得见的数据回答。具体来说每次工具执行前会经过一个权限拦截器func (i *Interceptor) BeforeToolExec(ctx context.Context, tool string, args map[string]any) error { user : auth.MustUserFrom(ctx) // 检查该用户是否有工具调用权限 if !policies.Check(user.Roles, tool) { return ErrPermissionDenied } // 给数据查询类工具注入行级权限条件 InjectRowLevelFilter(ctx, user, args) return nil }基于角色判断能不能调用这个工具是粗粒度权限在数据查询工具里注入行级过滤条件是细粒度权限。两层都过才算有权限。5.2 输入侧过滤与输出侧审查企业级场景里AI模块的输入输出都要过一道内容安全网关这个没有妥协余地。输入侧我对用户提交的内容做敏感信息识别和脱敏涉及个人敏感字段手机号、身份证、银行卡号的自动打码后再送入模型。输出侧模型生成的内容会经过关键词过滤和敏感分类器对包含风险的内容进行拦截或改写。这里有一个实操细节输出审查不要放在流式推送完成之后否则用户已经把有风险的内容看到了拦截就是马后炮。我做的方案是流式审查——对增量片段做快速过滤风险内容不入流保证用户看到的每一段都是干净的片段命中高风险标记时网关立即终止这次生成。5.3 审计日志每次AI行为都可追溯我在前面提到AI模块必须可追溯这套日志系统在GoWind Admin里叫AI审计日志覆盖每一次模型调用和Agent执行。审计日志需要记录的最小字段集我列在这里请求会话ID与用户ID、所属部门使用的模型别名与上游真实模型名输入内容摘要与输出内容摘要完整内容做加密存储调用的工具列表与各步骤执行耗时token消耗量输入、输出、缓存命中分别记录触发的内容安全拦截记录模型的置信度/风险标记如果网关给出了的话这些日志不仅用于问题排查还是成本分析的数据基础。没有这套日志用户说AI生成的东西有问题的时候你连是哪个模型、哪次请求、什么prompt产生的都无法定位那个后果在企业环境里是很严重的。6. 成本、性能与稳定性生产环境才能真正暴露的问题6.1 语义缓存同样的内容不要反复花钱生产环境里最容易被忽视的是token成本。我观察到的真实数据是在客服、运营这类高频场景重复的问题占比能到三成以上。用户问退款流程是什么和退款怎么操作语义上是同一件事让模型每次都重新生成一遍既费钱又慢。GoWind Admin的模型网关内置语义缓存请求进来先做向量化去Redis里检索相似度超过阈值我这边用0.92的历史请求命中就直接返回缓存结果不调用模型。为了不让语义缓存误伤需要实时数据的场景我加了一个路由标记——业务方在请求里带上cache: true/false明确控制哪些请求允许命中缓存。缓存命中率在客服类场景大概能到25%到35%节省的token成本相当可观。而且响应速度变快后用户感知到的AI很流畅也会提升。6.2 限流与降级模型服务也会像数据库一样被打挂接入AI之后模型服务自然成了系统的核心依赖。但模型API是一个典型的公有依赖——所有人的流量会挤在同一个上游上游抖动是家常便饭这时候网关没有限流和降级策略整个后台都会跟着抖动。我在网关里做了两层限流一是按调用频率限流防止单个用户刷接口用的是令牌桶二是按token预算限流基于当前周期的剩余预算动态拒绝或降级请求防止月底成本失控。rate_limit: per_user: 20 # 每秒每用户最多20次请求 per_user_embedding: 60 # 向量化接口单独放宽 monthly_token_budget: 200000000 # 每月token预算 over_budget_policy: degrade # 超预算后的策略降级降级策略也分几档超预算后优先把高频低质量任务切到低价模型自动摘要、生成草稿这类非关键路径在模型服务异常时直接降级成规则模板涉及安全风控的任务不允许降级宁可报错也不能给出错误结果。6.3 稳定性设计超时、重试与熔断的三板斧模型API的延迟波动很大P99经常能到几秒甚至十几秒。网关层的超时设计必须明确连接建立超时用3秒首字节超时用10秒流式场景整体生成超时根据任务类型区分普通对话30秒长文档生成放宽到120秒。重试策略要讲究。模型超时之后盲目重试是很危险的事——生成类请求不是幂等的用户可能已经收到一半内容了再重试会导致重复或混乱。我的做法是只在请求还未开始执行的阶段重试比如连接超时、上游返回429或5xx错误码已经收到部分流式内容的请求不自动重试而是将当前状态标记为interrupted引导前端做恢复。熔断器的思路和数据库连接池一样上游连续错误率达到阈值比如50%网关直接熔断一段时间把流量切到另一个供应商的备用模型——前提是路由配置里有备选provider这也是我一开始设计配置化路由的另一个原因。7. 实战中踩过的坑与解决思路7.1 大模型输出JSON的不稳定性Agent场景里规划器需要模型输出JSON格式的计划。理论上各家都声称支持JSON格式约束但实测下来即便开了JSON Mode偶尔也会出现字段缺失、嵌套结构错乱、多余解释文本。尤其在加了Tools定义之后模型返回来强行调用了一个不存在的工具或者参数类型和Schema对不上。踩过一次大坑之后我总结出的对策是约束加容错双保险一方面在请求里显式开启响应格式约束另一方面在解析层做容错处理——先试严格解析失败后做括号配对修复再不行就提示规划器基于历史对话重新生成最多重试两次仍失败就转为人工处理兜底。绝对不要假设模型一定会输出合法JSON这个假设迟早让你在生产环境里吃苦。7.2 不同模型对同一套提示词的兼容性差异用统一网关之后你以为换模型很容易但真正切过去才发现同一套System Prompt在不同模型上的表现差异巨大。有的模型按它训练时的偏好读了反而更好有的模型对Prompt里的示例过于敏感有的模型对少量指令更听话。这不是bug是不同模型的指令遵循风格差异。我现在的做法是Prompt模板按模型族做适配层同一个业务语义维护多套模板变体。网关在选好模型之后会按模型类型匹配合适的模板。模板变体的维护放到后台配置中心产品人员可以直接在线调整不需要改代码重新上线。这比单纯把模型SDK封装一层更实用。7.3 长任务超时引发的异步化改造有一次我把生成一份30页的行业分析报告做成同步接口前端浮层转圈网关死死等模型出完。结果模型生成时间超过了我设的60秒超时连接中断用户那边什么都没拿到钱却已经烧掉了。后来所有预计耗时超过10秒的任务全部改成异步任务模型提交任务立即返回task_id后台由任务队列驱动执行执行中通过WebSocket推送进度事件完成后通知前端拉取结果。这个改造不仅解决了超时问题还给后续扩展多Agent协作提供了很好的基础——Agent运行几分钟甚至十几分钟都是常态没有异步化根本扛不住。7.4 向量检索踩过的embedding兼容坑做知识库问答时我一开始顺手用了A家的Embedding模型建索引后面为了成本换了B家的向量化服务结果检索效果直线下降——大量文档召回不到。排查后发现A家和B家输出的向量维度不一样更关键的是向量空间分布不一致各自索引的数据在对方度量下完全错乱。这个坑的教训是向量索引一旦建立embedding模型就不能随便换换的话必须全量重建索引。正确做法是在网关的Embedding接口上冻结版本升级语义模型要开索引迁移流程先在影子环境用新旧两个向量模型分别跑同一批测试查询确认新区召回质量不低于旧区再切线上。我自己吃过这个亏之后把embedding模型变更必须走索引重建流程写进了框架的操作手册里列为一等变更。向量检索这种方案看似简单版本管理一旦松懈数据质量滑坡往往是悄无声息的。在GoWind Admin实际落地AI模块的过程中我最大的体会是AI能力和传统后台架构的融合最大的阻力往往不是技术而是定位和边界问题。把AI当作基础设施而非页面功能来看待将模型接入、权限控制、成本治理、可观测性做成平台能力之后上面的业务模块无论怎么长都不慌。如果你正在规划自己框架里的AI能力我建议先从模型网关和权限审计这两个基础层动手它们决定了后面垂直场景能走多远。
返回列表