1. 项目定位:为什么我要把智能体调度押注在“技能手册”上
1.1 这个项目要解决的实际问题
如果你做过AI智能体的调度,大概率体会过这种别扭:一个智能体要完成“查天气、订机票、提醒日程”这套动作,常规思路要么是把意图硬编码成if-else,要么是训一个模型让它自己学会按什么顺序调用工具。硬编码的问题是不灵活,换一个场景就要改逻辑;上强化学习的问题是太重,光是设计奖励函数、稳定环境交互、处理训练周期,就能把一个本来两周能上线的功能拖成两个月。
SkillOrchestra这个项目,我给它定的核心命题其实只有一句话:能不能让智能体在复杂调度时,既不打离散的规则补丁,也不背上强化学习那套重型武器,而是用一份“技能手册”描述清楚什么条件下做什么、怎么做、做不了怎么办,然后让调度框架按手册执行。
所谓“技能手册”,在我这里指的是一套结构化的技能描述文档,它对智能体可执行的每个动作都做了三件事:登记能力边界、声明触发条件和上下文要求、写明执行步骤与降级策略。智能体在运行时通过检索手册来决定动手方式,而不是靠端到端模型硬猜,也不是靠人写满分支规则硬塞。
这个项目适合谁看?如果你正在做客服机器人、办公自动化助理、IoT设备联动这类场景,被多步动作编排搞得焦头烂额,或者你已经被强化学习的训练成本劝退,想找个在工程上更可控、在效果上更确定的方案,这篇分享应该能给你一条新思路。我会把SkillOrchestra从设计思路到落地代码再到踩坑实录完整过一遍。
1.2 为什么我会放弃强化学习路线
我先说清楚,不是强化学习不行,是它在很多业务场景里“贵”得不划算。
强化学习的本质是通过大量试错让模型学会策略,这个过程需要三个硬条件:可交互的环境、可量化的奖励信号、足够长的训练周期。真实业务里这三个条件往往都很难满足。比如你做一个日程助手,用户说“明天上午开会,帮我定个会议室,同时把参会人拉进群”,你要怎么定义奖励?会议室订到了得几分,参会人被正确拉群得几分,如果会议室订错楼栋扣几分?这些规则写下来,本质上已经是在做人工策略设计,而不是让模型自己在环境里“悟”。
更现实的问题是可交互环境。强化学习要求智能体不断尝试动作并观察反馈,但真实系统中的工具调用是有成本的,比如调一次外部API要花钱、要等响应,甚至可能因为错误调用导致脏数据。没人敢让一个还在训练中的智能体拿生产环境随便试探。你只能在模拟环境里训,但模拟环境的保真度做到什么程度,本身又是一个无底洞工程。
我之前在一个项目里试过用强化学习做工具调用排序,训练环境里构建了十几个模拟接口,结果到线上第一周就翻车了。模拟环境里永远模拟不出真实用户那种“你说东我偏问西”的迂回表达。后来我换了个思路,把用户意图拆成有限的技能调用模板,再配合策略约束,反而在两周内就把线上准确率拉到了可接受的水平。这个经历直接催生了SkillOrchestra的设计原型。
1.3 技能手册方案的适用边界
技能手册当然不是银弹,我得先把这个边界划清楚。
它适合的场景有两个特征:第一,技能种类有限且行为可枚举,第二,组合逻辑虽然复杂但可以被显式描述。比如企业内部的工单系统,可能涉及创建工单、分配审批人、通知相关方、回填状态这些动作,动作总数可能就二三十个,但组合起来能覆盖几百种流程。这种场景你用规则写会写吐血,用强化学习训又小题大做,技能手册则恰到好处。
不适合的场景也很明显:如果技能种类在持续涌现且很难被提前枚举,比如完全开放式的人机对话,用户随时会抛出各种千奇百怪的要求,那技能手册会面临手册永远写不完的尴尬。这种情况下你更需要一个具备泛化能力的模型来兜底。
SkillOrchestra的定位就是服务前一类场景,它做的不是取代大模型的推理能力,而是把大模型从“每次都要现想怎么调用函数”的负担里解放出来,让它专注于理解用户意图,至于调用什么技能、按什么顺序调用、失败怎么降级,交给手册编排层来处理。
2. 系统整体设计与取舍思考
2.1 核心思路:把“训练”变成“编写”
在我的设计里,SkillOrchestra最关键的思维转变,是把“训练策略”变成“编写行为规范”。
强化学习里你描述的是“目标”,希望智能体在环境里自己学出一套策略;SkillOrchestra里你描述的是“过程”,直接给智能体规定好每一步的输入、输出、前置条件和后置检查。有人会质疑这是不是退回了专家系统,我认为不是,因为SkillOrchestra保留了动态检索和组合能力,手册里的技能条目不是死的顺序,而是带评分和匹配条件的可路由节点。
举个例子,同样是“处理退款”这个动作,在传统规则系统里你可能写:
如果订单状态是已发货且用户要求退款,则调用退款接口,设置退款原因为用户申请。
在SkillOrchestra里你会把这个动作描述成一个技能条目,注明这条技能的输入参数、适用上下文、执行函数、成功率指标、可替代技能。调度层在运行时看到“退款”需求,会去手册里检索候选技能,结合当前订单状态、用户身份、历史行为打分,选出最合适的路径。这比硬编码灵活,又比强化学习可解释。
整套系统在运行时只回答三个问题:现在是什么状态,有哪些技能可以用,选哪个技能最合适。状态来自上下文模块,候选技能来自手册索引,选择来自匹配评分器。我把这个流程叫“读手册、对条件、做决策”。
2.2 模块拆解:技能目录、注册中心、路由决策、执行引擎
SkillOrchestra在结构上分成五块,我给每块做了很明确的边界:
- 技能目录:负责存放所有技能条目的元数据,包括技能ID、名称、描述、输入输出Schema、版本号、依赖关系。
- 注册中心:负责技能条目的新增、下线、更新,以及运行时的一致性校验。任何技能要上线,必须先注册,注册时会自动检测技能ID冲突和依赖缺失。
- 路由决策:负责把当前任务映射到技能条目。它会先根据任务类型做粗筛,再根据上下文参数做精排,最后输出一个技能执行序列。
- 执行引擎:负责按序列调用真实函数,处理超时、重试、异常,并记录执行日志。
- 质量监控:负责统计每个技能的成功率、平均耗时、调用频次,并把数据反馈给路由决策做动态调整。
这五块的依赖关系是单向的:路由决策依赖技能目录,执行引擎依赖路由决策,质量监控依赖执行引擎。这样的好处是每一层都能独立测试,我可以在不启动真实执行的情况下,用mock数据单独压测路由决策的准确性。
我见过很多项目把编排逻辑直接写在智能体的system prompt里,靠模型自觉去遵循。短期看很灵活,但一旦技能数量超过20个,模型就开始选择性遗忘,某些技能死活不会被触发。SkillOrchestra把技能选择从模型推理中剥离出来,用显式检索代替模型记忆,这是它“好用”的第一个原因。
2.3 上下文与状态管理:调度容易翻车的地方几乎都在这里
多步调度之所以难,除了选不对技能,还有一个更隐蔽的坑——上下文丢失。每执行完一步,环境状态就变了,下一步的决策必须基于最新的状态,而不是依赖最初的用户输入。
我设计了一套状态管理规范,把上下文分成三层:
- 会话层:保存用户与系统之间的原始对话内容,长期不变。
- 任务层:保存当前这个任务的目标、已完成步骤、待完成步骤、关键中间结果。
- 技能层:保存某个技能执行前后的局部变量,比如调用某个API返回的原始JSON,只保留到该技能链结束。
路由决策在每一步都优先读取任务层,而不是重新解析会话层的对话。这个设计很关键,因为真实用户的表达经常会自相矛盾,一开始说“帮我订明天的票”,过两句又说“其实我是想后天走”。如果每次都拿最新对话去理解意图,系统会反复横跳。任务层一旦锁定了目标日期,后续技能执行就不会被新表达带偏。
为这个我调过好几次优先级。刚开始实现时,任务层的字段很容易被会话层覆盖,结果用户改一次口述,整个流程就推翻重来。后来我给任务层字段加“置信等级”,只有用户明确使用纠正性表达时才允许覆盖,比如“不,我说的是后天”,而不是简单地把最新一句当作最新意图。这个机制上线后,误调度率下降了大概40%。
3. 核心实现过程与实操细节
3.1 技能手册的数据结构怎么设计
手册听起来很虚,落地到代码就是一套严谨的Schema。我在SkillOrchestra里用类型化字典来描述技能条目,每个条目包含下面这些核心字段:
- skill_id:全局唯一标识,建议用“域.动作.对象”的格式,比如schedule.meeting.create。
- name:人类可读名称,用于日志和排查。
- description:这个技能是干什么的,给路由决策做初步相关性匹配用。
- input_schema:期望的输入参数,包含参数名、类型、是否必填、约束条件。
- output_schema:执行后的返回结果格式,用于后续步骤读取。
- preconditions:执行前必须满足的条件表达式。
- effects:执行后预计会改变哪些状态,用于更新任务层。
- timeout_ms:最大执行时间。
- fallback_chain:降级技能列表,当前技能失败时按顺序尝试。
- version:技能版本号,支持灰度发布。
下面是一个精简版示例,我用的是类JSON格式,便于和代码中的结构互相映射:
{ "skill_id": "schedule.meeting.create", "name": "创建会议", "description": "根据参会人列表和时间段创建会议室邀请", "input_schema": { "required": ["start_time", "attendees", "duration"], "properties": { "start_time": {"type": "string", "format": "datetime"}, "attendees": {"type": "array", "items": {"type": "string"}}, "duration": {"type": "integer", "min": 15, "max": 240} } }, "preconditions": [ "task_layer.meeting_conflict == false" ], "effects": [ "task_layer.meeting_created |= true" ], "timeout_ms": 5000, "fallback_chain": ["schedule.meeting.suggest_alternative"], "version": "1.2.0" }这套Schema设计出来,第一版我就踩了坑。当时只有description和input_schema两个核心字段,结果路由决策在匹配时经常把两个语义相近但执行动作完全不同的技能搞混。后来我强行加上了preconditions和effects,让每个技能不光是“能做什么”,还要声明“在什么条件下能做”和“做完了会改变什么”。加了这两个字段之后,路由准确率的提升非常明显,因为这个设计逼迫你在注册技能时就把边界想清楚,而不是让决策器自己去猜。
3.2 路由决策的匹配与排序策略
路由决策是我花了最多心思的模块。它的工作分两步:粗筛和精排。
粗筛层用关键词和语义向量混合检索。每个技能条目的description会被离线嵌入成向量,用户输入的任务描述在同一空间里计算向量相似度。这一步会从技能目录里捞出Top 20的候选技能。我特意在这个环节保留关键词匹配作为兜底,因为有些领域术语的向量表达很不稳定,比如“拉群”和“创建群聊”在向量空间里相似度很高,但和“创建会议”反而容易混淆。关键词匹配能确保精确命中的技能不会在粗筛阶段被漏掉。
精排层则用一个轻量级评分模型,给每个候选技能算一个综合分。评分因子包括:语义相似度、用户的技能使用偏好、技能在当前状态下的成功率、技能执行成本估算。不同业务场景可以调整因子权重。
这里我给一个比较通用的评分逻辑示例,用Python风格来说明思路:
def score_skill(candidate, context): semantic_score = candidate.embedding_similarity preference_score = user_profile.get_skill_score(candidate.skill_id, default=0.0) reliability_score = monitor.get_reliability( candidate.skill_id, window=7d ) if monitor.has_data(candidate.skill_id) else 0.6 cost_penalty = estimate_cost(candidate) / max_cost_threshold total = ( semantic_score * 0.5 + preference_score * 0.2 + reliability_score * 0.2 + (1 - cost_penalty) * 0.1 ) return total这里面的权重不是我拍脑袋定的,是从一次线上事故里反推出来的。早期我把可靠性的权重调得过高,导致某个刚上线的技能因为没有历史数据,一直被评分压制,永远不被选中。后来加了一个“冷启动加成”——新技能在注册后7天内,可靠性分默认设为0.6,避免被老技能垄断。
精排之后还有一个“冲突检测”步骤,检查候选技能是否与当前任务层的状态有冲突。比如技能要求“会议时间未被占用”,但任务层的meeting_conflict已经是true,这种候选技能再高分也直接pass。这一步有效拦截了很多理论可行但实操必然失败的路径。
3.3 执行引擎的容错与降级设计
执行引擎是整个系统里最考验工程素质的部分。技能调用不是理论推演,网络超时、参数格式错误、第三方接口返回异常,每一种失败模式都要处理。
我把失败分成三类:可重试错误、可降级错误、不可恢复错误。可重试错误包括网络抖动、接口返回500、调用超时,这类我会按指数退避策略重试两次,间隔分别是1秒和3秒。可降级错误指的是当前技能内部逻辑失败,但存在一个效果相近的替代技能,比如创建会议失败但系统支持直接发送会议链接邀请,此时触发fallback_chain。不可恢复错误则直接终止当前技能链,把控制权交还给人机交互层,让用户决定下一步。
我踩过最深的坑是降级链死循环。早期fallback_chain里技能A失败后会尝试技能B,技能B失败后会尝试技能A,两个技能互相降级,导致执行引擎在内部打转。后来我在设计和实现里都引入了一个“降级深度上限”,默认最多降级两次,超过后直接返回失败。同时在注册时新增了依赖环检测,任何技能的fallback_chain中出现循环引用都注册失败。这个规则看起来简单,但能把最隐性的故障扼杀在上线前。
执行日志是另一个容易被忽视的重头戏。每次技能执行我都会记录:触发时间、输入参数摘要、执行结果、耗时、重试次数、最终错误码。这些日志不只是用来排查问题,更是质量监控模块的数据来源。调度系统的优化如果没有日志做支撑,基本就是凭感觉瞎改。我周期性地分析成功率曲线,识别哪些技能在什么时段下滑,然后反向定位是依赖的外部服务问题还是参数分布发生了变化。
3.4 一个完整的多步调度场景跑通
为了让大家更直观地理解,我放一个完整的案例。场景是用户在办公助手对话框里说:帮我约会议室,叫上产品组的几位同学,下午两点讨论一下上线前的验收清单。
SkillOrchestra的处理流程如下:
第一步,会话理解模块把这句话拆成任务目标:创建会议、邀请参会人、设置议题为验收清单。其中“产品组的几位同学”会被解析成一个参会人列表,“下午两点”解析成当天14点的时间戳。
第二步,路由决策检索技能目录,命中三个候选技能:schedule.meeting.create、schedule.room.find、schedule.attendee.invite。精排后确认先调用schedule.room.find,找出空闲且容纳人数足够的会议室,并把返回的room_id写入任务层。
第三步,执行引擎按序列调用schedule.meeting.create,入参是时间、会议室ID、参会人列表。如果会议室查询结果只有一间可用但容纳人数不够,create技能的preconditions检查会失败,系统随即触发fallback到schedule.meeting.suggest_alternative,直接给用户推荐替代时段。
第四步,全部执行成功后,任务层的状态被更新,系统向用户返回会议已创建的摘要,并把验收清单议题自动挂到会议邀请里。
如果你在用强化学习做法,这个流程你需要为“找会议室”“建会议”“邀人”分别定义奖励,还要担心环境里的会议室占用数据怎么同步模拟。而SkillOrchestra里每一步都是可预期、可控制、可解释的,这对生产系统来说价值非常大。
4. 实测效果:部署两个多月后我看到了什么
4.1 效果对比:从离线回放到线上观察
我在某公司内部的一个办公自动化场景里部署了SkillOrchestra的前身版本,运行了两个多月。这套系统需要调度的技能种类有28个,覆盖会议安排、日程提醒、工单流转、报表推送等。
效果有三个方面比较直观:
- 端到端任务完成率。用强化学习方案时,线上任务完成率大约在68%到73%之间波动,主要受用户表达多样性和工具接口变动影响。SkillOrchestra上线后,完成率稳定在89%左右。它不是把所有任务都做成了固定流程,而是在手册之外保留了一个“未知意图转人工”的出口,遇到手册覆盖不住的需求,系统会明确告诉用户“这个我还不会”,不会硬着头皮乱调用工具。
- 平均处理耗时。技能手册的检索加路由决策平均耗时在80到200毫秒之间,相比强化学习推理再加环境交互的耗时,体感上是秒开和转圈的区别。当然这个对比不完全公平,因为强化学习方案在线下推理时也要做序列采样,但工程角度上大家关心的就是落地体感。
- 调试效率。在老方案里,用户报一个“会议没建成功”的问题,你至少要倒查模型输入输出、工具调用记录、奖励值日志,链路又长又绕。SkillOrchestra里我直接在日志面板上看到是哪一条技能、哪一个前置条件没满足,问题定位时间从小时级压缩到分钟级。
4.2 成本账本:算算省在哪里
标题里说“又省”,这个省不只是省代码,更重要的是省算力和省维护人力。
算力方面,强化学习的线上推理通常需要采样多个候选动作序列,特别是用蒙特卡洛树搜索之类的策略时,一次决策可能要调很多次模型。SkillOrchestra的路由决策只用一次轻量级评分,大部分技能匹配甚至可以退化为查表,这部分在计算成本上的差距是数量级的。
维护人力方面,强化学习调奖励函数是个“牵一发动全身”的活,改一个权重可能影响所有策略行为。SkillOrchestra的技能手册是增量式的,新增一个技能只需要注册条目、写清楚条件和顺序,风险被限制在单一技能维度内。我这边实际维护成本大概是老方案的1/4。
我劝大家不要只看着省成本就无脑切换。技能手册的编写初期成本其实是高的。每个技能都要写清楚前置条件和执行效果,这要求实施者对这个业务领域有足够的分析深度。你如果连自己的业务流程都梳理不清楚,那写出来的手册一定是含混的,系统也就无从谈起好用。
4.3 好与省的平衡点
有一段时间我把“省”这个指标推到了极致,试图让路由决策完全依赖关键词匹配,不引入任何语义模型。结果是准确率掉了近15个百分点。我才意识到,过度的“省”本质上是把复杂度转移给了用户或下游环节。后来我在“极简匹配”和“重量级大模型推理”之间找了一个折中:本地部署一个小尺度语义编码模型,专门负责技能描述和任务描述的匹配。它不需要像通用大模型那样什么都会,只需要把垂直领域的技能描述编码得足够准确,性价比非常高。
这个经验也回答了一个更本质的问题:SkillOrchestra没有拒绝所有模型能力,它拒绝的是把调度策略也交给模型去自由发挥。模型负责理解人类语言,编排层负责控制执行路径,各管一段,这可能是当前工程约束下最稳的组合方式。
5. 常见问题与排障实录
5.1 技能命中不准:语义相似但动作不同的技能混淆
这是上线初期被反馈最多的一个问题,典型表现是用户说“建个会议”,系统却调了“建个任务清单”。原因是两个技能的描述文本在向量空间里太近,精排时又都满足基本条件。
我给出的解法是两个。第一是调整描述文本的写法,让描述从“它是什么”变成“什么场景下用它”。比如创建会议的描述改成“当用户需要安排多人特定时间讨论或同步信息时使用”,创建任务的描述改成“当用户需要记录待办事项或跟踪交付物时使用”。第二是在精排时加入否定约束,允许技能条目声明“本技能不处理哪些输入”,比如创建会议可以声明不处理包含“清单”字样的需求,这个词经常和任务清单强绑定。
这个办法比单纯调向量模型权重要直观得多,因为否定约束是人预先对齐业务边界的,不需要模型自己揣摩。
5.2 执行到一半状态过期:怎么防止链路里的“僵尸状态”
有一次用户先发起了“重新安排会议”的请求,系统正在执行过程中,用户又发了一句“算了不用改了”。由于会话层新增了这条消息,任务层的原任务目标却还停留在“重新安排会议”,执行引擎继续完成了动作,最后给用户创建了两次会议邀请。
这个问题的本质是任务层没有及时响应会话层的终止意图。我在任务层新增了一个cancellation_flag机制:当会话理解模块识别到用户表达了放弃、取消、暂缓等意图时,不是简单替换任务目标,而是先设置取消标记,执行引擎每执行一步前检查这个标记。一旦发现标记为真,立即终止当前链路,并把已执行的部分做回滚或者标记为无效。
我还加了一个“状态新鲜度”字段,任务层的每个关键参数都记录最近更新时间。如果该参数在链路执行过程中超过指定时长没有刷新,在执行下一步前会重新拉取真实状态。这个机制在处理会议室预订时特别有用,因为一间会议室可能在你执行其他技能期间就被人占用了。
5.3 手册版本管理:改了技能却影响旧任务怎么破
技能手册不是写一次就再也不动的,业务需求总是会变。但改版如果不小心,正在执行中的旧任务可能会突然调用到新逻辑,造成前后行为不一致。
我的方案是给任务绑定技能版本快照。任务一开始执行时,就把当前使用的技能手册版本号记录在任务层里。执行过程中所有技能查询都基于快照版本,不会混入最新版本的内容。只有当新任务启动时,才会使用最新的手册。
这个设计保证了任务的确定性,但也带来一个困扰:某个技能如果发现了严重问题,想紧急修复怎么办?快照机制会让存量任务继续踩坑。我的折中方案是支持“安全热补丁”机制,允许注册中心给某个技能打紧急补丁。补丁标记为必须强制生效,执行引擎在执行该技能前检查补丁版本号,即使任务绑定的是旧快照也会使用最新补丁。热补丁权限被我限定在故障修复场景,普通优化类变更走普通版本发布流程。
5.4 排查问题速查表
我把实际运营中比较常见的异常类型整理成一张速查表,方便团队其他同学照着排查:
| 异常现象 | 可能原因 | 排查步骤 | 解决建议 |
|---|---|---|---|
| 某技能长期不被选中 | 可靠性分数被历史失败拖低 | 查看质量监控中该技能近30天成功率 | 重置监控窗口或排查失败根因 |
| 技能链全部执行但结果不符合预期 | 任务层里的中间参数被错误覆盖 | 检查任务层字段更新时间与写入来源 | 加强任务层参数的写权限控制 |
| 系统对用户改口不敏感 | 会话层意图与任务层目标冲突 | 查看cancellation_flag是否被正确设置 | 优化意图分类模型对纠正表达的识别 |
| 新技能注册后无法被检索到 | 技能描述向量索引过期 | 检查索引更新任务是否执行成功 | 手动触发索引重建 |
| 同一技能反复触发降级 | 主技能成功率骤降 | 查看该技能的失败错误码分布 | 分析是依赖接口变动还是输入参数异常 |
配合这个表,我再提醒一点:所有的排查动作都要落到日志上。SkillOrchestra里我强制要求每个环节都打印带有request_id的链路日志,没有链路追踪的调度系统出问题时基本等于摸黑排雷。request_id贯通会话层、任务层、路由决策、执行引擎,你才能在看日志时像看一部连续剧一样重建整个执行过程。
6. 一些实际心得与后续扩展方向
6.1 我在这套方案里最深的三个体会
第一,把决策从模型手里拿回来,不是倒退,是一种工程上的“责任回归”。大模型擅长的是理解模糊表达,但它在精确稳定地执行多步操作方面并不可靠。SkillOrchestra让模型做自己擅长的事,把确定的流程交给确定性的模块,这种分工带来的稳定性提升比我想象中还要明显。
第二,技能手册的质量决定了系统的上限。你说这个系统聪明,其实是手册写得好;你说它笨,大概率是手册里的前置条件没写对。我在项目中期把大量时间投入到了技能条目的评审上,找业务方一个个对前置条件和执行效果,当时觉得是在做苦力活,事后看这恰恰是回报率最高的投入。
第三,没有完美的架构,只有不断修补的系统。SkillOrchestra刚诞生时我自信满满,觉得有了手册就万事大吉。结果两个月的线上运营让我陆续给它打上了好多个补丁,比如冷启动加成、cancellation_flag、否定约束、版本快照。系统的健壮性从来不是设计出来的,是运营过程中一层层补出来的。
6.2 这个方案还能往哪些方向延伸
SkillOrchestra目前的技能手册还依赖于人工编写,后续最值得做的方向有两个。一个是基于历史执行日志自动生成技能草案,比如把成功执行的技能调用序列自动归纳成初始版本的手册条目,再由人来审核确认,这样能大幅降低新场景接入的成本。另一个是面向多智能体协作的升级,让不同智能体维护各自独立的技能手册,同时通过一个协调协议相互查询和调用,形成有组织、可跨域的调度网络。
如果你有兴趣试这个方向,我建议不要一上来就搞大而全的框架,先从三个技能写起,把路由决策和执行引擎的骨架跑通,再逐步增量扩展。先把小场景吃透,你会发现这套“编写行为规范”的思路,比调一堆参数训一个黑盒模型要踏实得多。