做Agent开发这几年,我印象最深的一件事,不是模型能力不够,而是“换模型等于重构”。同一个技能提示词,在某个模型上调好的温度、工具描述、输出格式,换个平台几乎全崩,连带着业务代码、上下文管理、异常重试全要改一遍。后来我们下定决心做了一层平台抽象层,核心就干两件事:统一提示词优化,以及多AI应用调度。这套结构支撑了内部多个跨平台Agent技能稳定跑了大半年,今天把这套设计和落地细节完整整理出来。
这篇文章重点面向三类人:正在做Agent系统架构的技术负责人,被提示词迁移折磨的AI应用开发工程师,以及想搞懂“多模型调度到底怎么设计”的架构读者。内容不空谈原理,所有模块都会落到实际代码、配置和踩坑记录上。
1. 平台抽象层到底抽象了什么
1.1 先拆解“跨平台”里的平台都有哪些
很多团队做跨平台,第一反应是封装SDK,把各家API包一层,对外暴露同一个chat函数。这个思路能做,但远远不够。我们说的“平台”至少包含四层:
第一层是模型服务商,比如闭源的对话模型、开源模型的托管服务、本地私有化部署的推理服务。第二层是模型家族,同一家可能提供通用对话、长文本、低成本、高推理等不同型号,这些模型的能力边界、上下文上限、定价策略完全不同。第三层是Agent运行框架,包括技能注册、工具调用、多轮对话状态、记忆读取这些基础设施。第四层是业务场景,比如客服、写作助手、代码辅助,每个场景对延迟、成本、回答风格的要求都不一样。
平台抽象层要处理的,其实是“这四层中任何一个变化,都不能牵动业务代码跟着改”。拿我们自己的例子说,最初只接了一个模型服务商,后来要接入本地部署的推理服务,想着“SDK换成另一套不就行了”。结果发现提示词不通用,有的模型吃XML格式的工具描述,有的只认JSON Schema,有的对Markdown表格极敏感。这已经不是SDK层面能解决的问题,必须在上层做一次抽象。
1.2 抽象层的边界:不该往上包的,坚决不包
做抽象层最容易犯的毛病,是把所有东西都往上包,最后包出一个“什么都能干、什么都干不好”的壳。我们内部讨论时定了一条边界:平台抽象层只负责三件事。
第一,统一交互协议,把不同平台的请求和响应转成内部标准结构。第二,统一提示词组织方式,包括系统提示、用户消息、工具描述、输出约束。第三,统一调度策略,让任务能按路由规则、成本预算、延迟要求自动选择合适的模型。
统一语言对话回复的“业务语义”、用户身份体系、权限控制,这些坚决留在应用层。抽象层一旦碰业务,就会被迫感知不同场景的特殊逻辑,逐渐变成一个怪物。我们的做法是:抽象层只提供一个标准出参结构,业务层自己决定怎么使用。
1.3 四个核心模块划分
实际落地时,我们把抽象层拆成了四个模块:连接适配器、提示词引擎、调度路由、观测与容错。
连接适配器负责把不同厂商的请求协议翻译成内部统一格式,比如把“角色体系”“多模态消息”“工具调用参数”归一化。提示词引擎负责按技能模板生成最终消息序列,同时记录每个变量的来源,方便优化。调度路由负责决定“这个请求找哪个模型”。观测与容错负责记录每次调用的token消耗、延迟、成功失败、重试情况。
四个模块关系很清晰,适配器是底座,提示词引擎产生请求,调度路由决定分发方向,容错机制兜底。这样设计之后,新增一个平台的成本从“改业务代码”降到了“写一个适配器”。
2. 统一提示词优化:把提示词从字符串变成可观测的资产
2.1 模板化、变量、语义插槽:提示词结构的核心
很多团队的提示词是散养的,要么写在代码里拼字符串,要么在调试平台里复制来复制去,要么躺在某个成员的个人笔记里。这种散养状态在单模型时代勉强能忍,一旦跨平台,问题立刻暴露:系统提示语序不同、变量命名不统一、工具描述格式不兼容。
我们统一提示词的第一步,是把所有提示词变成模板加变量。模板里不允许写死具体内容,只保留结构、指令、少量示例,业务相关的内容全部通过变量注入。
from dataclasses import dataclass, field @dataclass class PromptTemplate: skill: str version: str system_prompt: str = "" user_prompt: str = "" tool_descriptions: list = field(default_factory=list) output_format: str = "" skill_prompt = PromptTemplate( skill="meeting_summary", version="v1.3", system_prompt="你是一个会议纪要助手...", user_prompt="请根据以下会议记录生成摘要:{transcript}", tool_descriptions=["calendar_lookup", "todo_create"], output_format="json" )变量不是普通占位符,我们把变量进一步分成普通变量和语义插槽。普通变量是纯文本替换,比如会议原文、用户输入。语义插槽则是带有约束的变量,比如日期要转成ISO格式、代码块要保持缩进、列表要限制数量。语义插槽在模板渲染前会先做一次预处理,避免模型被格式混乱的输入干扰。
这样设计的原因很简单:提示词是Agent技能最容易变化的部分,不变成可版本化、可测试的资产,后面所有优化都无从谈起。
2.2 三种提示词各有各的优化套路
我们的提示词引擎最终只会维护三类提示词:系统提示、上下文注入、工具描述。小技巧是分开维护,渲染时才合并。
系统提示承担角色设定和全局约束,建议控制在合理长度以内,把“你是谁、你在做什么、你的输出边界”说清楚,不要塞过多业务细节。上下文注入是把用户输入、检索结果、历史对话填充到合适位置,关键是要控制顺序和信息密度。我们实测发现,把重要信息放在上下文开头和结尾,召回效果比埋在中间好很多。工具描述最容易被忽略,却是跨平台最难迁移的部分。
每个平台对工具调用格式的接受度差异很大,有的支持复杂嵌套类型,有的只支持简单的字符串参数。我们的经验是,在抽象层里定义一个独立的工具描述Schema,每个平台适配器负责把Schema渲染成该平台要求的格式,提示词模板层永远不感知具体平台。
2.3 优化闭环:版本管理、AB实验、质量评估
提示词优化不能靠“我觉得这样写更好”,必须形成闭环。我们给每个技能提示词都建立了版本链,每次修改都生成新版本,同时在请求日志里记录版本号。线上出现回答质量下降时,可以在观测系统里直接锁定是哪个版本引入的回归。
AB实验方面,不必一开始就上复杂的分流框架。最简单的做法是:在路由配置里给同一个技能配置两个提示词版本,按百分比分流,对比成功率、用户有效反馈、修正轮次这几个指标。数据量上来之后再逐步收敛。
skill: meeting_summary prompt_versions: - version: v1.2 weight: 0 - version: v1.3 weight: 1这个阶段的优化,本质上是把“提示词调试”从拍脑袋变成“数据驱动”。我们后来总结了一句内部口号:提示词和代码一样,必须有版本、有测试、有线上观测。这不是仪式感,是跨平台之后唯一能保证稳定性的方法。
3. 多AI应用调度核心架构:路由、并发与容错
3.1 调度器到底调度的是什么
一说到多AI调度,很多人先想到给用户请求做负载均衡,把流量随机分发到不同模型上。实际做下来,调度器要管的比这多得多。它首先要面对的是“任务类型识别”,决定这个请求是快任务还是重任务、是单轮回答还是需要工具调用。其次是“模型能力匹配”,不同任务对推理能力、上下文长度、指令遵循能力的要求不一样。然后是“成本与优先级控制”,同一类任务在不同时段可能希望走不同通道。最后才是“并发与容错”。
我们把调度器设计成两段式:前置路由决定目标模型,后置执行负责调用、重试、降级。前置路由是纯规则引擎,读请求上下文和配置,不发起实际调用。后置执行是统一的执行器,处理超时、限流、熔断、重试。
这种拆分的好处是,路由规则可以随时调整而不影响执行链。比如某天低成本模型服务质量抖动,可以直接把路由权重调低,业务方不需要知道底层的切换逻辑。
3.2 路由策略:不是越智能越好
路由策略有很多选择,最简单的是基于规则的配置路由,复杂一点的是基于反馈的强化学习路由。但我们落地之后认为,对绝大多数团队来说,规则路由已经足够,重点是把规则设计全面。
我们内部维护了一张路由决策表,关键字段包括任务优先级、技能类型、数据敏感级别、目标延迟、成本上限、模型历史可靠度。规则可以按场景叠加。
| 路由场景 | 策略 | 说明 |
|---|---|---|
| 低价值高并发任务 | 低成本模型优先 | 摘要、分类、信息抽取 |
| 高价值逻辑推理任务 | 强推理模型优先 | 代码生成、复杂规划 |
| 数据敏感场景 | 本地模型优先 | 私有数据不出内网 |
| 需要稳定输出格式 | 指令遵循强模型优先 | 结构化输出任务 |
| 高延迟容忍离线任务 | 排队执行,低优先级 | 后台批处理 |
这套规则也不复杂,核心是“让正确的任务找到正确的模型”,避免用贵模型跑简单任务,也避免用便宜模型硬撑复杂推理。实践中我们遇到过一个教训:为了省钱,把某类任务全部切到低成本模型,结果因为回答质量下降,用户需要多轮追问,看起来省了单次成本,实际总成本反而上升。后来在路由表里加入了“平均修正轮次”指标,低成本模型表现差时自动切回强模型。
3.3 并发、超时、熔断:调度器的稳定性底座
多AI调度真正的难点,不是在路由规则上玩花样,而是在不稳定环境下维持稳定。模型服务商从未承诺永远可用,限流、超时、偶尔返回异常状态是家常便饭。
我们给调度器设计了三层保护。第一层是超时控制,根据任务类型设置不同超时时间,短任务3到5秒,长任务放宽到30秒以上,一旦超时立刻终止。第二层是限流控制,在抽象层维护每个模型通道的并发水位,超过阈值就排队或降级,而不是无限放量。第三层是熔断降级,记录最近一段时间内某个模型的错误率,超过阈值则自动摘除,流量切换到备用模型,同时发出告警。
另外一个容易被忽略的点是幂等与重试。Agent技能常常会调用工具、写数据,如果上游模型超时我们盲目重试,可能造成重复执行。我们约定:抽象层只对“读类操作”自动重试,写类操作一律不自动重试,由业务层决定是否补偿。这个约定避免了很多数据一致性问题。
3.4 技能编排:调度不要只盯着单个模型
多AI应用调度还有一个上层视角:技能编排。很多Agent任务不是一个模型单打独斗能完成的,往往需要多个技能组合。比如生成一份行业分析报告,可能需要先做资料检索,再做数据汇总,再写结论,再排版输出。
调度器在这里的角色是“任务分解器”。我们把每个技能封装成独立的可调用单元,技能之间通过标准输入输出协议交互。调度器根据任务目标生成执行计划,决定哪些技能并行、哪些串行、哪些可以合并成一次调用。
技能编排刚开始不要设计得太重,用一个列表描述执行步骤即可。
pipeline: - skill: web_search params: query: "{input}" - skill: content_summarize params: docs: "{result}" - skill: report_writer params: summary: "{result}"每一行是一个技能节点,输出通过变量引用传给下一个节点。等业务复杂度上来之后,再加入分支、循环、条件判断。我们一度尝试上一套完整的工作流引擎,后来发现对大部分场景来说太过了,简单的列表式编排反而更容易维护。
4. 落地一个跨平台Agent Skill:实操记录
4.1 技能目录设计:先定契约,再写代码
我们在动手写适配器之前,先设计了一套技能目录,相当于给所有Agent技能建了注册表。每个技能在注册表里声明四件事:技能名称、输入参数Schema、输出参数Schema、依赖的模型能力标签。
- 模型能力标签包括:指令遵循、工具调用、长文本、结构化输出、多模态理解。
- 输入输出Schema用JSON格式定义,字段包含类型、是否必填、描述、示例值。
这步看起来繁琐,实际是跨平台最关键的一步。因为只有把“技能与模型的契约”显式化,后面的模板渲染和调度路由才可复用。
例如一个“会议纪要技能”的注册条目:
{ "skill": "meeting_summary", "input_schema": { "transcript": {"type": "string", "required": true}, "language": {"type": "string", "default": "zh"} }, "output_schema": { "summary": {"type": "string"}, "action_items": {"type": "array", "items": {"type": "string"}} }, "capability_tags": ["long_context", "structured_output"] }有了这份注册表,调度器才能根据标签匹配模型,提示词引擎才能自动校验变量,新接一个平台时才能快速判断能力是否满足技能需求。
4.2 平台适配器与统一API:不替换SDK,不换业务代码
平台抽象层的核心是一套统一API,我们内部管它叫BaseChat。它屏蔽了上游平台的差异,对业务层暴露的永远是同一个函数。
class BaseChat: def __init__(self, provider: str, model: str, config: ProviderConfig): self.adapter = ProviderAdapter.registry[provider](config) def invoke(self, request: UnifiedRequest) -> UnifiedResponse: native_request = self.adapter.convert_request(request) native_response = self.adapter.call(native_request) return self.adapter.convert_response(native_response)连接适配器把统一请求转换成目标平台的原生请求结构,再把原生响应转换成统一响应结构。统一响应至少包含三个部分:最终文本内容、结构化输出字段、原始元信息(token用量、延迟、模型名)。
转换层看起来机械,但坑很多。举例说,不同平台对“消息角色”的命名不同,有的用system/user/assistant,有的要求所有系统指令都由用户消息携带;不同平台对“多轮历史”的裁剪策略不同,有的会自动截断,有的需要调用方自己控制;不同平台对“工具参数”的编码方式也不同。这些差异如果在适配器里不处理干净,后续提示词引擎和调度器全都得跟着兼容。
我们当时的做法是:先只接两个平台,一个闭源服务商、一个本地推理服务,把适配器接口跑顺,再逐步扩充。不要一开始就追求接满所有平台,适配器的抽象需要真实用例来驱动,空想的适配层大概率要返工。
4.3 提示词优化闭环小实验:让数据告诉你答案
某一次我们对一个“文章润色”技能做提示词优化,测试了两种写法。第一种写法是把润色规则全部放在系统提示里,第二种写法是把规则拆成系统提示加两个正例、两个反例。直觉上第二种答题质量更高,但数据出来时还是有点惊讶:第二种写法的修正轮次直接降了40%。
我们把这两种版本写进了同一个技能模板,前端分流各占一半,线上跑了两天。对比指标选择了修正轮次、回答被采纳率、token消耗。结论是:跨平台环境下,示例的作用比我们想象的还大。所以后来统一提示词优化的基本原则就多了一条倾向:复杂的技能必须配备少量高质量示例,示例的格式要尽量贴近真实输入输出。
这类实验不需要专门的实验平台,在路由配置里加一个版本权重字段,再在观测报表里按版本聚合数据就够了。重点是形成习惯:每次改动提示词,都当作一次小实验来做,而不是改完就上、上完就忘。
4.4 灰度切换和回滚:抽象层带来的真实便利
有了抽象层后,做模型切换就变成了配置变更,而不是代码变更。我们把一个“行业信息提取”技能从原来的模型A切换到模型B,步骤很简单:在技能目录里把该技能的路由目标改成模型B,灰度权重先设5%,观测一天成功率和输出质量,没有问题再逐步放量到100%。
这个过程中业务代码完全没有改,提示词模板也没有改,只调整了调度配置里的路由表。这放在没有抽象层之前是无法想象的,以前换一个模型至少需要一个测试团队回归全量功能。
当然,切换并不总是顺利的。我们也遇到过因为示例风格和模型B的分布不一致,导致输出格式不稳定。这时候第一反应不是改业务代码,而是回滚。回滚同样是通过配置操作完成的,两分钟内完成,线上影响面被压到很小。这也是平台抽象层最有价值的地方:它把高风险的系统迁移变成了低风险的可控实验。
5. 常见问题排查与避坑清单
5.1 提示词在模型A好使,在模型B就崩
这是跨平台之后最频繁出现的问题,现象是同一个技能在某个模型上回答稳定,换一个模型后风格突变、格式错乱、逻辑松散。排查顺序建议如下。
先检查系统提示的长度和复杂度。有些模型对超长指令的遵循能力弱,系统提示一旦超过一定长度,后面的指令被忽略。再检查示例数量,示例过多在某些模型上会造成“过度模仿”,反而干扰任务本身。最后检查工具描述格式,不同模型对工具调用的表达要求差异极大,JSON Schema标准虽然是事实标准,但每个平台解析严格度不一样。
这一类问题不能靠“加一版提示词”解决,必须回到技能目录,看能力标签是否匹配。如果这个技能本质上是高指令遵循任务,而路由到的是一个以快为长的低成本模型,提示词写再好也白搭。
5.2 结构化输出解析失效
我们在统一输出格式时踩过一个大坑:某些模型嘴上答应输出JSON,实际偶尔会额外输出解释文字、Markdown代码块标记、甚至逗号遗漏。用严格解析器去解析,失败率会高到让人崩溃。
后来我们采取了三层容错策略。第一层,在提示词模板里明确输出结构,并提供示例。第二层,在适配器返回原始响应的同时,返回一个规范化后的字段,把Markdown代码块剥掉、提取第一个完整的JSON对象。第三层,在解析失败后再调用一次轻量模型做纯文本修复,把脏文本整理成合法JSON。
不需要把指望全押在“模型输出永远是JSON”上。模型不是数据库,它的输出天然有随机性,抽象层必须预留容错清洗环节。
5.3 调度策略导致的隐性成本增加
把任务路由到低成本模型表面省钱,如果低质量回答迫使系统多做一次重试、多跑一次修复流程,实际成本反而更高。我们曾经把“摘要生成”类任务全部切到低成本模型,连续跑了几天,发现token总消耗没怎么降,业务侧的二次改写量却明显上升。
解决方式是在路由决策表里增加质量成本指标,而不是只看单次调用成本。跟踪每个技能在不同模型上的“平均完成轮次”,如果低成本模型的完成轮次明显多于强模型,就把该技能切回强模型。成本优化一定要站在“完成任务总成本”的角度看,不能只看模型单次价格。
5.4 一份实测避坑清单
| 问题表现 | 可能原因 | 推荐处理 |
|---|---|---|
| 换平台后输出格式全乱 | 平台对相同提示词的遵循能力不同 | 降低系统提示长度,增加正反例 |
| 工具调用频繁失败 | 工具Schema格式不被目标平台支持 | 在适配器层做Schema格式转换 |
| 请求超时偶发 | 平台限流或网络不稳定 | 调度器增加超时重试与熔断 |
| 线上回答质量突然下降 | 提示词版本更新引入回归 | 按版本回滚,并对比观测数据 |
| 某低成本模型流量占比高但总成本不变 | 完成任务需要多次交互 | 在路由表中跟踪完成轮次 |
多AI应用调度不是配好路由就完事,稳定性和成本是持续调优的结果。团队需要建立一套常态化的观测报表,至少包括每个技能的平均响应时长、成功率、token消耗、完成轮次、模型分布。没有观测,就谈不上优化。
最后想说的几句话
平台抽象层这个东西,适合做多深,取决于你手里真实场景有多少。我见过不少团队,一上来就设计一个特别宏大的跨平台引擎,支持几十个平台、几十种技能,结果真正跑业务的就两三个场景,抽象倒是做得很漂亮,最后都被复杂性拖垮。
我个人建议,从两个平台、五个技能开始,把提示词模板化、路由规则化、观测数据化这三件事做好,已经能解决绝大多数跨平台痛点。抽象层不是越厚越好,而是刚好隔离变化的那一层。你把最频繁变化的模型、提示词、路由配置隔离开,剩下的业务逻辑保持稳定,这套架构就算立住了。
等到你手里的技能真的超过二十个、模型通道超过五个,你自然知道哪些抽象需要加强,哪些抽象是多余的。到那个时候再去做资源池管理、自动模型选择、多级降级,都不迟。保持抽象层小而稳,你的Agent系统才能活得久。