从零搭建工程化AI应用开发平台:Agent编排与多供应商接入实战
发布时间:2026/10/7 6:27:01 锦皓数字建站

1. 为什么我要自己搭一套 AI 应用开发平台先说结论市面上能用的 AI 应用开发平台我基本都试过一遍从纯代码框架到可视化编排工具最后促使我自己动手攒一套的核心原因就三个字——不趁手。要么是编排能力太弱稍微复杂一点的多 Agent 协作就得写一堆胶水代码要么是供应商锁死想换个模型得把整个业务层翻新一遍要么是扩展机制封闭想接个自己的知识库或者自定义工具文档翻半天找不到入口。XXL-AI 这个项目就是在这个背景下长出来的。它的定位很明确一个面向工程化落地的 AI 应用开发平台核心能力覆盖四块——Agent 编排、多供应商接入、MCP SKILL RAG 三位一体的扩展体系、以及一套能扛住生产环境的工程化底座。说白了它想解决的不是“怎么跑通一个 Demo”而是“怎么让 AI 应用从能跑到能上线”。这篇文章适合谁看如果你正在做 AI 应用开发被编排逻辑绕晕过、被供应商切换折磨过、被 RAG 的召回效果气到过那这篇内容应该能给你一些直接能抄的思路。如果你刚入门想了解一个完整的 AI 应用平台到底该有哪些模块也可以顺着往下看我会尽量把每个设计决策背后的“为什么”讲清楚。我先把整体架构的骨架摆出来后面再逐层拆。XXL-AI 的分层逻辑大致是这样的最底层是工程化底座负责配置管理、日志追踪、限流熔断、会话状态这些脏活累活往上是多供应商抽象层把不同模型厂商的 API 差异抹平再往上是扩展层MCP 管工具调用协议SKILL 管可复用的能力封装RAG 管知识注入最顶层才是Agent 编排层负责把上面这些能力串成一条可执行的链路。这个分层不是拍脑袋定的每一层都有它必须独立存在的理由下面我会一层一层说。2. 整体架构设计与分层思路拆解2.1 为什么是四层而不是三层或五层分层这件事少一层会把不同关注点揉在一起多一层会增加无谓的调用开销。我试过把供应商抽象和扩展层合并结果发现 MCP 的工具描述格式和模型厂商的 function calling 格式根本不是一回事硬揉在一起会导致每次加新工具都要动供应商适配代码。也试过把工程化底座拆成“监控层”和“状态层”两层后来发现会话状态和链路追踪本来就是强耦合的——一次请求的 trace 里必须带上会话上下文拆开反而要来回传引用。所以四层是一个经过取舍的平衡点。工程化底座解决的是“稳不稳”的问题供应商抽象层解决的是“换不换得动”的问题扩展层解决的是“能不能长”的问题编排层解决的是“跑不跑得通”的问题。四个问题互相独立所以四层各自独立。2.2 编排层Agent 不是越多越好Agent 编排是这套平台最核心的门面。我见过不少项目一上来就搞七八个 Agent 互相调用结果调试的时候连日志都串不起来。XXL-AI 的编排模型走的是有向图 状态机的混合路线节点是 Agent 或者工具调用边是条件跳转整个图有一个全局状态对象在流转。为什么用图而不是纯链式因为真实业务里很少有纯线性的流程。举个我实际做的场景用户问一个售后问题系统需要先判断是咨询类还是投诉类咨询类走知识库检索投诉类走工单创建工单创建完还要回查知识库补一个解决方案。这里面有分支、有汇合、有循环链式结构表达起来非常别扭图结构就很自然。状态机的部分负责的是节点内部的执行语义。每个 Agent 节点有自己的生命周期初始化、思考、调用工具、生成回复、结束。状态机保证这些阶段不会乱序也方便在任意阶段插入钩子做日志和中断。2.3 供应商抽象层抹平差异的关键在“能力声明”多供应商接入这件事很多人以为就是写几个 adapter 把 API 包一层。真做起来会发现坑在细节里有的厂商支持 function calling有的只支持 JSON mode有的两者都不支持只能靠 prompt 硬约束有的支持流式有的流式里不带工具调用结果有的上下文窗口 128K有的只有 8K。XXL-AI 的做法是给每个供应商维护一份能力声明表而不是假设所有供应商能力一致。编排层在生成执行计划的时候会先查这张表如果某个节点依赖 function calling 但当前供应商不支持就自动降级成 prompt 约束模式并在日志里打一个 warning。这个设计看起来多了一层间接但实际用下来切换供应商的成功率从“一半靠运气”变成了“基本无感”。2.4 扩展层MCP、SKILL、RAG 各管一段这三个词最近热度很高但很多人分不清它们的边界。我用一句话概括MCP 管“怎么调工具”SKILL 管“怎么封装能力”RAG 管“怎么喂知识”。MCP 是一个工具调用的协议标准它定义的是工具怎么描述、怎么被发现、怎么被调用。SKILL 是在 MCP 之上的一层封装把一个或多个工具调用加上提示词、加上后处理逻辑打包成一个可复用的“技能单元”。RAG 则是独立的一条线负责在生成之前把相关知识检索出来注入上下文。为什么要把这三个分开而不是做成一个“扩展模块”因为它们的变更频率完全不同。MCP 协议相对稳定SKILL 会随着业务快速迭代RAG 的检索策略和向量库选型更是经常换。分开之后换向量库不影响 SKILL改 SKILL 不影响 MCP 协议实现维护成本低很多。2.5 工程化底座决定能不能上生产的分水岭Demo 和产品的差距八成在底座上。XXL-AI 的底座我重点做了四件事配置热更新、全链路追踪、限流与熔断、会话状态持久化。配置热更新解决的是“改一个 prompt 要不要重启服务”的问题。全链路追踪解决的是“用户说回答错了我怎么知道是哪一步错了”的问题。限流熔断解决的是“某个供应商挂了会不会拖垮整个系统”的问题。会话状态持久化解决的是“服务重启后多轮对话上下文丢不丢”的问题。这四件事任何一件没做好上线之后都会变成事故。3. 核心细节解析与实操要点3.1 Agent 编排的节点定义与状态流转先看一个最简的节点定义结构。我用的是 YAML 来描述编排图因为 YAML 对非程序员友好产品经理也能看懂个大概。nodes: - id: classify type: agent model: gpt-4o-mini prompt: | 判断用户问题类型只输出 consult 或 complaint。 output_key: intent - id: retrieve type: rag condition: {{intent}} consult knowledge_base: kb_after_sale output_key: context - id: create_ticket type: skill condition: {{intent}} complaint skill_name: ticket_creator output_key: ticket_id这里有几个设计细节值得说。第一condition用的是表达式而不是代码这样编排图的序列化、反序列化、可视化都容易做。第二每个节点的输出都挂到全局状态的一个 key 上后续节点通过{{key}}引用避免了节点之间直接耦合。第三type字段决定了节点的执行器agent 走模型调用rag 走检索skill 走技能封装扩展新类型只需要注册一个新执行器。状态流转这块我用了一个显式的状态对象而不是把状态散落在各个节点的闭包里。状态对象在每次节点执行前后都会被快照一次存到追踪系统里。这样出问题的时候我可以精确回放“第 3 步执行前状态是什么、执行后变成了什么”。注意状态对象不要存大对象比如完整的检索结果。我踩过这个坑一次 RAG 召回 20 个 chunk 全塞进状态结果快照体积暴涨追踪系统直接被打爆。正确做法是状态里只存引用 ID实际内容放在独立的存储里按需取。3.2 多供应商接入的能力声明与降级策略能力声明表我建议用结构化的方式维护不要散在代码注释里。下面是一个简化的示例供应商function_callingjson_modestreaming上下文窗口工具流式供应商A支持支持支持128K支持供应商B支持不支持支持32K不支持供应商C不支持支持支持8K不支持有了这张表编排层在编译执行计划的时候就能做静态检查。比如某个节点用了 function calling但当前绑定的供应商是 C编译阶段就直接报错而不是等到运行时才失败。这个“提前失败”的原则在工程上非常值钱能把大量问题挡在上线之前。降级策略我分了三档能力降级function calling 不支持就转 prompt 约束、模型降级主模型超时就切备用模型、流程降级RAG 检索失败就跳过检索直接生成并在回复里标注“未参考知识库”。三档降级按顺序触发每一档都会记一条 metric方便事后分析降级频率。3.3 MCP 工具接入的实操流程MCP 工具接入我总结成四步发现、描述、绑定、调用。发现阶段平台会去读 MCP server 暴露的工具列表。描述阶段把 MCP 的工具描述转换成内部统一的工具描述格式。绑定阶段把工具挂到某个 Agent 或者 SKILL 上。调用阶段运行时根据模型返回的工具调用请求路由到对应的 MCP server 执行。这里有个容易忽略的点MCP 工具的入参校验。模型生成的参数经常有类型错误比如该传整数的传了字符串。我的做法是在调用 MCP server 之前加一层 schema 校验校验失败就把错误信息回传给模型让它重试而不是直接抛异常。这个重试机制让工具调用的成功率提升非常明显。def call_mcp_tool(tool_name, arguments, schema): errors validate(arguments, schema) if errors: return {status: retry, message: f参数错误: {errors}} result mcp_client.invoke(tool_name, arguments) return {status: ok, data: result}实操心得MCP server 的启动顺序有讲究。如果工具之间有依赖比如工具 B 需要工具 A 先初始化那 MCP server 的注册顺序必须和依赖顺序一致。我建议在平台启动时做一次依赖拓扑排序自动决定注册顺序别靠人工维护。3.4 SKILL 封装把重复的编排逻辑沉淀下来SKILL 的价值在于复用。我做过一个统计一个中等规模的 AI 应用里大概有 40% 的编排逻辑是重复的——都是“检索 生成 格式化”这个套路。如果每个场景都重新画一遍图维护成本会失控。SKILL 的封装粒度我建议控制在“一个完整的业务动作”。比如“生成售后回复”是一个 SKILL“创建工单”是一个 SKILL“查询订单状态”是一个 SKILL。太细了复用价值低太粗了灵活性差。SKILL 内部可以包含多个节点也可以调用其他 SKILL。但我要提醒一句SKILL 的嵌套层级别超过三层。我见过一个项目 SKILL 套 SKILL 套了五层最后调试的时候根本不知道哪一层出的问题。三层是一个经验值超过三层就该考虑拆分了。3.5 RAG 检索增强的工程化细节RAG 这块我踩的坑最多重点说三个。第一个坑是分块策略。很多人直接用固定长度分块结果把一句话切成两半检索出来的 chunk 语义不完整。我的做法是按语义边界分块 重叠窗口。语义边界优先按段落段落太长再按句子句子还长才按字符。重叠窗口一般设 chunk 长度的 10% 到 20%保证跨块的信息不丢。第二个坑是召回数量。召回太少覆盖不够召回太多噪声大。我的经验值是先召回 20 个再用 rerank 模型精排到 5 个。这个“粗排 精排”的两段式结构比单段召回效果好很多。rerank 模型可以用小模型成本可控。第三个坑是知识库更新。文档改了但向量库没更新检索出来的还是旧内容。我的做法是给每个文档维护一个版本号文档变更时触发增量更新只重新向量化变更的部分。全量重建的成本太高增量更新是必须的。RAG 环节常见问题我的处理方式分块语义割裂语义边界 重叠窗口召回噪声大粗排 20 精排 5更新数据陈旧版本号 增量更新评估效果难量化维护标注集定期跑召回率4. 实操过程与核心环节实现4.1 从零搭建一个多 Agent 协作流程我拿一个实际做过的场景来演示智能客服工单处理。需求是用户提交问题后系统自动分类、检索知识库、生成回复、必要时创建工单。第一步定义全局状态。状态里放user_query、intent、retrieved_context、reply、ticket_id这几个字段。第二步画编排图。节点依次是分类节点、条件分支、检索节点、生成节点、工单节点、汇总节点。第三步配置每个节点。分类节点用便宜的小模型生成节点用能力强的大模型这是成本优化的常规操作。第四步配置降级。检索节点如果超时直接跳过生成节点在 prompt 里加一句“未参考知识库”。第五步跑测试。我一般会准备 50 条左右的测试用例覆盖各种分支每次改动编排图都跑一遍回归。nodes: - id: classify type: agent model: small-model prompt: 分类用户问题consult / complaint / other output_key: intent - id: branch type: condition cases: - when: {{intent}} consult next: retrieve - when: {{intent}} complaint next: create_ticket - default: generate - id: retrieve type: rag knowledge_base: kb_main top_k: 20 rerank_top_k: 5 output_key: retrieved_context next: generate - id: create_ticket type: skill skill_name: ticket_creator output_key: ticket_id next: generate - id: generate type: agent model: large-model prompt: | 根据上下文回答用户问题。 上下文{{retrieved_context}} 工单号{{ticket_id}} output_key: reply这个配置跑下来一次完整请求的耗时大概在 2 到 4 秒取决于检索和模型调用的延迟。如果开了流式首字延迟能压到 800 毫秒左右。4.2 多供应商切换的实操验证切换供应商这件事我建议做成运行时可变而不是启动时写死。具体做法是在配置里给每个 Agent 节点指定一个“供应商组”组里按优先级排几个供应商。运行时如果主供应商失败自动切下一个。验证切换是否成功我有一套简单的检查清单模型调用是否正常返回、工具调用是否正常触发、流式输出是否正常、token 计数是否准确、错误码映射是否正确。这五项都过了才算切换成功。注意不同供应商的 token 计数方式不一样有的按字符有的按 token有的对中文有特殊处理。如果你做成本核算一定要按供应商分别统计别用一个统一的系数去乘误差会很大。4.3 RAG 知识库的搭建与调优实录搭建 RAG 知识库我走的是这条路径文档采集、清洗、分块、向量化、入库、检索、精排、注入。文档清洗这一步很多人跳过但我觉得很关键。PDF 里的页眉页脚、HTML 里的导航栏、Word 里的批注这些噪声如果不清理会严重污染检索结果。我的做法是先用规则清洗再用小模型做一遍语义去噪。分块参数我调过很多轮最后稳定在chunk 长度 512 token重叠 64 token。这个参数在中文场景下表现比较均衡。如果你的文档偏技术类句子长可以适当加大 chunk如果偏对话类句子短可以减小。检索这块我用的是向量检索 关键词检索的混合模式。纯向量检索对专有名词不敏感比如产品型号、人名这些用关键词检索更准。两路结果合并后再精排效果比单路好不少。4.4 工程化底座的落地配置底座这块我重点说配置热更新和全链路追踪。配置热更新我用的是配置中心 本地缓存 变更通知的结构。配置中心存全量配置本地缓存存当前生效的配置变更时通过通知机制推送到各个实例。这样既保证了配置的实时性又避免了每次读配置都走网络。全链路追踪我用的是trace_id 贯穿 span 分段的模式。一次请求一个 trace_id每个节点执行是一个 spanspan 里记录输入、输出、耗时、状态。追踪数据存到独立的存储里支持按 trace_id 查询和按时间范围检索。class TraceContext: def __init__(self, trace_id): self.trace_id trace_id self.spans [] def start_span(self, name): span Span(namename, start_timenow()) self.spans.append(span) return span def end_span(self, span, status, output): span.end_time now() span.status status span.output truncate(output, 1000) self.persist()限流熔断我用的是令牌桶 熔断器的组合。令牌桶控制整体 QPS熔断器针对每个供应商单独配置连续失败超过阈值就熔断一段时间。熔断期间请求自动走降级逻辑。5. 常见问题与排查技巧实录5.1 Agent 编排常见问题速查问题现象可能原因排查方向解决方法节点不执行条件表达式错误检查 condition 语法用调试模式单步执行状态丢失状态对象未正确传递检查 output_key 配置确认 key 名一致循环不退出缺少终止条件检查循环节点的退出条件加最大迭代次数保护分支走错表达式求值类型不匹配检查变量类型显式做类型转换我遇到最多的问题是条件表达式求值。比如{{intent}} consult如果 intent 的值带了空格或者大小写不一致判断就会失败。我的建议是在表达式求值前统一做 trim 和 lowercase 处理别指望模型输出的格式永远规范。5.2 多供应商接入的典型故障供应商接入最常见的故障是超时和限流。不同供应商的超时阈值不一样有的 30 秒有的 60 秒。我的做法是统一设一个较短的超时比如 20 秒超时就切备用供应商而不是傻等。另一个常见故障是错误码映射。不同供应商的错误码体系完全不同有的用 HTTP 状态码有的用业务错误码。我建议在适配层做一层统一的错误码映射把供应商错误码映射成平台内部的错误码上层逻辑只认内部错误码。实操心得供应商的健康检查不要只做 ping要做真实的模型调用。我见过 ping 正常但实际调用一直失败的案例因为 ping 走的是另一个接口。健康检查用一个小 prompt 做真实调用虽然多花一点 token但能真实反映可用性。5.3 RAG 效果不佳的排查思路RAG 效果不好先别急着换模型按这个顺序排查分块是否合理、召回是否覆盖、精排是否有效、注入位置是否合适。分块问题看 chunk 的边界如果经常出现半句话就是分块策略有问题。召回问题看标注集上的召回率如果召回率低于 80%说明检索环节有问题。精排问题看精排前后的排序变化如果精排后好结果反而排后面了说明精排模型不适合你的场景。注入位置问题看 prompt 结构知识库内容放在 prompt 开头还是结尾效果可能差很多。我实测下来知识库内容放在 prompt 开头、用户问题放在结尾效果比反过来好。原因可能是模型对结尾的内容注意力更集中把问题放结尾能让模型更聚焦。5.4 工程化底座的性能瓶颈底座的性能瓶颈通常出在状态持久化和追踪写入上。每次节点执行都要写状态快照和追踪数据如果同步写会严重拖慢请求。我的优化方案是异步写入 批量提交。状态快照和追踪数据先写到内存队列后台线程批量刷到存储。这样请求路径上几乎没有 IO 开销。代价是极端情况下可能丢少量追踪数据但相比性能提升这个代价可以接受。另一个瓶颈是配置读取。如果每次请求都读配置中心QPS 一高配置中心就扛不住。本地缓存是必须的缓存失效时间设短一点比如 5 秒兼顾实时性和性能。6. 扩展层的进阶玩法与个人体会6.1 MCP 与 SKILL 的组合使用MCP 和 SKILL 不是二选一的关系而是可以组合的。我的做法是底层用 MCP 接工具上层用 SKILL 做封装。比如我接了一个数据库查询的 MCP 工具然后在 SKILL 里封装成“查询订单”“查询物流”“查询售后”三个技能每个技能有自己的 prompt 和参数校验。这样上层编排的时候直接调 SKILL不用关心底层是哪个 MCP 工具。这种组合的好处是关注点分离。MCP 层只关心工具能不能调通SKILL 层只关心业务逻辑对不对。两层各自演进互不干扰。6.2 RAG 与 SKILL 的协同RAG 也可以封装成 SKILL。我把“检索 精排 格式化”打包成一个knowledge_retrievalSKILL任何需要知识注入的节点直接调这个 SKILL 就行。这样检索策略变更的时候只需要改一个 SKILL所有调用方自动生效。更进一步我做了多知识库路由。不同的 SKILL 绑定不同的知识库比如售后 SKILL 绑售后知识库产品 SKILL 绑产品知识库。检索的时候按 SKILL 绑定的知识库去查避免了全库检索的噪声。6.3 平台后续可以怎么扩展这套平台目前跑得比较稳但我还在持续加东西。近期在做的有几个方向Agent 的自我反思机制让 Agent 在生成回复后自己检查一遍不合格就重试SKILL 的市场化把常用 SKILL 做成可分享的包团队之间直接复用RAG 的多模态支持目前只支持文本图片和表格的检索还在探索。多模态这块我试过一些方案图片检索用 CLIP 类的模型做向量化表格检索用结构化解析后转文本。效果还在调等稳定了再单独写一篇。6.4 我踩过的几个印象深刻的坑第一个坑是过度编排。一开始我把所有逻辑都画进编排图结果图复杂到没人看得懂。后来我学会了一件事编排图只放主干流程细节逻辑封装进 SKILL。图要让人一眼看懂看不懂的图就是坏图。第二个坑是忽略成本。早期我没做 token 统计一个月下来账单吓一跳。后来加了按节点、按供应商、按用户的 token 统计才发现分类节点用了大模型纯属浪费。换成小模型后成本降了六成。第三个坑是追踪数据存太多。一开始我把每个节点的完整输入输出都存下来存储很快就爆了。后来改成只存摘要和引用完整内容按需查询存储压力小了很多。最后分享一个小技巧编排图的版本管理很重要。每次改图都打一个版本号出问题可以快速回滚。我见过太多团队改图改出事故又没有版本记录只能靠记忆回滚非常痛苦。这套平台我还在持续迭代后面如果有新的模块稳定了再单独写文章分享。如果你也在做类似的事情欢迎交流踩坑经验。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。