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_calling | json_mode | streaming | 上下文窗口 | 工具流式 |
|---|---|---|---|---|---|
| 供应商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,每个节点执行是一个 span,span 里记录输入、输出、耗时、状态。追踪数据存到独立的存储里,支持按 trace_id 查询和按时间范围检索。
class TraceContext: def __init__(self, trace_id): self.trace_id = trace_id self.spans = [] def start_span(self, name): span = Span(name=name, start_time=now()) 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 统计,才发现分类节点用了大模型,纯属浪费。换成小模型后成本降了六成。
第三个坑是追踪数据存太多。一开始我把每个节点的完整输入输出都存下来,存储很快就爆了。后来改成只存摘要和引用,完整内容按需查询,存储压力小了很多。
最后分享一个小技巧:编排图的版本管理很重要。每次改图都打一个版本号,出问题可以快速回滚。我见过太多团队改图改出事故,又没有版本记录,只能靠记忆回滚,非常痛苦。
这套平台我还在持续迭代,后面如果有新的模块稳定了,再单独写文章分享。如果你也在做类似的事情,欢迎交流踩坑经验。